docs: bump valve submodule pointer — wiki Home.md FSM + config rewrite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,203 +0,0 @@
|
||||
---
|
||||
title: measurement — User Manual
|
||||
node: measurement
|
||||
updated: 2026-04-13
|
||||
status: trial-ready
|
||||
---
|
||||
|
||||
# measurement — User Manual
|
||||
|
||||
The `measurement` node is the sensor-side of every EVOLV flow. It takes raw signal data, applies offset / scaling / smoothing / outlier rejection, and publishes a conditioned value into the shared `MeasurementContainer`. A parent equipment node (rotatingMachine, pumpingStation, reactor, ...) subscribes automatically via the child-registration handshake on port 2.
|
||||
|
||||
## At a glance
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Node category | EVOLV |
|
||||
| Inputs | 1 (message-driven) |
|
||||
| Outputs | 3 — `process` / `dbase` / `parent` |
|
||||
| Tick period | 1 s |
|
||||
| Input modes | `analog` (default) — one scalar per msg. `digital` — object payload with many keys. |
|
||||
| Smoothing methods | 12 (`none`, `mean`, `min`, `max`, `sd`, `lowPass`, `highPass`, `weightedMovingAverage`, `bandPass`, `median`, `kalman`, `savitzkyGolay`) |
|
||||
| Outlier methods | 3 (`zScore`, `iqr`, `modifiedZScore`) |
|
||||
|
||||
## Choosing a mode
|
||||
|
||||
### Analog — one scalar per message (PLC / 4-20 mA)
|
||||
|
||||
The classic pattern — what the node did before v1.1. `msg.payload` is a single number. The node runs one offset → scaling → smoothing → outlier pipeline and emits exactly one MeasurementContainer slot keyed by the asset's type + position.
|
||||
|
||||
```json
|
||||
{ "topic": "measurement", "payload": 12.34 }
|
||||
```
|
||||
|
||||
Use when one Node-RED `measurement` node represents one physical sensor.
|
||||
|
||||
### Digital — object payload, many channels (MQTT / IoT / JSON)
|
||||
|
||||
Use when one Node-RED `measurement` node represents one physical **device** that publishes multiple readings. Common shapes:
|
||||
|
||||
```json
|
||||
{ "topic": "measurement",
|
||||
"payload": { "temperature": 22.5, "humidity": 45, "pressure": 1013 } }
|
||||
```
|
||||
|
||||
```json
|
||||
{ "topic": "measurement",
|
||||
"payload": { "co2": 618, "voc": 122, "pm25": 8 } }
|
||||
```
|
||||
|
||||
Each top-level key maps to a **channel** with its own `type`, `position`, `unit`, and pipeline parameters. Unknown keys are ignored (logged at debug).
|
||||
|
||||
## Configuration
|
||||
|
||||
### Common (both modes)
|
||||
|
||||
- **Asset** (menu): supplier, category, asset type (`assetType`), model, unit.
|
||||
- **Logger** (menu): log level + enable flag.
|
||||
- **Position** (menu): `upstream` / `atEquipment` / `downstream`, optional distance offset.
|
||||
|
||||
### Analog fields
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| **Scaling** | enables linear interpolation from source range to process range |
|
||||
| **Source Min / Max** | raw input bounds (e.g. `4` / `20` for mA) |
|
||||
| **Input Offset** | additive bias applied before scaling |
|
||||
| **Process Min / Max** | mapped output bounds (e.g. `0` / `3000` for mbar) |
|
||||
| **Simulator** | internal random-walk source for testing |
|
||||
| **Smoothing** | method (dropdown) |
|
||||
| **Window** | smoothing window size |
|
||||
|
||||
### Digital fields
|
||||
|
||||
- **Input Mode**: set to `digital` in the dropdown.
|
||||
- **Channels (JSON)**: array of channel definitions.
|
||||
|
||||
Each channel:
|
||||
|
||||
```json
|
||||
{
|
||||
"key": "temperature",
|
||||
"type": "temperature",
|
||||
"position": "atEquipment",
|
||||
"unit": "C",
|
||||
"scaling": { "enabled": false, "inputMin": 0, "inputMax": 1, "absMin": -50, "absMax": 150, "offset": 0 },
|
||||
"smoothing": { "smoothWindow": 5, "smoothMethod": "mean" },
|
||||
"outlierDetection": { "enabled": true, "method": "zScore", "threshold": 3 }
|
||||
}
|
||||
```
|
||||
|
||||
`scaling` / `smoothing` / `outlierDetection` are optional — missing sections inherit the top-level analog-mode fields. `key` is the JSON field name inside `msg.payload`; `type` is the MeasurementContainer axis — any string works, not just the physical-unit-backed defaults.
|
||||
|
||||
## Input topics
|
||||
|
||||
| Topic | Payload | Effect |
|
||||
|---|---|---|
|
||||
| `measurement` | number (analog) / object (digital) | drives the pipeline |
|
||||
| `simulator` | — | toggle the internal random-walk simulator |
|
||||
| `outlierDetection` | — | toggle outlier rejection |
|
||||
| `calibrate` | — | set the offset so the current output matches `Source Min` (scaling on) or `Process Min` (scaling off). Requires a stable window — aborts if the signal is fluctuating. |
|
||||
|
||||
## Output ports
|
||||
|
||||
### Port 0 — process
|
||||
|
||||
Delta-compressed payload.
|
||||
|
||||
**Analog** shape:
|
||||
|
||||
```json
|
||||
{ "mAbs": 4.2, "mPercent": 42, "totalMinValue": 0, "totalMaxValue": 100,
|
||||
"totalMinSmooth": 0, "totalMaxSmooth": 4.2 }
|
||||
```
|
||||
|
||||
**Digital** shape:
|
||||
|
||||
```json
|
||||
{ "channels": {
|
||||
"temperature": { "key": "temperature", "type": "temperature", "position": "atEquipment",
|
||||
"unit": "C", "mAbs": 24, "mPercent": 37,
|
||||
"totalMinValue": 22.5, "totalMaxValue": 25.5,
|
||||
"totalMinSmooth": 22.5, "totalMaxSmooth": 24 },
|
||||
"humidity": { ... },
|
||||
"pressure": { ... }
|
||||
} }
|
||||
```
|
||||
|
||||
### Port 1 — dbase
|
||||
|
||||
InfluxDB line-protocol telemetry. Tags = asset metadata; fields = measurements. See [InfluxDB Schema Design](../../concepts/influxdb-schema-design.md).
|
||||
|
||||
### Port 2 — parent
|
||||
|
||||
`{ topic: "registerChild", payload: <nodeId>, positionVsParent, distance }` — emitted once ~200 ms after deploy so the parent equipment node registers this sensor.
|
||||
|
||||
## Pipeline per value
|
||||
|
||||
1. **Outlier check** (if enabled) — rejects via zScore / IQR / modifiedZScore. Rejected values never advance, don't update min/max, don't emit.
|
||||
2. **Offset** — `value + scaling.offset`.
|
||||
3. **Scaling** (if enabled) — linear interpolation from `[inputMin, inputMax]` to `[absMin, absMax]` with boundary clamping.
|
||||
4. **Smoothing** — current value pushed into the rolling window; the configured method produces the smoothed output.
|
||||
5. **Min/Max tracking** — both raw (pre-smoothing) and smoothed min/max tracked for display.
|
||||
6. **Constrain** — smoothed value clamped to `[absMin, absMax]`.
|
||||
7. **Emit** — `MeasurementContainer.type(...).variant('measured').position(...).distance(...).value(out, ts, unit)` triggers the event `<type>.measured.<position>` (lowercase) that the parent equipment subscribes to.
|
||||
|
||||
In digital mode, each channel runs this pipeline independently.
|
||||
|
||||
## Smoothing methods — quick reference
|
||||
|
||||
| Method | Use case |
|
||||
|---|---|
|
||||
| `none` | pass raw value through — useful for testing |
|
||||
| `mean` | simple arithmetic average over window |
|
||||
| `min` / `max` | worst-case / peak reporting |
|
||||
| `sd` | outputs standard deviation (noise indicator) |
|
||||
| `median` | outlier-resistant central tendency |
|
||||
| `weightedMovingAverage` | later samples weighted higher |
|
||||
| `lowPass` | EMA-style attenuation of high-frequency noise |
|
||||
| `highPass` | emphasises rapid changes (step detection) |
|
||||
| `bandPass` | `lowPass + highPass - raw` — band-of-interest filtering |
|
||||
| `kalman` | recursive noise filter, converges to steady value |
|
||||
| `savitzkyGolay` | polynomial smoothing over 5-point window |
|
||||
|
||||
## Outlier methods — quick reference
|
||||
|
||||
| Method | Best when |
|
||||
|---|---|
|
||||
| `zScore` | signal is approximately normal; threshold = # of SDs |
|
||||
| `iqr` | signal is non-normal; robust to skewed distributions |
|
||||
| `modifiedZScore` | small samples; uses median / MAD instead of mean / SD |
|
||||
|
||||
> **Historical bug fixed 2026-04-13:** The dispatcher compared against camelCase keys (`lowPass`, `zScore`, ...) but the validator lowercases enum values. Result: 4 smoothing methods and 2 outlier methods were silently no-ops when chosen from the editor — they fell through to the "unknown" branch and emitted the raw last value. Review any flow deployed before 2026-04-13 that relied on these methods.
|
||||
|
||||
## Unit policy
|
||||
|
||||
Unknown measurement types (anything not in the container's built-in `measureMap`: `pressure`, `flow`, `power`, `temperature`, `volume`, `length`, `mass`, `energy`) are accepted without unit compatibility checks. This lets digital channels use `humidity` (`%`), `co2` (`ppm`), arbitrary IoT units. Known types still validate strictly.
|
||||
|
||||
## Example flow (digital)
|
||||
|
||||
```json
|
||||
[
|
||||
{ "id": "dig", "type": "measurement",
|
||||
"mode": "digital",
|
||||
"channels": "[{\"key\":\"temperature\",\"type\":\"temperature\",\"position\":\"atEquipment\",\"unit\":\"C\",\"scaling\":{\"enabled\":false,\"absMin\":-50,\"absMax\":150},\"smoothing\":{\"smoothWindow\":5,\"smoothMethod\":\"mean\"}},{\"key\":\"humidity\",\"type\":\"humidity\",\"position\":\"atEquipment\",\"unit\":\"%\",\"scaling\":{\"enabled\":false,\"absMin\":0,\"absMax\":100},\"smoothing\":{\"smoothWindow\":5,\"smoothMethod\":\"mean\"}}]",
|
||||
...
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cd nodes/measurement
|
||||
npm test
|
||||
```
|
||||
|
||||
71 tests — coverage includes every smoothing method, every outlier strategy, scaling, interpolation, constrain, calibration, stability, simulation, per-channel pipelines, digital-mode dispatch, malformed-channel handling, event emits.
|
||||
|
||||
End-to-end benchmark scripts live in the superproject at `/tmp/m_e2e_baseline.py` (analog) and `/tmp/m_digital_e2e.py` (digital). Run against a Dockerized Node-RED stack (`docker compose up -d nodered`).
|
||||
|
||||
## Production status
|
||||
|
||||
Trial-ready as of 2026-04-13 after the session that fixed the silent dispatcher bug and added digital mode. See [session 2026-04-13](../../sessions/2026-04-13-measurement-digital-mode.md) and the memory file `node_measurement.md`.
|
||||
@@ -1,247 +0,0 @@
|
||||
---
|
||||
title: rotatingMachine — User Manual
|
||||
node: rotatingMachine
|
||||
updated: 2026-04-13
|
||||
status: trial-ready
|
||||
---
|
||||
|
||||
# rotatingMachine — User Manual
|
||||
|
||||
The `rotatingMachine` node models a single pump, compressor, or blower. It runs an S88-style state machine, predicts flow and power from a supplier curve, and publishes process and telemetry data every second. It is the atomic control module beneath `machineGroupControl` and `pumpingStation`.
|
||||
|
||||
This manual is the operator-facing reference. For architecture and the 3-tier code layout see [Node Architecture](../../architecture/node-architecture.md); for curve theory see [3D Pump Curves](../../architecture/3d-pump-curves.md).
|
||||
|
||||
## At a glance
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Node category | EVOLV |
|
||||
| Inputs | 1 (message-driven) |
|
||||
| Outputs | 3 — `process` / `dbase` / `parent` |
|
||||
| Tick period | 1 s |
|
||||
| State machine | 10 states (S88) |
|
||||
| Predictions | curve-backed (nq flow, np power, reversed nq for ctrl) |
|
||||
| Canonical units | Pa, m³/s, W, K |
|
||||
|
||||
## Editor configuration
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| **Reaction Speed** | `1` | Ramp rate in controller-position units per second. `1` = 1 %/s. |
|
||||
| **Startup Time** | `0` | Seconds in the `starting` state. |
|
||||
| **Warmup Time** | `0` | Seconds in the protected `warmingup` state. |
|
||||
| **Shutdown Time** | `0` | Seconds in the `stopping` state. |
|
||||
| **Cooldown Time** | `0` | Seconds in the protected `coolingdown` state. |
|
||||
| **Movement Mode** | `staticspeed` | `staticspeed` = linear ramp; `dynspeed` = ease-in/out. |
|
||||
| **Process Output** | `process` | Port 0 payload format: `process` (delta-compressed) / `json` / `csv`. |
|
||||
| **Database Output** | `influxdb` | Port 1 payload format: `influxdb` line protocol / `json` / `csv`. |
|
||||
| **Asset** (menu) | — | Supplier, category, model (must match a curve file in `generalFunctions/datasets`), output flow unit, curve units. |
|
||||
| **Logger** (menu) | `info`, enabled | Log level and toggle. |
|
||||
| **Position** (menu) | `atEquipment` | `upstream` / `atEquipment` / `downstream` relative to parent. Icon and optional distance offset. |
|
||||
|
||||
> **Tip.** With `Reaction Speed = 1` and `Set 60%` from idle, the controller takes ~60 s to reach 60 %. Scale `Reaction Speed` up to emulate a faster actuator (e.g. `20` gives 1 second per 20 % = 3 s to reach 60 %).
|
||||
|
||||
## Input topics
|
||||
|
||||
Every command enters on the single input port. `msg.topic` selects the handler; `msg.payload` carries the arguments.
|
||||
|
||||
### `setMode`
|
||||
|
||||
```json
|
||||
{ "topic": "setMode", "payload": "virtualControl" }
|
||||
```
|
||||
|
||||
Valid values: `auto`, `virtualControl`, `fysicalControl`. The current mode gates *which source* may issue *which action* (mode/action/source policy lives in `generalFunctions/src/configs/rotatingMachine.json`).
|
||||
|
||||
### `execSequence`
|
||||
|
||||
```json
|
||||
{ "topic": "execSequence",
|
||||
"payload": { "source": "GUI", "action": "execSequence", "parameter": "startup" } }
|
||||
```
|
||||
|
||||
`parameter` values: `startup`, `shutdown`, `entermaintenance`, `exitmaintenance`. Case is normalized.
|
||||
|
||||
If a `shutdown` is issued while the machine is mid-ramp (`accelerating` / `decelerating`), the active movement is aborted and the shutdown proceeds as soon as the FSM has returned to `operational`.
|
||||
|
||||
### `execMovement`
|
||||
|
||||
```json
|
||||
{ "topic": "execMovement",
|
||||
"payload": { "source": "GUI", "action": "execMovement", "setpoint": 60 } }
|
||||
```
|
||||
|
||||
`setpoint` is expressed in controller units (0–100 %).
|
||||
|
||||
### `flowMovement`
|
||||
|
||||
```json
|
||||
{ "topic": "flowMovement",
|
||||
"payload": { "source": "parent", "action": "flowMovement", "setpoint": 150 } }
|
||||
```
|
||||
|
||||
`setpoint` is expressed in the configured **output flow unit** (e.g. m³/h). The node converts flow → controller-% via the reversed nq curve and then drives `execMovement`.
|
||||
|
||||
### `emergencystop`
|
||||
|
||||
```json
|
||||
{ "topic": "emergencystop",
|
||||
"payload": { "source": "GUI", "action": "emergencystop" } }
|
||||
```
|
||||
|
||||
Aborts any active movement, runs the `emergencystop` → `off` transition. Allowed from every active state. Case-insensitive.
|
||||
|
||||
### `simulateMeasurement`
|
||||
|
||||
Inject a dashboard-side measurement without wiring a sensor child. Useful for validation, smoke tests, demo flows.
|
||||
|
||||
```json
|
||||
{ "topic": "simulateMeasurement",
|
||||
"payload": { "type": "pressure", "position": "upstream", "value": 200, "unit": "mbar" } }
|
||||
```
|
||||
|
||||
`type`: `pressure` / `flow` / `temperature` / `power`. `unit` is required and must be convertible to the canonical unit for the type.
|
||||
|
||||
### Diagnostics
|
||||
|
||||
- `showWorkingCurves` — snapshot of current curve slices + computed metrics; reply on port 0.
|
||||
- `CoG` — current centre-of-gravity (peak efficiency point) indicators; reply on port 0.
|
||||
|
||||
### `registerChild`
|
||||
|
||||
Internal. Sensor children (typically `measurement` nodes) send this to bind themselves to the machine. The machine also emits one on port 2 shortly after deploy so a parent group/station can register it.
|
||||
|
||||
## Output ports
|
||||
|
||||
### Port 0 — process data
|
||||
|
||||
Delta-compressed payload. Only *changed* fields are emitted each tick. Keys use a **4-segment** format:
|
||||
|
||||
```
|
||||
<type>.<variant>.<position>.<childId>
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
| Key | Meaning |
|
||||
|---|---|
|
||||
| `flow.predicted.downstream.default` | predicted flow at discharge |
|
||||
| `flow.predicted.atequipment.default` | predicted flow at equipment |
|
||||
| `power.predicted.atequipment.default` | predicted electrical power draw |
|
||||
| `pressure.measured.downstream.dashboard-sim-downstream` | simulated discharge pressure |
|
||||
| `pressure.measured.upstream.<childId>` | real upstream sensor reading |
|
||||
| `state` | current FSM state |
|
||||
| `mode` | current mode |
|
||||
| `ctrl` | current controller position (0–100 %) |
|
||||
| `NCog` / `cog` | normalized / absolute centre-of-gravity |
|
||||
| `runtime` | cumulative operational hours |
|
||||
|
||||
Consumers must cache and merge deltas. The example flow `01 - Basic Manual Control.json` includes a function node that does exactly this — reuse its logic in your own flows.
|
||||
|
||||
### Port 1 — dbase (InfluxDB)
|
||||
|
||||
InfluxDB line-protocol payload formatted for the `telemetry` bucket. Tags are low-cardinality fields (node name, machine type); measurements are numeric values. See the [InfluxDB Schema Design](../../concepts/influxdb-schema-design.md) page for the full tag/field contract.
|
||||
|
||||
### Port 2 — parent
|
||||
|
||||
`{ topic: "registerChild", payload: <this-node-id>, positionVsParent }` — emitted once ~180 ms after deploy so a downstream parent group can discover this machine. Subsequent commands and data flow through the parent's input port.
|
||||
|
||||
## State machine
|
||||
|
||||
```
|
||||
┌────────────────────────────┐
|
||||
│ operational │◄────┐
|
||||
└────┬──────────┬────────┬────┘ │
|
||||
│ │ │ │
|
||||
execMovement │ │ │ │
|
||||
execMovement │ │ │ │
|
||||
▼ ▼ ▼ ▼ │
|
||||
accelerating decelerating │ emergencystop ─► off
|
||||
│ │ │
|
||||
└─── (abort)─┘ │
|
||||
│ │
|
||||
┌────▼──────────▼────┐
|
||||
│ stopping │
|
||||
└────────┬─────────────┘
|
||||
│
|
||||
coolingdown
|
||||
│
|
||||
idle
|
||||
│
|
||||
starting
|
||||
│
|
||||
warmingup
|
||||
│
|
||||
(operational)
|
||||
```
|
||||
|
||||
Protected states (cannot be aborted by a new command): `warmingup`, `coolingdown`.
|
||||
|
||||
Interruptible states: `accelerating`, `decelerating`. A `shutdown` or `emergencystop` issued during a ramp aborts the ramp and drives the FSM correctly to `idle` / `off`.
|
||||
|
||||
Active states (contribute to `runtime`): `operational`, `starting`, `warmingup`, `accelerating`, `decelerating`.
|
||||
|
||||
## Predictions and pressure
|
||||
|
||||
Flow and power are curve-backed. The curve set is indexed by the differential pressure across the machine:
|
||||
|
||||
1. Best: both upstream and downstream pressures present → real Δp.
|
||||
2. Degraded: only one side present → falls back to that side with a warn.
|
||||
3. Minimum: no pressure → `fDimension = 0`; flow and power predictions use the lowest curve slice and will look unrealistic.
|
||||
|
||||
Pressure sources are resolved in priority order **real sensor child > virtual dashboard child > aggregated fallback**. Real-child values always win.
|
||||
|
||||
Predictions are only emitted while the FSM is in an active state (`operational`, `starting`, `warmingup`, `accelerating`, `decelerating`). In `idle`, `stopping`, `coolingdown`, `off`, `maintenance` the outputs are clamped to zero.
|
||||
|
||||
### Supported curves and verification
|
||||
|
||||
| Model | Pressure envelope | Flow envelope | Power envelope |
|
||||
|---|---|---|---|
|
||||
| `hidrostal-H05K-S03R` | 700 – 3900 mbar (33 slices) | 9.5 – 227 m³/h | 8.2 – 65.1 kW |
|
||||
| `hidrostal-C5-D03R-SHN1` | 400 – 2900 mbar (26 slices) | 6.4 – 52.5 m³/h | 0.55 – 31.5 kW |
|
||||
|
||||
Both curves are covered by unit tests (`test/integration/curve-prediction.integration.test.js`) and a live E2E benchmark (`test/e2e/curve-prediction-benchmark.py`) that sweeps each pump through its own pressure × controller envelope. Last green run: **2026-04-13** — 12/12 samples per curve inside envelope, ctrl-monotonic, inverse-pressure monotonic.
|
||||
|
||||
> **Pressure out of envelope is not clamped.** If a measured pressure falls *below* the curve's minimum slice, the node extrapolates and may produce implausibly large flow values (e.g. H05K at 400 mbar, ctrl 20 % → flow ≈ 30 000 m³/h; real envelope max is 227). Use realistic sensor ranges on your pressure `measurement` children.
|
||||
|
||||
## Example flows
|
||||
|
||||
In the editor: **Import ▸ Examples ▸ EVOLV ▸ rotatingMachine**.
|
||||
|
||||
- `01 - Basic Manual Control.json` — single machine, inject-only. Good for smoke-testing a node installation.
|
||||
- `02 - Integration with Machine Group.json` — `machineGroupControl` with two pumps as children. Good for verifying registration and parent orchestration.
|
||||
- `03 - Dashboard Visualization.json` — FlowFuse dashboard with live charts. Depends on `@flowfuse/node-red-dashboard`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| Editor says `pressure not initialized`, status ring is yellow | No pressure child wired yet and no simulated pressure injected. | Inject a `simulateMeasurement` of type `pressure` (both sides preferred) or wire a `measurement` child. |
|
||||
| Predictions are enormous at `ctrl = 0 %` | At near-zero controller position with high backpressure, the intercept of the curve gives a nominally-nonzero flow. This is a curve-data artefact, not a runtime bug. | Confirm the curve with Rene / supplier data. For a conservative prediction use a lower `Reaction Speed` or constrain `setpoint` ≥ 10 %. |
|
||||
| "Transition aborted" / "Movement aborted" in logs | Expected during `shutdown` / `emergencystop` issued during a ramp — the fix path intentionally aborts the active move. | None — informational only. |
|
||||
| Status bar shows `pressure not initialized` even after inject | `simulateMeasurement` payload missing `unit` or with a non-convertible value. | Include `unit` (e.g. `"mbar"`) and a finite number in `value`. |
|
||||
| Shutdown does nothing and no error | Machine is in `warmingup` or `coolingdown` (protected). | Wait for the phase to complete (≤ configured seconds) and retry. |
|
||||
|
||||
## Running it locally
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://gitea.wbd-rd.nl/RnD/EVOLV.git
|
||||
cd EVOLV
|
||||
docker compose up -d
|
||||
# Node-RED: http://localhost:1880 InfluxDB: :8086 Grafana: :3000
|
||||
```
|
||||
|
||||
Then in Node-RED: **Import ▸ Examples ▸ EVOLV ▸ rotatingMachine ▸ 01 - Basic Manual Control**.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cd nodes/rotatingMachine
|
||||
npm test
|
||||
```
|
||||
|
||||
Unit tests (79) cover construction, mode gating, sequences, interruptible movement, emergency stop, shutdown, efficiency/CoG, pressure initialization, output formatting, listener cleanup. See also `examples/README.md` for the flow-level test matrix.
|
||||
|
||||
## Production status
|
||||
|
||||
See the project memory entry `node_rotatingMachine.md` for the latest benchmarks and wishlist. Trial-ready as of 2026-04-13 following the interruptibility + schema-sync fixes documented in [session 2026-04-13](../../sessions/2026-04-13-rotatingMachine-trial-ready.md).
|
||||
Reference in New Issue
Block a user