# Reference — 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. | 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. | 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 | 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. | ### Per-measurement keys > [!NOTE] > The diffuser does **not** emit typed `MeasurementContainer` keys. There is no `...` 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 | ` kg o2 / h` | | active (no alarm/warn) | 🟢 | green (default) | ` 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
payload = node.id
positionVsParent = atEquipment
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 |