Compare commits
1 Commits
42a0333b7c
...
2aa80212e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2aa80212e4 |
@@ -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",
|
||||
|
||||
262
wiki/Home.md
Normal file
262
wiki/Home.md
Normal file
@@ -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<br/>analog scalar or digital object]
|
||||
m[measurement<br/>Control Module]:::ctrl
|
||||
p1[rotatingMachine<br/>Equipment]:::equip
|
||||
p2[machineGroupControl<br/>Unit]:::unit
|
||||
p3[pumpingStation<br/>Process Cell]:::pc
|
||||
|
||||
raw -->|data.measurement| m
|
||||
m -->|child.register| p1
|
||||
m -->|child.register| p2
|
||||
m -->|child.register| p3
|
||||
m -.<type>.measured.<position>.-> p1
|
||||
m -.<type>.measured.<position>.-> p2
|
||||
m -.<type>.measured.<position>.-> 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()<br/>static DomainClass, commands<br/>static tickInterval = 1000ms"]
|
||||
end
|
||||
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
|
||||
sc["Measurement.configure()<br/>mode = analog | digital<br/>builds Channel(s)"]
|
||||
end
|
||||
subgraph concerns["src/ concern modules"]
|
||||
channel["channel.js<br/>outlier → offset → scaling →<br/>smoothing → minMax pipeline"]
|
||||
simulation["simulation/<br/>built-in Simulator"]
|
||||
calibration["calibration/<br/>Calibrator + stability"]
|
||||
commands["commands/<br/>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<br/>from config.channels[]]
|
||||
cfg -->|"=== 'analog' (default)"| ana[Build 1 Channel<br/>from flat config]
|
||||
dig --> emit_d[handleDigitalPayload<br/>fan-out per channel]
|
||||
ana --> emit_a[inputValue setter<br/>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`.
|
||||
|
||||
<!-- BEGIN AUTOGEN: topic-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. |
|
||||
|
||||
<!-- END AUTOGEN: topic-contract -->
|
||||
|
||||
## 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<br/>(Port 2 at startup)"| parent[rotatingMachine /<br/>MGC / pumpingStation /<br/>reactor / monster]
|
||||
m -.->|"<type>.measured.<position><br/>(measurements.emitter)"| parent
|
||||
classDef ctrl fill:#a9daee,color:#000
|
||||
```
|
||||
|
||||
| What | softwareType payload | Side-effect on parent |
|
||||
|---|---|---|
|
||||
| Registration | `measurement` | Parent attaches listener for `<asset.type>.measured.<positionVsParent>`. |
|
||||
| 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: <type>.measured.<position> {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,<br/>simulator.step() → inputValue
|
||||
```
|
||||
|
||||
## 8. Data model — `getOutput()`
|
||||
|
||||
Analog mode emits the legacy scalar shape. Digital mode emits a nested `{channels:{...}}` keyed by `channel.key`.
|
||||
|
||||
<!-- BEGIN AUTOGEN: data-model -->
|
||||
|
||||
| Key | Type | Unit | Sample |
|
||||
|---|---|---|---|
|
||||
| `mAbs` | number | — | `0` |
|
||||
| `mPercent` | number | — | `0` |
|
||||
| `totalMaxSmooth` | number | — | `0` |
|
||||
| `totalMaxValue` | number | — | `0` |
|
||||
| `totalMinSmooth` | number | — | `0` |
|
||||
| `totalMinValue` | number | — | `0` |
|
||||
|
||||
<!-- END AUTOGEN: data-model -->
|
||||
|
||||
**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 `<type>.measured.<position>` | `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`. |
|
||||
Reference in New Issue
Block a user