forked from RnD/rotatingMachine
first try to standardize
This commit is contained in:
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
247
rotatingMachine copy.js
Normal file
247
rotatingMachine copy.js
Normal file
@@ -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);
|
||||
};
|
||||
@@ -1,282 +1,132 @@
|
||||
<script type="module">
|
||||
|
||||
import * as menuUtils from "/generalFunctions/helper/menuUtils.js";
|
||||
import nodeTemplates from "/generalFunctions/helper/nodeTemplates.js";
|
||||
|
||||
// Grab the asset template from nodeTemplates
|
||||
const tm = nodeTemplates.asset;
|
||||
<!-- Load the dynamic menu & config endpoints -->
|
||||
<script src="/rotatingMachine/menu.js"></script>
|
||||
<script src="/rotatingMachine/configData.js"></script>
|
||||
|
||||
<script>
|
||||
RED.nodes.registerType("rotatingMachine", {
|
||||
category: tm.category,
|
||||
color: tm.color,
|
||||
category: "EVOLV",
|
||||
color: "#4f8582",
|
||||
defaults: {
|
||||
...tm.defaults,
|
||||
machineCurve: { value: {} }, // used to interpolate values
|
||||
// Define default properties
|
||||
name: { value: "", required: true }, // use asset category as name ?
|
||||
|
||||
// Define specific properties
|
||||
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,
|
||||
startup: { value: 0 },
|
||||
warmup: { value: 0 },
|
||||
shutdown: { value: 0 },
|
||||
machineCurve : { value: {}},
|
||||
|
||||
label: function () {
|
||||
return this.name || "asset";
|
||||
//define asset properties
|
||||
uuid: { value: "" },
|
||||
supplier: { value: "" },
|
||||
category: { value: "" },
|
||||
assetType: { value: "" },
|
||||
model: { value: "" },
|
||||
unit: { value: "" },
|
||||
|
||||
//logger properties
|
||||
enableLog: { value: false },
|
||||
logLevel: { value: "error" },
|
||||
|
||||
//physicalAspect
|
||||
positionVsParent: { value: "" },
|
||||
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 3,
|
||||
inputLabels: ["Input"],
|
||||
outputLabels: ["process", "dbase", "parent"],
|
||||
icon: "font-awesome/fa-tachometer",
|
||||
|
||||
label: function() {
|
||||
return this.name || "rotatingMachine";
|
||||
},
|
||||
|
||||
oneditprepare: function () {
|
||||
const node = this;
|
||||
console.log("------------ Edit Prepare for Rotating Machine Node ------------");
|
||||
|
||||
// specific fields of node
|
||||
const elements = {
|
||||
speed: document.getElementById("node-input-speed"),
|
||||
startup: document.getElementById("node-input-startup"),
|
||||
warmup: document.getElementById("node-input-warmup"),
|
||||
shutdown: document.getElementById("node-input-shutdown"),
|
||||
cooldown: document.getElementById("node-input-cooldown"),
|
||||
oneditprepare: function() {
|
||||
// wait for the menu scripts to load
|
||||
const waitForMenuData = () => {
|
||||
if (window.EVOLV?.nodes?.rotatingMachine?.initEditor) {
|
||||
window.EVOLV.nodes.rotatingMachine.initEditor(this);
|
||||
} else {
|
||||
setTimeout(waitForMenuData, 50);
|
||||
}
|
||||
};
|
||||
waitForMenuData();
|
||||
|
||||
// Loop through tm.elements to add non-specific elements
|
||||
Object.keys(tm.elements).forEach(key => {
|
||||
elements[key] = document.getElementById(tm.elements[key]);
|
||||
});
|
||||
// your existing project‐settings & asset dropdown logic can remain here
|
||||
document.getElementById("node-input-speed"),
|
||||
document.getElementById("node-input-startup"),
|
||||
document.getElementById("node-input-warmup"),
|
||||
document.getElementById("node-input-shutdown"),
|
||||
document.getElementById("node-input-cooldown")
|
||||
|
||||
console.log("Elements:", elements);
|
||||
|
||||
const projecSettingstURL = tm.projectSettingsURL;
|
||||
console.log("Project settings URL:", projecSettingstURL);
|
||||
|
||||
try{
|
||||
|
||||
// Fetch project settings
|
||||
menuUtils.fetchProjectData(projecSettingstURL)
|
||||
.then((projectSettings) => {
|
||||
|
||||
//assign to node vars
|
||||
node.configUrls = projectSettings.configUrls;
|
||||
|
||||
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("rotatingMachine",node.configUrls.cloud.taggcodeAPI);
|
||||
node.configUrls.cloud.config = cloudConfigURL; // first call
|
||||
node.configUrls.local.config = localConfigURL; // backup call
|
||||
|
||||
node.locationId = projectSettings.locationId;
|
||||
node.uuid = projectSettings.uuid;
|
||||
|
||||
// Gets the ID of the active workspace (Flow)
|
||||
const activeFlowId = RED.workspaces.active(); //fetches active flow id
|
||||
node.processId = activeFlowId;
|
||||
|
||||
// UI elements
|
||||
menuUtils.initBasicToggles(elements);
|
||||
menuUtils.fetchAndPopulateDropdowns(node.configUrls, elements, node); // function for all assets
|
||||
|
||||
})
|
||||
|
||||
}catch(e){
|
||||
console.log("Error fetching project settings", e);
|
||||
}
|
||||
|
||||
if(node.d){
|
||||
//this means node is disabled
|
||||
console.log("Current status of node is disabled");
|
||||
}
|
||||
},
|
||||
|
||||
oneditsave: function () {
|
||||
oneditsave: function() {
|
||||
const node = this;
|
||||
|
||||
console.log(`------------ Saving changes to node ------------`);
|
||||
console.log(`${node.uuid}`);
|
||||
|
||||
//save basic properties
|
||||
["name", "unit", "supplier", "subType", "model"].forEach(
|
||||
(field) => {
|
||||
const element = document.getElementById(`node-input-${field}`);
|
||||
if (element) {
|
||||
node[field] = element.value || "";
|
||||
// save asset fields
|
||||
if (window.EVOLV?.nodes?.rotatingMachine?.assetMenu?.saveEditor) {
|
||||
window.EVOLV.nodes.rotatingMachine.assetMenu.saveEditor(this);
|
||||
}
|
||||
// save logger fields
|
||||
if (window.EVOLV?.nodes?.rotatingMachine?.loggerMenu?.saveEditor) {
|
||||
window.EVOLV.nodes.rotatingMachine.loggerMenu.saveEditor(this);
|
||||
}
|
||||
);
|
||||
|
||||
// Save numeric and boolean properties
|
||||
// save position field
|
||||
if (window.EVOLV?.nodes?.rotatingMachine?.positionMenu?.saveEditor) {
|
||||
window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this);
|
||||
}
|
||||
// …plus any custom saves for speed, startup, etc.
|
||||
["speed", "startup", "warmup", "shutdown", "cooldown"].forEach(
|
||||
(field) => {
|
||||
const element = document.getElementById(`node-input-${field}`);
|
||||
if (element) {
|
||||
node[field] = Number(element.value) || 0;
|
||||
}
|
||||
}
|
||||
(field) => (node[field] = parseFloat(document.getElementById(`node-input-${field}`).value) || 0)
|
||||
);
|
||||
|
||||
/*
|
||||
//local db
|
||||
node[field] = node["modelMetadata"][field];
|
||||
//central db
|
||||
node[field] = node["modelMetadata"]["product_model_meta"][field];
|
||||
*/
|
||||
|
||||
//save meta data curve central db
|
||||
["machineCurve"].forEach(
|
||||
(field) => {
|
||||
node[field] = node.modelMetadata.product_model_meta
|
||||
? node.modelMetadata.product_model_meta[field]
|
||||
: node.modelMetadata[field];
|
||||
//console.log(node[field]);
|
||||
console.log("Machine curve saved");
|
||||
}
|
||||
);
|
||||
|
||||
const logLevelElement = document.getElementById("node-input-logLevel");
|
||||
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
|
||||
|
||||
if (!node.unit) {
|
||||
RED.notify("Unit selection is required.", "error");
|
||||
}
|
||||
|
||||
if (node.subType && !node.unit) {
|
||||
RED.notify("Unit must be set when specifying a subtype.", "error");
|
||||
}
|
||||
|
||||
try{
|
||||
console.log("Saving assetID and tagnumber");
|
||||
console.log(node.assetTagCode);
|
||||
// Fetch project settings
|
||||
menuUtils.apiCall(node,node.configUrls)
|
||||
.then((response) => {
|
||||
|
||||
console.log(" ====<<>>>> API call response", response);
|
||||
|
||||
//save response to node information
|
||||
node.assetId = response.asset_id;
|
||||
node.assetTagCode = response.asset_tag_number;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error during API call", error);
|
||||
});
|
||||
}catch(e){
|
||||
console.log("Error saving assetID and tagnumber", e);
|
||||
}
|
||||
},
|
||||
</script>
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<!-- Main UI Template -->
|
||||
<script type="text/html" data-template-name="rotatingMachine">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="node-input-name"
|
||||
placeholder="Machine Name"
|
||||
style="width:70%;"
|
||||
/>
|
||||
</div>
|
||||
<!-- Main UI Template -->
|
||||
<script type="text/html" data-template-name="rotatingMachine">
|
||||
|
||||
<!-- Machine-specific controls -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-speed"><i class="fa fa-clock-o"></i> Reaction Speed</label>
|
||||
<input type="number" id="node-input-speed" placeholder="1" />
|
||||
<input type="number" id="node-input-speed" style="width:60%;" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-startup"><i class="fa fa-clock-o"></i> Startup Time</label>
|
||||
<input type="number" id="node-input-startup" placeholder="0" />
|
||||
<input type="number" id="node-input-startup" style="width:60%;" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-warmup"><i class="fa fa-clock-o"></i> Warmup Time</label>
|
||||
<input type="number" id="node-input-warmup" placeholder="0" />
|
||||
<input type="number" id="node-input-warmup" style="width:60%;" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-shutdown"><i class="fa fa-clock-o"></i> Shutdown Time</label>
|
||||
<input type="number" id="node-input-shutdown" placeholder="0" />
|
||||
<input type="number" id="node-input-shutdown" style="width:60%;" />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="node-input-cooldown"><i class="fa fa-clock-o"></i> Cooldown Time</label>
|
||||
<input type="number" id="node-input-cooldown" placeholder="0" />
|
||||
<input type="number" id="node-input-cooldown" style="width:60%;" />
|
||||
</div>
|
||||
|
||||
<!-- Optional Extended Fields: supplier, type, subType, model -->
|
||||
<hr />
|
||||
<div class="form-row">
|
||||
<label for="node-input-supplier"
|
||||
><i class="fa fa-industry"></i> Supplier</label
|
||||
>
|
||||
<select id="node-input-supplier" style="width:60%;">
|
||||
<option value="">(optional)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-subType"
|
||||
><i class="fa fa-puzzle-piece"></i> SubType</label
|
||||
>
|
||||
<select id="node-input-subType" style="width:60%;">
|
||||
<option value="">(optional)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-model"><i class="fa fa-wrench"></i> Model</label>
|
||||
<select id="node-input-model" style="width:60%;">
|
||||
<option value="">(optional)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-unit"><i class="fa fa-balance-scale"></i> Unit</label>
|
||||
<select id="node-input-unit" style="width:60%;"></select>
|
||||
</div>
|
||||
<!-- Asset fields injected here -->
|
||||
<div id="asset-fields-placeholder"></div>
|
||||
|
||||
<hr />
|
||||
<!-- Logger fields injected here -->
|
||||
<div id="logger-fields-placeholder"></div>
|
||||
|
||||
<!-- loglevel checkbox -->
|
||||
<div class="form-row">
|
||||
<label for="node-input-enableLog"
|
||||
><i class="fa fa-cog"></i> Enable Log</label
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="node-input-enableLog"
|
||||
style="width:20px; vertical-align:baseline;"
|
||||
/>
|
||||
<span>Enable logging</span>
|
||||
</div>
|
||||
<!-- Position fields injected here -->
|
||||
<div id="position-fields-placeholder"></div>
|
||||
|
||||
<div class="form-row" id="row-logLevel">
|
||||
<label for="node-input-logLevel"><i class="fa fa-cog"></i> Log Level</label>
|
||||
<select id="node-input-logLevel" style="width:60%;">
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
<option value="warn">Warn</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="rotatingMachine">
|
||||
<p>
|
||||
<b>Rotating Machine Node</b>: Configure the behavior of a rotating machine
|
||||
used in a digital twin.
|
||||
</p>
|
||||
<script type="text/html" data-help-name="rotatingMachine">
|
||||
<p><b>Rotating Machine Node</b>: Configure a rotating‐machine asset.</p>
|
||||
<ul>
|
||||
<li><b>Supplier:</b> Select a supplier to populate machine options.</li>
|
||||
<li><b>SubType:</b> Select a subtype if applicable to further categorize the asset.</li>
|
||||
<li><b>Model:</b> Define the specific model for more granular asset configuration.</li>
|
||||
<li><b>Unit:</b> Assign a unit to standardize measurements or operations.</li>
|
||||
<li><b>Speed:</b> Reaction speed of the machine in response to inputs.</li>
|
||||
<li><b>Startup:</b> Define the startup time for the machine.</li>
|
||||
<li><b>Warmup:</b> Define the warmup time for the machine.</li>
|
||||
<li><b>Shutdown:</b> Define the shutdown time for the machine.</li>
|
||||
<li><b>Cooldown:</b> Define the cooldown time for the machine.</li>
|
||||
<li><b>Enable Log:</b> Enable or disable logging for the machine.</li>
|
||||
<li><b>Log Level:</b> Set the log level (Info, Debug, Warn, Error).</li>
|
||||
<li><b>Reaction Speed, Startup, Warmup, Shutdown, Cooldown:</b> timing parameters.</li>
|
||||
<li><b>Supplier / SubType / Model / Unit:</b> choose via Asset menu.</li>
|
||||
<li><b>Enable Log / Log Level:</b> toggle via Logger menu.</li>
|
||||
<li><b>Position:</b> set Upstream / At Equipment / Downstream via Position menu.</li>
|
||||
</ul>
|
||||
</script>
|
||||
|
||||
</script>
|
||||
@@ -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);
|
||||
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}`);
|
||||
}
|
||||
});
|
||||
|
||||
node.on('close', function(done) {
|
||||
if (node.interval_id) clearTimeout(node.interval_id);
|
||||
if (node.tick_interval) clearInterval(node.tick_interval);
|
||||
if (done) done();
|
||||
// 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}`);
|
||||
}
|
||||
});
|
||||
|
||||
} 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);
|
||||
};
|
||||
293
src/nodeClass.js
Normal file
293
src/nodeClass.js
Normal file
@@ -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;
|
||||
801
src/specificClass.js
Normal file
801
src/specificClass.js
Normal file
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//*/
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user