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>
10 KiB
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.jsgetOutput(), andgeneralFunctions/src/configs/diffuser.json.For an intuitive overview, return to the 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
MeasurementContainerkeys. There is no<type>.<variant>.<position>.<childId>shape on this node. Parents that want OTR / ΔP via the standardChildRouterhandshake have to wait for the future phase that promotesoOtr/oZoneOtrto typed series — tracked in 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).
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.
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 | Intuitive overview |
| Reference — Architecture | Code map, OTR / ΔP pipeline, output ports |
| Reference — Examples | Shipped flows + debug recipes |
| Reference — Limitations | Known issues and open questions |
| EVOLV — Topic Conventions | Platform-wide topic rules |
| EVOLV — Telemetry | Port 0 / 1 / 2 InfluxDB layout |