Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
Reference — Architecture
Note
Code structure for
valve: the three-tier sandwich, thesrc/layout, the position FSM, the hydraulic-model pipeline, the lifecycle, and the output ports. For an intuitive overview, return to Home.Pending full node review (2026-05). Content reflects
CONTRACT.mdand current source only.
Three-tier code layout
nodes/valve/
|
+-- valve.js entry: RED.nodes.registerType('valve', NodeClass)
|
+-- src/
| nodeClass.js extends BaseNodeAdapter (Node-RED bridge)
| specificClass.js extends BaseDomain (orchestration only)
| hydraulicModel.js ValveHydraulicModel + normalizeServiceType
| |
| +-- commands/
| | index.js topic descriptors
| | handlers.js pure handler functions
| |
| +-- curve/
| | supplierCurve.js SupplierCurvePredictor (Kv-vs-position load + interp)
| |
| +-- fluid/
| | fluidCompatibility.js FluidCompatibility — upstream service-type aggregation
| |
| +-- measurement/
| | measurementRouter.js MeasurementRouter + FORMULA_UNITS
| |
| +-- flow/
| | flowController.js handleInput, executeSequence, setpoint (pre-shutdown ramp)
| |
| +-- state/
| | stateBindings.js wires state.emitter('positionChange') → updatePosition()
| |
| +-- io/
| output.js buildOutput + buildStatusBadge
Tier responsibilities
| Tier | File | What it owns | Touches RED.* |
|---|---|---|---|
| entry | valve.js |
Type registration | Yes |
| nodeClass | src/nodeClass.js |
UI-config → domain config, legacy-asset-field reject, status-badge polling (statusInterval=1000). No tick loop (tickInterval=null) — event-driven. |
Yes |
| specificClass | src/specificClass.js |
Wire concern modules in configure(); expose the public surface tests + parents (VGC) depend on (handleInput, setMode, updatePosition, updateFlow, updatePressure, registerChild, showCurve, getOutput, …). Overrides BaseDomain.registerChild so upstream-source registration falls into FluidCompatibility instead of the generic ChildRouter. |
No |
specificClass is stitching. All real work lives in the concern modules: position bindings in state/, deltaP math in hydraulicModel.js, Kv interpolation in curve/, measurement → deltaP plumbing in measurement/, mode + sequences in flow/, fluid contract aggregation in fluid/, and Port-0 shaping in io/.
FSM
Note
The state machine is declared in
generalFunctions/src/state/stateConfig.json. Same allowed-transition graph asrotatingMachine, withaccelerating/deceleratingreused for position moves up / down.
stateDiagram-v2
[*] --> idle
idle --> starting: cmd.startup
idle --> off
idle --> maintenance
starting --> warmingup: timer (time.starting)
warmingup --> operational: timer (time.warmingup) [protected]
operational --> accelerating: set.position up
operational --> decelerating: set.position down
operational --> stopping: cmd.shutdown
accelerating --> operational: target reached
decelerating --> operational: target reached
stopping --> coolingdown: timer (time.stopping)
coolingdown --> idle: timer (time.coolingdown) [protected]
coolingdown --> off
off --> idle: boot
off --> maintenance
maintenance --> off
maintenance --> idle
note right of operational
any state -> emergencystop via cmd.estop
from emergencystop: idle / off / maintenance
end note
Default sequences (from valve.json):
| Sequence | States |
|---|---|
startup |
[starting, warmingup, operational] |
shutdown |
[stopping, coolingdown, idle] |
emergencystop |
[emergencystop, off] |
boot |
[idle, starting, warmingup, operational] |
Pre-shutdown ramp to zero
FlowController.executeSequence('shutdown') checks the FSM. When the valve is operational it first calls setpoint(0) — the position-ramp to fully closed is interruptible — then iterates the sequence states.
Protected states
warmingup and coolingdown are protected at the state-machine layer (same mechanism as rotatingMachine). Aborts during these phases are ignored to preserve safety guarantees.
Note
Whether
valveadopts thesequenceAbortTokenmechanism fromrotatingMachine(2026-05-15) for mid-shutdown re-engage races is an open question. TODO: confirm fromgeneralFunctions/src/state/state.jswhether valve inherits the token automatically. Source:nodes/valve/src/flow/flowController.js.
Position-move bindings
src/state/stateBindings.js wires the underlying state machine's positionChange event to host.updatePosition(). Every position tick triggers:
host.kv = host.curvePredictor.predictKvForPosition(x)— Kv lookup against the supplier curve.MeasurementRouter.updateDeltaP(currentFlow, kv, downstreamP)— recompute the hydraulic deltaP and writepressure.predicted.delta.host.emitter.emit('deltaPChange', deltaP)— upward to the parent VGC.
updatePosition() is a no-op outside of operational / accelerating / decelerating (see MeasurementRouter.updatePositionDependent).
Hydraulic + measurement pipeline
flowchart TB
set[set.position]:::input --> fc[FlowController.setpoint]
fc --> moveTo[state.moveTo]
moveTo --> tick[state.emitter 'positionChange']
tick --> upd[updatePosition]
upd --> kv[curvePredictor.predictKvForPosition]
fdat[data.flow]:::input --> mr[MeasurementRouter.updateFlow]
fpres[measurement child<br/>pressure.measured.*]:::input --> mp[MeasurementRouter.updatePressure]
mr --> dp[updateDeltaP]
mp --> dp
kv --> dp
dp --> hyd[ValveHydraulicModel<br/>calculateDeltaPMbar]
hyd --> write[write pressure.predicted.delta]
write --> emit[emitter 'deltaPChange']
write --> out[Port 0]
classDef input fill:#a9daee,color:#000
Curve loading
At configure() startup:
assetResolver.resolveAssetMetadata('valve', model)resolves supplier / type / allowed units fromgeneralFunctions/datasets/assetData/— may return null for valve; the predictor tolerates an inlineasset.valveCurvefallback.SupplierCurvePredictoris constructed with the model, the inline curve override, density, temperature, and valve diameter.predictKv(the curve-evaluation function) is exposed on the host;host.curveSelectionrecords which(densityKey, diameterKey)lane of the dataset is in use.
The asset.valveCurve schema is a nested map keyed by gas-density (kg per nm³) and valve diameter (mm); the leaf carries {x: [position%], y: [Kv (m³/h)]} lookup tables.
Hydraulic formula selection
ValveHydraulicModel.calculateDeltaPMbar picks one of two formulas by serviceType:
| serviceType | Formula | Notes |
|---|---|---|
liquid |
deltaP_bar = (Q / Kv)^2 * (rho / 1000) |
Density override via runtimeOptions.fluidDensity (default 997 kg/m³). |
gas |
deltaP_bar = (Q^2 * rho * T) / (514^2 * Kv^2 * P2_abs) |
Density (default 1.204), absolute downstream pressure, temperature K. Capped at gasChokedRatioLimit * P2_abs when choked. |
Inputs are validated: kv > 0, flow !== 0, and (for gas) a finite downstream gauge pressure are required — otherwise the function returns null and the router skips the write.
Formula units are pinned
measurement/measurementRouter.js declares:
const FORMULA_UNITS = Object.freeze({ pressure: 'mbar', flow: 'm3/h', temperature: 'K' });
The hydraulic model expects q in m³/h, downstream gauge in mbar, and T in K. The router reads MeasurementContainer values back in these units before calling calculateDeltaPMbar regardless of the per-node unitPolicy.output.* rendering choices.
Unit policy
Source: src/specificClass.js lines 20–24.
| Quantity | Canonical (internal) | Output (rendered) | Required-unit |
|---|---|---|---|
| Pressure | Pa |
mbar |
✓ |
| Flow | m3/s |
m3/h |
✓ |
| Temperature | K |
C |
✓ |
requireUnitForTypes means MeasurementContainer rejects writes that omit unit for these types.
Lifecycle — what one event does
sequenceDiagram
autonumber
participant parent as VGC / GUI
participant v as valve
participant fc as flowController
participant fsm as state (FSM)
participant hyd as hydraulicModel
participant out as Port 0 / parent
parent->>v: set.position {setpoint: 60}
v->>fc: flowController.handleInput('parent','execMovement', 60)
fc->>fc: isValidSourceForMode check
fc->>fsm: setpoint(60) → state.moveTo(60)
fsm-->>v: positionChange events per move tick
v->>v: kv = curvePredictor.predictKvForPosition(pos)
v->>hyd: calculateDeltaPMbar(q, kv, downP, rho, T)
hyd-->>v: { deltaPMbar, details }
v->>v: write pressure.predicted.delta
v->>parent: emitter.emit('deltaPChange', deltaP)
v->>out: notifyOutputChanged (Port 0 delta)
parent->>v: data.flow {variant, value, position, unit}
v->>v: MeasurementRouter.updateFlow → updateDeltaP
Mode + source allow-lists
Each input is gated in flowController.handleInput:
if (!this.isValidSourceForMode(source, this.host.currentMode)) {
this.logger.warn(`Source '${source}' is not valid for mode '${currentMode}'.`);
return { status: false, feedback: msg };
}
Defaults (per valve.json mode.allowedSources):
| Mode | Allowed sources |
|---|---|
auto |
parent, GUI, fysical |
virtualControl |
GUI, fysical |
fysicalControl |
fysical |
maintenance |
(no entry — no source accepted; only statusCheck action allowed) |
A rejected request logs at warn and short-circuits.
Note
Unlike
rotatingMachine,valve'sflowControllerdoes not currently gate byallowedActions— only by source. The schema definesmode.allowedActionsbut it isn't enforced inflowController.handleInput. TODO: confirm intentional or backlog. Source:nodes/valve/src/flow/flowController.jslines 18–24.
Output ports
| Port | Carries | Sample shape |
|---|---|---|
| 0 (process) | Delta-compressed state snapshot — FSM state, position %, mode, every populated MeasurementContainer slot | {topic, payload: {state, percentageOpen, moveTimeleft, mode, delta_predicted_pressure, downstream_measured_flow, ...}} |
| 1 (telemetry) | InfluxDB line-protocol payload (same fields as Port 0) | valve,id=valve_a state="operational",percentageOpen=60,delta_predicted_pressure=84,... |
| 2 (register / control) | child.register upward at startup; positionVsParent and optional distance carried on the msg |
{topic: 'child.register', payload: <node.id>, positionVsParent, distance} |
Port-0 key shape is <position>_<variant>_<type> (legacy three-segment). Examples: delta_predicted_pressure, downstream_measured_flow, downstream_predicted_pressure. Only keys with finite values are emitted — consumers must cache and merge.
On query.curve the node additionally emits {topic: 'Showing curve', payload: <SupplierCurvePredictor.snapshot()>} synchronously on Port 0.
See EVOLV — Telemetry for the full InfluxDB layout.
Event sources
| Source | Where it fires | What it triggers |
|---|---|---|
state.emitter 'positionChange' |
movementManager during a position move |
updatePosition() — Kv lookup, deltaP recompute, Port 0 |
state.emitter 'stateChange' |
stateManager.transitionTo resolve |
Logged; getOutput() picks up the new state value on the next tick |
source.emitter 'deltaPChange' |
MeasurementRouter.updateDeltaP after a finite deltaP |
Consumed by valveGroupControl to update group totals |
source.emitter 'fluidCompatibilityChange' |
FluidCompatibility on upstream-source contract change |
Consumed by parent for service-type aggregation |
source.emitter 'fluidContractChange' |
FluidCompatibility when the contract this valve advertises downstream changes |
Consumed by downstream consumers |
source.measurements.emitter '<type>.<variant>.<position>' |
MeasurementContainer write | Generic handshake; parents subscribe via child.measurements.emitter.on |
Inbound msg.topic |
Node-RED input wire | commandRegistry dispatch |
setInterval(statusInterval = 1000) |
BaseNodeAdapter |
Status badge re-render |
No per-second tick on the domain itself. Position moves drive their own animation interval inside movementManager.
Where to start reading
| If you're changing... | Read first |
|---|---|
| Kv curve load / inline-curve fallback | src/curve/supplierCurve.js |
| Liquid / gas deltaP math, choke cap | src/hydraulicModel.js |
| Measurement → deltaP plumbing (when a recompute fires) | src/measurement/measurementRouter.js |
| Position-tick → updatePosition wiring | src/state/stateBindings.js |
| Mode allow-list, setpoint, executeSequence, pre-shutdown ramp | src/flow/flowController.js |
| Upstream-source fluid tracking, contract aggregation | src/fluid/fluidCompatibility.js |
query.curve reply / status badge / Port 0 shape |
src/io/output.js |
| Topic registration, payload validation, aliases | src/commands/{index, handlers}.js |
Related pages
| Page | Why |
|---|---|
| Home | Intuitive overview |
| Reference — Contracts | Topic + config + child filters |
| Reference — Examples | Shipped flows + debug recipes |
| Reference — Limitations | Known issues and open questions |
| valveGroupControl wiki | The grouped-control parent |
| EVOLV — Architecture | Platform-wide three-tier pattern |