From 33a62674c6753aff9140ec1a3427468624a7651d Mon Sep 17 00:00:00 2001 From: znetsixe <73483679+znetsixe@users.noreply.github.com> Date: Mon, 26 May 2025 17:43:20 +0200 Subject: [PATCH] update --- dependencies/ggc/ggc.js | 526 ++++++++++++++++++++++++++++++++ dependencies/ggc/ggcConfig.json | 297 ++++++++++++++++++ ggc.html | 284 +++++++++++++++++ ggc.js | 223 ++++++++++++++ 4 files changed, 1330 insertions(+) create mode 100644 dependencies/ggc/ggc.js create mode 100644 dependencies/ggc/ggcConfig.json create mode 100644 ggc.html create mode 100644 ggc.js 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