Files
monster/wiki/Reference-Contracts.md
znetsixe aff866bd9b docs(wiki): regenerate topic-contract AUTOGEN block via wiki-gen
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>
2026-05-19 10:11:48 +02:00

205 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Reference &mdash; Contracts
![code-ref](https://img.shields.io/badge/code--ref-cd185dc-blue)
> [!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 &mdash; on monster, every topic dispatches unconditionally.
---
## Data model &mdash; `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 &mdash; 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 &mdash; 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` &mdash; 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 &mdash; 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 &mdash; informational. |
| Intake diameter (mm) | `constraints.intakeDiameter` | `12` | ≥ 0 | Schema only &mdash; 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 &mdash; Architecture](Reference-Architecture) | Code map, sampling-program loop, prediction + cooldown pipeline |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Known issues and open questions |
| [EVOLV &mdash; Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
| [EVOLV &mdash; Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |