Files
machineGroupControl/wiki/Reference-Contracts.md
znetsixe 472402c62d feat(mgc): rendezvous planner — same-time landing across all modes
Routes every dispatch through a tick-aware planner so all pumps reach
their setpoint at the same wall-clock instant t* = max(eta_i),
regardless of control strategy or per-pump reaction speed.

Architecture (src/movement/):
- machineProfile.js   – pure snapshot of a registered child (state,
                        position, velocityPctPerS, ladder timings,
                        flowAt / positionForFlow). Reads timings from
                        child.state.config.time (the actual storage
                        location — previous fallback paths silently
                        produced 0 s, collapsing every eta to ramp-only).
- moveTrajectory.js   – seconds-to-target per machine; handles
                        idle / starting / warmingup / operational / cooling.
- movementScheduler.js – t* = max eta over ALL non-noop moves. Every
                        command is delayed so its move finishes at t*.
                        Startup execsequence fires at 0; its flowmovement
                        is gated by max(ladderS, t* − rampS) so a fast
                        pump waits before ramping rather than landing
                        early. useRendezvous=false collapses to all
                        fireAtTickN=0 (legacy fire-and-forget).
- movementExecutor.js – wall-clock virtual cursor: each tick fires
                        every command whose fireAtTickN ≤ floor(elapsed/tickS).
                        tick() no longer awaits pending fireCommand
                        promises — the synchronous prologue of
                        handleInput claims the latest-wins gate, which
                        is what race-favouring relies on.

Shared dispatch path (src/specificClass.js):
- _dispatchFlowDistribution(distribution) — extracted from
  _optimalControl. Builds profiles, calls movementScheduler.plan,
  replans the executor, ticks once. Reads
  config.planner.useRendezvous (default true).
- _optimalControl computes its bestCombination and hands off.
- equalFlowControl (priorityControl mode) computes its
  flowDistribution and hands off via ctx.mgc._dispatchFlowDistribution.
  Same-time landing now applies in BOTH modes.

Editor toggle (mgc.html + src/nodeClass.js):
- New "Same-time landing" checkbox under Control Strategy.
- nodeClass.buildDomainConfig bridges uiConfig.useRendezvous →
  config.planner.useRendezvous. Default ON.

Tests:
- New: planner-convergence.integration.test.js (real-time end-to-end
  diagnostic — drives a 3-pump mixed-state dispatch and asserts both
  convergence to the demand setpoint AND same-time landing within
  one tick).
- New: planner-rendezvous.integration.test.js (schedule-shape
  assertions against real pump objects).
- New: movementScheduler.basic.test.js — includes a mixed-speed
  multi-startup case proving the fast pumps wait so all three land
  together (the regression that prompted this work).
- New: movementExecutor.basic.test.js + moveTrajectory.basic.test.js.
- Updated executor contract test: tick() must NOT await pending fires.

Commands + wiki:
- handlers.js: source/mode allow-list gate moved into a shared _gate()
  helper; every command now checks isValidActionForMode +
  isValidSourceForMode before dispatching. Status-level commands
  (set.mode, set.scaling) are allowed in every mode.
- commands.basic.test.js: coverage for the new gate behaviour.
- wiki regen: Home.md visual-first rewrite + Reference-{Architecture,
  Contracts,Examples,Limitations}.md split with _Sidebar.md index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:43:55 +02:00

12 KiB
Raw Permalink Blame History

Reference — Contracts

code-ref

Note

Full topic contract, configuration schema, and child-registration filters for machineGroupControl. Source of truth: src/commands/index.js, src/specificClass.js configure(), and the schema at generalFunctions/src/configs/machineGroupControl.json.

For an intuitive overview, return to the Home.


Topic contract

The MGC accepts three canonical topics. set.demand is the only one with semantic content; the other two are simple state changes.

Canonical topic Aliases Payload Unit handling Effect
set.mode setMode string ("optimalControl" | "priorityControl" | "maintenance") Switch the dispatch strategy. maintenance is monitoring-only — the dispatch switch warns and skips.
set.demand Qd bare number, OR {value: number, unit: string} self-describing (see below) Operator demand setpoint. Resolves to canonical m³/s, then enters the latest-wins gate. Negative value = stop all (any unit).
child.register registerChild string (Node-RED node id) Register a child machine manually. Port 2 wiring does this automatically in normal flows.

