'use strict'; /** * PressureRouter — routes a measured pressure value into the right * MeasurementContainer slot and triggers the downstream cascade * (preferred-pressure resolve → predicted recompute → drift → health) * on every pressure write, matching the pre-refactor * `updateMeasuredPressure` semantics. * * Why the cascade runs for virtual sources too: dashboard-sim pressure * sliders route through virtual children, and the operator expects the * predicted flow/power/efficiency/Cog to refresh on every slider tick. * The cascade is idempotent — running it on a virtual write is cheap * and matches what a real sensor would trigger. * * Why getPressure() runs first: getMeasuredPressure() writes the new * pressure differential onto predictFlow/Power/Ctrl.fDimension. Only * after that does updatePosition() compute flow/power via * predictFlow.y(x) — otherwise calcFlowPower runs against a stale * fDimension and the prediction lags one update behind the slider. */ class PressureRouter { /** * @param {object} ctx * - measurements: MeasurementContainer * - virtualPressureChildIds: { upstream, downstream } (kept for debug only) * - resolveMeasurementUnit(type, unit) -> canonical unit string (throws on invalid) * - getPressure?(): resolves preferred pressure and pushes fDimension to predictors * - updatePosition?(): recomputes predicted flow/power/efficiency/CoG at current ctrl * - refreshDrift?(): refreshes pressure drift status * - refreshHealth?(): refreshes prediction-health status * - logger */ constructor(ctx = {}) { this.measurements = ctx.measurements; this.virtualPressureChildIds = ctx.virtualPressureChildIds || {}; this.resolveMeasurementUnit = ctx.resolveMeasurementUnit || ((_t, u) => u); this.getPressure = ctx.getPressure; this.updatePosition = ctx.updatePosition; this.refreshDrift = ctx.refreshDrift; this.refreshHealth = ctx.refreshHealth; this.logger = ctx.logger || { warn() {}, debug() {} }; } /** * Route a measured pressure to the right container slot. * @returns {boolean} true on successful write, false on rejection. */ route(position, value, context = {}) { const pos = String(position || '').toLowerCase(); const childId = context.childId; let unit; try { unit = this.resolveMeasurementUnit('pressure', context.unit); } catch (err) { this.logger.warn(`Rejected pressure update: ${err.message}`); return false; } this.measurements ?.type('pressure').variant('measured').position(pos).child(childId) .value(value, context.timestamp, unit); const isVirtual = this._isVirtual(childId); this.logger.debug(`Pressure routed: ${value} ${unit} at ${pos} from ${context.childName || 'child'} (${childId || 'unknown-id'}) virtual=${isVirtual}`); // Legacy order: resolve preferred pressure (writes fDimension to // predictors) BEFORE recomputing predicted flow/power at the current // control position. Skipping any of these on virtual sources broke // the dashboard-sim demo (NCog / efficiency / absDistFromPeak stuck // at 0, predicted flow/power not updating with the pressure slider). let p; if (typeof this.getPressure === 'function') { p = this.getPressure(); this.logger.debug(`Using pressure: ${p} for calculations`); } if (typeof this.updatePosition === 'function') this.updatePosition(); if (typeof this.refreshDrift === 'function') this.refreshDrift(); if (typeof this.refreshHealth === 'function') this.refreshHealth(); return true; } _isVirtual(childId) { if (childId == null) return false; for (const id of Object.values(this.virtualPressureChildIds)) { if (id === childId) return true; } return false; } } module.exports = PressureRouter;