Files
valve/wiki/Home.md
znetsixe 95ccc77b25 docs(wiki): rewrite Home.md — correct FSM states + config keys for Section 9/10
Section 10 (State chart): replace invented opening/closing/closed states with
the real shared FSM states (accelerating/decelerating for moves; idle/starting/
warmingup/operational/stopping/coolingdown/emergencystop/off/maintenance for
lifecycle). Show all valid transitions from stateConfig.json allowedTransitions.
Document protected transitions (warmingup, coolingdown) and valve-specific
pre-shutdown ramp-to-zero behaviour.

Section 9 (Config): add missing editor fields from nodeClass.buildDomainConfig
(startup/warmup/shutdown/cooldown times, speed, serviceType, fluidDensity,
fluidTemperatureK, gasChokedRatioLimit). Correct config paths to match actual
stateConfig / runtimeOptions split.

Section 7 (Lifecycle): add FSM state labels to sequence diagram; show
accelerating → operational final step.

Sections 2/6/12/14: minor precision improvements (Port-2 note, abort-deadlock
recipe, execSequence Phase-7 removal warning).

Re-ran npm run wiki:all; AUTOGEN blocks intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:06:26 +02:00

314 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# valve
> **Reflects code as of `e27135b` · regenerated `2026-05-11` via `npm run wiki:all`**
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
## 1. What this node is
**valve** models a single actuated throttling valve at the S88 Equipment Module level. It loads a supplier Kv-vs-position characteristic curve, drives an FSM for open/close move sequences (using `accelerating`/`decelerating` states shared with `rotatingMachine`), and recomputes pressure drop from flow + Kv via a hydraulic model. Used standalone or as a child of `valveGroupControl`.
## 2. Position in the platform
```mermaid
flowchart LR
vgc[valveGroupControl<br/>Unit]:::unit -->|set.position| this[valve<br/>Equipment]:::equip
src["machine / MGC / PS<br/>(upstream source)"]:::unit -->|child.register| this
meas[measurement<br/>type=pressure / flow]:::ctrl -.->|data| this
this -->|child.register Port 2| vgc
this -->|evt.deltaPChange| vgc
this -->|evt.fluidCompatibilityChange| vgc
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: `.claude/rules/node-red-flow-layout.md`.
## 3. Capability matrix
| Capability | Status | Notes |
|---|---|---|
| Predicts deltaP from flow + Kv | ✅ | Hydraulic model picks liquid vs gas formula per `serviceType`. |
| Loads supplier curve by model name | ✅ | `asset.model` resolved through `loadModel`; inline `valveCurve` override supported. |
| Position move FSM | ✅ | `accelerating` / `decelerating` states with interruptible setpoints; `moveTo` uses shared state machine. |
| Startup / shutdown sequences | ✅ | Pre-shutdown ramps valve to position 0 before executing stop sequence. |
| Emergency-stop sequence | ✅ | `cmd.estop``emergencystop → off` sequence. |
| Fluid-contract aggregation | ✅ | Tracks upstream service type via registered sources through `FluidCompatibility`. |
| Gas-choke detection | ⚠️ | Hard cap at `gasChokedRatioLimit`; surfaced in `hydraulicDiagnostics`. |
| Multi-parent registration | ⚠️ | Allowed but not exercised in production tests. |
## 4. Code map
```mermaid
flowchart TB
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
nc["buildDomainConfig()<br/>static DomainClass=Valve, commands<br/>tickInterval=null (event-driven)"]
end
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
sc["Valve.configure()<br/>wires concern modules<br/>overrides registerChild → FluidCompatibility"]
end
subgraph concerns["src/ concern modules"]
state["state/<br/>stateBindings → positionChange"]
fluid["fluid/<br/>FluidCompatibility"]
curve["curve/<br/>SupplierCurvePredictor"]
meas["measurement/<br/>MeasurementRouter + FORMULA_UNITS"]
flow["flow/<br/>FlowController (setpoint, sequences)"]
io["io/<br/>buildOutput + buildStatusBadge"]
hyd["hydraulicModel.js<br/>ValveHydraulicModel"]
end
nc --> sc
sc --> state
sc --> fluid
sc --> curve
sc --> meas
sc --> flow
sc --> io
sc --> hyd
```
| Module | Owns | Read first if you're changing… |
|---|---|---|
| `state/` | Bindings from state-machine `positionChange``updatePosition()` | Move-finished triggers, position callbacks. |
| `fluid/` | Service-type compatibility, contract aggregation | Gas-vs-liquid mismatch warnings, upstream fluid tracking. |
| `curve/` | Supplier Kv curve load + interpolation | Curve fitting, model selection, density keys. |
| `measurement/` | Pressure/flow routing + deltaP recompute | What triggers a recalc, measurement container writes. |
| `flow/` | Sequence + setpoint execution, mode validation | Startup / shutdown / move semantics, allowed-source checks. |
| `io/` | Port-0 output shape + status badge | What lands on the wire each tick. |
| `hydraulicModel.js` | Liquid + gas deltaP formulas, choke detection | Hydraulic calculation errors, choke ratio behaviour. |
## 5. Topic contract
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
<!-- BEGIN AUTOGEN: topic-contract -->
| Canonical topic | Aliases | Payload | Unit | Effect |
|---|---|---|---|---|
| `set.mode` | `setMode` | `string` | — | Switch the valve between auto / manual control modes. |
| `cmd.startup` | _(none)_ | `any` | — | Initiate the valve startup sequence. |
| `cmd.shutdown` | _(none)_ | `any` | — | Initiate the valve shutdown sequence. |
| `cmd.estop` | `emergencystop`, `emergencyStop` | `any` | — | Trigger an emergency stop on the valve. |
| `execSequence` | _(none)_ | `object` | — | Legacy umbrella that demuxes payload.action to startup / shutdown / estop. |
| `set.position` | `execMovement` | `object` | — | Move the valve to a control-% position via execMovement. |
| `data.flow` | `updateFlow` | `object` | — | Push a measured flow into the valve (variant + position + unit). |
| `query.curve` | `showcurve` | `any` | — | Return the valve characteristic curve on the reply port. |
| `child.register` | `registerChild` | `string` | — | Register a child measurement with this valve. |
<!-- END AUTOGEN: topic-contract -->
## 6. Child registration
valve overrides `BaseDomain.registerChild` with `FluidCompatibility.registerChild`. Upstream sources feed the fluid-contract aggregator; measurement children attach through the standard measurement handshake and land in `MeasurementRouter`.
```mermaid
flowchart LR
subgraph kids["accepted children (softwareType)"]
src["machine / rotatingmachine<br/>machinegroup / pumpingstation<br/>valvegroupcontrol"]:::unit
m["measurement<br/>type=pressure or flow"]:::ctrl
end
src -->|getFluidContract| fluid[FluidCompatibility<br/>aggregates serviceType]
m -->|"&lt;type&gt;.measured.&lt;position&gt;"| router[MeasurementRouter<br/>updatePressure / updateFlow]
router --> deltaP[updateDeltaP<br/>writes pressure.predicted.delta]
fluid --> evt1["evt.fluidCompatibilityChange<br/>evt.fluidContractChange"]
deltaP --> evt2[evt.deltaPChange]
classDef unit fill:#50a8d9,color:#000
classDef ctrl fill:#a9daee,color:#000
```
| softwareType | onRegister side-effect | Subscribed events |
|---|---|---|
| `machine` / `rotatingmachine` | Stored as upstream source; reads `getFluidContract()` or defaults to `liquid`. | `fluidContractChange` |
| `machinegroup` / `machinegroupcontrol` | Same; recomputes aggregate service type. | `fluidContractChange` |
| `pumpingstation` | Same. | `fluidContractChange` |
| `valvegroupcontrol` | Same. | `fluidContractChange` |
| `measurement` | Routed via measurement handshake; values land in `MeasurementContainer`. | `<type>.measured.<position>` |
## 7. Lifecycle — what one event does
```mermaid
sequenceDiagram
participant parent as valveGroupControl
participant valve as valve
participant fsm as state FSM
participant hyd as hydraulicModel
participant out as Port-0
parent->>valve: set.position { setpoint: 60 }
valve->>fsm: moveTo(60)
fsm-->>fsm: operational → accelerating
fsm-->>valve: positionChange ticks (stateBindings)
valve->>valve: predictKv(position)
valve->>hyd: calculateDeltaPMbar(q, kv, downP, rho, T)
hyd-->>valve: { deltaPMbar, diagnostics }
valve->>valve: write pressure.predicted.delta
valve->>parent: emitter.emit('deltaPChange', deltaP)
valve->>out: msg { topic, payload (delta-compressed) }
fsm-->>fsm: accelerating → operational (setpoint reached)
```
## 8. Data model — `getOutput()`
What lands on Port 0. Composed in `io/output.buildOutput`, then delta-compressed by `outputUtils.formatMsg`.
<!-- BEGIN AUTOGEN: data-model -->
| Key | Type | Unit | Sample |
|---|---|---|---|
| `state` | string | — | `"operational"` |
| `percentageOpen` | number | % | `0` |
| `moveTimeleft` | number | s | `0` |
| `mode` | string | — | `"auto"` |
| `downstream_predicted_flow` | number | m3/h | `0` |
| `downstream_measured_flow` | number | m3/h | _(emitted when measurement child present)_ |
| `downstream_predicted_pressure` | number | mbar | _(emitted when upstream pressure present)_ |
| `downstream_measured_pressure` | number | mbar | _(emitted when measurement child present)_ |
| `delta_predicted_pressure` | number | mbar | `0` |
<!-- END AUTOGEN: data-model -->
Measurement keys follow the legacy `<position>_<variant>_<type>` shape (e.g. `downstream_predicted_flow`, `delta_predicted_pressure`). Only keys with finite values are emitted — consumers must cache and merge (delta-compression is active).
## 9. Configuration — editor form ↔ config keys
```mermaid
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Reaction Speed]
f2[Asset model / supplier / category]
f3[Service type]
f4[Fluid density / temperature K]
f5[Gas choke ratio limit]
f6[Startup / warmup / shutdown / cooldown times]
f7[Log level / enableLog]
f8[positionVsParent]
end
subgraph config["Domain config slice"]
c1["movement.speed (stateConfig)"]
c2[asset.model]
c3["runtimeOptions.serviceType → hydraulicModel"]
c4["runtimeOptions.fluidDensity / fluidTemperatureK"]
c5["runtimeOptions.gasChokedRatioLimit"]
c6["stateConfig.time.starting / warmingup / stopping / coolingdown"]
c7["general.logging.enabled / logLevel"]
c8["functionality.positionVsParent → Port-2 registration"]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
f7 --> c7
f8 --> c8
```
| Form field | Config path | Default | Range / type | Where used |
|---|---|---|---|---|
| Reaction Speed | `movement.speed` (stateConfig) | `1` | > 0 (%/s) | `MovementManager` — sets rate of position change |
| Asset model | `asset.model` | `'Unknown'` | string | `SupplierCurvePredictor` — selects Kv curve dataset |
| Service type | `runtimeOptions.serviceType` | `null` (from asset) | `'gas'` / `'liquid'` | `ValveHydraulicModel` formula selection |
| Fluid density | `runtimeOptions.fluidDensity` | model default | > 0 (kg/m³) | liquid hydraulic formula |
| Fluid temperature | `runtimeOptions.fluidTemperatureK` | model default | > 0 (K) | gas hydraulic formula |
| Gas choke limit | `runtimeOptions.gasChokedRatioLimit` | per asset | 01 | gas choke cap in `ValveHydraulicModel` |
| Startup time | `stateConfig.time.starting` | `10` s | > 0 (s) | `StateManager` transition timer |
| Warmup time | `stateConfig.time.warmingup` | `5` s | > 0 (s) | `StateManager` protected transition |
| Shutdown time | `stateConfig.time.stopping` | `5` s | > 0 (s) | `StateManager` transition timer |
| Cooldown time | `stateConfig.time.coolingdown` | `10` s | > 0 (s) | `StateManager` transition timer |
| Mode | `mode.current` | `'auto'` | `auto` / `virtualControl` / `fysicalControl` / `maintenance` | `FlowController.isValidSourceForMode` |
| Log level | `general.logging.logLevel` | `'info'` | enum | structured logger |
| positionVsParent | `functionality.positionVsParent` | `'atEquipment'` | enum | Port-2 registration message to parent |
## 10. State chart
```mermaid
stateDiagram-v2
[*] --> off
off --> idle : cmd.startup (boot sequence)
off --> emergencystop : cmd.estop
off --> maintenance : set.mode=maintenance
idle --> starting : cmd.startup
idle --> off : (direct transition)
idle --> emergencystop : cmd.estop
idle --> maintenance : set.mode=maintenance
starting --> warmingup : timed (starting duration)
starting --> emergencystop : cmd.estop
warmingup --> operational : timed (warmup duration) [protected — cannot abort]
warmingup --> emergencystop : cmd.estop
operational --> accelerating : set.position > current
operational --> decelerating : set.position < current
operational --> stopping : cmd.shutdown
operational --> emergencystop : cmd.estop
accelerating --> operational : setpoint reached
accelerating --> emergencystop : cmd.estop
decelerating --> operational : setpoint reached
decelerating --> emergencystop : cmd.estop
stopping --> coolingdown : timed (stopping duration)
stopping --> idle : (direct)
stopping --> emergencystop : cmd.estop
coolingdown --> idle : timed (cooldown duration) [protected — cannot abort]
coolingdown --> off : (direct)
coolingdown --> emergencystop : cmd.estop
emergencystop --> idle : cmd.reset / sequence
emergencystop --> off : cmd.reset / sequence
emergencystop --> maintenance : (allowed)
maintenance --> idle : manual reset
maintenance --> off : manual reset
```
**Key valve-specific behaviours:**
- `accelerating` = position moving up; `decelerating` = position moving down. Both fire `positionChange` ticks. The valve's `stateBindings` hooks these to `updatePosition()` → Kv lookup → deltaP recompute.
- `warmingup` and `coolingdown` are **protected** — the abort signal is disabled; these phases cannot be interrupted.
- `cmd.shutdown` from `operational` first ramps the valve to position 0 (via `FlowController.executeSequence('shutdown')`), then transitions `stopping → coolingdown → idle`.
- `cmd.estop` triggers `emergencystop → off` regardless of current state (except from within protected transitions).
- Default sequences: `startup` = `[starting, warmingup, operational]`; `boot` = `[idle, starting, warmingup, operational]`; `emergencystop` = `[emergencystop, off]`.
## 11. Examples
| Tier | File | What it shows | Mandatory? |
|---|---|---|---|
| Basic | `examples/basic.flow.json` | Inject `set.position` + minimal wiring, no parent | ✅ |
| Integration | `examples/integration.flow.json` | valve + VGC + upstream measurement source | ✅ |
| Edge | `examples/edge.flow.json` | Edge-case inputs (gas, choke, estop, bad setpoints) | ⭕ optional |
Renamed example files (`01-Basic.flow.json`, `02-Integration.flow.json`, `03-Dashboard.flow.json`) will replace the above when produced. Screenshots under `wiki/_partial-screenshots/valve/`. Docker compose snippet under `examples/README.md`.
## 12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| Status badge shows `⚠ no input` | Did any pressure / flow measurement register? Watch Port 2. | Debug tap on Port 2 |
| `delta_predicted_pressure` stuck at `0` | Is `kv > 0`? FSM may be in `off` / `idle` — valve is closed. | `state.getCurrentState()`, `percentageOpen` |
| Gas mismatch warning on status badge | `fluidCompatibility.status` is `mismatch` or `conflict`. | `getFluidCompatibility()` |
| `query.curve` returns empty curve | `asset.model` not found by `loadModel`; check `SupplierCurvePredictor.snapshot()`. | `SupplierCurvePredictor.snapshot()` |
| deltaP non-finite | Downstream gauge pressure absolute term ≤ 0, or choked ratio reached. | `hydraulicDiagnostics` in output |
| `set.position` has no effect | Check `currentMode` — source may not be in `mode.allowedSources[mode]`. | `FlowController.isValidSourceForMode` |
| FSM stuck in `accelerating` / `decelerating` | Movement was aborted but `_returnToOperationalOnAbort` was false. Send a new `set.position`. | `state.js` abort logic |
> 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 valve for a **throttling actuator** with a known Kv curve. For a fixed-restriction orifice (no actuator, no curve), model the deltaP externally.
- Don't use valve to model a **non-return / check valve** — no position control or FSM-driven actuation is exposed.
- Skip valve when an upstream source already provides flow directly and **no pressure-drop estimate is needed** — wire the source straight to the parent without inserting a valve.
## 14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | Gas-choke detection is a hard cap, not a smooth transition — chart traces show a step at the choked-ratio limit. | `hydraulicModel.js` |
| 2 | Multi-parent registration is allowed but not exercised in production tests. | `CONTRACT.md` — Children registered by this node |
| 3 | `set.position` move sequences are interruptible but tests cover happy-path only; abort-deadlock edge case documented separately. | `state.js` abort logic + `test/integration/` |
| 4 | `execSequence` (legacy umbrella topic) will be removed in Phase 7 — callers must migrate to `cmd.startup` / `cmd.shutdown` / `cmd.estop`. | `CONTRACT.md` — execSequence demux |