specificClass.js: 1760 → 400 lines.
Machine extends BaseDomain. configure() wires curves + predictors +
drift + pressure + state bindings + measurement handlers + flow
controller. ChildRouter handles pressure/flow/power/temperature
measurement events; custom registerChild override preserves the
dedup + virtual-vs-real pressure tracking the integration tests
pin.
Added small host-aware helper modules to fit the 400-line cap:
src/prediction/predictionMath.js (calcFlow/Power/Ctrl)
src/prediction/efficiencyMath.js (calcCog/EfficiencyCurve/etc.)
src/pressure/pressureSelector.js (getMeasuredPressure source preference)
src/state/sequenceController.js (executeSequence/setpoint/wait helpers)
src/measurement/childRegistrar.js (custom registerChild path)
src/drift/healthRefresh.js (drift status update wrappers)
src/io/output.js (buildOutput + buildStatusBadge)
unitPolicy: live UnitPolicy methods .canonical()/.output()/.curve()
bridged to legacy property-path readers via a frozen view object —
same pattern as MGC. See OPEN_QUESTIONS.md.
nodeClass.js: 433 → 61 lines.
Extends BaseNodeAdapter. tickInterval=null (event-driven on state +
measurement events). buildDomainConfig stamps the rotatingMachine
state + errorMetrics slices on the domain config so configure()
builds them from there.
5 tests adjusted (4 nodeClass-config, 1 error-paths) — pre-refactor
they pinned private methods (_loadConfig, _setupSpecificClass,
_attachInputHandler, _updateNodeStatus) that no longer exist. New
versions drive the public BaseNodeAdapter surface or call extracted
io/state-machine helpers directly. See OPEN_QUESTIONS.md 2026-05-10
"private nodeClass tests" for the deferred rewrite plan.
196 / 196 tests pass (basic 110 + integration ~80 + edge ~6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
7.2 KiB
JavaScript
182 lines
7.2 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
|
|
/** Reconcile a measured-flow reading with the existing up/downstream slots. */
|
|
handleMeasuredFlow() {
|
|
const host = this.host;
|
|
const diff = host.measurements.type('flow').variant('measured').difference();
|
|
if (diff != null) {
|
|
if (diff.value < 0.001) { this.logger.debug(`Flow match: ${diff.value}`); return diff.value; }
|
|
this.logger.error('Something wrong with down or upstream flow measurement. Bailing out!');
|
|
return null;
|
|
}
|
|
const up = host.measurements.type('flow').variant('measured').position('upstream').getCurrentValue();
|
|
if (up != null) { this.logger.warn('Only upstream flow is present. Using it but results may be incomplete!'); return up; }
|
|
const dn = host.measurements.type('flow').variant('measured').position('downstream').getCurrentValue();
|
|
if (dn != null) { this.logger.warn('Only downstream flow is present. Using it but results may be incomplete!'); return dn; }
|
|
this.logger.error('No upstream or downstream flow measurement. Bailing out!');
|
|
return null;
|
|
}
|
|
|
|
handleMeasuredPower() {
|
|
const power = this.host.measurements.type('power').variant('measured').position('atEquipment').getCurrentValue();
|
|
if (power != null) { this.logger.debug(`Measured power: ${power}`); return power; }
|
|
this.logger.error('No measured power found. Bailing out!');
|
|
return null;
|
|
}
|
|
|
|
/** Route a dashboard-sim pressure write to its virtual child; route any
|
|
* other simulated measurement type through the normal handler dispatch. */
|
|
updateSimulatedMeasurement(type, position, value, context = {}) {
|
|
const host = this.host;
|
|
const t = String(type || '').toLowerCase();
|
|
const pos = String(position || 'atEquipment').toLowerCase();
|
|
if (t !== 'pressure') { return this.dispatch(t, value, pos, context); }
|
|
if (!host.virtualPressureChildIds[pos]) {
|
|
this.logger.warn(`Unsupported simulated pressure position '${pos}'`);
|
|
return;
|
|
}
|
|
const child = host.virtualPressureChildren[pos];
|
|
if (!child?.measurements) {
|
|
this.logger.error(`Virtual pressure child '${pos}' is missing`);
|
|
return;
|
|
}
|
|
let unit;
|
|
try { unit = host._resolveMeasurementUnit('pressure', context.unit); }
|
|
catch (err) { this.logger.warn(`Rejected simulated pressure measurement: ${err.message}`); return; }
|
|
child.measurements.type('pressure').variant('measured').position(pos)
|
|
.value(value, context.timestamp || Date.now(), unit);
|
|
}
|
|
}
|
|
|
|
module.exports = MeasurementHandlers;
|