Table of Contents
- diffuser
- 1. What this node is
- 2. Position in the platform
- 3. Capability matrix
- 4. Code map
- 5. Topic contract
- 6. Child registration
- 7. Lifecycle — what one event does
- 8. Data model — getOutput()
- 9. Configuration — editor form ↔ config keys
- 10. State chart
- 11. Examples
- 12. Debug recipes
- 13. When you would NOT use this node
- 14. Known limitations / current issues
diffuser
Reflects code as of
15cfb22· regenerated2026-05-11vianpm run wiki:allIf this banner is stale, the page may be out of date. Treat as informative, not authoritative.
1. What this node is
diffuser models an aeration-diffuser zone. Given header pressure, water-column height, alpha factor, element count and airflow, it interpolates a supplier OTR curve, normalises airflow to Nm³/h, and emits oxygen-transfer rate plus a reactor-zone OTR for the downstream parent. Used as a leaf Equipment Module under a reactor.
2. Position in the platform
flowchart LR
src[blower / MGC / dashboard]:::unit -->|data.flow| diff[diffuser<br/>Equipment]:::equip
setters[dashboard setters]:::ctrl -->|set.density / set.water-height /<br/>set.elements / set.alfa-factor /<br/>set.header-pressure| diff
diff -->|child.register| reactor[reactor<br/>Unit]:::unit
classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000
classDef ctrl fill:#a9daee,color:#000
S88 colours: Unit #50a8d9, Equipment #86bbdd, Control Module #a9daee. Source of truth: .claude/rules/node-red-flow-layout.md.
3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| Supplier OTR curve interpolation | ✅ | Density-keyed; falls back to single key when only one available. |
| Air-density correction (header + atm) | ✅ | _calcAirDensityMbar per ideal-gas mix. |
| Per-element flow tracking | ✅ | o_flowElement = nFlow / nElements. |
| Static head loss from water column | ✅ | _heightToPressureMbar. |
| Warning / alarm bands on flow-per-element | ✅ | Hysteresis 2 % (warn) / 10 % (alarm). |
| Reactor-zone OTR for parent | ✅ | oZoneOtr derived from diffuser.zoneVolume. |
| Idle handling at flow ≤ 0 | ✅ | Resets all derived outputs to zero. |
Typed MeasurementContainer emission |
❌ | All output flows via getOutput(); no typed series yet. |
4. Code map
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass, commands"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Diffuser.configure()<br/>loads supplier specs<br/>setters → _recalculate()"]
end
subgraph concerns["src/ concern modules"]
cmds["commands/<br/>topic registry + handlers"]
end
nc --> sc
nc --> cmds
| Module | Owns | Read first if you're changing… |
|---|---|---|
commands/ |
Input-topic registry + per-topic handlers | Topic naming, payload validation. |
specificClass.js (single file) |
Setters, OTR/ΔP curve interpolation, alarms, output composition | Anything domain-side. |
The diffuser was a small node so the P6.4 refactor did not split it into per-concern directories. Future work may extract curves/ and alarms/ if the file grows past ~250 lines.
5. Topic contract
Auto-generated from
src/commands/index.js. Do NOT hand-edit between the markers. Re-runnpm run wiki: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. |
6. Child registration
diffuser is a leaf node — it accepts no children. It registers itself with its upstream parent (typically a reactor) at startup via the standard Port-2 handshake.
flowchart LR
diff[diffuser]:::equip -->|child.register payload=node.id<br/>positionVsParent=atEquipment| 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 | sends child.register on Port 2 with positionVsParent default atEquipment |
| inbound | — | none accepted |
7. Lifecycle — what one event does
sequenceDiagram
participant src as upstream source
participant diff as diffuser
participant curve as supplier specs
participant out as Port-0
src->>diff: data.flow (Nm³/h)
diff->>diff: setFlow → _recalculate
alt flow ≤ 0
diff->>diff: idle = true, reset derived outputs
else flow > 0
diff->>diff: air-density correction (atm + header)
diff->>curve: interpolate OTR by density + flow/element
diff->>curve: interpolate ΔP curve by flow/element
diff->>diff: kg O₂/h, combined efficiency, slope
diff->>diff: _checkLimits (warn / alarm bands)
end
diff->>diff: notifyOutputChanged()
diff->>out: msg{topic, payload (delta-compressed)}
8. Data model — getOutput()
What lands on Port 0. Composed in Diffuser.getOutput(), then delta-compressed by outputUtils.formatMsg.
| Key | Type | Unit | Sample |
|---|---|---|---|
alarm |
array | — | […] |
efficiency |
number | — | 0 |
iFlow |
number | — | 0 |
iMWater |
number | — | 0 |
iPressure |
number | — | 0 |
idle |
boolean | — | true |
nFlow |
number | — | 0 |
oFlowElement |
number | — | 0 |
oKgo2H |
number | — | 0 |
oOtr |
number | — | 0 |
oPLoss |
number | — | 0 |
oZoneOtr |
number | — | 0 |
slope |
number | — | 0 |
warning |
array | — | […] |
oZoneOtr is kg O₂ / m³ / day; it is 0 when diffuser.zoneVolume is unset.
9. Configuration — editor form ↔ config keys
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Element count]
f2[Diffuser density]
f3[Water height]
f4[Header pressure]
f5[Alpha factor]
f6[Zone volume]
end
subgraph config["Domain config slice"]
c1[diffuser.elements]
c2[diffuser.density]
c3[diffuser.waterHeight]
c4[diffuser.headerPressure]
c5[diffuser.alfaFactor]
c6[diffuser.zoneVolume]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
| Form field | Config key | Default | Range | Where used |
|---|---|---|---|---|
| Element count | diffuser.elements |
1 |
int ≥ 1 | per-element flow |
| Diffuser density | diffuser.density |
2.4 |
> 0 | OTR curve key |
| Water height | diffuser.waterHeight |
0 |
≥ 0 (m) | static head + kg O₂/h |
| Header pressure | diffuser.headerPressure |
0 |
≥ 0 (mbar) | air density correction |
| Alpha factor | diffuser.alfaFactor |
0.7 |
0–1 | oxygen-transfer correction |
| Local atm. pressure | diffuser.localAtmPressure |
1013.25 |
> 0 (mbar) | density baseline |
| Water density | diffuser.waterDensity |
997 |
> 0 (kg/m³) | static head |
| Zone volume | diffuser.zoneVolume |
0 |
≥ 0 (m³) | oZoneOtr |
10. State chart
Skipped — diffuser is stateless. Every input setter recomputes the full output snapshot; there are no transitions to track. The idle flag is a derived predicate (i_flow ≤ 0), not a state.
11. Examples
| Tier | File | What it shows | Mandatory? |
|---|---|---|---|
| Basic | examples/01-Basic.flow.json |
Inject data.flow + dashboard, no parent |
✅ |
| Integration | examples/02-Integration.flow.json |
diffuser registered under a reactor zone | ✅ |
| Dashboard | examples/03-Dashboard.flow.json |
Live FlowFuse charts (OTR, kg O₂/h, efficiency) | ⭕ |
Screenshots under wiki/_partial-screenshots/diffuser/ when produced. Docker compose snippet under examples/README.md.
12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
oOtr stuck at zero |
i_flow is zero or negative → idle = true. |
_recalculate early-return |
| Warning / alarm always firing | Flow-per-element outside curve minX / maxX ± hysteresis. |
_checkLimits |
oZoneOtr is zero despite valid OTR |
diffuser.zoneVolume is unset or non-positive. |
getReactorOtr |
nFlow differs from iFlow at non-zero flow |
Air-density correction — header pressure or atm differ from reference. | _calcAirDensityMbar |
efficiency flat at 0 |
OTR or ΔP curve span is zero in the operating band. | _combineEff |
Never ship
enableLog: 'debug'in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging.
13. When you would NOT use this node
- Use diffuser for a fine-bubble aeration zone with a supplier OTR curve. For coarse-bubble or jet aeration, model OTR externally.
- Don't use diffuser when the upstream blower already publishes oxygen-transfer telemetry — diffuser duplicates the calculation.
- Skip diffuser if you only need flow-per-element warning bands without OTR — a
measurementnode with bands is lighter.
14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | Port-count change (Phase 6): pre-refactor the diffuser exposed 4 outputs (process, dbase, reactor control with topic: 'OTR', parent registration). The reactor-control message merged into Port 0 as oZoneOtr; consumers reading the dedicated control port must migrate to payload.oZoneOtr. No alias is provided — the shape differs (single value vs full process payload). |
CONTRACT.md ## Port-count change |
| 2 | Supplier specs are hard-coded inside _loadSpecs() (GVA / ELASTOX-R). A configurable supplier registry is pending. |
specificClass.js _loadSpecs |
| 3 | No typed MeasurementContainer emission — oOtr / oZoneOtr cannot be subscribed via the generic ChildRouter handshake. Parents must read Port 0 messages. |
CONTRACT.md ## Events emitted |
| 4 | Warning / alarm thresholds are fixed (2 % / 10 % hysteresis); not yet config-driven. | configure() literals |