Merge remote-tracking branch 'origin/main' into dev-Rene

# Conflicts:
#	additional_nodes/recirculation-pump.js
#	additional_nodes/settling-basin.js
#	reactor.html
#	src/nodeClass.js
#	src/reaction_modules/asm3_class Koch.js
#	src/reaction_modules/asm3_class.js
#	src/specificClass.js
This commit is contained in:
znetsixe
2026-03-31 16:20:45 +02:00
8 changed files with 1753 additions and 1399 deletions

View File

@@ -1,5 +1,5 @@
const { Reactor_CSTR, Reactor_PFR } = require('./specificClass.js');
const { outputUtils } = require('generalFunctions');
const { outputUtils, configManager } = require('generalFunctions');
const REACTOR_SPECIES = [
'S_O',
@@ -16,23 +16,23 @@ const REACTOR_SPECIES = [
'X_A',
'X_TS'
];
class nodeClass {
/**
* Node-RED node class for advanced-reactor.
* @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;
class nodeClass {
/**
* Node-RED node class for advanced-reactor.
* @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._output = new outputUtils();
@@ -40,143 +40,133 @@ class nodeClass {
this._attachInputHandler();
this._registerChild();
this._startTickLoop();
this._attachCloseHandler();
}
/**
* Handle node-red input messages
*/
_attachInputHandler() {
this.node.on('input', (msg, send, done) => {
try {
switch (msg.topic) {
case "clock":
this.source.updateState(msg.timestamp);
send([msg, null, null]);
break;
case "Fluent":
this.source.setInfluent = msg;
break;
case "OTR":
this.source.setOTR = msg;
break;
case "Temperature":
this.source.setTemperature = msg;
break;
case "Dispersion":
this.source.setDispersion = msg;
break;
case 'registerChild': {
const childId = msg.payload;
const childObj = this.RED.nodes.getNode(childId);
if (!childObj || !childObj.source) {
this.source?.logger?.warn(`registerChild skipped: missing child/source for id=${childId}`);
break;
}
this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
break;
}
default:
this.source?.logger?.warn(`Unknown topic: ${msg.topic}`);
}
} catch (error) {
this.source?.logger?.error(`Input handler failure: ${error.message}`);
}
if (typeof done === 'function') {
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: "reactor" // should be set in config manager
},
reactor_type: uiConfig.reactor_type,
volume: parseFloat(uiConfig.volume),
length: parseFloat(uiConfig.length),
resolution_L: parseInt(uiConfig.resolution_L),
alpha: parseFloat(uiConfig.alpha),
n_inlets: parseInt(uiConfig.n_inlets),
kla: parseFloat(uiConfig.kla),
initialState: [
parseFloat(uiConfig.S_O_init),
parseFloat(uiConfig.S_I_init),
parseFloat(uiConfig.S_S_init),
parseFloat(uiConfig.S_NH_init),
parseFloat(uiConfig.S_N2_init),
parseFloat(uiConfig.S_NO_init),
parseFloat(uiConfig.S_HCO_init),
parseFloat(uiConfig.X_I_init),
parseFloat(uiConfig.X_S_init),
parseFloat(uiConfig.X_H_init),
parseFloat(uiConfig.X_STO_init),
parseFloat(uiConfig.X_A_init),
parseFloat(uiConfig.X_TS_init)
],
timeStep: parseFloat(uiConfig.timeStep),
speedUpFactor: Number(uiConfig.speedUpFactor) || 1
}
}
/**
* 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
*/
_setupClass() {
let new_reactor;
switch (this.config.reactor_type) {
case "CSTR":
new_reactor = new Reactor_CSTR(this.config);
break;
case "PFR":
new_reactor = new Reactor_PFR(this.config);
break;
default:
this.node.warn("Unknown reactor type: " + this.config.reactor_type + ". Falling back to CSTR.");
new_reactor = new Reactor_CSTR(this.config);
}
this.source = new_reactor; // protect from reassignment
this.node.source = this.source;
}
_startTickLoop() {
setTimeout(() => {
this._tickInterval = setInterval(() => this._tick(), 1000);
}, 1000);
}
this._attachCloseHandler();
}
/**
* Handle node-red input messages
*/
_attachInputHandler() {
this.node.on('input', (msg, send, done) => {
try {
switch (msg.topic) {
case "clock":
this.source.updateState(msg.timestamp);
send([msg, null, null]);
break;
case "Fluent":
this.source.setInfluent = msg;
break;
case "OTR":
this.source.setOTR = msg;
break;
case "Temperature":
this.source.setTemperature = msg;
break;
case "Dispersion":
this.source.setDispersion = msg;
break;
case 'registerChild': {
const childId = msg.payload;
const childObj = this.RED.nodes.getNode(childId);
if (!childObj || !childObj.source) {
this.source?.logger?.warn(`registerChild skipped: missing child/source for id=${childId}`);
break;
}
this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
break;
}
default:
this.source?.logger?.warn(`Unknown topic: ${msg.topic}`);
}
} catch (error) {
this.source?.logger?.error(`Input handler failure: ${error.message}`);
}
if (typeof done === 'function') {
done();
}
});
}
/**
* Parse node configuration using ConfigManager
* @param {object} uiConfig Config set in UI in node-red
*/
_loadConfig(uiConfig) {
const cfgMgr = new configManager();
// Build config: base sections + reactor-specific domain config
this.config = cfgMgr.buildConfig('reactor', uiConfig, this.node.id, {
reactor_type: uiConfig.reactor_type,
volume: parseFloat(uiConfig.volume),
length: parseFloat(uiConfig.length),
resolution_L: parseInt(uiConfig.resolution_L),
alpha: parseFloat(uiConfig.alpha),
n_inlets: parseInt(uiConfig.n_inlets),
kla: parseFloat(uiConfig.kla),
initialState: [
parseFloat(uiConfig.S_O_init),
parseFloat(uiConfig.S_I_init),
parseFloat(uiConfig.S_S_init),
parseFloat(uiConfig.S_NH_init),
parseFloat(uiConfig.S_N2_init),
parseFloat(uiConfig.S_NO_init),
parseFloat(uiConfig.S_HCO_init),
parseFloat(uiConfig.X_I_init),
parseFloat(uiConfig.X_S_init),
parseFloat(uiConfig.X_H_init),
parseFloat(uiConfig.X_STO_init),
parseFloat(uiConfig.X_A_init),
parseFloat(uiConfig.X_TS_init)
],
timeStep: parseFloat(uiConfig.timeStep),
speedUpFactor: Number(uiConfig.speedUpFactor) || 1
});
}
/**
* 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
*/
_setupClass() {
let new_reactor;
switch (this.config.reactor_type) {
case "CSTR":
new_reactor = new Reactor_CSTR(this.config);
break;
case "PFR":
new_reactor = new Reactor_PFR(this.config);
break;
default:
this.node.warn("Unknown reactor type: " + this.config.reactor_type + ". Falling back to CSTR.");
new_reactor = new Reactor_CSTR(this.config);
}
this.source = new_reactor; // protect from reassignment
this.node.source = this.source;
}
_startTickLoop() {
setTimeout(() => {
this._tickInterval = setInterval(() => this._tick(), 1000);
}, 1000);
}
_tick(){
const gridProfile = this.source.getGridProfile;
if (gridProfile) {
@@ -209,10 +199,10 @@ class nodeClass {
_attachCloseHandler() {
this.node.on('close', (done) => {
clearInterval(this._tickInterval);
if (typeof done === 'function') done();
});
}
}
module.exports = nodeClass;
clearInterval(this._tickInterval);
if (typeof done === 'function') done();
});
}
}
module.exports = nodeClass;