# Reference — 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. | 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. | ### 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 `` 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 | 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. | ### 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) · / L` | yellow | ring | | `running=true` + cooldown clear | `AI: RUNNING · / 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.` 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 |