# Reference — Examples ![code-ref](https://img.shields.io/badge/code--ref-48fa543-blue) > [!NOTE] > Usage patterns: how a consumer node imports and extends the library's base classes, how to register topic commands, how to declare child routes, and how to chain `MeasurementContainer` writes. Snippets are pulled from real consumer nodes (`rotatingMachine`, `pumpingStation`, `machineGroupControl`). For an intuitive overview, return to [Home](Home). --- ## 1. Single root import — the contract ```js const { BaseDomain, BaseNodeAdapter, UnitPolicy, ChildRouter, HealthStatus, LatestWinsGate, MeasurementContainer, outputUtils, logger, statusBadge, convert, PIDController, } = require('generalFunctions'); ``` The package root (`require('generalFunctions')`) is the only contractual import path. Internal subpaths (`require('generalFunctions/src/domain/UnitPolicy')`) are NOT contractual and may move at any time. --- ## 2. Extending `BaseDomain` — pattern from `pumpingStation/specificClass.js` ```js const { BaseDomain, UnitPolicy } = require('generalFunctions'); class PumpingStation extends BaseDomain { // static name must match src/configs/.json on the library side. static name = 'pumpingStation'; // Declarative unit triple. canonical = internal storage. output = render units. // curve = supplier curve units (only if the node consumes a characteristic curve). static unitPolicy = UnitPolicy.declare({ canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' }, output: { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' }, requireUnitForTypes: ['pressure', 'flow', 'power', 'temperature'], }); configure() { // Named child getters — readable in code, but the registry remains source of truth. this.declareChildGetter('machines', 'machine'); this.declareChildGetter('machineGroups', 'machinegroup'); // Declarative child routing — no per-node registerChild switch needed. this.router .onRegister('machinegroup', (child) => this._onMachineGroupRegistered(child)) .onMeasurement('measurement', { type: 'level' }, (data, child) => { this._onLevel(data.value, data); }); } getOutput() { return { ...this.measurements.getFlattenedOutput(), ...this.basin.snapshot(), }; } getStatusBadge() { return statusBadge.compose(['filling', 'V=12.4/50.0 m³']); } } module.exports = PumpingStation; ``` Key points: - `static name = '...'` — tells `configManager.buildConfig()` which `src/configs/.json` file to merge defaults from. - `static unitPolicy` — pre-built `UnitPolicy` instance; `BaseDomain` passes `unitPolicy.containerOptions()` to the `MeasurementContainer` so it auto-converts on write. - `configure()` is where you wire `ChildRouter` routes and instantiate concern modules. The constructor is owned by `BaseDomain`. - `getOutput()` and `getStatusBadge()` are the only two methods `BaseNodeAdapter` calls on the domain to produce ports + status — everything else is event-driven. --- ## 3. Extending `BaseNodeAdapter` — pattern from `pumpingStation/nodeClass.js` ```js const { BaseNodeAdapter } = require('generalFunctions'); const Domain = require('./specificClass'); const commands = require('./commands'); class nodeClass extends BaseNodeAdapter { static DomainClass = Domain; // The specificClass to instantiate. static commands = commands; // Array of command descriptors. static tickInterval = 1000; // ms — only for time-driven math. Omit for event-driven nodes. static statusInterval = 1000; // ms — how often to re-render the status badge. // Translate Node-RED editor field values into the domain's config slice. // The base class already merges schema defaults from src/configs/.json; // this hook lets the adapter shape per-node values before the domain sees them. buildDomainConfig(uiConfig, nodeId) { return { basin: { volume: Number(uiConfig.basinVolume), height: Number(uiConfig.basinHeight), surfaceArea: Number(uiConfig.basinSurface), }, hydraulics: { inflowPipeArea: Number(uiConfig.inflowArea), }, }; } } module.exports = nodeClass; ``` `BaseNodeAdapter` wires the full lifecycle: schema merge → domain instantiation → Port 2 registration after a 100 ms delay → status loop start → input dispatch via the registry → close handler that drains everything. The subclass only declares the static config and overrides `buildDomainConfig`. --- ## 4. Command descriptors with unit normalisation ```js // src/commands/index.js module.exports = [ { topic: 'set.demand', aliases: ['Qd'], // Legacy name — first use logs a one-time deprecation. units: { measure: 'volumeFlowRate', default: 'm3/h' }, payloadSchema: { type: 'number' }, description: 'Operator demand setpoint. Unit-normalised before handler runs.', handler: (source, msg) => { source.setDemand(msg.payload); }, }, { topic: 'cmd.startup', payloadSchema: { type: 'none' }, description: 'Trigger startup sequence.', handler: (source, msg) => { source.startup(msg.payload?.source); }, }, { topic: 'set.flow-setpoint', aliases: ['flowMovement'], units: { measure: 'volumeFlowRate', default: 'm3/h' }, payloadSchema: { type: 'object', properties: { setpoint: { type: 'number' } } }, description: 'Set a flow-unit setpoint. Auto-converted to canonical m³/s.', handler: (source, msg) => { source.setFlowSetpoint(msg.payload.setpoint); }, }, ]; ``` When `units` is declared, `CommandRegistry` reads `msg.unit` from the incoming message (falling back to `default`) and converts via the `convert` library to the canonical unit before invoking the handler. The handler always sees a canonical value — it never has to do its own unit conversion. A free side-effect: every command descriptor with a `units` field contributes a row to the auto-generated `query.units` reply, which dashboards can use to introspect a node's unit contract at runtime. --- ## 5. Declarative child routing — `ChildRouter` ```js configure() { this.router // Trigger a callback the first time a machine-group child registers. .onRegister('machinegroup', (child) => { this.logger.info(`MachineGroup ${child.general.id} attached`); this._mgcChild = child; }) // Filter on a measurement child's asset.type. .onMeasurement('measurement', { type: 'pressure', position: 'upstream' }, (data, child) => { this._onUpstreamPressure(data.value, data); }) .onMeasurement('measurement', { type: 'pressure', position: 'downstream' }, (data, child) => { this._onDownstreamPressure(data.value, data); }) .onMeasurement('measurement', { type: 'flow' }, (data, child) => { // No position filter → matches any position. this._onFlow(data.value, data, child); }) // React to a child's own predictions (e.g. a downstream MGC publishing predicted group flow). .onPrediction('machinegroup', { type: 'flow' }, (data, child) => { this._onChildPrediction(data, child); }); } ``` Pre-refactor, the same code lived as a `registerChild(child)` method on every node with a 30-line `switch (child.softwareType)` block. `ChildRouter` makes the wiring declarative; the underlying `childRegistrationUtils` calls are unchanged. --- ## 6. `MeasurementContainer` chaining ```js // Write: chainable, auto-converts from srcUnit to canonical per UnitPolicy. this.measurements .type('pressure') .variant('measured') .position('upstream', child.general.id) // childId narrows the storage slot. .value(3.4, Date.now(), 'mbar'); // value, timestamp, srcUnit. // Read: latest value in canonical or arbitrary unit. const p_Pa = this.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue(); const p_mbar = this.measurements.type('pressure').variant('measured').position('upstream').getCurrentValue('mbar'); // Read: windowed average. const avg = this.measurements.type('flow').variant('measured').position('atequipment').getAverage('m3/h'); // Read: difference over a time window (e.g. for integrators). const dV = this.measurements .type('level').variant('measured').position('atequipment') .difference({ from: Date.now() - 60_000, to: Date.now(), unit: 'm' }); // Introspect: the 4-segment flat output (used by getOutput()). const flat = this.measurements.getFlattenedOutput(); // → { // 'pressure.measured.upstream.dashboard-sim-upstream': 0, // 'pressure.measured.downstream.dashboard-sim-downstream': 1100, // 'flow.predicted.downstream.default': 12.4, // 'power.predicted.atequipment.default': 18.2, // } ``` Key shape: `...`. Position labels are always lowercase in keys (`atequipment`, not `atEquipment`). The `childId` is `default` for the node's own predictions; otherwise the registering child's `general.id`. --- ## 7. `HealthStatus` — prediction quality / drift state ```js const { HealthStatus } = require('generalFunctions'); // Ok state. const ok = HealthStatus.ok('Pressure source healthy', 'real-child'); // Degraded with reason flags. const warm = HealthStatus.degraded(1, ['pressure_init_warming'], 'Pressure not yet initialised', 'dashboard-sim'); // Compose multiple sub-statuses into the worst case. const overall = HealthStatus.compose([ok, warm, flowDrift, powerDrift]); // → frozen { level: max(level_i), flags: union(flags_i), message, source } ``` Levels: `0 = good`, `1 = warming`, `2 = degraded`, `3 = invalid`. The shape is frozen; you cannot mutate a `HealthStatus` instance, only compose new ones. --- ## 8. `LatestWinsGate` — latest-write-wins async dispatch ```js const { LatestWinsGate } = require('generalFunctions'); // Construct. this._dispatchGate = new LatestWinsGate({ dispatch: async (value) => { await this._reallySetDemand(value); }, logger: this.logger, }); // Fire (non-blocking; intermediate calls are superseded). this._dispatchGate.fire(newDemand); // Fire and await result. const result = await this._dispatchGate.fireAndWait(newDemand); if (result === LatestWinsGate.SUPERSEDED) { // A newer fire pre-empted this one; nothing to do. } // Wait until idle (useful in tests and clean shutdown). await this._dispatchGate.drain(); ``` Originally extracted from `machineGroupControl` to coordinate fast successive demand changes against a slow dispatcher. Now shared by `pumpingStation`, `valveGroupControl`, `machineGroupControl`. --- ## 9. PID controller ```js const { createPidController } = require('generalFunctions'); const pid = createPidController({ kp: 1.2, ki: 0.4, kd: 0.05, outputLimits: { min: 0, max: 100 }, rateLimitPerSec: 5, // %/s ramp cap derivativeFilterTau: 0.2, // first-order LPF on the D term antiWindup: 'clamping', setpoint: 50, }); pid.setSetpoint(60); // bumpless on the next compute call const output = pid.compute(processValue); // discrete tick ``` For cascaded loops (outer = level → inner = flow), use `createCascadePidController({ outer: {...}, inner: {...} })`. --- ## 10. Status badge composition ```js const { statusBadge } = require('generalFunctions'); getStatusBadge() { const state = this.state.getCurrentState(); const flowFmt = `${(this._predictedFlow * 3600).toFixed(1)} m³/h`; const powerFmt = `${(this._predictedPower / 1000).toFixed(1)} kW`; if (state === 'emergencystop') { return statusBadge.error('E-stop active'); } if (state === 'idle') { return statusBadge.idle('idle'); } return statusBadge.compose([state, flowFmt, powerFmt]); // → { fill: 'green', shape: 'dot', text: 'operational | 12.4 m³/h | 18.2 kW' } } ``` `StatusUpdater` polls `getStatusBadge()` every `statusInterval` ms and calls `node.status(...)`. Text clipped to 60 chars to fit the Node-RED editor. --- ## 11. Unit conversion (when you really do need it directly) ```js const { convert } = require('generalFunctions'); const m3s = convert(80).from('m3/h').to('m3/s'); // 0.0222... // What units can a measure take? const units = convert.possibilities('volumeFlowRate'); // → ['m3/s', 'm3/h', 'l/s', 'l/min', 'gpm', ...] ``` In domain code, you should usually be relying on the `UnitPolicy` + `MeasurementContainer` pipeline to convert at the boundary — calling `convert` directly is a smell unless you're processing a one-off ad-hoc payload. --- ## 12. Loading a per-node JSON schema ```js const { configManager } = require('generalFunctions'); const cm = new configManager(); // What schemas are registered? const names = cm.getAvailableConfigs(); // → ['baseConfig', 'rotatingMachine', 'pumpingStation', 'measurement', ...] // Merge editor values over schema defaults. const merged = cm.buildConfig('pumpingStation', uiConfig, nodeId, domainSlice); ``` `BaseNodeAdapter` does this for you in the constructor. Direct use is for tests and migration tooling. --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Contracts](Reference-Contracts) | Full public API surface, per-export stability tags | | [Reference — Architecture](Reference-Architecture) | Three-tier rule, `src/` layout, consumer responsibilities | | [Reference — Limitations](Reference-Limitations) | Known issues, deprecations, stability rules | | [Platform CONTRACTS.md](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/CONTRACTS.md) | The authoritative base-class spec | | [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home) | A consumer node that uses every primitive |