P5 wave 1: extract rotatingMachine concerns into focused modules
src/curves/ loader + normalizer (with cross-pressure anomaly
detection) + reverseCurve helper
src/prediction/ predictors (predictFlow/Power/Ctrl) +
groupPredictors (lazy group-scope views) +
OperatingPoint (pressure-driven prediction setpoints)
src/drift/ DriftAssessor (per-metric drift) + PredictionHealth
(composes flow/power/pressure into HealthStatus +
confidence sibling — see OPEN_QUESTIONS 2026-05-10)
src/pressure/ VirtualPressureChildren (dashboard-sim) +
PressureInitialization (real-vs-virtual tracking) +
PressureRouter (dispatches by position)
src/state/ stateBindings (state.emitter listener helper) +
isOperationalState
src/measurement/ measurementHandlers (dispatcher for flow/power/temp/pressure)
src/flow/ flowController (handleInput body — execSequence,
execMovement, flowMovement, emergencystop)
src/display/ workingCurves (showWorkingCurves + showCoG admin)
src/commands/ canonical names: set.mode, cmd.startup/shutdown/estop,
set.setpoint, set.flow-setpoint,
data.simulate-measurement, query.curves, query.cog,
child.register. execSequence demuxes by payload.action
to canonical cmd.* handlers.
CONTRACT.md inputs/outputs/events/children surface
110 basic tests pass (100 new + 10 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P5 wave 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
150
src/commands/handlers.js
Normal file
150
src/commands/handlers.js
Normal file
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
// Handler functions for rotatingMachine commands. Each handler receives:
|
||||
// source: the domain (specificClass) instance — exposes setMode, handleInput,
|
||||
// updateMeasured*, updateSimulatedMeasurement, isUnitValidForType,
|
||||
// showWorkingCurves, showCoG, childRegistrationUtils, logger.
|
||||
// msg: the Node-RED input message.
|
||||
// ctx: { node, RED, send, logger } — provided by BaseNodeAdapter.
|
||||
//
|
||||
// Pure functions: validation that goes beyond the registry's typeof-check
|
||||
// ladder lives here. Reply messages (query.*) use ctx.send when available.
|
||||
|
||||
const SUPPORTED_SIM_TYPES = new Set(['pressure', 'flow', 'temperature', 'power']);
|
||||
|
||||
function _logger(source, ctx) {
|
||||
return ctx?.logger || source?.logger || null;
|
||||
}
|
||||
|
||||
function _send(ctx, ports) {
|
||||
if (typeof ctx?.send === 'function') ctx.send(ports);
|
||||
}
|
||||
|
||||
exports.setMode = (source, msg) => {
|
||||
source.setMode(msg.payload);
|
||||
};
|
||||
|
||||
// Canonical execution handlers. The legacy execSequence demuxer below
|
||||
// forwards to these directly so behaviour is identical.
|
||||
exports.startup = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
await source.handleInput(p.source ?? 'parent', 'execSequence', 'startup');
|
||||
};
|
||||
|
||||
exports.shutdown = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
await source.handleInput(p.source ?? 'parent', 'execSequence', 'shutdown');
|
||||
};
|
||||
|
||||
exports.estop = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
// Legacy emergencystop carried { source, action } — action defaults to
|
||||
// 'emergencystop' when only source is supplied via the canonical topic.
|
||||
await source.handleInput(p.source ?? 'parent', p.action ?? 'emergencystop');
|
||||
};
|
||||
|
||||
// Content-based alias router: legacy `execSequence` carried payload.action in
|
||||
// {'startup','shutdown'}. We dispatch back into the canonical handler so the
|
||||
// behaviour and logs are identical regardless of which topic was used.
|
||||
exports.execSequenceAlias = async (source, msg, ctx) => {
|
||||
const log = _logger(source, ctx);
|
||||
const action = msg?.payload?.action;
|
||||
if (action === 'startup') return exports.startup(source, msg, ctx);
|
||||
if (action === 'shutdown') return exports.shutdown(source, msg, ctx);
|
||||
log?.warn?.(`execSequence: unsupported action '${action}'`);
|
||||
};
|
||||
|
||||
exports.setSetpoint = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
const action = p.action ?? 'execMovement';
|
||||
await source.handleInput(p.source ?? 'parent', action, Number(p.setpoint));
|
||||
};
|
||||
|
||||
exports.setFlowSetpoint = async (source, msg) => {
|
||||
const p = msg.payload || {};
|
||||
const action = p.action ?? 'flowMovement';
|
||||
await source.handleInput(p.source ?? 'parent', action, Number(p.setpoint));
|
||||
};
|
||||
|
||||
exports.simulateMeasurement = (source, msg, ctx) => {
|
||||
const log = _logger(source, ctx);
|
||||
const payload = msg.payload || {};
|
||||
const type = String(payload.type || '').toLowerCase();
|
||||
const position = payload.position || 'atEquipment';
|
||||
const value = Number(payload.value);
|
||||
const unit = typeof payload.unit === 'string' ? payload.unit.trim() : '';
|
||||
const context = {
|
||||
timestamp: payload.timestamp || Date.now(),
|
||||
unit,
|
||||
childName: 'dashboard-sim',
|
||||
childId: 'dashboard-sim',
|
||||
};
|
||||
|
||||
if (!Number.isFinite(value)) {
|
||||
log?.warn?.('simulateMeasurement payload.value must be a finite number');
|
||||
return;
|
||||
}
|
||||
if (!SUPPORTED_SIM_TYPES.has(type)) {
|
||||
log?.warn?.(`Unsupported simulateMeasurement type: ${type}`);
|
||||
return;
|
||||
}
|
||||
if (!unit) {
|
||||
log?.warn?.('simulateMeasurement payload.unit is required');
|
||||
return;
|
||||
}
|
||||
if (typeof source.isUnitValidForType === 'function' &&
|
||||
!source.isUnitValidForType(type, unit)) {
|
||||
log?.warn?.(`simulateMeasurement payload.unit '${unit}' is invalid for type '${type}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
_dispatchSimulated(source, type, position, value, context);
|
||||
};
|
||||
|
||||
function _dispatchSimulated(source, type, position, value, context) {
|
||||
switch (type) {
|
||||
case 'pressure':
|
||||
if (typeof source.updateSimulatedMeasurement === 'function') {
|
||||
source.updateSimulatedMeasurement(type, position, value, context);
|
||||
} else {
|
||||
source.updateMeasuredPressure(value, position, context);
|
||||
}
|
||||
return;
|
||||
case 'flow':
|
||||
source.updateMeasuredFlow(value, position, context);
|
||||
return;
|
||||
case 'temperature':
|
||||
source.updateMeasuredTemperature(value, position, context);
|
||||
return;
|
||||
case 'power':
|
||||
source.updateMeasuredPower(value, position, context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
exports.queryCurves = (source, msg, ctx) => {
|
||||
const reply = Object.assign({}, msg, {
|
||||
topic: 'showWorkingCurves',
|
||||
payload: source.showWorkingCurves(),
|
||||
});
|
||||
_send(ctx, [reply, null, null]);
|
||||
};
|
||||
|
||||
exports.queryCog = (source, msg, ctx) => {
|
||||
const reply = Object.assign({}, msg, {
|
||||
topic: 'showCoG',
|
||||
payload: source.showCoG(),
|
||||
});
|
||||
_send(ctx, [reply, null, null]);
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
85
src/commands/index.js
Normal file
85
src/commands/index.js
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
// rotatingMachine command registry. Consumed by BaseNodeAdapter via
|
||||
// `static commands = require('./commands')`. Each descriptor maps a
|
||||
// canonical msg.topic to its handler; legacy names are listed under
|
||||
// `aliases` and emit a one-time deprecation warning at runtime.
|
||||
//
|
||||
// `execSequence` is special: the legacy payload carried `{source, action,
|
||||
// parameter}` where `action` selected the canonical verb (startup /
|
||||
// shutdown). The registry does not natively dispatch by payload content,
|
||||
// so we keep `execSequence` as its own descriptor whose handler routes to
|
||||
// the canonical `cmd.startup` / `cmd.shutdown` handler. Behaviour matches
|
||||
// the canonical topics exactly; the deprecation warning still fires once.
|
||||
|
||||
const handlers = require('./handlers');
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
topic: 'set.mode',
|
||||
aliases: ['setMode'],
|
||||
payloadSchema: { type: 'string' },
|
||||
handler: handlers.setMode,
|
||||
},
|
||||
{
|
||||
topic: 'cmd.startup',
|
||||
payloadSchema: { type: 'any' },
|
||||
handler: handlers.startup,
|
||||
},
|
||||
{
|
||||
topic: 'cmd.shutdown',
|
||||
payloadSchema: { type: 'any' },
|
||||
handler: handlers.shutdown,
|
||||
},
|
||||
{
|
||||
topic: 'cmd.estop',
|
||||
aliases: ['emergencystop'],
|
||||
payloadSchema: { type: 'any' },
|
||||
handler: handlers.estop,
|
||||
},
|
||||
{
|
||||
// Legacy umbrella topic. Content-based demux inside the handler routes
|
||||
// to the canonical startup / shutdown logic. Emits the registry's
|
||||
// one-time deprecation warning the first time it fires.
|
||||
topic: 'execSequence',
|
||||
payloadSchema: { type: 'object' },
|
||||
handler: handlers.execSequenceAlias,
|
||||
_legacy: true,
|
||||
},
|
||||
{
|
||||
topic: 'set.setpoint',
|
||||
aliases: ['execMovement'],
|
||||
payloadSchema: { type: 'object' },
|
||||
handler: handlers.setSetpoint,
|
||||
},
|
||||
{
|
||||
topic: 'set.flow-setpoint',
|
||||
aliases: ['flowMovement'],
|
||||
payloadSchema: { type: 'object' },
|
||||
handler: handlers.setFlowSetpoint,
|
||||
},
|
||||
{
|
||||
topic: 'data.simulate-measurement',
|
||||
aliases: ['simulateMeasurement'],
|
||||
payloadSchema: { type: 'object' },
|
||||
handler: handlers.simulateMeasurement,
|
||||
},
|
||||
{
|
||||
topic: 'query.curves',
|
||||
aliases: ['showWorkingCurves'],
|
||||
payloadSchema: { type: 'any' },
|
||||
handler: handlers.queryCurves,
|
||||
},
|
||||
{
|
||||
topic: 'query.cog',
|
||||
aliases: ['CoG'],
|
||||
payloadSchema: { type: 'any' },
|
||||
handler: handlers.queryCog,
|
||||
},
|
||||
{
|
||||
topic: 'child.register',
|
||||
aliases: ['registerChild'],
|
||||
payloadSchema: { type: 'string' },
|
||||
handler: handlers.registerChild,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user