From 045a941ab440684cce913d4cb3856ca29e69b51d Mon Sep 17 00:00:00 2001 From: znetsixe Date: Mon, 11 May 2026 15:17:35 +0200 Subject: [PATCH] P9.3: wiki/Home.md following 14-section visual-first template + wiki:* scripts Auto-generated topic-contract + data-model sections via shared wikiGen script. Hand-written Mermaid diagrams for position-in-platform, code map, child registration, lifecycle, configuration, state chart (where applicable). Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 5 +- wiki/Home.md | 277 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 wiki/Home.md diff --git a/package.json b/package.json index f044e2b..112730b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "description": "Control module machineGroupControl", "main": "mgc.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..890355c --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,277 @@ +# machineGroupControl + +> **Reflects code as of `afc304b` · 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 + +**machineGroupControl (MGC)** is an S88 Unit orchestrator that coordinates multiple `rotatingMachine` children sharing a common header. It receives a demand setpoint, evaluates valid pump combinations against the group's totals and curves, picks the best operating point (BEP-Gravitation or NCog), and dispatches per-machine flow setpoints + start/stop commands. + +## 2. Position in the platform + +```mermaid +flowchart LR + parent[pumpingStation
Process Cell]:::pc -->|set.demand| mgc[machineGroupControl
Unit]:::unit + header[measurement
header pressure]:::ctrl -.data.-> mgc + mgc -->|flowmovement / execsequence| m_a[rotatingMachine A]:::equip + mgc -->|flowmovement / execsequence| m_b[rotatingMachine B]:::equip + mgc -->|flowmovement / execsequence| m_c[rotatingMachine C]:::equip + mgc -->|child.register| parent + m_a -->|child.register| mgc + m_b -->|child.register| mgc + m_c -->|child.register| mgc + classDef pc fill:#0c99d9,color:#fff + classDef unit fill:#50a8d9,color:#000 + classDef equip fill:#86bbdd,color:#000 + classDef ctrl fill:#a9daee,color:#000 +``` + +S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`. + +## 3. Capability matrix + +| Capability | Status | Notes | +|---|---|---| +| Aggregate group flow / power totals | ✅ | `TotalsCalculator` — absolute and dynamic. | +| Valid-combination enumeration | ✅ | `combinatorics/pumpCombinations`. | +| Best-combination optimiser (BEP-Gravitation) | ✅ | Directional or symmetric variant. | +| Best-combination optimiser (NCog) | ✅ | Normalised cost-of-goods score. | +| Priority / equal-flow control | ✅ | `mode='prioritycontrol'`. | +| Priority percentage control | ✅ | Requires `scaling='normalized'`. | +| Optimal control | ✅ | `mode='optimalcontrol'`. | +| Group efficiency + BEP distance | ✅ | `GroupEfficiency`. | +| Header-pressure equalisation | ✅ | `operatingPoint.equalize()`. | +| Demand serialisation (latest-wins) | ✅ | Inline gate; deferred call drains on completion. | +| Forced shutdown on `Qd ≤ 0` | ✅ | `turnOffAllMachines()`. | + +## 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["MachineGroup.configure()
ChildRouter rules
handleInput() dispatch gate"] + end + subgraph concerns["src/ concern modules"] + groupOps["groupOps/
GroupOperatingPoint + curves"] + totals["totals/
TotalsCalculator"] + combi["combinatorics/
validPumpCombinations"] + opt["optimizer/
BEP-Grav / NCog selectors"] + efficiency["efficiency/
GroupEfficiency + BEP dist"] + dispatch["control/
strategies (equalFlow / prioPct)"] + io["io/
output + status"] + commands["commands/
topic registry + handlers"] + end + nc --> sc + sc --> groupOps + sc --> totals + sc --> combi + sc --> opt + sc --> efficiency + sc --> dispatch + sc --> io + nc --> commands +``` + +| Module | Owns | Read first if you're changing… | +|---|---|---| +| `groupOps/` | Group operating point + child read helpers | Header pressure handling, child measurement plumbing. | +| `totals/` | Absolute + dynamic flow/power totals | Demand clamping, totals math. | +| `combinatorics/` | Enumeration of valid pump subsets | Which combinations are considered eligible. | +| `optimizer/` | Best-combination selectors | Optimiser selection method, scoring math. | +| `efficiency/` | Group efficiency, BEP distance | BEP gravitation tuning, peak math. | +| `control/strategies.js` | Per-mode dispatch (priority, prioPct) | Mode behaviour, priorityList usage. | +| `dispatch/` | Demand fan-out helpers (legacy alongside inline gate) | Serialisation, mid-flight overrides. | +| `commands/` | Input-topic registry and handlers | New input topics, payload validation. | +| `io/` | `getOutput`, `getStatusBadge` | Output shape, dashboard badge. | + +## 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. | +| `set.scaling` | `setScaling` | `string` | Replaces the named state value with the supplied payload. | +| `child.register` | `registerChild` | `string` | Parent/child plumbing — registers or unregisters a child node. | +| `set.demand` | `Qd` | `any` | Replaces the named state value with the supplied payload. | + + + +## 6. Child registration + +`ChildRouter` declarations in `specificClass.js → configure()`. + +```mermaid +flowchart LR + subgraph kids["accepted children (softwareType)"] + mach["machine
(rotatingMachine)"]:::equip + m["measurement
(header pressure)"]:::ctrl + end + mach -->|"pressure.measured.downstream
pressure.measured.differential
flow.predicted.downstream"| eq[operatingPoint.equalize
+ totals refresh] + m -->|"<type>.measured.<position>"| mirror[mirror into own
MeasurementContainer] + mirror -->|"if type === 'pressure'"| eq + eq --> emit[notifyOutputChanged] + classDef equip fill:#86bbdd,color:#000 + classDef ctrl fill:#a9daee,color:#000 +``` + +| softwareType | filter / subscribed events | Side-effect | +|---|---|---| +| `machine` | onRegister stores in `this.machines[id]`; subscribes to `pressure.measured.downstream`, `pressure.measured.differential`, `flow.predicted.downstream` | `handlePressureChange()` — equalise + recompute totals + recompute group efficiency. | +| `measurement` | onRegister attaches listener for `.measured.` | Mirror value into MGC's own MeasurementContainer; pressure also triggers `handlePressureChange()`. | + +## 7. Lifecycle — what one event does + +```mermaid +sequenceDiagram + participant parent as pumpingStation + participant mgc as MGC + participant op as GroupOperatingPoint + participant tot as TotalsCalculator + participant opt as optimizer + participant kids as rotatingMachine[] + + parent->>mgc: set.demand (Qd) + Note over mgc: dispatch gate — latest-wins + mgc->>mgc: abortActiveMovements('new demand') + mgc->>tot: calcDynamicTotals() + mgc->>mgc: clamp Qd to [minFlow, maxFlow] + alt mode=optimalcontrol + mgc->>mgc: validPumpCombinations(Qd) + mgc->>opt: pick best (BEP-Grav | NCog) + opt-->>mgc: bestCombination + bestFlow/Power + mgc->>kids: flowmovement (per-pump flow) + mgc->>kids: execsequence (startup / shutdown) + else mode=prioritycontrol + mgc->>mgc: equalFlowControl(Qd, powerCap, priorityList) + end + mgc->>op: writeOwn flow/power predicted (AT_EQUIPMENT + DOWNSTREAM) + mgc->>mgc: notifyOutputChanged() +``` + +## 8. Data model — `getOutput()` + +What lands on Port 0. Composed in `io/output.js → getOutput(this)` and delta-compressed by `outputUtils.formatMsg`. + + + +| Key | Type | Unit | Sample | +|---|---|---|---| +| `absDistFromPeak` | number | — | `0` | +| `mode` | string | — | `"optimalcontrol"` | +| `relDistFromPeak` | number | — | `0` | +| `scaling` | string | — | `"normalized"` | + + + +**Concrete sample** (excerpt — see live test output for the canonical shape): + +~~~json +{ + "mode": "optimalcontrol", + "scaling": "normalized", + "flow.predicted.atequipment.": 0.0125, + "flow.predicted.downstream.": 0.0125, + "power.predicted.atequipment.": 1800, + "efficiency.predicted.atequipment.": 0.65, + "absDistFromPeak": 0.02, + "relDistFromPeak": 0.10 +} +~~~ + +The `` segment is the Node-RED node id assigned at deploy time. + +## 9. Configuration — editor form ↔ config keys + +```mermaid +flowchart TB + subgraph editor["Node-RED editor form"] + f1[Control mode dropdown] + f2[Scaling dropdown] + f3[Optimisation method] + f4[Output unit (flow)] + f5[Position vs parent] + f6[Allowed sources / actions per mode] + end + subgraph cfg["Domain config slice"] + c1[mode.current] + c2[scaling.current] + c3[optimization.method] + c4[general.unit] + c5[functionality.positionVsParent] + c6[mode.allowedSources
mode.allowedActions] + end + f1 --> c1 + f2 --> c2 + f3 --> c3 + f4 --> c4 + f5 --> c5 + f6 --> c6 +``` + +| Form field | Config key | Default | Range | Where used | +|---|---|---|---|---| +| Control mode | `mode.current` | `optimalControl` | enum (`prioritycontrol`, `prioritypercentagecontrol`, `optimalcontrol`) | dispatch switch in `_runDispatch` | +| Scaling | `scaling.current` | `normalized` | enum (`absolute`, `normalized`) | demand mapping in `_runDispatch` | +| Optimisation method | `optimization.method` | `BEP-Gravitation-Directional` | enum (`NCog`, `BEP-Gravitation`, `BEP-Gravitation-Directional`) | `_optimalControl` selector | +| Output unit (flow) | `general.unit` | `m3/h` | unit string | unit policy `output.flow` | +| Position vs parent | `functionality.positionVsParent` | `atEquipment` | enum | event suffix for parent subscription | + +## 10. State chart + +MGC is **event-driven and stateless** with respect to operating modes — there is no FSM. The closest thing to "state" is the dispatch gate. Diagram for that single state vector: + +```mermaid +stateDiagram-v2 + [*] --> idle_disp: configure() + idle_disp --> dispatching: handleInput(Qd) + dispatching --> idle_disp: dispatch complete + dispatching --> dispatching: handleInput(Qd) — deferred and re-fired on completion + dispatching --> turning_off: Qd <= 0 + turning_off --> idle_disp: all machines acknowledged shutdown +``` + +While `dispatching`, additional `handleInput` calls overwrite `_delayedCall` (latest-wins); the gate drains the latest one on completion. `turnOffAllMachines()` clears `_delayedCall` to make turn-off the final intent. + +## 11. Examples + +| Tier | File | What it shows | Status | +|---|---|---|---| +| Basic | `examples/basic.flow.json` | Single MGC + 2 pumps, manual setDemand | ⚠️ legacy shape, pre-refactor | +| Integration | `examples/integration.flow.json` | MGC wired under pumpingStation | ⚠️ legacy shape, pre-refactor | +| Edge | `examples/edge.flow.json` | Mid-flight demand override + abort | ⚠️ legacy shape, pre-refactor | + +Tier 1/2/3 visual-first example flows are still TODO (see `MEMORY.md` "TODO: Example Flows"). Screenshots will land under `wiki/_partial-screenshots/machineGroupControl/` when the new flows ship. + +## 12. Debug recipes + +| Symptom | First thing to check | Where to look | +|---|---|---| +| No combination selected | Demand outside `[dynamicTotals.flow.min, max]` — clamped on entry; `_optimalControl` returns early if combinations empty. | `validPumpCombinations` + warn log. | +| Group flow stuck at zero | Machines never reach an `ACTIVE_STATE` — check per-pump startup logs. | `isMachineActive`. | +| Priority-percentage mode warns and exits | Mode requires `scaling='normalized'`. Set both. | `_runDispatch` switch. | +| Stale flow setpoints on chained calls | Dispatch gate may have collapsed multiple calls — confirm `_delayedCall` was honoured. | `handleInput` finally block. | +| Header pressure not equalising | Pressure children must register with `asset.type='pressure'` and a matching position. | `operatingPoint.equalize`. | +| Optimiser picks unexpected combo | Verify `optimization.method` and per-method scoring (NCog vs BEP-Grav). | `optimizer/`. | + +> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. + +## 13. When you would NOT use this node + +- Don't use MGC for a **single pump** — wire `rotatingMachine` directly. MGC's combinatorics + totals add no value below N=2. +- Don't use MGC for **valves** — use `valveGroupControl`. MGC's optimiser assumes a flow-vs-pressure characteristic curve. +- Don't use MGC when the pumps live behind **independent headers** — combinations assume a shared discharge / suction pressure. + +## 14. Known limitations / current issues + +| # | Issue | Tracked in | +|---|---|---| +| 1 | `optimalControl` requires every machine to expose a curve — null-curve members silently exclude themselves from combinations. | `combinatorics/pumpCombinations`. | +| 2 | Mid-flight setpoint overrides on `accelerating` / `decelerating` rely on `abortActiveMovements` per dispatch — a sequence with no awaitable `abortMovement` will warn but proceed. | `abortActiveMovements`. | +| 3 | Power-cap parameter exposed but not surfaced as a topic input — only programmatic via `handleInput(source, demand, powerCap)`. | `commands/index.js` — no canonical topic. | +| 4 | Tier 1/2/3 visual-first example flows not yet written. | P9 follow-up. |