From 2aa80212e44ec44b0a0c013374b03385c8d3f040 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Mon, 11 May 2026 15:17:33 +0200 Subject: [PATCH] P9.3: wiki/Home.md following 14-section visual-first template + wiki:* scripts Auto-generated topic-contract + data-model sections via shared wikiGen script. Hand-written Mermaid diagrams for position-in-platform, code map, child registration, lifecycle, configuration, state chart (where applicable). Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 5 +- wiki/Home.md | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 wiki/Home.md diff --git a/package.json b/package.json index 56ca664..dac5cc9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,10 @@ "description": "Control module measurement", "main": "measurement.js", "scripts": { - "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js" + "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js", + "wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md", + "wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md", + "wiki:all": "npm run wiki:contract && npm run wiki:datamodel" }, "repository": { "type": "git", diff --git a/wiki/Home.md b/wiki/Home.md new file mode 100644 index 0000000..cdea86c --- /dev/null +++ b/wiki/Home.md @@ -0,0 +1,262 @@ +# measurement + +> **Reflects code as of `afc304b` · regenerated `2026-05-11` via `npm run wiki:all`** +> If this banner is stale, the page may be out of date. Treat as informative, not authoritative. + +## 1. What this node is + +**measurement** is an S88 Control Module that turns a raw sensor signal into a validated, scaled, smoothed reading and re-emits it for any parent. Two modes: **analog** (one channel built from the flat config) and **digital** (one Channel per `config.channels[]` entry). It is a leaf in the hierarchy — no children of its own. + +## 2. Position in the platform + +```mermaid +flowchart LR + raw[Raw sensor / MQTT / inject
analog scalar or digital object] + m[measurement
Control Module]:::ctrl + p1[rotatingMachine
Equipment]:::equip + p2[machineGroupControl
Unit]:::unit + p3[pumpingStation
Process Cell]:::pc + + raw -->|data.measurement| m + m -->|child.register| p1 + m -->|child.register| p2 + m -->|child.register| p3 + m -..measured..-> p1 + m -..measured..-> p2 + m -..measured..-> p3 + classDef pc fill:#0c99d9,color:#fff + classDef unit fill:#50a8d9,color:#000 + classDef equip fill:#86bbdd,color:#000 + classDef ctrl fill:#a9daee,color:#000 +``` + +S88 colours: Control Module `#a9daee`, Equipment `#86bbdd`, Unit `#50a8d9`, Process Cell `#0c99d9`. Source of truth: `.claude/rules/node-red-flow-layout.md`. + +## 3. Capability matrix + +| Capability | Status | Notes | +|---|---|---| +| Analog mode — single channel from flat config | ✅ | Default. `data.measurement` payload is numeric. | +| Digital mode — many channels from `config.channels[]` | ✅ | Payload is an object keyed by `channel.key`. | +| Outlier detection | ✅ | Median ± window check. Toggleable via `set.outlier-detection`. | +| Scaling (input range → process range + offset) | ✅ | `config.scaling.{inputMin,inputMax,absMin,absMax,offset}`. | +| Smoothing (moving window) | ✅ | `config.smoothing.{smoothWindow,smoothMethod}`. | +| Min/max tracking | ✅ | `totalMinValue`, `totalMaxValue`, smoothed variants. | +| Calibration (capture current as zero/reference) | ✅ | `cmd.calibrate`. Mutates `config.scaling.offset`. | +| Built-in simulator | ✅ | Sinusoidal/noise driver — `set.simulator` toggles. | +| Repeatability / stability metrics | ✅ | `evaluateRepeatability()`, `isStable()`. | +| Accepts children of its own | ❌ | Leaf node. | + +## 4. Code map + +```mermaid +flowchart TB + subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"] + nc["buildDomainConfig()
static DomainClass, commands
static tickInterval = 1000ms"] + end + subgraph domain["specificClass.js — orchestrator (BaseDomain)"] + sc["Measurement.configure()
mode = analog | digital
builds Channel(s)"] + end + subgraph concerns["src/ concern modules"] + channel["channel.js
outlier → offset → scaling →
smoothing → minMax pipeline"] + simulation["simulation/
built-in Simulator"] + calibration["calibration/
Calibrator + stability"] + commands["commands/
topic registry + handlers"] + end + nc --> sc + sc --> channel + sc --> simulation + sc --> calibration + nc --> commands +``` + +| Module | Owns | Read first if you're changing… | +|---|---|---| +| `channel.js` | Per-channel pipeline (outlier → offset → scaling → smoothing → emit) | Per-tick reading flow, unit semantics, emitted event name. | +| `simulation/` | Built-in signal generator for demos and offline tests | Sim behaviour, period / amplitude. | +| `calibration/` | Stability checks, repeatability, offset capture | `cmd.calibrate` behaviour, stable-window heuristic. | +| `commands/` | Input-topic registry and handlers | New input topics, payload validation. | + +The analog/digital branch is decided once in `configure()` based on `config.mode.current`. There is no FSM — `tick()` only pumps the simulator when enabled. + +```mermaid +flowchart LR + cfg[config.mode.current] + cfg -->|"=== 'digital'"| dig[Build N Channels
from config.channels[]] + cfg -->|"=== 'analog' (default)"| ana[Build 1 Channel
from flat config] + dig --> emit_d[handleDigitalPayload
fan-out per channel] + ana --> emit_a[inputValue setter
single channel update] +``` + +## 5. Topic contract + +> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. + + + +| Canonical topic | Aliases | Payload | Effect | +|---|---|---|---| +| `set.simulator` | `simulator` | `any` | Replaces the named state value with the supplied payload. | +| `set.outlier-detection` | `outlierDetection` | `any` | Replaces the named state value with the supplied payload. | +| `cmd.calibrate` | `calibrate` | `any` | Triggers an action / sequence — not idempotent. | +| `data.measurement` | `measurement` | `any` | Pushes a value into the node's measurement stream. | + + + +## 6. Child registration + +`measurement` does **not accept children**. It only **registers itself** as a child on its upstream parent. + +```mermaid +flowchart LR + m[measurement]:::ctrl -->|"child.register
(Port 2 at startup)"| parent[rotatingMachine /
MGC / pumpingStation /
reactor / monster] + m -.->|"<type>.measured.<position>
(measurements.emitter)"| parent + classDef ctrl fill:#a9daee,color:#000 +``` + +| What | softwareType payload | Side-effect on parent | +|---|---|---| +| Registration | `measurement` | Parent attaches listener for `.measured.`. | +| Subsequent updates | event on `child.measurements.emitter` | Parent mirrors value into its own `MeasurementContainer`. | + +Position labels are normalised to **lowercase** in the event name (`upstream`, `downstream`, `atequipment`). + +## 7. Lifecycle — what one event (or tick) does + +```mermaid +sequenceDiagram + participant ext as external sender + participant m as measurement + participant ch as Channel pipeline + participant emitter as measurements.emitter + participant parent as parent (e.g. rotatingMachine) + + ext->>m: data.measurement (12.4) + m->>m: command dispatch (analog branch) + m->>ch: update(12.4) + ch->>ch: outlier check → offset → scale → smooth → minMax + ch->>emitter: .measured. {value, ts, unit} + emitter-->>parent: child event (subscribed at register-time) + m->>m: notifyOutputChanged() + m-->>ext: Port 0 + Port 1 (delta-compressed) + Note over m: every 1000 ms: if simulation.enabled,
simulator.step() → inputValue +``` + +## 8. Data model — `getOutput()` + +Analog mode emits the legacy scalar shape. Digital mode emits a nested `{channels:{...}}` keyed by `channel.key`. + + + +| Key | Type | Unit | Sample | +|---|---|---|---| +| `mAbs` | number | — | `0` | +| `mPercent` | number | — | `0` | +| `totalMaxSmooth` | number | — | `0` | +| `totalMaxValue` | number | — | `0` | +| `totalMinSmooth` | number | — | `0` | +| `totalMinValue` | number | — | `0` | + + + +**Concrete digital sample** (when `mode='digital'`): + +~~~json +{ + "channels": { + "level-a": { "mAbs": 1.84, "mPercent": 73.6, "totalMinValue": 0.1, "totalMaxValue": 2.4 }, + "temp-a": { "mAbs": 18.2, "mPercent": 36.4, "totalMinValue": 14.0, "totalMaxValue": 22.1 } + } +} +~~~ + +In addition, the legacy `source.emitter` fires `'mAbs'` (analog only) — kept for the editor status badge during the refactor window. + +## 9. Configuration — editor form ↔ config keys + +```mermaid +flowchart TB + subgraph editor["Node-RED editor form"] + f1[Mode: analog / digital] + f2[Asset type + unit] + f3[Position vs parent] + f4[Scaling: inputMin/Max, absMin/Max, offset] + f5[Smoothing: window + method] + f6[Outlier detection: enabled + window] + f7[Simulation: enabled + amplitude/period] + f8[Digital channels list] + end + subgraph cfg["Domain config slice"] + c1[mode.current] + c2[asset.type / asset.unit] + c3[functionality.positionVsParent] + c4[scaling.*] + c5[smoothing.*] + c6[outlierDetection.*] + c7[simulation.*] + c8[channels []] + end + f1 --> c1 + f2 --> c2 + f3 --> c3 + f4 --> c4 + f5 --> c5 + f6 --> c6 + f7 --> c7 + f8 --> c8 +``` + +| Form field | Config key | Default | Range | Where used | +|---|---|---|---|---| +| Mode | `mode.current` | `analog` | enum (`analog`, `digital`) | `Measurement.configure` | +| Asset type | `asset.type` | `pressure` | enum | event name + unit policy | +| Position vs parent | `functionality.positionVsParent` | `atEquipment` | enum | event name suffix | +| Scaling enabled | `scaling.enabled` | `false` | bool | `Channel._applyScaling` | +| Input min/max | `scaling.inputMin/Max` | `0` / `1` | numeric | linear map foot/top | +| Output min/max | `scaling.absMin/absMax` | `50` / `100` | numeric | linear map foot/top | +| Offset | `scaling.offset` | `0` | numeric | calibration target | +| Smoothing window | `smoothing.smoothWindow` | `10` | ≥ 1 (samples) | moving window | +| Outlier detection | `outlierDetection.enabled` | varies | bool | `Channel._isOutlier` | +| Simulation enabled | `simulation.enabled` | `false` | bool | `tick()` step | + +## 10. State chart + +**Skipped** — measurement is a pure pipeline. There is no FSM. The only mode switch (analog vs digital) is decided once at `configure()` time and never transitions thereafter; see section 4 for the static branching diagram. + +## 11. Examples + +| Tier | File | What it shows | Status | +|---|---|---|---| +| Basic | `examples/basic.flow.json` | Inject + dashboard, no parent | ⚠️ legacy shape, pre-refactor | +| Integration | `examples/integration.flow.json` | measurement registered as child of a parent | ⚠️ legacy shape, pre-refactor | +| Edge | `examples/edge.flow.json` | Outlier / scaling / simulator edge cases | ⚠️ legacy shape, pre-refactor | + +Tier 1/2/3 visual-first example flows are still TODO (see `MEMORY.md` "TODO: Example Flows"). Screenshots will land under `wiki/_partial-screenshots/measurement/` when the new flows ship. + +## 12. Debug recipes + +| Symptom | First thing to check | Where to look | +|---|---|---| +| Parent never receives `.measured.` | `assetType` must match parent's filter exactly (e.g. `flow` — not `flow-electromagnetic`). | `config.asset.type` + `MEMORY.md` integration gotcha. | +| Position labels look uppercase to parent | Event name lowercases — but `functionality.positionVsParent` is sent as-is on `child.register`. | `_buildAnalogChannel` event-name composition. | +| Outliers seem to pass through | `outlierDetection.enabled` may be off (default varies by config). Toggle with `set.outlier-detection`. | `Channel._isOutlier`. | +| `cmd.calibrate` does nothing | Calibrator requires ≥ 2 stable samples — check `isStable()` first. | `calibration/calibrator.js`. | +| Digital payload silently dropped | Unknown channel keys land in the `unknown` log line only at debug level. | enable `logging.logLevel=debug` momentarily. | +| Simulator still running after toggle off | `tick()` reads `config.simulation.enabled` each tick — confirm the toggle actually mutated the config. | `toggleSimulation`. | + +> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. + +## 13. When you would NOT use this node + +- Don't use measurement to **fuse** signals from multiple sensors — it's per-channel only. Aggregate at the parent. +- Don't use measurement for **control output** — it's read-only signal conditioning. Use `rotatingMachine` / `valve` for actuation. +- Don't use measurement for **alarm logic** — there is no threshold-trip output. Build that on top of the emitted reading at the parent or in a dashboard rule. + +## 14. Known limitations / current issues + +| # | Issue | Tracked in | +|---|---|---| +| 1 | Legacy `source.emitter` 'mAbs' event still fired alongside `measurements.emitter` — slated for removal in Phase 7. | `OPEN_QUESTIONS.md` (2026-05-10) | +| 2 | Digital mode's per-channel scaling/smoothing falls back to the analog block's defaults when not specified per channel. | `_buildDigitalChannels`. | +| 3 | Tier 1/2/3 visual-first example flows not yet written; current `examples/` only contains pre-refactor flows. | P9 / P2.14 follow-up. | +| 4 | No automatic recalibration — `cmd.calibrate` is operator-triggered. | `calibration/calibrator.js`. |