# Reference &mdash; 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 &mdash; 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` &mdash; pattern from `pumpingStation/specificClass.js`

```js
const { BaseDomain, UnitPolicy } = require('generalFunctions');

class PumpingStation extends BaseDomain {
  // static name must match src/configs/<nodeName>.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 = '...'` &mdash; tells `configManager.buildConfig()` which `src/configs/<n>.json` file to merge defaults from.
- `static unitPolicy` &mdash; 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 &mdash; everything else is event-driven.

---

## 3. Extending `BaseNodeAdapter` &mdash; 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/<nodeName>.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 &rarr; domain instantiation &rarr; Port 2 registration after a 100 ms delay &rarr; status loop start &rarr; input dispatch via the registry &rarr; 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 &mdash; 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 &mdash; `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: `<type>.<variant>.<position>.<childId>`. 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` &mdash; 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` &mdash; 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 &rarr; 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 &mdash; 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 &mdash; Contracts](Reference-Contracts) | Full public API surface, per-export stability tags |
| [Reference &mdash; Architecture](Reference-Architecture) | Three-tier rule, `src/` layout, consumer responsibilities |
| [Reference &mdash; 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 |
