measurement.html:
• sidebar swatch → #D4A02E (amber, sensor family) — EVOLV palette redesign
2026-05-21 (see superproject .claude/rules/node-red-flow-layout.md §10.0).
• Add "frost" option to dbaseOutputFormat dropdown (CoreSync FROST handoff).
src/commands/handlers.js + test/basic/commands-units.basic.test.js:
• Unit handling for data.measurement command. Analog + digital modes both
accept scalar / object / per-channel-map payloads; supplied units are
converted into the channel's configured (dropdown) unit.
CONTRACT.md: document the unit semantics.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.0 KiB
measurement — Contract
Hand-maintained for Phase 3; the ## Inputs table is generated from
src/commands/index.js (see Phase 9 generator). Keep ≤ 80 lines.
Inputs (msg.topic on Port 0)
| Canonical | Aliases (deprecated) | Payload | Effect |
|---|---|---|---|
set.simulator |
simulator |
none (payload ignored) | Toggles source.toggleSimulation() — flips config.simulation.enabled. |
set.outlier-detection |
outlierDetection |
none (payload ignored) | Toggles source.toggleOutlierDetection() — flips config.outlierDetection.enabled. |
cmd.calibrate |
calibrate |
none | Calls source.calibrate() — captures the current input as the zero/reference offset. |
data.measurement |
measurement |
mode-dependent — see Payload shape below | Pushes a sensor reading into the pipeline. Analog → source.inputValue; digital → source.handleDigitalPayload(<flat map>). Wrong shape for the configured mode logs a helpful warning suggesting the other mode. |
Aliases log a one-time deprecation warning the first time they fire.
data.measurement payload shape
Both modes accept the same three forms, mirroring pumpingStation's
set.inflow contract:
- Bare scalar —
msg.payload = 12.5(number or numeric string). The unit falls back tomsg.unit, and finally to the channel's configured unit (the dropdown selection in the node editor). - Rich object —
msg.payload = { value, unit?, timestamp? }. Used per- call to declare the unit of a single sample. - Digital map (digital mode only) —
msg.payload = { <channelKey>: <bare scalar | rich object>, … }. Each entry follows the rules above independently, so different channels in one message may carry different units.
When a supplied unit differs from the channel's configured unit, the value
is converted into the channel unit via generalFunctions.convert before it
enters the outlier / scaling / smoothing pipeline. If the supplied unit is
unknown or belongs to a different measure (e.g. kg on a pressure
channel), the handler logs a warning and uses the raw value treated as the
channel unit — the sample is not silently dropped.
Outputs (msg.topic on Port 0/1/2)
- Port 0 (process):
msg.topic = config.general.name. Payload built byoutputUtils.formatMsg(..., 'process')fromgetOutput()(analog) orgetDigitalOutput()(digital). Delta-compressed — only changed fields are emitted. - 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, distance }to its parent.
Events emitted by source.measurements.emitter
The MeasurementContainer fires <type>.measured.<position> whenever a
matching series receives a new value. The type / position labels are set
from config.asset.type and config.functionality.positionVsParent
(analog), or per-channel from config.channels[*] (digital). Examples:
pressure.measured.upstreamflow.measured.atequipmentlevel.measured.downstreamtemperature.measured.atequipment
Position labels are always lowercase in the event name. Parents subscribe
through the generic child.measurements.emitter.on(eventName, ...) handshake
established by childRegistrationUtils.
In digital mode one input message can fan out into several events — one per channel that accepted a value on that tick.
The legacy internal source.emitter also fires 'mAbs' with the current
scaled absolute value (analog mode only). This is deprecated in favour of
measurements.emitter and kept only for the editor status badge during the
refactor window.
Children registered by this node
None — measurement is a leaf in the S88 hierarchy (Control Module). It
registers itself as a child of an upstream parent (rotatingMachine,
pumpingStation, reactor, monster, …) but does not accept its own children.
Registration goes via Port 2 at startup and is keyed off
positionVsParent / distance in the node's UI config.