Compare commits

..

1 Commits

Author SHA1 Message Date
znetsixe
35eb965609 not working yet need to fix child registration? 2025-07-01 17:03:36 +02:00
7 changed files with 1554 additions and 274 deletions

View File

@@ -33,7 +33,7 @@
* Author: * Author:
* - Rene De Ren * - Rene De Ren
* Email: * Email:
* - rene@thegoldenbasket.nl * - r.de.ren@brabantsedelta.nl
*/ */
//load local dependencies //load local dependencies

153
mgc.html
View File

@@ -11,96 +11,61 @@
#4f8582 #4f8582
#c4cce0 #c4cce0
--> -->
<script type="text/javascript"> <script src="/machineGroupControl/menu.js"></script> <!-- Load the menu script for dynamic dropdowns -->
<script src="/machineGroupControl/configData.js"></script> <!-- Load the config script for node information -->
<script>
RED.nodes.registerType('machineGroupControl',{ RED.nodes.registerType('machineGroupControl',{
category: "EVOLV",
category: 'digital twin', color: "#eaf4f1",
color: '#eaf4f1', defaults: {
defaults: { // Define default properties
name: {value:""}, name: { value: "" },
enableLog: { value: false },
logLevel: { value: "error" }, // Logger properties
}, enableLog: { value: false },
logLevel: { value: "error" },
// Physical aspect
positionVsParent: { value: "" },
positionLabel: { value: "" },
positionIcon: { value: "" },
},
inputs:1, inputs:1,
outputs:4, outputs:3,
inputLabels: "Usage see manual", inputLabels: ["Input"],
outputLabels: ["process", "dbase", "upstreamParent", "downstreamParent"], outputLabels: ["process", "dbase", "parent"],
icon: "font-awesome/fa-tachometer", icon: "font-awesome/fa-tachometer",
//define label function
label: function() { label: function () {
return this.name || "MachineGroup controller"; return this.positionIcon + " " + "machineGroup";
}, },
oneditprepare: function() { oneditprepare: function() {
const node = this; // Initialize the menu data for the node
const waitForMenuData = () => {
console.log("Rotating Machine Node: Edit Prepare"); if (window.EVOLV?.nodes?.machineGroupControl?.initEditor) {
window.EVOLV.nodes.machineGroupControl.initEditor(this);
const elements = { } else {
// Basic fields setTimeout(waitForMenuData, 50);
name: document.getElementById("node-input-name"),
// Logging fields
logCheckbox: document.getElementById("node-input-enableLog"),
logLevelSelect: document.getElementById("node-input-logLevel"),
rowLogLevel: document.getElementById("row-logLevel"),
};
const projecSettingstURL = "http://localhost:1880/generalFunctions/settings/projectSettings.json";
try{
// Fetch project settings
menuUtils.fetchProjectData(projecSettingstURL)
.then((projectSettings) => {
//assign to node vars
node.configUrls = projectSettings.configUrls;
const { cloudConfigURL, localConfigURL } = menuUtils.getSpecificConfigUrl("machineGroupControl",node.configUrls.cloud.taggcodeAPI);
node.configUrls.cloud.config = cloudConfigURL; // first call
node.configUrls.local.config = localConfigURL; // backup call
// 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);
})
}catch(e){
console.log("Error fetching project settings", e);
} }
};
if(node.d){ // Wait for the menu data to be ready before initializing the editor
//this means node is disabled waitForMenuData();
console.log("Current status of node is disabled");
}
}, },
oneditsave: function(){ oneditsave: function(){
const node = this; const node = this;
//save basic properties // Validate logger properties using the logger menu
["name"].forEach( if (window.EVOLV?.nodes?.measurement?.loggerMenu?.saveEditor) {
(field) => { success = window.EVOLV.nodes.measurement.loggerMenu.saveEditor(node);
const element = document.getElementById(`node-input-${field}`);
if (element) {
node[field] = element.value || "";
}
} }
);
const logLevelElement = document.getElementById("node-input-logLevel");
node.logLevel = logLevelElement ? logLevelElement.value || "info" : "info";
// save position field
if (window.EVOLV?.nodes?.rotatingMachine?.positionMenu?.saveEditor) {
window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this);
}
} }
}); });
@@ -108,39 +73,13 @@
<script type="text/html" data-template-name="machineGroupControl"> <script type="text/html" data-template-name="machineGroupControl">
<!-------------------------------------------INPUT NAME / TYPE -----------------------------------------------> <!-- Logger fields injected here -->
<div class="form-row"> <div id="logger-fields-placeholder"></div>
<label for="node-input-name"><i class="fa fa-tag"></i>Name</label>
<input type="text" id="node-input-name" placeholder="Name">
</div>
<hr /> <!-- Position fields injected here -->
<div id="position-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>
<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>
<!-------------------------------------------INPUT TRANSLATION TO OUTPUT ----------------------------------------------->
<hr />
<div class="form-tips"></div> <div class="form-tips"></div>
<b>Tip:</b> Ensure that the "Name" field is unique to easily identify the node. <b>Tip:</b> Ensure that the "Name" field is unique to easily identify the node.
Enable logging if you need detailed information for debugging purposes. Enable logging if you need detailed information for debugging purposes.

