Replaces the agent-written placeholder inside Reference-Contracts.md with the authoritative table generated from src/commands/index.js. Both the BEGIN and END markers are normalized to the canonical form used by `@evolv/wiki-gen`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
205 lines
12 KiB
Markdown
205 lines
12 KiB
Markdown
# Reference — Contracts
|
||
|
||

|
||
|
||
> [!NOTE]
|
||
> Full topic contract, configuration schema, and child-registration filters for `monster`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/monster.json`.
|
||
>
|
||
> For an intuitive overview, return to the [Home](Home).
|
||
|
||
> [!NOTE]
|
||
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
|
||
|
||
---
|
||
|
||
## Topic contract
|
||
|
||
The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire.
|
||
|
||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||
|
||
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||
|---|---|---|---|---|
|
||
| `cmd.start` | `i_start` | any | — | Trigger / release the sampler start gate. |
|
||
| `set.schedule` | `monsternametijden` | any | — | Replace the sampling-times schedule. |
|
||
| `set.rain` | `rain_data` | any | — | Push current rain-event data into the sampler logic. |
|
||
| `data.flow` | `input_q` | `object` | — | Push the upstream flow measurement (payload: {value, unit}). |
|
||
| `set.mode` | `setMode` | any | — | Switch the monster between auto / manual modes. |
|
||
| `set.model-prediction` | `model_prediction` | any | — | Push the upstream rain-prediction snapshot used by the sampler. |
|
||
| `child.register` | `registerChild` | `string` | — | Register a child node (typically a measurement) with this monster. |
|
||
|
||
<!-- END AUTOGEN: topic-contract -->
|
||
|
||
### Mode / source / action allow-lists
|
||
|
||
monster has **no allow-list enforcement**. There is no `flowController.handle` equivalent and no `mode.allowedActions` / `mode.allowedSources` config slice. The `set.mode` handler is a placeholder. Compare `rotatingMachine`, which gates every topic through the mode matrix — on monster, every topic dispatches unconditionally.
|
||
|
||
---
|
||
|
||
## Data model — `getOutput()` shape
|
||
|
||
Composed each tick by `src/io/output.js` `buildOutput()`. Delta-compressed: consumers see only the keys that changed.
|
||
|
||
### Flat measurement keys
|
||
|
||
For every `(type, variant, position)` stored in MeasurementContainer, `getFlattenedOutput()` emits the three-segment key (note: monster does **not** add a `<childId>` segment, unlike `rotatingMachine`):
|
||
|
||
| Key | Type | Unit | Notes |
|
||
|:---|:---|:---|:---|
|
||
| `flow.manual.atequipment` | number | m³/h | Last `data.flow` value (after conversion). |
|
||
| `flow.measured.upstream` | number | m³/h | Last measured-child reading at this position. |
|
||
| `flow.measured.atequipment` | number | m³/h | Same. |
|
||
| `flow.measured.downstream` | number | m³/h | Same. |
|
||
|
||
### Scalar keys
|
||
|
||
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
|
||
|
||
| Key | Type | Source | Notes |
|
||
|:---|:---|:---|:---|
|
||
| `running` | boolean | `m.running` | True between `_beginRun` and `_endRun`. |
|
||
| `pulse` | boolean | `m.pulse` | True only on the tick a pulse is emitted; false otherwise. |
|
||
| `bucketVol` | number (L) | `m.bucketVol` | Composite volume accumulated this run. |
|
||
| `bucketWeight` | number (kg) | `m.bucketWeight` | `bucketVol + emptyWeightBucket`. |
|
||
| `sumPuls` | number | `m.sumPuls` | Pulses emitted this run. |
|
||
| `pulsesRemaining` | number | `targetPuls - sumPuls` | Clamped to ≥ 0. |
|
||
| `m3PerPuls` | number (m³) | `m.m3PerPuls` | Volume per pulse; set in `_beginRun` from `predFlow / targetPuls`. |
|
||
| `m3PerPulse` | number (m³) | (alias of `m3PerPuls`) | Both keys emitted; kept for legacy consumers. |
|
||
| `m3Total` | number (m³) | `m.m3Total` | Integrated total flow this run. |
|
||
| `q` | number (m³/h) | `m.q` | Effective flow (`getEffectiveFlow`). |
|
||
| `predFlow` | number (m³) | `m.predFlow` | Predicted total volume over the run window. |
|
||
| `predM3PerSec` | number (m³/s) | `m.predM3PerSec` | Predicted average rate during the run. |
|
||
| `predictedRateM3h` | number (m³/h) | `params.getPredictedFlowRate` | Rain-scaled flow band between `nominalFlowMin` and `flowMax`. |
|
||
| `timePassed` | number (s) | `m.timePassed` | Seconds since `start_time`. |
|
||
| `timeLeft` | number (s) | `m.timeLeft` | Seconds remaining until `stop_time`. |
|
||
| `pulseFraction` | number | `m.temp_pulse` | Sub-pulse integrator value (0..1+). |
|
||
| `flowToNextPulseM3` | number (m³) | derived | Volume left to integrate before the next pulse trigger. |
|
||
| `timeToNextPulseSec` | number (s) | derived | ETA to next pulse at current `q`; 0 when `q=0`. |
|
||
| `targetVolumeM3` | number (m³) | derived from `targetVolume` (L) | Target composite volume converted to m³. |
|
||
| `targetProgressPct` | number | derived | `bucketVol / targetVolume × 100`, 2-dp. |
|
||
| `targetDeltaL` | number (L) | derived | Signed L difference vs `targetVolume`. |
|
||
| `targetDeltaM3` | number (m³) | derived | Same in m³, 4-dp. |
|
||
| `nextDate` | number (epoch ms) | `m.nextDate` | Next scheduled START_DATE for `aquonSampleName`. `null` if never set. |
|
||
| `daysPerYear` | number | `m.daysPerYear` | Count of remaining scheduled runs this calendar year. |
|
||
| `sumRain` | number | `m.rainAggregator.sumRain` | Probability-weighted hourly precipitation sum. |
|
||
| `avgRain` | number | `m.rainAggregator.avgRain` | `sumRain / numberOfLocations`. |
|
||
| `nominalFlowMin` | number (m³/h) | config | Lower band for prediction. |
|
||
| `flowMax` | number (m³/h) | config | Upper band for prediction. |
|
||
| `minVolume` | number (L) | config | Lower bucket bound. |
|
||
| `maxVolume` | number (L) | derived | `maxWeight - emptyWeightBucket`. |
|
||
| `invalidFlowBounds` | boolean | derived | True when `nominalFlowMin >= flowMax`. |
|
||
| `missedSamples` | number | `m.missedSamples` | Pulse attempts blocked by cooldown. |
|
||
| `sampleCooldownMs` | number (ms) | derived | Ms remaining on the active cooldown; 0 when none. |
|
||
| `minSampleIntervalSec` | number (s) | config | Cooldown window. |
|
||
|
||
<!-- END AUTOGEN -->
|
||
|
||
### Status badge
|
||
|
||
`buildStatusBadge` in `io/statusBadge.js`:
|
||
|
||
| Condition | Badge | Fill | Shape |
|
||
|:---|:---|:---|:---|
|
||
| `invalidFlowBounds=true` | `Config error: nominalFlowMin (…) >= flowMax (…)` | red | (error preset) |
|
||
| `running=true` + `sampleCooldownMs > 0` | `SAMPLING (Ns) · <bucketVol>/<maxVolume> L` | yellow | ring |
|
||
| `running=true` + cooldown clear | `AI: RUNNING · <bucketVol>/<maxVolume> L` | green | dot |
|
||
| idle | `AI: IDLE` | grey | (idle preset) |
|
||
|
||
---
|
||
|
||
## Configuration schema — editor form to config keys
|
||
|
||
Source of truth: `generalFunctions/src/configs/monster.json` plus `nodeClass.buildDomainConfig`.
|
||
|
||
### General (`config.general`)
|
||
|
||
| Form field | Config key | Default | Notes |
|
||
|:---|:---|:---|:---|
|
||
| Name | `general.name` | `"Monster Configuration"` | Free-text. |
|
||
| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
|
||
| Default unit | `general.unit` | `unitless` | Not used by the sampling program. |
|
||
| Enable logging | `general.logging.enabled` | `true` | Master switch. |
|
||
| Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. |
|
||
|
||
### Functionality (`config.functionality`)
|
||
|
||
| Form field | Config key | Default | Notes |
|
||
|:---|:---|:---|:---|
|
||
| (hidden) | `functionality.softwareType` | `monster` | Constant. |
|
||
| (hidden) | `functionality.role` | `samplingCabinet` | Constant. |
|
||
| AQUON sample name | `functionality.aquonSampleName` | _unset_ | Forwarded to `source.aquonSampleName` in `extraSetup`. Falls back to `'112100'` in `_initState`. |
|
||
|
||
### Asset (`config.asset`)
|
||
|
||
| Form field | Config key | Default | Notes |
|
||
|:---|:---|:---|:---|
|
||
| Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. |
|
||
| Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | |
|
||
| Supplier | `asset.supplier` | `"Unknown"` | Schema only — not used by the sampling program. |
|
||
| Type | `asset.type` | `"sensor"` (enum) | Schema only. |
|
||
| SubType | `asset.subType` | `"pressure"` | Schema only; misleading default for a sampling cabinet (flag). |
|
||
| Model | `asset.model` | `"Unknown"` | Schema only. |
|
||
| Empty bucket weight (kg) | `asset.emptyWeightBucket` | `3` | Used in `bucketWeight = bucketVol + emptyWeightBucket` and in `maxVolume = maxWeight - emptyWeightBucket`. |
|
||
|
||
### Constraints (`config.constraints`)
|
||
|
||
| Form field | Config key | Default | Range | Notes |
|
||
|:---|:---|:---|:---|:---|
|
||
| Sampling time (hr) | `constraints.samplingtime` | `0` | ≥ 0 | Run length. Used as `samplingtime · 3600 · 1000` ms for `stop_time`. |
|
||
| Sampling period (hr) | `constraints.samplingperiod` | `24` | ≥ 1 | Documented as the fixed composite-collection period; not enforced by `samplingProgram` — the AQUON schedule arms the run. |
|
||
| Min volume (L) | `constraints.minVolume` | `5` | ≥ 5 | Used in `targetVolume = minVolume · √(maxVolume / minVolume)`. |
|
||
| Max weight (kg) | `constraints.maxWeight` | `23` | ≤ 23 | Bucket-overload bound; `maxVolume = maxWeight - emptyWeightBucket`. |
|
||
| Sub-sample volume (mL) | `constraints.subSampleVolume` | `50` | fixed | Schema enforces `min=max=50`. Hard-coded as `volume_pulse = 0.05` in domain. |
|
||
| Storage temperature (°C) | `constraints.storageTemperature.min/max` | `{1, 5}` | per-leg | Schema only — informational. |
|
||
| Flowmeter present | `constraints.flowmeter` | `true` | bool | ⚠️ In schema but **not** wired in `buildDomainConfig`. Effectively always `true`. |
|
||
| Closed system | `constraints.closedSystem` | `false` | bool | Schema only. |
|
||
| Intake speed (m/s) | `constraints.intakeSpeed` | `0.3` | ≥ 0 | Schema only — informational. |
|
||
| Intake diameter (mm) | `constraints.intakeDiameter` | `12` | ≥ 0 | Schema only — informational. |
|
||
| Nominal flow min (m³/h) | `constraints.nominalFlowMin` | `0` | ≥ 0 | Lower bound of rain-driven flow prediction. |
|
||
| Flow max (m³/h) | `constraints.flowMax` | `0` | ≥ 0 | Upper bound of rain-driven flow prediction. |
|
||
| Max rain reference | `constraints.maxRainRef` | `10` | > 0 | Rain index that maps to `flowMax`. |
|
||
| Min sample interval (s) | `constraints.minSampleIntervalSec` | `60` | ≥ 0 | Cooldown guard. |
|
||
|
||
> [!WARNING]
|
||
> **Default `flowMax = 0` blocks every run.** `validateFlowBounds` requires `0 ≤ nominalFlowMin < flowMax`. Out of the box the bounds are invalid and `_beginRun` never fires. Set `flowMax` to a realistic upper bound before deploying.
|
||
|
||
### Unit policy
|
||
|
||
monster has **no `requireUnitForTypes` policy** declared in `specificClass`. Conversions happen at the boundary:
|
||
|
||
| Quantity | Canonical (internal) | Carried as | Notes |
|
||
|:---|:---|:---|:---|
|
||
| Flow (`data.flow`) | m³/h | m³/h | `handlers.dataFlow` converts inbound via `convert(value).from(unit).to('m3/h')`. |
|
||
| Flow (measured-child) | as supplied | as supplied | `flowTracker.handleMeasuredFlow` defaults to `'m3/h'` when `unit` is missing; **does not convert**. Wire children that emit in m³/h. |
|
||
| Volume (`bucketVol`) | L | L | Output also exposes m³ derivations (`targetVolumeM3`, `targetDeltaM3`). |
|
||
| Weight | kg | kg | |
|
||
| Time | s (timers) / ms (timestamps) | mixed | `timePassed` / `timeLeft` in s, `nextDate` in epoch ms. |
|
||
|
||
---
|
||
|
||
## Child registration
|
||
|
||
Source: `src/specificClass.js` `_wireMeasurementChild`. The registrar subscribes to all three `flow.measured.<position>` events on the child's measurement emitter as long as the child's `config.asset.type` is `'flow'` or unset.
|
||
|
||
| Software type | Filter | Wired to | Side-effect |
|
||
|:---|:---|:---|:---|
|
||
| `measurement` | `asset.type='flow'` (or missing) | `flowTracker.handleMeasuredFlow` (handles all three positions) | Latches latest measured flow per position; `getEffectiveFlow` blends across positions and with `manualFlow`. |
|
||
| `measurement` | `asset.type` anything else | _ignored_ | The branch returns early; no listener is attached. |
|
||
|
||
monster has **no position-based filtering**. Unlike `rotatingMachine` (which routes upstream pressure separately from downstream), all three flow positions are wired to the same handler and the latest value per position wins.
|
||
|
||
There are **no auto-registered virtual children** (no `dashboard-sim-*` equivalent). Inject simulated flow via `data.flow` instead.
|
||
|
||
---
|
||
|
||
## Related pages
|
||
|
||
| Page | Why |
|
||
|:---|:---|
|
||
| [Home](Home) | Intuitive overview |
|
||
| [Reference — Architecture](Reference-Architecture) | Code map, sampling-program loop, prediction + cooldown pipeline |
|
||
| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
|
||
| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
|
||
| [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
|
||
| [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |
|