Add initial implementation of the settler node for Node-RED
- Create package.json to define project metadata and dependencies - Implement settler.html for the node's UI in Node-RED - Add settler.js to register the settler node type and handle HTTP requests - Introduce src/nodeClass.js for the settler node class functionality - Define src/specificClass.js for settler-specific logic and utilities
This commit is contained in:
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "settler",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Sludge settler model for Node-Red",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.centraal.wbd-rd.nl/p.vanderwilt/settler.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"settler",
|
||||||
|
"activated sludge",
|
||||||
|
"wastewater",
|
||||||
|
"node-red"
|
||||||
|
],
|
||||||
|
"license": "SEE LICENSE",
|
||||||
|
"author": "P.R. van der Wilt",
|
||||||
|
"main": "settler.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node settler.js"
|
||||||
|
},
|
||||||
|
"node-red": {
|
||||||
|
"nodes": {
|
||||||
|
"settler": "settler.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git",
|
||||||
|
"mathjs": "^14.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
86
settler.html
Normal file
86
settler.html
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<script src="/reactor/menu.js"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType("settler", {
|
||||||
|
category: "WWTP",
|
||||||
|
color: "#e4a363",
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
TS_set: { value: 0.1, required: true },
|
||||||
|
inlet: { value: 1, required: true }
|
||||||
|
},
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 3,
|
||||||
|
outputLabels: ["process", "dbase", "parent"],
|
||||||
|
icon: "font-awesome/fa-random",
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Settling basin";
|
||||||
|
},
|
||||||
|
oneditprepare: function() {
|
||||||
|
// wait for the menu scripts to load
|
||||||
|
const waitForMenuData = () => {
|
||||||
|
if (window.EVOLV?.nodes?.reactor?.initEditor) {
|
||||||
|
window.EVOLV.nodes.reactor.initEditor(this);
|
||||||
|
} else {
|
||||||
|
setTimeout(waitForMenuData, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
waitForMenuData();
|
||||||
|
|
||||||
|
$("#node-input-TS_set").typedInput({
|
||||||
|
type:"num",
|
||||||
|
types:["num"]
|
||||||
|
});
|
||||||
|
$("#node-input-inlet").typedInput({
|
||||||
|
type:"num",
|
||||||
|
types:["num"]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
oneditsave: function() {
|
||||||
|
// save logger fields
|
||||||
|
if (window.EVOLV?.nodes?.reactor?.loggerMenu?.saveEditor) {
|
||||||
|
window.EVOLV.nodes.reactor.loggerMenu.saveEditor(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// save position field
|
||||||
|
if (window.EVOLV?.nodes?.measurement?.positionMenu?.saveEditor) {
|
||||||
|
window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
let TS_set = parseFloat($("#node-input-TS_set").typedInput("value"));
|
||||||
|
if (isNaN(TS_set) || TS_set < 0) {
|
||||||
|
RED.notify("TS is not set correctly", {type: "error"});
|
||||||
|
}
|
||||||
|
let inlet = parseInt($("#node-input-n_inlets").typedInput("value"));
|
||||||
|
if (inlet < 1) {
|
||||||
|
RED.notify("Number of inlets not set correctly", {type: "error"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="settler">
|
||||||
|
<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="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-TS_set"><i class="fa fa-tag"></i> Total Solids set point [g m-3]</label>
|
||||||
|
<input type="text" id="node-input-TS_set" placeholder="">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-inlet"><i class="fa fa-tag"></i> Assigned inlet return line</label>
|
||||||
|
<input type="text" id="node-input-inlet" placeholder="#">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logger fields injected here -->
|
||||||
|
<div id="logger-fields-placeholder"></div>
|
||||||
|
|
||||||
|
<!-- Position fields will be injected here -->
|
||||||
|
<div id="position-fields-placeholder"></div>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-help-name="settler">
|
||||||
|
<p>Settling tank</p>
|
||||||
|
</script>
|
||||||
26
settler.js
Normal file
26
settler.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
const nameOfNode = "settler"; // name of the node, should match file name and node type in Node-RED
|
||||||
|
const nodeClass = require('./src/nodeClass.js'); // node class
|
||||||
|
const { MenuManager } = require('generalFunctions');
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
// Then create your custom class and attach it
|
||||||
|
this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
const menuMgr = new MenuManager();
|
||||||
|
|
||||||
|
// Serve /settler/menu.js
|
||||||
|
RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
|
||||||
|
try {
|
||||||
|
const script = menuMgr.createEndpoint(nameOfNode, ['logger', 'position']);
|
||||||
|
res.type('application/javascript').send(script);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).send(`// Error generating menu: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
114
src/nodeClass.js
Normal file
114
src/nodeClass.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
const { Settler } = require('./specificClass.js');
|
||||||
|
|
||||||
|
|
||||||
|
class nodeClass {
|
||||||
|
/**
|
||||||
|
* Node-RED node class for settler.
|
||||||
|
* @param {object} uiConfig - Node-RED node configuration
|
||||||
|
* @param {object} RED - Node-RED runtime API
|
||||||
|
* @param {object} nodeInstance - Node-RED node instance
|
||||||
|
* @param {string} nameOfNode - Name of the node
|
||||||
|
*/
|
||||||
|
constructor(uiConfig, RED, nodeInstance, nameOfNode) {
|
||||||
|
// Preserve RED reference for HTTP endpoints if needed
|
||||||
|
this.node = nodeInstance;
|
||||||
|
this.RED = RED;
|
||||||
|
this.name = nameOfNode;
|
||||||
|
this.source = null;
|
||||||
|
|
||||||
|
this._loadConfig(uiConfig)
|
||||||
|
this._setupClass();
|
||||||
|
|
||||||
|
this._attachInputHandler();
|
||||||
|
this._registerChild();
|
||||||
|
this._startTickLoop();
|
||||||
|
this._attachCloseHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle node-red input messages
|
||||||
|
*/
|
||||||
|
_attachInputHandler() {
|
||||||
|
this.node.on('input', (msg, send, done) => {
|
||||||
|
|
||||||
|
switch (msg.topic) {
|
||||||
|
case 'registerChild':
|
||||||
|
// Register this node as a parent of the child node
|
||||||
|
const childId = msg.payload;
|
||||||
|
const childObj = this.RED.nodes.getNode(childId);
|
||||||
|
this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unknown topic: " + msg.topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse node configuration
|
||||||
|
* @param {object} uiConfig Config set in UI in node-red
|
||||||
|
*/
|
||||||
|
_loadConfig(uiConfig) {
|
||||||
|
this.config = {
|
||||||
|
general: {
|
||||||
|
name: uiConfig.name || this.name,
|
||||||
|
id: this.node.id,
|
||||||
|
unit: null,
|
||||||
|
logging: {
|
||||||
|
enabled: uiConfig.enableLog,
|
||||||
|
logLevel: uiConfig.logLevel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
functionality: {
|
||||||
|
positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified
|
||||||
|
softwareType: "settler" // should be set in config manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup settler class
|
||||||
|
*/
|
||||||
|
_setupClass() {
|
||||||
|
|
||||||
|
this.source = new Settler(this.config); // protect from reassignment
|
||||||
|
this.node.source = this.source;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startTickLoop() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this._tickInterval = setInterval(() => this._tick(), 1000);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
_tick(){
|
||||||
|
this.node.send([null, null, null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_attachCloseHandler() {
|
||||||
|
this.node.on('close', (done) => {
|
||||||
|
clearInterval(this._tickInterval);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nodeClass;
|
||||||
15
src/specificClass.js
Normal file
15
src/specificClass.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
|
||||||
|
class Settler {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
// EVOLV stuff
|
||||||
|
this.logger = new logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, config.general.name);
|
||||||
|
this.emitter = new EventEmitter();
|
||||||
|
this.measurements = new MeasurementContainer();
|
||||||
|
this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Settler };
|
||||||
Reference in New Issue
Block a user