diff --git a/package.json b/package.json new file mode 100644 index 0000000..1503baf --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/settler.html b/settler.html new file mode 100644 index 0000000..429f818 --- /dev/null +++ b/settler.html @@ -0,0 +1,86 @@ + + + + + + + \ No newline at end of file diff --git a/settler.js b/settler.js new file mode 100644 index 0000000..06f81a1 --- /dev/null +++ b/settler.js @@ -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}`); + } + }); +}; \ No newline at end of file diff --git a/src/nodeClass.js b/src/nodeClass.js new file mode 100644 index 0000000..2d16352 --- /dev/null +++ b/src/nodeClass.js @@ -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; \ No newline at end of file diff --git a/src/specificClass.js b/src/specificClass.js new file mode 100644 index 0000000..32589f9 --- /dev/null +++ b/src/specificClass.js @@ -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 }; \ No newline at end of file