From d5db1ae0a0fc4c00c4fb71761e29c4bbe8f07734 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 16 Jul 2025 10:57:35 +0200 Subject: [PATCH 1/6] Added seperate process, DB and parent outputs --- advanced-reactor.html | 5 +++-- src/nodeClass.js | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index 40ea4f6..91df690 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -26,8 +26,9 @@ X_TS_init: { value: 125.0009, required: true } }, inputs: 1, - outputs: 1, - outputLabels: "Effluent", + outputs: 3, + inputLabels: ["input"], + outputLabels: ["process", "dbase", "parent"], icon: "font-awesome/fa-recycle", label: function() { return this.name || "advanced-reactor"; diff --git a/src/nodeClass.js b/src/nodeClass.js index f4bbe25..9eba995 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -16,7 +16,7 @@ class nodeClass { this.name = nameOfNode; this._loadConfig(uiConfig) - + this._registerChild(); this._setupClass(); this._attachInputHandler(); @@ -60,7 +60,7 @@ class nodeClass { if (toggleUpdate) { this.reactor.updateState(msg.timestamp); - send(this.reactor.getEffluent); + send([this.reactor.getEffluent, null, null]); } if (done) { @@ -100,6 +100,20 @@ class nodeClass { } } + /** + * 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 reactor class based on config */ From 633a0884835c69095aa2696779371bbefafc8900 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 16 Jul 2025 16:08:14 +0200 Subject: [PATCH 2/6] Added handles for influent change emitter --- src/nodeClass.js | 20 ++++++++++--------- src/specificClass.js | 46 +++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 9eba995..c36152d 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -32,12 +32,11 @@ class nodeClass { switch (msg.topic) { case "clock": toggleUpdate = true; + this.reactor.updateState(msg.timestamp); + send([this.reactor.getEffluent, null, null]); break; case "Fluent": this.reactor.setInfluent = msg; - if (msg.payload.inlet == 0) { - toggleUpdate = true; - } break; case "OTR": this.reactor.setOTR = msg; @@ -58,11 +57,6 @@ class nodeClass { console.log("Unknown topic: " + msg.topic); } - if (toggleUpdate) { - this.reactor.updateState(msg.timestamp); - send([this.reactor.getEffluent, null, null]); - } - if (done) { done(); } @@ -75,6 +69,14 @@ class nodeClass { */ _loadConfig(uiConfig) { this.config = { + general: { + name: uiConfig.name || this.name, + id: this.node.id, + unit: null + }, + functionality: { + softwareType: null // should be set in config manager + }, reactor_type: uiConfig.reactor_type, volume: parseFloat(uiConfig.volume), length: parseFloat(uiConfig.length), @@ -109,7 +111,7 @@ class nodeClass { this.node.send([ null, null, - { topic: 'registerChild', payload: this.node.id , positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, + { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, ]); }, 100); } diff --git a/src/specificClass.js b/src/specificClass.js index 8fb1448..474ebf7 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,13 +1,14 @@ const ASM3 = require('./reaction_modules/asm3_class.js'); -const { create, all } = require('mathjs'); +const { create, all, isArray } = require('mathjs'); const { assertNoNaN } = require('./utils.js'); const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions'); +const EventEmitter = require('events'); -const config = { +const mathConfig = { matrix: 'Array' // use Array as the matrix type }; -const math = create(all, config); +const math = create(all, mathConfig); const S_O_INDEX = 0; const NUM_SPECIES = 13; @@ -20,7 +21,8 @@ class Reactor { */ constructor(config) { // EVOLV stuff - this.logger = new logger(); //TODO: attach config + this.logger = new logger(undefined, undefined, config.general.name); + this.emitter = new EventEmitter(); this.measurements = new MeasurementContainer(); this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility @@ -70,6 +72,17 @@ class Reactor { this.OTR = input.payload; } + /** + * Getter for effluent data. + * @returns {object} Effluent data object (msg), defaults to inlet 0. + */ + get getEffluent() { // getter for Effluent, defaults to inlet 0 + if (isArray(this.state.at(-1))) { + return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; + } + return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; + } + /** * Calculate the oxygen transfer rate (OTR) based on the dissolved oxygen concentration and temperature. * @param {number} S_O - Dissolved oxygen concentration [g O2 m-3]. @@ -104,11 +117,12 @@ class Reactor { let n_iter = Math.floor(this.speedUpFactor * (newTime-this.currentTime) / (this.timeStep*day2ms)); if (n_iter) { let n = 0; - while (n < n_iter) { - this.tick(this.timeStep); - n += 1; - } + while (n < n_iter) { + this.tick(this.timeStep); + n += 1; + } this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; + this.emitter.emit("stateChanges", this.getEffluent); } } } @@ -123,14 +137,6 @@ class Reactor_CSTR extends Reactor { this.state = config.initialState; } - /** - * Getter for effluent data. - * @returns {object} Effluent data object (msg), defaults to inlet 0. - */ - get getEffluent() { // getter for Effluent, defaults to inlet 0 - return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; - } - /** * Tick the reactor state using the forward Euler method. * @param {number} time_step - Time step for the simulation [d]. @@ -191,14 +197,6 @@ class Reactor_PFR extends Reactor { this.D = input.payload; } - /** - * Getter for effluent data. - * @returns {object} Effluent data object (msg), defaults to inlet 0. - */ - get getEffluent() { - return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; - } - updateState(newTime) { super.updateState(newTime); let Pe_local = this.d_x*math.sum(this.Fs)/(this.D*this.A) From 73c2b654e145e7c8ec3f96282f49fb54b6e8a1aa Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 16 Jul 2025 16:54:30 +0200 Subject: [PATCH 3/6] Deal with clock singal --- additional_nodes/recirculation-pump.js | 2 ++ additional_nodes/settling-basin.js | 2 ++ src/nodeClass.js | 1 + 3 files changed, 5 insertions(+) diff --git a/additional_nodes/recirculation-pump.js b/additional_nodes/recirculation-pump.js index cd9fc21..2a02932 100644 --- a/additional_nodes/recirculation-pump.js +++ b/additional_nodes/recirculation-pump.js @@ -24,6 +24,8 @@ module.exports = function(RED) { send([msg_F1, msg_F2]); break; + case "clock": + break; default: console.log("Unknown topic: " + msg.topic); } diff --git a/additional_nodes/settling-basin.js b/additional_nodes/settling-basin.js index e19457a..dd3d697 100644 --- a/additional_nodes/settling-basin.js +++ b/additional_nodes/settling-basin.js @@ -41,6 +41,8 @@ module.exports = function(RED) { send([msg_F1, msg_F2]); break; + case "clock": + break; default: console.log("Unknown topic: " + msg.topic); } diff --git a/src/nodeClass.js b/src/nodeClass.js index c36152d..94b3965 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -34,6 +34,7 @@ class nodeClass { toggleUpdate = true; this.reactor.updateState(msg.timestamp); send([this.reactor.getEffluent, null, null]); + send([msg, null, null]) break; case "Fluent": this.reactor.setInfluent = msg; From b1719376cffcd0e32d96dae20e438262b528851d Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 21 Jul 2025 12:44:07 +0200 Subject: [PATCH 4/6] Rewrite reactor to source and register it properly to node object --- src/nodeClass.js | 25 ++++++++++++++----------- src/specificClass.js | 3 ++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 94b3965..989b806 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -14,12 +14,13 @@ class nodeClass { this.node = nodeInstance; this.RED = RED; this.name = nameOfNode; + this.source = null; this._loadConfig(uiConfig) - this._registerChild(); this._setupClass(); this._attachInputHandler(); + this._registerChild(); } /** @@ -32,27 +33,28 @@ class nodeClass { switch (msg.topic) { case "clock": toggleUpdate = true; - this.reactor.updateState(msg.timestamp); - send([this.reactor.getEffluent, null, null]); + this.source.updateState(msg.timestamp); + send([this.source.getEffluent, null, null]); send([msg, null, null]) break; case "Fluent": - this.reactor.setInfluent = msg; + this.source.setInfluent = msg; break; case "OTR": - this.reactor.setOTR = msg; + this.source.setOTR = msg; break; case "Temperature": - this.reactor.setTemperature = msg; + this.source.setTemperature = msg; break; case "Dispersion": - this.reactor.setDispersion = msg; + this.source.setDispersion = msg; break; case 'registerChild': // Register this node as a child of the parent node const childId = msg.payload; const childObj = this.RED.nodes.getNode(childId); - this.reactor.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); + console.log(childObj.source); + this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); break; default: console.log("Unknown topic: " + msg.topic); @@ -76,7 +78,7 @@ class nodeClass { unit: null }, functionality: { - softwareType: null // should be set in config manager + softwareType: "reactor" // should be set in config manager }, reactor_type: uiConfig.reactor_type, volume: parseFloat(uiConfig.volume), @@ -112,7 +114,7 @@ class nodeClass { this.node.send([ null, null, - { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, + { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' } ]); }, 100); } @@ -134,7 +136,8 @@ class nodeClass { console.warn("Unknown reactor type: " + uiConfig.reactor_type); } - this.reactor = new_reactor; // protect from reassignment + this.source = new_reactor; // protect from reassignment + this.node.source = this.source; } } diff --git a/src/specificClass.js b/src/specificClass.js index 474ebf7..99ed988 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -20,6 +20,7 @@ class Reactor { * @param {object} config - Configuration object containing reactor parameters. */ constructor(config) { + this.config = config; // EVOLV stuff this.logger = new logger(undefined, undefined, config.general.name); this.emitter = new EventEmitter(); @@ -122,7 +123,7 @@ class Reactor { n += 1; } this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; - this.emitter.emit("stateChanges", this.getEffluent); + this.emitter.emit("stateChange", this.getEffluent); } } } From 57aafe3e0b8ae4ebaf3784dc24497b21be791592 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 21 Jul 2025 17:28:09 +0200 Subject: [PATCH 5/6] Minor optimisations --- src/nodeClass.js | 6 ++---- src/specificClass.js | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 989b806..97ec545 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -28,14 +28,12 @@ class nodeClass { */ _attachInputHandler() { this.node.on('input', (msg, send, done) => { - let toggleUpdate = false; switch (msg.topic) { case "clock": - toggleUpdate = true; this.source.updateState(msg.timestamp); send([this.source.getEffluent, null, null]); - send([msg, null, null]) + send([msg, null, null]); break; case "Fluent": this.source.setInfluent = msg; @@ -53,7 +51,6 @@ class nodeClass { // Register this node as a child of the parent node const childId = msg.payload; const childObj = this.RED.nodes.getNode(childId); - console.log(childObj.source); this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); break; default: @@ -139,6 +136,7 @@ class nodeClass { this.source = new_reactor; // protect from reassignment this.node.source = this.source; } + } module.exports = nodeClass; \ No newline at end of file diff --git a/src/specificClass.js b/src/specificClass.js index 99ed988..a240275 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -25,6 +25,7 @@ class Reactor { this.logger = new logger(undefined, undefined, config.general.name); this.emitter = new EventEmitter(); this.measurements = new MeasurementContainer(); + this.upstreamReactor = null; this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility this.asm = new ASM3(); @@ -115,6 +116,10 @@ class Reactor { updateState(newTime) { // expect update with timestamp const day2ms = 1000 * 60 * 60 * 24; + if (this.upstreamReactor) { + this.setInfluent = this.upstreamReactor.getEffluent; + } + let n_iter = Math.floor(this.speedUpFactor * (newTime-this.currentTime) / (this.timeStep*day2ms)); if (n_iter) { let n = 0; @@ -123,7 +128,7 @@ class Reactor { n += 1; } this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; - this.emitter.emit("stateChange", this.getEffluent); + this.emitter.emit("stateChange", newTime); } } } From f81161b2d571b881fe291ec15401528796e1e9ab Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 22 Jul 2025 12:20:29 +0200 Subject: [PATCH 6/6] Process output using tick function rather than clock message --- src/nodeClass.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 97ec545..637ec5b 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -21,6 +21,8 @@ class nodeClass { this._attachInputHandler(); this._registerChild(); + this._startTickLoop(); + this._attachCloseHandler(); } /** @@ -32,7 +34,6 @@ class nodeClass { switch (msg.topic) { case "clock": this.source.updateState(msg.timestamp); - send([this.source.getEffluent, null, null]); send([msg, null, null]); break; case "Fluent": @@ -137,6 +138,22 @@ class nodeClass { this.node.source = this.source; } + _startTickLoop() { + setTimeout(() => { + this._tickInterval = setInterval(() => this._tick(), 1000); + }, 1000); + } + + _tick(){ + this.node.send([this.source.getEffluent, null, null]); + } + + _attachCloseHandler() { + this.node.on('close', (done) => { + clearInterval(this._tickInterval); + done(); + }); + } } module.exports = nodeClass; \ No newline at end of file