set.demand — unit-self-describing semantics

src/commands/handlers.js setDemand. The payload itself decides the meaning:

Payload form Interpretation
42 (bare number) 42 %. Mapped through interpolation.interpolate_lin_single_point(value, 0, 100, dt.flow.min, dt.flow.max) to a canonical m³/s, clamped to the dynamic envelope.
{value: 42, unit: '%'} Same as above — explicit-percent form.
{value: 80, unit: 'm3/h'} (or l/s / m3/s / …) Absolute flow. Converted via convert(value).from(unit).to('m3/s').
42 or {value: …, unit: 'm3/h'} with value < 0 Triggers turnOffAllMachines() regardless of unit.
Anything else (NaN, missing) Logged at error level; dispatch is skipped.

There is no persistent scaling state on the orchestrator. Each set.demand carries its own unit context; callers can switch between absolute and percent at will.

After a successful dispatch the handler replies on the input port with {topic: <node.name>, payload: 'done'} — the legacy "done" handshake some downstream flows still rely on.


Data model — getOutput() shape

Composed each tick by src/io/output.js getOutput() and emitted via outputUtils.formatMsg on Port 0. Delta-compressed: consumers see only the keys that changed.

Per-measurement keys

For every (type, variant) MeasurementContainer pair, the formatter emits up to four keys — one per position plus a differential when both upstream and downstream are present:

<position>_<variant>_<type>

Examples (with variant=predicted, type=flow):

