From 95ccc77b258ce386e43619c0ecaca13979eea371 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Mon, 11 May 2026 21:06:26 +0200 Subject: [PATCH] =?UTF-8?q?docs(wiki):=20rewrite=20Home.md=20=E2=80=94=20c?= =?UTF-8?q?orrect=20FSM=20states=20+=20config=20keys=20for=20Section=209/1?= =?UTF-8?q?0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- wiki/Home.md | 208 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 131 insertions(+), 77 deletions(-) diff --git a/wiki/Home.md b/wiki/Home.md index fc73c7a..b6647f7 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -5,35 +5,36 @@ ## 1. What this node is -**valve** models a single throttling valve. It loads a supplier characteristic curve (Kv-vs-position), drives an FSM-style move sequence for opening/closing, and recomputes pressure drop across the valve from current flow + Kv. Used standalone or as a child of `valveGroupControl` when grouped. +**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
Unit]:::unit -->|set.position| this[valve
Equipment]:::equip - src[machine / MGC / PS
upstream source]:::unit -->|child.register| this - meas[measurement
type=pressure / flow]:::ctrl -.data.-> this - this -->|child.register| vgc + src["machine / MGC / PS
(upstream source)"]:::unit -->|child.register| this + meas[measurement
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 of truth: `.claude/rules/node-red-flow-layout.md`. +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 curve override supported. | -| Position move FSM | ✅ | `opening` / `closing` states with interruptible setpoints. | -| Startup / shutdown sequences | ✅ | Pre-shutdown ramps to position 0 when operational. | -| Emergency-stop sequence | ✅ | Aliased `cmd.estop` → state-machine `emergencystop`. | -| Fluid-contract aggregation | ✅ | Tracks upstream service type via registered sources. | -| Gas-choke detection | ⚠️ | Capped at `gasChokedRatioLimit`; surfaced in `hydraulicDiagnostics`. | +| 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 @@ -41,10 +42,10 @@ S88 colours: Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Sour ```mermaid flowchart TB subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] - nc["buildDomainConfig()
static DomainClass, commands"] + nc["buildDomainConfig()
static DomainClass=Valve, commands
tickInterval=null (event-driven)"] end subgraph domain["specificClass.js — orchestrator (BaseDomain)"] - sc["Valve.configure()
wires concern modules
installs FluidCompatibility registerChild"] + sc["Valve.configure()
wires concern modules
overrides registerChild → FluidCompatibility"] end subgraph concerns["src/ concern modules"] state["state/
stateBindings → positionChange"] @@ -67,12 +68,13 @@ flowchart TB | Module | Owns | Read first if you're changing… | |---|---|---| -| `state/` | Bindings from state-machine `positionChange` → `updatePosition()` | Move-finished triggers. | -| `fluid/` | Service-type compatibility, contract aggregation | Gas-vs-liquid mismatch warnings. | -| `curve/` | Supplier Kv curve load + interpolation | Curve fitting, model selection. | -| `measurement/` | Pressure/flow routing + deltaP recompute | What triggers a recalc. | -| `flow/` | Sequence + setpoint execution | Startup / shutdown / move semantics. | +| `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 @@ -96,18 +98,18 @@ flowchart TB ## 6. Child registration -valve overrides BaseDomain's default `registerChild` with `FluidCompatibility.registerChild` so upstream-source contracts feed the fluid aggregator. Measurement children attach through the generic measurement handshake. +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 /
machinegroup / pumpingstation /
valvegroupcontrol"]:::unit - m["measurement"]:::ctrl + src["machine / rotatingmachine
machinegroup / pumpingstation
valvegroupcontrol"]:::unit + m["measurement
type=pressure or flow"]:::ctrl end src -->|getFluidContract| fluid[FluidCompatibility
aggregates serviceType] m -->|"<type>.measured.<position>"| router[MeasurementRouter
updatePressure / updateFlow] router --> deltaP[updateDeltaP
writes pressure.predicted.delta] - fluid --> evt1[evt.fluidCompatibilityChange] + fluid --> evt1["evt.fluidCompatibilityChange
evt.fluidContractChange"] deltaP --> evt2[evt.deltaPChange] classDef unit fill:#50a8d9,color:#000 classDef ctrl fill:#a9daee,color:#000 @@ -115,11 +117,11 @@ flowchart LR | softwareType | onRegister side-effect | Subscribed events | |---|---|---| -| `machine` / `rotatingmachine` | Stored as upstream source; reads `getFluidContract()` or default `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. | `.measured.`. | +| `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`. | `.measured.` | ## 7. Lifecycle — what one event does @@ -127,19 +129,21 @@ flowchart LR sequenceDiagram participant parent as valveGroupControl participant valve as valve - participant state as state FSM + participant fsm as state FSM participant hyd as hydraulicModel participant out as Port-0 parent->>valve: set.position { setpoint: 60 } - valve->>state: moveTo(60) - state-->>valve: positionChange ticks + 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, details } + hyd-->>valve: { deltaPMbar, diagnostics } valve->>valve: write pressure.predicted.delta valve->>parent: emitter.emit('deltaPChange', deltaP) - valve->>out: msg{topic, payload (delta-compressed)} + valve->>out: msg { topic, payload (delta-compressed) } + fsm-->>fsm: accelerating → operational (setpoint reached) ``` ## 8. Data model — `getOutput()` @@ -162,27 +166,31 @@ What lands on Port 0. Composed in `io/output.buildOutput`, then delta-compressed -Measurement-derived keys follow the legacy `__` shape (e.g. `downstream_predicted_flow`, `delta_predicted_pressure`) and are emitted only when the container holds a finite value. +Measurement keys follow the legacy `__` 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[Mode] - f2[Asset model] + f1[Reaction Speed] + f2[Asset model / supplier / category] f3[Service type] - f4[Diameter] - f5[Fluid density / temperature] - f6[Inline valveCurve override] + 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[mode.current] + c1["movement.speed (stateConfig)"] c2[asset.model] - c3[asset.serviceType] - c4[asset.valveDiameter] - c5[asset.fluidDensity / fluidTemperatureK] - c6[asset.valveCurve] + 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 @@ -190,70 +198,116 @@ flowchart TB f4 --> c4 f5 --> c5 f6 --> c6 + f7 --> c7 + f8 --> c8 ``` -| Form field | Config key | Default | Range | Where used | +| Form field | Config path | Default | Range / type | Where used | |---|---|---|---|---| -| Mode | `mode.current` | per schema | enum | `setMode`, `flowController` | -| Asset model | `asset.model` | `null` | string | `SupplierCurvePredictor` | -| Service type | `asset.serviceType` | per asset | `gas` / `liquid` | `ValveHydraulicModel` | -| Diameter | `asset.valveDiameter` | per asset | > 0 (m) | curve key selection | -| Fluid density | `asset.fluidDensity` | model default | > 0 (kg/m³) | hydraulic formula | -| Fluid temperature | `asset.fluidTemperatureK` | model default | > 0 (K) | hydraulic formula | -| Choked-flow cap | `asset.gasChokedRatioLimit` | per asset | 0–1 | gas formula clamp | +| 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 | 0–1 | 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 - idle --> opening: set.position > 0 - opening --> operational: position reached - operational --> opening: set.position changed - operational --> closing: set.position < current - closing --> closed: position == 0 - closed --> opening: set.position > 0 - operational --> stopping: cmd.shutdown (ramps to 0) - stopping --> idle: cooldown elapsed - operational --> emergencystop: cmd.estop - emergencystop --> off: cmd.reset + 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 ``` -The `opening` / `closing` states cover the move-in-progress window; `positionChange` ticks fire until the setpoint is reached, then the FSM lands on `operational`. Pre-shutdown ramp to 0 is enforced by `FlowController.executeSequence('shutdown')`. +**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/01-Basic.flow.json` | Inject `set.position` + dashboard, no parent | ✅ | -| Integration | `examples/02-Integration.flow.json` | valve + VGC + upstream source | ✅ | -| Dashboard | `examples/03-Dashboard.flow.json` | Live FlowFuse charts (position, ΔP, flow) | ⭕ | +| 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 | -Screenshots under `wiki/_partial-screenshots/valve/` when produced. Docker compose snippet under `examples/README.md`. +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. | Editor debug tap on Port 2 | -| `delta_predicted_pressure` stuck at zero | Is `kv > 0`? FSM may be in `off` / `closed`. | `state.getCurrentState()` | -| Gas mismatch warning on status badge | `fluidCompatibility.status` is `mismatch` / `conflict`. | `getFluidCompatibility()` | -| `query.curve` returns empty curve | Asset model not found by `loadModel`; fallback to `config.asset.valveCurve`. | `SupplierCurvePredictor.snapshot()` | -| deltaP non-finite | Downstream gauge pressure absolute term ≤ 0, or choked ratio reached. | `hydraulicDiagnostics` | +| 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 element** with a known Kv curve. For a fixed-restriction orifice with no actuator, model the deltaP externally. -- Don't use valve to model a non-return / check valve — no position control or curve fitting is exposed. -- Skip valve when an upstream source provides flow directly and no pressure-drop estimate is needed; just wire the source straight to the parent. +- 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. | P10 test-suite refactor | +| 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 |