Hey STeve,
Yeah understood what you are trying to do however this destroys my reference to the emitter of that node. So I didnt want to make a lengthy post so Ill try to give as much as possible without overkilling. (its good for my thought process as well so I dont mind sharing).
This is my flow:
Here is my measurement.js (node red interface) that sends the msg on deploy to all the other nodes. This has always worked fine when directly handled by the receiving class.
// register child on first output this timeout is needed because of node - red stuff
setTimeout(
() => {
/*---execute code on first start----*/
let msgs = [];
msgs[2] = { topic : "registerChild" , payload: source, positionVsParent: "upStream" };
msgs[3] = { topic : "registerChild" , payload: source, positionVsParent: "downStream" };
//send msg
this.send(msgs);
},
100
);
My receiving class does:
node.on("input", function(msg, send, done) {
try {
switch(msg.topic) {
case 'registerChild':
m.childRegistrationUtils.registerChild(msg.payload ,msg.positionVsParent);
break;
Etc.. but that code is all between try's and catches so nothing goes wrong there and I also see stuf happening in my debug statements so far so good.
This m stands for my machine node, calls a shared childRegistrationUtils script which calls the function registerChild and passes the child and the position of the child vs the parent. I have upStream and downStream variants.
My complete class:
// ChildRegistrationUtils.js
class ChildRegistrationUtils {
constructor(mainClass) {
this.mainClass = mainClass; // Reference to the main class
this.logger = mainClass.logger; // Access logger directly from main class
}
registerChild(child, positionVsParent) {
const { softwareType } = child.config.functionality;
const { name, id, unit } = child.config.general;
const { type, subType } = child.config.asset;
const emitter = child.emitter;
// Ensure nested structures
if (!this.mainClass.child) this.mainClass.child = {};
if (!this.mainClass.child[softwareType]) {
this.mainClass.child[softwareType] = {};
}
if (!this.mainClass.child[softwareType][type]) {
this.mainClass.child[softwareType][type] = {};
}
if (!this.mainClass.child[softwareType][type][subType]) {
this.mainClass.child[softwareType][type][subType] = [];
}
// Check if we already have an entry with the same 'id'
const existing = this.mainClass.child[softwareType][type][subType].filter(
(c) => c.id === id
);
if (existing.length > 0) {
this.logger.warn(
`Child with ID '${id}' already exists. Registering an additional instance anyway.`
);
}
// Push (add) the new child object to the array no matter what
this.mainClass.child[softwareType][type][subType].push({
name,
id,
unit,
emitter
});
// Then connect the child depending on its 'desc'
// We keep calling connectChild so each instance gets its own listener
this.connectChild(id, softwareType, emitter, type, child, subType, positionVsParent);
}
connectChild(id, desc, emitter, type, child, subType, positionVsParent) {
this.logger.debug(
`Connecting child id=${id}: desc=${desc}, type=${type}, subType=${subType}, position=${positionVsParent}`
);
switch (desc) {
case "measurement":
this.logger.debug(`Registering measurement child: ${id} with type=${type}`);
this.connectMeasurement(emitter, subType, positionVsParent);
break;
case "machine":
this.logger.debug(`Registering complete machine child: ${id}`);
this.connectMachine(child);
break;
default:
this.logger.error(`Unrecognized desc: ${desc}`);
}
}
connectMeasurement(emitter, subType, position) {
this.logger.debug(
`Connecting measurement child: ${subType} with position=${position}`
);
// Attach a new listener for every registration
emitter.on("mAbs", (value) => {
// Hard-coded debug to prove it's the second machine
if (this.mainClass.config.general.name === "m1") {
console.log("\n[M1 CALLBACK FIRED]", "value=", value);
}
if (this.mainClass.config.general.name === "m2") {
console.log("\n[M2 CALLBACK FIRED]", "value=", value);
}
this.logger.debug(`Checking position ${position} of measurement`);
if (position === "upStream") {
this.mainClass.measurements
.type(subType)
.variant("measured")
.setUpstream(value);
this.logger.debug(
` subType : ${subType} updated to ` +
this.mainClass.measurements.type(subType).variant("measured").getUpstream()
);
} else if (position === "downStream") {
this.mainClass.measurements
.type(subType)
.variant("measured")
.setDownstream(value);
this.logger.debug(
` subType : ${subType} updated to ` +
this.mainClass.measurements.type(subType).variant("measured").getDownstream()
);
} else {
this.logger.error(`Position ${position} not recognized!`);
}
this.mainClass.updateMeasurement(subType);
this.logger.debug("---- End of the emit function ----- ");
});
}
connectMachine(machine) {
if (!machine) {
this.logger.warn("Invalid machine provided.");
return;
}
// Example: you create a new machineId for each registration
const machineId = Object.keys(this.mainClass.machines).length + 1;
this.mainClass.machines[machineId] = machine;
this.mainClass.outputs.machineOutputs[machineId] = { flow: 0, power: 0 };
this.logger.info(`Initializing combination curves for machine ${machineId}`);
this.mainClass.updateDynamicTotals();
this.logger.info(`Setting up pressureChange listener for machine ${machineId}`);
machine.emitter.on("pressureChange", () => {
this.mainClass.handlePressureChange(machine);
});
this.logger.info(`Machine ${machineId} registered successfully.`);
}
}
module.exports = ChildRegistrationUtils;
I hardcoded the m1 and m2 name to match the node names just to see if it fires because in my own debug I can see the following:
[DEBUG] -> m2: Connecting child id=6aa12d48388bad0c: desc=measurement, type=sensor, subType=pressure, position=downStream
[DEBUG] -> m2: Registering measurement child: 6aa12d48388bad0c with type=sensor
[DEBUG] -> m2: Connecting measurement child: pressure with position=downStream
[DEBUG] -> m1: Connecting child id=6aa12d48388bad0c: desc=measurement, type=sensor, subType=pressure, position=downStream
[DEBUG] -> m1: Registering measurement child: 6aa12d48388bad0c with type=sensor
[DEBUG] -> m1: Connecting measurement child: pressure with position=downStream
[WARN] -> PT1: Simulated value 0 is outside of abs range constraining between min=800 and max=1500
[WARN] -> PT1: New value=0 is constrained to fit between min=800 and max=1500
[DEBUG] -> PT1: Applying smoothing method "none"
[WARN] -> PT1: Output value=788.889471739861 is outside of ABS range. Constraining.
[WARN] -> PT1: New value=788.889471739861 is constrained to fit between min=800 and max=1500
[DEBUG] -> PT1: Emitting mAbs=800
[M2 CALLBACK FIRED] value= 800
[DEBUG] -> m2: Checking position downStream of measurement
[DEBUG] -> m2: subType : pressure updated to 800
[DEBUG] -> m2: Pressure measurements - Upstream: null, Downstream: 800
[DEBUG] -> m2: Using downstream pressure: 800
[DEBUG] -> m2: ---- End of the emit function -----
[WARN] -> PT1: Simulated value 788.889471739861 is outside of abs range constraining between min=800 and max=1500
[WARN] -> PT1: New value=788.889471739861 is constrained to fit between min=800 and max=1500
[DEBUG] -> PT1: Applying smoothing method "none"
[DEBUG] -> PT1: Emitting mAbs=816
Here we see no M1 callback fired. The number of the node is not of importance I suspect its always only showing the first node that registered this child.
Perhaps overkill but if you want I can also share the class that holds the measurements:
class MeasurementContainer {
constructor() {
this.measurements = {};
}
type(mainKey) {
if (!this.measurements[mainKey]) {
this.measurements[mainKey] = {};
}
return new SubKeyHandler(this, mainKey);
}
_getOrCreateMeasurement(mainKey, subKey) {
if (!this.measurements[mainKey][subKey]) {
this.measurements[mainKey][subKey] = new MeasurementBuilder()
.setType(`${mainKey}-${subKey}`)
.build();
}
return this.measurements[mainKey][subKey];
}
getMeasurement(mainKey, subKey) {
return this.measurements[mainKey]?.[subKey];
}
removeMeasurement(mainKey, subKey) {
if (!this.measurements[mainKey]) return this;
if (subKey) {
delete this.measurements[mainKey][subKey];
if (Object.keys(this.measurements[mainKey]).length === 0) {
delete this.measurements[mainKey];
}
} else {
delete this.measurements[mainKey];
}
return this;
}
//returns difference between actual and predicted values
getDifferenceActualPredicted(mainKey) {
if (this.measurements[mainKey]) {
let subKeys = Object.keys(this.measurements[mainKey]);
if (subKeys.length == 2) {
let actual = this.measurements[mainKey][subKeys[0]].downstream;
let predicted = this.measurements[mainKey][subKeys[1]].downstream;
return actual - predicted;
}
else {
return null;
}
}
}
addMeasurement(mainKey, subKey, measurement) {
if (!this.measurements[mainKey]) {
this.measurements[mainKey] = {};
}
if (!(measurement instanceof Measurement)) {
measurement = new MeasurementBuilder()
.setType(`${mainKey}-${subKey}`)
.build();
}
this.measurements[mainKey][subKey] = measurement;
return this;
}
getSubKeys(mainKey) {
return this.measurements[mainKey] ? new Set(Object.keys(this.measurements[mainKey])) : new Set();
}
}
class SubKeyHandler {
constructor(container, mainKey) {
this.container = container;
this.mainKey = mainKey;
}
variant(subKey) {
return this.container._getOrCreateMeasurement(this.mainKey, subKey);
}
}
class Measurement {
constructor(type) {
this.type = type; // e.g. 'pressure', 'flow', etc.
this.upstream = null; // upstream measurement value
this.downstream = null; // downstream measurement value
}
// -- Updater methods --
setType(type) {
this.type = type;
return this;
}
setUpstream(upstream) {
this.upstream = upstream;
return this;
}
setDownstream(downstream) {
this.downstream = downstream;
return this;
}
hasDownUp() {
return this.upstream != null && this.downstream != null? true : false;
}
// -- Calculators --
getDifferential() {
if (this.valideDiff()) {
return this.downstream - this.upstream;
}
return null;
}
getDownstream() {
return this.downstream;
}
getUpstream() {
return this.upstream;
}
// helper functions
valideDiff() {
return this.upstream != null && this.downstream != null? true : false;
}
}
class MeasurementBuilder {
constructor() {
this.type = null;
}
setType(type) {
this.type = type;
return this;
}
build() {
return new Measurement(
this.type,
);
}
}
module.exports = MeasurementContainer;
/*
// -- Example usage --
// 1) Create a new container
const container = new MeasurementContainer();
// 2) Create or get a measurement with:
// - mainKey = "pressure"
// - subKey = "predicted"
// Then set upstream/downstream on that measurement.
container
.type("pressure") // Stage 1: mainKey
.variant("predicted") // Stage 2: subKey
.setUpstream(42)
console.log(
container
.type("pressure") // Stage 1: mainKey
.variant("predicted") // Stage 2: subKey
.getUpstream()
);
// Retrieve and log the differential
console.log("Predicted Pressure Differential:", container.getMeasurement("pressure","predicted").getDifferential());
console.log("Get difference between actual and predicted " , container.getDifferenceActualPredicted("pressure"));
// => 50 - 42 = 8
// 3) Do the same for "pressure" -> "actual"
container
.type("pressure")
.variant("predicted")
.setUpstream(45)
.setDownstream(49);
console.log("Predicted Pressure Differential:", container.getMeasurement("pressure","predicted").getDifferential());
// 3) Do the same for "pressure" -> "actual"
container
.type("pressure")
.variant("actual")
.setUpstream(45)
.setDownstream(49);
console.log("Predicted Pressure Differential:", container.getMeasurement("pressure","actual").getDifferential());
// => 49 - 45 = 4
// 4) Add a "temperature" measurement:
container
.type("temperature")
.variant("current")
.setUpstream(20)
.setDownstream(25);
const temperatureCurrent = container.getMeasurement("temperature", "current");
console.log("Current Temperature Differential:", temperatureCurrent.getDifferential());
// => 25 - 20 = 5
// 5) Check what sub-keys exist under "pressure"
const pressureVariants = container.getSubKeys("pressure");
console.log('Sub-keys for "pressure":', pressureVariants);
// => e.g. ["predicted", "actual"]
// 6) Remove one sub-key, e.g. "predicted"
container.removeMeasurement("pressure", "predicted");
console.log('After removal, sub-keys for "pressure":', container.getSubKeys("pressure"));
// => e.g. ["actual"]
// 7) (Optional) Remove entire "pressure" by omitting subKey
container.removeMeasurement("pressure");
console.log('After removing "pressure":', container.getSubKeys("pressure"));
// => []
// 8) We still have "temperature" -> "current" in the container.
console.log('Sub-keys for "temperature":', container.getSubKeys("temperature"));
// => ["current"]
//*/
And if needed I can share the rest of my functions of the machine node I made but really suspect Im not seeing something that's really clear for someone with a lot of years of javascript / node red coding.