Files
diffuser/wiki/Reference-Contracts.md
znetsixe 37ecfe5726 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:47 +02:00

179 lines
10 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-4973a8b-blue)
> [!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** &mdash; the diffuser has no operational modes (auto / virtualControl / fysicalControl). Every input topic is accepted from every source.
---
## Data model &mdash; `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&ndash;100) | `_combineEff` | Combined OTR / ΔP score: high OTR + low ΔP &rarr; 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 | &mdash; | `this.i_flow ≤ 0` | Derived predicate, not an FSM state. |
| `warning` | array of strings | &mdash; | `this.warning.text` | Flow-per-element band excursions at &pm; 2 % hysteresis. |
| `alarm` | array of strings | &mdash; | `this.alarm.text` | Flow-per-element band excursions at &pm; 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 &mdash; 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 &mdash; 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 &ge; 1 | Sequential zone number; used in the node label. |
| Element count | `diffuser.elements` | `1` | int &ge; 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&ndash;25 | Curve-family key. Multi-coverage curves are interpolated; single-coverage curves are clamped. Replaces the legacy "elements per m²" semantics &mdash; an earlier refactor mislabelled this column. |
| Water height | `diffuser.waterHeight` | `0` | m &ge; 0 | Static head + kg O₂/h factor. |
| Alpha factor | `diffuser.alfaFactor` | `0.7` | typically 0&ndash;1 | Oxygen-transfer correction. |
| Header pressure | `diffuser.headerPressure` | `0` | mbar &ge; 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³ &ge; 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 &mdash; 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 &harr; 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** &mdash; 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 | &mdash; | none accepted |
---
## Events emitted
| Emitter | Event | When |
|:---|:---|:---|
| `source.emitter` | `'output-changed'` | At the end of every `_recalculate()` &mdash; 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 &mdash; Architecture](Reference-Architecture) | Code map, OTR / ΔP pipeline, output ports |
| [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 |