Compare commits
1 Commits
f21e2aa8bb
...
c7e561e593
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7e561e593 |
452
wiki/Home.md
Normal file
452
wiki/Home.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# generalFunctions
|
||||
|
||||
> **Reflects code as of `f21e2aa` · regenerated `2026-05-11` (hand-written)**
|
||||
> No `npm run wiki:all` script exists for this library. The API surface block (section 5) is hand-maintained between the AUTOGEN markers. If the banner is stale, treat this page as informative, not authoritative.
|
||||
|
||||
---
|
||||
|
||||
## 1. What this library is
|
||||
|
||||
**generalFunctions** is the shared infrastructure every EVOLV node depends on. It provides the base classes all nodes extend (`BaseDomain`, `BaseNodeAdapter`), the command dispatch engine, the measurement store, unit-policy system, child-registration machinery, InfluxDB output formatting, and a set of domain utilities (PID, curve interpolation, prediction, statistics, coolprop). Nodes hold zero duplicated scaffolding — they only write the logic that differs.
|
||||
|
||||
---
|
||||
|
||||
## 2. Position in the platform
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
gf["generalFunctions\n(shared library)"]:::lib
|
||||
|
||||
rm["rotatingMachine\nEquipment"]:::equip
|
||||
mgc["machineGroupControl\nUnit"]:::unit
|
||||
ps["pumpingStation\nProcess Cell"]:::proc
|
||||
meas["measurement\nControl Module"]:::ctrl
|
||||
valve["valve\nEquipment"]:::equip
|
||||
vgc["valveGroupControl\nUnit"]:::unit
|
||||
reactor["reactor\nUnit"]:::unit
|
||||
settler["settler\nUnit"]:::unit
|
||||
monster["monster\nUnit"]:::unit
|
||||
diffuser["diffuser\nEquipment"]:::equip
|
||||
dashAPI["dashboardAPI\nutility"]:::util
|
||||
|
||||
gf --> rm
|
||||
gf --> mgc
|
||||
gf --> ps
|
||||
gf --> meas
|
||||
gf --> valve
|
||||
gf --> vgc
|
||||
gf --> reactor
|
||||
gf --> settler
|
||||
gf --> monster
|
||||
gf --> diffuser
|
||||
gf --> dashAPI
|
||||
|
||||
classDef lib fill:#222,color:#fff,stroke:#444
|
||||
classDef proc fill:#0c99d9,color:#fff
|
||||
classDef unit fill:#50a8d9,color:#000
|
||||
classDef equip fill:#86bbdd,color:#000
|
||||
classDef ctrl fill:#a9daee,color:#000
|
||||
classDef util fill:#dddddd,color:#000
|
||||
```
|
||||
|
||||
Every EVOLV node declares `generalFunctions` as a `dependencies` entry and imports from the package root only (`require('generalFunctions')`). Cross-node coupling happens exclusively through this library's API surface and Node-RED messages — never through direct imports between node packages.
|
||||
|
||||
---
|
||||
|
||||
## 3. Capability matrix
|
||||
|
||||
| Capability | Status | Notes |
|
||||
|---|---|---|
|
||||
| Base domain scaffolding (`BaseDomain`) | ✅ | Constructor, emitter, logger, measurements, child registry wired automatically |
|
||||
| Base Node-RED adapter (`BaseNodeAdapter`) | ✅ | Tick/event loop, status badge, input dispatch, Port 0/1/2 output |
|
||||
| Declarative command dispatch (`CommandRegistry`) | ✅ | Alias deprecation warnings, unit normalisation, `query.units` auto-topic |
|
||||
| Declarative child-registration routing (`ChildRouter`) | ✅ | Replaces per-node `registerChild` switch blocks |
|
||||
| Unit policy + conversion (`UnitPolicy`, `convert`) | ✅ | Canonical ↔ output ↔ curve unit sets; dual method/property access |
|
||||
| Measurement store (`MeasurementContainer`) | ✅ | Chainable, windowed, auto-convert, 4-segment key output |
|
||||
| InfluxDB + process output formatting (`outputUtils`) | ✅ | Delta-compressed; consumers must cache and merge |
|
||||
| Status badge helpers (`statusBadge`, `StatusUpdater`) | ✅ | Converged look-and-feel across all nodes |
|
||||
| Latest-wins async gate (`LatestWinsGate`) | ✅ | Extracted from MGC; shared by PS, VGC, MGC |
|
||||
| Prediction quality / drift tracking (`HealthStatus`) | ✅ | Frozen plain-object shape; composable |
|
||||
| Config schema registry (`configManager`) | ✅ | One JSON schema per node in `src/configs/` |
|
||||
| PID control (`PIDController`, `CascadePIDController`) | ✅ | Full-featured discrete PID with bumpless transfer |
|
||||
| Curve interpolation (`interpolation`, `predict`) | ✅ | Multidimensional characteristic-curve predictor |
|
||||
| Statistical helpers (`stats`, `nrmse`, `outliers`) | ✅ | Mean, stddev, median, NRMSE, dynamic-cluster outlier detection |
|
||||
| Thermodynamic properties (`coolprop`) | ✅ | CoolProp bindings for fluid/gas property lookup |
|
||||
| FSM for valve/machine states (`state`) | ✅ | StateManager + MovementManager |
|
||||
| Gravity calculations (`gravity`) | ✅ | WGS-84 model |
|
||||
| Physical constants (`Fysics`) | ✅ | Air density, viscosity, etc. |
|
||||
| Browser-side editor dropdowns (`MenuManager`, `menuUtils`) | ✅ | Node-RED editor form population |
|
||||
|
||||
---
|
||||
|
||||
## 4. Module map
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph domain["src/domain/ — base classes"]
|
||||
BD["BaseDomain.js"]
|
||||
CR["ChildRouter.js"]
|
||||
UP["UnitPolicy.js"]
|
||||
LWG["LatestWinsGate.js"]
|
||||
HS["HealthStatus.js"]
|
||||
end
|
||||
|
||||
subgraph nodered["src/nodered/ — Node-RED adapter layer"]
|
||||
BNA["BaseNodeAdapter.js"]
|
||||
CMR["commandRegistry.js"]
|
||||
SB["statusBadge.js"]
|
||||
SU["statusUpdater.js"]
|
||||
end
|
||||
|
||||
subgraph measurements["src/measurements/ — measurement store"]
|
||||
MC["MeasurementContainer.js"]
|
||||
MB["MeasurementBuilder.js"]
|
||||
Meas["Measurement.js"]
|
||||
end
|
||||
|
||||
subgraph helper["src/helper/ — shared utilities"]
|
||||
LOG["logger.js"]
|
||||
OU["outputUtils.js"]
|
||||
CRU["childRegistrationUtils.js"]
|
||||
CFG["configUtils.js"]
|
||||
VAL["validationUtils.js"]
|
||||
MU["menuUtils.js"]
|
||||
GR["gravity.js"]
|
||||
end
|
||||
|
||||
subgraph predict_grp["src/predict/ — curve prediction"]
|
||||
PRED["predict_class.js"]
|
||||
INTERP["interpolation.js"]
|
||||
end
|
||||
|
||||
subgraph configs["src/configs/ — schema registry"]
|
||||
CFGM["index.js (ConfigManager)"]
|
||||
JSON["*.json — per-node schemas"]
|
||||
end
|
||||
|
||||
subgraph math["numeric & domain utilities"]
|
||||
PID["src/pid/ — PIDController"]
|
||||
NRMSE["src/nrmse/ — ErrorMetrics"]
|
||||
STATS["src/stats/ — mean/stddev/median"]
|
||||
OUT["src/outliers/ — DynamicClusterDeviation"]
|
||||
STATE["src/state/ — state FSM"]
|
||||
CONV["src/convert/ — unit conversion"]
|
||||
COOL["src/coolprop-node/ — thermodynamics"]
|
||||
FYS["src/convert/fysics.js — physical constants"]
|
||||
end
|
||||
|
||||
subgraph menu_grp["src/menu/ — editor menus"]
|
||||
MM["MenuManager"]
|
||||
end
|
||||
|
||||
subgraph constants["src/constants/"]
|
||||
POS["positions.js"]
|
||||
end
|
||||
|
||||
BD --> CR
|
||||
BD --> UP
|
||||
BD --> MC
|
||||
BD --> CRU
|
||||
BD --> LOG
|
||||
BNA --> BD
|
||||
BNA --> CMR
|
||||
BNA --> OU
|
||||
BNA --> SU
|
||||
```
|
||||
|
||||
| Directory | Primary export | Read first if you're changing… |
|
||||
|---|---|---|
|
||||
| `src/domain/` | `BaseDomain`, `ChildRouter`, `UnitPolicy`, `LatestWinsGate`, `HealthStatus` | Base class contracts, child routing, unit system |
|
||||
| `src/nodered/` | `BaseNodeAdapter`, `CommandRegistry`, `statusBadge`, `StatusUpdater` | Input dispatch, output loops, editor status |
|
||||
| `src/measurements/` | `MeasurementContainer` | Measurement storage, statistics, 4-segment key output |
|
||||
| `src/helper/` | `logger`, `outputUtils`, `childRegistrationUtils`, `configUtils`, `validationUtils`, `menuUtils`, `gravity` | Logging, InfluxDB formatting, child registration |
|
||||
| `src/configs/` | `ConfigManager` + per-node JSON schemas | Schema loading, config validation, default values |
|
||||
| `src/predict/` | `predict`, `interpolation` | Characteristic curve fitting and flow/power prediction |
|
||||
| `src/pid/` | `PIDController`, `CascadePIDController` | Closed-loop control |
|
||||
| `src/nrmse/` | `ErrorMetrics` (NRMSE) | Prediction quality scoring |
|
||||
| `src/stats/` | `stats` (mean, stddev, median) | Statistical reducers |
|
||||
| `src/outliers/` | `DynamicClusterDeviation` | Online outlier detection |
|
||||
| `src/state/` | `state`, `StateManager`, `MovementManager` | FSM for valve/machine state machines |
|
||||
| `src/convert/` | `convert`, `Fysics` | Unit conversion, physical constants |
|
||||
| `src/coolprop-node/` | `coolprop` | Thermodynamic property lookup |
|
||||
| `src/menu/` | `MenuManager` | Editor-form dropdown population |
|
||||
| `src/constants/` | `POSITIONS`, `POSITION_VALUES`, `isValidPosition` | Canonical spatial position constants |
|
||||
|
||||
---
|
||||
|
||||
## 5. API surface
|
||||
|
||||
<!-- BEGIN AUTOGEN: api-surface -->
|
||||
|
||||
All imports use the package root: `const { X } = require('generalFunctions');`
|
||||
|
||||
| Export | Import name | Source file | Contract |
|
||||
|---|---|---|---|
|
||||
| `BaseDomain` | `BaseDomain` | `src/domain/BaseDomain.js` | Abstract base class for every `specificClass.js`. Provides `emitter`, `config`, `logger`, `measurements`, `childRegistrationUtils`, `router`. Subclass must declare `static name` (maps to schema JSON) and implement `configure()`. See CONTRACTS.md §3. |
|
||||
| `BaseNodeAdapter` | `BaseNodeAdapter` | `src/nodered/BaseNodeAdapter.js` | Abstract base for every `nodeClass.js`. Wires config build → domain instantiation → registration delay → output strategy → status loop → input dispatch → close handler. Subclass declares `static DomainClass`, `static commands`, `static tickInterval`, `static statusInterval`, and overrides `buildDomainConfig(uiConfig, nodeId)`. See CONTRACTS.md §2. |
|
||||
| `ChildRouter` | `ChildRouter` | `src/domain/ChildRouter.js` | Declarative parent-side child registration. Replaces per-node `registerChild` switch. Chain `.onRegister(softwareType, cb)`, `.onMeasurement(softwareType, filter, cb)`, `.onPrediction(softwareType, filter, cb)`. See CONTRACTS.md §5. |
|
||||
| `CommandRegistry` | `CommandRegistry` | `src/nodered/commandRegistry.js` | Class form of the command registry. Accepts array of descriptors (topic, aliases, payloadSchema, units, description, handler). Dispatches by O(1) lookup, normalises units before handler runs, warns on alias use. |
|
||||
| `createRegistry` | `createRegistry` | `src/nodered/commandRegistry.js` | Factory: `createRegistry(descriptors, options)` → `CommandRegistry`. Used by `BaseNodeAdapter`; rarely needed directly. |
|
||||
| `UnitPolicy` | `UnitPolicy` | `src/domain/UnitPolicy.js` | Declare unit sets: `UnitPolicy.declare({ canonical, output, curve?, requireUnitForTypes? })`. Returns policy with dual method/property access (`policy.canonical('flow')` and `policy.canonical.flow`). Methods: `canonical`, `output`, `curve`, `resolve`, `convert`, `containerOptions`, `setLogger`. See CONTRACTS.md §6. |
|
||||
| `LatestWinsGate` | `LatestWinsGate` | `src/domain/LatestWinsGate.js` | Serialises async dispatches so only the latest value wins. `fire(value)` — non-blocking. `fireAndWait(value)` → `Promise` that resolves with dispatch result or `LatestWinsGate.SUPERSEDED`. `drain()` — await idle. See CONTRACTS.md §8. |
|
||||
| `HealthStatus` | `HealthStatus` | `src/domain/HealthStatus.js` | Factory functions for frozen health objects: `HealthStatus.ok(msg, src)`, `HealthStatus.degraded(level, flags, msg, src)`, `HealthStatus.compose(statuses)`. Shape: `{ level: 0–3, flags: string[], message, source }`. See CONTRACTS.md §9. |
|
||||
| `MeasurementContainer` | `MeasurementContainer` | `src/measurements/MeasurementContainer.js` | Chainable measurement store: `.type().variant().position().value(v, ts, srcUnit)`. Query: `getCurrentValue(unit)`, `getAverage(unit)`, `difference({ from, to, unit })`. Introspect: `getFlattenedOutput()` returns 4-segment keyed object (`type.variant.position.childId`). |
|
||||
| `outputUtils` | `outputUtils` | `src/helper/outputUtils.js` | Singleton-per-node delta-compression engine. `formatMsg(output, config, format)` returns `msg` only when fields changed, or `undefined`. `format` is `'process'` or `'influxdb'`. Consumers must cache and merge. |
|
||||
| `logger` | `logger` | `src/helper/logger.js` | `new logger(enabled, logLevel, moduleName)`. Methods: `debug`, `info`, `warn`, `error`, `setLogLevel`, `toggleLogging`. Never use `console.log` directly. |
|
||||
| `configManager` | `configManager` | `src/configs/index.js` | `new configManager()`. Methods: `getConfig(name)`, `buildConfig(name, uiConfig, nodeId, domainSlice?)`, `getAvailableConfigs()`, `hasConfig(name)`. Config files live in `src/configs/*.json`. |
|
||||
| `configUtils` | `configUtils` | `src/helper/configUtils.js` | `new configUtils(defaultConfig)`. `initConfig(userConfig)` validates and merges user values over defaults via `validationUtils`. |
|
||||
| `validation` | `validation` | `src/helper/validationUtils.js` | `new validation(logEnabled, logLevel)`. `validateSchema(config, schema, name)` walks schema, clamps numbers, coerces types, strips unknown keys. |
|
||||
| `childRegistrationUtils` | `childRegistrationUtils` | `src/helper/childRegistrationUtils.js` | `new childRegistrationUtils(parentDomain)`. `registerChild(child, positionVsParent, distance?)` stores child by softwareType/category with alias normalisation. `getChildrenOfType(softwareType, category?)`, `getChildById(id)`, `getAllChildren()`. Normally used via `ChildRouter` — direct use is for advanced cases. |
|
||||
| `statusBadge` | `statusBadge` | `src/nodered/statusBadge.js` | Pure-function badge builder. `statusBadge.compose(parts, opts?)` → `{ fill, shape, text }`. `statusBadge.error(msg)`, `statusBadge.idle(label)`. Text clipped to 60 chars. See CONTRACTS.md §7. |
|
||||
| `StatusUpdater` | `StatusUpdater` | `src/nodered/statusUpdater.js` | `new StatusUpdater({ node, source, intervalMs, logger })`. `start()`, `stop()`. Calls `source.getStatusBadge()` on interval; catches errors and shows a red badge. Owned by `BaseNodeAdapter` — rarely needed directly. |
|
||||
| `convert` | `convert` | `src/convert/index.js` | unit-converter factory. `convert(value).from(unit).to(unit)`. `convert.possibilities(measure)` lists accepted units. Measures: `volumeFlowRate`, `pressure`, `power`, `temperature`, `volume`, `length`, `mass`, `energy`, `reactivePower`, `apparentPower`, `reactiveEnergy`, and more. |
|
||||
| `Fysics` | `Fysics` | `src/convert/fysics.js` | `new Fysics()`. Physical constants: `air_density`, `g0`; methods for gravity and viscosity calculations. |
|
||||
| `gravity` | `gravity` | `src/helper/gravity.js` | Singleton-style `Gravity` instance. `getStandardGravity()` → 9.80665 m/s². WGS-84 latitude/altitude corrections available. |
|
||||
| `predict` | `predict` | `src/predict/predict_class.js` | `new predict(config, logger)`. Multidimensional characteristic-curve predictor; emits results via internal EventEmitter. |
|
||||
| `interpolation` | `interpolation` | `src/predict/interpolation.js` | Class for 1-D and 2-D curve interpolation (linear, cubic-spline). Used internally by `predict`. |
|
||||
| `PIDController` | `PIDController` | `src/pid/PIDController.js` | Discrete PID with bumpless auto/manual transfer, anti-windup, derivative filtering, rate limiting, gain scheduling, feedforward. |
|
||||
| `CascadePIDController` | `CascadePIDController` | `src/pid/PIDController.js` | Outer-inner PID cascade built on `PIDController`. |
|
||||
| `createPidController` | `createPidController` | `src/pid/index.js` | Factory shorthand: `createPidController(options)` → `PIDController`. |
|
||||
| `createCascadePidController` | `createCascadePidController` | `src/pid/index.js` | Factory shorthand for cascade PID. |
|
||||
| `nrmse` | `nrmse` | `src/nrmse/index.js` | `ErrorMetrics` class for normalised-root-mean-squared-error tracking. Multi-metric via `registerMetric(id)`, `update(id, predicted, measured)`. |
|
||||
| `stats` | `stats` | `src/stats/index.js` | Pure functions: `mean(arr)`, `stdDev(arr)`, `median(arr)`. No state; safe to call on any numeric array. |
|
||||
| `state` | `state` | `src/state/index.js` | `new state(config, logger)`. FSM for valve/machine: StateManager (transitions) + MovementManager (timed moves). Emits state-change events. |
|
||||
| `MenuManager` | `MenuManager` | `src/menu/index.js` | `new MenuManager()`. Manages editor dropdown menus (asset, logger, position, aquon). `registerMenu(type, factory)`. Used in node entry files to power Node-RED editor forms. |
|
||||
| `menuUtils` / `MenuUtils` | via `menuUtils` in helper | `src/helper/menuUtils.js` | Browser-side editor helper. Toggles, data fetching, URL construction, dropdown population, HTML generation. Served to browser via `endpointUtils`. |
|
||||
| `POSITIONS` | `POSITIONS` | `src/constants/positions.js` | Frozen enum: `{ UPSTREAM, DOWNSTREAM, AT_EQUIPMENT, DELTA }`. |
|
||||
| `POSITION_VALUES` | `POSITION_VALUES` | `src/constants/positions.js` | `string[]` of all four position strings. |
|
||||
| `isValidPosition` | `isValidPosition` | `src/constants/positions.js` | `(pos: string) => boolean`. |
|
||||
| `coolprop` | `coolprop` | `src/coolprop-node/src/index.js` | CoolProp fluid/gas thermodynamic property lookup. Used by nodes that model heat transfer or gas compression. |
|
||||
| `loadModel` | `loadModel` | `datasets/assetData/modelData/index.js` | Load a JSON model-data asset by dataset type and asset ID (with LRU cache). Preferred over deprecated `loadCurve`. |
|
||||
| `loadCurve` | `loadCurve` | `datasets/assetData/curves/index.js` | **Deprecated** — load a pump-curve JSON. Replaced by `loadModel`. |
|
||||
|
||||
<!-- END AUTOGEN: api-surface -->
|
||||
|
||||
---
|
||||
|
||||
## 6. Config schema registry
|
||||
|
||||
One JSON file per node in `src/configs/`. `ConfigManager.buildConfig` merges the schema defaults with the Node-RED editor values before the domain sees them.
|
||||
|
||||
| File | Node | What it defines |
|
||||
|---|---|---|
|
||||
| `baseConfig.json` | all nodes | Shared `general`, `asset`, `functionality`, `logging` sections |
|
||||
| `rotatingMachine.json` | rotatingMachine | Curve selection, startup/shutdown ramps, safety thresholds, unit config |
|
||||
| `machineGroupControl.json` | machineGroupControl | Demand targets, strategy selection, dispatcher settings |
|
||||
| `pumpingStation.json` | pumpingStation | Basin geometry, hydraulics, control strategies, safety levels |
|
||||
| `measurement.json` | measurement | Scaling, smoothing, stability threshold, digital/MQTT mode |
|
||||
| `valve.json` | valve | Actuator travel time, position limits, FSM config |
|
||||
| `valveGroupControl.json` | valveGroupControl | Group strategy, demand distribution |
|
||||
| `reactor.json` | reactor | ASM kinetics, reactor type (CSTR/PFR), volume, influent |
|
||||
| `settler.json` | settler | Sludge settling parameters, effluent quality |
|
||||
| `monster.json` | monster | Multi-parameter monitoring, flow bounds, sample intervals |
|
||||
| `diffuser.json` | diffuser | Aeration model, oxygen transfer parameters |
|
||||
|
||||
To add a new node: create `src/configs/<nodeName>.json` extending `baseConfig.json`, declare `static name = '<nodeName>'` in the domain class. `configManager.buildConfig` finds it automatically.
|
||||
|
||||
---
|
||||
|
||||
## 7. Lifecycle — how a node tick or event reaches the output port
|
||||
|
||||
The sequence below uses `rotatingMachine` as the example. Every stateful EVOLV node follows the same path. See the [rotatingMachine wiki](../rotatingMachine/Home.md) for node-specific detail.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant RED as Node-RED runtime
|
||||
participant BNA as BaseNodeAdapter
|
||||
participant CMD as CommandRegistry
|
||||
participant DOM as Domain (specificClass)
|
||||
participant CR as ChildRouter
|
||||
participant MC as MeasurementContainer
|
||||
participant OU as outputUtils
|
||||
participant PORT as Port 0 / 1 / 2
|
||||
|
||||
RED->>BNA: constructor(uiConfig, RED, node, name)
|
||||
BNA->>BNA: configManager.buildConfig()
|
||||
BNA->>DOM: new DomainClass(config)
|
||||
DOM->>MC: new MeasurementContainer(unitPolicy.containerOptions())
|
||||
DOM->>DOM: configure() — wire ChildRouter, concern modules
|
||||
BNA-->>PORT: Port 2 registration msg (after 100 ms delay)
|
||||
BNA->>BNA: start status loop (1000 ms)
|
||||
|
||||
Note over RED,PORT: Event-driven path (default)
|
||||
|
||||
RED->>BNA: input msg {topic: 'data.pressure', payload: 3.4}
|
||||
BNA->>CMD: dispatch(msg)
|
||||
CMD->>CMD: unit normalisation (Pa → mbar)
|
||||
CMD->>DOM: handler(source, msg, ctx)
|
||||
DOM->>MC: .type('pressure').variant('measured').position('upstream').value(3.4)
|
||||
DOM->>DOM: emitter.emit('output-changed')
|
||||
BNA->>DOM: getOutput()
|
||||
DOM-->>BNA: flat snapshot object
|
||||
BNA->>OU: formatMsg(snapshot, config, 'process')
|
||||
OU-->>BNA: delta msg (only changed fields)
|
||||
BNA-->>PORT: Port 0 process msg, Port 1 influx msg
|
||||
|
||||
Note over RED,PORT: Tick-driven path (opt-in — tickInterval set)
|
||||
|
||||
RED->>BNA: timer fires every tickInterval ms
|
||||
BNA->>DOM: tick()
|
||||
DOM->>DOM: time-based math; emitter.emit('output-changed')
|
||||
BNA->>DOM: getOutput()
|
||||
BNA->>OU: formatMsg(...)
|
||||
BNA-->>PORT: Port 0 / 1 msgs (delta only)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Stability + versioning
|
||||
|
||||
Source of truth: `.claude/rules/general-functions.md`.
|
||||
|
||||
| Category | Rule |
|
||||
|---|---|
|
||||
| **Safe to add** | New named exports. New optional methods on existing classes. New config keys with defaults in the schema. |
|
||||
| **Requires decision-gate interview** | Removing or renaming any export. Changing a method signature. Changing the output key format of `MeasurementContainer.getFlattenedOutput()`. Changing the `formatMsg` delta-compression behaviour. |
|
||||
| **Forbidden without migration** | Breaking the 4-segment key shape (`type.variant.position.childId`). Changing Port 0/1/2 payload envelope. Changing the CONTRACTS.md §1–§9 shapes. |
|
||||
|
||||
`generalFunctions` is a git submodule shared by all 12 node repos. A breaking change here requires updating every consumer in a single coordinated commit. Before modifying any module, run `grep -r "require('generalFunctions')" nodes/*/` to identify all call sites.
|
||||
|
||||
---
|
||||
|
||||
## 9. No editor form — consumers' config forms map to config slices
|
||||
|
||||
`generalFunctions` has no Node-RED editor form of its own. The library is never placed directly in a flow.
|
||||
|
||||
Consumer nodes expose their own editor forms. Each form field writes into a config key that `configManager.buildConfig` validates against the node's schema (in `src/configs/<nodeName>.json`). The resulting merged config is passed to the domain constructor.
|
||||
|
||||
For the form-to-config mapping of a specific node, see section 9 of that node's wiki page.
|
||||
|
||||
---
|
||||
|
||||
## 10. Examples — usage snippets from a real node
|
||||
|
||||
### 10.1 Extending `BaseDomain` (from `pumpingStation/specificClass.js` pattern)
|
||||
|
||||
```js
|
||||
const { BaseDomain, UnitPolicy, ChildRouter } = require('generalFunctions');
|
||||
|
||||
class PumpingStation extends BaseDomain {
|
||||
static name = 'pumpingStation';
|
||||
|
||||
static unitPolicy = UnitPolicy.declare({
|
||||
canonical: { flow: 'm3/s', pressure: 'Pa', power: 'W', temperature: 'K' },
|
||||
output: { flow: 'm3/h', pressure: 'mbar', power: 'kW', temperature: 'C' },
|
||||
});
|
||||
|
||||
configure() {
|
||||
// Declare named child getters — readable in code, registry is source of truth
|
||||
this.declareChildGetter('machines', 'machine');
|
||||
this.declareChildGetter('machineGroups', 'machinegroup');
|
||||
|
||||
// Declarative child routing — no per-node registerChild switch
|
||||
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() {
|
||||
const { statusBadge } = require('generalFunctions');
|
||||
return statusBadge.compose(['filling', 'V=12.4/50.0 m³']);
|
||||
}
|
||||
}
|
||||
module.exports = PumpingStation;
|
||||
```
|
||||
|
||||
### 10.2 Extending `BaseNodeAdapter` (from `pumpingStation/nodeClass.js` pattern)
|
||||
|
||||
```js
|
||||
const { BaseNodeAdapter } = require('generalFunctions');
|
||||
const Domain = require('./specificClass');
|
||||
const commands = require('./commands');
|
||||
|
||||
class nodeClass extends BaseNodeAdapter {
|
||||
static DomainClass = Domain;
|
||||
static commands = commands;
|
||||
static tickInterval = 1000; // ms — only for time-driven math
|
||||
static statusInterval = 1000;
|
||||
|
||||
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;
|
||||
```
|
||||
|
||||
### 10.3 Command descriptor with unit normalisation
|
||||
|
||||
```js
|
||||
// src/commands/index.js
|
||||
module.exports = [
|
||||
{
|
||||
topic: 'set.demand',
|
||||
aliases: ['Qd'], // legacy name — logs 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); },
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Debug recipes
|
||||
|
||||
| Symptom | First check | Where to look |
|
||||
|---|---|---|
|
||||
| Child never registers (no `registerChild` log) | Is the child's `softwareType` in the `SOFTWARE_TYPE_ALIASES` map? | `src/helper/childRegistrationUtils.js` line 1–12 and `src/domain/ChildRouter.js` |
|
||||
| Port 0 sends nothing after an input | `outputUtils` only emits on changes. Is the field actually different from the last call? | Add a debug tap after `formatMsg`; check `outputUtils._output[format]` state |
|
||||
| Unit mismatch — handler receives wrong value | Did the command descriptor declare `units: { measure, default }`? Is `msg.unit` set by the sender? | `commandRegistry.js` → `_normaliseUnit()`; check the warn log |
|
||||
| `query.units` returns empty object | The commands array has no descriptors with a `units` field. | `BaseNodeAdapter._buildImplicitUnitsCommand()` |
|
||||
| `MeasurementContainer.getFlattenedOutput()` returns unexpected key shape | Key is `type.variant.position.childId` — position is always lowercase. Check `setChildId()` was called. | `src/measurements/MeasurementContainer.js` → `getFlattenedOutput()` |
|
||||
| `LatestWinsGate` promise never resolves | A superseded fire resolves with `{ superseded: true }`, not `undefined`. Branch on `r && r.superseded`. | `src/domain/LatestWinsGate.js` |
|
||||
| Status badge stuck at grey | `getStatusBadge()` threw and `StatusUpdater` caught it. Look for `statusBadge.error(...)` in the container log. | `src/nodered/statusUpdater.js` |
|
||||
|
||||
> Never ship `enableLog: 'debug'` in a demo or production config — it fills the container log within seconds and obscures real errors.
|
||||
|
||||
---
|
||||
|
||||
## 12. When NOT to depend on this library
|
||||
|
||||
- **Passive HTTP gateway nodes** (e.g. `dashboardAPI`) may skip `BaseDomain` and `BaseNodeAdapter` entirely if they hold no domain state. A plain Node-RED node with HTTP endpoints needs only `logger`, `outputUtils`, and `configManager`. See the `dashboardAPI` wiki for the rationale.
|
||||
- **External scripts or standalone tools** that need only unit conversion can import just `const { convert } = require('generalFunctions')` without pulling in the full domain stack.
|
||||
- **Nodes at a different S88 level** that inherit from a third-party base class must not import from `src/domain/` or `src/nodered/` internal paths — they may only use root-level exports.
|
||||
|
||||
---
|
||||
|
||||
## 13. Known limitations
|
||||
|
||||
| # | Issue | Tracked in |
|
||||
|---|---|---|
|
||||
| 1 | `loadCurve` is deprecated; replacement `loadModel` exists but not all nodes have migrated | `OPEN_QUESTIONS.md` — Phase 8.5 cleanup |
|
||||
| 2 | `outlierDetection` (`DynamicClusterDeviation`) prints to `console.log` internally — not routed through `logger` | Code review backlog |
|
||||
| 3 | `configUtils.initConfig` strips unknown keys silently; schema must include every key the domain reads or defaults are lost | `OPEN_QUESTIONS.md` — e.g. monster schema fix 2026-05-11 |
|
||||
| 4 | `state` (FSM) and `predict` are not yet integrated with `BaseDomain` lifecycle — nodes wire them manually in `configure()` | Architecture backlog |
|
||||
| 5 | `menuUtils` / `MenuManager` are served as browser JavaScript and bypass the normal Node.js import path — deep changes require testing in both environments | `endpointUtils.js` |
|
||||
| 6 | `CascadePIDController` has no dedicated test suite | Test backlog |
|
||||
| 7 | `substrate_score` / wiki autogen script (`wiki:all`) not yet wired for this library; API surface block is hand-maintained | Phase 9 follow-up |
|
||||
Reference in New Issue
Block a user