diff --git a/package.json b/package.json index 784c1ee..84889e8 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "description": "Control module valve", "main": "valve.js", "scripts": { - "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js" + "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js", + "wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md", + "wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md", + "wiki:all": "npm run wiki:contract && npm run wiki:datamodel" }, "repository": { "type": "git", diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..5212d5a --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,259 @@ +# 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 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. + +## 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 + this -->|evt.deltaPChange| 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`. + +## 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`. | +| Multi-parent registration | ⚠️ | Allowed but not exercised in production tests. | + +## 4. Code map + +```mermaid +flowchart TB + subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] + nc["buildDomainConfig()
static DomainClass, commands"] + end + subgraph domain["specificClass.js — orchestrator (BaseDomain)"] + sc["Valve.configure()
wires concern modules
installs FluidCompatibility registerChild"] + end + subgraph concerns["src/ concern modules"] + state["state/
stateBindings → positionChange"] + fluid["fluid/
FluidCompatibility"] + curve["curve/
SupplierCurvePredictor"] + meas["measurement/
MeasurementRouter + FORMULA_UNITS"] + flow["flow/
FlowController (setpoint, sequences)"] + io["io/
buildOutput + buildStatusBadge"] + hyd["hydraulicModel.js
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. | +| `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. | +| `io/` | Port-0 output shape + status badge | What lands on the wire each tick. | + +## 5. Topic contract + +> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. + + + +| Canonical topic | Aliases | Payload | Effect | +|---|---|---|---| +| `set.mode` | `setMode` | `string` | Replaces the named state value with the supplied payload. | +| `cmd.startup` | _(none)_ | `any` | Triggers an action / sequence — not idempotent. | +| `cmd.shutdown` | _(none)_ | `any` | Triggers an action / sequence — not idempotent. | +| `cmd.estop` | `emergencystop`, `emergencyStop` | `any` | Triggers an action / sequence — not idempotent. | +| `execSequence` | _(none)_ | `object` | _(see handler)_ | +| `set.position` | `execMovement` | `object` | Replaces the named state value with the supplied payload. | +| `data.flow` | `updateFlow` | `object` | Pushes a value into the node's measurement stream. | +| `query.curve` | `showcurve` | `any` | Read-only query; node replies on the same msg. | +| `child.register` | `registerChild` | `string` | Parent/child plumbing — registers or unregisters a child node. | + + + +## 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. + +```mermaid +flowchart LR + subgraph kids["accepted children (softwareType)"] + src["machine / rotatingmachine /
machinegroup / pumpingstation /
valvegroupcontrol"]:::unit + m["measurement"]:::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] + 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 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.`. | + +## 7. Lifecycle — what one event does + +```mermaid +sequenceDiagram + participant parent as valveGroupControl + participant valve as valve + participant state 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->>valve: predictKv(position) + valve->>hyd: calculateDeltaPMbar(q, kv, downP, rho, T) + hyd-->>valve: { deltaPMbar, details } + valve->>valve: write pressure.predicted.delta + valve->>parent: emitter.emit('deltaPChange', deltaP) + valve->>out: msg{topic, payload (delta-compressed)} +``` + +## 8. Data model — `getOutput()` + +What lands on Port 0. Composed in `io/output.buildOutput`, then delta-compressed by `outputUtils.formatMsg`. + + + +| 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` | + + + +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. + +## 9. Configuration — editor form ↔ config keys + +```mermaid +flowchart TB + subgraph editor["Node-RED editor form"] + f1[Mode] + f2[Asset model] + f3[Service type] + f4[Diameter] + f5[Fluid density / temperature] + f6[Inline valveCurve override] + end + subgraph config["Domain config slice"] + c1[mode.current] + c2[asset.model] + c3[asset.serviceType] + c4[asset.valveDiameter] + c5[asset.fluidDensity / fluidTemperatureK] + c6[asset.valveCurve] + end + f1 --> c1 + f2 --> c2 + f3 --> c3 + f4 --> c4 + f5 --> c5 + f6 --> c6 +``` + +| Form field | Config key | Default | Range | 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 | + +## 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 +``` + +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')`. + +## 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) | ⭕ | + +Screenshots under `wiki/_partial-screenshots/valve/` when produced. 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` | + +> 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. + +## 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 | diff --git a/wiki/_partial-datamodel.md.template b/wiki/_partial-datamodel.md.template new file mode 100644 index 0000000..2a51e5c --- /dev/null +++ b/wiki/_partial-datamodel.md.template @@ -0,0 +1,11 @@ +| 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` |