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:
100
src/pressure/pressureInitialization.js
Normal file
100
src/pressure/pressureInitialization.js
Normal file
@@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* PressureInitialization — tracks real pressure children per position
|
||||
* and reports the overall pressure-input status (initialized, has
|
||||
* differential, preferred source).
|
||||
*
|
||||
* Extracted from rotatingMachine specificClass.getPressureInitializationStatus
|
||||
* + the realPressureChildIds set tracking.
|
||||
*/
|
||||
|
||||
class PressureInitialization {
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* - measurements: MeasurementContainer
|
||||
* - virtualPressureChildIds: { upstream, downstream }
|
||||
* - realPressureChildIds?: { upstream: Set<string>, downstream: Set<string> }
|
||||
* - logger
|
||||
*/
|
||||
constructor(ctx = {}) {
|
||||
this.measurements = ctx.measurements;
|
||||
this.virtualPressureChildIds = ctx.virtualPressureChildIds || {};
|
||||
this.realPressureChildIds = ctx.realPressureChildIds || {
|
||||
upstream: new Set(),
|
||||
downstream: new Set(),
|
||||
};
|
||||
this.logger = ctx.logger || { warn() {}, debug() {} };
|
||||
}
|
||||
|
||||
registerReal(position, childId) {
|
||||
const pos = this._normPosition(position);
|
||||
if (!this.realPressureChildIds[pos]) this.realPressureChildIds[pos] = new Set();
|
||||
this.realPressureChildIds[pos].add(childId);
|
||||
}
|
||||
|
||||
unregisterReal(position, childId) {
|
||||
const pos = this._normPosition(position);
|
||||
if (this.realPressureChildIds[pos]) this.realPressureChildIds[pos].delete(childId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ hasUpstream, hasDownstream, hasDifferential, initialized, source }}
|
||||
* source ∈ 'differential' | 'upstream' | 'downstream' | null.
|
||||
* Matches the original getPressureInitializationStatus() shape.
|
||||
*/
|
||||
getStatus() {
|
||||
const upstream = this._getPreferred('upstream');
|
||||
const downstream = this._getPreferred('downstream');
|
||||
const hasUpstream = upstream != null;
|
||||
const hasDownstream = downstream != null;
|
||||
const hasDifferential = hasUpstream && hasDownstream;
|
||||
|
||||
let source = null;
|
||||
if (hasDifferential) source = 'differential';
|
||||
else if (hasDownstream) source = 'downstream';
|
||||
else if (hasUpstream) source = 'upstream';
|
||||
|
||||
return {
|
||||
hasUpstream,
|
||||
hasDownstream,
|
||||
hasDifferential,
|
||||
initialized: hasUpstream || hasDownstream,
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred pressure value at a position. Real children win
|
||||
* over virtual; final fallback is the bare (position-only) container slot.
|
||||
*/
|
||||
getPreferredValue(position) {
|
||||
return this._getPreferred(this._normPosition(position));
|
||||
}
|
||||
|
||||
_getPreferred(position) {
|
||||
const realIds = Array.from(this.realPressureChildIds[position] || []);
|
||||
for (const id of realIds) {
|
||||
const v = this._readChild(position, id);
|
||||
if (v != null) return v;
|
||||
}
|
||||
const virtualId = this.virtualPressureChildIds[position];
|
||||
if (virtualId) {
|
||||
const v = this._readChild(position, virtualId);
|
||||
if (v != null) return v;
|
||||
}
|
||||
return this.measurements
|
||||
?.type('pressure').variant('measured').position(position).getCurrentValue();
|
||||
}
|
||||
|
||||
_readChild(position, childId) {
|
||||
return this.measurements
|
||||
?.type('pressure').variant('measured').position(position).child(childId).getCurrentValue();
|
||||
}
|
||||
|
||||
_normPosition(position) {
|
||||
return String(position || '').toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PressureInitialization;
|
||||
80
src/pressure/pressureRouter.js
Normal file
80
src/pressure/pressureRouter.js
Normal file
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* PressureRouter — routes a measured pressure value into the right
|
||||
* MeasurementContainer slot and triggers downstream side-effects
|
||||
* (position recompute + drift/health refresh) only when the source
|
||||
* is a real child (not a dashboard-sim virtual one).
|
||||
*
|
||||
* Extracted from rotatingMachine specificClass.updateMeasuredPressure.
|
||||
*/
|
||||
|
||||
class PressureRouter {
|
||||
/**
|
||||
* @param {object} ctx
|
||||
* - measurements: MeasurementContainer
|
||||
* - virtualPressureChildIds: { upstream, downstream }
|
||||
* - resolveMeasurementUnit(type, unit) -> canonical unit string (throws on invalid)
|
||||
* - updatePosition?(): called after a real-source write
|
||||
* - refreshDrift?(): called after a real-source write (e.g. _updatePressureDriftStatus)
|
||||
* - refreshHealth?(): called after a real-source write (e.g. _updatePredictionHealth)
|
||||
* - getPressure?(): optional, returns the current preferred pressure (for logging)
|
||||
* - logger
|
||||
*/
|
||||
constructor(ctx = {}) {
|
||||
this.measurements = ctx.measurements;
|
||||
this.virtualPressureChildIds = ctx.virtualPressureChildIds || {};
|
||||
this.resolveMeasurementUnit = ctx.resolveMeasurementUnit || ((_t, u) => u);
|
||||
this.updatePosition = ctx.updatePosition;
|
||||
this.refreshDrift = ctx.refreshDrift;
|
||||
this.refreshHealth = ctx.refreshHealth;
|
||||
this.getPressure = ctx.getPressure;
|
||||
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}`);
|
||||
|
||||
if (!isVirtual) {
|
||||
if (typeof this.updatePosition === 'function') this.updatePosition();
|
||||
if (typeof this.refreshDrift === 'function') this.refreshDrift();
|
||||
if (typeof this.refreshHealth === 'function') this.refreshHealth();
|
||||
}
|
||||
|
||||
if (typeof this.getPressure === 'function') {
|
||||
const p = this.getPressure();
|
||||
this.logger.debug(`Using pressure: ${p} for calculations`);
|
||||
}
|
||||
|
||||
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;
|
||||
92
src/pressure/virtualChildren.js
Normal file
92
src/pressure/virtualChildren.js
Normal file
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
const { MeasurementContainer } = require('generalFunctions');
|
||||
|
||||
/**
|
||||
* VirtualPressureChildren — builds two dashboard-sim children backed
|
||||
* by their own MeasurementContainer (upstream + downstream). Children
|
||||
* are signed as belonging to a parent machine via `setParentRef`.
|
||||
*
|
||||
* Extracted from rotatingMachine specificClass._initVirtualPressureChildren.
|
||||
*/
|
||||
|
||||
const DEFAULT_IDS = {
|
||||
upstream: 'dashboard-sim-upstream',
|
||||
downstream: 'dashboard-sim-downstream',
|
||||
};
|
||||
|
||||
class VirtualPressureChildren {
|
||||
/**
|
||||
* @param {object} opts
|
||||
* - logger: pass-through to MeasurementContainer
|
||||
* - unitPolicy: { canonical, output }
|
||||
* - parentRef: object to use as parent for setParentRef (optional)
|
||||
* - ids: override the default { upstream, downstream } id pair (optional)
|
||||
*/
|
||||
constructor({ logger, unitPolicy, parentRef = null, ids = DEFAULT_IDS } = {}) {
|
||||
this.logger = logger || { warn() {}, debug() {} };
|
||||
this.unitPolicy = unitPolicy;
|
||||
this.parentRef = parentRef;
|
||||
this.ids = { ...DEFAULT_IDS, ...(ids || {}) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ upstream: VirtualChild, downstream: VirtualChild }}
|
||||
* Each child = { config: { general, functionality, asset }, measurements }.
|
||||
*/
|
||||
build() {
|
||||
return {
|
||||
upstream: this._createChild('upstream'),
|
||||
downstream: this._createChild('downstream'),
|
||||
};
|
||||
}
|
||||
|
||||
_createChild(position) {
|
||||
const id = this.ids[position];
|
||||
const name = `dashboard-sim-${position}`;
|
||||
const measurements = new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
defaultUnits: this._unitMap('output'),
|
||||
preferredUnits: this._unitMap('output'),
|
||||
canonicalUnits: this.unitPolicy?.canonical,
|
||||
storeCanonical: true,
|
||||
strictUnitValidation: true,
|
||||
throwOnInvalidUnit: true,
|
||||
requireUnitForTypes: ['pressure'],
|
||||
}, this.logger);
|
||||
|
||||
if (typeof measurements.setChildId === 'function') measurements.setChildId(id);
|
||||
if (typeof measurements.setChildName === 'function') measurements.setChildName(name);
|
||||
if (this.parentRef && typeof measurements.setParentRef === 'function') {
|
||||
measurements.setParentRef(this.parentRef);
|
||||
}
|
||||
|
||||
return {
|
||||
config: {
|
||||
general: { id, name },
|
||||
functionality: {
|
||||
softwareType: 'measurement',
|
||||
positionVsParent: position,
|
||||
},
|
||||
asset: {
|
||||
type: 'pressure',
|
||||
unit: this.unitPolicy?.output?.pressure,
|
||||
},
|
||||
},
|
||||
measurements,
|
||||
};
|
||||
}
|
||||
|
||||
_unitMap(section) {
|
||||
const src = this.unitPolicy?.[section] || {};
|
||||
return {
|
||||
pressure: src.pressure,
|
||||
flow: src.flow,
|
||||
power: src.power,
|
||||
temperature: src.temperature,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
VirtualPressureChildren.DEFAULT_IDS = DEFAULT_IDS;
|
||||
module.exports = VirtualPressureChildren;
|
||||
Reference in New Issue
Block a user