diff --git a/README.md b/README.md index f33b006..62583c2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1 @@ -# convert - -Makes unit conversions \ No newline at end of file +# rotating machine \ No newline at end of file diff --git a/dependencies/rotatingMachine/rotatingMachineConfig.json b/dependencies/rotatingMachine/rotatingMachineConfig.json deleted file mode 100644 index dfc366b..0000000 --- a/dependencies/rotatingMachine/rotatingMachineConfig.json +++ /dev/null @@ -1,381 +0,0 @@ -{ - "general": { - "name": { - "default": "Rotating 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": "machine", - "rules": { - "type": "string", - "description": "Specified software type for this configuration." - } - }, - "role": { - "default": "RotationalDeviceController", - "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." - } - } - } - } - }, - "supplier": { - "default": "Unknown", - "rules": { - "type": "string", - "description": "The supplier or manufacturer of the asset." - } - }, - "type": { - "default": "pump", - "rules": { - "type": "string", - "description": "A general classification of the asset tied to the specific software. This is not chosen from the asset dropdown menu." - } - }, - "subType": { - "default": "Centrifugal", - "rules": { - "type": "string", - "description": "A more specific classification within 'type'. For example, 'centrifugal' for a centrifugal pump." - } - }, - "model": { - "default": "Unknown", - "rules": { - "type": "string", - "description": "A user-defined or manufacturer-defined model identifier for the asset." - } - }, - "accuracy": { - "default": null, - "rules": { - "type": "number", - "nullable": true, - "description": "The accuracy of the machine or sensor, typically as a percentage or absolute value." - } - }, - "machineCurve": { - "default": { - "nq": { - "1": { - "x": [ - 1, - 2, - 3, - 4, - 5 - ], - "y": [ - 10, - 20, - 30, - 40, - 50 - ] - } - }, - "np": { - "1": { - "x": [ - 1, - 2, - 3, - 4, - 5 - ], - "y": [ - 10, - 20, - 30, - 40, - 50 - ] - } - } - }, - "rules": { - "type": "machineCurve", - "description": "All machine curves must have a 'nq' and 'np' curve. nq stands for the flow curve, np stands for the power curve. Together they form the efficiency curve." - } - } - }, - "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." - } - } - }, - "source": { - "default": "parent", - "rules": { - "type": "enum", - "values": [ - { - "value": "parent", - "description": "Commands are received from a parent controller." - }, - { - "value": "GUI", - "description": "Commands are received from a graphical user interface." - }, - { - "value": "fysical", - "description": "Commands are received from physical buttons or switches." - } - ], - "description": "Information about valid command sources recognized by the machine." - } - }, - "sequences":{ - "default":{}, - "rules": { - "type": "object", - "schema": { - "startup": { - "default": ["starting","warmingup","operational"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for starting up the machine." - } - }, - "shutdown": { - "default": ["stopping","coolingdown","idle"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for shutting down the machine." - } - }, - "emergencystop": { - "default": ["emergencystop","off"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for an emergency stop." - } - }, - "boot": { - "default": ["idle","starting","warmingup","operational"], - "rules": { - "type": "set", - "itemType": "string", - "description": "Sequence of states for booting up the machine." - } - } - } - }, - "description": "Predefined sequences of states for the machine." - - }, - "calculationMode": { - "default": "medium", - "rules": { - "type": "enum", - "values": [ - { - "value": "low", - "description": "Calculations run at fixed intervals (time-based)." - }, - { - "value": "medium", - "description": "Calculations run when new setpoints arrive or measured changes occur (event-driven)." - }, - { - "value": "high", - "description": "Calculations run on all event-driven info, including every movement." - } - ], - "description": "The frequency at which calculations are performed." - } - } - } - \ No newline at end of file diff --git a/package.json b/package.json index d38a86b..2d11dfc 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "author": "Rene De Ren", "license": "SEE LICENSE", "dependencies": { - "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git", - "convert": "git+https://gitea.centraal.wbd-rd.nl/RnD/convert.git" + "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git" }, "node-red": { "nodes": { diff --git a/rotatingMachine copy.js b/rotatingMachine copy.js new file mode 100644 index 0000000..1205bc4 --- /dev/null +++ b/rotatingMachine copy.js @@ -0,0 +1,247 @@ +module.exports = function (RED) { + function rotatingMachine(config) { + RED.nodes.createNode(this, config); + var node = this; + + try { + // Load Machine class and curve data + const Machine = require("./dependencies/machine/machine"); + const OutputUtils = require("../generalFunctions/helper/outputUtils"); + + const machineConfig = { + general: { + name: config.name || "Default Machine", + 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", + machineCurve: config.machineCurve + } + }; + + 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 m = new Machine(machineConfig, stateConfig); + + // put m on node memory as source + node.source = m; + + //load output utils + const output = new OutputUtils(); + + function updateNodeStatus() { + try { + const mode = m.currentMode; + const state = m.state.getCurrentState(); + const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue()); + const power = Math.round(m.measurements.type("power").variant("predicted").position('upstream').getCurrentValue()); + let symbolState; + switch(state){ + case "off": + symbolState = "⬛"; + break; + case "idle": + symbolState = "⏸️"; + break; + case "operational": + symbolState = "⏵️"; + break; + case "starting": + symbolState = "⏯️"; + break; + case "warmingup": + symbolState = "🔄"; + break; + case "accelerating": + symbolState = "⏩"; + break; + case "stopping": + symbolState = "⏹️"; + break; + case "coolingdown": + symbolState = "❄️"; + break; + case "decelerating": + symbolState = "⏪"; + break; + } + const position = m.state.getCurrentPosition(); + const roundedPosition = Math.round(position * 100) / 100; + + let status; + switch (state) { + case "off": + status = { fill: "red", shape: "dot", text: `${mode}: OFF` }; + break; + case "idle": + status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "operational": + status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; + break; + case "starting": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "warmingup": + status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; + break; + case "accelerating": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}%| 💨${flow}m³/h | ⚡${power}kW` }; + break; + case "stopping": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "coolingdown": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "decelerating": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; + 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 = m.getOutput(); + const dbOutput = output.formatMsg(classOutput, m.config, "influxdb"); + const pOutput = output.formatMsg(classOutput, m.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); + m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); + break; + case 'setMode': + m.setMode(msg.payload); + break; + case 'execSequence': + const { source, action, parameter } = msg.payload; + m.handleInput(source, action, parameter); + break; + case 'execMovement': + const { source: mvSource, action: mvAction, setpoint } = msg.payload; + m.handleInput(mvSource, mvAction, Number(setpoint)); + break; + case 'flowMovement': + const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload; + m.handleInput(fmSource, fmAction, Number(fmSetpoint)); + + break; + case 'emergencystop': + const { source: esSource, action: esAction } = msg.payload; + m.handleInput(esSource, esAction); + break; + case 'showCompleteCurve': + m.showCompleteCurve(); + send({ topic : "Showing curve" , payload: m.showCompleteCurve() }); + break; + case 'CoG': + m.showCoG(); + send({ topic : "Showing CoG" , payload: m.showCoG() }); + 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("rotatingMachine", rotatingMachine); +}; \ No newline at end of file diff --git a/rotatingMachine.html b/rotatingMachine.html index bd288ed..3978958 100644 --- a/rotatingMachine.html +++ b/rotatingMachine.html @@ -1,282 +1,132 @@ - + - // Grab the asset template from nodeTemplates - const tm = nodeTemplates.asset; - - RED.nodes.registerType("rotatingMachine", { - category: tm.category, - color: tm.color, - defaults: { - ...tm.defaults, - machineCurve: { value: {} }, // used to interpolate values - speed: { value: 1, required: true }, - startup: { value: 0, required: false }, - warmup: { value: 0, required: false }, - shutdown:{ value: 0, required: false }, - cooldown:{ value: 0, required: false }, - }, - inputs: tm.inputs, - outputs: tm.outputs, - inputLabels: tm.inputLabels, - outputLabels: tm.outputLabels, - icon: tm.icon, + - - - - - - - \ No newline at end of file + +
+ + + + + + + + + + \ No newline at end of file diff --git a/rotatingMachine.js b/rotatingMachine.js index 1205bc4..86647c0 100644 --- a/rotatingMachine.js +++ b/rotatingMachine.js @@ -1,247 +1,35 @@ -module.exports = function (RED) { - function rotatingMachine(config) { +const nameOfNode = 'rotatingMachine'; +const NodeClass = require('./src/nodeClass.js'); +const { MenuManager, configManager } = require('generalFunctions'); + +module.exports = function(RED) { + // 1) Register the node type and delegate to your class + RED.nodes.registerType(nameOfNode, function(config) { RED.nodes.createNode(this, config); - var node = this; + this.nodeClass = new NodeClass(config, RED, this, nameOfNode); + }); + // 2) Setup the dynamic menu & config endpoints + const menuMgr = new MenuManager(); + const cfgMgr = new configManager(); + + // Serve /rotatingMachine/menu.js + RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => { try { - // Load Machine class and curve data - const Machine = require("./dependencies/machine/machine"); - const OutputUtils = require("../generalFunctions/helper/outputUtils"); - - const machineConfig = { - general: { - name: config.name || "Default Machine", - 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", - machineCurve: config.machineCurve - } - }; - - 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 m = new Machine(machineConfig, stateConfig); - - // put m on node memory as source - node.source = m; - - //load output utils - const output = new OutputUtils(); - - function updateNodeStatus() { - try { - const mode = m.currentMode; - const state = m.state.getCurrentState(); - const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue()); - const power = Math.round(m.measurements.type("power").variant("predicted").position('upstream').getCurrentValue()); - let symbolState; - switch(state){ - case "off": - symbolState = "⬛"; - break; - case "idle": - symbolState = "⏸️"; - break; - case "operational": - symbolState = "⏵️"; - break; - case "starting": - symbolState = "⏯️"; - break; - case "warmingup": - symbolState = "🔄"; - break; - case "accelerating": - symbolState = "⏩"; - break; - case "stopping": - symbolState = "⏹️"; - break; - case "coolingdown": - symbolState = "❄️"; - break; - case "decelerating": - symbolState = "⏪"; - break; - } - const position = m.state.getCurrentPosition(); - const roundedPosition = Math.round(position * 100) / 100; - - let status; - switch (state) { - case "off": - status = { fill: "red", shape: "dot", text: `${mode}: OFF` }; - break; - case "idle": - status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "operational": - status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; - break; - case "starting": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "warmingup": - status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; - break; - case "accelerating": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}%| 💨${flow}m³/h | ⚡${power}kW` }; - break; - case "stopping": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "coolingdown": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; - break; - case "decelerating": - status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; - 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 = m.getOutput(); - const dbOutput = output.formatMsg(classOutput, m.config, "influxdb"); - const pOutput = output.formatMsg(classOutput, m.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); - m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); - break; - case 'setMode': - m.setMode(msg.payload); - break; - case 'execSequence': - const { source, action, parameter } = msg.payload; - m.handleInput(source, action, parameter); - break; - case 'execMovement': - const { source: mvSource, action: mvAction, setpoint } = msg.payload; - m.handleInput(mvSource, mvAction, Number(setpoint)); - break; - case 'flowMovement': - const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload; - m.handleInput(fmSource, fmAction, Number(fmSetpoint)); - - break; - case 'emergencystop': - const { source: esSource, action: esAction } = msg.payload; - m.handleInput(esSource, esAction); - break; - case 'showCompleteCurve': - m.showCompleteCurve(); - send({ topic : "Showing curve" , payload: m.showCompleteCurve() }); - break; - case 'CoG': - m.showCoG(); - send({ topic : "Showing CoG" , payload: m.showCoG() }); - 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"}); + const script = menuMgr.createEndpoint(nameOfNode, ['asset','logger','position']); + res.type('application/javascript').send(script); + } catch (err) { + res.status(500).send(`// Error generating menu: ${err.message}`); } - } + }); - RED.nodes.registerType("rotatingMachine", rotatingMachine); + // Serve /rotatingMachine/configData.js + RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => { + try { + const script = cfgMgr.createEndpoint(nameOfNode); + res.type('application/javascript').send(script); + } catch (err) { + res.status(500).send(`// Error generating configData: ${err.message}`); + } + }); }; \ No newline at end of file diff --git a/src/nodeClass.js b/src/nodeClass.js new file mode 100644 index 0000000..26c3bb9 --- /dev/null +++ b/src/nodeClass.js @@ -0,0 +1,293 @@ +/** + * measurement.class.js + * + * Encapsulates all node logic in a reusable class. In future updates we can split this into multiple generic classes and use the config to specifiy which ones to use. + * This allows us to keep the Node-RED node clean and focused on wiring up the UI and event handlers. + */ +const { outputUtils, configManager } = require('generalFunctions'); +const Specific = require("./specificClass"); + +/** + * Class representing a Node-RED node. + */ +class nodeClass { + /** + * Create a Node. + * @param {object} uiConfig - Node-RED node configuration. + * @param {object} RED - Node-RED runtime API. + */ + constructor(uiConfig, RED, nodeInstance, nameOfNode) { + + // Preserve RED reference for HTTP endpoints if needed + this.node = nodeInstance; // This is the Node-RED node instance, we can use this to send messages and update status + this.RED = RED; // This is the Node-RED runtime API, we can use this to create endpoints if needed + this.name = nameOfNode; // This is the name of the node, it should match the file name and the node type in Node-RED + this.source = null; // Will hold the specific class instance + + // Load default & UI config + this._loadConfig(uiConfig,this.node); + + // Instantiate core Measurement class + this._setupSpecificClass(); + + // Wire up event and lifecycle handlers + this._bindEvents(); + this._registerChild(); + this._startTickLoop(); + this._attachInputHandler(); + this._attachCloseHandler(); + } + + /** + * Load and merge default config with user-defined settings. + * @param {object} uiConfig - Raw config from Node-RED UI. + */ + _loadConfig(uiConfig,node) { + const cfgMgr = new configManager(); + this.defaultConfig = cfgMgr.getConfig(this.name); + + // Merge UI config over defaults + this.config = { + general: { + name: uiConfig.name, + id: node.id, // node.id is for the child registration process + unit: uiConfig.unit, // add converter options later to convert to default units (need like a model that defines this which units we are going to use and then conver to those standards) + logging: { + enabled: uiConfig.enableLog, + logLevel: uiConfig.logLevel + } + }, + asset: { + uuid: uiConfig.assetUuid, //need to add this later to the asset model + tagCode: uiConfig.assetTagCode, //need to add this later to the asset model + supplier: uiConfig.supplier, + category: uiConfig.category, //add later to define as the software type + type: uiConfig.assetType, + model: uiConfig.model, + unit: uiConfig.unit + }, + functionality: { + positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified + } + }; + + // Utility for formatting outputs + this._output = new outputUtils(); + } + + /** + * Instantiate the core Measurement logic and store as source. + */ + _setupSpecificClass() { + + // need extra state for this + 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) + } + }; + + this.source = new Specific(this.config, stateConfig); + } + + /** + * Bind Measurement events to Node-RED status updates. Using internal emitter. --> REMOVE LATER WE NEED ONLY COMPLETE CHILDS AND THEN CHECK FOR UPDATES + */ + _bindEvents() { + this.source.emitter.on('mAbs', (val) => { + this.node.status({ fill: 'green', shape: 'dot', text: `${val} ${this.config.general.unit}` }); + }); + } + + _updateNodeStatus() { + const m = this.source; + try { + const mode = m.currentMode; + const state = m.state.getCurrentState(); + const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue()); + const power = Math.round(m.measurements.type("power").variant("predicted").position('upstream').getCurrentValue()); + let symbolState; + switch(state){ + case "off": + symbolState = "⬛"; + break; + case "idle": + symbolState = "⏸️"; + break; + case "operational": + symbolState = "⏵️"; + break; + case "starting": + symbolState = "⏯️"; + break; + case "warmingup": + symbolState = "🔄"; + break; + case "accelerating": + symbolState = "⏩"; + break; + case "stopping": + symbolState = "⏹️"; + break; + case "coolingdown": + symbolState = "❄️"; + break; + case "decelerating": + symbolState = "⏪"; + break; + } + const position = m.state.getCurrentPosition(); + const roundedPosition = Math.round(position * 100) / 100; + + let status; + switch (state) { + case "off": + status = { fill: "red", shape: "dot", text: `${mode}: OFF` }; + break; + case "idle": + status = { fill: "blue", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "operational": + status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; + break; + case "starting": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "warmingup": + status = { fill: "green", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; + break; + case "accelerating": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} | ${roundedPosition}%| 💨${flow}m³/h | ⚡${power}kW` }; + break; + case "stopping": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "coolingdown": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState}` }; + break; + case "decelerating": + status = { fill: "yellow", shape: "dot", text: `${mode}: ${symbolState} - ${roundedPosition}% | 💨${flow}m³/h | ⚡${power}kW` }; + 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" }; + } + } + /** + * Register this node as a child upstream and downstream. + * Delayed to avoid Node-RED startup race conditions. + */ + _registerChild() { + setTimeout(() => { + this.node.send([ + null, + null, + { topic: 'registerChild', payload: this.config.general.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, + ]); + }, 100); + } + + /** + * Start the periodic tick loop to drive the Measurement class. + */ + _startTickLoop() { + setTimeout(() => { + this._tickInterval = setInterval(() => this._tick(), 1000); + + // Update node status on nodered screen every second ( this is not the best way to do this, but it works for now) + this._statusInterval = setInterval(() => { + const status = this._updateNodeStatus(); + this.node.status(status); + }, 1000); + + }, 1000); + } + + /** + * Execute a single tick: update measurement, format and send outputs. + */ + _tick() { + this.source.tick(); + + const raw = this.source.getOutput(); + const processMsg = this._output.formatMsg(raw, this.config, 'process'); + const influxMsg = this._output.formatMsg(raw, this.config, 'influxdb'); + + // Send only updated outputs on ports 0 & 1 + this.node.send([processMsg, influxMsg]); + } + + /** + * Attach the node's input handler, routing control messages to the class. + */ + _attachInputHandler() { + this.node.on('input', (msg, send, done) => { + /* Update to complete event based node by putting the tick function after an input event */ + const m = this.source; + switch(msg.topic) { + case 'registerChild': + const childId = msg.payload; + const childObj = this.RED.nodes.getNode(childId); + m.childRegistrationUtils.registerChild(childObj.source ,msg.positionVsParent); + break; + case 'setMode': + m.setMode(msg.payload); + break; + case 'execSequence': + const { source, action, parameter } = msg.payload; + m.handleInput(source, action, parameter); + break; + case 'execMovement': + const { source: mvSource, action: mvAction, setpoint } = msg.payload; + m.handleInput(mvSource, mvAction, Number(setpoint)); + break; + case 'flowMovement': + const { source: fmSource, action: fmAction, setpoint: fmSetpoint } = msg.payload; + m.handleInput(fmSource, fmAction, Number(fmSetpoint)); + + break; + case 'emergencystop': + const { source: esSource, action: esAction } = msg.payload; + m.handleInput(esSource, esAction); + break; + case 'showCompleteCurve': + m.showCompleteCurve(); + send({ topic : "Showing curve" , payload: m.showCompleteCurve() }); + break; + case 'CoG': + m.showCoG(); + send({ topic : "Showing CoG" , payload: m.showCoG() }); + break; + } + }); + } + + /** + * Clean up timers and intervals when Node-RED stops the node. + */ + _attachCloseHandler() { + this.node.on('close', (done) => { + clearInterval(this._tickInterval); + clearInterval(this._statusInterval); + done(); + }); + } +} + +module.exports = nodeClass; diff --git a/src/specificClass.js b/src/specificClass.js new file mode 100644 index 0000000..6d53763 --- /dev/null +++ b/src/specificClass.js @@ -0,0 +1,801 @@ +/** + * @file machine.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 machine + * @exports machine + * @version 0.1.0 + * @since 0.1.0 + * + * Author: + * - Rene De Ren + * Email: + * - r.de.ren@brabantsedelta.nl + * + * Add functionality later + // -------- Operational Metrics -------- // +maintenanceAlert: this.state.checkMaintenanceStatus() + + +*/ + +//load local dependencies +const EventEmitter = require('events'); +const {logger,configUtils,configManager,state, nrmse, MeasurementContainer, predict, interpolation , childRegistrationUtils} = require('generalFunctions'); + +class Machine { + + /*------------------- Construct and set vars -------------------*/ + constructor(machineConfig = {}, stateConfig = {}, errorMetricsConfig = {}) { + + //basic setup + this.emitter = new EventEmitter(); // Own EventEmitter + this.configManager = new configManager(); + this.defaultConfig = this.configManager.getConfig('rotatingMachine'); // Load default config for rotating machine + this.configUtils = new configUtils(this.defaultConfig); + this.config = this.configUtils.initConfig(machineConfig); + + // Init after config is set + this.logger = new logger(this.config.general.logging.enabled,this.config.general.logging.logLevel, this.config.general.name); + + 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(); + this.interpolation = new interpolation(); + this.child = {}; // object to hold child information so we know on what to subscribe + + this.flowDrift = null; + + 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.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; + + this.state.emitter.on("positionChange", (data) => { + this.logger.debug(`Position change detected: ${data}`); + this.updatePosition(); + }); + + //this.calcCog(); + + this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility + + } + + // 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] || []; + return allowedSourcesSet.has(source); + } + + isValidActionForMode(action, mode) { + const allowedActionsSet = this.config.mode.allowedActions[mode] || []; + return allowedActionsSet.has(action); + } + + async handleInput(source, action, parameter) { + if (!this.isValidSourceForMode(source, this.currentMode)) { + let warningTxt = `Source '${source}' is not valid for mode '${this.currentMode}'.`; + this.logger.warn(warningTxt); + return {status : false , feedback: warningTxt}; + } + + + this.logger.info(`Handling input from source '${source}' with action '${action}' in mode '${this.currentMode}'.`); + try { + switch (action) { + case "execSequence": + await this.executeSequence(parameter); + //recalc flow and power + this.updatePosition(); + break; + case "execMovement": + await this.setpoint(parameter); + break; + case "flowMovement": + // Calculate the control value for a desired flow + const pos = this.calcCtrl(parameter); + // Move to the desired setpoint + await this.setpoint(pos); + break; + case "emergencyStop": + this.logger.warn(`Emergency stop activated by '${source}'.`); + await this.executeSequence("emergencyStop"); + break; + 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}`); + } + } + + setMode(newMode) { + const availableModes = 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 + } + } + } + + async setpoint(setpoint) { + + try { + // Validate setpoint + if (typeof setpoint !== 'number' || setpoint < 0) { + throw new Error("Invalid setpoint: Setpoint must be a non-negative number."); + } + + // 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) { + const state = this.state.getCurrentState(); + + if (!["operational", "accelerating", "decelerating"].includes(state)) { + 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; + + } + + // Calculate power based on current pressure and position + calcPower(x) { + const state = this.state.getCurrentState(); + if (!["operational", "accelerating", "decelerating"].includes(state)) { + this.measurements.type("power").variant("predicted").position('upstream').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('upstream').value(cPower); + //this.logger.debug(`Calculated power: ${cPower} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); + return cPower; + } + + // calculate the power consumption using only flow and pressure + inputFlowCalcPower(flow) { + this.predictCtrl.currentX = flow; + const cCtrl = this.predictCtrl.y(flow); + this.predictPower.currentX = cCtrl; + const cPower = this.predictPower.y(cCtrl); + return cPower; + } + + // Function to predict control value for a desired flow + calcCtrl(x) { + + this.predictCtrl.currentX = x; + const cCtrl = this.predictCtrl.y(x); + this.measurements.type("ctrl").variant("predicted").position('upstream').value(cCtrl); + //this.logger.debug(`Calculated ctrl: ${cCtrl} for pressure: ${this.getMeasuredPressure()} and position: ${x}`); + return cCtrl; + + } + + // this function returns the pressure for calculations + getMeasuredPressure() { + const pressureDiff = this.measurements.type('pressure').variant('measured').difference(); + + // Both upstream & downstream => differential + if (pressureDiff != null) { + 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.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('pressure').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('pressure').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("upstream").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; + } + } + + updatePressure(variant,value,position) { + + switch (variant) { + case ("measured"): + //only update when machine is in a state where it can be used + if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") { + // put value in measurements + this.measurements.type("pressure").variant("measured").position(position).value(value); + //when measured pressure gets updated we need some logic to fetch the relevant value which could be downstream or differential pressure + const pressure = this.getMeasuredPressure(); + //update the flow power and cog + this.updatePosition(); + this.logger.debug(`Measured pressure: ${pressure}`); + } + break; + + default: + this.logger.warn(`Unrecognized variant '${variant}' for pressure update.`); + break; + } + } + + updateFlow(variant,value,position) { + + switch (variant) { + case ("measured"): + // put value in measurements + this.measurements.type("flow").variant("measured").position(position).value(value); + //when measured flow gets updated we need to push the last known value in the prediction measurements to keep them synced + this.measurements.type("flow").variant("predicted").position("downstream").value(this.predictFlow.outputY); + break; + + case ("predicted"): + this.logger.debug('not doing anythin yet'); + break; + + default: + this.logger.warn(`Unrecognized variant '${variant}' for flow update.`); + break; + } + } + + updateMeasurement(variant, subType, value, position) { + this.logger.debug(`---------------------- updating ${subType} ------------------ `); + switch (subType) { + case "pressure": + // Update pressure measurement + this.updatePressure(variant,value,position); + break; + case "flow": + this.updateFlow(variant,value,position); + // Update flow measurement + this.flowDrift = this.assessDrift("flow", this.predictFlow.currentFxyYMin , this.predictFlow.currentFxyYMax); + this.logger.debug(`---------------------------------------- `); + break; + case "power": + // Update power measurement + break; + default: + this.logger.error(`Type '${type}' not recognized for measured update.`); + return; + } + } + + //what is the internal functions that need updating when something changes that has influence on this. + updatePosition() { + if (this.state.getCurrentState() == "operational" || this.state.getCurrentState() == "accelerating" || this.state.getCurrentState() == "decelerating") { + + 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; + } + + // 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('downstream').value((flow / power)); + } else { + this.measurements.type("efficiency").variant(variant).position('downstream').value(null); + } + + return this.measurements.type("efficiency").variant(variant).position('downstream').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 = {}; + //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["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 ; + + 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/dependencies/measurement/measurement'); + +console.log(`Creating child...`); +const PT1 = new Child(config={ + general:{ + name:"PT1", + logging:{ + enabled:true, + logLevel:"debug", + }, + }, + functionality:{ + softwareType:"measurement", + }, + asset:{ + type:"sensor", + subType:"pressure", + }, +}); + +const PT2 = new Child(config={ + general:{ + name:"PT2", + logging:{ + enabled:true, + logLevel:"debug", + }, + }, + functionality:{ + softwareType:"measurement", + }, + asset:{ + type:"sensor", + subType:"pressure", + }, +}); + +//create a machine +console.log(`Creating machine...`); + +const machineConfig = { + general: { + name: "Hydrostal", + logging: { + enabled: true, + logLevel: "debug", + } + }, + asset: { + supplier: "Hydrostal", + type: "pump", + subType: "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}`); + } +} + + +//*/ + + +