195
mgc.js
View File

@@ -1,170 +1,39 @@
module.exports = function (RED) { const nameOfNode = 'machineGroupControl'; // this is the name of the node, it should match the file name and the node type in Node-RED
function machineGroupControl(config) { const nodeClass = require('./src/nodeClass.js'); // this is the specific node class
//create node const { MenuManager, configManager } = require('generalFunctions');
// This is the main entry point for the Node-RED node, it will register the node and setup the endpoints
module.exports = function(RED) {
// Register the node type
RED.nodes.registerType(nameOfNode, function(config) {
// Initialize the Node-RED node first
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
// Then create your custom class and attach it
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
});
//call this => node so whenver you want to call a node function type node and the function behind it // Setup admin UIs
var node = this; const menuMgr = new MenuManager(); //this will handle the menu endpoints so we can load them dynamically
const cfgMgr = new configManager(); // this will handle the config endpoints so we can load them dynamically
//fetch machine object from machine.js // Register the different menu's for the node
const MachineGroup = require('./dependencies/machineGroup/machineGroup'); RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
const OutputUtils = require("../generalFunctions/helper/outputUtils"); try {
const script = menuMgr.createEndpoint(nameOfNode, ['logger','position']);
const mgConfig = config = { res.type('application/javascript').send(script);
general: { } catch (err) {
name: config.name, res.status(500).send(`// Error generating menu: ${err.message}`);
id : config.id,
logging: {
enabled: config.loggingEnabled,
logLevel: config.logLevel,
}
},
};
//make new class on creation to work with.
const mg = new MachineGroup(mgConfig);
// put mg on node memory as source
node.source = mg;
//load output utils
const output = new OutputUtils();
//update node status
function updateNodeStatus(mg) {
const mode = mg.mode;
const scaling = mg.scaling;
const totalFlow = Math.round(mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() * 1) / 1;
const totalPower = Math.round(mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() * 1) / 1;
// Calculate total capacity based on available machines
const availableMachines = Object.values(mg.machines).filter(machine => {
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
return !(state === "off" || state === "maintenance" || mode === "maintenance");
});
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
// Determine overall status based on available machines
const status = availableMachines.length > 0
? `${availableMachines.length} machines`
: "No machines";
let scalingSymbol = '';
switch (scaling.toLowerCase()) {
case 'absolute':
scalingSymbol = 'Ⓐ'; // Clear symbol for Absolute mode
break;
case 'normalized':
scalingSymbol = 'Ⓝ'; // Clear symbol for Normalized mode
break;
default:
scalingSymbol = mode;
break;
}
// Generate status text in a single line
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
return {
fill: availableMachines.length > 0 ? "green" : "red",
shape: "dot",
text
};
} }
});
//never ending functions // Endpoint to get the configuration data for the specific node
function tick(){ RED.httpAdmin.get(`/${nameOfNode}/configData.js`, (req, res) => {
//source.tick(); try {
const status = updateNodeStatus(mg); const script = cfgMgr.createEndpoint(nameOfNode);
node.status(status); // Send the configuration data as JSON response
res.type('application/javascript').send(script);
//get output } catch (err) {
const classOutput = mg.getOutput(); res.status(500).send(`// Error generating configData: ${err.message}`);
const dbOutput = output.formatMsg(classOutput, mg.config, "influxdb");
const pOutput = output.formatMsg(classOutput, mg.config, "process");
//only send output on values that changed
let msgs = [];
msgs[0] = pOutput;
msgs[1] = dbOutput;
node.send(msgs);
} }
});
// 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
);
//-------------------------------------------------------------------->>what to do on input
node.on("input", async function (msg,send,done) {
if(msg.topic == 'registerChild'){
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
mg.childRegistrationUtils.registerChild(childObj.source,msg.positionVsParent);
}
if(msg.topic == 'setMode'){
const mode = msg.payload;
const source = "parent";
mg.setMode(source,mode);
}
if(msg.topic == 'setScaling'){
const scaling = msg.payload;
mg.setScaling(scaling);
}
if(msg.topic == 'Qd'){
const Qd = parseFloat(msg.payload);
const source = "parent";
if (isNaN(Qd)) {
return mg.logger.error(`Invalid demand value: ${Qd}`);
};
try{
await mg.handleInput(source,Qd);
msg.topic = mg.config.general.name;
msg.payload = "done";
send(msg);
}catch(e){
console.log(e);
}
}
// tidy up any async code here - shutdown connections and so on.
node.on('close', function() {
clearTimeout(this.interval_id);
});
});
}
RED.nodes.registerType("machineGroupControl", machineGroupControl);
}; };

