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/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..637ec5b 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -14,12 +14,15 @@ class nodeClass { 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(); } /** @@ -27,42 +30,34 @@ 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([msg, null, null]); break; case "Fluent": - this.reactor.setInfluent = msg; - if (msg.payload.inlet == 0) { - toggleUpdate = true; - } + 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); + this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); break; default: console.log("Unknown topic: " + msg.topic); } - if (toggleUpdate) { - this.reactor.updateState(msg.timestamp); - send(this.reactor.getEffluent); - } - if (done) { done(); } @@ -75,6 +70,14 @@ class nodeClass { */ _loadConfig(uiConfig) { this.config = { + general: { + name: uiConfig.name || this.name, + id: this.node.id, + unit: null + }, + functionality: { + softwareType: "reactor" // should be set in config manager + }, reactor_type: uiConfig.reactor_type, volume: parseFloat(uiConfig.volume), length: parseFloat(uiConfig.length), @@ -100,6 +103,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 */ @@ -117,7 +134,25 @@ 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; + } + + _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(); + }); } } diff --git a/src/specificClass.js b/src/specificClass.js index 8fb1448..a240275 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; @@ -19,9 +20,12 @@ class Reactor { * @param {object} config - Configuration object containing reactor parameters. */ constructor(config) { + this.config = 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.upstreamReactor = null; this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility this.asm = new ASM3(); @@ -70,6 +74,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]. @@ -101,14 +116,19 @@ 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; - 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("stateChange", newTime); } } } @@ -123,14 +143,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 +203,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)