diff --git a/CONTRACT.md b/CONTRACT.md new file mode 100644 index 0000000..cc38b7b --- /dev/null +++ b/CONTRACT.md @@ -0,0 +1,57 @@ +# pumpingStation — Contract + +Hand-maintained for Phase 2; the `## Inputs` table is generated from +`src/commands/index.js` (see Phase 9 generator). Keep ≤ 80 lines. + +## Inputs (msg.topic on Port 0) + +| Canonical | Aliases (deprecated) | Payload | Effect | +|---|---|---|---| +| `set.mode` | `changemode` | `string` — one of `manual`, `levelbased`, `flowbased`, `none` | Switches the control strategy. | +| `child.register` | `registerChild` | `string` — the child node's Node-RED id | Resolves the child via `RED.nodes.getNode` and registers it through `childRegistrationUtils` at the supplied `msg.positionVsParent`. | +| `cmd.calibrate.volume` | `calibratePredictedVolume` | numeric (number or numeric string) — m³ | Resets the predicted-volume series and seeds it with the supplied value; recomputes level. | +| `cmd.calibrate.level` | `calibratePredictedLevel` | numeric — metres | Resets the predicted-level series and seeds it with the supplied value; recomputes volume. | +| `set.inflow` | `q_in` | number, numeric string, or `{ value, unit, timestamp }` | Pushes a manual inflow measurement onto the predicted-flow series. `unit` may be on the message (`msg.unit`) or inside the object payload. | +| `set.demand` | `Qd` | numeric — child setpoint demand | Forwards the demand to direct children (machineGroups / machines / stations). Only honoured in `manual` mode; in other modes the call is logged at `debug` and discarded. | + +Aliases log a one-time deprecation warning the first time they fire. + +## Outputs (msg.topic on Port 0/1/2) + +- **Port 0 (process):** `msg.topic = config.general.name`. Payload built by + `outputUtils.formatMsg(..., 'process')` from `getOutput()` — delta-compressed + (only changed fields are emitted). +- **Port 1 (InfluxDB telemetry):** same shape as Port 0, formatted with the + `'influxdb'` formatter. +- **Port 2 (registration):** at startup the node sends one + `{ topic: 'registerChild', payload: , positionVsParent, distance }` + to the upstream parent. + +## Events emitted by `source.measurements.emitter` + +The `MeasurementContainer` fires `..` whenever +the corresponding series receives a new value. Parents subscribe via the +generic `child.measurements.emitter.on(eventName, ...)` handshake. +pumpingStation publishes: + +- `volume.predicted.atequipment` — basin volume integrator output (m³). +- `level.predicted.atequipment` — basin level (m), recomputed from volume. +- `flow.predicted.in` (childed `manual-qin`) — manual inflow injections. +- `volume.measured.atequipment`, `level.measured.`, + `pressure.measured.`, `temperature.measured.atequipment`, + `flow.predicted.` (childed by upstream child id) — when a + matching child measurement arrives. + +The exact set is data-driven by which children register and what they +publish; downstream consumers should subscribe by event name, not assume +a fixed catalogue. + +## Children registered by this node + +pumpingStation acts as a parent for `measurement`, `machine`, `machinegroup`, +and `pumpingstation` software types. Position labels accepted from +children are `upstream`, `downstream`, `atequipment` (and the synonyms +`in` / `out` for predicted-flow children). Child-registration plumbing is +documented in `MODULE_SPLIT.md`; this node does not receive children +through Port 0 input — registration arrives on Port 2 from the child via +the standard `childRegistrationUtils` handshake. diff --git a/examples/standalone-demo.js b/examples/standalone-demo.js new file mode 100644 index 0000000..08a1839 --- /dev/null +++ b/examples/standalone-demo.js @@ -0,0 +1,57 @@ +/** + * Standalone PumpingStation demo — run with `node examples/standalone-demo.js`. + * Builds a station + one pump, calibrates predicted volume, ticks once. + * Useful for sanity-checking the orchestrator without Node-RED. + */ +const PumpingStation = require('../src/specificClass'); +const RotatingMachine = require('../../rotatingMachine/src/specificClass'); + +function createPumpingStationConfig(name) { + return { + general: { + logging: { enabled: true, logLevel: 'debug' }, + name, + id: `${name}-${Date.now()}`, + flowThreshold: 1e-4, + }, + functionality: { softwareType: 'pumpingStation', role: 'stationcontroller' }, + basin: { volume: 43.75, height: 10, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 3.2 }, + hydraulics: { refHeight: 'NAP', basinBottomRef: 0 }, + safety: { enableDryRunProtection: false, enableOverfillProtection: false }, + }; +} + +function createMachineConfig(name, position) { + return { + general: { name, logging: { enabled: false, logLevel: 'debug' } }, + functionality: { softwareType: 'machine', positionVsParent: position }, + asset: { supplier: 'Hydrostal', type: 'pump', category: 'centrifugal', model: 'hidrostal-H05K-S03R' }, + }; +} + +function createMachineStateConfig() { + return { + general: { logging: { enabled: true, logLevel: 'debug' } }, + movement: { speed: 1 }, + time: { starting: 2, warmingup: 3, stopping: 2, coolingdown: 3 }, + }; +} + +(async function demo() { + const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo')); + const pump1 = new RotatingMachine(createMachineConfig('Pump1', 'downstream'), createMachineStateConfig()); + + station.childRegistrationUtils.registerChild(pump1, 'machine'); + + setInterval(() => station.tick(), 1000); + await new Promise((resolve) => setTimeout(resolve, 10)); + + console.log('Initial state:', station.state); + station.setManualInflow(300, Date.now(), 'l/s'); + station.calibratePredictedVolume(3.4); + + console.log('Station state:', station.state); + console.log('Station output:', station.getOutput()); +})().catch((err) => { + console.error('Demo failed:', err); +}); diff --git a/pumpingStation.html b/pumpingStation.html index 216f660..ebe9096 100644 --- a/pumpingStation.html +++ b/pumpingStation.html @@ -8,8 +8,9 @@ | **Control Module** | `#a9daee` | zwart | --> - - + + +