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>
95 lines
6.3 KiB
Markdown
95 lines
6.3 KiB
Markdown
# rotatingMachine — Contract
|
|
|
|
Hand-maintained for Phase 5; the `## Inputs` table is generated from
|
|
`src/commands/index.js` (see Phase 9 generator). Keep ≤ 100 lines.
|
|
|
|
## Inputs (msg.topic on Port 0)
|
|
|
|
| Canonical | Aliases (deprecated) | Payload | Effect |
|
|
|---|---|---|---|
|
|
| `set.mode` | `setMode` | `string` — one of the allowed mode names | Calls `source.setMode(payload)`. |
|
|
| `cmd.startup` | — | `{ source?: string }` | Calls `source.handleInput(payload.source ?? 'parent', 'execSequence', 'startup')`. |
|
|
| `cmd.shutdown` | — | `{ source?: string }` | Calls `source.handleInput(payload.source ?? 'parent', 'execSequence', 'shutdown')`. |
|
|
| `cmd.estop` | `emergencystop` | `{ source?: string, action?: string }` | Calls `source.handleInput(payload.source ?? 'parent', payload.action ?? 'emergencystop')`. |
|
|
| `execSequence` | — (legacy umbrella) | `{ source, action, parameter }` with `action ∈ {'startup','shutdown'}` | Content-based router: forwards to `cmd.startup` / `cmd.shutdown` handler based on `payload.action`. Unknown action logs `warn` and is dropped. Whole topic is legacy — prefer the canonical `cmd.*` topics. |
|
|
| `set.setpoint` | `execMovement` | `{ source, action, setpoint }` — setpoint coerced to `Number` | Calls `source.handleInput(payload.source ?? 'parent', payload.action ?? 'execMovement', Number(payload.setpoint))`. |
|
|
| `set.flow-setpoint` | `flowMovement` | `{ source, action, setpoint }` | Calls `source.handleInput(payload.source ?? 'parent', payload.action ?? 'flowMovement', Number(payload.setpoint))`. |
|
|
| `data.simulate-measurement` | `simulateMeasurement` | `{ type, position?, value, unit, timestamp? }` — `type ∈ {pressure, flow, temperature, power}`; `position` defaults to `'atEquipment'` | Validated dispatch: rejects non-finite `value`, unsupported `type`, missing `unit`, or unit that fails `isUnitValidForType`. Pressure routes via `updateSimulatedMeasurement(type, position, value, ctx)`; flow/temperature/power route via `updateMeasured<Type>(value, position, ctx)`. The injected `childId/childName = 'dashboard-sim'` marks the source. |
|
|
| `query.curves` | `showWorkingCurves` | none | Calls `source.showWorkingCurves()` and replies on **Port 0** with `{ topic: 'showWorkingCurves', payload: <result> }` via `ctx.send`. |
|
|
| `query.cog` | `CoG` | none | Calls `source.showCoG()` and replies on **Port 0** with `{ topic: 'showCoG', payload: <result> }`. |
|
|
| `child.register` | `registerChild` | `string` — child Node-RED id; `msg.positionVsParent` carries position | Resolves child via `RED.nodes.getNode(payload)` and registers it through `childRegistrationUtils.registerChild(child.source, msg.positionVsParent)`. Unknown ids log `warn`. |
|
|
|
|
Aliases log a one-time deprecation warning the first time they fire.
|
|
|
|
### `execSequence` demux
|
|
|
|
The pre-refactor topic `execSequence` carried `{ source, action, parameter }`
|
|
where `action` selected the verb (`startup` or `shutdown`). The command
|
|
registry does not natively dispatch by payload content, so `execSequence`
|
|
keeps its own descriptor whose handler **forwards directly** to the
|
|
canonical `cmd.startup` / `cmd.shutdown` handler based on
|
|
`payload.action`. The deprecation warning fires once. Future-Phase-7
|
|
removal of `execSequence` is a behavioural change — callers must migrate
|
|
to `cmd.startup` / `cmd.shutdown`.
|
|
|
|
## Outputs (msg.topic on Port 0/1/2)
|
|
|
|
- **Port 0 (process):** `msg.topic = config.general.name`. Payload built by
|
|
`outputUtils.formatMsg(..., 'process')` from `getOutput()` — delta-compressed
|
|
(only changed fields are emitted). On `query.curves` / `query.cog` the
|
|
node additionally emits `{ topic: 'showWorkingCurves' | 'showCoG',
|
|
payload: <result> }` as a synchronous reply on Port 0.
|
|
- **Port 1 (InfluxDB telemetry):** same shape as Port 0, formatted with the
|
|
`'influxdb'` formatter.
|
|
- **Port 2 (registration):** at startup the node sends one
|
|
`{ topic: 'registerChild', payload: <node.id>, positionVsParent }` to
|
|
the upstream parent (typically a `machineGroupControl` or
|
|
`pumpingStation`). `positionVsParent` defaults to `'atEquipment'`.
|
|
|
|
## Events emitted by `source.measurements.emitter`
|
|
|
|
The `MeasurementContainer` fires `<type>.<variant>.<position>` whenever
|
|
the corresponding series receives a new value. Parents subscribe via the
|
|
generic `child.measurements.emitter.on(eventName, ...)` handshake.
|
|
rotatingMachine publishes:
|
|
|
|
- `flow.predicted.atequipment`, `flow.predicted.downstream`,
|
|
`flow.predicted.max`, `flow.predicted.min` — predicted operating point.
|
|
- `power.predicted.atequipment` — predicted shaft power.
|
|
- `temperature.measured.atequipment` — ambient/process temperature.
|
|
- `atmPressure.measured.atequipment` — barometric reference.
|
|
- `pressure.measured.upstream`, `pressure.measured.downstream`,
|
|
`pressure.measured.differential` — when pressure children register or
|
|
`data.simulate-measurement type=pressure` runs.
|
|
- `flow.measured.<position>`, `power.measured.atequipment`,
|
|
`temperature.measured.<position>` — when sensor children register or
|
|
the `data.simulate-measurement` topic supplies values.
|
|
|
|
Position labels are normalised to lowercase in the event name. The exact
|
|
set is data-driven by which children register and what they publish.
|
|
|
|
## Events emitted by `source.state.emitter`
|
|
|
|
- `positionChange` — fires when the position percentage changes (per
|
|
movement tick). Data: `{ position, state, mode, timestamp }`.
|
|
- `stateChange` — fires on transitions of the operating state machine
|
|
(`idle → starting → warmingup → operational → accelerating →
|
|
decelerating → stopping → coolingdown → idle`, plus `off`,
|
|
`maintenance`). Data: the new state string.
|
|
|
|
## Children registered by this node
|
|
|
|
rotatingMachine accepts `measurement` children through the
|
|
`childRegistrationUtils` handshake. Children typically have
|
|
`asset.type ∈ {pressure, flow, power, temperature}`. The machine
|
|
subscribes to the matching `<asset.type>.measured.<positionVsParent>`
|
|
event and mirrors the value into its own `MeasurementContainer`.
|
|
|
|
Two **virtual** children are reserved by the `data.simulate-measurement`
|
|
topic: incoming simulated values are tagged with
|
|
`childId/childName = 'dashboard-sim'` so dashboard-driven inputs are
|
|
distinguishable from real sensor children in downstream telemetry.
|
|
|
|
Position labels accepted from children are `upstream`, `downstream`,
|
|
`atEquipment` (and case variants — normalised internally).
|