170
mgcOLD.js Normal file
View File

@@ -0,0 +1,170 @@
module.exports = function (RED) {
function machineGroupControl(config) {
//create node
RED.nodes.createNode(this, config);
//call this => node so whenver you want to call a node function type node and the function behind it
var node = this;
//fetch machine object from machine.js
const MachineGroup = require('./dependencies/machineGroup/machineGroup');
const OutputUtils = require("../generalFunctions/helper/outputUtils");
const mgConfig = config = {
general: {
name: config.name,
id : config.id,
logging: {
enabled: config.loggingEnabled,
logLevel: config.logLevel,
}
},
};
//make new class on creation to work with.
const mg = new MachineGroup(mgConfig);
// put mg on node memory as source
node.source = mg;
//load output utils
const output = new OutputUtils();
//update node status
function updateNodeStatus(mg) {
const mode = mg.mode;
const scaling = mg.scaling;
const totalFlow = Math.round(mg.measurements.type("flow").variant("predicted").position("downstream").getCurrentValue() * 1) / 1;
const totalPower = Math.round(mg.measurements.type("power").variant("predicted").position("upstream").getCurrentValue() * 1) / 1;
// Calculate total capacity based on available machines
const availableMachines = Object.values(mg.machines).filter(machine => {
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
return !(state === "off" || state === "maintenance" || mode === "maintenance");
});
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
// Determine overall status based on available machines
const status = availableMachines.length > 0
? `${availableMachines.length} machines`
: "No machines";
let scalingSymbol = '';
switch (scaling.toLowerCase()) {
case 'absolute':
scalingSymbol = 'Ⓐ'; // Clear symbol for Absolute mode
break;
case 'normalized':
scalingSymbol = 'Ⓝ'; // Clear symbol for Normalized mode
break;
default:
scalingSymbol = mode;
break;
}
// Generate status text in a single line
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
return {
fill: availableMachines.length > 0 ? "green" : "red",
shape: "dot",
text
};
}
//never ending functions
function tick(){
//source.tick();
const status = updateNodeStatus(mg);
node.status(status);
//get output
const classOutput = mg.getOutput();
const dbOutput = output.formatMsg(classOutput, mg.config, "influxdb");
const pOutput = output.formatMsg(classOutput, mg.config, "process");
//only send output on values that changed
let msgs = [];
msgs[0] = pOutput;
msgs[1] = dbOutput;
node.send(msgs);
}
// 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
);
//-------------------------------------------------------------------->>what to do on input
node.on("input", async function (msg,send,done) {
if(msg.topic == 'registerChild'){
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
mg.childRegistrationUtils.registerChild(childObj.source,msg.positionVsParent);
}
if(msg.topic == 'setMode'){
const mode = msg.payload;
const source = "parent";
mg.setMode(source,mode);
}
if(msg.topic == 'setScaling'){
const scaling = msg.payload;
mg.setScaling(scaling);
}
if(msg.topic == 'Qd'){
const Qd = parseFloat(msg.payload);
const source = "parent";
if (isNaN(Qd)) {
return mg.logger.error(`Invalid demand value: ${Qd}`);
};
try{
await mg.handleInput(source,Qd);
msg.topic = mg.config.general.name;
msg.payload = "done";
send(msg);
}catch(e){
console.log(e);
}
}
// tidy up any async code here - shutdown connections and so on.
node.on('close', function() {
clearTimeout(this.interval_id);
});
});
}
RED.nodes.registerType("machineGroupControl", machineGroupControl);
};

