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>
154 lines
8.9 KiB
Markdown
154 lines
8.9 KiB
Markdown
# valveGroupControl
|
|
|
|
  
|
|
|
|
A `valveGroupControl` (VGC) coordinates a group of `valve` children that share a common manifold — selector-valve banks, dosing-valve trains, mixing manifolds. It accepts a group-level total flow target, splits the flow proportional to each valve's Kv rating, runs a residual-reconciliation pass against what every valve actually accepted, and aggregates max delta-P across the group. It also reconciles upstream-source fluid-contract advertisements (`liquid` / `gas`) into one group-level service-type view that downstream consumers can read.
|
|
|
|
> [!NOTE]
|
|
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
|
|
|
|
---
|
|
|
|
## At a glance
|
|
|
|
| Thing | Value |
|
|
|:---|:---|
|
|
| What it represents | A parallel valve manifold — 2 + valves sharing a header, distributing one total flow target |
|
|
| S88 level | Unit |
|
|
| Use it when | You have 2 + parallel valves that should share an upstream flow target proportional to their Kv |
|
|
| Don't use it for | A single valve (wire `valve` directly), series valves (the Kv-share solver assumes parallel branches), or a manifold whose upstream already publishes per-branch setpoints |
|
|
| Children it accepts | `valve` (group members) + upstream sources (`machine` / `rotatingmachine` / `machinegroup` / `machinegroupcontrol` / `pumpingstation` / `valvegroupcontrol`) |
|
|
| Parents it talks to | Any upstream node — typically `pumpingStation`, `reactor`, or another VGC; VGC registers via Port 2 |
|
|
|
|
---
|
|
|
|
## How it fits
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
src[machine / MGC /<br/>pumpingStation<br/>upstream source]:::unit -.flow.predicted.*.-> vgc[valveGroupControl<br/>Unit]:::unit
|
|
vgc -->|updateFlow predicted<br/>downstream| v1[valve A]:::equip
|
|
vgc -->|updateFlow predicted<br/>downstream| v2[valve B]:::equip
|
|
v1 -->|positionChange<br/>deltaPChange| vgc
|
|
v2 -->|positionChange<br/>deltaPChange| vgc
|
|
vgc -->|child.register| parent[upstream parent]:::pc
|
|
classDef pc fill:#0c99d9,color:#fff
|
|
classDef unit fill:#50a8d9,color:#000
|
|
classDef equip fill:#86bbdd,color:#000
|
|
```
|
|
|
|
S88 colours are anchored in `.claude/rules/node-red-flow-layout.md`.
|
|
|
|
---
|
|
|
|
## Try it — 3-minute demo
|
|
|
|
Import the basic example flow, deploy, and drive a 2-valve group with an injected total-flow setpoint.
|
|
|
|
```bash
|
|
curl -X POST -H 'Content-Type: application/json' \
|
|
--data @nodes/valveGroupControl/examples/basic.flow.json \
|
|
http://localhost:1880/flow
|
|
```
|
|
|
|
What to click after deploy (the inject buttons map to canonical topics in [Reference — Contracts](Reference-Contracts#topic-contract)):
|
|
|
|
1. `child.register` for each `valve` — or rely on Port-2 wiring to auto-register.
|
|
2. `set.mode = auto` — lets the parent source drive the group.
|
|
3. `data.totalFlow = 80` (with `unit: 'm3/h'`) — VGC splits 80 m³/h across the available valves by Kv share; runs up to `maxPasses: 2` residual passes; writes back `atEquipment_predicted_flow` = sum of accepted per-valve flows.
|
|
4. `cmd.execSequence` with `{action: "startup"}` — runs the group-wide startup sequence through `executeSequence`, transitioning the group state machine through each step.
|
|
5. `cmd.emergencyStop` — runs the `emergencystop` sequence on all valves (`[emergencystop, off]`).
|
|
6. `set.reconcileInterval = 2` — re-tunes the periodic tick to 2 s (`reconcileIntervalChange` event triggers the adapter to restart its tick loop; minimum 100 ms).
|
|
|
|
> [!IMPORTANT]
|
|
> **GIF needed.** Demo recording of steps 1–6 with the live status panel. Save as `wiki/_partial-gifs/valveGroupControl/01-basic-demo.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`.
|
|
|
|
---
|
|
|
|
## The seven things you'll send
|
|
|
|
| Topic | Aliases | Payload | What it does |
|
|
|:---|:---|:---|:---|
|
|
| `set.mode` | `setMode` | `"auto"` \| `"virtualControl"` \| `"fysicalControl"` \| `"maintenance"` | Switch operational mode. Each mode has its own allow-list of sources (`mode.allowedSources`). |
|
|
| `set.position` | `setpoint` | any | **No-op pending Phase 7.** Reserved for future per-valve positional override. Debug-logged only. |
|
|
| `child.register` | `registerChild` | `string` (child node id) | Manually register a child via `RED.nodes.getNode`; Port 2 wiring does this automatically in most flows. |
|
|
| `cmd.execSequence` | `execSequence` | `{ source, action, parameter }` | Forward to `source.handleInput(source, action, parameter)` — runs a group-wide sequence (`startup` / `shutdown` / `emergencystop` / `boot`). |
|
|
| `data.totalFlow` | `totalFlowChange` | number, `{ value, position?, variant?, unit? }`, or `{ source, action, ... }` | Update the total measured/predicted flow at the configured position; triggers `calcValveFlows` to re-distribute across valves. |
|
|
| `cmd.emergencyStop` | `emergencyStop`, `emergencystop` | optional `{ source }` | Run the `emergencystop` sequence on all valves. |
|
|
| `set.reconcileInterval` | `setReconcileInterval` | number — seconds (> 0) | Re-tune the periodic flow-reconciliation interval. Min clamp 100 ms. |
|
|
|
|
Aliases log a one-time deprecation warning the first time they fire.
|
|
|
|
---
|
|
|
|
## What you'll see come out
|
|
|
|
Sample Port 0 message (delta-compressed, after a `data.totalFlow = 80` split across two valves):
|
|
|
|
```json
|
|
{
|
|
"topic": "valveGroupControl#VGC1",
|
|
"payload": {
|
|
"mode": "auto",
|
|
"maxDeltaP": 1450,
|
|
"atEquipment_measured_flow": 80,
|
|
"atEquipment_predicted_flow": 80,
|
|
"deltaMax_predicted_pressure": 1450
|
|
}
|
|
}
|
|
```
|
|
|
|
Key shape: **`<position>_<variant>_<type>`** — same as MGC's key shape (inverse of `rotatingMachine`'s per-measurement form). The output reflects the group aggregate, not per-valve snapshots; per-valve detail comes off each valve's own Port 0.
|
|
|
|
| Field | Meaning |
|
|
|:---|:---|
|
|
| `mode` | Current operational mode (`auto` / `virtualControl` / `fysicalControl` / `maintenance`). |
|
|
| `maxDeltaP` | Max delta-P across registered valves — refreshed whenever a child emits `deltaPChange`. Also surfaced as `deltaMax_predicted_pressure` via the measurement container. |
|
|
| `atEquipment_measured_flow` | Total measured flow at the group inlet (from an upstream source's `flow.measured.*` event). |
|
|
| `atEquipment_predicted_flow` | Sum of per-valve accepted flows after the Kv-share + residual pass. |
|
|
| `deltaMax_predicted_pressure` | Max delta-P across the group, written via the measurement container at position `delta` / variant `predicted` / type `pressure`. |
|
|
|
|
---
|
|
|
|
## Flow-distribution loop — what one event does
|
|
|
|
When a `data.totalFlow` arrives (or an upstream source publishes `flow.predicted.*` / `flow.measured.*`), VGC re-distributes by Kv share:
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
src[data.totalFlow /<br/>upstream source event] --> upd[updateFlow<br/>predicted/measured atEquipment]
|
|
upd --> avail[getAvailableValves<br/>state ≠ off/maintenance, kv > 0]
|
|
avail --> solve[solveFlowDistribution<br/>share by Kv / totalKv]
|
|
solve --> push[valve.updateFlow predicted<br/>downstream]
|
|
push --> readback[read accepted from<br/>flow.predicted.downstream]
|
|
readback --> residual[residual = target − sum(accepted)]
|
|
residual -->|residual > tol & passes < max| solve
|
|
residual --> writeback[write flow.predicted.atEquipment<br/>= sum(accepted)]
|
|
writeback --> dp[calcMaxDeltaP]
|
|
dp --> emit[notifyOutputChanged]
|
|
```
|
|
|
|
Reconciliation defaults (`flowReconciliation`):
|
|
|
|
| Field | Default | Notes |
|
|
|:---|:---:|:---|
|
|
| `maxPasses` | `2` | Max iterations of the residual-correction loop. |
|
|
| `residualTolerance` | `0.001` | Stops the loop when `|residual| <` tolerance (canonical units — m³/s for flow). |
|
|
|
|
A valve is **available** if: `state.getCurrentState() !== 'off'` and `!== 'maintenance'`, `currentMode !== 'maintenance'`, and `kv > 0`. Unavailable valves are skipped and receive `updateFlow('predicted', 0, 'downstream')`.
|
|
|
|
VGC has **no FSM of its own** — state semantics belong to the child valves. `specificClass` instantiates a state object internally and stamps it `operational` at boot for sequence dispatch; the group's only coordination loop is the Kv-share solver above.
|
|
|
|
---
|
|
|
|
## Need more?
|
|
|
|
| Page | What you'll find |
|
|
|:---|:---|
|
|
| [Reference — Contracts](Reference-Contracts) | Topic registry, config schema, child-registration filters |
|
|
| [Reference — Architecture](Reference-Architecture) | Code map, flow-distribution loop, source aggregation, output ports |
|
|
| [Reference — Examples](Reference-Examples) | Shipped flows, debug recipes |
|
|
| [Reference — Limitations](Reference-Limitations) | When not to use, known issues, open questions |
|
|
|
|
[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
|