release: palette redesign + CoreSync scaffolding + dashboardAPI MODULE_NOT_FOUND fix
PALETTE REDESIGN (2026-05-21)
Sidebar swatches switched from S88 level (all blue) to domain-hue per node.
Family hue = function (rotating=orange, valves=teal, biology=green/olive,
sampling=violet, sensor=amber, aeration=sky-blue, infrastructure=slate);
within a family, darker = higher S88 / "more controller-ish."
Editor-group rectangles in flow.json still follow S88 — only the
registerType colour changed.
Submodule bumps for palette: rotatingMachine, machineGroupControl,
pumpingStation, valve, valveGroupControl, reactor, settler, monster,
measurement, diffuser, dashboardAPI.
Docs touched:
- CLAUDE.md: palette swatch vs. editor-group bullets split out.
- .claude/rules/node-red-flow-layout.md: new §10.0 introduces the two
color systems, full 12-row palette table, and explicit warning not to
mix the two hexes.
- .claude/refactor/MODULE_SPLIT.md: per-node headers annotated with
both `group #XXX` and `palette #XXX`.
- .claude/refactor/WIKI_HOME_TEMPLATE.md + WIKI_TEMPLATE.md: clarify
Mermaid classDefs visualize hierarchy, not palette swatches.
- .claude/refactor/OPEN_QUESTIONS.md: dated decision entry with
rationale, file list, and follow-ups.
CORESYNC SUBMODULE (new)
nodes/coresync added pointing at https://gitea.wbd-rd.nl/RnD/coresync.
FROST/SensorThings handoff path — first version forwards FROST-ready HTTP
request messages on the dbase output; a downstream http-request node
performs the POST and feeds responses back on msg.topic = "frost.response".
Lazy stream resolver, latest-wins queue (keep first + latest, drop middle),
knot-emit on slope change, provenance preserved in Observation parameters.
- .gitmodules: add nodes/coresync entry.
- package.json: register coresync as a Node-RED node.
- generalFunctions bump: new frostFormatter + 4 node config schemas
expose the dbase format option.
- measurement bump: "frost" option added to dbaseOutputFormat dropdown
(plus the in-flight data.measurement unit-handling work).
- machineGroupControl bump: small editor compact-fields tweak alongside
the palette change.
- CORESYNC_FROST_INTERVIEW_HANDOFF.md added at root with interview state
(Q20 open: slope angle vs. relative delta comparison).
DASHBOARDAPI MODULE_NOT_FOUND FIX
package.json: dashboardapi entry path corrected to
nodes/dashboardAPI/dashboardAPI.js. Commit e04c4a1 renamed the files to
camelCase but missed package.json; on case-sensitive filesystems
(Linux/Docker, where the tarball lands) the require resolved to nothing
and the node showed MODULE_NOT_FOUND in the Node-RED palette.
MISC CLEANUP
- examples/README.md + examples/pumpingstation-complete-example/ removal
(build_flow.py, flow.json, README.md superseded by per-node examples).
- jest.config.js: in-progress tweak.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ nodes/<name>/
|
|||||||
edge/
|
edge/
|
||||||
```
|
```
|
||||||
|
|
||||||
## pumpingStation (Process Cell — L5, `#0c99d9`)
|
## pumpingStation (Process Cell — L5, group `#0c99d9` · palette `#8B4513`)
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
@@ -61,7 +61,7 @@ examples/
|
|||||||
standalone-demo.js # extracted from the bottom of specificClass.js
|
standalone-demo.js # extracted from the bottom of specificClass.js
|
||||||
```
|
```
|
||||||
|
|
||||||
## measurement (Control Module — L2, `#a9daee`)
|
## measurement (Control Module — L2, group `#a9daee` · palette `#D4A02E`)
|
||||||
|
|
||||||
The good news: `Channel.js` already exists and is pure. Most of the
|
The good news: `Channel.js` already exists and is pure. Most of the
|
||||||
analog mode in `specificClass.js` is duplication that vanishes when the
|
analog mode in `specificClass.js` is duplication that vanishes when the
|
||||||
@@ -90,7 +90,7 @@ src/
|
|||||||
`generalFunctions/src/stats/`. Both `Channel.static helpers` and the
|
`generalFunctions/src/stats/`. Both `Channel.static helpers` and the
|
||||||
calibrator use them.
|
calibrator use them.
|
||||||
|
|
||||||
## machineGroupControl (Unit — L4, `#50a8d9`)
|
## machineGroupControl (Unit — L4, group `#50a8d9` · palette `#B5651D`)
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
@@ -117,7 +117,7 @@ src/
|
|||||||
handlers.js
|
handlers.js
|
||||||
```
|
```
|
||||||
|
|
||||||
## rotatingMachine (Equipment Module — L3, `#86bbdd`)
|
## rotatingMachine (Equipment Module — L3, group `#86bbdd` · palette `#E89B3A`)
|
||||||
|
|
||||||
The biggest specificClass (1760 lines). The split mirrors the natural
|
The biggest specificClass (1760 lines). The split mirrors the natural
|
||||||
boundaries the existing comments suggest.
|
boundaries the existing comments suggest.
|
||||||
@@ -166,6 +166,8 @@ src/
|
|||||||
| `diffuser` | Equipment Module. Aeration controller. Likely small. |
|
| `diffuser` | Equipment Module. Aeration controller. Likely small. |
|
||||||
| `dashboardAPI` | Utility. InfluxDB endpoints. Likely no `BaseDomain` — it's a passive HTTP server. |
|
| `dashboardAPI` | Utility. InfluxDB endpoints. Likely no `BaseDomain` — it's a passive HTTP server. |
|
||||||
|
|
||||||
|
Palette swatches for these (sidebar): `valve` `#3CAEA3`, `valveGroupControl` `#2A8A82`, `reactor` `#6FAE5F`, `settler` `#8FAD3F`, `monster` `#9C5BB0`, `diffuser` `#6EB5E5`, `dashboardAPI` `#7A8BA3`. Group-box hex still follows S88 level (see `.claude/rules/node-red-flow-layout.md` §10.0).
|
||||||
|
|
||||||
The "skeleton" refactor for these is just:
|
The "skeleton" refactor for these is just:
|
||||||
- Convert `nodeClass.js` to extend `BaseNodeAdapter`.
|
- Convert `nodeClass.js` to extend `BaseNodeAdapter`.
|
||||||
- Convert `specificClass.js` to extend `BaseDomain`.
|
- Convert `specificClass.js` to extend `BaseDomain`.
|
||||||
|
|||||||
@@ -741,3 +741,25 @@ work can decide whether to preserve original casing globally.
|
|||||||
**Decision needed by:** Phase 7 (topic-name + schema standardisation) —
|
**Decision needed by:** Phase 7 (topic-name + schema standardisation) —
|
||||||
once enums standardise on a canonical casing, drop the `.toUpperCase()`
|
once enums standardise on a canonical casing, drop the `.toUpperCase()`
|
||||||
guard here.
|
guard here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-05-21 — Palette swatches switched to domain-hue (resolved)
|
||||||
|
|
||||||
|
**Context:** Node-RED sidebar showed every EVOLV node in a shade of blue because palette colours were set from the S88 level (Area / ProcessCell / Unit / Equipment / ControlModule). Operators reported difficulty picking the right node by eye.
|
||||||
|
|
||||||
|
**Decision:** Split the colour systems. The **palette swatch** in each `<node>.html` (`RED.nodes.registerType({ color })`) becomes domain-hue per node; family hue = function (rotating = orange, valves = teal, biology = green/olive, sampling = violet, sensor = amber, infrastructure = slate, aeration = sky blue). Within a family, darker = higher S88 (e.g. RM → MGC → pumpingStation darkens the orange). **Editor-group rectangles** in `flow.json` (`style.fill`) continue to follow S88 level — the hierarchy story stays visible in flow diagrams. Two systems, two purposes.
|
||||||
|
|
||||||
|
**Final palette table:** see `.claude/rules/node-red-flow-layout.md` §10.0.
|
||||||
|
|
||||||
|
**Why split rather than rework S88:** S88 hierarchy is genuinely useful for flow-diagram readability (it's the whole point of group boxes). Throwing it out to fix palette identifiability would have cost the hierarchy signal. Two systems = both problems solved.
|
||||||
|
|
||||||
|
**Files touched (palette):** the 12 `nodes/<n>/<n>.html` files, one line each.
|
||||||
|
|
||||||
|
**Files touched (docs):** `CLAUDE.md` (L52 split into palette + group lines); `.claude/rules/node-red-flow-layout.md` (new §10.0); `.claude/refactor/MODULE_SPLIT.md` (per-node headers annotated with both hexes); `.claude/refactor/WIKI_HOME_TEMPLATE.md` + `WIKI_TEMPLATE.md` (clarifying sentence — Mermaid classDefs are hierarchy, not palette); this entry.
|
||||||
|
|
||||||
|
**Unchanged on purpose:** 32 submodule wiki/CLAUDE.md files that name S88 hexes — they describe hierarchy diagrams or editor-group boxes, both of which still use S88. Spot-checked `rotatingMachine` + `reactor` wikis to confirm.
|
||||||
|
|
||||||
|
**Open follow-ups:**
|
||||||
|
- If `coresync` ends up classified as a process-data node rather than infrastructure, repick a non-slate hue.
|
||||||
|
- Consider a `tools/palette-lint/` check that diffs declared palette hexes vs. this table to catch future drift (low priority).
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ flowchart TB
|
|||||||
classDef neutral fill:#dddddd,color:#000
|
classDef neutral fill:#dddddd,color:#000
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
S88 colours: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Solid arrow = parent/child relationship. Dashed arrow = data flow (`measurement` feeds many node types).
|
S88 colours (used here for **hierarchy visualization only** — distinct from the node-palette swatches in the Node-RED sidebar, which are domain-hue; see `.claude/rules/node-red-flow-layout.md` §10.0): Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Solid arrow = parent/child relationship. Dashed arrow = data flow (`measurement` feeds many node types).
|
||||||
|
|
||||||
## Live nodes
|
## Live nodes
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ flowchart LR
|
|||||||
classDef ctrl fill:#a9daee,color:#000
|
classDef ctrl fill:#a9daee,color:#000
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
S88 colours are mandatory. Map: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
|
S88 colours are mandatory **inside hierarchy diagrams** (Mermaid `classDef`, flow.json group `style.fill`). They are NOT the node-palette swatch hexes shown in the Node-RED sidebar — those are domain-hue per node. Map (hierarchy use): Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md` (§10.0 for palette, §10.1 for groups/lanes).
|
||||||
|
|
||||||
## 3. Capability matrix
|
## 3. Capability matrix
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,34 @@ Before declaring a flow done:
|
|||||||
|
|
||||||
The lane assignment maps to the **S88 hierarchy**, not to specific node names. Any node that lives at a given S88 level goes in the same lane regardless of what kind of equipment it is. New node types added to the platform inherit a lane by their S88 category — no rule change needed.
|
The lane assignment maps to the **S88 hierarchy**, not to specific node names. Any node that lives at a given S88 level goes in the same lane regardless of what kind of equipment it is. New node types added to the platform inherit a lane by their S88 category — no rule change needed.
|
||||||
|
|
||||||
|
### 10.0 Two color systems — palette swatch vs. editor group
|
||||||
|
|
||||||
|
EVOLV uses two distinct color schemes for two distinct purposes. Mixing them up is the most common visual-design bug we see in flows.
|
||||||
|
|
||||||
|
| System | Where it's set | What it signals | Scheme |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Palette swatch** | `RED.nodes.registerType(..., { color })` in `<node>.html` | "Which node am I picking from the sidebar?" | **Domain-hue per node** (table below) |
|
||||||
|
| **Editor group rectangle** | `style.fill` on a `group` node in `flow.json` | "Which S88 cluster does this box represent?" | **S88 level** (§10.1 table) |
|
||||||
|
|
||||||
|
**Palette swatches (set 2026-05-21).** Family hue = function. Within a family, darker = higher S88 / "more controller-ish."
|
||||||
|
|
||||||
|
| Node | Hex | Family |
|
||||||
|
|---|---|---|
|
||||||
|
| `rotatingMachine` | `#E89B3A` | 🟧 orange — leaf (individual machine) |
|
||||||
|
| `machineGroupControl` | `#B5651D` | 🟫 orange — mid (parent of RM) |
|
||||||
|
| `pumpingStation` | `#8B4513` | 🟤 orange — dark (top of pump hierarchy) |
|
||||||
|
| `valve` | `#3CAEA3` | 🟦 teal — leaf |
|
||||||
|
| `valveGroupControl` | `#2A8A82` | 🟦 teal — dark (parent of valve) |
|
||||||
|
| `reactor` | `#6FAE5F` | 🟩 green — biology |
|
||||||
|
| `settler` | `#8FAD3F` | 🟢 olive — biology |
|
||||||
|
| `diffuser` | `#6EB5E5` | 🟦 sky blue — aeration |
|
||||||
|
| `monster` | `#9C5BB0` | 🟪 violet — sampling |
|
||||||
|
| `measurement` | `#D4A02E` | 🟨 amber — sensor |
|
||||||
|
| `dashboardAPI` | `#7A8BA3` | ⬜ slate — infrastructure |
|
||||||
|
| `coresync` | `#54647B` | ⬛ dark slate — infrastructure |
|
||||||
|
|
||||||
|
**Important:** the §10.1 "Colour" column below refers to **editor groups + lane backgrounds** (S88), not to the palette swatch. Don't use the S88 hex inside `registerType`; don't use the palette hex inside a `flow.json` group `style.fill`.
|
||||||
|
|
||||||
### 10.1 Lane convention (x-axis = S88 level)
|
### 10.1 Lane convention (x-axis = S88 level)
|
||||||
|
|
||||||
| Lane | x | Purpose | S88 level | Colour | Current EVOLV nodes |
|
| Lane | x | Purpose | S88 level | Colour | Current EVOLV nodes |
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -35,3 +35,6 @@
|
|||||||
[submodule "nodes/settler"]
|
[submodule "nodes/settler"]
|
||||||
path = nodes/settler
|
path = nodes/settler
|
||||||
url = https://gitea.wbd-rd.nl/RnD/settler.git
|
url = https://gitea.wbd-rd.nl/RnD/settler.git
|
||||||
|
[submodule "nodes/coresync"]
|
||||||
|
path = nodes/coresync
|
||||||
|
url = https://gitea.wbd-rd.nl/RnD/coresync.git
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ only the file paths change. `dashboardAPI` was migrated this way on 2026-05-19.
|
|||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
- Nodes register under category `'EVOLV'` in Node-RED
|
- Nodes register under category `'EVOLV'` in Node-RED
|
||||||
- S88 color scheme: Area=#0f52a5, ProcessCell=#0c99d9, Unit=#50a8d9, Equipment=#86bbdd, ControlModule=#a9daee
|
- Two color systems (don't confuse):
|
||||||
|
- **Palette swatch** (Node-RED sidebar, set in `<node>.html`) = domain-hue per node — full table in `.claude/rules/node-red-flow-layout.md` §10.0. Changed 2026-05-21; see `.claude/refactor/OPEN_QUESTIONS.md`.
|
||||||
|
- **Editor-group rectangle** (flow.json `style.fill`) = S88 level (unchanged): Area=#0f52a5, ProcessCell=#0c99d9, Unit=#50a8d9, Equipment=#86bbdd, ControlModule=#a9daee
|
||||||
- Config JSON files in `generalFunctions/src/configs/` define defaults, types, enums per node
|
- Config JSON files in `generalFunctions/src/configs/` define defaults, types, enums per node
|
||||||
- Tick loop is **opt-in per node** — default cadence 1000 ms, but each node sets `static tickInterval` (or skips it). See `.claude/refactor/OPEN_QUESTIONS.md` (2026-05-10 entry) for the design decision
|
- Tick loop is **opt-in per node** — default cadence 1000 ms, but each node sets `static tickInterval` (or skips it). See `.claude/refactor/OPEN_QUESTIONS.md` (2026-05-10 entry) for the design decision
|
||||||
- Output ports + 3-tier architecture + file-naming + `src/editor/` layout: see `.claude/rules/node-architecture.md`
|
- Output ports + 3-tier architecture + file-naming + `src/editor/` layout: see `.claude/rules/node-architecture.md`
|
||||||
|
|||||||
414
CORESYNC_FROST_INTERVIEW_HANDOFF.md
Normal file
414
CORESYNC_FROST_INTERVIEW_HANDOFF.md
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
# CoreSync FROST Interview Handoff
|
||||||
|
|
||||||
|
Date: 2026-05-19
|
||||||
|
|
||||||
|
## Continue Here First
|
||||||
|
|
||||||
|
Resume the interview at **Question 20**. The last open design topic was the reducer comparison method:
|
||||||
|
|
||||||
|
**Q20. Should slope change be compared by angle in degrees or by relative slope delta?**
|
||||||
|
|
||||||
|
Recommended direction before pausing:
|
||||||
|
|
||||||
|
- Support both eventually.
|
||||||
|
- Default to angle comparison with normalized time/value axes.
|
||||||
|
- Compute `dx = deltaTimeMs / timeScaleMs`.
|
||||||
|
- Compute `dy = deltaValue / valueScale`.
|
||||||
|
- Compare `atan2(dy, dx)` direction changes against `angleToleranceDeg`.
|
||||||
|
|
||||||
|
## Agreed Decisions
|
||||||
|
|
||||||
|
- Use FROST/SensorThings instead of direct InfluxDB for the new CoreSync path.
|
||||||
|
- Keep EVOLV standard outputs:
|
||||||
|
- `process`
|
||||||
|
- `dbase`
|
||||||
|
- `parent`
|
||||||
|
- Add a `dbase` output format option for `frost`.
|
||||||
|
- `dbase = frost` emits FROST-ready HTTP request messages.
|
||||||
|
- The CoreSync node does not post directly to FROST in the first version.
|
||||||
|
- A normal Node-RED HTTP request node sends the FROST messages.
|
||||||
|
- HTTP responses feed back into the same CoreSync input with `msg.topic = "frost.response"`.
|
||||||
|
- All FROST metadata lookup/create/patch requests leave on `dbase`, not `process`.
|
||||||
|
- `process` is reserved for functional process data and optional functional state.
|
||||||
|
- The resolver is lazy: streams are resolved only when telemetry arrives.
|
||||||
|
- Pending queue policy for unresolved/FROST-down streams is keep first + latest, drop middle.
|
||||||
|
- Observation writes use nested Datastream endpoints:
|
||||||
|
- `POST /v1.1/Datastreams({datastreamId})/Observations`
|
||||||
|
- Preserve provenance in Observation `parameters`.
|
||||||
|
- On angle/slope change, emit the previous point as the knot.
|
||||||
|
- Do not forward-fill delta-compressed fields.
|
||||||
|
- Latest values are queried per Datastream:
|
||||||
|
- `/Datastreams(id)/Observations?$orderby=phenomenonTime desc&$top=1`
|
||||||
|
|
||||||
|
## SensorThings Mapping
|
||||||
|
|
||||||
|
- EVOLV asset/apparatus/node -> FROST `Thing`
|
||||||
|
- EVOLV field `type` -> FROST `ObservedProperty`
|
||||||
|
- EVOLV `variant` (`measured`, `predicted`, `setpoint`) -> FROST `Sensor`
|
||||||
|
- EVOLV `position` -> stable FROST `FeatureOfInterest`
|
||||||
|
- EVOLV numeric field -> one FROST `Datastream`
|
||||||
|
- One reducer-kept knot -> one FROST `Observation`
|
||||||
|
|
||||||
|
Stable FOI convention:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{thingId}:upstream
|
||||||
|
{thingId}:atEquipment
|
||||||
|
{thingId}:downstream
|
||||||
|
```
|
||||||
|
|
||||||
|
Also copy position into `Datastream.properties.position` for filtering.
|
||||||
|
|
||||||
|
## Units
|
||||||
|
|
||||||
|
Use EVOLV canonical ingest units. UI conversion happens client-side.
|
||||||
|
|
||||||
|
- pressure: `Pa`
|
||||||
|
- flow: `m3/s`
|
||||||
|
- power: `W`
|
||||||
|
- temperature: `K`
|
||||||
|
- density: `kg/m3`
|
||||||
|
- level: `m`
|
||||||
|
- volume: `m3`
|
||||||
|
- control / percentage / efficiency: normalized ratio `1`
|
||||||
|
|
||||||
|
No leading zeros in engineering tags:
|
||||||
|
|
||||||
|
```text
|
||||||
|
P-1
|
||||||
|
PT-1
|
||||||
|
FT-9999999
|
||||||
|
```
|
||||||
|
|
||||||
|
Never:
|
||||||
|
|
||||||
|
```text
|
||||||
|
P-001
|
||||||
|
PT-0001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Identity And Registry
|
||||||
|
|
||||||
|
- Node-RED is not the source of truth for asset identity.
|
||||||
|
- Future central asset registry owns tag allocation and duplicate detection.
|
||||||
|
- Use one central counter per tag prefix:
|
||||||
|
- `P`
|
||||||
|
- `PT`
|
||||||
|
- `FT`
|
||||||
|
- `TT`
|
||||||
|
- etc.
|
||||||
|
- The central registry, not local Node-RED, performs atomic `+1`.
|
||||||
|
- For now, assume the central registry is future work.
|
||||||
|
- First implementation derives identity when possible and allows overrides.
|
||||||
|
- Keep a boundary like `resolveIdentity(input)` so future registry integration is straightforward.
|
||||||
|
|
||||||
|
First-version identity behavior:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Thing tag: configured/derived, e.g. P-1
|
||||||
|
Sensor tag: configured/derived, e.g. PT-1, MODEL-P-1, CTRL-P-1
|
||||||
|
Stream key: thingTag:type:variant:position:sensorTag
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shared Collector Model
|
||||||
|
|
||||||
|
Use one shared CoreSync per FROST target/stack level.
|
||||||
|
|
||||||
|
Many EVOLV nodes can connect their `dbase` output to the CoreSync input, assuming payloads are structured as:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
measurement: "P-1",
|
||||||
|
fields: {
|
||||||
|
"pressure.measured.upstream.PT-1": 12345
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
tagcode: "P-1"
|
||||||
|
},
|
||||||
|
timestamp: Date
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also accept arrays of such payloads.
|
||||||
|
|
||||||
|
Internal stream key:
|
||||||
|
|
||||||
|
```text
|
||||||
|
thingTag:type:variant:position:sensorTag
|
||||||
|
```
|
||||||
|
|
||||||
|
Per-stream state:
|
||||||
|
|
||||||
|
- FROST id cache
|
||||||
|
- latest FROST `phenomenonTime`
|
||||||
|
- reducer anchor point
|
||||||
|
- reducer previous point
|
||||||
|
- pending latest point
|
||||||
|
- bounded pending queue
|
||||||
|
|
||||||
|
## FROST Request Message Shape
|
||||||
|
|
||||||
|
Outgoing request messages should preserve correlation metadata:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
topic: "frost.metadata.lookup",
|
||||||
|
requestId: "thing:P-1:lookup",
|
||||||
|
_coreSync: {
|
||||||
|
kind: "thing",
|
||||||
|
action: "lookup",
|
||||||
|
externalKey: "thing:P-1",
|
||||||
|
streamKey: "P-1:pressure:measured:upstream:PT-1"
|
||||||
|
},
|
||||||
|
method: "GET",
|
||||||
|
url: "...",
|
||||||
|
payload: null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
FROST response feedback:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
topic: "frost.response",
|
||||||
|
requestId: "...",
|
||||||
|
statusCode: 200,
|
||||||
|
payload: {},
|
||||||
|
_coreSync: {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Observation write target:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /v1.1/Datastreams({datastreamId})/Observations
|
||||||
|
```
|
||||||
|
|
||||||
|
Observation payload:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"phenomenonTime": "2026-05-19T10:15:30.000Z",
|
||||||
|
"result": 123.4,
|
||||||
|
"FeatureOfInterest": {
|
||||||
|
"@iot.id": 7
|
||||||
|
},
|
||||||
|
"parameters": {
|
||||||
|
"reduction": "knot",
|
||||||
|
"reductionReason": "first|angle-change|max-gap|flush",
|
||||||
|
"evolvFieldKey": "pressure.measured.upstream.PT-1",
|
||||||
|
"evolvStreamKey": "P-1:pressure:measured:upstream:PT-1",
|
||||||
|
"sourceMeasurement": "Pump A"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reducer Decisions So Far
|
||||||
|
|
||||||
|
- Reducer runs independently per Datastream.
|
||||||
|
- 2D vector means time on X and numeric field value on Y.
|
||||||
|
- On direction change, emit the previous point.
|
||||||
|
- First point of a stream is kept.
|
||||||
|
- Previous point is kept on angle change.
|
||||||
|
- Pending latest point is emitted on explicit flush, max gap, or close.
|
||||||
|
- No forward-fill.
|
||||||
|
|
||||||
|
Pending queue during unresolved metadata/FROST downtime:
|
||||||
|
|
||||||
|
```text
|
||||||
|
queue empty -> store observation
|
||||||
|
queue has 1 -> keep first, append latest
|
||||||
|
queue has 2 -> keep first, replace second with latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Open Interview Questions
|
||||||
|
|
||||||
|
## Implementation Progress 2026-05-21
|
||||||
|
|
||||||
|
First coding pass added:
|
||||||
|
|
||||||
|
- New Node-RED node: `nodes/coresync/coresync.js` / `coresync.html`.
|
||||||
|
- New `frost` dbase formatter in `generalFunctions`.
|
||||||
|
- Root Node-RED registration: `package.json` -> `coresync`.
|
||||||
|
- Focused tests: `nodes/coresync/test/basic/coresync.basic.test.js`.
|
||||||
|
- FROST request builder for lazy lookup/create and nested Observation writes.
|
||||||
|
- Per-stream normalized-angle reducer, defaulting to:
|
||||||
|
- `angleToleranceDeg = 5`
|
||||||
|
- `timeScaleMs = 60000`
|
||||||
|
- `maxGapMs = 300000`
|
||||||
|
- keep first + latest pending queue
|
||||||
|
- Minimal response state machine:
|
||||||
|
- `GET lookup`
|
||||||
|
- `POST create if missing`
|
||||||
|
- cache returned `@iot.id`
|
||||||
|
- drain pending Observations once Datastream and FOI ids are known
|
||||||
|
|
||||||
|
Validation run:
|
||||||
|
|
||||||
|
```text
|
||||||
|
npx jest nodes/coresync/test/basic/coresync.basic.test.js --runInBand
|
||||||
|
PASS, 4 tests
|
||||||
|
|
||||||
|
npx eslint nodes/coresync/**/*.js nodes/generalFunctions/src/helper/formatters/frostFormatter.js nodes/generalFunctions/src/helper/formatters/index.js
|
||||||
|
PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
Full `npm run lint` still fails on pre-existing unrelated repo issues, mostly browser globals in editor scripts and older lint findings.
|
||||||
|
|
||||||
|
Q20 decision implemented: default to normalized angle comparison. Relative slope mode is present as an advanced option in the reducer and editor config.
|
||||||
|
|
||||||
|
Q21 decision implemented: first defaults are the candidate defaults from this document, with per-type value-scale defaults in the CoreSync domain and fallback scale `1`.
|
||||||
|
|
||||||
|
Q22 decision implemented: explicit `msg.topic = "coresync.flush"`, `maxGapMs`, and close flush are supported. No periodic flush timer was added.
|
||||||
|
|
||||||
|
Q23 decision implemented: lazy resolver order is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Thing
|
||||||
|
ObservedProperty
|
||||||
|
Sensor
|
||||||
|
FeatureOfInterest
|
||||||
|
Datastream
|
||||||
|
Observation
|
||||||
|
```
|
||||||
|
|
||||||
|
Each metadata entity uses lookup/create only. PATCH drift correction is not in this pass.
|
||||||
|
|
||||||
|
Q24 decision implemented in editor defaults:
|
||||||
|
|
||||||
|
```text
|
||||||
|
frostBaseUrl
|
||||||
|
serviceVersion
|
||||||
|
assetTagOverride
|
||||||
|
sensorTagOverride
|
||||||
|
comparisonMode
|
||||||
|
angleToleranceDeg
|
||||||
|
timeScaleMs
|
||||||
|
maxGapMs
|
||||||
|
minDeltaTimeMs
|
||||||
|
minDeltaValue
|
||||||
|
maxQueuedObservationsPerStream
|
||||||
|
diagnosticsEnabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Q25 decision implemented: emitted request messages are plain Node-RED HTTP-compatible messages and preserve `requestId` / `_coreSync` correlation fields.
|
||||||
|
|
||||||
|
Q26 decision implemented: id cache is runtime-only.
|
||||||
|
|
||||||
|
Q27 partial: failed metadata responses emit a process diagnostic and clear in-flight metadata. Backoff timing is not implemented yet.
|
||||||
|
|
||||||
|
Q28 implemented scope: skeleton, normalizer, reducer, FROST request builder, and minimal response state machine.
|
||||||
|
|
||||||
|
### Q20. Reducer comparison method
|
||||||
|
|
||||||
|
Should slope change be compared by:
|
||||||
|
|
||||||
|
- angle in degrees, using normalized axes, or
|
||||||
|
- relative slope delta?
|
||||||
|
|
||||||
|
Recommended: default to normalized angle comparison and keep relative slope as an optional advanced mode.
|
||||||
|
|
||||||
|
### Q21. Reducer defaults
|
||||||
|
|
||||||
|
What should the first defaults be?
|
||||||
|
|
||||||
|
Candidate defaults:
|
||||||
|
|
||||||
|
```text
|
||||||
|
angleToleranceDeg = 5
|
||||||
|
timeScaleMs = 60000
|
||||||
|
valueScaleMode = auto
|
||||||
|
minDeltaTimeMs = 0
|
||||||
|
minDeltaValue = 0
|
||||||
|
maxGapMs = 300000
|
||||||
|
```
|
||||||
|
|
||||||
|
Need decide whether `valueScale` is:
|
||||||
|
|
||||||
|
- configured per observed property/unit,
|
||||||
|
- auto-learned per stream,
|
||||||
|
- fixed to `1`.
|
||||||
|
|
||||||
|
Recommended: configured defaults by type, with auto fallback.
|
||||||
|
|
||||||
|
### Q22. Flush behavior
|
||||||
|
|
||||||
|
When should pending latest points flush?
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
- on node close only,
|
||||||
|
- on explicit `msg.topic = "coresync.flush"`,
|
||||||
|
- on `maxGapMs`,
|
||||||
|
- on periodic flush timer.
|
||||||
|
|
||||||
|
Recommended: support explicit flush and `maxGapMs`; avoid periodic flush unless needed.
|
||||||
|
|
||||||
|
### Q23. Metadata bootstrap order
|
||||||
|
|
||||||
|
What exact lazy resolver chain should the first implementation use?
|
||||||
|
|
||||||
|
Candidate:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Thing
|
||||||
|
ObservedProperty
|
||||||
|
Sensor
|
||||||
|
FeatureOfInterest
|
||||||
|
Datastream
|
||||||
|
Observation
|
||||||
|
```
|
||||||
|
|
||||||
|
Need decide whether each entity is `GET lookup -> POST create if missing`, and whether PATCH metadata drift is included in v1.
|
||||||
|
|
||||||
|
Recommended v1: lookup/create only; no PATCH drift correction yet.
|
||||||
|
|
||||||
|
### Q24. FROST base URL config
|
||||||
|
|
||||||
|
What config fields belong on the CoreSync node?
|
||||||
|
|
||||||
|
Candidate:
|
||||||
|
|
||||||
|
```text
|
||||||
|
frostBaseUrl
|
||||||
|
serviceVersion = v1.1
|
||||||
|
dbaseFormat = frost
|
||||||
|
assetTagOverride
|
||||||
|
sensorTagOverride
|
||||||
|
angleToleranceDeg
|
||||||
|
timeScaleMs
|
||||||
|
maxGapMs
|
||||||
|
maxQueuedObservationsPerStream
|
||||||
|
diagnosticsEnabled
|
||||||
|
```
|
||||||
|
|
||||||
|
Need decide which are required for v1 editor UI.
|
||||||
|
|
||||||
|
### Q25. HTTP node compatibility
|
||||||
|
|
||||||
|
Do we require a wrapper function around Node-RED HTTP request to preserve `_coreSync` and `requestId`, or should CoreSync emit messages exactly in the shape the HTTP node preserves by default?
|
||||||
|
|
||||||
|
Recommended: design emitted messages to survive the standard HTTP node, then add a helper/example flow if needed.
|
||||||
|
|
||||||
|
### Q26. Local id cache persistence
|
||||||
|
|
||||||
|
Should resolved FROST ids be runtime-only, or persisted in Node-RED context?
|
||||||
|
|
||||||
|
Recommended v1: runtime-only cache, because metadata lookup is lazy and deterministic. Add persistent context later if lookups become expensive.
|
||||||
|
|
||||||
|
### Q27. Error handling policy
|
||||||
|
|
||||||
|
For failed FROST responses, should the stream:
|
||||||
|
|
||||||
|
- retry immediately,
|
||||||
|
- back off,
|
||||||
|
- mark unresolved and keep first/latest pending,
|
||||||
|
- drop until manual reset?
|
||||||
|
|
||||||
|
Recommended: exponential-ish backoff per stream plus keep first/latest pending.
|
||||||
|
|
||||||
|
### Q28. First implementation scope
|
||||||
|
|
||||||
|
Should the first coding pass create only the node skeleton plus reducer tests, or include the lazy FROST resolver end-to-end?
|
||||||
|
|
||||||
|
Recommended: implement skeleton, normalizer, reducer, and FROST request builder together; keep HTTP response state machine minimal but functional.
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# EVOLV — End-to-End Example Flows
|
|
||||||
|
|
||||||
> **Working with these examples?** See [`WORKFLOW.md`](WORKFLOW.md) — the canonical guide for editing, switching projects, persistence, and debugging.
|
|
||||||
|
|
||||||
Demo flows that show how multiple EVOLV nodes work together in a realistic wastewater-automation scenario. Each example is self-contained: its folder has a `flow.json` you can import directly into Node-RED plus a `README.md` that walks through the topology, control modes, and dashboard layout.
|
|
||||||
|
|
||||||
These flows complement the per-node example flows under `nodes/<name>/examples/` (which exercise a single node in isolation). Use the per-node flows for smoke tests during development; use the flows here when you want to see how a real plant section behaves end-to-end.
|
|
||||||
|
|
||||||
## Catalogue
|
|
||||||
|
|
||||||
| Folder | What it shows |
|
|
||||||
|---|---|
|
|
||||||
| [`pumpingstation-complete-example/`](pumpingstation-complete-example/) | End-to-end stack: pumpingStation + MGC + 3 pumps + 12 measurement nodes (4 per pump, physics-coupled), operator-driven inflow with scenario buttons (Constant / Sine / Diurnal / Storm), FlowFuse dashboard (realtime + 1h trends), and provisioned Grafana dashboard backed by InfluxDB. |
|
|
||||||
|
|
||||||
## How it loads
|
|
||||||
|
|
||||||
Each subfolder here is a **Node-RED project**. The Docker stack has Node-RED's Projects feature enabled and bootstraps each `examples/<name>/` into `/data/projects/<name>/` on first container start.
|
|
||||||
|
|
||||||
To run:
|
|
||||||
|
|
||||||
1. `docker compose up -d` from the EVOLV root.
|
|
||||||
2. Open Node-RED at `http://localhost:1880`.
|
|
||||||
3. Menu → **Projects** → **Open Project** → pick one.
|
|
||||||
4. Open the FlowFuse dashboard at `http://localhost:1880/dashboard`.
|
|
||||||
|
|
||||||
The default active project is `pumpingstation-complete-example` (override via `DEFAULT_PROJECT` env var on the nodered service). Switching is two clicks; persistence is handled by the `evolv_nodered_data` named volume — `docker compose down && up` doesn't lose the active flow.
|
|
||||||
|
|
||||||
Each example uses a unique dashboard `path` so they can coexist if you load multiple in the same runtime.
|
|
||||||
|
|
||||||
## Adding new examples
|
|
||||||
|
|
||||||
When you create a new end-to-end example:
|
|
||||||
|
|
||||||
1. Make a subfolder under `examples/` named `<scenario>-<focus>`.
|
|
||||||
2. Include at least `flow.json` and `README.md`. A `build_flow.py` (or equivalent generator) is recommended so the JSON stays diff-friendly.
|
|
||||||
3. `docker compose restart nodered` — the entrypoint will bootstrap your new folder as a Node-RED project (synthesizes `package.json`, `git init`, initial commit) under `/data/projects/<name>/`.
|
|
||||||
4. Editor → Projects → Open Project → pick your new one.
|
|
||||||
5. Add a row to the catalogue table above.
|
|
||||||
|
|
||||||
The bootstrap skips folders that already exist in the volume. To force a refresh of an existing project from the repo source (e.g. after editing `build_flow.py`), use `./scripts/sync-example.sh <name>`.
|
|
||||||
|
|
||||||
## Wishlist for future examples
|
|
||||||
|
|
||||||
These are scenarios worth building when there's a session for it:
|
|
||||||
|
|
||||||
- **Pump failure + MGC re-routing** — kill pump 2 mid-run, watch MGC redistribute to pumps 1 and 3.
|
|
||||||
- **Energy-optimal vs equal-flow control** — same demand profile run through `optimalcontrol` and `prioritycontrol` modes side-by-side, energy comparison chart.
|
|
||||||
- **Schedule-driven demand** — diurnal flow pattern (low at night, peak at 7 am), MGC auto-tuning over 24 simulated hours.
|
|
||||||
- **Reactor + clarifier loop** — `reactor` upstream feeding `settler`, return sludge controlled by a small `pumpingStation`.
|
|
||||||
- **Diffuser + DO control** — aeration grid driven by a PID controller from a dissolved-oxygen sensor.
|
|
||||||
- **Digital sensor bundle** — MQTT-style sensor (BME280, ATAS, etc.) feeding a `measurement` node in digital mode + parent equipment node.
|
|
||||||
- **Maintenance window** — entermaintenance / exitmaintenance cycle with operator handover dashboard.
|
|
||||||
- **Calibration walk-through** — measurement node calibrate cycle with stable / unstable input demonstrations.
|
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
# Pumping Station — Complete Example
|
|
||||||
|
|
||||||
End-to-end EVOLV stack: 1 pumpingStation + 1 machineGroupControl + 3 rotatingMachine pumps + 12 measurement nodes (4 per pump), wired through Node-RED to InfluxDB and Grafana.
|
|
||||||
|
|
||||||
This is the canonical "everything works together" demo. After any cross-node refactor, run this and verify the Node-RED dashboard, the InfluxDB writes, and the Grafana dashboard all populate.
|
|
||||||
|
|
||||||
## Quick start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/znetsixe/EVOLV
|
|
||||||
docker compose up -d
|
|
||||||
# Wait for http://localhost:1880/nodes to return 200, then:
|
|
||||||
curl -s -X POST http://localhost:1880/flows \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Node-RED-Deployment-Type: full" \
|
|
||||||
--data-binary @examples/pumpingstation-complete-example/flow.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open:
|
|
||||||
|
|
||||||
- Node-RED dashboard (realtime + 1h trends): <http://localhost:1880/dashboard>
|
|
||||||
- Grafana dashboard (realtime gauges + historic graphs): <http://localhost:3000> (anonymous viewer is on; the dashboard is `EVOLV / Pumping Station (complete)`)
|
|
||||||
- InfluxDB UI: <http://localhost:8086> (user `evolv` / password `evolv-dev-pw`)
|
|
||||||
|
|
||||||
## What the flow contains
|
|
||||||
|
|
||||||
| Layer | Node(s) | Role |
|
|
||||||
|---|---|---|
|
|
||||||
| Process Cell | `pumpingStation` "Pumping Station" | Wet-well basin model. Levelbased control: drives MGC by basin level. Inflow comes from the Drivers tab; outflow is computed from the pumps. |
|
|
||||||
| Unit | `machineGroupControl` "MGC — Pump Group" | Distributes flow across the 3 pumps via `optimalcontrol`. |
|
|
||||||
| Equipment | `rotatingMachine` × 3 — Pump A / B / C | Hidrostal H05K-S03R curve. Auto by default; manual setpoint slider per pump when in `virtualControl`. |
|
|
||||||
| Control Modules | `measurement` × 12 (4 per pump) | Upstream pressure, downstream pressure, flow, power. Each pump's 4 sensors are driven by a per-pump physics function — values are physically coupled to plant state, not random. |
|
|
||||||
| Telemetry | shared `evt:tlm` link channel → http POST → InfluxDB | Every EVOLV node's port-1 payload is converted to v2 line protocol and POSTed to `telemetry` bucket. |
|
|
||||||
|
|
||||||
## Tabs
|
|
||||||
|
|
||||||
The flow is split across 5 tabs, by **concern**:
|
|
||||||
|
|
||||||
| Tab | Lives here | Why |
|
|
||||||
|---|---|---|
|
|
||||||
| 🏭 **Process Plant** | EVOLV nodes (PS, MGC, 3 pumps, 12 sensors) + per-node output formatters + per-pump physics feeders | The deployable plant model. |
|
|
||||||
| 📊 **Dashboard UI** | All `ui-*` widgets, button/setpoint wrappers, dispatch functions | Display + operator inputs. No business logic. |
|
|
||||||
| 🎛️ **Demo Drivers** | Inflow generator (Constant / Sine / Diurnal / Storm) + 1Hz tick | Inflow is operator-driven via slider + scenario buttons. Outflow is implicit (the pumps drain the basin). |
|
|
||||||
| ⚙️ **Setup & Init** | One-shot `once: true` injects (MGC scaling/mode, pumps mode, initial inflow scenario) | Runs at deploy time only. |
|
|
||||||
| 📈 **Telemetry** | link-in `evt:tlm` → line-protocol function → http POST | InfluxDB writer. |
|
|
||||||
|
|
||||||
Cross-tab wiring uses **named link-out / link-in pairs**, never direct cross-tab wires.
|
|
||||||
|
|
||||||
### Channel contract
|
|
||||||
|
|
||||||
| Channel | Direction | What it carries |
|
|
||||||
|---|---|---|
|
|
||||||
| `cmd:inflow-baseline` | UI → Drivers | numeric m³/h baseline |
|
|
||||||
| `cmd:inflow-scenario` | UI → Drivers | `'constant' \| 'sine' \| 'diurnal' \| 'storm'` |
|
|
||||||
| `cmd:q_in` | Drivers → process | computed inflow in m³/s |
|
|
||||||
| `cmd:Qd` | UI → process | manual demand m³/h (manual mode only) |
|
|
||||||
| `cmd:ps-mode` | UI → process | `'levelbased' \| 'manual'` |
|
|
||||||
| `cmd:mode` | Setup → process | per-pump `setMode` broadcast |
|
|
||||||
| `cmd:station-startup / -shutdown / -estop` | UI → process | station-wide command, fanned to all 3 pumps |
|
|
||||||
| `cmd:setpoint-A / -B / -C` | UI → process | per-pump setpoint slider value |
|
|
||||||
| `cmd:pump-A-seq / -B-seq / -C-seq` | UI → process | per-pump start/stop |
|
|
||||||
| `evt:pump-A / -B / -C` | process → UI | formatted per-pump status |
|
|
||||||
| `evt:mgc` | process → UI | MGC totals |
|
|
||||||
| `evt:ps` | process → UI | basin state, level, fill |
|
|
||||||
| `evt:inflow` | Drivers → UI | live inflow value + active scenario |
|
|
||||||
| `evt:tlm` | every EVOLV node → Telemetry | port-1 payload in `{measurement, fields, tags}` shape |
|
|
||||||
| `setup:to-mgc` | Setup → process | one-shot MGC scaling/mode init |
|
|
||||||
|
|
||||||
## Per-pump physics feeder
|
|
||||||
|
|
||||||
Each pump has a `physics_<pump>` function node on the Process Plant tab. It receives:
|
|
||||||
|
|
||||||
1. The pump's own port-0 stream (state, predicted flow, predicted power).
|
|
||||||
2. PS port-0 stream (basin level), fanned out by `ps_to_physics`.
|
|
||||||
|
|
||||||
It computes physically-coupled values for each sensor and emits them to the 4 measurement nodes:
|
|
||||||
|
|
||||||
| Sensor | Computation |
|
|
||||||
|---|---|
|
|
||||||
| Upstream pressure | `ρ g h` where `h = max(0, basinLevel − outflowLevel)`; pump suction sees the basin's hydrostatic head. |
|
|
||||||
| Downstream pressure | Idle → static head only (12 m → 1177 mbar). Running → static + flow²-scaled dynamic head (up to ~2354 mbar at q=200 m³/h). |
|
|
||||||
| Flow | Mirrors rotatingMachine's predicted flow with 1% Gaussian noise. Zero when the pump is idle. |
|
|
||||||
| Power | Mirrors rotatingMachine's predicted power with 0.5% Gaussian noise. Zero when the pump is idle. |
|
|
||||||
|
|
||||||
Gaussian noise uses a 12-uniform-sum approximation (no external libs).
|
|
||||||
|
|
||||||
## Inflow scenarios
|
|
||||||
|
|
||||||
Pick a scenario on the **Realtime** dashboard page (group "Inflow"):
|
|
||||||
|
|
||||||
| Scenario | Behaviour |
|
|
||||||
|---|---|
|
|
||||||
| Constant | `q_h = baseline` (no modulation) |
|
|
||||||
| Sine | `baseline · (1 + 0.5 · sin(2πt/240))` — period 4 min |
|
|
||||||
| Diurnal | `baseline · (1 + 0.6 · sin(2πt/480 − π/2))` — period 8 min, peak offset |
|
|
||||||
| Storm | 4-min cycle: rapid 5× ramp, then linear decay back to baseline |
|
|
||||||
|
|
||||||
Slider sets `baseline` in m³/h (0–250). The generator emits `q_in` to PS every second.
|
|
||||||
|
|
||||||
## Dashboard map
|
|
||||||
|
|
||||||
### Node-RED — `/dashboard`
|
|
||||||
|
|
||||||
Realtime page (`/dashboard/realtime`):
|
|
||||||
|
|
||||||
1. Inflow — slider, 4 scenario buttons, live value + active scenario label
|
|
||||||
2. Station mode + commands — Auto/Manual switch, manual Qd slider, Start All / Stop All / Emergency Stop
|
|
||||||
3. Basin realtime — direction, level, volume, fill %, net flow, time-to-full/empty, inflow, outflow, safety state, gauges (level + fill)
|
|
||||||
4. MGC — total flow + power (text + gauges), efficiency
|
|
||||||
5. Pump A / B / C — state, mode, controller %, flow, power, up/dn pressure (text), setpoint slider, Startup / Shutdown buttons
|
|
||||||
|
|
||||||
Trends page (`/dashboard/trends`) — 1-hour rolling windows:
|
|
||||||
|
|
||||||
- Basin level + fill %
|
|
||||||
- Inflow / Outflow / Per-pump flow (one chart, multi-series)
|
|
||||||
- Per-pump power
|
|
||||||
- Per-pump up/dn pressure
|
|
||||||
|
|
||||||
### Grafana — `EVOLV / Pumping Station (complete)`
|
|
||||||
|
|
||||||
Two rows:
|
|
||||||
|
|
||||||
- **Realtime** — gauges for basin level + fill, stat panels for total flow / total power / per-pump state.
|
|
||||||
- **Historic** — line charts for level + fill, inflow/outflow/net, per-pump flow + power (predicted), per-pump pressure, per-pump sensor flow + power (measured).
|
|
||||||
|
|
||||||
Default time range: last 15 minutes. Adjust with the Grafana picker for longer history.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Bring up the stack
|
|
||||||
docker compose up -d
|
|
||||||
sleep 10 # wait for Node-RED ready
|
|
||||||
|
|
||||||
# 2. Deploy the flow
|
|
||||||
curl -s -X POST http://localhost:1880/flows \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-H 'Node-RED-Deployment-Type: full' \
|
|
||||||
--data-binary @examples/pumpingstation-complete-example/flow.json | jq .
|
|
||||||
|
|
||||||
# 3. Quick sanity check on Influx writes
|
|
||||||
curl -s -X POST 'http://localhost:8086/api/v2/query?org=evolv' \
|
|
||||||
-H 'Authorization: Token evolv-dev-token' \
|
|
||||||
-H 'Accept: application/csv' \
|
|
||||||
-H 'Content-type: application/vnd.flux' \
|
|
||||||
--data 'from(bucket:"telemetry") |> range(start: -1m) |> count() |> group(columns: ["_measurement"])'
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see counts per measurement (`Pumping Station`, `Pump A`, `MGC — Pump Group`, the per-pump sensors, …) growing in real time.
|
|
||||||
|
|
||||||
## Regenerating `flow.json`
|
|
||||||
|
|
||||||
`flow.json` is generated from `build_flow.py`. Edit the Python (cleaner diff) and regenerate:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd examples/pumpingstation-complete-example
|
|
||||||
python3 build_flow.py > flow.json
|
|
||||||
```
|
|
||||||
|
|
||||||
The Python is the source of truth.
|
|
||||||
|
|
||||||
After regenerating, push the new flow into the running runtime:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/sync-example.sh pumpingstation-complete-example
|
|
||||||
```
|
|
||||||
|
|
||||||
## Projects + persistence (Node-RED)
|
|
||||||
|
|
||||||
The Docker stack uses a named volume (`evolv_nodered_data`) for `/data`, and Node-RED's **Projects** feature is enabled. Each folder under `examples/` is bootstrapped into `/data/projects/<name>/` on first container start with its own `git init` and a synthesized `package.json`. Switching between projects is two clicks in the editor: **menu → Projects → Open Project**.
|
|
||||||
|
|
||||||
| What you do | Where it lives | What persists |
|
|
||||||
|---|---|---|
|
|
||||||
| `docker compose down && up` | Container is recreated; named volume survives | Active flow + project list survive |
|
|
||||||
| Edit a flow in the Node-RED editor | `/data/projects/<name>/flow.json` (in volume) | Until `docker compose down -v` |
|
|
||||||
| Edit `examples/<name>/build_flow.py` then regenerate | `examples/<name>/flow.json` (in repo) | Always — it's in Git |
|
|
||||||
| Run `scripts/sync-example.sh <name>` | Copies repo's `flow.json` → volume's project + reloads | Volume copy now matches repo |
|
|
||||||
|
|
||||||
### Adding a new example as a project
|
|
||||||
|
|
||||||
1. Create `examples/<your-name>/flow.json` (build it however you like — `build_flow.py` is one way).
|
|
||||||
2. Restart the Node-RED container: `docker compose restart nodered`.
|
|
||||||
3. Editor → Projects → Open Project → pick `<your-name>`.
|
|
||||||
|
|
||||||
The bootstrap is idempotent: existing projects in the volume aren't overwritten. To force a refresh from the repo: delete the project in the volume (`docker exec evolv-nodered rm -rf /data/projects/<name>`) and restart, or use `scripts/sync-example.sh` for a flow-only refresh.
|
|
||||||
|
|
||||||
To start fresh (wipe all volume state including flows, sessions, project history): `docker compose down -v`.
|
|
||||||
|
|
||||||
## Notable design choices
|
|
||||||
|
|
||||||
- **PS in `levelbased` mode** with `manual` mode toggleable from the UI. Levelbased = PS commands MGC by basin level; manual = operator drives MGC via the Qd slider.
|
|
||||||
- **Inflow is operator-driven**, outflow is implicit (computed from pump activity). Single steerable knob (the Inflow group) keeps the demo focused.
|
|
||||||
- **Sensors driven externally**, not by the measurement node's built-in simulator. The physics feeder is a function node on the Process Plant tab — disable it and sensors freeze, which is a useful failure mode to demonstrate.
|
|
||||||
- **All EVOLV port 1 → one shared telemetry channel** (`evt:tlm`) → one writer. Adding a new EVOLV node anywhere in the flow only needs a new `lout_tlm_<id>` link-out + appending the id to `_all_tlm_lout_ids()` in `build_flow.py`.
|
|
||||||
- **Dashboard pages split by concern, not data**: realtime widgets never share a page with historical charts.
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ module.exports = {
|
|||||||
'<rootDir>/nodes/generalFunctions/test/**/*.test.js',
|
'<rootDir>/nodes/generalFunctions/test/**/*.test.js',
|
||||||
'<rootDir>/nodes/dashboardAPI/test/**/*.test.js',
|
'<rootDir>/nodes/dashboardAPI/test/**/*.test.js',
|
||||||
'<rootDir>/nodes/diffuser/test/specificClass.test.js',
|
'<rootDir>/nodes/diffuser/test/specificClass.test.js',
|
||||||
|
'<rootDir>/nodes/coresync/test/**/*.test.js',
|
||||||
'<rootDir>/nodes/monster/test/**/*.test.js',
|
'<rootDir>/nodes/monster/test/**/*.test.js',
|
||||||
'<rootDir>/nodes/pumpingStation/test/**/*.test.js',
|
'<rootDir>/nodes/pumpingStation/test/**/*.test.js',
|
||||||
'<rootDir>/nodes/reactor/test/**/*.test.js',
|
'<rootDir>/nodes/reactor/test/**/*.test.js',
|
||||||
|
|||||||
1
nodes/coresync
Submodule
1
nodes/coresync
Submodule
Submodule nodes/coresync added at aefec90485
Submodule nodes/dashboardAPI updated: e04c4a1132...dac8576cab
Submodule nodes/diffuser updated: bf645cfe68...f5fd8039f5
Submodule nodes/generalFunctions updated: ae30cef89c...6c4db03aba
Submodule nodes/machineGroupControl updated: aeb938c205...a47aa53d17
Submodule nodes/measurement updated: b0e8bbb95d...5d79314229
Submodule nodes/monster updated: 4eb286771e...6c88b6464d
Submodule nodes/pumpingStation updated: 2e4ad8d3f1...df18e97b8b
Submodule nodes/reactor updated: 75d0413994...46fc8dddf7
Submodule nodes/rotatingMachine updated: 8c5822c853...a18aec32b9
Submodule nodes/settler updated: 70acef22d5...0ba28b9cdf
Submodule nodes/valve updated: 167b1026f1...74951e7a23
Submodule nodes/valveGroupControl updated: 91f98414d1...bd67b22197
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "EVOLV",
|
"name": "EVOLV",
|
||||||
"version": "1.0.29",
|
"version": "1.0.32",
|
||||||
"description": "Modular Node-RED package containing all control and automation nodes developed under the EVOLV project.",
|
"description": "Modular Node-RED package containing all control and automation nodes developed under the EVOLV project.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"node-red",
|
"node-red",
|
||||||
@@ -11,8 +11,9 @@
|
|||||||
],
|
],
|
||||||
"node-red": {
|
"node-red": {
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"dashboardapi": "nodes/dashboardAPI/dashboardapi.js",
|
"dashboardapi": "nodes/dashboardAPI/dashboardAPI.js",
|
||||||
"diffuser": "nodes/diffuser/diffuser.js",
|
"diffuser": "nodes/diffuser/diffuser.js",
|
||||||
|
"coresync": "nodes/coresync/coresync.js",
|
||||||
"machineGroupControl": "nodes/machineGroupControl/mgc.js",
|
"machineGroupControl": "nodes/machineGroupControl/mgc.js",
|
||||||
"measurement": "nodes/measurement/measurement.js",
|
"measurement": "nodes/measurement/measurement.js",
|
||||||
"monster": "nodes/monster/monster.js",
|
"monster": "nodes/monster/monster.js",
|
||||||
@@ -25,8 +26,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "node scripts/patch-deps.js",
|
|
||||||
"postinstall": "git checkout -- package.json 2>/dev/null || true",
|
|
||||||
"docker:build": "docker compose build",
|
"docker:build": "docker compose build",
|
||||||
"docker:up": "docker compose up -d",
|
"docker:up": "docker compose up -d",
|
||||||
"docker:down": "docker compose down",
|
"docker:down": "docker compose down",
|
||||||
|
|||||||
Reference in New Issue
Block a user