From e47de87adbb0e311c14dc2ac4ce5596702a34d2c Mon Sep 17 00:00:00 2001 From: znetsixe Date: Fri, 29 May 2026 18:41:32 +0200 Subject: [PATCH] feat(commands): unit shorthand + collapse duplicated value/unit parsing; wiki sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5 descriptors -> unit: shorthand (cmd.calibrate.volume/level, set.inflow/ outflow/demand). - setInflow/setOutflow: drop the hand-rolled scalar-vs-object parsing — the registry now normalises every shape to a number in the descriptor unit; the handlers become guarded one-liners (matching setDemand). - Regenerate wiki topic-contract + command-envelope note (msg.origin). 143/143 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/commands/handlers.js | 51 ++++++++++++++----------------------- src/commands/index.js | 14 +++++----- wiki/Reference-Contracts.md | 16 +++++++----- 3 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/commands/handlers.js b/src/commands/handlers.js index c803948..abbc856 100644 --- a/src/commands/handlers.js +++ b/src/commands/handlers.js @@ -48,42 +48,29 @@ exports.calibrateLevel = (source, msg, ctx) => { source.calibratePredictedLevel(v); }; -exports.setInflow = (source, msg) => { - // Payload is either a number (legacy q_in shape) or - // { value, unit, timestamp } (richer object form). - const p = msg.payload; - let value; - let unit; - let timestamp; - if (p !== null && typeof p === 'object') { - value = Number(p.value); - unit = p.unit; - timestamp = p.timestamp || Date.now(); - } else { - value = Number(p); - unit = msg?.unit; - timestamp = msg?.timestamp || Date.now(); +// The registry has already normalised any accepted shape (number, numeric +// string, or { value, unit } object) to a number in the descriptor unit +// (m3/h) and tagged msg.unit. Handlers just read the normalised scalar. +exports.setInflow = (source, msg, ctx) => { + const log = _logger(source, ctx); + const value = Number(msg.payload); + if (!Number.isFinite(value)) { + log?.warn?.(`set.inflow: non-numeric payload '${JSON.stringify(msg.payload)}'`); + return; } - source.setManualInflow(value, timestamp, unit); + source.setManualInflow(value, msg.timestamp, msg.unit); }; -exports.setOutflow = (source, msg) => { - // Manual q_out — basin-docs dashboard injects a drain rate without - // wiring a real pump. Same payload shape as q_in. - const p = msg.payload; - let value; - let unit; - let timestamp; - if (p !== null && typeof p === 'object') { - value = Number(p.value); - unit = p.unit; - timestamp = p.timestamp || Date.now(); - } else { - value = Number(p); - unit = msg?.unit; - timestamp = msg?.timestamp || Date.now(); +exports.setOutflow = (source, msg, ctx) => { + // Manual q_out — basin-docs dashboard injects a drain rate without wiring a + // real pump. Same normalised shape as set.inflow. + const log = _logger(source, ctx); + const value = Number(msg.payload); + if (!Number.isFinite(value)) { + log?.warn?.(`set.outflow: non-numeric payload '${JSON.stringify(msg.payload)}'`); + return; } - source.setManualOutflow(value, timestamp, unit); + source.setManualOutflow(value, msg.timestamp, msg.unit); }; exports.setDemand = (source, msg, ctx) => { diff --git a/src/commands/index.js b/src/commands/index.js index 6a5a5fb..c9780ae 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -26,9 +26,10 @@ module.exports = [ { topic: 'cmd.calibrate.volume', aliases: ['calibratePredictedVolume'], - // any: payload may be a number or numeric string. + // any: payload may be a number, numeric string, or { value, unit } object — + // the registry normalises all of them to a number in `unit` before the handler. payloadSchema: { type: 'any' }, - units: { measure: 'volume', default: 'm3' }, + unit: 'm3', description: 'Calibrate the predicted-volume integrator to a known basin volume.', handler: handlers.calibrateVolume, }, @@ -36,16 +37,15 @@ module.exports = [ topic: 'cmd.calibrate.level', aliases: ['calibratePredictedLevel'], payloadSchema: { type: 'any' }, - units: { measure: 'length', default: 'm' }, + unit: 'm', description: 'Calibrate the predicted-volume integrator to a known basin level.', handler: handlers.calibrateLevel, }, { topic: 'set.inflow', aliases: ['q_in'], - // any: number, numeric string, or { value, unit, timestamp } object. payloadSchema: { type: 'any' }, - units: { measure: 'volumeFlowRate', default: 'm3/h' }, + unit: 'm3/h', description: 'Push a measured inflow value into the basin balance.', handler: handlers.setInflow, }, @@ -53,7 +53,7 @@ module.exports = [ topic: 'set.outflow', aliases: ['q_out'], payloadSchema: { type: 'any' }, - units: { measure: 'volumeFlowRate', default: 'm3/h' }, + unit: 'm3/h', description: 'Push a measured outflow value into the basin balance.', handler: handlers.setOutflow, }, @@ -61,7 +61,7 @@ module.exports = [ topic: 'set.demand', aliases: ['Qd'], payloadSchema: { type: 'any' }, - units: { measure: 'volumeFlowRate', default: 'm3/h' }, + unit: 'm3/h', description: 'Operator outflow demand setpoint for the station.', handler: handlers.setDemand, }, diff --git a/wiki/Reference-Contracts.md b/wiki/Reference-Contracts.md index 8ed4c13..64a491e 100644 --- a/wiki/Reference-Contracts.md +++ b/wiki/Reference-Contracts.md @@ -11,7 +11,11 @@ ## Topic contract -The **Unit** column reflects each descriptor's `units: { measure, default }` declaration. The default unit is what the commandRegistry coerces incoming `msg.unit` values to before the handler runs. +The **Unit** column reflects each descriptor's declared unit (via the `unit: 'm3/h'` shorthand or the legacy `units: { measure, default }`; the measure is derived from the unit). The default unit is what the commandRegistry coerces incoming values to before the handler runs. + +**Command envelope (all EVOLV nodes).** Every command shares one envelope on top of `msg.topic`: +- **Value + unit** — send `msg.payload` as a number (with optional sibling `msg.unit`) **or** as `{ value, unit }`. The registry always converts the value to the descriptor's unit before the handler; numeric strings are converted too. A missing unit assumes the descriptor default. +- **`msg.origin`** — the control authority that issued the command: `parent` (automation/parent controller, the default), `GUI` (SCADA/HMI operator), or `fysical` (physical buttons). On nodes with a control mode, the mode's `allowedSources` decides which origins are accepted; releasing control is done by changing the mode. @@ -19,11 +23,11 @@ The **Unit** column reflects each descriptor's `units: { measure, default }` dec |---|---|---|---|---| | `set.mode` | `changemode` | `string` | — | Switch the station between auto / manual control modes. | | `child.register` | `registerChild` | `string` | — | Register a child node (machine group, measurement, …) with this station. | -| `cmd.calibrate.volume` | `calibratePredictedVolume` | `any` | `volume` (default `m3`) | Calibrate the predicted-volume integrator to a known basin volume. | -| `cmd.calibrate.level` | `calibratePredictedLevel` | `any` | `length` (default `m`) | Calibrate the predicted-volume integrator to a known basin level. | -| `set.inflow` | `q_in` | `any` | `volumeFlowRate` (default `m3/h`) | Push a measured inflow value into the basin balance. | -| `set.outflow` | `q_out` | `any` | `volumeFlowRate` (default `m3/h`) | Push a measured outflow value into the basin balance. | -| `set.demand` | `Qd` | `any` | `volumeFlowRate` (default `m3/h`) | Operator outflow demand setpoint for the station. | +| `cmd.calibrate.volume` | `calibratePredictedVolume` | any | `volume` (default `m3`) | Calibrate the predicted-volume integrator to a known basin volume. | +| `cmd.calibrate.level` | `calibratePredictedLevel` | any | `length` (default `m`) | Calibrate the predicted-volume integrator to a known basin level. | +| `set.inflow` | `q_in` | any | `volumeFlowRate` (default `m3/h`) | Push a measured inflow value into the basin balance. | +| `set.outflow` | `q_out` | any | `volumeFlowRate` (default `m3/h`) | Push a measured outflow value into the basin balance. | +| `set.demand` | `Qd` | any | `volumeFlowRate` (default `m3/h`) | Operator outflow demand setpoint for the station. |