Key Source
downstream_predicted_flow Group aggregate at the discharge side.
atEquipment_predicted_flow Optimizer intent (what the controller's solving for).
upstream_predicted_flow Group suction-side aggregate (when populated).
differential_predicted_flow downstream upstream when both legs read.

Same shape for pressure, power, temperature, efficiency, Ncog. Output units are taken from the unit policy (flow=m3/h, pressure=mbar, power=kW, temperature=°C).

Scalar group keys

Key Type Source Notes
mode string mgc.mode Current dispatch mode.
scaling (legacy) mgc.scaling Always undefined in the current code — the orchestrator no longer carries a scaling field. Kept in the formatter for now; will be removed.
absDistFromPeak number mgc.efficiency.calcDistanceBEP Absolute η distance to the group "peak" (mean of per-pump cogs).
relDistFromPeak number | undefined same Normalised 0..1; undefined when the η spread collapses (homogeneous pump group).
headerDiffPa number mgc.operatingPoint.headerDiffPa Last header differential the equaliser resolved. Pa.
headerDiffMbar number derived Only emitted when output.pressure === 'mbar'.
flowCapacityMax / flowCapacityMin number mgc.dynamicTotals.flow.{max,min} The group's current envelope at the active header pressure.
machineCount number Object.keys(mgc.machines).length All registered children.
machineCountActive number derived Children whose state ≠ off / maintenance and currentMode ≠ maintenance.

Status badge

src/io/output.js getStatusBadge() composes:

<mode> · <scaling-abbrev> · Q=<flow>/<capacity> m³/h · P=<power> kW · <active>/<count>x

Fill colour: green when any pump is available, yellow when machines are registered but all are off/maintenance, grey when no pumps are registered.


Configuration schema — editor form to config keys

Source of truth: generalFunctions/src/configs/machineGroupControl.json plus nodeClass.buildDomainConfig.

General (config.general)

Form field Config key Default Notes
Name general.name Machine Group Configuration Human-readable label.
(auto-assigned) general.id null Node-RED node id; assigned at deploy.
Default unit general.unit m3/h Surfaces as the unit-policy output for flow.
Enable logging general.logging.enabled true Master logger 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 atEquipment One of atEquipment / upstream / downstream. Used in the child-register payload.
(hidden) functionality.softwareType machinegroupcontrol Constant.
(hidden) functionality.role GroupController Constant.
Distance offset functionality.distance null Optional spatial offset; populated from the editor when hasDistance is enabled.
Distance unit functionality.distanceUnit m
Distance description functionality.distanceDescription "" Free-text.

Output (config.output)

Form field Config key Default Range Notes
Process Output output.process process process / json / csv Port-0 formatter.
Database Output output.dbase influxdb influxdb / json / csv Port-1 formatter.

Mode (config.mode)

Form field Config key Default Range Where used
Control mode mode.current optimalControl optimalControl / priorityControl / maintenance dispatch switch in _runDispatch; mode-source/-action gates in commands/handlers.js.
(defaults) mode.allowedActions.optimalControl [statusCheck, execOptimalCombination, balanceLoad, emergencyStop] Enforced at command-handler entry via specificClass.isValidActionForMode.
(defaults) mode.allowedActions.priorityControl [statusCheck, execSequentialControl, balanceLoad, emergencyStop] Same.
(defaults) mode.allowedActions.maintenance [statusCheck] Same — dispatch/emergencyStop are dropped with a warn log.
(defaults) mode.allowedSources.optimalControl ["parent","GUI","physical","API"] Enforced via specificClass.isValidSourceForMode.
(defaults) mode.allowedSources.priorityControl ["parent","GUI","physical","API"] Same.
(defaults) mode.allowedSources.maintenance ["parent","GUI"] Physical/HMI and API writes dropped in maintenance — monitoring only.

Note

mode.current is normalised at write time by specificClass.setMode: legacy lowercase inputs (optimalcontrol, prioritycontrol) are accepted and stored as the canonical camelCase. The _runDispatch switch then lowercases for its comparison — both forms reach the correct branch. Garbage modes (e.g. 'wat') are rejected with a warn log and the previous mode is preserved.

Selecting maintenance no longer reaches _runDispatch at all in normal operation: the mode-action gate at commands/handlers.js drops the incoming set.demand before the dispatcher sees it. Status messages (set.mode, child.register) continue to flow.

Unit policy

Source: src/specificClass.js lines 3337.

Quantity Canonical (internal) Output (rendered) Required-unit
Flow m3/s m3/h
Pressure Pa mbar
Power W kW
Temperature K °C

requireUnitForTypes means MeasurementContainer rejects writes without an explicit unit for these types — protects against accidentally writing raw numbers in the wrong scale.


Child registration

Source: src/specificClass.js configure() lines 92118.

softwareType Filter / subscribed events Side-effect
machine onRegister stores the child in this.machines[id]. Subscribes to pressure.measured.downstream, pressure.measured.differential, and flow.predicted.downstream from the child's emitter. Every event calls handlePressureChange() — equalises the header, recomputes dynamic totals, refreshes group η, fires notifyOutputChanged().
measurement onRegister reads asset.type and positionVsParent, subscribes to <type>.measured.<position> on the child's measurement emitter. Mirrors the value into MGC's own MeasurementContainer; pressure values additionally trigger handlePressureChange().

A child whose asset.type or positionVsParent is missing is logged at warn and skipped (not registered).

There is no filter on machinegroup / pumpingstation children — MGC is a leaf controller; it parents pumps but doesn't accept fellow aggregators.


Header-pressure equalisation

Source: src/groupOps/groupOperatingPoint.js equalize().

MGC ensures every registered child uses the same header differential pressure when computing predicted flow / power. Algorithm:

  1. Read MGC's own group-scope pressure (downstream and upstream) from its MeasurementContainer.
  2. Read each child's measured pressure (downstream / upstream).
  3. Pick:
    • headerDownstream = group reading if positive, else max across children.
    • headerUpstream = group reading if positive, else min across children.
  4. If the differential is non-positive, skip the equalisation (debug log).
  5. Stash the diff on this.headerDiffPa (used by getOutput and by every η computation).
  6. Push the diff onto each child's predictFlow.fDimension / predictPower.fDimension / predictCtrl.fDimension — preferred path is child.setGroupOperatingPoint(downstream, upstream), which lets the child re-build its groupPredict* interpolators. Older children fall back to a direct fDimension write.

The equaliser is called from handlePressureChange (on every child pressure / predicted-flow event) and from the start of _optimalControl.


Page Why
Home Intuitive overview
Reference — Architecture Code map, dispatch lifecycle, planner internals
Reference — Examples Shipped flows
Reference — Limitations Known issues and open questions
EVOLV — Topic Conventions Platform-wide topic rules
EVOLV — Telemetry Port 0 / 1 / 2 InfluxDB layout