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>
179 lines
10 KiB
Markdown
179 lines
10 KiB
Markdown
# Reference — Contracts
|
||
|
||

|
||
|
||
> [!NOTE]
|
||
> Pending full node review (2026-05). Topic contract, output shape and configuration schema for `diffuser`. Sources of truth: `src/commands/index.js`, `src/specificClass.js` `getOutput()`, and `generalFunctions/src/configs/diffuser.json`.
|
||
>
|
||
> For an intuitive overview, return to the [Home](Home).
|
||
|
||
---
|
||
|
||
## Topic contract
|
||
|
||
The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler in `src/commands/handlers.js`; aliases emit a one-time deprecation warning the first time they fire.
|
||
|
||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||
|
||
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||
|---|---|---|---|---|
|
||
| `data.flow` | `air_flow` | `number` | `volumeFlowRate` (default `m3/h`) | Push the measured air flow into the diffuser model. |
|
||
| `set.density` | `density` | `number` | — | Update the air density used in OTR / SOTR calculations. |
|
||
| `set.water-height` | `height_water` | `number` | — | Update the water column height above the diffusers (m). |
|
||
| `set.header-pressure` | `header_pressure` | `number` | — | Update the header (supply) pressure feeding the diffusers (mbar). |
|
||
| `set.elements` | `elements` | `number` | — | Update the count of active diffuser elements. |
|
||
| `set.alfa-factor` | `alfaFactor` | `number` | — | Update the alfa factor used in oxygen-transfer correction. |
|
||
|
||
<!-- END AUTOGEN: topic-contract -->
|
||
|
||
There are **no query topics** today (no `query.curves` / `query.cog` analogue). The full state is on Port 0 every time a setter fires.
|
||
|
||
There are **no mode / source allow-lists** — the diffuser has no operational modes (auto / virtualControl / fysicalControl). Every input topic is accepted from every source.
|
||
|
||
---
|
||
|
||
## Data model — `getOutput()` shape
|
||
|
||
Composed in `Diffuser.getOutput()` then delta-compressed by `outputUtils.formatMsg('process')`. Consumers see only the keys that changed since the last emit.
|
||
|
||
### Scalar keys
|
||
|
||
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
|
||
|
||
| Key | Type | Unit | Source | Notes |
|
||
|:---|:---|:---|:---|:---|
|
||
| `iPressure` | number | mbar (gauge) | `this.i_pressure` | Echo of last `set.header-pressure`. |
|
||
| `iMWater` | number | m | `this.i_m_water` | Echo of last `set.water-height`. |
|
||
| `iFlow` | number | Nm³/h (config-defaulted) | `this.i_flow` | Echo of last `data.flow`. |
|
||
| `nFlow` | number | Nm³/h | derived | Normalised air flow at standard conditions (T=20 °C, p=1.01325 bar, RH=0). Rounded 2 dp. |
|
||
| `oOtr` | number | g O₂ / Nm³ | curve interpolation | Oxygen transfer rate at current density + flux. Rounded 2 dp. |
|
||
| `oPLoss` | number | mbar | `o_p_water + o_p_flow` | Total head loss: static head from water column + diffuser ΔP. Rounded 2 dp. |
|
||
| `oKgo2H` | number | kg O₂ / h | derived | Mass-rate of oxygen transfer. Uses α-factor and water height. |
|
||
| `oFlowElement` | number | Nm³/h per element | `nFlow / elements` | Per-element air flow. Rounded 2 dp. |
|
||
| `oFluxPerM2` | number | Nm³ / (h · m² membrane) | `nFlow / totalMembraneArea` | Canonical curve x-axis. Rounded 2 dp. |
|
||
| `efficiency` | number | % (0–100) | `_combineEff` | Combined OTR / ΔP score: high OTR + low ΔP → high score. Rounded 2 dp. |
|
||
| `slope` | number | g O₂/Nm³ per Nm³/(h·m²) | curve segment | Local OTR-vs-flux slope at the operating point. Rounded 3 dp. |
|
||
| `oZoneOtr` | number | kg O₂ / m³ / day | `getReactorOtr(zoneVolume)` | Reactor-zone OTR. Zero when `diffuser.zoneVolume` is unset or non-positive. |
|
||
| `idle` | boolean | — | `this.i_flow ≤ 0` | Derived predicate, not an FSM state. |
|
||
| `warning` | array of strings | — | `this.warning.text` | Flow-per-element band excursions at ± 2 % hysteresis. |
|
||
| `alarm` | array of strings | — | `this.alarm.text` | Flow-per-element band excursions at ± 10 % hysteresis. |
|
||
|
||
<!-- END AUTOGEN: data-model -->
|
||
|
||
### Per-measurement keys
|
||
|
||
> [!NOTE]
|
||
> The diffuser does **not** emit typed `MeasurementContainer` keys. There is no `<type>.<variant>.<position>.<childId>` shape on this node. Parents that want OTR / ΔP via the standard `ChildRouter` handshake have to wait for the future phase that promotes `oOtr` / `oZoneOtr` to typed series — tracked in [Limitations](Reference-Limitations).
|
||
|
||
### Status badge
|
||
|
||
`getStatusBadge()` in `specificClass.js`:
|
||
|
||
| Condition | Symbol | Fill | Text |
|
||
|:---|:---:|:---|:---|
|
||
| `alarm.state` | (error compose) | red | first entry of `alarm.text` |
|
||
| `warning.state` | ⚠ | yellow | first entry of `warning.text` |
|
||
| `idle` (no alarm/warn) | (idle compose) | grey | `<oKgo2H> kg o2 / h` |
|
||
| active (no alarm/warn) | 🟢 | green (default) | `<oKgo2H> kg o2 / h` |
|
||
|
||
---
|
||
|
||
## Configuration schema — editor form to config keys
|
||
|
||
Source of truth: `generalFunctions/src/configs/diffuser.json` + `src/nodeClass.js` `buildDomainConfig`.
|
||
|
||
### General (`config.general`)
|
||
|
||
| Form field | Config key | Default | Notes |
|
||
|:---|:---|:---|:---|
|
||
| Name | `general.name` | `"Diffuser"` | Human-readable label. |
|
||
| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
|
||
| Default unit | `general.unit` | `"Nm3/h"` | Default airflow unit. |
|
||
| Enable logging | `general.logging.enabled` | `true` | Master switch. |
|
||
| Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. |
|
||
|
||
### Asset (`config.asset`)
|
||
|
||
| Form field | Config key | Default | Notes |
|
||
|:---|:---|:---|:---|
|
||
| Asset model | `asset.model` | `"gva-elastox-r"` | Curve registry id. Resolved via `loadCurve(model)`. Falls back to the default on miss. |
|
||
| Asset tag number | `asset.assetTagNumber` | `""` | External asset registry tag (Bedrijfsmiddelenregister). |
|
||
|
||
### Functionality (`config.functionality`)
|
||
|
||
| Form field | Config key | Default | Notes |
|
||
|:---|:---|:---|:---|
|
||
| Software type | `functionality.softwareType` | `"diffuser"` | Constant. Used in the parent-register handshake. |
|
||
| Role | `functionality.role` | `"Aeration diffuser"` | Free-text role label. |
|
||
| Position vs parent | `functionality.positionVsParent` | `"atEquipment"` | One of `upstream` / `atEquipment` / `downstream`. Carried on the `child.register` Port-2 message to the reactor. |
|
||
|
||
### Diffuser (`config.diffuser`)
|
||
|
||
| Form field | Config key | Default | Range | Notes |
|
||
|:---|:---|:---|:---|:---|
|
||
| Zone number | `diffuser.number` | `1` | int ≥ 1 | Sequential zone number; used in the node label. |
|
||
| Element count | `diffuser.elements` | `1` | int ≥ 1 | Number of active diffuser elements. |
|
||
| Membrane area / element | `diffuser.membraneAreaPerElement` | `null` | m² > 0 | Overrides curve `_meta.membraneArea_m2_per_element`. Final fallback is 0.18 m² (Jäger TD-65 / GVA). |
|
||
| Diffuser density (bottom coverage) | `diffuser.density` | `15` | % > 0, typical 10–25 | Curve-family key. Multi-coverage curves are interpolated; single-coverage curves are clamped. Replaces the legacy "elements per m²" semantics — an earlier refactor mislabelled this column. |
|
||
| Water height | `diffuser.waterHeight` | `0` | m ≥ 0 | Static head + kg O₂/h factor. |
|
||
| Alpha factor | `diffuser.alfaFactor` | `0.7` | typically 0–1 | Oxygen-transfer correction. |
|
||
| Header pressure | `diffuser.headerPressure` | `0` | mbar ≥ 0 (gauge) | Above atmospheric. Feeds air-density correction. |
|
||
| Local atmospheric pressure | `diffuser.localAtmPressure` | `1013.25` | mbar > 0 | Density baseline (hidden by default). |
|
||
| Water density | `diffuser.waterDensity` | `997` | kg/m³ > 0 | Static head calculation (hidden by default). |
|
||
| Zone volume | `diffuser.zoneVolume` | `0` | m³ ≥ 0 | Aeration zone volume. When > 0, populates `oZoneOtr` (kg O₂ / m³ / day). |
|
||
|
||
### Unit policy
|
||
|
||
The diffuser uses a non-canonical, supplier-curve-friendly unit policy — airflow lives in **Nm³/h** (not m³/s) on the wire, and pressure stays in **mbar** (not Pa) at every boundary. Internal arithmetic converts mbar ↔ Pa where needed (`_heightToPressureMbar`, `_calcAirDensityMbar`).
|
||
|
||
| Quantity | Boundary unit | Internal | Notes |
|
||
|:---|:---|:---|:---|
|
||
| Air flow | `Nm3/h` | `Nm3/h` | Normalised internally; curves are in this unit. |
|
||
| Header pressure | `mbar` (gauge) | `Pa` (intermediate) | Converted in `_calcAirDensityMbar`. |
|
||
| Atmospheric pressure | `mbar` | `Pa` (intermediate) | Same. |
|
||
| Water height | `m` | `m` | Converted to `mbar` head via `_heightToPressureMbar`. |
|
||
| Membrane area | `m² / element` | `m² / element` | Curve metadata or config override. |
|
||
| Temperature | hardcoded 20 °C | `K` (internal in `_calcAirDensityMbar`) | No temperature input topic today. |
|
||
|
||
This deliberately diverges from rotatingMachine's canonical-Pa/m³·s⁻¹/W/K policy because the supplier curves and operator-facing dashboards are all in Nm³/h + mbar. The cost is reduced reuse with `MeasurementContainer` (see [Limitations](Reference-Limitations)).
|
||
|
||
---
|
||
|
||
## Child registration
|
||
|
||
The diffuser is a **leaf node** — it accepts no children. Itself, it registers with the upstream parent (typically a `reactor`) at startup via the Port-2 handshake.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
diff[diffuser]:::equip -->|child.register<br/>payload = node.id<br/>positionVsParent = atEquipment<br/>distance| reactor[reactor / parent]:::unit
|
||
classDef equip fill:#86bbdd,color:#000
|
||
classDef unit fill:#50a8d9,color:#000
|
||
```
|
||
|
||
| Direction | Counterparty | Side-effect |
|
||
|:---|:---|:---|
|
||
| outbound at startup | upstream reactor (Port 2) | sends `child.register` with `positionVsParent` default `atEquipment` and the configured `distance` |
|
||
| inbound | — | none accepted |
|
||
|
||
---
|
||
|
||
## Events emitted
|
||
|
||
| Emitter | Event | When |
|
||
|:---|:---|:---|
|
||
| `source.emitter` | `'output-changed'` | At the end of every `_recalculate()` — i.e. on every input setter. `BaseNodeAdapter` listens and pushes delta-compressed Port 0 + Port 1 messages. |
|
||
| `source.measurements.emitter` | (none) | The diffuser does not currently publish typed `MeasurementContainer` series. Future phase. |
|
||
|
||
---
|
||
|
||
## Related pages
|
||
|
||
| Page | Why |
|
||
|:---|:---|
|
||
| [Home](Home) | Intuitive overview |
|
||
| [Reference — Architecture](Reference-Architecture) | Code map, OTR / ΔP pipeline, output ports |
|
||
| [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 |
|