/** * @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"); })(); //*/