const EventEmitter = require('events'); const {loadCurve,logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions'); const { name } = require('../../generalFunctions/src/convert/lodash/lodash._shimkeys'); class Machine { /*------------------- Construct and set vars -------------------*/ constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) { //basic setup this.emitter = new EventEmitter(); // Own EventEmitter this.logger = new logger(machineConfig.general.logging.enabled,machineConfig.general.logging.logLevel, machineConfig.general.name); this.configManager = new configManager(); this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine ( use software type name ? ) this.configUtils = new configUtils(this.defaultConfig); // Load a specific curve this.model = machineConfig.asset.model; // Get the model from the machineConfig this.curve = this.model ? loadCurve(this.model) : null; //Init config and check if it is valid this.config = this.configUtils.initConfig(machineConfig); //add unique name for this node. this.config = this.configUtils.updateConfig(this.config, {general:{name: this.config.functionality?.softwareType + "_" + machineConfig.general.id}}); // add unique name if not present if (!this.model || !this.curve) { this.logger.error(`${!this.model ? 'Model not specified' : 'Curve not found for model ' + this.model} in machineConfig. Cannot make predictions.`); // Set prediction objects to null to prevent method calls this.predictFlow = null; this.predictPower = null; this.predictCtrl = null; this.hasCurve = false; } else{ this.hasCurve = true; this.config = this.configUtils.updateConfig(this.config, { asset: { ...this.config.asset, machineCurve: this.curve } }); machineConfig = { ...machineConfig, asset: { ...machineConfig.asset, machineCurve: this.curve } }; // Merge curve into machineConfig this.predictFlow = new predict({ curve: this.config.asset.machineCurve.nq }); // load nq (x : ctrl , y : flow relationship) this.predictPower = new predict({ curve: this.config.asset.machineCurve.np }); // load np (x : ctrl , y : power relationship) this.predictCtrl = new predict({ curve: this.reverseCurve(this.config.asset.machineCurve.nq) }); // load reversed nq (x: flow, y: ctrl relationship) } this.state = new state(stateConfig, this.logger); // Init State manager and pass logger this.errorMetrics = new nrmse(errorMetricsConfig, this.logger); // Initialize measurements this.measurements = new MeasurementContainer({ autoConvert: true, windowSize: 50, defaultUnits: { pressure: 'mbar', flow: this.config.general.unit, power: 'kW', temperature: 'C' } }); this.interpolation = new interpolation(); this.flowDrift = null; this.currentMode = this.config.mode.current; this.currentEfficiencyCurve = {}; this.cog = 0; this.NCog = 0; this.cogIndex = 0; this.minEfficiency = 0; this.absDistFromPeak = 0; this.relDistFromPeak = 0; // When position state changes, update position this.state.emitter.on("positionChange", (data) => { this.logger.debug(`Position change detected: ${data}`); this.updatePosition(); }); //When state changes look if we need to do other updates this.state.emitter.on("stateChange", (newState) => { this.logger.debug(`State change detected: ${newState}`); this._updateState(); }); this.child = {}; // object to hold child information so we know on what to subscribe this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility } _updateState(){ const isOperational = this._isOperationalState(); if(!isOperational){ //overrule the last prediction this should be 0 now this.measurements.type("flow").variant("predicted").position("downstream").value(0); } } /*------------------- Register child events -------------------*/ registerChild(child, softwareType) { this.logger.debug('Setting up child event for softwaretype ' + softwareType); if(softwareType === "measurement"){ const position = child.config.functionality.positionVsParent; const distance = child.config.functionality.distanceVsParent || 0; const measurementType = child.config.asset.type; const key = `${measurementType}_${position}`; //rebuild to measurementype.variant no position and then switch based on values not strings or names. const eventName = `${measurementType}.measured.${position}`; this.logger.debug(`Setting up listener for ${eventName} from child ${child.config.general.name}`); // Register event listener for measurement updates child.measurements.emitter.on(eventName, (eventData) => { this.logger.debug(`🔄 ${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`); console.log(` Emitting... ${eventName} with data:`); // Store directly in parent's measurement container this.measurements .type(measurementType) .variant("measured") .position(position) .value(eventData.value, eventData.timestamp, eventData.unit); // Call the appropriate handler this._callMeasurementHandler(measurementType, eventData.value, position, eventData); }); } } // Centralized handler dispatcher _callMeasurementHandler(measurementType, value, position, context) { switch (measurementType) { case 'pressure': this.updateMeasuredPressure(value, position, context); break; case 'flow': this.updateMeasuredFlow(value, position, context); break; case 'temperature': this.updateMeasuredTemperature(value, position, context); break; default: this.logger.warn(`No handler for measurement type: ${measurementType}`); // Generic handler - just update position this.updatePosition(); break; } } //---------------- END child stuff -------------// // Method to assess drift using errorMetrics assessDrift(measurement, processMin, processMax) { this.logger.debug(`Assessing drift for measurement: ${measurement} processMin: ${processMin} processMax: ${processMax}`); const predictedMeasurement = this.measurements.type(measurement).variant("predicted").position("downstream").getAllValues().values; const measuredMeasurement = this.measurements.type(measurement).variant("measured").position("downstream").getAllValues().values; if (!predictedMeasurement || !measuredMeasurement) return null; return this.errorMetrics.assessDrift( predictedMeasurement, measuredMeasurement, processMin, processMax ); } reverseCurve(curve) { const reversedCurve = {}; for (const [pressure, values] of Object.entries(curve)) { reversedCurve[pressure] = { x: [...values.y], // Previous y becomes new x y: [...values.x] // Previous x becomes new y }; } return reversedCurve; } // -------- Config -------- // updateConfig(newConfig) { this.config = this.configUtils.updateConfig(this.config, newConfig); } // -------- Mode and Input Management -------- // isValidSourceForMode(source, mode) { const allowedSourcesSet = this.config.mode.allowedSources[mode] || []; const allowed = allowedSourcesSet.has(source); allowed? this.logger.debug(`source is allowed proceeding with ${source} for mode ${mode}`) : this.logger.warn(`${source} is not allowed in mode ${mode}`); return allowed; } isValidActionForMode(action, mode) { const allowedActionsSet = this.config.mode.allowedActions[mode] || []; const allowed = allowedActionsSet.has(action); allowed ? this.logger.debug(`Action is allowed proceeding with ${action} for mode ${mode}`) : this.logger.warn(`${action} is not allowed in mode ${mode}`); return allowed; } async handleInput(source, action, parameter) { this.logger.debug("hello"); //sanitize input if( typeof action !== 'string'){this.logger.error(`Action must be string`); return;} //convert to lower case to avoid to many mistakes in commands action = action.toLowerCase(); // check for validity of the request if(!this.isValidActionForMode(action,this.currentMode)){return ;} if (!this.isValidSourceForMode(source, this.currentMode)) {return ;} this.logger.debug("hello2"); this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`); try { switch (action) { case "execsequence": return await this.executeSequence(parameter); case "execmovement": return await this.setpoint(parameter); case "entermaintenance": return await this.executeSequence(parameter); case "exitmaintenance": return await this.executeSequence(parameter); case "flowmovement": // Calculate the control value for a desired flow const pos = this.calcCtrl(parameter); // Move to the desired setpoint return await this.setpoint(pos); case "emergencystop": this.logger.warn(`Emergency stop activated by '${source}'.`); return await this.executeSequence("emergencyStop"); case "statuscheck": this.logger.info(`Status Check: Mode = '${this.currentMode}', Source = '${source}'.`); break; default: this.logger.warn(`Action '${action}' is not implemented.`); break; } this.logger.debug(`Action '${action}' successfully executed`); return {status : true , feedback: `Action '${action}' successfully executed.`}; } catch (error) { this.logger.error(`Error handling input: ${error}`); } } abortMovement(reason = "group override") { if (this.state?.abortCurrentMovement) { this.state.abortCurrentMovement(reason); } } setMode(newMode) { const availableModes = this.defaultConfig.mode.current.rules.values.map(v => v.value); if (!availableModes.includes(newMode)) { this.logger.warn(`Invalid mode '${newMode}'. Allowed modes are: ${availableModes.join(', ')}`); return; } this.currentMode = newMode; this.logger.info(`Mode successfully changed to '${newMode}'.`); } // -------- Sequence Handlers -------- // async executeSequence(sequenceName) { const sequence = this.config.sequences[sequenceName]; if (!sequence || sequence.size === 0) { this.logger.warn(`Sequence '${sequenceName}' not defined.`); return; } if (this.state.getCurrentState() == "operational" && sequenceName == "shutdown") { this.logger.info(`Machine will ramp down to position 0 before performing ${sequenceName} sequence`); await this.setpoint(0); } this.logger.info(` --------- Executing sequence: ${sequenceName} -------------`); for (const state of sequence) { try { await this.state.transitionToState(state); // Update measurements after state change } catch (error) { this.logger.error(`Error during sequence '${sequenceName}': ${error}`); break; // Exit sequence execution on error } } //recalc flow and power this.updatePosition(); } async setpoint(setpoint) { try { // Validate setpoint if (typeof setpoint !== 'number' || setpoint < 0) { throw new Error("Invalid setpoint: Setpoint must be a non-negative number."); } this.logger.info(`Setting setpoint to ${setpoint}. Current position: ${this.state.getCurrentPosition()}`); // Move to the desired setpoint await this.state.moveTo(setpoint); } catch (error) { console.error(`Error setting setpoint: ${error}`); } } // Calculate flow based on current pressure and position calcFlow(x) { if(this.hasCurve) { if (!this._isOperationalState()) { this.measurements.type("flow").variant("predicted").position("downstream").value(0); this.logger.debug(`Machine is not operational. Setting predicted flow to 0.`); return 0; } //this.predictFlow.currentX = x; Decrepated const cFlow = this.predictFlow.y(x); this.measurements.type("flow").variant("predicted").position("downstream").value(cFlow); //this.logger.debug(`Calculated flow: ${cFlow} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); return cFlow; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for flow calculation. Returning 0.`); this.measurements.type("flow").variant("predicted").position("downstream").value(0); return 0; } // Calculate power based on current pressure and position calcPower(x) { if(this.hasCurve) { if (!this._isOperationalState()) { this.measurements.type("power").variant("predicted").position('atEquipment').value(0); this.logger.debug(`Machine is not operational. Setting predicted power to 0.`); return 0; } //this.predictPower.currentX = x; Decrepated const cPower = this.predictPower.y(x); this.measurements.type("power").variant("predicted").position('atEquipment').value(cPower); //this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); return cPower; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for power calculation. Returning 0.`); this.measurements.type("power").variant("predicted").position('atEquipment').value(0); return 0; } // calculate the power consumption using only flow and pressure inputFlowCalcPower(flow) { if(this.hasCurve) { this.predictCtrl.currentX = flow; const cCtrl = this.predictCtrl.y(flow); this.predictPower.currentX = cCtrl; const cPower = this.predictPower.y(cCtrl); return cPower; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for power calculation. Returning 0.`); this.measurements.type("power").variant("predicted").position('atEquipment').value(0); return 0; } // Function to predict control value for a desired flow calcCtrl(x) { if(this.hasCurve) { this.predictCtrl.currentX = x; const cCtrl = this.predictCtrl.y(x); this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(cCtrl); //this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); return cCtrl; } // If no curve data is available, log a warning and return 0 this.logger.warn(`No curve data available for control calculation. Returning 0.`); this.measurements.type("ctrl").variant("predicted").position('atEquipment').value(0); return 0; } // returns the best available pressure measurement to use in the prediction calculation // this will be either the differential pressure, downstream or upstream pressure getMeasuredPressure() { const pressureDiff = this.measurements.type('pressure').variant('measured').difference(); // Both upstream & downstream => differential if (pressureDiff) { this.logger.debug(`Pressure differential: ${pressureDiff.value}`); this.predictFlow.fDimension = pressureDiff.value; this.predictPower.fDimension = pressureDiff.value; this.predictCtrl.fDimension = pressureDiff.value; //update the cog const { cog, minEfficiency } = this.calcCog(); // calc efficiency const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); return pressureDiff.value; } // get downstream const downstreamPressure = this.measurements.type('pressure').variant('measured').position('downstream').getCurrentValue(); // Only downstream => use it, warn that it's partial if (downstreamPressure != null) { this.logger.warn(`Using downstream pressure only for prediction: ${downstreamPressure} This is less acurate!!`); this.predictFlow.fDimension = downstreamPressure; this.predictPower.fDimension = downstreamPressure; this.predictCtrl.fDimension = downstreamPressure; //update the cog const { cog, minEfficiency } = this.calcCog(); // calc efficiency const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); return downstreamPressure; } this.logger.error(`No valid pressure measurements available to calculate prediction using last known pressure`); //set default at 0 => lowest pressure possible this.predictFlow.fDimension = 0; this.predictPower.fDimension = 0; this.predictCtrl.fDimension = 0; //update the cog const { cog, minEfficiency } = this.calcCog(); // calc efficiency const efficiency = this.calcEfficiency(this.predictPower.outputY, this.predictFlow.outputY, "predicted"); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); return 0; } handleMeasuredFlow() { const flowDiff = this.measurements.type('flow').variant('measured').difference(); // If both are present if (flowDiff != null) { // In theory, mass flow in = mass flow out, so they should match or be close. if (flowDiff.value < 0.001) { // flows match within tolerance this.logger.debug(`Flow match: ${flowDiff.value}`); return flowDiff.value; } else { // Mismatch => decide how to handle. Maybe take the average? // Or bail out with an error. Example: we bail out here. this.logger.error(`Something wrong with down or upstream flow measurement. Bailing out!`); return null; } } // get const upstreamFlow = this.measurements.type('flow').variant('measured').position('upstream').getCurrentValue(); // Only upstream => might still accept it, but warn if (upstreamFlow != null) { this.logger.warn(`Only upstream flow is present. Using it but results may be incomplete!`); return upstreamFlow; } // get const downstreamFlow = this.measurements.type('flow').variant('measured').position('downstream').getCurrentValue(); // Only downstream => might still accept it, but warn if (downstreamFlow != null) { this.logger.warn(`Only downstream flow is present. Using it but results may be incomplete!`); return downstreamFlow; } // Neither => error this.logger.error(`No upstream or downstream flow measurement. Bailing out!`); return null; } handleMeasuredPower() { const power = this.measurements.type("power").variant("measured").position("atEquipment").getCurrentValue(); // If your system calls it "upstream" or just a single "value", adjust accordingly if (power != null) { this.logger.debug(`Measured power: ${power}`); return power; } else { this.logger.error(`No measured power found. Bailing out!`); return null; } } // context handler for pressure updates updateMeasuredPressure(value, position, context = {}) { this.logger.debug(`Pressure update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`); // Store in parent's measurement container this.measurements.type("pressure").variant("measured").position(position).value(value, context.timestamp, context.unit); // Determine what kind of value to use as pressure (upstream , downstream or difference) const pressure = this.getMeasuredPressure(); this.updatePosition(); this.logger.debug(`Using pressure: ${pressure} for calculations`); } // NEW: Flow handler updateMeasuredFlow(value, position, context = {}) { if (!this._isOperationalState()) { this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`); return; } this.logger.debug(`Flow update: ${value} at ${position} from ${context.childName || 'child'}`); // Store in parent's measurement container this.measurements.type("flow").variant("measured").position(position).value(value, context.timestamp, context.unit); // Update predicted flow if you have prediction capability if (this.predictFlow) { this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY || 0); } } // Helper method for operational state check _isOperationalState() { const state = this.state.getCurrentState(); this.logger.debug(`Checking operational state ${this.state.getCurrentState()} ? ${["operational", "accelerating", "decelerating"].includes(state)}`); return ["operational", "accelerating", "decelerating"].includes(state); } //what is the internal functions that need updating when something changes that has influence on this. updatePosition() { if (this._isOperationalState()) { const currentPosition = this.state.getCurrentPosition(); // Update the predicted values based on the new position const { cPower, cFlow } = this.calcFlowPower(currentPosition); // Calc predicted efficiency const efficiency = this.calcEfficiency(cPower, cFlow, "predicted"); //update the cog const { cog, minEfficiency } = this.calcCog(); //update the distance from peak this.calcDistanceBEP(efficiency,cog,minEfficiency); } } calcDistanceFromPeak(currentEfficiency,peakEfficiency){ return Math.abs(currentEfficiency - peakEfficiency); } calcRelativeDistanceFromPeak(currentEfficiency,maxEfficiency,minEfficiency){ let distance = 1; if(currentEfficiency != null){ distance = this.interpolation.interpolate_lin_single_point(currentEfficiency,maxEfficiency, minEfficiency, 0, 1); } return distance; } showWorkingCurves() { // Show the current curves for debugging const { powerCurve, flowCurve } = this.getCurrentCurves(); return { powerCurve: powerCurve, flowCurve: flowCurve, cog: this.cog, cogIndex: this.cogIndex, NCog: this.NCog, minEfficiency: this.minEfficiency, currentEfficiencyCurve: this.currentEfficiencyCurve, absDistFromPeak: this.absDistFromPeak, relDistFromPeak: this.relDistFromPeak }; } // Calculate the center of gravity for current pressure calcCog() { //fetch current curve data for power and flow const { powerCurve, flowCurve } = this.getCurrentCurves(); const {efficiencyCurve, peak, peakIndex, minEfficiency } = this.calcEfficiencyCurve(powerCurve, flowCurve); // Calculate the normalized center of gravity const NCog = (flowCurve.y[peakIndex] - this.predictFlow.currentFxyYMin) / (this.predictFlow.currentFxyYMax - this.predictFlow.currentFxyYMin); //store in object for later retrieval this.currentEfficiencyCurve = efficiencyCurve; this.cog = peak; this.cogIndex = peakIndex; this.NCog = NCog; this.minEfficiency = minEfficiency; return { cog: peak, cogIndex: peakIndex, NCog: NCog, minEfficiency: minEfficiency }; } calcEfficiencyCurve(powerCurve, flowCurve) { const efficiencyCurve = []; let peak = 0; let peakIndex = 0; let minEfficiency = 0; // Calculate efficiency curve based on power and flow curves powerCurve.y.forEach((power, index) => { // Get flow for the current power const flow = flowCurve.y[index]; // higher efficiency is better efficiencyCurve.push( Math.round( ( flow / power ) * 100 ) / 100); // Keep track of peak efficiency peak = Math.max(peak, efficiencyCurve[index]); peakIndex = peak == efficiencyCurve[index] ? index : peakIndex; minEfficiency = Math.min(...efficiencyCurve); }); return { efficiencyCurve, peak, peakIndex, minEfficiency }; } //calc flow power based on pressure and current position calcFlowPower(x) { // Calculate flow and power const cFlow = this.calcFlow(x); const cPower = this.calcPower(x); return { cPower, cFlow }; } calcEfficiency(power, flow, variant) { if (power != 0 && flow != 0) { // Calculate efficiency after measurements update this.measurements.type("efficiency").variant(variant).position('atEquipment').value((flow / power)); } else { this.measurements.type("efficiency").variant(variant).position('atEquipment').value(null); } return this.measurements.type("efficiency").variant(variant).position('atEquipment').getCurrentValue(); } updateCurve(newCurve) { this.logger.info(`Updating machine curve`); const newConfig = { asset: { machineCurve: newCurve } }; //validate input of new curve fed to the machine this.config = this.configUtils.updateConfig(this.config, newConfig); //After we passed validation load the curves into their predictors this.predictFlow.updateCurve(this.config.asset.machineCurve.nq); this.predictPower.updateCurve(this.config.asset.machineCurve.np); this.predictCtrl.updateCurve(this.reverseCurve(this.config.asset.machineCurve.nq)); } getCompleteCurve() { const powerCurve = this.predictPower.inputCurveData; const flowCurve = this.predictFlow.inputCurveData; return { powerCurve, flowCurve }; } getCurrentCurves() { const powerCurve = this.predictPower.currentFxyCurve[this.predictPower.currentF]; const flowCurve = this.predictFlow.currentFxyCurve[this.predictFlow.currentF]; return { powerCurve, flowCurve }; } calcDistanceBEP(efficiency,maxEfficiency,minEfficiency) { const absDistFromPeak = this.calcDistanceFromPeak(efficiency,maxEfficiency); const relDistFromPeak = this.calcRelativeDistanceFromPeak(efficiency,maxEfficiency,minEfficiency); //store internally this.absDistFromPeak = absDistFromPeak ; this.relDistFromPeak = relDistFromPeak; return { absDistFromPeak: absDistFromPeak, relDistFromPeak: relDistFromPeak }; } getOutput() { // Improved output object generation const output = {}; Object.entries(this.measurements.measurements).forEach(([type, variants]) => { Object.entries(variants).forEach(([variant, positions]) => { Object.entries(positions).forEach(([position, measurement]) => { output[`${type}.${variant}.${position}`] = measurement.getCurrentValue(); }); }); }); //fill in the rest of the output object output["state"] = this.state.getCurrentState(); output["runtime"] = this.state.getRunTimeHours(); output["ctrl"] = this.state.getCurrentPosition(); output["moveTimeleft"] = this.state.getMoveTimeLeft(); output["mode"] = this.currentMode; output["cog"] = this.cog; // flow / power efficiency output["NCog"] = this.NCog; // normalized cog output["NCogPercent"] = Math.round(this.NCog * 100 * 100) / 100 ; output["maintenanceTime"] = this.state.getMaintenanceTimeHours(); if(this.flowDrift != null){ const flowDrift = this.flowDrift; output["flowNrmse"] = flowDrift.nrmse; output["flowLongterNRMSD"] = flowDrift.longTermNRMSD; output["flowImmediateLevel"] = flowDrift.immediateLevel; output["flowLongTermLevel"] = flowDrift.longTermLevel; } //should this all go in the container of measurements? output["effDistFromPeak"] = this.absDistFromPeak; output["effRelDistFromPeak"] = this.relDistFromPeak; //this.logger.debug(`Output: ${JSON.stringify(output)}`); return output; } } // end of class module.exports = Machine; /*------------------- Testing -------------------*/ /* curve = require('C:/Users/zn375/.node-red/public/fallbackData.json'); //import a child const Child = require('../../measurement/src/specificClass'); console.log(`Creating child...`); const PT1 = new Child(config={ general:{ name:"PT1", logging:{ enabled:true, logLevel:"debug", }, }, functionality:{ softwareType:"measurement", positionVsParent:"upstream", }, asset:{ supplier:"Vega", category:"sensor", type:"pressure", model:"Vegabar 82", unit: "mbar" }, }); const PT2 = new Child(config={ general:{ name:"PT2", logging:{ enabled:true, logLevel:"debug", }, }, functionality:{ softwareType:"measurement", positionVsParent:"upstream", }, asset:{ supplier:"Vega", category:"sensor", type:"pressure", model:"Vegabar 82", unit: "mbar" }, }); //create a machine console.log(`Creating machine...`); const machineConfig = { general: { name: "Hydrostal", logging: { enabled: true, logLevel: "debug", } }, asset: { supplier: "Hydrostal", type: "pump", category: "centrifugal", model: "H05K-S03R+HGM1X-X280KO", // Ensure this field is present. machineCurve: curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"], } } const stateConfig = { general: { logging: { enabled: true, logLevel: "debug", }, }, // Your custom config here (or leave empty for defaults) movement: { speed: 1, }, time: { starting: 2, warmingup: 3, stopping: 2, coolingdown: 3, }, }; const machine = new Machine(machineConfig, stateConfig); //machine.logger.info(JSON.stringify(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"])); machine.logger.info(`Registering child...`); machine.childRegistrationUtils.registerChild(PT1, "upstream"); machine.childRegistrationUtils.registerChild(PT2, "downstream"); //feed curve to the machine class //machine.updateCurve(curve["machineCurves"]["Hydrostal"]["H05K-S03R+HGM1X-X280KO"]); PT1.logger.info(`Enable sim...`); PT1.toggleSimulation(); PT2.logger.info(`Enable sim...`); PT2.toggleSimulation(); machine.getOutput(); //manual test //machine.handleInput("parent", "execSequence", "startup"); machine.measurements.type("pressure").variant("measured").position('upstream').value(-200); machine.measurements.type("pressure").variant("measured").position('downstream').value(1000); testingSequences(); const tickLoop = setInterval(changeInput,1000); function changeInput(){ PT1.logger.info(`tick...`); PT1.tick(); PT2.tick(); } async function testingSequences(){ try{ console.log(` ********** Testing sequence startup... **********`); await machine.handleInput("parent", "execSequence", "startup"); console.log(` ********** Testing movement to 15... **********`); await machine.handleInput("parent", "execMovement", 15); machine.getOutput(); console.log(` ********** Testing sequence shutdown... **********`); await machine.handleInput("parent", "execSequence", "shutdown"); console.log(`********** Testing moving to setpoint 10... while in idle **********`); await machine.handleInput("parent", "execMovement", 10); console.log(` ********** Testing sequence emergencyStop... **********`); await machine.handleInput("parent", "execSequence", "emergencystop"); console.log(`********** Testing sequence boot... **********`); await machine.handleInput("parent", "execSequence", "boot"); }catch(error){ console.error(`Error: ${error}`); } } //*/