diff --git a/dependencies/ggc/ggc.js b/dependencies/ggc/ggc.js
new file mode 100644
index 0000000..afbeb8d
--- /dev/null
+++ b/dependencies/ggc/ggc.js
@@ -0,0 +1,526 @@
+/**
+ * @file gate.js
+ *
+ * Permission is hereby granted to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to use it for personal
+ * or non-commercial purposes, with the following restrictions:
+ *
+ * 1. **No Copying or Redistribution**: The Software or any of its parts may not
+ * be copied, merged, distributed, sublicensed, or sold without explicit
+ * prior written permission from the author.
+ *
+ * 2. **Commercial Use**: Any use of the Software for commercial purposes requires
+ * a valid license, obtainable only with the explicit consent of the author.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM,
+ * OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ * Ownership of this code remains solely with the original author. Unauthorized
+ * use of this Software is strictly prohibited.
+ *
+ * @summary A class to interact and manipulate machines with a non-euclidian curve
+ * @description A class to interact and manipulate machines with a non-euclidian curve
+ * @module ggc
+ * @exports ggc
+ * @version 2.0.0
+ * @since 0.1.0
+ *
+ * Author:
+ * - Rene De Ren
+ * Email:
+ * - rene@thegoldenbasket.nl
+ *
+*/
+
+//load local dependencies
+const EventEmitter = require('events');
+const Logger = require('../../../generalFunctions/helper/logger');
+const { MeasurementContainer } = require('../../../generalFunctions/helper/measurements/index');
+const Interpolation = require('../../../predict/dependencies/predict/interpolation');
+//load all config modules
+const defaultConfig = require('./ggcConfig.json');
+const ConfigUtils = require('../../../generalFunctions/helper/configUtils');
+//load registration utility
+const ChildRegistrationUtils = require('../../../generalFunctions/helper/childRegistrationUtils');
+
+class Ggc {
+
+
+ /*------------------- Construct and set vars -------------------*/
+ constructor(ggcConfig = {}) {
+
+ //basic setup
+ this.emitter = new EventEmitter(); // Own EventEmitter
+ this.configUtils = new ConfigUtils(defaultConfig);
+ this.config = this.configUtils.initConfig(ggcConfig);
+
+ // Initialize measurements
+ this.measurements = new MeasurementContainer();
+ this.interpolation = new Interpolation();
+ this.child = {}; // object to hold child
+ this.actuators = []; // object to hold actuators
+ this.abortController = null; // new abort controller for aborting async tasks
+
+ // Init after config is set
+ this.logger = new Logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, this.config.general.name);
+ this.mode = this.config.mode.current;
+
+ this.move_delay = this.config.settings.moveDelay ; //define opening delay in seconds between 2 gates
+ this.state = "gateGroupClosed"; //define default starting state of the gates
+
+ //auto close
+ this.autoClose = true;
+ this.autoCloseTime = this.config.settings.autoClose;
+ this.autoCloseCnt = 0;
+
+ //protection sensor
+ this.safetySensor = false;
+ this.retryDelay = this.config.settings.retryDelay; // in seconds
+ this.closeAttempt = 0;
+ this.maxCloseAttempts = this.config.settings.maxRetries ;
+ this.safetySensorCnt = 0;
+
+
+ //ground loop trigger
+ this.ground_loop = false;
+ this.ground_loop_start = Date.now();
+ this.ground_loop_open = 10; //define time in seconds for when the ground loop should trigger a respons
+
+ //define if something has gone through the gate
+ this.goneThrough = false;
+
+ //define if the gate is closed
+ this.checkGateClosed = [false, false]; // gate 1 and gate 2
+
+ /* time controlled functions*/
+ //this.sleep = ms => new Promise(res => setTimeout(res, ms));
+
+ this.childRegistrationUtils = new ChildRegistrationUtils(this); // Child registration utility
+ }
+
+
+ isValidSourceForMode(source, mode) {
+ const allowedSourcesSet = this.config.mode.allowedSources[mode] || [];
+ return allowedSourcesSet.has(source);
+ }
+
+ isValidActionForMode(action, mode) {
+ const allowedActionsSet = this.config.mode.allowedActions[mode] || [];
+ return allowedActionsSet.has(action);
+ }
+
+ sleep(ms, signal) {
+ return new Promise((resolve, reject) => {
+ const timer = setTimeout(resolve, ms);
+ // only attach abort listener if a valid signal is provided
+ if (signal && typeof signal.addEventListener === 'function') {
+ signal.addEventListener('abort', () => {
+ clearTimeout(timer);
+ reject(new Error('aborted'));
+ });
+ }
+ });
+ }
+
+ // -------- Sequence Handlers -------- //
+ async executeSequence(name) {
+
+ const sequence = this.config.sequences[name];
+ const positions = this.actuators.map(a => a.state.getCurrentPosition());
+ const states = this.actuators.map(a => a.state.getCurrentState());
+
+ if (!sequence || sequence.size === 0) {
+ this.logger.warn(`Sequence '${name}' not defined.`);
+ return;
+ }
+
+ // Abort any prior sequence and start fresh
+ this.abortController?.abort();
+ this.abortController = new AbortController();
+ const { signal } = this.abortController;
+
+ if ( states.some(s => s !== "operational") && name !== "stop2gates" ) {
+ this.logger.warn(`Actuators not operational, aborting sequence '${name}'.`);
+ this.handleInput("parent", "execSequence", "stop2gates");
+ this.sleep(1000).then(() => {
+ this.handleInput("parent", "execSequence", name);
+ });
+ return;
+ }
+
+ try {
+ for (const action of sequence) {
+
+ this.transitionToSequence(action);
+
+ //If someone has already called abort(), skip the delay
+ if (signal.aborted) {
+ continue;
+ }
+
+ //otherwise, wait for the delay
+ await this.sleep(this.move_delay * 1000, signal);
+ }
+ } catch (err) {
+ if (err.message === 'aborted') {
+ this.logger.debug(`Sequence '${name}' aborted mid-delay.`);
+ } else {
+ this.logger.error(`Error in sequence '${name}': ${err.stack}`);
+ }
+ } finally {
+ // Clean up so we know no sequence is running
+ this.abortController = null;
+ }
+
+ }
+
+
+ async transitionToSequence(action) {
+ this.logger.debug(`Executing action: ${action}`);
+ const positions = this.actuators.map(a => a.state.getCurrentPosition());
+ const states = this.actuators.map(a => a.state.getCurrentState());
+
+ // Perform actions based on the state
+ switch (action) {
+ case "openGate1":
+ this.logger.debug("Opening gate 1");
+ this.actuators[0].handleInput("parent", "execMovement", 100);
+ this.checkGateClosed[0] = false;
+ break;
+ case "openGate2":
+ this.logger.debug("Opening gate 2");
+ this.actuators[1].handleInput("parent", "execMovement", 100);
+ break;
+ case "stopGate1":
+ this.logger.debug("Stopping gate 1");
+ // abort the delayed sleep, if any
+ this.abortController?.abort();
+ // immediately stop actuator 1
+ this.actuators[0].stop();
+ break;
+ case "stopGate2":
+ this.logger.debug("Stopping gate 2");
+ // abort the delayed sleep, if any
+ this.abortController?.abort();
+ // immediately stop actuator 2
+ this.actuators[1].stop();
+ break;
+ case "closeGate1":
+ this.actuators[0].handleInput("parent", "execMovement", 0);
+ break;
+ case "closeGate2":
+ this.actuators[1].handleInput("parent", "execMovement", 0);
+ break;
+ default:
+ this.logger.warn(`Unknown state: ${state}`);
+ }
+ }
+
+ async handleInput(source, action, parameter) {
+
+ if (!this.isValidSourceForMode(source, this.mode)) {
+ this.logger.warn(`Invalid source ${source} for mode ${this.mode}`);
+ return;
+ }
+
+ if (!this.isValidActionForMode(action, this.mode)) {
+ this.logger.warn(`Invalid action ${action} for mode ${this.mode}`);
+ return;
+ }
+
+ switch (action) {
+ case 'execSequence':
+ this.executeSequence(parameter);
+ break;
+ case 'setMode':
+ this.setMode(parameter);
+ break;
+ default:
+ this.logger.warn(`Unknown action ${action}`);
+ }
+
+ }
+
+ groundLoopAction(){
+ if(this.ground_loop){
+ //keep track of time
+ this.ground_loop_time = Date.now() - this.ground_loop_trigger;
+ }
+ else{
+ this.ground_loop_time = 0;
+ }
+
+ if(this.ground_loop_time >= ( this.ground_loop_open * 1000) ){
+ this.openGates();
+ }
+ }
+
+ updateMeasurement(variant, subType, value, position) {
+ this.logger.debug(`---------------------- updating ${subType} ------------------ `);
+ switch (subType) {
+ case "power":
+ // Update power measurement
+ this.updatePower(variant, value, position);
+ break;
+ default:
+ this.logger.error(`Type '${type}' not recognized for measured update.`);
+ return;
+ }
+ }
+
+ updatePower(variant,value,position) {
+ switch (variant) {
+ case ("measured"):
+ // put value in measurements
+ this.measurements.type("power").variant(variant).position("wire").value(value);
+ this.eventUpdate();
+ this.logger.debug(`Measured: ${value}`);
+ break;
+
+ default:
+ this.logger.warn(`Unrecognized variant '${variant}' for update.`);
+ break;
+ }
+ }
+
+ eventUpdate() {
+ // Gather raw data in arrays
+ const positions = this.actuators.map(a => a.state.getCurrentPosition());
+ const states = this.actuators.map(a => a.state.getCurrentState());
+ this.logger.debug(`States: ${JSON.stringify(states)}`);
+ this.logger.debug(`Positions: ${JSON.stringify(positions)}`);
+ const totPower = this.measurements.type("power").variant("measured").position("wire").getCurrentValue();
+
+ // Utility flags
+ const allOperational = states.every(s => s === "operational");
+ const allAtOpen = positions.every(p => p === 100);
+ const allAtClosed = positions.every(p => p === 0);
+ const allAccelerating = states.every(s => s === "accelerating");
+ const allDecelerating = states.every(s => s === "decelerating");
+ const allStopped = states.every(s => s === "operational") && positions.every( p => p !== 0 && p != 100);
+ const onlyGateOneAccelerating = states[0] === "accelerating" && states[1] === "operational";
+ const onlyGateTwoAccelerating = states[1] === "accelerating" && states[0] === "operational";
+ const onlyGateOneDecelerating = states[0] === "decelerating" && states[1] === "operational";
+ const onlyGateTwoDecelerating = states[1] === "decelerating" && states[0] === "operational";
+ const oneOpenOneClosed = allOperational && positions.some(p => p === 0) && positions.some(p => p === 100);
+
+ // Threshold for “spike” detection (tune as needed)
+ const SPIKE_THRESHOLD_1gate = 50;
+ const SPIKE_THRESHOLD_2gates = 100;
+ const lowerPositionThreshold = 10; // 10% of the total range
+ const upperPositionThreshold = 90; // 90% of the total range
+
+ // When something is blocking the gate we need to reopen the gates (True means nothing is blocking)
+ if (!this.safetySensor) {
+ // always add 1 to the safety sensor counter
+ this.safetySensorCnt++;
+
+ //add 1 to the autoclose counter to check weither we dont exceedd the max retries
+ if(this.autoClose) {
+ this.autoCloseCnt++;
+ }
+ //check if the safety sensor is triggered and the gates are closing
+ if( allDecelerating || onlyGateOneDecelerating || onlyGateTwoDecelerating) {
+ this.closeAttempt++;
+ this.handleInput("parent", "execSequence", "stop2gates");
+ this.logger.debug("something is blocking the gate, stopping actuators");
+ this.sleep(1000).then(() => {
+ this.handleInput("parent", "execSequence", "open2gates");
+ });
+ }
+ }
+
+ // Detect if any single gate is decelerating into its stop
+ if( onlyGateOneDecelerating ) {
+ //check for power spike so we know the gate is closed
+ if ( totPower > SPIKE_THRESHOLD_1gate ) {
+ this.logger.debug("Gate 1 is decelerating into the stop (power spike)");
+ //check flag for knowing if the gate is closed
+ this.checkGateClosed[0] = true;
+ this.closeAttempt = 0;
+ }
+ }
+
+ if( allDecelerating || allAccelerating) {
+ if( totPower > SPIKE_THRESHOLD_2gates && ( positions.some(p => p > lowerPositionThreshold) || positions.some(p => p < upperPositionThreshold) ) ) {
+ this.logger.debug("Unexpected power spike detected");
+ // stop the actuators
+ this.handleInput("parent", "execSequence", "stop2gates");
+ }
+ }
+
+ // Decide group state
+ if (allAtOpen && allOperational) {
+ this.state = "gateGroupOpened";
+ //trigger auto close if count is smaller than max
+ if( this.autoClose && this.autoCloseCnt < this.maxCloseAttempts && this.safetySensorCnt > 0) {
+ this.sleep(this.autoCloseTime * 1000).then(() => {
+ this.handleInput("parent", "execSequence", "close2gates");
+ //reset the safetySensor count because we are automatically closing the gates and if its bigger than 0 it means some1 passed through it
+ this.safetySensorCnt = 0;
+ });
+ }
+ this.logger.debug("Gates are open");
+ }
+ else if (allAtClosed && allOperational) {
+ this.state = "gateGroupClosed";
+ //after everything was closed and the auto close is enabled we need to reset the auto close count
+ if(this.autoClose) {
+ this.autoCloseCnt = 0;
+ };
+ this.logger.debug("Gates are closed");
+ }
+ else if (oneOpenOneClosed) {
+ this.state = "oneGateOpenOneGateClosed";
+ this.logger.debug("One gate open, one gate closed");
+ }
+ else if (allAccelerating) {
+ this.state = "gateGroupAccelerating";
+ this.logger.debug("Gates are accelerating");
+ }
+ else if (onlyGateOneAccelerating) {
+ this.state = "gateOneAccelerating";
+ this.logger.debug("Only gate 1 is accelerating");
+ }
+ else if (onlyGateTwoAccelerating) {
+ this.state = "gateTwoAccelerating";
+ this.logger.debug("Only gate 2 is accelerating");
+ }
+ else if (allDecelerating) {
+ this.state = "gateGroupDecelerating";
+ this.logger.debug("Gates are decelerating");
+ }
+ else if (onlyGateOneDecelerating) {
+ this.state = "gateOneDecelerating";
+ this.logger.debug("Only gate 1 is decelerating");
+ }
+ else if (onlyGateTwoDecelerating) {
+ this.state = "gateTwoDecelerating";
+ this.logger.debug("Only gate 2 is decelerating");
+ }
+ else if (allStopped) {
+ this.state = "gateGroupStopped";
+ this.logger.debug("Gates are stopped");
+ }
+ else {
+ this.state = "unknown";
+ this.logger.warn(`Unhandled combination: positions=${positions}, states=${states}`);
+ }
+
+ // if the gates are operational and close but we dont see the truely closed state then we need to nudge the gate to force the close
+
+ }
+
+ getOutput() {
+
+ // Improved output object generation
+ const output = {};
+
+ //build the output object
+ this.measurements.getTypes().forEach(type => {
+ this.measurements.getVariants(type).forEach(variant => {
+
+ const downstreamVal = this.measurements.type(type).variant(variant).position("downstream").getCurrentValue();
+ const upstreamVal = this.measurements.type(type).variant(variant).position("upstream").getCurrentValue();
+
+ if (downstreamVal != null) {
+ output[`downstream_${variant}_${type}`] = downstreamVal;
+ }
+ if (upstreamVal != null) {
+ output[`upstream_${variant}_${type}`] = upstreamVal;
+ }
+ if (downstreamVal != null && upstreamVal != null) {
+ const diffVal = this.measurements.type(type).variant(variant).difference().value;
+ output[`differential_${variant}_${type}`] = diffVal;
+ }
+ });
+ });
+
+ //fill in the rest of the output object
+ output["mode"] = this.mode;
+ output["totPower"] = this.power;
+ //this.logger.debug(`Output: ${JSON.stringify(output)}`);
+
+ return output;
+ }
+
+} // end of class
+
+module.exports = Ggc;
+
+/*
+const ggcConfig = {
+ general: {
+ name: "TestGGC",
+ logging: {
+ enabled: true,
+ logLevel: "debug"
+ }
+ },
+ settings: {
+ moveDelay: 3,
+ autoClose: 5,
+ retryDelay: 10,
+ maxRetries: 5
+ }
+};
+
+const ggc = new Ggc(ggcConfig);
+const linearActuator = require('../../../linearActuator/dependencies/linearActuator/linearActuator');
+const linActConfig =
+{
+ general: {
+ logging: {
+ enabled: true,
+ logLevel: "debug",
+ }
+ },
+ settings: {
+ moveDelay: 3,
+ autoClose: 5,
+ retryDelay: 10,
+ maxRetries: 5
+ }
+};
+const stateConfig = {
+ general: {
+ logging: {
+ enabled: true,
+ logLevel: "debug"
+ }
+ },
+ movement: {
+ speed: 0.1,
+ mode: "staticspeed"
+ },
+ time: {
+ starting: 0,
+ warmingup: 0,
+ stopping: 0,
+ coolingdown: 0
+ }
+};
+
+const gate1 = new linearActuator(linActConfig,stateConfig);
+const gate2 = new linearActuator(linActConfig,stateConfig);
+
+ggc.childRegistrationUtils.registerChild(gate1,"upstream");
+ggc.childRegistrationUtils.registerChild(gate2,"downstream");
+
+
+//open completely 2 gates inside an async IIFE
+(async () => {
+ await ggc.actuators[0].handleInput("parent","execSequence","startup");
+ await ggc.actuators[1].handleInput("parent","execSequence","startup");
+
+ ggc.handleInput("parent","execSequence","open2gates");
+ await ggc.sleep(5000);
+ ggc.handleInput("parent","execSequence","stop2gates");
+
+})();
+//*/
diff --git a/dependencies/ggc/ggcConfig.json b/dependencies/ggc/ggcConfig.json
new file mode 100644
index 0000000..430fd4e
--- /dev/null
+++ b/dependencies/ggc/ggcConfig.json
@@ -0,0 +1,297 @@
+{
+ "general": {
+ "name": {
+ "default": "gate group control Machine",
+ "rules": {
+ "type": "string",
+ "description": "A human-readable name or label for this machine configuration."
+ }
+ },
+ "id": {
+ "default": null,
+ "rules": {
+ "type": "string",
+ "nullable": true,
+ "description": "A unique identifier for this configuration. If not provided, defaults to null."
+ }
+ },
+ "unit": {
+ "default": "m3/h",
+ "rules": {
+ "type": "string",
+ "description": "The default measurement unit for this configuration (e.g., 'meters', 'seconds', 'unitless')."
+ }
+ },
+ "logging": {
+ "logLevel": {
+ "default": "info",
+ "rules": {
+ "type": "enum",
+ "values": [
+ {
+ "value": "debug",
+ "description": "Log messages are printed for debugging purposes."
+ },
+ {
+ "value": "info",
+ "description": "Informational messages are printed."
+ },
+ {
+ "value": "warn",
+ "description": "Warning messages are printed."
+ },
+ {
+ "value": "error",
+ "description": "Error messages are printed."
+ }
+ ]
+ }
+ },
+ "enabled": {
+ "default": true,
+ "rules": {
+ "type": "boolean",
+ "description": "Indicates whether logging is active. If true, log messages will be generated."
+ }
+ }
+ }
+ },
+ "functionality": {
+ "softwareType": {
+ "default": "gateGroupControl",
+ "rules": {
+ "type": "string",
+ "description": "Specified software type for this configuration."
+ }
+ },
+ "role": {
+ "default": "gate controller",
+ "rules": {
+ "type": "string",
+ "description": "Indicates the role this configuration plays within the system."
+ }
+ }
+ },
+ "asset": {
+ "uuid": {
+ "default": null,
+ "rules": {
+ "type": "string",
+ "nullable": true,
+ "description": "A universally unique identifier for this asset. May be null if not assigned."
+ }
+ },
+ "geoLocation": {
+ "default": {},
+ "rules": {
+ "type": "object",
+ "description": "An object representing the asset's physical coordinates or location.",
+ "schema": {
+ "x": {
+ "default": 0,
+ "rules": {
+ "type": "number",
+ "description": "X coordinate of the asset's location."
+ }
+ },
+ "y": {
+ "default": 0,
+ "rules": {
+ "type": "number",
+ "description": "Y coordinate of the asset's location."
+ }
+ },
+ "z": {
+ "default": 0,
+ "rules": {
+ "type": "number",
+ "description": "Z coordinate of the asset's location."
+ }
+ }
+ }
+ }
+ }
+ },
+ "mode": {
+ "current": {
+ "default": "auto",
+ "rules": {
+ "type": "enum",
+ "values": [
+ {
+ "value": "auto",
+ "description": "Machine accepts setpoints from a parent controller and runs autonomously."
+ },
+ {
+ "value": "virtualControl",
+ "description": "Controlled via GUI setpoints; ignores parent commands."
+ },
+ {
+ "value": "fysicalControl",
+ "description": "Controlled via physical buttons or switches; ignores external automated commands."
+ },
+ {
+ "value": "maintenance",
+ "description": "No active control from auto, virtual, or fysical sources."
+ }
+ ],
+ "description": "The operational mode of the machine."
+ }
+ },
+ "allowedActions":{
+ "default":{},
+ "rules": {
+ "type": "object",
+ "schema":{
+ "auto": {
+ "default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in auto mode."
+ }
+ },
+ "virtualControl": {
+ "default": ["statusCheck", "execMovement", "execSequence", "emergencyStop"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in virtualControl mode."
+ }
+ },
+ "fysicalControl": {
+ "default": ["statusCheck", "emergencyStop"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in fysicalControl mode."
+ }
+ },
+ "maintenance": {
+ "default": ["statusCheck"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Actions allowed in maintenance mode."
+ }
+ }
+ },
+ "description": "Information about valid command sources recognized by the machine."
+ }
+ },
+ "allowedSources":{
+ "default": {},
+ "rules": {
+ "type": "object",
+ "schema":{
+ "auto": {
+ "default": ["parent", "GUI", "fysical"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sources allowed in auto mode."
+ }
+ },
+ "virtualControl": {
+ "default": ["GUI", "fysical"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sources allowed in virtualControl mode."
+ }
+ },
+ "fysicalControl": {
+ "default": ["fysical"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sources allowed in fysicalControl mode."
+ }
+ }
+ },
+ "description": "Information about valid command sources recognized by the machine."
+ }
+ }
+ },
+ "sequences":{
+ "default":{},
+ "rules": {
+ "type": "object",
+ "schema": {
+ "open2gates": {
+ "default": ["openGate1","openGate2"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for starting up the machine."
+ }
+ },
+ "open1gate": {
+ "default": ["openGate1"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for shutting down the machine."
+ }
+ },
+ "stop2gates": {
+ "default": ["stopGate1","stopGate2"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for stopping the machine."
+ }
+ },
+ "close2gates": {
+ "default": ["closeGate2","closeGate1"],
+ "rules": {
+ "type": "set",
+ "itemType": "string",
+ "description": "Sequence of states for closing the gates."
+ }
+ }
+ }
+ },
+ "description": "Predefined sequences of states for the machine."
+
+ },
+ "settings": {
+ "moveDelay": {
+ "default": 3,
+ "rules": {
+ "type": "number",
+ "description": "delay between opening first and second linear actuator in seconds"
+ }
+ },
+ "autoClose": {
+ "default": 30,
+ "rules": {
+ "type": "number",
+ "description": "When auto close is enabled, the gate will close automatically after this time in seconds"
+ }
+ },
+ "retryDelay": {
+ "default": 5,
+ "rules": {
+ "type": "number",
+ "description": "Delay in seconds before retrying a failed command."
+ }
+ },
+ "maxRetries": {
+ "default": 5,
+ "rules": {
+ "type": "number",
+ "description": "Maximum number of retries for a failed command."
+ }
+ },
+ "groundLoopOpen": {
+ "default": 10,
+ "rules": {
+ "type": "number",
+ "description": "Time before ground loop triggers opening of the gate in seconds"
+ }
+ }
+
+ }
+ }
+
\ No newline at end of file
diff --git a/ggc.html b/ggc.html
new file mode 100644
index 0000000..3a7a864
--- /dev/null
+++ b/ggc.html
@@ -0,0 +1,284 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ggc.js b/ggc.js
new file mode 100644
index 0000000..c2ad8fd
--- /dev/null
+++ b/ggc.js
@@ -0,0 +1,223 @@
+module.exports = function (RED) {
+ function ggc(config) {
+ RED.nodes.createNode(this, config);
+ var node = this;
+
+ try {
+ // Load Machine class and curve data
+ const Ggc = require("./dependencies/ggc/ggc");
+ const OutputUtils = require("../generalFunctions/helper/outputUtils");
+
+ const ggcConfig = {
+ general: {
+ name: config.name || "Unknown",
+ id: node.id,
+ logging: {
+ enabled: config.eneableLog,
+ logLevel: config.logLevel
+ }
+ },
+ asset: {
+ supplier: config.supplier || "Unknown",
+ type: config.machineType || "generic",
+ subType: config.subType || "generic",
+ model: config.model || "generic",
+ }
+ };
+
+ const stateConfig = {
+ general: {
+ logging: {
+ enabled: config.eneableLog,
+ logLevel: config.logLevel
+ }
+ },
+ movement: {
+ speed: Number(config.speed)
+ },
+ time: {
+ starting: Number(config.startup),
+ warmingup: Number(config.warmup),
+ stopping: Number(config.shutdown),
+ coolingdown: Number(config.cooldown)
+ }
+ };
+
+ // Create machine instance
+ const ggc = new Ggc(ggcConfig, stateConfig);
+
+ // put m on node memory as source
+ node.source = ggc;
+
+ //load output utils
+ const output = new OutputUtils();
+
+ function updateNodeStatus() {
+ try {
+ const mode = "auto";//ggc.currentMode;
+ const state = ggc.state;
+ const totPower = Math.round(ggc.measurements.type("power").variant("measured").position('wire').getCurrentValue()) || 0;
+
+ const SYMBOL_MAP = {
+ gateGroupClosed: "G1🔴 & G2🔴",
+ gateGroupOpened: "G1🟢 & G2🟢",
+ gateGroupStopped: "G1🟥 & G2🟥",
+ gateGroupAccelerating: "G1🟡 & G2🟡",
+ gateGroupDecelerating: "G1🟠 & G2🟠",
+ gateOneAccelerating: "G1🟡 & G2🟢",
+ gateTwoAccelerating: "G1🟢 & G2🟡",
+ gateOneDecelerating: "G1🟠 & G2🟢",
+ gateTwoDecelerating: "G1🟢 & G2🟠",
+ oneGateOpenOneGateClosed: "G1🟢 & G2🔴",
+ gatePushingStop: "G1⚡ & G2⚡",
+ unknown: "❓ & ❓",
+ };
+
+ symbolState = SYMBOL_MAP[state] || "Unknown";
+
+ const position = "" ; //ggc.getGatePositions();
+ const roundedPosition = Math.round(position * 100) / 100;
+
+ let status;
+ switch (state) {
+ // —— gateGroup states first ——
+ case "gateGroupClosed":
+ status = {
+ fill: "red",
+ shape: "dot",
+ text: `${mode}: ${symbolState}`
+ };
+ break;
+ case "gateGroupOpened":
+ status = {
+ fill: "green",
+ shape: "dot",
+ text: `${mode}: ${symbolState}`
+ };
+ break;
+ case "gateGroupStopped":
+ status = {
+ fill: "red",
+ shape: "dot",
+ text: `${mode}: ${symbolState}`
+ };
+ break;
+ case "oneGateOpenOneGateClosed":
+ status = {
+ fill: "green",
+ shape: "dot",
+ text: `${mode}: ${symbolState}`
+ };
+ break;
+ default:
+ status = { fill: "grey", shape: "dot", text: `${mode}: ${symbolState}` };
+ }
+ return status;
+ } catch (error) {
+ node.error("Error in updateNodeStatus: " + error.message);
+ return { fill: "red", shape: "ring", text: "Status Error" };
+ }
+ }
+
+ function tick() {
+ try {
+ const status = updateNodeStatus();
+ node.status(status);
+
+ //get output
+ const classOutput = ggc.getOutput();
+ const dbOutput = output.formatMsg(classOutput, ggc.config, "influxdb");
+ const pOutput = output.formatMsg(classOutput, ggc.config, "process");
+
+ //console.log(pOutput);
+
+ //only send output on values that changed
+ let msgs = [];
+ msgs[0] = pOutput;
+ msgs[1] = dbOutput;
+
+ node.send(msgs);
+
+ } catch (error) {
+ node.error("Error in tick function: " + error);
+ node.status({ fill: "red", shape: "ring", text: "Tick Error" });
+ }
+ }
+
+ // register child on first output this timeout is needed because of node - red stuff
+ setTimeout(
+ () => {
+
+ /*---execute code on first start----*/
+ let msgs = [];
+
+ msgs[2] = { topic : "registerChild" , payload: node.id, positionVsParent: "upstream" };
+ msgs[3] = { topic : "registerChild" , payload: node.id, positionVsParent: "downstream" };
+
+ //send msg
+ this.send(msgs);
+ },
+ 100
+ );
+
+ //declare refresh interval internal node
+
+ setTimeout(
+ () => {
+ //---execute code on first start----
+ this.interval_id = setInterval(function(){ tick() },1000)
+ },
+ 1000
+ );
+
+ node.on("input", function(msg, send, done) {
+ try {
+
+ /* Update to complete event based node by putting the tick function after an input event */
+ switch(msg.topic) {
+ case 'registerChild':
+ const childId = msg.payload;
+ const childObj = RED.nodes.getNode(childId);
+ ggc.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent);
+ break;
+ case 'setMode':
+ ggc.setMode(msg.payload);
+ break;
+ case 'execSequence':
+ const { source, action, parameter } = msg.payload;
+ ggc.handleInput(source, action, parameter);
+ break;
+ case 'emergencystop':
+ const { source: esSource, action: esAction } = msg.payload;
+ ggc.handleInput(esSource, esAction);
+ break;
+ case 'safetySensor':
+ if(typeof msg.payload === "boolean") {
+ const safetySensor = msg.payload;
+ ggc.safetySensor = safetySensor;
+ }
+
+ break;
+ }
+
+ if (done) done();
+ } catch (error) {
+ node.error("Error processing input: " + error.message);
+ if (done) done(error);
+ }
+ });
+
+ node.on('close', function(done) {
+ if (node.interval_id) clearTimeout(node.interval_id);
+ if (node.tick_interval) clearInterval(node.tick_interval);
+ if (done) done();
+ });
+
+ } catch (error) {
+ node.error("Fatal error in node initialization: " + error.stack);
+ node.status({fill: "red", shape: "ring", text: "Fatal Error"});
+ }
+ }
+
+ RED.nodes.registerType("ggc", ggc);
+};
\ No newline at end of file