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:
134
src/measurement/measurementHandlers.js
Normal file
134
src/measurement/measurementHandlers.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Centralised measurement update routing for rotatingMachine.
|
||||
*
|
||||
* Wraps the four measurement types coming from child measurement nodes
|
||||
* (flow / power / temperature / pressure) and dispatches each to the
|
||||
* appropriate handler. Pressure is delegated to the host's pressureRouter
|
||||
* (built in P5.4); the other three are normalised + written + drift-tracked
|
||||
* here.
|
||||
*
|
||||
* The handlers reach back into the host for `_resolveMeasurementUnit`,
|
||||
* `_updateMetricDrift`, `_updatePredictionHealth`, `updatePosition` and the
|
||||
* measurements container. Behaviour is preserved 1:1 from the original
|
||||
* specificClass methods.
|
||||
*/
|
||||
|
||||
class MeasurementHandlers {
|
||||
constructor(ctx) {
|
||||
if (!ctx || !ctx.host) {
|
||||
throw new Error('MeasurementHandlers: ctx.host is required');
|
||||
}
|
||||
this.host = ctx.host;
|
||||
this.logger = ctx.logger || ctx.host.logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single entry point used by child-measurement event listeners.
|
||||
* Unknown types warn and fall back to a no-op position refresh so a
|
||||
* mis-configured child can't silently break the FSM tick.
|
||||
*/
|
||||
dispatch(measurementType, value, position, context = {}) {
|
||||
switch (measurementType) {
|
||||
case 'pressure':
|
||||
return this.host.updateMeasuredPressure(value, position, context);
|
||||
case 'flow':
|
||||
return this.updateMeasuredFlow(value, position, context);
|
||||
case 'power':
|
||||
return this.updateMeasuredPower(value, position, context);
|
||||
case 'temperature':
|
||||
return this.updateMeasuredTemperature(value, position, context);
|
||||
default:
|
||||
this.logger.warn(`No handler for measurement type: ${measurementType}`);
|
||||
return this.host.updatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
updateMeasuredTemperature(value, position, context = {}) {
|
||||
const host = this.host;
|
||||
this.logger.debug(
|
||||
`Temperature update: ${value} at ${position} from ${context.childName || 'child'} (${context.childId || 'unknown-id'})`,
|
||||
);
|
||||
let unit;
|
||||
try {
|
||||
unit = host._resolveMeasurementUnit('temperature', context.unit);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Rejected temperature update: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
host.measurements
|
||||
.type('temperature')
|
||||
.variant('measured')
|
||||
.position(position || 'atEquipment')
|
||||
.child(context.childId)
|
||||
.value(value, context.timestamp, unit);
|
||||
}
|
||||
|
||||
updateMeasuredFlow(value, position, context = {}) {
|
||||
const host = this.host;
|
||||
if (!host._isOperationalState()) {
|
||||
this.logger.warn(`Machine not operational, skipping flow update from ${context.childName || 'unknown'}`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug(`Flow update: ${value} at ${position} from ${context.childName || 'child'}`);
|
||||
let unit;
|
||||
try {
|
||||
unit = host._resolveMeasurementUnit('flow', context.unit);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Rejected flow update: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
host.measurements
|
||||
.type('flow').variant('measured').position(position).child(context.childId)
|
||||
.value(value, context.timestamp, unit);
|
||||
|
||||
if (host.predictFlow) {
|
||||
const canonical = host.unitPolicy.canonical.flow;
|
||||
const predicted = host.predictFlow.outputY || 0;
|
||||
host.measurements.type('flow').variant('predicted').position('downstream')
|
||||
.value(predicted, Date.now(), canonical);
|
||||
host.measurements.type('flow').variant('predicted').position('atEquipment')
|
||||
.value(predicted, Date.now(), canonical);
|
||||
}
|
||||
|
||||
const measuredCanonical = host.measurements
|
||||
.type('flow').variant('measured').position(position)
|
||||
.getCurrentValue(host.unitPolicy.canonical.flow);
|
||||
|
||||
host._updateMetricDrift('flow', measuredCanonical, context);
|
||||
host._updatePredictionHealth();
|
||||
}
|
||||
|
||||
updateMeasuredPower(value, position, context = {}) {
|
||||
const host = this.host;
|
||||
if (!host._isOperationalState()) {
|
||||
this.logger.warn(`Machine not operational, skipping power update from ${context.childName || 'unknown'}`);
|
||||
return;
|
||||
}
|
||||
this.logger.debug(`Power update: ${value} at ${position} from ${context.childName || 'child'}`);
|
||||
let unit;
|
||||
try {
|
||||
unit = host._resolveMeasurementUnit('power', context.unit);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Rejected power update: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
host.measurements
|
||||
.type('power').variant('measured').position(position).child(context.childId)
|
||||
.value(value, context.timestamp, unit);
|
||||
|
||||
if (host.predictPower) {
|
||||
host.measurements.type('power').variant('predicted').position('atEquipment')
|
||||
.value(host.predictPower.outputY || 0, Date.now(), host.unitPolicy.canonical.power);
|
||||
}
|
||||
|
||||
const measuredCanonical = host.measurements
|
||||
.type('power').variant('measured').position(position)
|
||||
.getCurrentValue(host.unitPolicy.canonical.power);
|
||||
|
||||
host._updateMetricDrift('power', measuredCanonical, context);
|
||||
host._updatePredictionHealth();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MeasurementHandlers;
|
||||
Reference in New Issue
Block a user