Splits pumpingStation/src/ into focused concern modules. specificClass.js
will be slimmed to an orchestrator in P2.9 (integration); for now both
the inlined logic AND the new modules coexist so tests stay green
throughout.
src/basin/ BasinGeometry + thresholdValidator (pure)
src/measurement/ flowAggregator + measurementRouter + calibration
src/control/ levelBased + flowBased(stub) + manual + index dispatcher
src/safety/ safetyController split into dryRun + overfill rules
src/commands/ registry array + handlers (canonical names from start)
src/editor.js 260 lines of SVG basin-diagram redraw, was inline in .html
examples/standalone-demo.js was if(require.main===module) at bottom of specificClass.js
CONTRACT.md canonical inputs + outputs + emitted events
Modified:
src/specificClass.js removed the 170-line standalone demo block
pumpingStation.html oneditprepare/oneditsave delegate to editor.{init,save}
pumpingStation.js added admin endpoint serving src/editor.js
102 basic tests pass (60 new + 42 existing).
specificClass.js itself is unchanged in behaviour — integration is P2.9.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
2.7 KiB
JavaScript
88 lines
2.7 KiB
JavaScript
'use strict';
|
|
|
|
// Handler functions for pumpingStation commands. Each handler receives:
|
|
// source: the domain (specificClass) instance — has the public methods
|
|
// (changeMode, calibratePredicted*, setManualInflow, ...).
|
|
// msg: the Node-RED input message.
|
|
// ctx: { node, RED, send, logger } — provided by BaseNodeAdapter.
|
|
//
|
|
// Handlers are pure functions: they don't keep state. Validation that goes
|
|
// beyond the registry's typeof-check ladder lives here.
|
|
|
|
function _logger(source, ctx) {
|
|
return ctx?.logger || source?.logger || null;
|
|
}
|
|
|
|
exports.setMode = (source, msg) => {
|
|
source.changeMode(msg.payload);
|
|
};
|
|
|
|
exports.registerChild = (source, msg, ctx) => {
|
|
const log = _logger(source, ctx);
|
|
const childId = msg.payload;
|
|
const childObj = ctx?.RED?.nodes?.getNode?.(childId);
|
|
if (!childObj || !childObj.source) {
|
|
log?.warn?.(`registerChild: child '${childId}' not found or has no .source`);
|
|
return;
|
|
}
|
|
source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
|
|
};
|
|
|
|
exports.calibrateVolume = (source, msg, ctx) => {
|
|
const log = _logger(source, ctx);
|
|
const v = parseFloat(msg.payload);
|
|
if (!Number.isFinite(v)) {
|
|
log?.warn?.(`cmd.calibrate.volume: non-numeric payload '${msg.payload}'`);
|
|
return;
|
|
}
|
|
source.calibratePredictedVolume(v);
|
|
};
|
|
|
|
exports.calibrateLevel = (source, msg, ctx) => {
|
|
const log = _logger(source, ctx);
|
|
const v = parseFloat(msg.payload);
|
|
if (!Number.isFinite(v)) {
|
|
log?.warn?.(`cmd.calibrate.level: non-numeric payload '${msg.payload}'`);
|
|
return;
|
|
}
|
|
source.calibratePredictedLevel(v);
|
|
};
|
|
|
|
exports.setInflow = (source, msg) => {
|
|
// Payload is either a number (legacy q_in shape) or
|
|
// { value, unit, timestamp } (richer object form).
|
|
const p = msg.payload;
|
|
let value;
|
|
let unit;
|
|
let timestamp;
|
|
if (p !== null && typeof p === 'object') {
|
|
value = Number(p.value);
|
|
unit = p.unit;
|
|
timestamp = p.timestamp || Date.now();
|
|
} else {
|
|
value = Number(p);
|
|
unit = msg?.unit;
|
|
timestamp = msg?.timestamp || Date.now();
|
|
}
|
|
source.setManualInflow(value, timestamp, unit);
|
|
};
|
|
|
|
exports.setDemand = (source, msg, ctx) => {
|
|
const log = _logger(source, ctx);
|
|
const demand = Number(msg.payload);
|
|
if (!Number.isFinite(demand)) {
|
|
log?.warn?.(`set.demand: invalid Qd value '${msg.payload}'`);
|
|
return;
|
|
}
|
|
if (source.mode !== 'manual') {
|
|
log?.debug?.(
|
|
`set.demand ignored in '${source.mode}' mode; switch to manual to use the demand slider`
|
|
);
|
|
return;
|
|
}
|
|
// forwardDemandToChildren returns a promise — surface failures via logger.
|
|
Promise.resolve(source.forwardDemandToChildren(demand)).catch((err) => {
|
|
log?.error?.(`set.demand: failed to forward demand: ${err && err.message}`);
|
|
});
|
|
};
|