> 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.
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. |
| `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. |
| 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. |
| 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. |
| 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.
| 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. |