/** * 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;