View File

@@ -17,9 +17,7 @@
"author": "Rene De Ren", "author": "Rene De Ren",
"license": "SEE LICENSE", "license": "SEE LICENSE",
"dependencies": { "dependencies": {
"generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git", "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git"
"convert": "git+https://gitea.centraal.wbd-rd.nl/RnD/convert.git",
"predict": "git+https://gitea.centraal.wbd-rd.nl/RnD/predict.git"
}, },
"node-red": { "node-red": {
"nodes": { "nodes": {

256
src/nodeClass.js Normal file
View File

@@ -0,0 +1,256 @@
const { outputUtils, configManager } = require("generalFunctions");
const Specific = require("./specificClass");
class nodeClass {
/**
* Create a MeasurementNode.
* @param {object} uiConfig - Node-RED node configuration.
* @param {object} RED - Node-RED runtime API.
* @param {object} nodeInstance - The Node-RED node instance.
* @param {string} nameOfNode - The name of the node, used for
*/
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,
},
},
functionality: {
positionVsParent: uiConfig.positionVsParent || "atEquipment", // Default to 'atEquipment' if not set
},
};
// Utility for formatting outputs
this._output = new outputUtils();
}
_updateNodeStatus() {
const mg = this.source;
const mode = mg.mode;
const scaling = mg.scaling;
const totalFlow =
Math.round(
mg.measurements
.type("flow")
.variant("predicted")
.position("downstream")
.getCurrentValue() * 1
) / 1;
const totalPower =
Math.round(
mg.measurements
.type("power")
.variant("predicted")
.position("upstream")
.getCurrentValue() * 1
) / 1;
// Calculate total capacity based on available machines
const availableMachines = Object.values(mg.machines).filter((machine) => {
const state = machine.state.getCurrentState();
const mode = machine.currentMode;
return !(
state === "off" ||
state === "maintenance" ||
mode === "maintenance"
);
});
const totalCapacity = Math.round(mg.dynamicTotals.flow.max * 1) / 1;
// Determine overall status based on available machines
const status =
availableMachines.length > 0
? `${availableMachines.length} machines`
: "No machines";
let scalingSymbol = "";
switch (scaling.toLowerCase()) {
case "absolute":
scalingSymbol = "Ⓐ"; // Clear symbol for Absolute mode
break;
case "normalized":
scalingSymbol = "Ⓝ"; // Clear symbol for Normalized mode
break;
default:
scalingSymbol = mode;
break;
}
// Generate status text in a single line
const text = ` ${mode} | ${scalingSymbol}: 💨=${totalFlow}/${totalCapacity} | ⚡=${totalPower} | ${status}`;
return {
fill: availableMachines.length > 0 ? "green" : "red",
shape: "dot",
text,
};
}
/**
* Instantiate the core logic and store as source.
*/
_setupSpecificClass() {
this.source = new Specific(this.config);
this.node.source = this.source; // Store the source in the node instance for easy access
}
/**
* Bind 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}`,
});
});
}
/**
* 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.node.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() {
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) =>
async function () {
switch (msg.topic) {
case "registerChild":
const childId = msg.payload;
const childObj = RED.nodes.getNode(childId);
mg.childRegistrationUtils.registerChild(
childObj.source,
msg.positionVsParent
);
break;
case "setMode":
const mode = msg.payload;
const source = "parent";
mg.setMode(source, mode);
break;
case "setScaling":
const scaling = msg.payload;
mg.setScaling(scaling);
break;
case "Qd":
const Qd = parseFloat(msg.payload);
const sourceQd = "parent";
if (isNaN(Qd)) {
return mg.logger.error(`Invalid demand value: ${Qd}`);
}
try {
await mg.handleInput(sourceQd, Qd);
msg.topic = mg.config.general.name;
msg.payload = "done";
send(msg);
} catch (e) {
console.log(e);
}
break;
default:
// Handle unknown topics if needed
break;
}
done();
}
);
}
/**
* Clean up timers and intervals when Node-RED stops the node.
*/
_attachCloseHandler() {
this.node.on("close", (done) => {
clearInterval(this._tickInterval);
done();
});
}
}
module.exports = nodeClass; // Export the class for Node-RED to use

1048
src/specificClass.js Normal file

File diff suppressed because it is too large Load Diff