Replaces the agent-written placeholder inside Reference-Contracts.md with the authoritative table generated from src/commands/index.js. Both the BEGIN and END markers are normalized to the canonical form used by `@evolv/wiki-gen`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
258 lines
12 KiB
Markdown
258 lines
12 KiB
Markdown
# Reference — Contracts
|
|
|
|

|
|
|
|
> [!NOTE]
|
|
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
|
|
>
|
|
> Full topic contract, configuration schema, and child-registration filters for `valveGroupControl`. Source of truth: `src/commands/index.js`, `src/specificClass.js` `configure()`, and the schema at `generalFunctions/src/configs/valveGroupControl.json`.
|
|
>
|
|
> For an intuitive overview, return to the [Home](Home).
|
|
|
|
---
|
|
|
|
## Topic contract
|
|
|
|
The registry lives in `src/commands/index.js`. Each descriptor maps a canonical `msg.topic` to its handler; aliases emit a one-time deprecation warning the first time they fire.
|
|
|
|
<!-- BEGIN AUTOGEN: topic-contract -->
|
|
|
|
| Canonical topic | Aliases | Payload | Unit | Effect |
|
|
|---|---|---|---|---|
|
|
| `set.mode` | `setMode` | `string` | — | Switch the valve group between auto / manual control modes. |
|
|
| `set.position` | `setpoint` | any | — | Set the group-level valve position (currently a no-op pending Phase 7). |
|
|
| `child.register` | `registerChild` | `string` | — | Register a child valve with this group. |
|
|
| `cmd.execSequence` | `execSequence` | `object` | — | Run a group-wide sequence (startup / shutdown / emergencystop). |
|
|
| `data.totalFlow` | `totalFlowChange` | any | — | Notify the group that the total flow setpoint has changed. |
|
|
| `cmd.emergencyStop` | `emergencyStop`, `emergencystop` | any | — | Trigger an emergency stop across all valves in the group. |
|
|
| `set.reconcileInterval` | `setReconcileInterval` | any | — | Update the reconciliation interval (seconds). |
|
|
|
|
<!-- END AUTOGEN: topic-contract -->
|
|
|
|
### Mode / source allow-lists
|
|
|
|
A topic that survives the registry still passes through `flowController` → `handleInput`, which enforces:
|
|
|
|
```js
|
|
if (!host.isValidSourceForMode(source, host.currentMode)) {
|
|
this.logger.warn(`Source '${source}' is not valid for mode '${this.currentMode}'.`);
|
|
return { status: false, feedback: ... };
|
|
}
|
|
```
|
|
|
|
Defaults from the schema:
|
|
|
|
| Mode | `allowedActions` | `allowedSources` |
|
|
|:---|:---|:---|
|
|
| `auto` | `statusCheck, execSequence, emergencyStop, valvePositionChange, totalFlowChange, valveDeltaPchange` | `parent, GUI, fysical` |
|
|
| `virtualControl` | `statusCheck, execSequence, emergencyStop, valvePositionChange, totalFlowChange, valveDeltaPchange` | `GUI, fysical` |
|
|
| `fysicalControl` | `statusCheck, emergencyStop` | `fysical` |
|
|
| `maintenance` | `statusCheck` | (schema does NOT define `allowedSources.maintenance`; `isValidSourceForMode` returns `false` for every source — effectively monitoring-only) |
|
|
|
|
> [!WARNING]
|
|
> **Source contradiction:** `CONTRACT.md` describes `set.mode` as switching between "auto / manual control modes", but the schema defines four modes (`auto` / `virtualControl` / `fysicalControl` / `maintenance`) and `specificClass.setMode` validates against the schema's enum. The wider four-mode set is the implementation. TODO: tighten the prose in `CONTRACT.md` to enumerate the schema modes.
|
|
|
|
> [!WARNING]
|
|
> **Source contradiction:** the schema declares an `mode.allowedActions` table, but the running implementation only consults `isValidSourceForMode` — **`isValidActionForMode` is not implemented on VGC**. Action allow-lists are effectively dead config. TODO: either implement the action check (mirroring `rotatingMachine`'s pattern) or remove `allowedActions` from the schema.
|
|
|
|
---
|
|
|
|
## Data model — `getOutput()` shape
|
|
|
|
Composed each tick by `src/io/output.getOutput()`. Delta-compressed: consumers see only keys whose `getCurrentValue()` is non-null.
|
|
|
|
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
|
|
|
|
### Scalar keys
|
|
|
|
| Key | Type | Source | Notes |
|
|
|:---|:---|:---|:---|
|
|
| `mode` | string | `vgc.currentMode` | `auto` / `virtualControl` / `fysicalControl` / `maintenance`. |
|
|
| `maxDeltaP` | number | `vgc.maxDeltaP` | Cached max delta-P over registered valves (in output pressure unit, default `mbar`). Same data is also surfaced via the measurement-derived key `deltaMax_predicted_pressure`. |
|
|
|
|
### Measurement-derived keys
|
|
|
|
For every `(type, variant, position)` in MeasurementContainer with a finite value, the flattened output emits:
|
|
|
|
```
|
|
<position>_<variant>_<type>
|
|
```
|
|
|
|
| Example key | Unit | Source | Notes |
|
|
|:---|:---|:---|:---|
|
|
| `atEquipment_measured_flow` | m³/h | upstream source `flow.measured.*` events; `data.totalFlow` with `variant=measured` | Total measured flow at the group inlet. |
|
|
| `atEquipment_predicted_flow` | m³/h | written by `distributeFlow` as `sum(accepted)` | Sum of per-valve accepted flows after Kv-share + residual. |
|
|
| `deltaMax_predicted_pressure` | mbar | written by `calcMaxDeltaP` | Max `pressure.predicted.delta` across registered valves. |
|
|
|
|
> Delta compression: only changed fields are sent per tick. Consumers must cache and merge. See `outputUtils.formatMsg`.
|
|
|
|
<!-- END AUTOGEN -->
|
|
|
|
### Status badge
|
|
|
|
`io/output.getStatusBadge`:
|
|
|
|
```
|
|
<mode> | flow=<int> <flowUnit> | <N> valve(s) connected | (or 'No valves')
|
|
```
|
|
|
|
| State | Fill |
|
|
|:---|:---|
|
|
| `getAvailableValves().length > 0` | green dot |
|
|
| `getAvailableValves().length === 0` | red dot |
|
|
|
|
`flow` is the rounded `flow.measured.atEquipment`, or `flow.predicted.atEquipment` if no measured value is available.
|
|
|
|
---
|
|
|
|
## Configuration schema — editor form to config keys
|
|
|
|
Source of truth: `generalFunctions/src/configs/valveGroupControl.json` plus `nodeClass.buildDomainConfig` (which returns `{}` — no domain overrides).
|
|
|
|
### General (`config.general`)
|
|
|
|
| Form field | Config key | Default | Notes |
|
|
|:---|:---|:---|:---|
|
|
| Name | `general.name` | `"ValveGroupControl"` | Node label, status badge prefix (via `topic`). |
|
|
| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
|
|
| Default unit | `general.unit` | `"unitless"` (schema) / `m3/h` (`configure()` overrides via `unitPolicy.output('flow')`) | Re-derived in `configure()`. |
|
|
| Enable logging | `general.logging.enabled` | `true` | Master switch. |
|
|
| Log level | `general.logging.logLevel` | `info` | `debug` / `info` / `warn` / `error`. |
|
|
|
|
### Functionality (`config.functionality`)
|
|
|
|
| Form field | Config key | Default | Notes |
|
|
|:---|:---|:---|:---|
|
|
| Position vs parent | `functionality.positionVsParent` | `""` (per `CONTRACT.md`) | Used in the Port-2 register payload sent to the upstream parent. (Not in the JSON schema; supplied at runtime from the editor.) |
|
|
| (hidden) | `functionality.softwareType` | `"valvegroupcontrol"` | Constant. |
|
|
| (hidden) | `functionality.role` | `"ValveGroupController"` | Constant. |
|
|
|
|
### Asset (`config.asset`)
|
|
|
|
VGC's asset block is informational — there is no curve to load, no model registry, no allowed-unit validation.
|
|
|
|
| Form field | Config key | Default | Notes |
|
|
|:---|:---|:---|:---|
|
|
| Asset UUID | `asset.uuid` | `null` | Globally-unique identifier. |
|
|
| Geolocation | `asset.geoLocation` | `{x:0, y:0, z:0}` | |
|
|
| Supplier | `asset.supplier` | `"Unknown"` | Informational. |
|
|
| Type | `asset.type` | `"valve"` | Classification only. |
|
|
| Sub-type | `asset.subType` | `"Unknown"` | |
|
|
| Model | `asset.model` | `"Unknown"` | Informational; no registry lookup. |
|
|
| Accuracy | `asset.accuracy` | `null` | |
|
|
|
|
### Mode (`config.mode`)
|
|
|
|
| Form field | Config key | Default | Range | Notes |
|
|
|:---|:---|:---|:---|:---|
|
|
| Mode | `mode.current` | `auto` | `auto` / `virtualControl` / `fysicalControl` / `maintenance` | The active operational mode. |
|
|
| (defaults) | `mode.allowedActions.<mode>` | see [Mode allow-lists](#mode--source-allow-lists) | enforced by `flowController` (NOT implemented — see warning above) |
|
|
| (defaults) | `mode.allowedSources.<mode>` | see [Mode allow-lists](#mode--source-allow-lists) | enforced by `isValidSourceForMode` |
|
|
|
|
### Sequences (`config.sequences`)
|
|
|
|
Per-sequence state-transition lists. Defaults:
|
|
|
|
| Sequence | States |
|
|
|:---|:---|
|
|
| `startup` | `[starting, warmingup, operational]` |
|
|
| `shutdown` | `[stopping, coolingdown, idle]` |
|
|
| `emergencystop` | `[emergencystop, off]` |
|
|
| `boot` | `[idle, starting, warmingup, operational]` |
|
|
|
|
`executeSequence(name)` iterates the list and awaits `state.transitionToState(stateName)` per step. The default state object is created at boot with `currentState = 'operational'` so `executeSequence` works without a pre-warmup phase. (See [Architecture — What VGC does NOT have](Reference-Architecture#what-vgc-does-not-have).)
|
|
|
|
### Calculation mode (`config.calculationMode`)
|
|
|
|
| Value | Description |
|
|
|:---|:---|
|
|
| `low` | Calculations run at fixed intervals (time-based). |
|
|
| `medium` (default) | Calculations run when new setpoints arrive or measured changes occur (event-driven). |
|
|
| `high` | Calculations run on all event-driven info, including every movement. |
|
|
|
|
> [!WARNING]
|
|
> `calculationMode` is in the schema but is not currently consulted by `specificClass` or `nodeClass`. The tick interval is fixed at `tickInterval = 1000 ms` and only retunable through `set.reconcileInterval`. TODO: wire `calculationMode` through or remove it.
|
|
|
|
### Flow reconciliation (runtime only)
|
|
|
|
`flowReconciliation` lives on the domain (not in the schema):
|
|
|
|
| Field | Default | Notes |
|
|
|:---|:---:|:---|
|
|
| `maxPasses` | `2` | Max iterations of the Kv-share residual loop. |
|
|
| `residualTolerance` | `0.001` | Stops loop when `|residual| < tolerance` (canonical units). |
|
|
|
|
These are read by `solveFlowDistribution` each call; not currently exposed via a topic or editor field.
|
|
|
|
### Unit policy
|
|
|
|
Source: `src/specificClass.js`.
|
|
|
|
| Quantity | Canonical (internal) | Output (rendered) | Required-unit |
|
|
|:---|:---|:---|:---:|
|
|
| Flow | `m3/s` | `m3/h` | ✓ |
|
|
| Pressure | `Pa` | `mbar` | ✓ |
|
|
|
|
`requireUnitForTypes: ['pressure', 'flow']` — MeasurementContainer rejects writes that omit `unit` for these types.
|
|
|
|
---
|
|
|
|
## Child registration
|
|
|
|
Source: `src/specificClass.js` `_registerValve` / `_registerSource` and `src/sources/fluidContract.js`.
|
|
|
|
| Software type | Filter | Wired to | Side-effect |
|
|
|:---|:---|:---|:---|
|
|
| `valve` | `child` exposes `updateFlow`, `state.getCurrentState`, `measurements` (`_isValveLike`) | Stored in `vgc.valves[id]`; events bound. | Subscribes to `state.emitter.positionChange` (→ `calcValveFlows`) and `emitter.deltaPChange` (→ `calcMaxDeltaP`). Triggers an initial `calcValveFlows` + `calcMaxDeltaP` + `refreshFluidContract`. |
|
|
| `machine` (incl. canonicalised `rotatingmachine`) | router callback | `registerSource` (`sources/fluidContract`) | Subscribes to 6 flow event names on `child.measurements.emitter`; subscribes to `child.emitter.fluidContractChange`. |
|
|
| `machinegroup` (incl. canonicalised `machinegroupcontrol`) | router callback | `registerSource` | Same as `machine`. |
|
|
| `pumpingstation` | router callback | `registerSource` | Same as `machine`. |
|
|
| `valvegroupcontrol` | router callback | `registerSource` | Cascaded VGC; accepted by router. Not exercised in production — see [Limitations](Reference-Limitations#cascaded-vgc-not-test-covered). |
|
|
|
|
Position labels accepted from children are `upstream`, `downstream`, `atEquipment` (and case variants — normalised internally).
|
|
|
|
### Source flow events
|
|
|
|
`bindSource` attaches a listener for every event name in `SOURCE_FLOW_EVENTS`:
|
|
|
|
```
|
|
flow.predicted.downstream
|
|
flow.predicted.atEquipment
|
|
flow.predicted.atequipment
|
|
flow.measured.downstream
|
|
flow.measured.atEquipment
|
|
flow.measured.atequipment
|
|
```
|
|
|
|
The handler reads `eventData.value` (number) and `eventData.unit` and writes `vgc.updateFlow(variant, value, 'atEquipment', unit)`. `variant` is derived from the event-name middle segment (`measured` vs `predicted`).
|
|
|
|
### Fluid contract reconciliation
|
|
|
|
See [Architecture — Source aggregation](Reference-Architecture#fluid-contract-aggregation) for the full reconciliation logic. The aggregated `fluidContract` is exposed via `vgc.getFluidContract()`:
|
|
|
|
```json
|
|
{
|
|
"status": "resolved" | "conflict" | "inferred" | "unknown",
|
|
"serviceType": "liquid" | "gas" | null,
|
|
"upstreamServiceTypes": ["liquid"],
|
|
"sourceCount": 2,
|
|
"message": "Upstream fluid resolved as liquid.",
|
|
"source": "valvegroupcontrol"
|
|
}
|
|
```
|
|
|
|
Changes are broadcast via `source.emitter.emit('fluidContractChange', ...)`.
|
|
|
|
---
|
|
|
|
## Related pages
|
|
|
|
| Page | Why |
|
|
|:---|:---|
|
|
| [Home](Home) | Intuitive overview |
|
|
| [Reference — Architecture](Reference-Architecture) | Code map, flow-distribution loop, source aggregation |
|
|
| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
|
|
| [Reference — Limitations](Reference-Limitations) | Known issues and open questions |
|
|
| [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
|
|
| [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout |
|