Files
valve/wiki/Reference-Architecture.md
znetsixe 87214788d2 docs(wiki): full 5-page wiki matching the rotatingMachine reference format
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>
2026-05-19 09:42:11 +02:00

14 KiB
Raw Blame History

Reference — Architecture

code-ref

Note

Code structure for valve: the three-tier sandwich, the src/ 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.md and 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 as rotatingMachine, with accelerating / decelerating reused 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 valve adopts the sequenceAbortToken mechanism from rotatingMachine (2026-05-15) for mid-shutdown re-engage races is an open question. TODO: confirm from generalFunctions/src/state/state.js whether 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:

  1. host.kv = host.curvePredictor.predictKvForPosition(x) — Kv lookup against the supplier curve.
  2. MeasurementRouter.updateDeltaP(currentFlow, kv, downstreamP) — recompute the hydraulic deltaP and write pressure.predicted.delta.
  3. 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:

  1. assetResolver.resolveAssetMetadata('valve', model) resolves supplier / type / allowed units from generalFunctions/datasets/assetData/may return null for valve; the predictor tolerates an inline asset.valveCurve fallback.
  2. SupplierCurvePredictor is constructed with the model, the inline curve override, density, temperature, and valve diameter.
  3. predictKv (the curve-evaluation function) is exposed on the host; host.curveSelection records 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 2024.

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's flowController does not currently gate by allowedActions — only by source. The schema defines mode.allowedActions but it isn't enforced in flowController.handleInput. TODO: confirm intentional or backlog. Source: nodes/valve/src/flow/flowController.js lines 1824.


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

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