diff --git a/.agents/decisions/DECISION-20260216-agent-harness-defaults.md b/.agents/decisions/DECISION-20260216-agent-harness-defaults.md new file mode 100644 index 0000000..f4e8914 --- /dev/null +++ b/.agents/decisions/DECISION-20260216-agent-harness-defaults.md @@ -0,0 +1,38 @@ +# DECISION-20260216-agent-harness-defaults + +## Context +- Task/request: Adapt EVOLV agents/skills using Harness Engineering patterns and set owner-controlled operating defaults. +- Impacted files/contracts: `AGENTS.md`, `.agents/skills/*/SKILL.md`, `.agents/skills/*/agents/openai.yaml`, decision-log policy. +- Why a decision is required now: New harness workflow needs explicit defaults for compatibility, safety bias, and governance discipline. + +## Options +1. Compatibility posture +- Option A: strict backward compatibility +- Option B: controlled compatibility breaks with migration notes + +2. Safety posture +- Option A: protection-first +- Option B: availability-first + +3. Decision logging scope +- Option A: required only for breaking/risky changes +- Option B: required for all decision-gate outcomes + +## Decision +- Selected option: Compatibility `controlled`; Safety `availability-first`; Decision logging `required for all decision-gate changes`. +- Decision owner: User +- Date: February 16, 2026 +- Rationale: Maintain delivery and operational continuity while preserving governance through mandatory, durable decision records. + +## Consequences +- Compatibility impact: Breaking contract changes are permissible only when migration/deprecation is explicit. +- Safety/security impact: Control changes should bias toward continuity with bounded safeguards; critical protections still require explicit constraints. +- Data/operations impact: Decision traceability improves cross-turn consistency and auditability. + +## Implementation Notes +- Required code/doc updates: Set defaults in `AGENTS.md` and orchestrator skill instructions; keep decision-log template active. +- Validation evidence required: Presence of defaults in policy docs and this decision artifact under `.agents/decisions/`. + +## Rollback / Migration +- Rollback strategy: Update defaults in `AGENTS.md` and orchestrator SKILL; create a superseding decision log entry. +- Migration/deprecation plan: For any future hard-break preference, require explicit migration plan and effective date in a new decision entry. diff --git a/.agents/decisions/DECISION-20260223-pid-controller-placement-and-flowbased.md b/.agents/decisions/DECISION-20260223-pid-controller-placement-and-flowbased.md new file mode 100644 index 0000000..b34e39c --- /dev/null +++ b/.agents/decisions/DECISION-20260223-pid-controller-placement-and-flowbased.md @@ -0,0 +1,43 @@ +# Decision: Shared Modern PID in generalFunctions + PumpingStation Flow-Based Adoption + +- Date: 2026-02-23 +- Scope: `nodes/generalFunctions/src/pid/*`, `nodes/pumpingStation/src/*` + +## Context +Flow-based control in `pumpingStation` needed a production-grade PID with freeze/unfreeze, runtime retuning, and support for cascade/secondary-loop architecture. + +## Options Considered +1. Implement PID only inside `pumpingStation`. +2. Implement shared PID in `generalFunctions` and consume it from `pumpingStation`. +3. Keep current heuristic (non-PID) flow controller. + +## Decision +Chose option 2. + +## Rationale +- PID behavior is cross-domain control functionality and should be reusable across EVOLV nodes. +- `generalFunctions` already serves as shared utility/runtime infrastructure. +- Reuse reduces drift and duplicated control logic. +- PumpingStation can immediately adopt shared PID while preserving existing topic contracts. + +## Consequences +- Positive: + - Single, test-covered PID implementation with modern features. + - PumpingStation flow mode becomes true closed-loop control. + - Runtime support for freeze/unfreeze and tuning updates without redeploy. +- Risks: + - Behavioral differences versus prior heuristic flow control. + - Requires conservative tuning per site. + +## Safety / Compatibility +- No existing topic names were removed. +- Added optional control topics for PID runtime management. +- Existing non-flowbased modes remain intact. + +## Rollback +- Revert `nodes/pumpingStation/src/specificClass.js` flow-based branch to previous heuristic logic. +- Keep shared PID module in `generalFunctions` for future use, or revert `nodes/generalFunctions/src/pid/*` if required. + +## Migration Notes +- For `flowbased`, start with low `kp/ki`, verify stability in commissioning, then tune upward. +- Use `freezeFlowPid` and `setFlowPidMode` during maintenance or manual takeover. diff --git a/.agents/decisions/DECISION-20260224-nrmse-hardening-and-metric-profiles.md b/.agents/decisions/DECISION-20260224-nrmse-hardening-and-metric-profiles.md new file mode 100644 index 0000000..3210a18 --- /dev/null +++ b/.agents/decisions/DECISION-20260224-nrmse-hardening-and-metric-profiles.md @@ -0,0 +1,33 @@ +# Decision: Harden NRMSE and Use Metric Profiles in RotatingMachine + +- Date: 2026-02-24 +- Scope: `nodes/generalFunctions/src/nrmse/*`, `nodes/rotatingMachine/src/specificClass.js` + +## Context +Drift analytics were previously single-path and flow-focused with weak input safeguards in NRMSE. +Requirement: make NRMSE architecturally robust and apply it across multiple measurements in rotatingMachine. + +## Decision +Adopt a metric-profile drift architecture: + +1. Harden `generalFunctions/nrmse` with: +- strict validation for malformed inputs +- timestamp-aware alignment support +- per-metric state +- configurable rolling window and EWMA long-term trend +- point-based API (`assessPoint`) while retaining legacy calls + +2. Rewire rotatingMachine to consume NRMSE per metric: +- `flow` model drift +- `power` model drift +- pressure-quality drift as node-specific plausibility/redundancy assessment + +3. Expose drift and confidence outputs per metric in node output payload. + +## Consequences +- Drift computations are deterministic and safer under bad inputs. +- RotatingMachine confidence now reflects multiple measurement channels. +- Output schema expands with power/pressure drift fields. + +## Rollback Notes +- Revert `errorMetrics.js` and rotatingMachine drift wiring to return to legacy flow-only drift behavior. diff --git a/.agents/decisions/DECISION-20260224-rotatingmachine-hydraulic-efficiency-and-confidence.md b/.agents/decisions/DECISION-20260224-rotatingmachine-hydraulic-efficiency-and-confidence.md new file mode 100644 index 0000000..dce543e --- /dev/null +++ b/.agents/decisions/DECISION-20260224-rotatingmachine-hydraulic-efficiency-and-confidence.md @@ -0,0 +1,34 @@ +# Decision: RotatingMachine Hydraulic Efficiency Correction and Prediction Confidence + +- Date: 2026-02-24 +- Scope: `nodes/rotatingMachine/src/specificClass.js`, rotatingMachine integration tests + +## Context +Hydraulic efficiency calculation in `rotatingMachine` was dimensionally inconsistent and could over/under-report efficiency KPIs. +At the same time, prediction drift tooling (`nrmse`) existed but was not actively connected to rotatingMachine output confidence. + +## Options Considered +1. Keep existing formula and only tune thresholds. +2. Replace formula with standard hydraulic power/efficiency equations and expose prediction confidence from live pressure source + drift. + +## Decision +Adopt option 2. + +- Hydraulic power now follows standard engineering relation: + - `P_h = Q * Δp` (equivalent to `ρ g Q H`) + - `η_h = P_h / P_in` +- RotatingMachine now computes flow drift via `nrmse` from measured vs predicted flow windows. +- RotatingMachine now exposes prediction confidence fields in output: + - `predictionQuality` + - `predictionConfidence` + - `predictionPressureSource` + - `predictionFlags` + +## Consequences +- Efficiency KPIs become physically interpretable and traceable to pressure/flow/power inputs. +- Prediction trust is now observable by downstream control/dashboard layers. +- Output schema is expanded with new prediction confidence fields. + +## Rollback / Migration Notes +- Rollback path: revert `specificClass.js` hydraulic block and prediction-health integration. +- No mandatory migration required for existing flows unless they choose to consume new prediction confidence fields. diff --git a/.agents/decisions/DECISION-20260224-rotatingmachine-unit-anchor-and-curve-normalization.md b/.agents/decisions/DECISION-20260224-rotatingmachine-unit-anchor-and-curve-normalization.md new file mode 100644 index 0000000..2f6a532 --- /dev/null +++ b/.agents/decisions/DECISION-20260224-rotatingmachine-unit-anchor-and-curve-normalization.md @@ -0,0 +1,38 @@ +# Decision: Canonical Unit Anchoring and Curve Unit Normalization in RotatingMachine + +- Date: 2026-02-24 +- Scope: `nodes/rotatingMachine/*`, `nodes/generalFunctions/src/measurements/MeasurementContainer.js`, `nodes/generalFunctions/src/configs/rotatingMachine.json` + +## Context +RotatingMachine previously relied on node-local defaults for measurement storage units, with implicit assumptions that loaded machine curves used the same units as runtime configuration. This made unit drift likely when model curves, simulated inputs, and runtime settings differed. + +Owner decision direction: +- use a single unit anchor strategy +- treat node/UI units as ingress/egress only +- add explicit curve unit metadata +- reject or flag blank/invalid measurement units + +## Decision +1. Extend `MeasurementContainer` with optional canonical-anchor mode: +- per-type canonical unit mapping +- strict unit validation and required-unit policy +- compatibility checks by measure family +- requested-unit conversion at flattened output stage + +2. Apply canonical policy in `rotatingMachine` runtime: +- internal storage and calculations anchored to SI-like canonical units (`Pa`, `m3/s`, `W`, `K`) +- egress payloads converted back to configured output units +- ingress `simulateMeasurement` path requires explicit valid units + +3. Add explicit curve unit metadata (`asset.curveUnits`) and normalize loaded curves into canonical units before predictor initialization. + +## Consequences +- Unit handling is centralized and deterministic for RotatingMachine. +- Curve/model-unit mismatch risk is reduced by explicit metadata plus normalization. +- Existing output topic/field names remain stable; values are emitted in configured output units while internals stay canonical. +- This establishes a migration template for remaining EVOLV nodes. + +## Rollback Notes +- Revert `MeasurementContainer` canonical/validation extensions. +- Revert RotatingMachine unit-policy and curve-normalization wiring. +- Remove `asset.curveUnits` schema entry and restore previous node-local default-unit behavior. diff --git a/.agents/decisions/DECISION-20260224-unit-anchor-rollout-phase1-machinegroup-pumping-valves.md b/.agents/decisions/DECISION-20260224-unit-anchor-rollout-phase1-machinegroup-pumping-valves.md new file mode 100644 index 0000000..bc69e30 --- /dev/null +++ b/.agents/decisions/DECISION-20260224-unit-anchor-rollout-phase1-machinegroup-pumping-valves.md @@ -0,0 +1,37 @@ +# Decision: Unit-Anchor Rollout Phase 1 (MachineGroup, PumpingStation, Valve, ValveGroupControl) + +- Date: 2026-02-24 +- Scope: + - `nodes/machineGroupControl/src/nodeClass.js` + - `nodes/machineGroupControl/src/specificClass.js` + - `nodes/pumpingStation/src/nodeClass.js` + - `nodes/pumpingStation/src/specificClass.js` + - `nodes/valve/src/nodeClass.js` + - `nodes/valve/src/specificClass.js` + - `nodes/valveGroupControl/src/nodeClass.js` + - `nodes/valveGroupControl/src/specificClass.js` + +## Context +After adopting canonical-unit anchoring in `rotatingMachine`, adjacent controller nodes still mixed local units, unitless writes, and implicit conversions. That left cross-node behavior sensitive to registration order and source-unit assumptions. + +## Decision +1. Apply the same canonical storage policy per node: +- internal storage in canonical units (`Pa`, `m3/s`, `W`, `K` where relevant), +- preferred/output units for operator-facing status and output payloads. + +2. Enable strict measurement ingress discipline on migrated nodes: +- `strictUnitValidation: true`, +- `throwOnInvalidUnit: true`, +- required unit for physically dimensional types (`flow`, `pressure`, `power`, `temperature`, and node-specific equivalents). + +3. Replace unitless runtime writes/reads with explicit-unit helpers in each node’s domain class, including child-machine/child-valve interactions. + +## Consequences +- Cross-node calculations now run against a deterministic unit anchor in phase-1 nodes. +- Status/output values remain in preferred/output units, while internal math stays canonical. +- Legacy paths that send dimensional values without units now fail fast instead of silently coercing. + +## Rollback Notes +- Revert the eight files listed in scope. +- Restore previous `MeasurementContainer` initialization (non-canonical, non-strict behavior) in each node. +- Remove helper-based explicit unit reads/writes and revert to prior direct chain usage. diff --git a/.agents/decisions/DECISION_TEMPLATE.md b/.agents/decisions/DECISION_TEMPLATE.md new file mode 100644 index 0000000..c1f26b8 --- /dev/null +++ b/.agents/decisions/DECISION_TEMPLATE.md @@ -0,0 +1,36 @@ +# DECISION-YYYYMMDD- + +## Context +- Task/request: +- Impacted files/contracts: +- Why a decision is required now: + +## Options +1. Option A +- Benefits: +- Risks: +- Rollout notes: + +2. Option B +- Benefits: +- Risks: +- Rollout notes: + +## Decision +- Selected option: +- Decision owner: +- Date: +- Rationale: + +## Consequences +- Compatibility impact: +- Safety/security impact: +- Data/operations impact: + +## Implementation Notes +- Required code/doc updates: +- Validation evidence required: + +## Rollback / Migration +- Rollback strategy: +- Migration/deprecation plan: diff --git a/.agents/decisions/README.md b/.agents/decisions/README.md new file mode 100644 index 0000000..4fe5b92 --- /dev/null +++ b/.agents/decisions/README.md @@ -0,0 +1,15 @@ +# EVOLV Decision Log + +Use this folder to store high-impact agent/user decisions that affect compatibility, safety, security, schema, or rollout risk. + +Naming: +- `DECISION-YYYYMMDD-.md` + +When to log: +- topic/payload/API contract changes +- safety envelope or fail-safe strategy changes +- security posture/default changes +- Influx retention/backfill/schema tradeoffs +- explicit acceptance of deferred high-risk debt + +Start from `DECISION_TEMPLATE.md` for new entries. diff --git a/.agents/function-anchors/README.md b/.agents/function-anchors/README.md new file mode 100644 index 0000000..2401ee5 --- /dev/null +++ b/.agents/function-anchors/README.md @@ -0,0 +1,36 @@ +# Function Anchors + +This folder stores class-level anchor documents that define EVOLV logic truth for long-term maintainability. + +## Standard +1. Start each anchor with a **Connection Map (At a Glance)**. +2. Then provide a **Unit Table** as the first data section. +3. Cover the class end-to-end: config, I/O contracts, mode/state logic, full function inventory, calculations, safeguards, tests, invariants, and known gaps. +4. Keep references tied to file/line evidence. + +## Mandatory Architecture Rule +All EVOLV node anchors must use the same folder and artifact structure as `rotatingMachine`. + +Required per node: +- `.agents/function-anchors//ANCHOR-.md` +- `.agents/function-anchors//ANCHOR-.html` +- `.agents/function-anchors//EVIDENCE--tests.md` +- `nodes//test/basic/*.test.js` +- `nodes//test/integration/*.test.js` +- `nodes//test/edge/*.test.js` + +Enforcement policy: +- Do not ship behavioral changes in `nodes//` without updating the matching anchor and evidence files. +- New EVOLV nodes must be created with this structure from day one. +- Existing nodes missing this structure are considered incomplete and must be brought to parity. + +## Files +- `TEMPLATE.md`: reusable format for all future anchor points. +- `rotatingMachine/ANCHOR-rotatingMachine.md`: current rotatingMachine anchor. +- `rotatingMachine/EVIDENCE-rotatingMachine-tests.md`: test-evidence companion. +- `pumpingStation/ANCHOR-pumpingStation.md`: pumpingStation anchor preparation baseline. +- `pumpingStation/ANCHOR-pumpingStation.html`: pumpingStation visual topology anchor baseline. +- `pumpingStation/EVIDENCE-pumpingStation-tests.md`: pumpingStation test plan/evidence baseline. +- `monster/ANCHOR-monster.md`: monster node anchor baseline with API/report integration context. +- `monster/ANCHOR-monster.html`: monster visual topology anchor baseline. +- `monster/EVIDENCE-monster-tests.md`: monster test evidence baseline. diff --git a/.agents/function-anchors/TEMPLATE.md b/.agents/function-anchors/TEMPLATE.md new file mode 100644 index 0000000..3d61b08 --- /dev/null +++ b/.agents/function-anchors/TEMPLATE.md @@ -0,0 +1,94 @@ +# Function Anchor Template + +Use this template to document any EVOLV class as a stable "logic truth" anchor. + +## Mandatory File Layout (Required For Every Node) +- `.agents/function-anchors//ANCHOR-.md` +- `.agents/function-anchors//ANCHOR-.html` +- `.agents/function-anchors//EVIDENCE--tests.md` +- `nodes//test/basic/*.test.js` +- `nodes//test/integration/*.test.js` +- `nodes//test/edge/*.test.js` + +Any deviation from this layout must be treated as technical debt and resolved before closing the work item. + +## 1) Connection Map (At a Glance) +- **Node type**: +- **Consumes from EVOLV nodes/topics**: +- **Publishes to EVOLV nodes/topics**: +- **Registers as child to**: +- **Accepts child registration from**: +- **Admin/UI endpoints**: + +## 2) Unit Table (Always First Data Section) +| Signal/Field | Represents | Asset Type | Default Unit | Accepted Units | Source of Truth (file:line) | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---|---|---| + +## 3) Class Identity +- **Class**: +- **Primary files**: +- **Runtime responsibility**: +- **Editor responsibility**: + +## 4) Configuration Contract +| UI Field | Runtime Path | Default | Validation/Coercion | Behavior Impact | Source | +|---|---|---|---|---|---| + +## 5) Input/Output Contract +### Input topics +| Topic | Payload schema | Handler | Side effects | Source | +|---|---|---|---|---| + +### Output ports +| Port | Message type | Producer method | Typical consumers | Source | +|---|---|---|---|---| + +### Admin endpoints +| Endpoint | Method | Purpose | Source | +|---|---|---|---| + +## 6) Mode, State, and Control Model +- **Modes**: +- **Allowed actions by mode**: +- **Allowed sources by mode**: +- **Operational states for prediction**: +- **Sequence definitions**: + +## 7) End-to-End Execution Flow +1. Constructor and initialization flow. +2. Registration and child wiring flow. +3. Input routing flow. +4. Tick/output emission flow. +5. Status update flow. + +## 8) Full Function Inventory +| Function | Purpose | Reads | Writes | Calls | Emits/Returns | Failure/Fallback | Source | Covered by tests | +|---|---|---|---|---|---|---|---|---| + +## 9) Calculations and Physical Semantics +- **Prediction paths** (flow, power, control). +- **Pressure selection order**. +- **Efficiency, CoG, and BEP distance calculations**. +- **Assumptions and plausibility constraints**. + +## 10) Error Handling and Safeguards +- Validation guards. +- Warning/error paths. +- Availability-first behavior. + +## 11) Test Evidence Matrix +| Test file | What is covered | Methods/contracts anchored | +|---|---|---| + +## 12) Invariants (Anchor Truth) +- Non-negotiable behaviors this class must preserve. + +## 13) Known Gaps / Risks +- Mismatches, TODOs, or technical debt observed in current implementation. + +## 14) Change Checklist +- Required updates when logic changes: + - Code sections + - Contract docs + - Tests + - Example flows diff --git a/.agents/function-anchors/dashboardAPI/ANCHOR-dashboardAPI.html b/.agents/function-anchors/dashboardAPI/ANCHOR-dashboardAPI.html new file mode 100644 index 0000000..d54ce6e --- /dev/null +++ b/.agents/function-anchors/dashboardAPI/ANCHOR-dashboardAPI.html @@ -0,0 +1,16 @@ + + + + + + dashboardAPI Anchor + + + +

dashboardAPI Function Anchor

+
Baseline topology placeholder. Expand during functional changes.
+ + diff --git a/.agents/function-anchors/dashboardAPI/ANCHOR-dashboardAPI.md b/.agents/function-anchors/dashboardAPI/ANCHOR-dashboardAPI.md new file mode 100644 index 0000000..f0f058e --- /dev/null +++ b/.agents/function-anchors/dashboardAPI/ANCHOR-dashboardAPI.md @@ -0,0 +1,29 @@ +# dashboardAPI Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- Node type: dashboardAPI +- Scope: baseline anchor scaffold to satisfy EVOLV required architecture. + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| TBD | TBD | TBD | nodes/dashboardAPI/src/* | TBD | TBD | TBD | + +## 2) Class Identity +- Runtime registration: nodes/dashboardAPI +- Node-RED wrapper: nodes/dashboardAPI/src/nodeClass.js (when present) +- Domain logic: nodes/dashboardAPI/src/specificClass.js (when present) +- Editor UI: nodes/dashboardAPI/*.html (when present) + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +- Replace placeholder sections with full contract mapping on first functional change. + +## 4) Standardization Plan +1. Maintain this anchor and evidence docs with behavior changes. +2. Maintain tests under test/basic, test/integration, test/edge. +3. Maintain examples package (README, basic.flow.json, integration.flow.json, edge.flow.json). + +## 5) Acceptance Criteria For Completion +- Anchor/evidence artifacts exist. +- Test structure exists. +- Example structure exists. diff --git a/.agents/function-anchors/dashboardAPI/EVIDENCE-dashboardAPI-tests.md b/.agents/function-anchors/dashboardAPI/EVIDENCE-dashboardAPI-tests.md new file mode 100644 index 0000000..e421764 --- /dev/null +++ b/.agents/function-anchors/dashboardAPI/EVIDENCE-dashboardAPI-tests.md @@ -0,0 +1,15 @@ +# dashboardAPI Test Evidence + +Status: baseline structure scaffolded. + +## Required Test Layout +- nodes/dashboardAPI/test/basic/*.test.js +- nodes/dashboardAPI/test/integration/*.test.js +- nodes/dashboardAPI/test/edge/*.test.js + +## Baseline Mapping +| Test file | Scope | +|---|---| +| nodes/dashboardAPI/test/basic/structure-module-load.basic.test.js | module load smoke | +| nodes/dashboardAPI/test/integration/structure-examples.integration.test.js | examples package integrity | +| nodes/dashboardAPI/test/edge/structure-examples-node-type.edge.test.js | node-type presence in basic example | diff --git a/.agents/function-anchors/diffuser/ANCHOR-diffuser.html b/.agents/function-anchors/diffuser/ANCHOR-diffuser.html new file mode 100644 index 0000000..08b2aac --- /dev/null +++ b/.agents/function-anchors/diffuser/ANCHOR-diffuser.html @@ -0,0 +1,16 @@ + + + + + + diffuser Anchor + + + +

diffuser Function Anchor

+
Baseline topology placeholder. Expand during functional changes.
+ + diff --git a/.agents/function-anchors/diffuser/ANCHOR-diffuser.md b/.agents/function-anchors/diffuser/ANCHOR-diffuser.md new file mode 100644 index 0000000..d4c7f1d --- /dev/null +++ b/.agents/function-anchors/diffuser/ANCHOR-diffuser.md @@ -0,0 +1,29 @@ +# diffuser Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- Node type: diffuser +- Scope: baseline anchor scaffold to satisfy EVOLV required architecture. + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| TBD | TBD | TBD | nodes/diffuser/src/* | TBD | TBD | TBD | + +## 2) Class Identity +- Runtime registration: nodes/diffuser +- Node-RED wrapper: nodes/diffuser/src/nodeClass.js (when present) +- Domain logic: nodes/diffuser/src/specificClass.js (when present) +- Editor UI: nodes/diffuser/*.html (when present) + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +- Replace placeholder sections with full contract mapping on first functional change. + +## 4) Standardization Plan +1. Maintain this anchor and evidence docs with behavior changes. +2. Maintain tests under test/basic, test/integration, test/edge. +3. Maintain examples package (README, basic.flow.json, integration.flow.json, edge.flow.json). + +## 5) Acceptance Criteria For Completion +- Anchor/evidence artifacts exist. +- Test structure exists. +- Example structure exists. diff --git a/.agents/function-anchors/diffuser/EVIDENCE-diffuser-tests.md b/.agents/function-anchors/diffuser/EVIDENCE-diffuser-tests.md new file mode 100644 index 0000000..0c8edb9 --- /dev/null +++ b/.agents/function-anchors/diffuser/EVIDENCE-diffuser-tests.md @@ -0,0 +1,15 @@ +# diffuser Test Evidence + +Status: baseline structure scaffolded. + +## Required Test Layout +- nodes/diffuser/test/basic/*.test.js +- nodes/diffuser/test/integration/*.test.js +- nodes/diffuser/test/edge/*.test.js + +## Baseline Mapping +| Test file | Scope | +|---|---| +| nodes/diffuser/test/basic/structure-module-load.basic.test.js | module load smoke | +| nodes/diffuser/test/integration/structure-examples.integration.test.js | examples package integrity | +| nodes/diffuser/test/edge/structure-examples-node-type.edge.test.js | node-type presence in basic example | diff --git a/.agents/function-anchors/machineGroupControl/ANCHOR-machineGroupControl.html b/.agents/function-anchors/machineGroupControl/ANCHOR-machineGroupControl.html new file mode 100644 index 0000000..1ff3dea --- /dev/null +++ b/.agents/function-anchors/machineGroupControl/ANCHOR-machineGroupControl.html @@ -0,0 +1,16 @@ + + + + + + machineGroupControl Anchor + + + +

machineGroupControl Function Anchor

+
Baseline topology placeholder. Expand during functional changes.
+ + diff --git a/.agents/function-anchors/machineGroupControl/ANCHOR-machineGroupControl.md b/.agents/function-anchors/machineGroupControl/ANCHOR-machineGroupControl.md new file mode 100644 index 0000000..c0329fb --- /dev/null +++ b/.agents/function-anchors/machineGroupControl/ANCHOR-machineGroupControl.md @@ -0,0 +1,29 @@ +# machineGroupControl Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- Node type: machineGroupControl +- Scope: baseline anchor scaffold to satisfy EVOLV required architecture. + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| TBD | TBD | TBD | nodes/machineGroupControl/src/* | TBD | TBD | TBD | + +## 2) Class Identity +- Runtime registration: nodes/machineGroupControl +- Node-RED wrapper: nodes/machineGroupControl/src/nodeClass.js (when present) +- Domain logic: nodes/machineGroupControl/src/specificClass.js (when present) +- Editor UI: nodes/machineGroupControl/*.html (when present) + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +- Replace placeholder sections with full contract mapping on first functional change. + +## 4) Standardization Plan +1. Maintain this anchor and evidence docs with behavior changes. +2. Maintain tests under test/basic, test/integration, test/edge. +3. Maintain examples package (README, basic.flow.json, integration.flow.json, edge.flow.json). + +## 5) Acceptance Criteria For Completion +- Anchor/evidence artifacts exist. +- Test structure exists. +- Example structure exists. diff --git a/.agents/function-anchors/machineGroupControl/EVIDENCE-machineGroupControl-tests.md b/.agents/function-anchors/machineGroupControl/EVIDENCE-machineGroupControl-tests.md new file mode 100644 index 0000000..a515d1d --- /dev/null +++ b/.agents/function-anchors/machineGroupControl/EVIDENCE-machineGroupControl-tests.md @@ -0,0 +1,15 @@ +# machineGroupControl Test Evidence + +Status: baseline structure scaffolded. + +## Required Test Layout +- nodes/machineGroupControl/test/basic/*.test.js +- nodes/machineGroupControl/test/integration/*.test.js +- nodes/machineGroupControl/test/edge/*.test.js + +## Baseline Mapping +| Test file | Scope | +|---|---| +| nodes/machineGroupControl/test/basic/structure-module-load.basic.test.js | module load smoke | +| nodes/machineGroupControl/test/integration/structure-examples.integration.test.js | examples package integrity | +| nodes/machineGroupControl/test/edge/structure-examples-node-type.edge.test.js | node-type presence in basic example | diff --git a/.agents/function-anchors/measurement/ANCHOR-measurement.html b/.agents/function-anchors/measurement/ANCHOR-measurement.html new file mode 100644 index 0000000..e9234a5 --- /dev/null +++ b/.agents/function-anchors/measurement/ANCHOR-measurement.html @@ -0,0 +1,45 @@ + + + + + + Measurement Anchor Topology + + + +

Measurement Function Anchor

+
+

Topology

+
    +
  • Node: measurement
  • +
  • Runtime: nodes/measurement/src/nodeClass.js
  • +
  • Domain: nodes/measurement/src/specificClass.js
  • +
  • Admin endpoints: /measurement/menu.js, /measurement/configData.js, /measurement/asset-reg
  • +
+
+ +
+

Input Topics

+
    +
  • measurement -> set input value when payload is numeric
  • +
  • simulator -> toggle simulation mode
  • +
  • outlierDetection -> toggle outlier mode flag
  • +
  • calibrate -> run calibration logic
  • +
+
+ +
+

Output Ports

+
    +
  • Port 0: process message
  • +
  • Port 1: influx message
  • +
  • Port 2: parent registration (registerChild)
  • +
+
+ + diff --git a/.agents/function-anchors/measurement/ANCHOR-measurement.md b/.agents/function-anchors/measurement/ANCHOR-measurement.md new file mode 100644 index 0000000..0469025 --- /dev/null +++ b/.agents/function-anchors/measurement/ANCHOR-measurement.md @@ -0,0 +1,53 @@ +# Measurement Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- **Node type**: `measurement` (`nodes/measurement/measurement.js:1`, `nodes/measurement/measurement.html:14`) +- **Consumes topics**: `measurement`, `simulator`, `outlierDetection`, `calibrate` (`nodes/measurement/src/nodeClass.js:147`) +- **Publishes periodic outputs**: + - Output `0`: process payload (`nodes/measurement/src/nodeClass.js:137`) + - Output `1`: influx payload (`nodes/measurement/src/nodeClass.js:138`) + - Output `2`: parent registration (`registerChild`) (`nodes/measurement/src/nodeClass.js:118`) +- **Cross-node integrations (direct observed)**: + - Registers as child to parent with `positionVsParent` and optional `distance` (`nodes/measurement/src/nodeClass.js:118`) + - Emits measurement updates through `MeasurementContainer` in `specificClass` (`nodes/measurement/src/specificClass.js:479`) +- **Admin/UI endpoints**: + - `GET /measurement/menu.js` (`nodes/measurement/measurement.js:23`) + - `GET /measurement/configData.js` (`nodes/measurement/measurement.js:33`) + - `POST /measurement/asset-reg` (`nodes/measurement/measurement.js:43`) + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| `inputValue` | raw measurement input | asset-dependent | `nodes/measurement/src/specificClass.js:30` | `measurement` topic or simulator | scaling/smoothing pipeline | defaults to `0` | +| `outputAbs` (`mAbs`) | processed absolute output | `config.asset.unit` | `nodes/measurement/src/specificClass.js:472` | `updateOutputAbs()` | process/influx outputs + event emitter | constrained to configured abs range | +| `outputPercent` (`mPercent`) | normalized percent-like output | `%` semantic | `nodes/measurement/src/specificClass.js:483` | `updateOutputPercent()` | process/influx outputs | interpolated from abs range or observed min/max | +| `storedValues` | smoothing window values | same as processed value | `nodes/measurement/src/specificClass.js:24` | `applySmoothing()` | smoothing and repeatability checks | capped to `smoothWindow` length | +| `simulation.enabled` | internal simulated signal mode | boolean | `nodes/measurement/src/specificClass.js:52` | config/topic toggle | `tick()` behavior | off by default | + +## 2) Class Identity +- **Runtime registration + endpoints**: `nodes/measurement/measurement.js` +- **Node-RED wrapper/routing**: `nodes/measurement/src/nodeClass.js` +- **Domain measurement logic**: `nodes/measurement/src/specificClass.js` +- **Editor UI/defaults**: `nodes/measurement/measurement.html` + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +1. Node label precedence can hide fallback text due to expression order: +- `return this.positionIcon + " " + this.assetType || "Measurement";` (`nodes/measurement/measurement.html:63`) +2. `success` variable is assigned without declaration in editor save path: +- `success = window.EVOLV.nodes.measurement.assetMenu.saveEditor(this);` (`nodes/measurement/measurement.html:131`) +3. `toggleOutlierDetection()` mutates config object to boolean: +- `this.config.outlierDetection = !this.config.outlierDetection;` (`nodes/measurement/src/specificClass.js:503`) +4. Input handler ignores numeric strings for `measurement` topic: +- accepts only `typeof msg.payload === 'number'` (`nodes/measurement/src/nodeClass.js:152`) + +## 4) Standardization Plan (Mirror RotatingMachine) +1. Keep this anchor pair (`.md` + `.html`) and evidence file maintained with behavior changes. +2. Maintain test layout under `nodes/measurement/test/`: +- `basic/`, `integration/`, `edge/`, `helpers/` +3. Maintain examples package under `nodes/measurement/examples/`: +- `README.md`, `basic.flow.json`, `integration.flow.json`, `edge.flow.json` + +## 5) Acceptance Criteria For Completion +- Required anchor artifacts exist and map to current behavior. +- Test suite runs with node-level command. +- Example flow files exist and pass flow-structure tests. diff --git a/.agents/function-anchors/measurement/EVIDENCE-measurement-tests.md b/.agents/function-anchors/measurement/EVIDENCE-measurement-tests.md new file mode 100644 index 0000000..c1f1c5c --- /dev/null +++ b/.agents/function-anchors/measurement/EVIDENCE-measurement-tests.md @@ -0,0 +1,32 @@ +# Measurement Test Evidence + +Status: baseline suite created and executed. + +## Required Test Layout +- `nodes/measurement/test/basic/*.test.js` +- `nodes/measurement/test/integration/*.test.js` +- `nodes/measurement/test/edge/*.test.js` +- `nodes/measurement/test/helpers/*.js` + +## Test-to-Contract Mapping +| Test file | Scope | Primary contracts anchored | +|---|---|---| +| `nodes/measurement/test/basic/specific-constructor.basic.test.js` | constructor baseline and range derivation | `Measurement` constructor | +| `nodes/measurement/test/basic/scaling-and-output.basic.test.js` | scaling constraint and output update path | `calculateInput`, `updateOutputAbs`, `getOutput` | +| `nodes/measurement/test/basic/nodeclass-routing.basic.test.js` | topic routing and registration output shape | `nodeClass._attachInputHandler`, `_registerChild` | +| `nodes/measurement/test/integration/examples-flows.integration.test.js` | example package integrity and expected topic drivers | `nodes/measurement/examples/*.flow.json` | +| `nodes/measurement/test/integration/measurement-event.integration.test.js` | measurement container event emission contract | `updateOutputAbs`, measurement emitter wiring | +| `nodes/measurement/test/edge/invalid-payload.edge.test.js` | non-numeric input payload ignored behavior | `nodeClass._attachInputHandler` measurement branch | +| `nodes/measurement/test/edge/outlier-toggle.edge.test.js` | current outlier toggle behavior capture | `toggleOutlierDetection` | + +## Executed +- Command: + - `cd nodes/measurement && npm test` +- Result: + - `pass: baseline suite` (see latest run in session) +- Date: + - February 19, 2026 + +## Known Gaps Captured by Tests +- Outlier toggle currently converts config object to boolean. +- Measurement topic currently ignores numeric strings. diff --git a/.agents/function-anchors/monster/ANCHOR-monster.html b/.agents/function-anchors/monster/ANCHOR-monster.html new file mode 100644 index 0000000..f1af2fc --- /dev/null +++ b/.agents/function-anchors/monster/ANCHOR-monster.html @@ -0,0 +1,74 @@ + + + + + + Monster Anchor Map + + + +
+
+

Monster Function Anchor

+

External APIs are orchestrated by surrounding flows; the `monster` node computes sampling state and report fields.

+
+ input: input_q / i_start / monsternametijden / rain_data + output: pulse, m3Total, m3PerPuls, bucketVol, running + report path: Z-Info import + operator m3/pulse reference +
+ + + + + + + + + monster + + + PLC/measurement flow input + + + Open-Meteo + Aquon schedule + + + Dashboard / Influx / Grafana + + + PLC pulse + Z-Info report tooling + + + + + + + input_q / i_start / registerChild + rain_data / monsternametijden + process + influx streams + pulse + m3Total + m3PerPuls + +
+
+ + diff --git a/.agents/function-anchors/monster/ANCHOR-monster.md b/.agents/function-anchors/monster/ANCHOR-monster.md new file mode 100644 index 0000000..ec456b2 --- /dev/null +++ b/.agents/function-anchors/monster/ANCHOR-monster.md @@ -0,0 +1,85 @@ +# Monster Function Anchor (Baseline) + +## 0) Connection Map (At a Glance) +- **Node type**: `monster` (`nodes/monster/monster.js:1`, `nodes/monster/monster.html:5`) +- **Consumes control/input topics**: `input_q`, `i_start`, `monsternametijden`, `rain_data`, `registerChild` (`nodes/monster/src/nodeClass.js:202`) +- **Publishes periodic outputs**: + - Output `0`: process payload (`nodes/monster/src/nodeClass.js:185`) + - Output `1`: influx payload (`nodes/monster/src/nodeClass.js:186`) + - Output `2`: parent registration (`registerChild`) (`nodes/monster/src/nodeClass.js:158`) +- **Cross-node integrations**: + - Accepts measurement children of type `flow` (`nodes/monster/src/specificClass.js:300`) + - Common external orchestration pattern around this node: + - Open-Meteo -> `rain_data` + - Aquon schedule feed -> `monsternametijden` + - PLC/MQTT pulse sink fed by `output.pulse` + - Z-Info/report tooling fed by `m3Total` + `m3PerPuls` + - Dashboard API/Grafana and Influx consumers +- **Admin/UI endpoints**: + - `GET /monster/menu.js` + - `GET /monster/configData.js` (`nodes/monster/monster.js:17`, `nodes/monster/monster.js:27`) + +## 1) Unit Table (Always First Data Section) +| Signal/Field | Represents | Asset Type | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---|---| +| `input_q.payload.value` | influent flow command | manual/control input | `m3/h` (normalized in wrapper) | `nodes/monster/src/nodeClass.js:216` | upstream control flow | sampling calculation loop | invalid/unit conversion failure is warned and ignored | +| `flow.measured.*` | measured flow from child sensors | measurement child | `m3/h` | `nodes/monster/src/specificClass.js:300` | measurement nodes | effective flow selection | if missing, manual flow or 0 is used | +| `q` | effective flow used by model | derived | `m3/h` | `nodes/monster/src/specificClass.js:775` | `tick()` | pulse and volume progression | defaults to `0` if no measured/manual flow | +| `m3PerPuls` | reporting conversion factor for sampler pulse | derived/report field | `m3/pulse` | `nodes/monster/src/specificClass.js:660` | `sampling_program()` | Z-Info/report tooling, operations | `0` when not running | +| `m3Total` | accumulated volume during active run | derived/report field | `m3` | `nodes/monster/src/specificClass.js:687` | `sampling_program()` | Z-Info/report tooling | reset to `0` when sampling window ends | +| `pulse` | pulse command signal | control output | boolean | `nodes/monster/src/specificClass.js:707` | `sampling_program()` | PLC/MQTT pulse output paths | forced `false` under cooldown/capacity/end-of-run | +| `bucketVol` | sampled bucket fill volume | derived/state | `L` | `nodes/monster/src/specificClass.js:712` | pulse accumulation | dashboard/operator checks | reset to `0` after run | +| `predictedRateM3h` | rain-scaled prediction reference | derived | `m3/h` | `nodes/monster/src/specificClass.js:367` | `getOutput()` | dashboards/diagnostics | falls back to measured/manual effective rate | + +## 2) Class Identity +- **Runtime registration + endpoints**: `nodes/monster/monster.js` +- **Node-RED wrapper/routing**: `nodes/monster/src/nodeClass.js` +- **Domain sampling logic**: `nodes/monster/src/specificClass.js` +- **Editor UI/defaults**: `nodes/monster/monster.html` +- **Default config schema**: `nodes/generalFunctions/src/configs/monster.json` + +## 3) Configuration Contract (Key) +| UI Field | Runtime Path | Default | Behavior Impact | Source | +|---|---|---|---|---| +| `samplingtime` | `constraints.samplingtime` | `0` | sampling window hours | `nodes/monster/monster.html:16`, `nodes/monster/src/nodeClass.js:68` | +| `minvolume` | `constraints.minVolume` | `5` | min valid sample volume | `nodes/monster/monster.html:17`, `nodes/monster/src/nodeClass.js:69` | +| `maxweight` | `constraints.maxWeight` | `22` | max bucket load before invalid sample | `nodes/monster/monster.html:18`, `nodes/monster/src/nodeClass.js:70` | +| `nominalFlowMin` / `flowMax` | `constraints.nominalFlowMin` / `constraints.flowMax` | `0` / `0` | prediction bounds and start guard | `nodes/monster/monster.html:19`, `nodes/monster/src/specificClass.js:226` | +| `minSampleIntervalSec` | `constraints.minSampleIntervalSec` | `60` | pulse cooldown protection | `nodes/monster/monster.html:22`, `nodes/monster/src/specificClass.js:693` | +| `emptyWeightBucket` | `asset.emptyWeightBucket` | `3` | max bucket volume derivation | `nodes/monster/monster.html:23`, `nodes/monster/src/specificClass.js:378` | +| `aquon_sample_name` | `aquonSampleName` | `"112100"` internal default | schedule selector key | `nodes/monster/monster.html:24`, `nodes/monster/src/nodeClass.js:96` | + +## 4) I/O and Integration Notes +- Node-level output is process/influx/parent only. +- External APIs are normally handled by surrounding flows, not by the node class itself. +- Report tooling integration should read from process payload fields: + - `m3Total` + - `m3PerPuls` + - `running` + - `pulse` +- Reference examples: + - dashboard baseline: `nodes/monster/examples/monster-dashboard.flow.json` + - full API + dashboard template: `nodes/monster/examples/monster-api-dashboard.flow.json` + +## 5) Current Gaps / Risks +1. Wrapper exposes topics (`setMode`, `execSequence`, `execMovement`, `flowMovement`, `emergencystop`) that are not implemented in `Monster.handleInput` contract. +2. `showWorkingCurves`/`CoG` routes in wrapper call methods that are not present in `Monster`. +3. Existing legacy tests were not organized in required `basic/integration/edge` folders before this update. +4. External API credentials/tokens must remain outside committed example flows. + +## 6) Test Evidence Matrix (Current Baseline) +| Test file | What is covered | Methods/contracts anchored | +|---|---|---| +| `nodes/monster/test/basic/constructor.basic.test.js` | constructor + output field contract | `constructor`, `set_boundries_and_targets`, `getOutput` | +| `nodes/monster/test/integration/flow-and-schedule.integration.test.js` | flow averaging, rain/schedule ingestion | `registerChild`, `handleInput`, `tick`, `updateRainData`, `regNextDate` | +| `nodes/monster/test/edge/sampling-guards.edge.test.js` | invalid-bound guard + cooldown behavior | `validateFlowBounds`, `sampling_program`, cooldown gate | + +## 7) Change Checklist +- When `monster` behavior changes, update: + - `nodes/monster/src/nodeClass.js` + - `nodes/monster/src/specificClass.js` + - `nodes/monster/monster.html` + - `nodes/monster/test/basic|integration|edge/*` + - `.agents/function-anchors/monster/ANCHOR-monster.md` + - `.agents/function-anchors/monster/ANCHOR-monster.html` + - `.agents/function-anchors/monster/EVIDENCE-monster-tests.md` diff --git a/.agents/function-anchors/monster/EVIDENCE-monster-tests.md b/.agents/function-anchors/monster/EVIDENCE-monster-tests.md new file mode 100644 index 0000000..579fa7d --- /dev/null +++ b/.agents/function-anchors/monster/EVIDENCE-monster-tests.md @@ -0,0 +1,30 @@ +# Monster Test Evidence + +## Executed Baseline +- Command: + - `node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js` +- Working directory: + - `nodes/monster` +- Result: + - `pass: 6`, `fail: 0` + +## Test Matrix +| Test file | Scope | Contracts anchored | +|---|---|---| +| `nodes/monster/test/basic/constructor.basic.test.js` | initialization and output field contract | constructor, boundary setup, report output fields | +| `nodes/monster/test/basic/structure-module-load.basic.test.js` | required structure/module load guard | baseline architecture compliance | +| `nodes/monster/test/integration/flow-and-schedule.integration.test.js` | flow aggregation + rain/schedule ingestion | measured/manual flow merge, `handleInput`, schedule update path | +| `nodes/monster/test/integration/structure-examples.integration.test.js` | required examples contract guard | example flow presence/shape | +| `nodes/monster/test/edge/sampling-guards.edge.test.js` | sampling safety guards | invalid flow bounds guard, cooldown pulse throttling | +| `nodes/monster/test/edge/structure-examples-node-type.edge.test.js` | node-type structure guard | example includes `monster` node usage | + +## Coverage Notes +- Structure guards now require both dashboard examples: + - `nodes/monster/examples/monster-dashboard.flow.json` + - `nodes/monster/examples/monster-api-dashboard.flow.json` +- Focused on the most operationally critical report fields: + - `m3Total` + - `m3PerPuls` + - `pulse` + - `running` +- Additional contract tests are still recommended for wrapper topic routes that currently map to unsupported domain handlers. diff --git a/.agents/function-anchors/pumpingStation/ANCHOR-pumpingStation.html b/.agents/function-anchors/pumpingStation/ANCHOR-pumpingStation.html new file mode 100644 index 0000000..556ff75 --- /dev/null +++ b/.agents/function-anchors/pumpingStation/ANCHOR-pumpingStation.html @@ -0,0 +1,75 @@ + + + + + + PumpingStation Anchor Map + + + +
+
+

PumpingStation Function Anchor

+

Preparation baseline map. Keep this topology in sync with `ANCHOR-pumpingStation.md` and runtime contracts.

+
+ input: registerChild / calibrate* / q_in / changemode + output[0]: process + output[1]: influx + output[2]: registerChild +
+ + + + + + + + + pumpingStation + + + machine / machineGroupControl + + + measurement (level/flow/pressure) + + + dashboard / manual control + + + parent process / orchestrator + + + + + + + flow.predicted.* / control handoff + *.measured.<position> + q_in / calibrate / mode + registerChild + process/influx consumers + +
+
+ + diff --git a/.agents/function-anchors/pumpingStation/ANCHOR-pumpingStation.md b/.agents/function-anchors/pumpingStation/ANCHOR-pumpingStation.md new file mode 100644 index 0000000..095ccb4 --- /dev/null +++ b/.agents/function-anchors/pumpingStation/ANCHOR-pumpingStation.md @@ -0,0 +1,69 @@ +# Pumping Station Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- **Node type**: `pumpingStation` (`nodes/pumpingStation/pumpingStation.js:1`, `nodes/pumpingStation/pumpingStation.html:15`) +- **Consumes parent/control topics**: `changemode`, `registerChild`, `calibratePredictedVolume`, `calibratePredictedLevel`, `q_in` (`nodes/pumpingStation/src/nodeClass.js:209`) +- **Publishes periodic outputs**: + - Output `0`: process payload (`nodes/pumpingStation/src/nodeClass.js:197`) + - Output `1`: influx payload (`nodes/pumpingStation/src/nodeClass.js:198`) + - Output `2`: parent registration/control plumbing (`registerChild`) (`nodes/pumpingStation/src/nodeClass.js:114`) +- **Cross-node integrations (direct observed)**: + - Registers `measurement` children and listens for `*.measured.` events (`nodes/pumpingStation/src/specificClass.js:73`) + - Registers `machine`, `machinegroup`, `pumpingstation` children and listens for predicted flow (`nodes/pumpingStation/src/specificClass.js:59`) + - Commands child machines/stations/groups during control/safety transitions (`nodes/pumpingStation/src/specificClass.js:258`, `nodes/pumpingStation/src/specificClass.js:528`) +- **Admin/UI endpoints**: + - `GET /pumpingStation/menu.js` + - `GET /pumpingStation/configData.js` (`nodes/pumpingStation/pumpingStation.js:22`, `nodes/pumpingStation/pumpingStation.js:33`) + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| `flow.measured.*` / `flow.predicted.*` | inflow/outflow streams | `m3/s` preferred | `nodes/pumpingStation/src/specificClass.js:24` | measurement/machine/machinegroup children | net-flow selection + predicted volume integration | falls back to level-rate estimate when unavailable (`nodes/pumpingStation/src/specificClass.js:458`) | +| `level.measured.*` / `level.predicted.*` | wet well level | `m` | `nodes/pumpingStation/src/specificClass.js:24` | measurement or pressure conversion path | control decisions + remaining-time estimate | if no level available, remaining time becomes null (`nodes/pumpingStation/src/specificClass.js:487`) | +| `volume.predicted.atequipment` | integrated basin volume | `m3` | `nodes/pumpingStation/src/specificClass.js:393` | tick-based integration | safety + status + output | if volume unreadable, station shuts down machines availability-first (`nodes/pumpingStation/src/specificClass.js:503`) | +| `volumePercent.*.atequipment` | normalized fill percentage | `%` | `nodes/pumpingStation/src/specificClass.js:424` | level/volume conversion | status + dashboards | not emitted until level/volume is known | +| `netFlowRate.*.atequipment` | selected net flow | measured unit or `m3/s` | `nodes/pumpingStation/src/specificClass.js:454` | `_selectBestNetFlow()` | status + remaining-time + safety | defaults to `0` with `steady` direction (`nodes/pumpingStation/src/specificClass.js:466`) | +| `timeleft` | estimated seconds to empty/full limit | `s` | `nodes/pumpingStation/src/specificClass.js:470` | `_computeRemainingTime()` | safety logic + output | null if insufficient data | + +## 2) Class Identity +- **Runtime registration + endpoints**: `nodes/pumpingStation/pumpingStation.js` +- **Node-RED wrapper/routing**: `nodes/pumpingStation/src/nodeClass.js` +- **Domain/station logic**: `nodes/pumpingStation/src/specificClass.js` +- **Editor UI/defaults**: `nodes/pumpingStation/pumpingStation.html` +- **Default config schema/validation rules**: `nodes/generalFunctions/src/configs/pumpingStation.json` + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +1. Topic/mode mismatch: +- UI default uses `controlMode: "none"` (`nodes/pumpingStation/pumpingStation.html:59`) +- runtime switch expects `manual` not `none` (`nodes/pumpingStation/src/specificClass.js:234`) +2. Position token mismatch risk: +- code mixes `atEquipment` and `atequipment` variants (`nodes/pumpingStation/src/nodeClass.js:122`, `nodes/pumpingStation/src/specificClass.js:103`) +3. Child softwareType mismatch risk: +- checks for `'pumpingstation'`/`'machinegroup'` lowercase (`nodes/pumpingStation/src/specificClass.js:61`, `nodes/pumpingStation/src/specificClass.js:63`) +- other configs generally use camelCase (`nodes/generalFunctions/src/configs/pumpingStation.json:48`) +4. Missing guards in input registration path: +- no null check after `RED.nodes.getNode` (`nodes/pumpingStation/src/nodeClass.js:217`) +5. Test baseline exists but is not yet full parity: +- basic/edge/integration scaffolding is present; additional safety/control math coverage is still pending. + +## 4) Standardization Plan (Mirror RotatingMachine) +1. Create `ANCHOR-pumpingStation.html` with: +- always-visible topology map +- unit/signal catalog table +- control and safety flow diagram +- known invariants and risk list +2. Expand the current unit/integration/edge test suite under `nodes/pumpingStation/test/`: +- config defaults/overrides +- topic routing and child registration +- predicted volume integration and remaining-time math +- safety triggers and control actions +- regression for string casing mismatches and missing child node IDs +3. Add evidence companion doc: +- `EVIDENCE-pumpingStation-tests.md` with fail-before/pass-after references. +4. Keep this anchor and tests updated on every pumpingStation behavior change. + +## 5) Acceptance Criteria For Completion +- Anchor markdown complete to template parity with rotatingMachine. +- Anchor HTML visualization added and aligned with actual contracts. +- Test suite runnable with `node --test nodes/pumpingStation/test/**/*.test.js`. +- Evidence file links each test file to anchored behavior. diff --git a/.agents/function-anchors/pumpingStation/EVIDENCE-pumpingStation-tests.md b/.agents/function-anchors/pumpingStation/EVIDENCE-pumpingStation-tests.md new file mode 100644 index 0000000..d8b0a1e --- /dev/null +++ b/.agents/function-anchors/pumpingStation/EVIDENCE-pumpingStation-tests.md @@ -0,0 +1,34 @@ +# PumpingStation Test Evidence (Preparation) + +Status: baseline suite created and executed. + +## Executed +- Command: + - `node --test test/basic/*.test.js test/edge/*.test.js test/integration/*.test.js` +- Working directory: + - `nodes/pumpingStation` +- Result: + - `pass: 4`, `fail: 0` + +## Planned Test Matrix +| Planned test file | Scope | Primary contracts anchored | +|---|---|---| +| `nodes/pumpingStation/test/basic/constructor.basic.test.js` | config initialization, basin property derivation | constructor, `initBasinProperties`, config defaults | +| `nodes/pumpingStation/test/basic/nodeClass-routing.basic.test.js` | topic routing and registration handling | `nodeClass._attachInputHandler`, `registerChild`, calibration topics, `q_in` parsing | +| `nodes/pumpingStation/test/integration/registration-normalization.integration.test.js` | softwareType/position normalization and listener dedupe | `registerChild`, `_registerPredictedFlowChild`, `_registerMeasurementChild` | +| `nodes/pumpingStation/test/edge/mode-alias.edge.test.js` | mode alias normalization | `_normalizeMode`, `changeMode` compatibility path | +| `nodes/pumpingStation/test/integration/flow-balance.integration.test.js` | inflow/outflow aggregation and predicted volume update | `_updatePredictedVolume`, `_selectBestNetFlow`, `_computeRemainingTime` | +| `nodes/pumpingStation/test/integration/measurement.integration.test.js` | level/pressure measurement handling and conversions | `_onLevelMeasurement`, `_onPressureMeasurement` | +| `nodes/pumpingStation/test/integration/safety.integration.test.js` | dry-run/overfill/time threshold behavior | `_safetyController` | +| `nodes/pumpingStation/test/integration/control-levelbased.integration.test.js` | level-based machine command dispatch behavior | `_controlLevelBased`, `_applyMachineLevelControl` | +| `nodes/pumpingStation/test/edge/status.edge.test.js` | status output formatting under sparse data | `_updateNodeStatus` | + +## Execution Target +- Preferred command (after suite exists): `node --test nodes/pumpingStation/test/**/*.test.js` + +## Coverage Goal +- Match rotatingMachine discipline: + - config contract coverage + - topic routing coverage + - control/safety path coverage + - regression cases for known risk patterns diff --git a/.agents/function-anchors/reactor/ANCHOR-reactor.html b/.agents/function-anchors/reactor/ANCHOR-reactor.html new file mode 100644 index 0000000..b368e6b --- /dev/null +++ b/.agents/function-anchors/reactor/ANCHOR-reactor.html @@ -0,0 +1,16 @@ + + + + + + reactor Anchor + + + +

reactor Function Anchor

+
Baseline topology placeholder. Expand during functional changes.
+ + diff --git a/.agents/function-anchors/reactor/ANCHOR-reactor.md b/.agents/function-anchors/reactor/ANCHOR-reactor.md new file mode 100644 index 0000000..46e03c8 --- /dev/null +++ b/.agents/function-anchors/reactor/ANCHOR-reactor.md @@ -0,0 +1,29 @@ +# reactor Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- Node type: reactor +- Scope: baseline anchor scaffold to satisfy EVOLV required architecture. + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| TBD | TBD | TBD | nodes/reactor/src/* | TBD | TBD | TBD | + +## 2) Class Identity +- Runtime registration: nodes/reactor +- Node-RED wrapper: nodes/reactor/src/nodeClass.js (when present) +- Domain logic: nodes/reactor/src/specificClass.js (when present) +- Editor UI: nodes/reactor/*.html (when present) + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +- Replace placeholder sections with full contract mapping on first functional change. + +## 4) Standardization Plan +1. Maintain this anchor and evidence docs with behavior changes. +2. Maintain tests under test/basic, test/integration, test/edge. +3. Maintain examples package (README, basic.flow.json, integration.flow.json, edge.flow.json). + +## 5) Acceptance Criteria For Completion +- Anchor/evidence artifacts exist. +- Test structure exists. +- Example structure exists. diff --git a/.agents/function-anchors/reactor/EVIDENCE-reactor-tests.md b/.agents/function-anchors/reactor/EVIDENCE-reactor-tests.md new file mode 100644 index 0000000..1a23907 --- /dev/null +++ b/.agents/function-anchors/reactor/EVIDENCE-reactor-tests.md @@ -0,0 +1,15 @@ +# reactor Test Evidence + +Status: baseline structure scaffolded. + +## Required Test Layout +- nodes/reactor/test/basic/*.test.js +- nodes/reactor/test/integration/*.test.js +- nodes/reactor/test/edge/*.test.js + +## Baseline Mapping +| Test file | Scope | +|---|---| +| nodes/reactor/test/basic/structure-module-load.basic.test.js | module load smoke | +| nodes/reactor/test/integration/structure-examples.integration.test.js | examples package integrity | +| nodes/reactor/test/edge/structure-examples-node-type.edge.test.js | node-type presence in basic example | diff --git a/.agents/function-anchors/rotatingMachine/ANCHOR-rotatingMachine.html b/.agents/function-anchors/rotatingMachine/ANCHOR-rotatingMachine.html new file mode 100644 index 0000000..9bb3365 --- /dev/null +++ b/.agents/function-anchors/rotatingMachine/ANCHOR-rotatingMachine.html @@ -0,0 +1,810 @@ + + + + + + EVOLV RotatingMachine Anchor + + + +
+
+ +
+ +
+
+

RotatingMachine Engineering Anchor

+

Function-design truth source for runtime behavior, control contracts, units/signals, and integration boundaries.

+
+ Node Type: rotatingMachine + Domain Class: specificClass.js + Wrapper: nodeClass.js + Ports: 3 +
+
+
+
Control Topics In
9
+
Signal Rows Catalogued
31
+
Anchored Tests
9
+
Known Risks
4
+
+
+ +
+
+

Connection Map (Always Visible)

+
+
+
Primary integrations and contracts
+
+ machineGroupControl -> parent command source + pumpingStation -> orchestration source + measurement -> real pressure child via registerChild + dashboard flows -> simulateMeasurement + chart consumers + output[2] -> registerChild to parent + /rotatingMachine/menu.js + /rotatingMachine/configData.js +
+
+
Pressure Policy
Real > Virtual > 0
+
Mode Gate
Action + Source
+
Tick Rate
1s
+
Operational States
4 Active
+
+
+
+ + + + + + + + rotatingMachine + + + machineGroupControl + + + pumpingStation + + + dashboard / examples + + + measurement + + + + + + + + setMode / exec* + station-level dispatch + simulateMeasurement + process + influx out + registerChild + pressure.measured.* + +
+
+
+ +
+

Engineering Unit & Signal Catalog

+
+ + + Showing input topics/signals grouped by engineering function. +
+ + + + + + + + + + + + + + + + +
GroupSignal / TopicDirectionUnitTypical RangeCriticalityMeaningPrimary SourceConversion HintsFallback / Notes
+
+ +
+

Functional Parameter Sheet (Template + Example)

+
Engineering-oriented parameter table for BEP/curve/mechanical context. Example values are placeholders and scenario-dependent.
+ + + + + + + + + + + + +
ParameterSymboolEenheidWaarde (+/-)ToelichtingNode Mapping
+
+ +
+

True Graphs (Data-Derived)

+
Curves below are drawn from repository data in nodes/rotatingMachine/misc/measured_curve.json (pressure slice 175), plus derived efficiency ratio.
+
+
+
Flow vs Control (pressure=175)
+ +
+
+
Power vs Control (pressure=175)
+ +
+
+
+
+
Derived Efficiency Index (flow/power) vs Control
+ +
+
+
Allowed Actions by Mode (config defaults)
+ +
+
+
+ +
+

Execution Flow (Core Open)

+
+
1
Construct
Load defaults and model, initialize predictors/state/measurement containers, attach state listeners.
+
2
Connect
Register virtual pressure children, listen to real/virtual measurement streams, register self to parent.
+
3
Control
Route topics, validate mode/source/action, execute movement or sequence transitions.
+
4
Compute + Emit
Pressure basis selection -> flow/power prediction -> efficiency/CoG/BEP -> output formatting and status update.
+
+
+ +
+

Extended Sections (Selective Collapse)

+
+ Function Inventory Snapshot +
Anchored files: rotatingMachine.js, src/nodeClass.js, src/specificClass.js. 44+ callable methods/paths inventoried in markdown anchor.
+
+ constructor + init + registerChild + handler dispatch + handleInput + setMode + sequences + calcFlow/calcPower/calcCtrl + calcEfficiency + calcCog + getOutput +
+
+ +
+ Risks And Invariants +
Risk: CoG input path calls showCoG() but method is not present in current specificClass.js.
+
Risk: emergency sequence key mismatch (emergencyStop vs config emergencystop).
+
Risk: eneableLog typo in state logging mapping.
+
Risk: node label expression precedence may render unexpected labels.
+
Invariant: mechanical truth remains in specificClass.js; wrapper remains routing/lifecycle.
+
Invariant: pressure selection order stays real sensor > virtual sensor > fallback 0.
+
Invariant: output channels remain process/influx/parent registration separation.
+
+ +
+ Test Evidence +
+ constructor.basic.test.js + mode-and-input.basic.test.js + error-paths.edge.test.js + nodeClass-routing.edge.test.js + sequences.integration.test.js + registration.integration.test.js + pressure-initialization.integration.test.js + coolprop.integration.test.js + basic-flow-dashboard.integration.test.js +
+
+
+ +
+ Iteration 3: engineering-grade signal table (ranges, criticality, conversion hints), functional parameter sheet, input/output filters, dark mode toggle, selective collapsible sections, and data-derived graphs. +
+
+
+ + + + diff --git a/.agents/function-anchors/rotatingMachine/ANCHOR-rotatingMachine.md b/.agents/function-anchors/rotatingMachine/ANCHOR-rotatingMachine.md new file mode 100644 index 0000000..7f71505 --- /dev/null +++ b/.agents/function-anchors/rotatingMachine/ANCHOR-rotatingMachine.md @@ -0,0 +1,226 @@ +# Rotating Machine Function Anchor + +## 0) Connection Map (At a Glance) +- **Node type**: `rotatingMachine` (`nodes/rotatingMachine/rotatingMachine.js:1`, `nodes/rotatingMachine/rotatingMachine.html:16`) +- **Consumes parent/control topics**: `setMode`, `execSequence`, `execMovement`, `flowMovement`, `emergencystop`, `simulateMeasurement`, `registerChild`, `showWorkingCurves`, `CoG` (`nodes/rotatingMachine/src/nodeClass.js:267`) +- **Publishes periodic outputs**: + - Output `0`: process payload (`nodes/rotatingMachine/src/nodeClass.js:249`) + - Output `1`: influx payload (`nodes/rotatingMachine/src/nodeClass.js:251`) + - Output `2`: registration/control plumbing (`registerChild`) (`nodes/rotatingMachine/src/nodeClass.js:222`) +- **Cross-node integrations (direct observed)**: + - Registered/managed by `machineGroupControl` as `machine`, which then commands each machine via `handleInput('parent', ...)` (`nodes/machineGroupControl/src/specificClass.js:50`, `nodes/machineGroupControl/src/specificClass.js:711`, `nodes/machineGroupControl/src/specificClass.js:1028`) + - Can be orchestrated by `pumpingStation` via `execSequence` and movement commands (`nodes/pumpingStation/src/specificClass.js:296`, `nodes/pumpingStation/src/specificClass.js:297`) + - Dashboard/test flows inject `simulateMeasurement` and consume process output topics (`nodes/rotatingMachine/examples/basic.flow.json:380`, `nodes/rotatingMachine/examples/basic.flow.json:412`) +- **Admin/UI endpoints**: + - `GET /rotatingMachine/menu.js` + - `GET /rotatingMachine/configData.js` (`nodes/rotatingMachine/rotatingMachine.js:17`, `nodes/rotatingMachine/rotatingMachine.js:27`) + +## 1) Unit Table (Anchor Starts Here) +| Signal/Field | Represents | Asset Type | Default Unit | Accepted Units | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---|---|---| +| `pressure.measured.*` | pressure input (upstream/downstream) | measurement child (`pressure`) | `mbar` | any convertible via `MeasurementContainer` | `nodes/rotatingMachine/src/specificClass.js:48`, `nodes/rotatingMachine/src/specificClass.js:708` | real child sensors and virtual dashboard child | pressure dimension for curve selection (`predict*.fDimension`) | if missing, pressure dimension forced to minimum (`0`) (`nodes/rotatingMachine/src/specificClass.js:552`) | +| `flow.predicted.downstream` | predicted flow at discharge | rotating machine | `general.unit` from config | convertible (`m3/h`, `l/s`, etc.) | `nodes/rotatingMachine/src/specificClass.js:53`, `nodes/rotatingMachine/src/specificClass.js:423` | `calcFlow()` | output formatting, status text, parent/group logic | forced `0` if non-operational or no curve (`nodes/rotatingMachine/src/specificClass.js:415`, `nodes/rotatingMachine/src/specificClass.js:429`) | +| `flow.predicted.atEquipment` | same flow at equipment point | rotating machine | `general.unit` | convertible | `nodes/rotatingMachine/src/specificClass.js:53`, `nodes/rotatingMachine/src/specificClass.js:424` | `calcFlow()` | efficiency calculations | forced `0` if non-operational/no curve | +| `power.predicted.atEquipment` | predicted power draw | rotating machine | `kW` | convertible (`W`, `kW`) | `nodes/rotatingMachine/src/specificClass.js:54`, `nodes/rotatingMachine/src/specificClass.js:448` | `calcPower()` | efficiency calculations, status text | forced `0` if non-operational/no curve | +| `ctrl.predicted.atEquipment` | predicted control position for requested flow | rotating machine | unitless (%) semantic | numeric | `nodes/rotatingMachine/src/specificClass.js:482` | `calcCtrl()` | `flowmovement` command path | returns `0` if no curve | +| `temperature.measured.atEquipment` | process temp for density lookup | machine fluid context | `C` default, converted to `K` when used | convertible | init at `15 C` (`nodes/rotatingMachine/src/specificClass.js:149`) | init + measurement updates | CoolProp density input | if missing conversion, efficiency can degrade silently | +| `atmPressure.measured.atEquipment` | atmospheric pressure for density lookup | machine fluid context | `Pa` | convertible | init at `101325 Pa` (`nodes/rotatingMachine/src/specificClass.js:151`) | init | CoolProp density input | fallback density `1000 kg/m3` on CoolProp error (`nodes/rotatingMachine/src/specificClass.js:867`) | +| `efficiency.*` | specific flow (`flow/power`) | derived metric | implicit unitless ratio | numeric | `nodes/rotatingMachine/src/specificClass.js:878`, `nodes/rotatingMachine/src/specificClass.js:881` | `calcEfficiency()` | output and BEP distance metrics | unchanged if power/flow are zero | +| `specificEnergyConsumption.*` | power per flow | derived metric | implicit | numeric | `nodes/rotatingMachine/src/specificClass.js:879`, `nodes/rotatingMachine/src/specificClass.js:882` | `calcEfficiency()` | output consumers | unchanged if power/flow zero | +| `nHydraulicEfficiency.*` | hydraulic-efficiency-like metric | derived metric | unitless | numeric | `nodes/rotatingMachine/src/specificClass.js:884` | `calcEfficiency()` | diagnostic output | skipped if pressure/flow/power conversions unavailable | +| `cog`, `NCog`, `NCogPercent` | efficiency-curve peak indicators | derived curve metrics | unitless | numeric | `nodes/rotatingMachine/src/specificClass.js:796`, `nodes/rotatingMachine/src/specificClass.js:948` | `calcCog()`, `getOutput()` | group optimization, dashboards | retains last computed values | +| `effDistFromPeak`, `effRelDistFromPeak` | distance from best-efficiency point | derived | unitless | numeric | `nodes/rotatingMachine/src/specificClass.js:924`, `nodes/rotatingMachine/src/specificClass.js:962` | `calcDistanceBEP()` | output consumers | remains last computed value | +| `runtime`, `maintenanceTime`, `moveTimeleft`, `state` | movement/state telemetry | state machine | `h`/`s`/enum | numeric/string | `nodes/rotatingMachine/src/specificClass.js:943` | `state` module | output/status/parent control | depends on `state` module behavior | + +## 2) Class Identity +- **Runtime registration + endpoints**: `nodes/rotatingMachine/rotatingMachine.js` +- **Node-RED wrapper/routing**: `nodes/rotatingMachine/src/nodeClass.js` +- **Domain/mechanical logic**: `nodes/rotatingMachine/src/specificClass.js` +- **Editor UI/defaults**: `nodes/rotatingMachine/rotatingMachine.html` +- **Default config schema/validation rules**: `nodes/generalFunctions/src/configs/rotatingMachine.json` + +## 3) Configuration Contract +| UI Field | Runtime Path | Default | Validation/Coercion | Behavior Impact | Source | +|---|---|---|---|---|---| +| `speed` | `stateConfig.movement.speed` | `1` | `Number(uiConfig.speed)` | movement progression speed | `nodes/rotatingMachine/rotatingMachine.html:22`, `nodes/rotatingMachine/src/nodeClass.js:91` | +| `startup/warmup/shutdown/cooldown` | `stateConfig.time.*` | `0` | `Number(...)` | sequence transition durations | `nodes/rotatingMachine/rotatingMachine.html:23`, `nodes/rotatingMachine/src/nodeClass.js:95` | +| `movementMode` | `stateConfig.movement.mode` | `staticspeed` | raw string | state movement model selection | `nodes/rotatingMachine/rotatingMachine.html:27`, `nodes/rotatingMachine/src/nodeClass.js:92` | +| `unit` | `config.general.unit` + `config.asset.unit` | UI empty, config default `l/s` | direct assign then config init | base flow unit for measurements and outputs | `nodes/rotatingMachine/src/nodeClass.js:50`, `nodes/generalFunctions/src/configs/rotatingMachine.json:18` | +| `model` | `config.asset.model` | UI empty, config default `Unknown` | direct assign | curve loading via `loadCurve(model)` | `nodes/rotatingMachine/src/nodeClass.js:62`, `nodes/rotatingMachine/src/specificClass.js:18` | +| logging fields | `config.general.logging.*` | `enableLog=false`, `logLevel=error` in UI; config default enabled/info | direct assign | runtime verbosity | `nodes/rotatingMachine/rotatingMachine.html:39`, `nodes/rotatingMachine/src/nodeClass.js:51` | +| `positionVsParent` | `config.functionality.positionVsParent` | UI empty, config default `atEquipment` | direct assign + default in schema | registration topology to parent | `nodes/rotatingMachine/src/nodeClass.js:66`, `nodes/generalFunctions/src/configs/rotatingMachine.json:74` | +| Mode/action/source rules | `config.mode.*` | schema defaults | configUtils validation into `Set` semantics | command gating | `nodes/generalFunctions/src/configs/rotatingMachine.json:231`, `nodes/rotatingMachine/src/specificClass.js:269` | +| Sequences | `config.sequences.*` | schema defaults | configUtils validation | machine state transitions | `nodes/generalFunctions/src/configs/rotatingMachine.json:360`, `nodes/rotatingMachine/src/specificClass.js:363` | + +## 4) Input/Output Contract +### 4.1 Input topics (`nodeClass`) +| Topic | Payload schema | Handler | Side effects | +|---|---|---|---| +| `registerChild` | `payload=`, optional `positionVsParent` | registers child source via `childRegistrationUtils` | starts measurement event wiring (`nodes/rotatingMachine/src/nodeClass.js:268`) | +| `setMode` | `payload=` | `setMode()` | updates command policy mode | +| `execSequence` | `{source, action, parameter}` | `handleInput()` | executes state sequence | +| `execMovement` | `{source, action, setpoint}` | `handleInput()` | moves position | +| `flowMovement` | `{source, action, setpoint}` | `handleInput()` | converts flow->ctrl then moves | +| `emergencystop` | `{source, action}` | `handleInput()` | emergency sequence attempt | +| `simulateMeasurement` | `{type, position, value, unit, timestamp?}` | measurement update handlers | updates virtual pressure or measured values | +| `showWorkingCurves` | any | immediate response on output 0 | emits curve/cog debug payload | +| `CoG` | any | immediate response on output 0 | calls `m.showCoG()` (method currently not defined in `specificClass`) | + +### 4.2 Output ports +| Port | Message type | Source | +|---|---|---| +| `0` | formatted process message from flattened measurements + state fields | `nodes/rotatingMachine/src/nodeClass.js:250` | +| `1` | formatted influxdb message | `nodes/rotatingMachine/src/nodeClass.js:251` | +| `2` | registration to parent: `{topic:'registerChild', payload:id, positionVsParent}` | `nodes/rotatingMachine/src/nodeClass.js:222` | + +### 4.3 Admin endpoints +| Endpoint | Purpose | Source | +|---|---|---| +| `/rotatingMachine/menu.js` | dynamic editor menu script (`asset`, `logger`, `position`) | `nodes/rotatingMachine/rotatingMachine.js:17` | +| `/rotatingMachine/configData.js` | dynamic default/config script | `nodes/rotatingMachine/rotatingMachine.js:27` | + +## 5) Mode, State, and Control Model +- **Modes**: `auto`, `virtualControl`, `fysicalControl` (`nodes/generalFunctions/src/configs/rotatingMachine.json:233`) +- **Mode gate enforcement**: + - `isValidActionForMode(action, mode)` (`nodes/rotatingMachine/src/specificClass.js:279`) + - `isValidSourceForMode(source, mode)` (`nodes/rotatingMachine/src/specificClass.js:269`) +- **Actions supported by handler**: `execsequence`, `execmovement`, `flowmovement`, `entermaintenance`, `exitmaintenance`, `emergencystop`, `statuscheck` (`nodes/rotatingMachine/src/specificClass.js:303`) +- **Operational states for active prediction**: `operational`, `warmingup`, `accelerating`, `decelerating` (`nodes/rotatingMachine/src/specificClass.js:739`) +- **Sequence defaults**: startup/shutdown/emergencystop/maintenance flows defined in config schema (`nodes/generalFunctions/src/configs/rotatingMachine.json:365`) + +## 6) End-to-End Execution Flow +1. Node registration instantiates `nodeClass`, then `Specific` (`Machine`). +2. `Machine` constructor loads model curve, initializes predictors/state/measurements, creates virtual pressure children, and subscribes to state events. +3. `nodeClass` starts delayed child registration (`output 2`) and 1-second tick/status loops. +4. Incoming topics route through `switch(msg.topic)` to mode changes, movement/sequence commands, child registration, and simulated measurements. +5. Child measurement events update parent measurement container and dispatch typed handlers. +6. Pressure updates set predictor dimension, recompute flow/power/efficiency/CoG/BEP metrics. +7. Each tick emits formatted process + influx messages. + +## 7) Full Function Inventory +### 7.1 `nodes/rotatingMachine/rotatingMachine.js` +| Function | Purpose | Source | +|---|---|---| +| module export init | register Node-RED node type and admin endpoints | `nodes/rotatingMachine/rotatingMachine.js:5` | + +### 7.2 `nodes/rotatingMachine/src/nodeClass.js` +| Function | Purpose | Key effects | Source | +|---|---|---|---| +| `constructor` | boot wrapper lifecycle | load config, create source, start loops/handlers | `nodes/rotatingMachine/src/nodeClass.js:16` | +| `_loadConfig` | map UI config to runtime config | builds `general/asset/functionality`; builds `outputUtils` | `nodes/rotatingMachine/src/nodeClass.js:44` | +| `_setupSpecificClass` | build `Machine` with movement/time state config | instantiates `Specific`; stores on `node.source` | `nodes/rotatingMachine/src/nodeClass.js:77` | +| `_bindEvents` | placeholder | no-op currently | `nodes/rotatingMachine/src/nodeClass.js:112` | +| `_updateNodeStatus` | compose Node-RED status icon/text | warns once for missing pressure init; includes flow/power/state | `nodes/rotatingMachine/src/nodeClass.js:116` | +| `_registerChild` | announce self to parent | sends `registerChild` on output 2 after 100ms | `nodes/rotatingMachine/src/nodeClass.js:217` | +| `_startTickLoop` | periodic work | 1s `_tick`; 1s status refresh | `nodes/rotatingMachine/src/nodeClass.js:230` | +| `_tick` | periodic output generation | `formatMsg(process)` + `formatMsg(influxdb)` send to outputs 0/1 | `nodes/rotatingMachine/src/nodeClass.js:246` | +| `_attachInputHandler` | route inbound topics | dispatches all command/simulation/register/show topics | `nodes/rotatingMachine/src/nodeClass.js:260` | +| `_attachCloseHandler` | shutdown cleanup | clears intervals | `nodes/rotatingMachine/src/nodeClass.js:357` | + +### 7.3 `nodes/rotatingMachine/src/specificClass.js` +| Function | Purpose | Key effects | Source | +|---|---|---|---| +| `constructor` | initialize machine domain object | config/curve/predictors/state/measurement/events/virtual children | `nodes/rotatingMachine/src/specificClass.js:7` | +| `_initVirtualPressureChildren` | create simulated upstream/downstream pressure children | registers virtual measurement children | `nodes/rotatingMachine/src/specificClass.js:104` | +| `_init` | seed base measurements and min/max flow | sets temperature, atmPressure, curve min/max | `nodes/rotatingMachine/src/specificClass.js:147` | +| `_updateState` | enforce non-operational flow = 0 | overwrites predicted flow when inactive | `nodes/rotatingMachine/src/specificClass.js:163` | +| `registerChild` | subscribe to child measurement events | stores real pressure child IDs, updates measurements, calls handlers | `nodes/rotatingMachine/src/specificClass.js:173` | +| `_callMeasurementHandler` | typed measurement dispatch | pressure/flow/temp handlers + fallback | `nodes/rotatingMachine/src/specificClass.js:212` | +| `assessDrift` | compare measured vs predicted windows | delegates to `nrmse.assessDrift` | `nodes/rotatingMachine/src/specificClass.js:237` | +| `reverseCurve` | flip x/y arrays | used for flow->ctrl predictor | `nodes/rotatingMachine/src/specificClass.js:252` | +| `updateConfig` | apply validated config patch | merges via `configUtils` | `nodes/rotatingMachine/src/specificClass.js:264` | +| `isValidSourceForMode` | mode source gate | checks configured allowed set | `nodes/rotatingMachine/src/specificClass.js:269` | +| `isValidActionForMode` | mode action gate | checks configured allowed set | `nodes/rotatingMachine/src/specificClass.js:279` | +| `handleInput` | main command dispatcher | executes sequence/movement/status commands | `nodes/rotatingMachine/src/specificClass.js:289` | +| `abortMovement` | cancel current movement | delegates to state abort if available | `nodes/rotatingMachine/src/specificClass.js:345` | +| `setMode` | update current mode | validates against schema enum values | `nodes/rotatingMachine/src/specificClass.js:351` | +| `executeSequence` | run state sequence | transitions through configured states; updatePosition at end | `nodes/rotatingMachine/src/specificClass.js:363` | +| `setpoint` | move to numeric target | validates non-negative number then `state.moveTo` | `nodes/rotatingMachine/src/specificClass.js:394` | +| `calcFlow` | predict flow from current curve and ctrl position | writes predicted flow measurements | `nodes/rotatingMachine/src/specificClass.js:413` | +| `calcPower` | predict power from current curve and ctrl position | writes predicted power measurement | `nodes/rotatingMachine/src/specificClass.js:438` | +| `inputFlowCalcPower` | estimate power from requested flow | flow->ctrl->power chained prediction | `nodes/rotatingMachine/src/specificClass.js:460` | +| `calcCtrl` | estimate ctrl for desired flow | writes predicted ctrl | `nodes/rotatingMachine/src/specificClass.js:478` | +| `getMeasuredPressure` | choose pressure basis for prediction | differential preferred; then downstream; then upstream; else 0 | `nodes/rotatingMachine/src/specificClass.js:496` | +| `_getPreferredPressureValue` | pressure source priority resolver | real child > virtual child > aggregated position value | `nodes/rotatingMachine/src/specificClass.js:570` | +| `getPressureInitializationStatus` | pressure readiness status model | upstream/downstream/differential flags | `nodes/rotatingMachine/src/specificClass.js:600` | +| `updateSimulatedMeasurement` | write dashboard-sim values | pressure route via virtual child; others dispatch typed handler | `nodes/rotatingMachine/src/specificClass.js:617` | +| `handleMeasuredFlow` | reconcile measured flow availability/consistency | returns matched/single measurement or null | `nodes/rotatingMachine/src/specificClass.js:644` | +| `handleMeasuredPower` | read measured power | returns value or null with error | `nodes/rotatingMachine/src/specificClass.js:685` | +| `updateMeasuredTemperature` | temp update hook | currently log-only | `nodes/rotatingMachine/src/specificClass.js:698` | +| `updateMeasuredPressure` | pressure update hook | stores pressure, recomputes pressure basis and position metrics | `nodes/rotatingMachine/src/specificClass.js:703` | +| `updateMeasuredFlow` | flow update hook | stores measured flow if operational; mirrors predicted flow value | `nodes/rotatingMachine/src/specificClass.js:718` | +| `_isOperationalState` | operational predicate | active states used by prediction guards | `nodes/rotatingMachine/src/specificClass.js:737` | +| `updatePosition` | core recompute pipeline on movement/state changes | calc flow/power -> efficiency -> cog -> BEP distance | `nodes/rotatingMachine/src/specificClass.js:745` | +| `calcDistanceFromPeak` | abs distance metric | absolute efficiency delta | `nodes/rotatingMachine/src/specificClass.js:767` | +| `calcRelativeDistanceFromPeak` | normalized distance metric | interpolation to [0,1] | `nodes/rotatingMachine/src/specificClass.js:771` | +| `showWorkingCurves` | debugging snapshot of current curve context | returns current curves + metrics | `nodes/rotatingMachine/src/specificClass.js:779` | +| `calcCog` | compute peak efficiency point on current curve | updates `cog`, `NCog`, indexes, minEfficiency | `nodes/rotatingMachine/src/specificClass.js:796` | +| `calcEfficiencyCurve` | derive efficiency curve and peak/min | from aligned power/flow arrays | `nodes/rotatingMachine/src/specificClass.js:817` | +| `calcFlowPower` | convenience combined prediction | calls `calcFlow` and `calcPower` | `nodes/rotatingMachine/src/specificClass.js:845` | +| `calcEfficiency` | compute efficiency family metrics | CoolProp density path + fallback + writes derived metrics | `nodes/rotatingMachine/src/specificClass.js:854` | +| `updateCurve` | replace machine curve at runtime | validates config and updates predictors | `nodes/rotatingMachine/src/specificClass.js:897` | +| `getCompleteCurve` | return full loaded curves | power+flow input curves | `nodes/rotatingMachine/src/specificClass.js:910` | +| `getCurrentCurves` | return currently selected pressure curve slices | current flow/power curves | `nodes/rotatingMachine/src/specificClass.js:916` | +| `calcDistanceBEP` | write BEP distance metrics | updates `absDistFromPeak`, `relDistFromPeak` | `nodes/rotatingMachine/src/specificClass.js:924` | +| `getOutput` | flatten and enrich output object | adds state/runtime/ctrl/mode/cog/drift/eff-distance | `nodes/rotatingMachine/src/specificClass.js:936` | + +## 8) Calculations and Capability Matrix +- **Curve-backed capabilities**: + - flow prediction (`nq`) via `predictFlow` + - power prediction (`np`) via `predictPower` + - control inversion (flow->ctrl) via reversed `nq` +- **Pressure basis selection order**: + 1. real differential (`downstream - upstream`) + 2. real/virtual downstream only + 3. real/virtual upstream only + 4. fallback `0` (minimum pressure behavior) +- **Availability-first behavior**: + - missing pressure does not stop operation; it degrades predictions and warns. + - CoolProp failure does not stop operation; density fallback is used. + - non-operational states force predicted flow/power to zero. +- **BEP indicators**: + - computes peak efficiency index and normalized CoG (`NCog`) for optimization usage by parent/group controllers. + +## 9) Error Handling and Safeguards +- Invalid actions/sources are rejected by mode gates, with warnings (`nodes/rotatingMachine/src/specificClass.js:297`). +- Invalid setpoint (`<0` or non-number) is rejected in `setpoint()` (`nodes/rotatingMachine/src/specificClass.js:398`). +- Missing curve model disables predictor objects but keeps class alive (`nodes/rotatingMachine/src/specificClass.js:27`). +- Missing pressure initialization surfaces Node-RED warning ring status (`nodes/rotatingMachine/src/nodeClass.js:126`). +- `simulateMeasurement` rejects non-finite values and unsupported types (`nodes/rotatingMachine/src/nodeClass.js:312`). + +## 10) Test Evidence Matrix +| Test file | Covered behavior | +|---|---| +| `nodes/rotatingMachine/test/basic/constructor.basic.test.js` | constructor curve/no-curve behavior and output shape | +| `nodes/rotatingMachine/test/basic/mode-and-input.basic.test.js` | `setMode`, `handleInput` validation, operational-state predicate | +| `nodes/rotatingMachine/test/edge/error-paths.edge.test.js` | negative setpoint resilience, status failure path | +| `nodes/rotatingMachine/test/edge/nodeClass-routing.edge.test.js` | input-topic routing, pressure initialization status warning, curve/CoG reply routing | +| `nodes/rotatingMachine/test/integration/sequences.integration.test.js` | startup sequence and movement execution | +| `nodes/rotatingMachine/test/integration/registration.integration.test.js` | child registration and pressure event propagation | +| `nodes/rotatingMachine/test/integration/pressure-initialization.integration.test.js` | explicit pressure init combinations and real-vs-virtual pressure priority | +| `nodes/rotatingMachine/test/integration/coolprop.integration.test.js` | efficiency path with CoolProp and medium-pressure initialization behavior | +| `nodes/rotatingMachine/test/integration/basic-flow-dashboard.integration.test.js` | example-flow parser/wiring contracts for dashboard topics | + +## 11) Invariants (Anchor Truth) +- `specificClass` is the mechanical/logic source of truth; `nodeClass` is routing/lifecycle only. +- Prediction calculations must be curve-backed when curve exists, and availability-first fallback when it does not. +- Pressure selection priority is **real sensor > virtual dashboard > aggregated fallback**. +- Command execution must remain mode-gated by both action and source. +- Output shape must keep process/influx separation and parent registration on output port 2. +- Operational-state gating must continue to prevent active prediction outputs in inactive states. + +## 12) Known Gaps / Risks (Current Implementation) +- `nodeClass` routes topic `CoG` to `m.showCoG()`, but `showCoG` is not present in `specificClass` (runtime risk on that topic): `nodes/rotatingMachine/src/nodeClass.js:340`. +- `handleInput('emergencystop')` calls sequence `"emergencyStop"`, but config default key is `"emergencystop"` (case/name mismatch risk): `nodes/rotatingMachine/src/specificClass.js:327`, `nodes/generalFunctions/src/configs/rotatingMachine.json:381`. +- `_setupSpecificClass` uses `machineConfig.eneableLog` (typo) for state logging config; likely not intended: `nodes/rotatingMachine/src/nodeClass.js:86`. +- Label expression can evaluate unexpectedly because `+` and `||` precedence are mixed: `nodes/rotatingMachine/rotatingMachine.html:58`. + +## 13) Change Checklist +When changing rotatingMachine logic, update all of: +1. Runtime logic in `nodes/rotatingMachine/src/specificClass.js`. +2. Node-RED routing/lifecycle in `nodes/rotatingMachine/src/nodeClass.js`. +3. UI defaults/fields in `nodes/rotatingMachine/rotatingMachine.html`. +4. Config schema and mode/action/source/sequence defaults in `nodes/generalFunctions/src/configs/rotatingMachine.json`. +5. Example flow contracts in `nodes/rotatingMachine/examples/*.flow.json`. +6. Tests under `nodes/rotatingMachine/test/` (basic, edge, integration). + diff --git a/.agents/function-anchors/rotatingMachine/EVIDENCE-rotatingMachine-tests.md b/.agents/function-anchors/rotatingMachine/EVIDENCE-rotatingMachine-tests.md new file mode 100644 index 0000000..d40b3dc --- /dev/null +++ b/.agents/function-anchors/rotatingMachine/EVIDENCE-rotatingMachine-tests.md @@ -0,0 +1,23 @@ +# Rotating Machine Test Evidence + +## Scope +Evidence source for `ANCHOR-rotatingMachine.md`. + +## Test-to-Contract Mapping +| Test file | Contract/Behavior Anchored | +|---|---| +| `nodes/rotatingMachine/test/basic/constructor.basic.test.js` | Constructor should tolerate missing model curve and still return output object with core fields. | +| `nodes/rotatingMachine/test/basic/mode-and-input.basic.test.js` | Mode validation, source/action gating behavior, and active-state definition (`warmingup` active). | +| `nodes/rotatingMachine/test/edge/error-paths.edge.test.js` | Error path resilience in `setpoint()` and status update exception fallback (`Status Error`). | +| `nodes/rotatingMachine/test/edge/nodeClass-routing.edge.test.js` | Topic routing for control and simulation commands, pressure init warning behavior, and debug topic reply routing. | +| `nodes/rotatingMachine/test/integration/sequences.integration.test.js` | End-to-end state transitions for startup and movement command paths. | +| `nodes/rotatingMachine/test/integration/registration.integration.test.js` | Child measurement registration pipeline stores measured pressure in parent container. | +| `nodes/rotatingMachine/test/integration/pressure-initialization.integration.test.js` | Pressure initialization matrix (none/upstream/downstream/both) and preference for real child pressure over virtual dashboard pressure. | +| `nodes/rotatingMachine/test/integration/coolprop.integration.test.js` | Efficiency calculation path passes through CoolProp logic and verifies pressure dimension initialization behavior. | +| `nodes/rotatingMachine/test/integration/basic-flow-dashboard.integration.test.js` | Example dashboard parser wiring and topic/index contracts for flow/power/pressure charts. | + +## Remaining Coverage Gaps +- No direct test proves `handleInput('emergencystop')` sequence-name alignment with config key. +- No direct test for `CoG` input topic when `showCoG` is absent. +- No direct test for UI label precedence behavior in `rotatingMachine.html`. +- No direct test for typo path `machineConfig.eneableLog` in `_setupSpecificClass`. diff --git a/.agents/function-anchors/settler/ANCHOR-settler.html b/.agents/function-anchors/settler/ANCHOR-settler.html new file mode 100644 index 0000000..b2aa757 --- /dev/null +++ b/.agents/function-anchors/settler/ANCHOR-settler.html @@ -0,0 +1,16 @@ + + + + + + settler Anchor + + + +

settler Function Anchor

+
Baseline topology placeholder. Expand during functional changes.
+ + diff --git a/.agents/function-anchors/settler/ANCHOR-settler.md b/.agents/function-anchors/settler/ANCHOR-settler.md new file mode 100644 index 0000000..afd5a1f --- /dev/null +++ b/.agents/function-anchors/settler/ANCHOR-settler.md @@ -0,0 +1,29 @@ +# settler Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- Node type: settler +- Scope: baseline anchor scaffold to satisfy EVOLV required architecture. + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| TBD | TBD | TBD | nodes/settler/src/* | TBD | TBD | TBD | + +## 2) Class Identity +- Runtime registration: nodes/settler +- Node-RED wrapper: nodes/settler/src/nodeClass.js (when present) +- Domain logic: nodes/settler/src/specificClass.js (when present) +- Editor UI: nodes/settler/*.html (when present) + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +- Replace placeholder sections with full contract mapping on first functional change. + +## 4) Standardization Plan +1. Maintain this anchor and evidence docs with behavior changes. +2. Maintain tests under test/basic, test/integration, test/edge. +3. Maintain examples package (README, basic.flow.json, integration.flow.json, edge.flow.json). + +## 5) Acceptance Criteria For Completion +- Anchor/evidence artifacts exist. +- Test structure exists. +- Example structure exists. diff --git a/.agents/function-anchors/settler/EVIDENCE-settler-tests.md b/.agents/function-anchors/settler/EVIDENCE-settler-tests.md new file mode 100644 index 0000000..987aa61 --- /dev/null +++ b/.agents/function-anchors/settler/EVIDENCE-settler-tests.md @@ -0,0 +1,15 @@ +# settler Test Evidence + +Status: baseline structure scaffolded. + +## Required Test Layout +- nodes/settler/test/basic/*.test.js +- nodes/settler/test/integration/*.test.js +- nodes/settler/test/edge/*.test.js + +## Baseline Mapping +| Test file | Scope | +|---|---| +| nodes/settler/test/basic/structure-module-load.basic.test.js | module load smoke | +| nodes/settler/test/integration/structure-examples.integration.test.js | examples package integrity | +| nodes/settler/test/edge/structure-examples-node-type.edge.test.js | node-type presence in basic example | diff --git a/.agents/function-anchors/valve/ANCHOR-valve.html b/.agents/function-anchors/valve/ANCHOR-valve.html new file mode 100644 index 0000000..1a3b24d --- /dev/null +++ b/.agents/function-anchors/valve/ANCHOR-valve.html @@ -0,0 +1,16 @@ + + + + + + valve Anchor + + + +

valve Function Anchor

+
Baseline topology placeholder. Expand during functional changes.
+ + diff --git a/.agents/function-anchors/valve/ANCHOR-valve.md b/.agents/function-anchors/valve/ANCHOR-valve.md new file mode 100644 index 0000000..9cf7c19 --- /dev/null +++ b/.agents/function-anchors/valve/ANCHOR-valve.md @@ -0,0 +1,29 @@ +# valve Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- Node type: valve +- Scope: baseline anchor scaffold to satisfy EVOLV required architecture. + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| TBD | TBD | TBD | nodes/valve/src/* | TBD | TBD | TBD | + +## 2) Class Identity +- Runtime registration: nodes/valve +- Node-RED wrapper: nodes/valve/src/nodeClass.js (when present) +- Domain logic: nodes/valve/src/specificClass.js (when present) +- Editor UI: nodes/valve/*.html (when present) + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +- Replace placeholder sections with full contract mapping on first functional change. + +## 4) Standardization Plan +1. Maintain this anchor and evidence docs with behavior changes. +2. Maintain tests under test/basic, test/integration, test/edge. +3. Maintain examples package (README, basic.flow.json, integration.flow.json, edge.flow.json). + +## 5) Acceptance Criteria For Completion +- Anchor/evidence artifacts exist. +- Test structure exists. +- Example structure exists. diff --git a/.agents/function-anchors/valve/EVIDENCE-valve-tests.md b/.agents/function-anchors/valve/EVIDENCE-valve-tests.md new file mode 100644 index 0000000..201c180 --- /dev/null +++ b/.agents/function-anchors/valve/EVIDENCE-valve-tests.md @@ -0,0 +1,15 @@ +# valve Test Evidence + +Status: baseline structure scaffolded. + +## Required Test Layout +- nodes/valve/test/basic/*.test.js +- nodes/valve/test/integration/*.test.js +- nodes/valve/test/edge/*.test.js + +## Baseline Mapping +| Test file | Scope | +|---|---| +| nodes/valve/test/basic/structure-module-load.basic.test.js | module load smoke | +| nodes/valve/test/integration/structure-examples.integration.test.js | examples package integrity | +| nodes/valve/test/edge/structure-examples-node-type.edge.test.js | node-type presence in basic example | diff --git a/.agents/function-anchors/valveGroupControl/ANCHOR-valveGroupControl.html b/.agents/function-anchors/valveGroupControl/ANCHOR-valveGroupControl.html new file mode 100644 index 0000000..dfb0585 --- /dev/null +++ b/.agents/function-anchors/valveGroupControl/ANCHOR-valveGroupControl.html @@ -0,0 +1,16 @@ + + + + + + valveGroupControl Anchor + + + +

valveGroupControl Function Anchor

+
Baseline topology placeholder. Expand during functional changes.
+ + diff --git a/.agents/function-anchors/valveGroupControl/ANCHOR-valveGroupControl.md b/.agents/function-anchors/valveGroupControl/ANCHOR-valveGroupControl.md new file mode 100644 index 0000000..07a16b6 --- /dev/null +++ b/.agents/function-anchors/valveGroupControl/ANCHOR-valveGroupControl.md @@ -0,0 +1,29 @@ +# valveGroupControl Function Anchor (Preparation Baseline) + +## 0) Connection Map (At a Glance) +- Node type: valveGroupControl +- Scope: baseline anchor scaffold to satisfy EVOLV required architecture. + +## 1) Unit Table (Initial Baseline) +| Signal/Field | Represents | Default Unit | Source of Truth | Produced By | Consumed By | Fallback/Degraded Behavior | +|---|---|---|---|---|---|---| +| TBD | TBD | TBD | nodes/valveGroupControl/src/* | TBD | TBD | TBD | + +## 2) Class Identity +- Runtime registration: nodes/valveGroupControl +- Node-RED wrapper: nodes/valveGroupControl/src/nodeClass.js (when present) +- Domain logic: nodes/valveGroupControl/src/specificClass.js (when present) +- Editor UI: nodes/valveGroupControl/*.html (when present) + +## 3) Current Gaps To Resolve Before Declaring Anchor Complete +- Replace placeholder sections with full contract mapping on first functional change. + +## 4) Standardization Plan +1. Maintain this anchor and evidence docs with behavior changes. +2. Maintain tests under test/basic, test/integration, test/edge. +3. Maintain examples package (README, basic.flow.json, integration.flow.json, edge.flow.json). + +## 5) Acceptance Criteria For Completion +- Anchor/evidence artifacts exist. +- Test structure exists. +- Example structure exists. diff --git a/.agents/function-anchors/valveGroupControl/EVIDENCE-valveGroupControl-tests.md b/.agents/function-anchors/valveGroupControl/EVIDENCE-valveGroupControl-tests.md new file mode 100644 index 0000000..80cc24e --- /dev/null +++ b/.agents/function-anchors/valveGroupControl/EVIDENCE-valveGroupControl-tests.md @@ -0,0 +1,15 @@ +# valveGroupControl Test Evidence + +Status: baseline structure scaffolded. + +## Required Test Layout +- nodes/valveGroupControl/test/basic/*.test.js +- nodes/valveGroupControl/test/integration/*.test.js +- nodes/valveGroupControl/test/edge/*.test.js + +## Baseline Mapping +| Test file | Scope | +|---|---| +| nodes/valveGroupControl/test/basic/structure-module-load.basic.test.js | module load smoke | +| nodes/valveGroupControl/test/integration/structure-examples.integration.test.js | examples package integrity | +| nodes/valveGroupControl/test/edge/structure-examples-node-type.edge.test.js | node-type presence in basic example | diff --git a/.agents/improvements/IMPROVEMENTS_BACKLOG.md b/.agents/improvements/IMPROVEMENTS_BACKLOG.md new file mode 100644 index 0000000..85bf187 --- /dev/null +++ b/.agents/improvements/IMPROVEMENTS_BACKLOG.md @@ -0,0 +1,24 @@ +# Improvements Backlog + +Purpose: +- Capture functional and architectural improvements discovered during analysis runs where functionality is not changed. +- Keep an implementation queue outside active feature work. + +Lifecycle: +- Add item when discovered. +- When implemented, remove the item from this file and record the fix in session notes/PR. + +## Open Items (Beyond Current Top 10) + +| ID | Date | Area | Summary | Evidence | Status | +|---|---|---|---|---|---| +| IMP-20260219-004 | 2026-02-19 | measurement editor | Label expression precedence can hide fallback label text. | `nodes/measurement/measurement.html:63` | open | +| IMP-20260219-005 | 2026-02-19 | measurement editor | `success` assigned without declaration in editor save path. | `nodes/measurement/measurement.html:131` | open | +| IMP-20260219-016 | 2026-02-19 | generalFunctions/state | `movementManager` constructor writes startup `console.log` on runtime path, adding noisy non-structured logs. | `nodes/generalFunctions/src/state/movementManager.js:16` | open | +| IMP-20260219-018 | 2026-02-19 | generalFunctions/helper | Legacy menu endpoint still uses generated class source (`menuUtils.legacy.js`) as compatibility fallback; plan removal after UI validation of stable bootstrap/data path. | `nodes/generalFunctions/src/helper/endpointUtils.js:113`, `nodes/generalFunctions/src/helper/menuUtils.js:575` | open | +| IMP-20260219-019 | 2026-02-19 | generalFunctions/menu | `menuUtils.js` embeds extensive runtime `console.*` and inline API/debug logging in generated client scripts, increasing noise and making prod diagnostics harder. | `nodes/generalFunctions/src/helper/menuUtils.js:35` | open | +| IMP-20260219-020 | 2026-02-19 | generalFunctions/predict | `interpolation.js` contains raw runtime debug log (`console.log(this.interpolationtype)`), leaking internal state in production paths. | `nodes/generalFunctions/src/predict/interpolation.js:127` | open | +| IMP-20260219-021 | 2026-02-19 | generalFunctions/menu | Two active menu utility implementations (`menuUtils.js` and `menuUtils_DEPRECATED.js`) still coexist, increasing drift risk and maintenance overhead. | `nodes/generalFunctions/src/helper/menuUtils.js:1`, `nodes/generalFunctions/src/helper/menuUtils_DEPRECATED.js:1` | open | +| IMP-20260219-022 | 2026-02-19 | generalFunctions/outliers | `DynamicClusterDeviation.update()` emits verbose `console.log` traces on each call with no log-level guard, unsafe for production telemetry volume. | `nodes/generalFunctions/src/outliers/outlierDetection.js:7` | open | +| IMP-20260224-006 | 2026-02-24 | rotatingMachine prediction fallback | When only one pressure side is available, predictor uses absolute pressure as surrogate differential, which can materially bias flow prediction under varying suction/discharge conditions. | `nodes/rotatingMachine/src/specificClass.js:573`, `nodes/rotatingMachine/src/specificClass.js:588` | open | +| IMP-20260224-012 | 2026-02-24 | cross-node unit architecture | Canonical unit-anchor strategy is implemented in rotatingMachine plus phase-1 controllers (`machineGroupControl`, `pumpingStation`, `valve`, `valveGroupControl`); continue rollout to remaining nodes so all runtime paths use canonical storage + explicit ingress/egress units. | `nodes/machineGroupControl/src/specificClass.js:42`, `nodes/pumpingStation/src/specificClass.js:48`, `nodes/valve/src/specificClass.js:87`, `nodes/valveGroupControl/src/specificClass.js:78` | open | diff --git a/.agents/improvements/TOP10_PRODUCTION_PRIORITIES_2026-02-19-R2.md b/.agents/improvements/TOP10_PRODUCTION_PRIORITIES_2026-02-19-R2.md new file mode 100644 index 0000000..3b35f9d --- /dev/null +++ b/.agents/improvements/TOP10_PRODUCTION_PRIORITIES_2026-02-19-R2.md @@ -0,0 +1,53 @@ +# Top 10 Production Priorities (Round 2, Availability-First) + +Context: +- Generated after implementing the first top-10 and follow-up items `IMP-20260219-011/012/013`. +- Focus remains: keep runtime up, prefer degraded/null outputs over hard failures. + +## Priority List + +1. Fix measurement outlier toggle corruption. +- Why: toggling replaces the outlier config object with a boolean, breaking later config reads. +- Evidence: `nodes/measurement/src/specificClass.js:509`. + +2. Fix rotating machine pressure-difference unit request API mismatch. +- Why: `difference('Pa')` no longer matches container API; requested unit is ignored, risking incorrect efficiency basis. +- Evidence: `nodes/rotatingMachine/src/specificClass.js:856`, `nodes/generalFunctions/src/measurements/MeasurementContainer.js:436`. + +3. Guard reactor PFR state indexing at boundary conditions. +- Why: known edge behavior can overrun index mapping near exact reactor length and destabilize updates. +- Evidence: `nodes/reactor/src/specificClass.js:326`. + +4. Make dashboard template resolution fail-soft. +- Why: missing template currently throws and aborts dashboard generation path. +- Evidence: `nodes/dashboardAPI/src/specificClass.js:91`. + +5. Make dashboard input path skip invalid children instead of throwing. +- Why: missing child source/config currently throws; should warn and continue in availability-first mode. +- Evidence: `nodes/dashboardAPI/src/nodeClass.js:55`. + +6. Harden shared config merge semantics for arrays/types. +- Why: recursive merge mutates destination and treats arrays as objects, risking config drift. +- Evidence: `nodes/generalFunctions/src/helper/configUtils.js:77`. + +7. Fix machineGroupControl child position source path. +- Why: reads `positionVsParent` from `general` instead of `functionality`, causing inconsistent routing metadata. +- Evidence: `nodes/machineGroupControl/src/specificClass.js:53`. + +8. Accept numeric-string measurement payloads safely. +- Why: measurement node currently ignores numeric strings common in PLC/edge integrations. +- Evidence: `nodes/measurement/src/nodeClass.js:167`. + +9. Fix reactor editor save wiring mismatch. +- Why: editor save hook references the wrong node helpers, risking mis-saved position settings. +- Evidence: `nodes/reactor/reactor.html:133`. + +10. Replace raw `structuredClone` usage with compatibility-safe clone strategy. +- Why: runtime portability risk across constrained Node-RED deployments. +- Evidence: `nodes/settler/src/specificClass.js:34`, `nodes/settler/src/specificClass.js:45`. + +## Implementation Status + +- Implemented on 2026-02-19 in current session. +- Verification: tests passed for `generalFunctions`, `measurement`, `reactor`, `rotatingMachine`, `dashboardAPI`, `machineGroupControl`, `settler`, `pumpingStation`, `valve`, `valveGroupControl`, `monster`. +- Follow-up architectural items are tracked in `.agents/improvements/IMPROVEMENTS_BACKLOG.md`. diff --git a/.agents/improvements/TOP10_PRODUCTION_PRIORITIES_2026-02-19.md b/.agents/improvements/TOP10_PRODUCTION_PRIORITIES_2026-02-19.md new file mode 100644 index 0000000..749d1fe --- /dev/null +++ b/.agents/improvements/TOP10_PRODUCTION_PRIORITIES_2026-02-19.md @@ -0,0 +1,63 @@ +# Top 10 Production Priorities (Availability-First) + +Context: +- Scope reviewed: all nodes under `nodes/*` plus shared `nodes/generalFunctions/*`. +- Target posture: keep runtime alive; emit `null`/degraded outputs instead of crashing. + +## Priority List + +1. Remove import-time executable code from machine group runtime module. +- Why: `makeMachines()` runs at module load and can execute demo logic in production runtime. +- Evidence: `nodes/machineGroupControl/src/specificClass.js:1294`, `nodes/machineGroupControl/src/specificClass.js:1399`. +- Availability target: no side effects on `require`; test/demo code must be isolated from runtime path. + +2. Guard `registerChild` in node wrappers to prevent null dereference crashes. +- Why: multiple wrappers dereference `childObj.source` without checking child existence. +- Evidence: `nodes/reactor/src/nodeClass.js:54`, `nodes/settler/src/nodeClass.js:39`, `nodes/valve/src/nodeClass.js:256`, `nodes/valveGroupControl/src/nodeClass.js:178`, `nodes/machineGroupControl/src/nodeClass.js:215`. +- Availability target: if child missing, log warning and continue. + +3. Harden shared child registration utility contract checks. +- Why: shared helper destructures `child.config.*` without validation. +- Evidence: `nodes/generalFunctions/src/helper/childRegistrationUtils.js:9`, `nodes/generalFunctions/src/helper/childRegistrationUtils.js:10`. +- Availability target: reject invalid child payload with warning, no throw. + +4. Add global input-handler error boundary pattern across nodes. +- Why: many handlers do work without `try/catch`; one thrown error can bubble and destabilize node behavior. +- Evidence: `nodes/measurement/src/nodeClass.js:156`, `nodes/valve/src/nodeClass.js:250`, `nodes/valveGroupControl/src/nodeClass.js:169`, `nodes/settler/src/nodeClass.js:32`. +- Availability target: wrap topic routing; map failures to warning + safe `done(err)`/`done()` handling. + +5. Normalize `done` callback handling for Node-RED compatibility. +- Why: several nodes call `done()` unguarded; older/inconsistent runtime callbacks can fail. +- Evidence: `nodes/measurement/src/nodeClass.js:167`, `nodes/valve/src/nodeClass.js:280`, `nodes/valveGroupControl/src/nodeClass.js:201`, `nodes/machineGroupControl/src/nodeClass.js:257`. +- Availability target: `if (typeof done === 'function') done();` everywhere. + +6. Fix reactor runtime routing and setup defects. +- Why: `Temperature` topic routes to missing setter path and default reactor warning references wrong variable. +- Evidence: `nodes/reactor/src/nodeClass.js:46`, `nodes/reactor/src/nodeClass.js:140`. +- Availability target: unknown/unsupported control topics fail soft with warning; setup path cannot throw from bad references. + +7. Fix valve mode-selection bug using undefined config source. +- Why: `setMode` references `defaultConfig` instead of instance config; can throw or silently break mode changes. +- Evidence: `nodes/valve/src/specificClass.js:142`. +- Availability target: invalid mode inputs are rejected safely, valid mode changes deterministic. + +8. Replace hard-throw chain semantics in measurement container with safe-return options. +- Why: chain API currently throws for sequence misuse; upstream callers can crash control loops. +- Evidence: `nodes/generalFunctions/src/measurements/MeasurementContainer.js:99`, `nodes/generalFunctions/src/measurements/MeasurementContainer.js:109`. +- Availability target: invalid chain usage logs and returns no-op/null path in production mode. + +9. Remove noisy console/debug/test logging from runtime paths. +- Why: heavy `console.log/error` in control and shared code adds noise and can hide real failures. +- Evidence: `nodes/reactor/src/nodeClass.js:58`, `nodes/measurement/src/nodeClass.js:90`, `nodes/pumpingStation/src/nodeClass.js:87`, `nodes/valve/src/specificClass.js:207`. +- Availability target: use structured logger with levels; disable debug by default. + +10. Standardize output contract for unchanged state to explicit `null` output. +- Why: `formatMsg` returns `undefined` when no change; behavior differs by node send implementation. +- Evidence: `nodes/generalFunctions/src/helper/outputUtils.js:37`, `nodes/generalFunctions/src/helper/outputUtils.js:62`. +- Availability target: unchanged outputs always return `null` to keep port contract deterministic. + +## Implementation Status + +- Implemented on 2026-02-19 in current session. +- Verification: node test suites passed for modified runtime nodes (`measurement`, `reactor`, `valve`, `valveGroupControl`, `machineGroupControl`, `settler`, `pumpingStation`, `dashboardAPI`, `monster`, `rotatingMachine`). +- Remaining follow-up items are tracked in `.agents/improvements/IMPROVEMENTS_BACKLOG.md`. diff --git a/.agents/skills/evolv-alarms-interlocks-permissives/SKILL.md b/.agents/skills/evolv-alarms-interlocks-permissives/SKILL.md new file mode 100644 index 0000000..e42a7c2 --- /dev/null +++ b/.agents/skills/evolv-alarms-interlocks-permissives/SKILL.md @@ -0,0 +1,54 @@ +--- +name: evolv-alarms-interlocks-permissives +description: Design and review alarms, interlocks, and permissive logic for EVOLV control nodes. Use when implementing trip conditions, permissive checks, startup/shutdown guards, alarm priorities, latching/reset behavior, and operator-facing fault handling. +--- + +# EVOLV Alarms Interlocks Permissives + +## Mission +Make alarm and interlock behavior explicit, testable, and operationally safe while preserving availability-first policy bounds. + +## Harness Execution Contract +- Build alarm/interlock map from current node contracts and state logic. +- Define invariants before edits: + - trips/permissives are deterministic + - latching/reset behavior is explicit + - operator-visible diagnostics are preserved +- Validate with sequence and fail-state tests. + +## Scope +- `nodes/pumpingStation/` +- `nodes/machineGroupControl/` +- `nodes/rotatingMachine/` +- Any node with mode/state transitions and protective actions + +## Workflow +1. Enumerate alarm conditions and priority/severity. +2. Define interlock and permissive truth tables. +3. Verify startup/shutdown/emergency sequences. +4. Confirm reset, auto-recovery, and manual acknowledgement behavior. +5. Ensure outputs expose actionable fault context. + +## Standards +- Avoid hidden permissives; every gate should be observable. +- Keep alarm naming stable and semantically clear. +- Separate advisory warnings from trip-level protection. +- Preserve controlled compatibility for released fault topics. + +## Test Expectations +Cover: +- trip activation and reset/latch behavior +- permissive-denied and permissive-restored transitions +- out-of-order signal handling in sequence transitions +- degraded sensor quality paths and alarm escalation + +## Deliverables +Return: +- alarm/interlock/permissive matrix +- changed files/tests and evidence +- unresolved protection-vs-availability tradeoffs + +Decision interview triggers: +- changed trip thresholds or permissive logic with operational impact +- altered reset authority (auto vs manual) +- alarm contract changes affecting external consumers diff --git a/.agents/skills/evolv-alarms-interlocks-permissives/agents/openai.yaml b/.agents/skills/evolv-alarms-interlocks-permissives/agents/openai.yaml new file mode 100644 index 0000000..b364523 --- /dev/null +++ b/.agents/skills/evolv-alarms-interlocks-permissives/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV Alarms Interlocks Permissives" + short_description: "Protective logic and operator alarm specialist" + default_prompt: "Map alarm/interlock/permissive behavior in the impacted EVOLV nodes, define deterministic trip and reset rules, validate sequence edge cases, and return test-backed recommendations with clear operational tradeoffs." diff --git a/.agents/skills/evolv-biological-process-engineering/SKILL.md b/.agents/skills/evolv-biological-process-engineering/SKILL.md new file mode 100644 index 0000000..f41c709 --- /dev/null +++ b/.agents/skills/evolv-biological-process-engineering/SKILL.md @@ -0,0 +1,54 @@ +--- +name: evolv-biological-process-engineering +description: Engineer biological wastewater process behavior for EVOLV nodes. Use when implementing or reviewing reactor/settler biology, ASM-style kinetics, oxygen demand, nitrification/denitrification, sludge behavior, calibration assumptions, and biologically plausible constraints. +--- + +# EVOLV Biological Process Engineering + +## Mission +Keep EVOLV biological process models physically plausible, calibratable, and operationally useful. + +## Harness Execution Contract +- Ground changes in current biology/state variables and connected control topics. +- Define invariants before edits: + - biological mass-balance intent is preserved + - model assumptions remain explicit and traceable + - degraded behavior remains availability-first and bounded +- Validate with deterministic tests and representative operating scenarios. + +## Scope +- `nodes/reactor/` +- `nodes/settler/` +- `nodes/pumpingStation/` (where biology interacts with flow/retention assumptions) +- Related reaction modules and utilities under `nodes/*/src/` + +## Workflow +1. Identify biological state variables, units, and expected ranges. +2. Map kinetic pathways (growth, decay, transfer, conversion) and rate constraints. +3. Verify oxygen/temperature dependencies and fallback behavior. +4. Check integration stability for configured time-step and resolution choices. +5. Confirm outputs remain interpretable for control and dashboard consumers. + +## Standards +- Keep state vectors explicit and index mappings documented. +- Avoid silent clipping/coercion without test coverage and rationale. +- Preserve topic/payload compatibility unless migration is defined. +- Record calibration assumptions and required field data. + +## Test Expectations +Cover: +- kinetic branch behavior under representative and boundary conditions +- non-negativity and boundedness safeguards +- temperature and oxygen transfer sensitivity +- time-step/resolution edge behavior and stability warnings + +## Deliverables +Return: +- biological assumptions and constraints used +- changed files/tests and evidence +- calibration notes and unresolved biological uncertainties + +Decision interview triggers: +- altered biology assumptions that can change plant behavior +- parameter/default changes with startup or compliance impact +- compatibility breaks in biological outputs or topic contracts diff --git a/.agents/skills/evolv-biological-process-engineering/agents/openai.yaml b/.agents/skills/evolv-biological-process-engineering/agents/openai.yaml new file mode 100644 index 0000000..eb76ff6 --- /dev/null +++ b/.agents/skills/evolv-biological-process-engineering/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV Biological Process Engineering" + short_description: "Wastewater biology and kinetics specialist" + default_prompt: "Map biological state variables and kinetics in the impacted EVOLV nodes, define non-negotiable biological invariants, validate oxygen/temperature/time-step behavior, and return test-backed recommendations with calibration assumptions and risks." diff --git a/.agents/skills/evolv-commissioning-validation/SKILL.md b/.agents/skills/evolv-commissioning-validation/SKILL.md new file mode 100644 index 0000000..64ae562 --- /dev/null +++ b/.agents/skills/evolv-commissioning-validation/SKILL.md @@ -0,0 +1,54 @@ +--- +name: evolv-commissioning-validation +description: Plan and verify EVOLV commissioning readiness. Use when defining FAT/SAT test plans, acceptance criteria, loop checks, simulation-to-field validation, startup sequencing evidence, and rollout gates for operational deployment. +--- + +# EVOLV Commissioning Validation + +## Mission +Convert implementation changes into deployment-ready evidence with clear acceptance gates. + +## Harness Execution Contract +- Start from impacted contracts, modes, and site-operational constraints. +- Define invariants before edits: + - validation criteria are measurable and reproducible + - startup and failover behavior is proven, not assumed + - rollback path is explicit +- Produce evidence artifacts tied to concrete tests/checks. + +## Scope +- Cross-node behavior spanning control, measurement, and integrations +- Test plans and validation docs under repository documentation paths +- Node-level suites where commissioning evidence is derived + +## Workflow +1. Build FAT/SAT matrix from impacted contracts and risk areas. +2. Define pass/fail criteria and required instrumentation visibility. +3. Execute or script reproducible validation checks. +4. Capture evidence with timestamps, commands, and outcomes. +5. Define rollout gates and rollback triggers. + +## Standards +- Prefer deterministic replayable checks over ad-hoc validation. +- Include negative-path and recovery-path tests. +- Tie each acceptance criterion to a concrete artifact. +- Keep operator handoff notes concise and actionable. + +## Test Expectations +Cover: +- startup/shutdown commissioning sequences +- failover and reconnect scenarios +- alarm/interlock behavior under commissioning cases +- post-deploy smoke checks and regression shortlist + +## Deliverables +Return: +- FAT/SAT-style validation matrix +- executed evidence summary +- go/no-go risks and mitigations +- rollback plan and monitoring checklist + +Decision interview triggers: +- reduced commissioning scope under schedule pressure +- acceptance of unresolved high-severity risks +- rollout sequencing choices with operational impact diff --git a/.agents/skills/evolv-commissioning-validation/agents/openai.yaml b/.agents/skills/evolv-commissioning-validation/agents/openai.yaml new file mode 100644 index 0000000..9742603 --- /dev/null +++ b/.agents/skills/evolv-commissioning-validation/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV Commissioning Validation" + short_description: "FAT/SAT and deployment-readiness specialist" + default_prompt: "Create a commissioning evidence plan from impacted EVOLV contracts, define measurable FAT/SAT acceptance criteria, verify failure and recovery paths, and return go/no-go risks with rollback guidance." diff --git a/.agents/skills/evolv-database-influx-architecture/SKILL.md b/.agents/skills/evolv-database-influx-architecture/SKILL.md index cb08539..6459c47 100644 --- a/.agents/skills/evolv-database-influx-architecture/SKILL.md +++ b/.agents/skills/evolv-database-influx-architecture/SKILL.md @@ -8,6 +8,14 @@ description: Design and review EVOLV data modeling and storage architecture for ## Mission Shape telemetry data so it is queryable, performant, and maintainable for operations dashboards and analytics. +## Harness Execution Contract +- Start from current measurement/tag/field usage and dashboard queries. +- Define invariants before edits: + - query compatibility for existing Grafana/consumer flows + - bounded tag cardinality + - explicit units and timestamp policy +- Provide validation evidence using representative payloads and queries. + ## Scope - Output payload structure from EVOLV nodes - InfluxDB write model: measurement, tags, fields, timestamp policy @@ -43,3 +51,8 @@ Return: - rationale tied to dashboard and analytics use - changed files/tests - migration and retention plan + +Decision interview triggers: +- schema changes that break existing queries/panels +- retention/downsampling policies with data-loss tradeoffs +- backfill rules that alter historical comparability diff --git a/.agents/skills/evolv-database-influx-architecture/agents/openai.yaml b/.agents/skills/evolv-database-influx-architecture/agents/openai.yaml index 8221375..5347ba7 100644 --- a/.agents/skills/evolv-database-influx-architecture/agents/openai.yaml +++ b/.agents/skills/evolv-database-influx-architecture/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV Database + Influx Architect" short_description: "Design telemetry schema for Influx and Grafana" - default_prompt: "Define EVOLV telemetry modeling, Influx measurement/tag/field choices, retention strategy, and Grafana-compatible query shape." + default_prompt: "Define EVOLV telemetry schema from current payload/query usage, enforce cardinality and compatibility invariants, validate with representative queries, and escalate decision-gate tradeoffs for retention/backfill or breaking schema changes." diff --git a/.agents/skills/evolv-frontend-node-red/SKILL.md b/.agents/skills/evolv-frontend-node-red/SKILL.md index f78506a..8005d86 100644 --- a/.agents/skills/evolv-frontend-node-red/SKILL.md +++ b/.agents/skills/evolv-frontend-node-red/SKILL.md @@ -8,6 +8,14 @@ description: Design and implement Node-RED node editor UI and runtime-facing con ## Mission Implement robust Node-RED editor experiences that keep UI defaults, runtime config parsing, and behavior in lockstep. +## Harness Execution Contract +- Start from impacted files and active runtime/editor contracts. +- Define invariants before editing: + - `.html` defaults and runtime parsing parity + - endpoint path stability (`//menu.js`, `//configData.js`) + - released topic/output compatibility unless migration is declared +- Provide evidence for changed behavior (tests or smoke checks). + ## Repo-Specific Rules - Use CommonJS patterns in runtime files. - Keep node names aligned across `RED.nodes.registerType('', ...)`, runtime registration in `.js`, and package/node mapping. @@ -52,3 +60,8 @@ Return: - changed files - tests added and what they prove - migration notes if backward compatibility changed + +Decision interview triggers: +- form or default changes that alter existing deployed behavior +- endpoint contract/path changes +- UI/runtime migration that could break saved Node-RED flows diff --git a/.agents/skills/evolv-frontend-node-red/agents/openai.yaml b/.agents/skills/evolv-frontend-node-red/agents/openai.yaml index e84c935..00fa871 100644 --- a/.agents/skills/evolv-frontend-node-red/agents/openai.yaml +++ b/.agents/skills/evolv-frontend-node-red/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV Frontend + Node-RED" short_description: "Build EVOLV Node-RED editor/runtime UX safely" - default_prompt: "Implement EVOLV Node-RED editor HTML and runtime config mapping with strict UI/runtime parity, stable endpoint contracts, and tests." + default_prompt: "Implement EVOLV Node-RED editor/runtime changes from a file-level impact map, preserve UI/runtime parity and stable endpoint contracts, provide verification evidence, and ask decision-gate questions before compatibility-breaking edits." diff --git a/.agents/skills/evolv-instrumentation-assets/SKILL.md b/.agents/skills/evolv-instrumentation-assets/SKILL.md index 73e437b..4fb6cf8 100644 --- a/.agents/skills/evolv-instrumentation-assets/SKILL.md +++ b/.agents/skills/evolv-instrumentation-assets/SKILL.md @@ -8,6 +8,14 @@ description: Engineer measurement and instrumentation behavior for EVOLV assets. ## Mission Ensure measurements are trustworthy, well-modeled, and usable by control and analytics logic. +## Harness Execution Contract +- Ground changes in current measurement definitions and downstream consumers. +- Define invariants before edits: + - unit consistency and conversion transparency + - explicit quality-state handling + - no silent coercion that hides sensor faults +- Provide evidence for data-quality transitions and fallback behavior. + ## Scope - `nodes/measurement/` - `nodes/generalFunctions/src/helper/measurement*` @@ -40,3 +48,8 @@ Return: - validation and fallback rules - file/test changes - open instrumentation risks for commissioning + +Decision interview triggers: +- changed units or semantics for released measurements +- new fallback logic that may mask real field failures +- altered quality thresholds affecting control decisions diff --git a/.agents/skills/evolv-instrumentation-assets/agents/openai.yaml b/.agents/skills/evolv-instrumentation-assets/agents/openai.yaml index 9602486..7b90662 100644 --- a/.agents/skills/evolv-instrumentation-assets/agents/openai.yaml +++ b/.agents/skills/evolv-instrumentation-assets/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV Instrumentation Engineer" short_description: "Define sensor and measurement asset behavior" - default_prompt: "Design instrumentation mappings, units, validation, quality handling, and robust measurement-driven logic for EVOLV assets." + default_prompt: "Design EVOLV measurement behavior from current assets and consumers, enforce unit/quality invariants, provide validation evidence for edge conditions, and ask decision-gate questions before semantic or threshold changes." diff --git a/.agents/skills/evolv-measurement-product-specialist/SKILL.md b/.agents/skills/evolv-measurement-product-specialist/SKILL.md new file mode 100644 index 0000000..8cd6b8c --- /dev/null +++ b/.agents/skills/evolv-measurement-product-specialist/SKILL.md @@ -0,0 +1,54 @@ +--- +name: evolv-measurement-product-specialist +description: Apply measurement product and device expertise for EVOLV. Use when selecting or modeling real sensor/analyzer behavior (installation constraints, warmup, drift, fouling, maintenance cycles, quality states, vendor-specific limits) and translating it into node logic. +--- + +# EVOLV Measurement Product Specialist + +## Mission +Model real-world measurement device behavior so EVOLV control logic receives realistic, diagnosable signals. + +## Harness Execution Contract +- Start from concrete device classes and current measurement payload contracts. +- Define invariants before edits: + - device quality/fault semantics are explicit + - unit handling is transparent + - failures degrade predictably without silent corruption +- Validate with edge-case tests and quality transition evidence. + +## Scope +- `nodes/measurement/` +- Measurement consumption paths in `nodes/*/src/` +- Shared measurement utilities in `nodes/generalFunctions/src/measurements/` + +## Workflow +1. Define device class behavior (transmitter, analyzer, meter, switch). +2. Capture startup/warmup/maintenance/fault states. +3. Map quality codes and stale/noisy behavior into payload semantics. +4. Verify conversion and plausibility bounds. +5. Confirm downstream control impact under bad/suspect states. + +## Standards +- Separate raw, filtered, and engineered values where needed. +- Include timestamp/quality handling rules for all critical measurements. +- Avoid masking device faults with silent defaults. +- Document maintenance and recalibration assumptions. + +## Test Expectations +Cover: +- warmup and delayed validity behavior +- drift/fouling/noise injection paths +- quality-state transitions and downstream handling +- device-specific bounds and unit compatibility + +## Deliverables +Return: +- device behavior model and assumptions +- payload/quality mapping +- changed files/tests with evidence +- commissioning checks required in field + +Decision interview triggers: +- changed quality semantics used by control decisions +- new fallback paths that could hide instrumentation failure +- device defaults likely to alter operator behavior diff --git a/.agents/skills/evolv-measurement-product-specialist/agents/openai.yaml b/.agents/skills/evolv-measurement-product-specialist/agents/openai.yaml new file mode 100644 index 0000000..014e60f --- /dev/null +++ b/.agents/skills/evolv-measurement-product-specialist/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV Measurement Product Specialist" + short_description: "Sensor/analyzer product behavior expert" + default_prompt: "Model real device behavior for the impacted EVOLV measurement paths, including warmup, drift, fouling, quality states, and bounds; preserve payload contracts and provide test-backed fallback behavior." diff --git a/.agents/skills/evolv-mechanical-rotating-equipment/SKILL.md b/.agents/skills/evolv-mechanical-rotating-equipment/SKILL.md index 5ecc942..5dfb096 100644 --- a/.agents/skills/evolv-mechanical-rotating-equipment/SKILL.md +++ b/.agents/skills/evolv-mechanical-rotating-equipment/SKILL.md @@ -8,6 +8,14 @@ description: Provide rotating equipment engineering guidance for EVOLV machine a ## Mission Keep rotating equipment behavior physically plausible, safe, and diagnosable in EVOLV JavaScript domain classes. +## Harness Execution Contract +- Start from equipment assumptions and current curve/sensor sources in repo. +- Define invariants before edits: + - physical plausibility constraints remain enforced + - degraded/fail-safe behavior remains explicit + - outputs remain diagnosable for operations teams +- Validate with boundary-condition evidence and deterministic test outcomes. + ## Scope - `nodes/rotatingMachine/` - `nodes/machineGroupControl/` @@ -43,3 +51,8 @@ Return: - implemented constraints and rationale - test coverage for boundary cases - remaining mechanical uncertainties requiring field data + +Decision interview triggers: +- safety envelope changes (speed, power, flow/head boundaries) +- curve-source replacement with uncertain field alignment +- degraded-mode changes that affect availability vs protection tradeoffs diff --git a/.agents/skills/evolv-mechanical-rotating-equipment/agents/openai.yaml b/.agents/skills/evolv-mechanical-rotating-equipment/agents/openai.yaml index 0604383..d530f10 100644 --- a/.agents/skills/evolv-mechanical-rotating-equipment/agents/openai.yaml +++ b/.agents/skills/evolv-mechanical-rotating-equipment/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV Rotating Equipment Engineer" short_description: "Model rotating assets with physical realism" - default_prompt: "Review and implement rotating-machine logic with unit consistency, envelope constraints, failure-safe behavior, and regression tests." + default_prompt: "Review EVOLV rotating-machine logic from current curves/sensors, enforce physical and fail-safe invariants, verify with boundary evidence, and trigger decision-gate interviews before changing safety envelopes." diff --git a/.agents/skills/evolv-orchestrator/SKILL.md b/.agents/skills/evolv-orchestrator/SKILL.md index 955af91..7678bf0 100644 --- a/.agents/skills/evolv-orchestrator/SKILL.md +++ b/.agents/skills/evolv-orchestrator/SKILL.md @@ -8,26 +8,38 @@ description: Orchestrate multi-agent execution for the EVOLV repository. Use whe ## Mission Coordinate specialized EVOLV agents, split work into clear tasks, and ensure integrations are coherent across JavaScript/CommonJS Node-RED nodes, process assets, and observability/data concerns. +## Harness-Style Operating Rules +- Start from the live repo state, not generic playbooks. +- Build a file-level impact map before assigning specialist work. +- Define invariants first, then implement changes. +- Require evidence for each claim (tests, smoke checks, endpoint validation, or concrete diffs). +- Convert repeated lessons into updated skill guidance to reduce future ambiguity. + ## Execution Flow 1. Frame the objective and constraints in one paragraph. -2. Identify impacted areas in the repo before assigning work: +2. Build an impact map before assigning work. Identify touched contracts and files: - `nodes//.html` (editor UI) - `nodes//.js` (runtime entry) - `nodes//src/nodeClass.js` (Node-RED wrapper) - `nodes//src/specificClass.js` (domain logic) - `nodes/generalFunctions/` (shared helpers/config) -3. Route tasks to specialist skills with explicit deliverables and acceptance criteria. -4. Require each specialist to return: +3. Declare invariants and acceptance criteria: +- backward compatibility posture: controlled breaks allowed only with migration +- safety posture: availability-first unless user overrides for a specific task +- security trust boundary/default behavior +- data schema/query compatibility where relevant +4. Route tasks to specialist skills with explicit deliverables and acceptance criteria. +5. Require each specialist to return: - assumptions - changed files - tests added/updated - unresolved risks -5. Integrate outputs and check cross-skill consistency: +6. Integrate outputs and check cross-skill consistency: - config fields aligned between `.html` and runtime parsing - admin endpoints stable (`//menu.js`, `//configData.js`) - topic contracts (`msg.topic`) unchanged unless migration is defined -6. Ask targeted user interview questions only when decisions are ambiguous and unblock execution. -7. Produce a final integrated plan or implementation with a risk log. +7. Ask targeted user interview questions only at decision gates (see protocol below). +8. Produce a final integrated implementation with a risk log and decision log updates when needed. ## Delegation Map - Use `evolv-frontend-node-red` for Node-RED editor/runtime UX and HTML config input design. @@ -44,9 +56,28 @@ Ask at most 3 focused questions at a time. Prioritize: 2. Safety/availability constraints (what must never break). 3. Backward compatibility expectations for flows and topics. +Trigger an interview before finalizing when any of these are true: +- Breaking topic/payload/schema/API change is proposed +- Safety envelope or fail-safe defaults are loosened/tightened +- Security defaults or endpoint exposure changes +- Data retention/backfill/query behavior changes +- Rollout strategy has material operational risk + +Default resolution rules when interviewing: +- prefer compatibility option with migration over undocumented hard breaks +- prefer availability-first behavior with explicit bounded safeguards +- always create/update a decision log entry for every decision-gate outcome + +Question format: +- decision statement (one sentence) +- options with tradeoff +- recommended option and why + ## Output Contract Return: - task breakdown by specialist - execution order and dependencies - measurable acceptance criteria - integration risks and mitigation +- evidence summary (what was verified and how) +- decision log entries created/updated (if any) diff --git a/.agents/skills/evolv-orchestrator/agents/openai.yaml b/.agents/skills/evolv-orchestrator/agents/openai.yaml index 4eafc6f..04de48f 100644 --- a/.agents/skills/evolv-orchestrator/agents/openai.yaml +++ b/.agents/skills/evolv-orchestrator/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV Orchestrator" short_description: "Coordinate EVOLV specialist agent workflows" - default_prompt: "Break EVOLV work into specialist tasks, sequence execution, ask focused clarifying questions when needed, and return integrated acceptance criteria with risks." + default_prompt: "Build a repo-grounded impact map, define invariants, delegate EVOLV work to specialists with measurable acceptance criteria, run decision-gate interviews for ambiguous high-impact choices, and return integrated evidence plus risks." diff --git a/.agents/skills/evolv-ot-edge-plc-integration/SKILL.md b/.agents/skills/evolv-ot-edge-plc-integration/SKILL.md new file mode 100644 index 0000000..0ba81b4 --- /dev/null +++ b/.agents/skills/evolv-ot-edge-plc-integration/SKILL.md @@ -0,0 +1,54 @@ +--- +name: evolv-ot-edge-plc-integration +description: Engineer OT edge connectivity and PLC interoperability for EVOLV. Use when implementing or reviewing OPC UA/Modbus and similar integrations, namespace/tag mapping, quality/timestamp semantics, retry/reconnect behavior, and deterministic command/feedback contracts at the edge. +--- + +# EVOLV OT Edge PLC Integration + +## Mission +Deliver reliable, deterministic edge protocol integration between EVOLV Node-RED nodes and PLC/SCADA systems. + +## Harness Execution Contract +- Start from current integration topology, topic contracts, and protocol endpoints. +- Define invariants before edits: + - command/feedback contracts remain deterministic + - reconnect/retry behavior is bounded and observable + - quality/timestamp semantics are preserved end-to-end +- Validate with connection-loss and recovery evidence. + +## Scope +- Edge/connector nodes (existing and new) +- Topic mapping code in `nodes/*/src/` +- Admin endpoints/config for connector behavior and credentials + +## Workflow +1. Map PLC tags/NodeIds/registers to EVOLV message contracts. +2. Define write acknowledgement and feedback confirmation rules. +3. Implement reconnect/backoff/session handling. +4. Enforce quality, timestamp, and stale-value semantics. +5. Verify failover behavior and command idempotency. + +## Standards +- Never assume connection continuity; model transient faults explicitly. +- Keep protocol mappings versioned and auditable. +- Separate transport errors from process-state errors. +- Ensure secure defaults align with OT/IT security skill. + +## Test Expectations +Cover: +- disconnect/reconnect and session re-establish paths +- duplicate/late/out-of-order message handling +- read/write mapping correctness and unit conversion +- safe behavior under degraded quality or timeout + +## Deliverables +Return: +- integration contract map (protocol <-> topic/payload) +- retry/recovery strategy and limits +- changed files/tests with failure-injection evidence +- operational rollout risks and mitigations + +Decision interview triggers: +- command authority or handshake behavior changes +- protocol mapping breaks requiring migration +- timeout/retry strategy changes affecting availability/safety diff --git a/.agents/skills/evolv-ot-edge-plc-integration/agents/openai.yaml b/.agents/skills/evolv-ot-edge-plc-integration/agents/openai.yaml new file mode 100644 index 0000000..c469d16 --- /dev/null +++ b/.agents/skills/evolv-ot-edge-plc-integration/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV OT Edge PLC Integration" + short_description: "OPC UA/PLC edge interoperability specialist" + default_prompt: "Build a protocol-to-topic contract map for the affected EVOLV integration, define deterministic read/write and reconnect semantics, validate failure and recovery behavior, and return evidence-backed implementation guidance." diff --git a/.agents/skills/evolv-ot-it-security/SKILL.md b/.agents/skills/evolv-ot-it-security/SKILL.md index a6640ce..e0adb27 100644 --- a/.agents/skills/evolv-ot-it-security/SKILL.md +++ b/.agents/skills/evolv-ot-it-security/SKILL.md @@ -8,6 +8,14 @@ description: Perform OT/IT security analysis for EVOLV Node-RED automation syste ## Mission Identify and reduce security risk while preserving operational reliability for process automation workloads. +## Harness Execution Contract +- Model trust boundaries first (admin HTTP, message ingress, external integrations). +- Define security invariants before edits: + - secure defaults stay secure unless explicitly approved + - no sensitive leakage in logs/UI/errors + - malformed control inputs are rejected predictably +- Support findings with reproducible evidence and concrete remediation steps. + ## Scope - Node-RED admin endpoints in node entry files - Input validation across `msg.topic` and payload paths @@ -41,3 +49,8 @@ Return: - concrete remediation plan by file - tests added for security regressions - residual risks and compensating controls + +Decision interview triggers: +- any change that relaxes authentication/authorization checks +- exposure of new admin routes or integration interfaces +- security control deferrals that require compensating controls diff --git a/.agents/skills/evolv-ot-it-security/agents/openai.yaml b/.agents/skills/evolv-ot-it-security/agents/openai.yaml index c984cb8..0643061 100644 --- a/.agents/skills/evolv-ot-it-security/agents/openai.yaml +++ b/.agents/skills/evolv-ot-it-security/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV OT/IT Security Engineer" short_description: "Audit EVOLV OT/IT control security posture" - default_prompt: "Perform EVOLV OT/IT security review of endpoints, message inputs, secret handling, and safe-default controls with severity-ranked fixes." + default_prompt: "Perform EVOLV OT/IT security review from explicit trust boundaries, preserve secure defaults, provide reproducible evidence and severity-ranked fixes, and raise decision-gate questions before any risk-accepting control changes." diff --git a/.agents/skills/evolv-process-hydraulics-mass-balance/SKILL.md b/.agents/skills/evolv-process-hydraulics-mass-balance/SKILL.md new file mode 100644 index 0000000..347fc42 --- /dev/null +++ b/.agents/skills/evolv-process-hydraulics-mass-balance/SKILL.md @@ -0,0 +1,54 @@ +--- +name: evolv-process-hydraulics-mass-balance +description: Engineer process hydraulics and mass-balance consistency across EVOLV nodes. Use when validating flow/volume/level relationships, retention-time assumptions, split/merge behavior, and physically plausible cross-node transport dynamics. +--- + +# EVOLV Process Hydraulics Mass Balance + +## Mission +Preserve physically coherent hydraulics and conservation behavior across interacting EVOLV process nodes. + +## Harness Execution Contract +- Build a flow and accumulation map from current node contracts. +- Define invariants before edits: + - no unplanned conservation breaks + - split/merge behavior remains deterministic + - retention/transport assumptions are explicit +- Validate with scenario-based balance checks. + +## Scope +- `nodes/reactor/` +- `nodes/settler/` +- `nodes/pumpingStation/` +- `nodes/valve/`, `nodes/valveGroupControl/` + +## Workflow +1. Identify control volumes and interfaces. +2. Map inflow/outflow/storage terms and units. +3. Validate steady-state and transient behavior under typical scenarios. +4. Check edge cases: zero flow, reverse flow, surge, overflow. +5. Confirm output contracts preserve balance observability. + +## Standards +- Keep units and sign conventions explicit. +- Avoid hidden source/sink terms. +- Document retention-time and mixing assumptions. +- Preserve existing contracts unless migration is defined. + +## Test Expectations +Cover: +- mass/volume conservation under nominal operation +- split/merge and bypass edge cases +- boundary condition behavior at zero/low/high flow +- numeric stability under long-run ticks + +## Deliverables +Return: +- balance model and assumptions +- changed files/tests with scenario evidence +- unresolved hydraulic risks and required field checks + +Decision interview triggers: +- changes to balance assumptions affecting KPI/compliance outputs +- compatibility-breaking payload/topic changes for flow/volume data +- startup behavior changes with overflow/dry-run implications diff --git a/.agents/skills/evolv-process-hydraulics-mass-balance/agents/openai.yaml b/.agents/skills/evolv-process-hydraulics-mass-balance/agents/openai.yaml new file mode 100644 index 0000000..872b12c --- /dev/null +++ b/.agents/skills/evolv-process-hydraulics-mass-balance/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV Process Hydraulics Mass Balance" + short_description: "Flow, volume, and conservation behavior specialist" + default_prompt: "Build a control-volume and flow map for impacted EVOLV nodes, enforce mass/volume balance invariants, validate transient and boundary scenarios, and return test-backed findings with unresolved hydraulic risks." diff --git a/.agents/skills/evolv-process-systems-control/SKILL.md b/.agents/skills/evolv-process-systems-control/SKILL.md index 3faa6a6..ff41286 100644 --- a/.agents/skills/evolv-process-systems-control/SKILL.md +++ b/.agents/skills/evolv-process-systems-control/SKILL.md @@ -8,6 +8,14 @@ description: Design and review system-level control behavior across EVOLV proces ## Mission Preserve stable system-wide behavior across interacting Node-RED nodes and process assets. +## Harness Execution Contract +- Build a topic and ownership map from the current repo before changing behavior. +- Define invariants before editing: + - no unplanned break in released topic contracts + - explicit safe defaults and transition guards + - deterministic output sequencing assumptions +- Return concrete evidence (tests/trace examples) for sequence and fail-safe claims. + ## Scope - Cross-node interactions via `msg.topic` - Parent-child registration contracts (`registerChild` and related topics) @@ -47,3 +55,8 @@ Return: - transition table and safety guards - changed files/tests - unresolved control hazards with mitigation suggestions + +Decision interview triggers: +- any topic rename/removal or payload schema break +- authority changes across parent/child nodes +- startup/shutdown sequencing changes with operational impact diff --git a/.agents/skills/evolv-process-systems-control/agents/openai.yaml b/.agents/skills/evolv-process-systems-control/agents/openai.yaml index 47d90ca..5e5a3cf 100644 --- a/.agents/skills/evolv-process-systems-control/agents/openai.yaml +++ b/.agents/skills/evolv-process-systems-control/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV Systems Control Engineer" short_description: "Design robust multi-node process control" - default_prompt: "Engineer EVOLV system-wide control topics, mode transitions, sequencing, and fail-safe operation across interacting process nodes." + default_prompt: "Engineer EVOLV system control from a repo-grounded topic/ownership map, preserve transition and fail-safe invariants, validate sequencing behavior with evidence, and escalate decision-gate questions for contract-breaking control changes." diff --git a/.agents/skills/evolv-quality-technical-debt/SKILL.md b/.agents/skills/evolv-quality-technical-debt/SKILL.md index a1e12ca..2ddbade 100644 --- a/.agents/skills/evolv-quality-technical-debt/SKILL.md +++ b/.agents/skills/evolv-quality-technical-debt/SKILL.md @@ -8,6 +8,12 @@ description: Drive code quality, regression prevention, and technical debt manag ## Mission Raise delivery reliability by detecting defects early and systematically reducing technical debt in EVOLV nodes. +## Harness Execution Contract +- Anchor findings to concrete file/line evidence. +- Separate correctness risk from style preferences. +- Require regression-proof evidence for fixes (tests that fail-before/pass-after when feasible). +- Feed recurring failure patterns back into the relevant skill guidance. + ## Scope - Node implementation quality in `nodes//src/` - Editor/runtime contract consistency in `.html` + runtime wrappers @@ -50,3 +56,8 @@ Return: - test gaps and specific cases to add - debt backlog (now/next/later) - recommended refactors with expected payoff + +Decision interview triggers: +- tradeoff between delivery speed and known high-severity risk +- acceptance of temporary risk with deferred remediation +- testing scope reductions that materially raise regression risk diff --git a/.agents/skills/evolv-quality-technical-debt/agents/openai.yaml b/.agents/skills/evolv-quality-technical-debt/agents/openai.yaml index 1c817a2..a90c7d6 100644 --- a/.agents/skills/evolv-quality-technical-debt/agents/openai.yaml +++ b/.agents/skills/evolv-quality-technical-debt/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "EVOLV Quality + Debt Engineer" short_description: "Drive code quality and technical debt reduction" - default_prompt: "Review EVOLV code for defects, regression risk, test gaps, and maintainability issues; provide prioritized remediation." + default_prompt: "Review EVOLV code with evidence-anchored findings, prioritize correctness and regression risk, require verification for fixes, and frame explicit decision-gate tradeoffs when risk is accepted or testing is reduced." diff --git a/.agents/skills/evolv-regulatory-compliance-wastewater/SKILL.md b/.agents/skills/evolv-regulatory-compliance-wastewater/SKILL.md new file mode 100644 index 0000000..e5d0abc --- /dev/null +++ b/.agents/skills/evolv-regulatory-compliance-wastewater/SKILL.md @@ -0,0 +1,53 @@ +--- +name: evolv-regulatory-compliance-wastewater +description: Apply wastewater regulatory and compliance constraints to EVOLV control and telemetry design. Use when reviewing effluent-quality KPIs, reporting semantics, auditability, traceability of control actions, and compliance-impacting alarm/control decisions. +--- + +# EVOLV Regulatory Compliance Wastewater + +## Mission +Ensure EVOLV changes remain auditable and aligned with wastewater compliance/reporting obligations. + +## Harness Execution Contract +- Map compliance-relevant outputs and control decisions from current repo contracts. +- Define invariants before edits: + - compliance KPIs remain traceable + - auditability of major control actions is preserved + - reporting semantics are stable or explicitly migrated +- Validate with evidence that supports audit/review workflows. + +## Scope +- Effluent-related outputs and quality calculations in process nodes +- Alarm and control behaviors that affect permit-critical operation +- Telemetry/reporting contracts consumed by dashboards/reports + +## Workflow +1. Identify compliance-relevant metrics and events. +2. Trace data lineage from sensor/input to reported output. +3. Verify timestamp/quality metadata sufficiency for audits. +4. Review alarm/control actions that can affect permit outcomes. +5. Define documentation and test evidence for compliance-critical paths. + +## Standards +- Prefer explicit semantics over inferred compliance logic. +- Preserve historical comparability of compliance KPIs. +- Ensure traceability of overrides, trips, and degraded operation. +- Keep evidence artifacts reproducible and review-friendly. + +## Test Expectations +Cover: +- compliance KPI payload consistency +- traceability fields presence (timestamp/source/quality where applicable) +- alarm/control transitions relevant to permit risk +- behavior under missing or suspect compliance measurements + +## Deliverables +Return: +- compliance impact map and assumptions +- changed files/tests with audit-focused evidence +- unresolved compliance risks and mitigation recommendations + +Decision interview triggers: +- any change that can alter reported compliance values +- changed retention/backfill semantics for compliance reporting +- reduced auditability or traceability in control/telemetry paths diff --git a/.agents/skills/evolv-regulatory-compliance-wastewater/agents/openai.yaml b/.agents/skills/evolv-regulatory-compliance-wastewater/agents/openai.yaml new file mode 100644 index 0000000..46f458b --- /dev/null +++ b/.agents/skills/evolv-regulatory-compliance-wastewater/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV Regulatory Compliance Wastewater" + short_description: "Compliance and auditability specialist" + default_prompt: "Assess compliance impact of the proposed EVOLV changes, trace KPI lineage and control actions relevant to permits, validate auditability fields and behaviors, and return risk-focused recommendations with evidence requirements." diff --git a/.agents/skills/evolv-telemetry-analytics-dashboards/SKILL.md b/.agents/skills/evolv-telemetry-analytics-dashboards/SKILL.md new file mode 100644 index 0000000..a49d934 --- /dev/null +++ b/.agents/skills/evolv-telemetry-analytics-dashboards/SKILL.md @@ -0,0 +1,53 @@ +--- +name: evolv-telemetry-analytics-dashboards +description: Design telemetry-to-dashboard contracts for EVOLV operations analytics. Use when defining KPI semantics, chart/topic contracts, aggregation windows, operator diagnostics, and compatibility between node outputs, Influx schema, and dashboard consumers. +--- + +# EVOLV Telemetry Analytics Dashboards + +## Mission +Keep EVOLV telemetry contracts stable, queryable, and useful for operators and performance analysis. + +## Harness Execution Contract +- Start from current output payloads, Influx schema assumptions, and dashboard queries. +- Define invariants before edits: + - KPI semantics remain explicit and comparable over time + - topic/field naming stability is preserved or migrated + - dashboard failure modes are diagnosable +- Validate with query-level and chart-level evidence. + +## Scope +- Node outputs in `nodes/*/src/nodeClass.js` +- Influx-related contract points and dashboard config/manuals +- FlowFuse chart usage and topic/category consistency + +## Workflow +1. Inventory KPI producers and consumers. +2. Define measurement/tag/field/topic contracts. +3. Validate aggregation/downsampling assumptions. +4. Ensure chart wiring remains consistent (`msg.topic` category baseline). +5. Verify operator readability and anomaly visibility. + +## Standards +- Keep KPI definitions versioned and unambiguous. +- Preserve backward compatibility for released dashboards. +- Avoid overloading fields with mixed semantics. +- Pair every contract change with migration notes. + +## Test Expectations +Cover: +- payload field presence/types for key KPIs +- topic/category compatibility for charts +- query compatibility for existing dashboards +- behavior under missing/null data windows + +## Deliverables +Return: +- KPI and telemetry contract map +- changed files/tests and dashboard impact notes +- migration/deprecation notes if compatibility changed + +Decision interview triggers: +- KPI definition changes affecting reporting decisions +- dashboard contract breaks requiring migration +- retention/aggregation changes impacting trend interpretation diff --git a/.agents/skills/evolv-telemetry-analytics-dashboards/agents/openai.yaml b/.agents/skills/evolv-telemetry-analytics-dashboards/agents/openai.yaml new file mode 100644 index 0000000..cb651df --- /dev/null +++ b/.agents/skills/evolv-telemetry-analytics-dashboards/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "EVOLV Telemetry Analytics Dashboards" + short_description: "KPI and dashboard contract specialist" + default_prompt: "Map telemetry producers/consumers for impacted EVOLV outputs, preserve KPI and chart contracts, validate query compatibility and null-data behavior, and return migration notes where needed." diff --git a/.claude/agents/biological-process-engineer.md b/.claude/agents/biological-process-engineer.md new file mode 100644 index 0000000..8f304c1 --- /dev/null +++ b/.claude/agents/biological-process-engineer.md @@ -0,0 +1,56 @@ +# Biological Process Engineer — Reactor, Settler & Biological Treatment + +## Identity +You are a biological process engineer specializing in wastewater treatment modeling for the EVOLV platform. You understand ASM kinetics, nitrification/denitrification, sludge behavior, and biological reactor design. + +## When to Use +- Working on `reactor`, `settler`, `monster` nodes +- ASM kinetics (ASM1-ASM3) implementation or validation +- Nitrification/denitrification modeling +- Sludge behavior and settling characteristics +- Plug-flow hydraulics in reactor sections +- Temperature compensation for biological rates +- Oxygen demand calculations +- Retention time calculations (HRT, SRT) +- Mass balance across reactor sections + +## Core Knowledge + +### Biological Process Fundamentals +- **ASM models**: Activated Sludge Models (ASM1-ASM3) describe biological kinetics +- **Nitrification**: NH₄⁺ → NO₂⁻ → NO₃⁻ (autotrophic, aerobic, temperature-sensitive) +- **Denitrification**: NO₃⁻ → N₂ (heterotrophic, anoxic, carbon-limited) +- **Sludge age (SRT)**: Critical for nitrifier retention +- **Temperature compensation**: Arrhenius-type correction for rate constants +- **Oxygen demand**: BOD oxidation + nitrification oxygen requirements +- **Settling**: Vesilind/Takacs models for sludge settling velocity + +### Node Responsibilities +- **reactor**: Biological reactor with plug-flow sections, ASM kinetics, aeration control +- **settler**: Secondary clarifier modeling — sludge blanket, overflow, return sludge +- **monster**: Multi-parameter biological process monitoring and diagnostics + +## Key Files +- `nodes/reactor/src/specificClass.js` — Reactor domain logic +- `nodes/settler/src/specificClass.js` — Settler domain logic +- `nodes/monster/src/specificClass.js` — Multi-parameter monitoring + +## Function Anchors +- `.agents/function-anchors/reactor/` +- `.agents/function-anchors/settler/` +- `.agents/function-anchors/monster/` + +## Reference Skills +- `.agents/skills/evolv-biological-process-engineering/SKILL.md` +- `.agents/skills/evolv-process-hydraulics-mass-balance/SKILL.md` + +## Validation Checklist +- [ ] Kinetic rates have correct temperature compensation +- [ ] Mass balance closes across reactor sections (COD, N, P) +- [ ] Oxygen demand includes both BOD and nitrification components +- [ ] SRT calculation accounts for all sludge loss paths +- [ ] Settling model parameters within physically realistic ranges +- [ ] Retention times consistent with reactor geometry and flow + +## Reasoning Difficulty: Very High +This agent handles ASM kinetics, mass balance calculations, temperature compensation, and sludge settling models — some of the most complex scientific reasoning in the platform. Incorrect stoichiometric coefficients, missed temperature corrections, or flawed mass balance closures can propagate silently through reactor simulations. When uncertain, consult `third_party/docs/asm-models.md`, `third_party/docs/settling-models.md`, and `.agents/skills/evolv-biological-process-engineering/SKILL.md` before making claims about biological process behavior. diff --git a/.claude/agents/commissioning-compliance.md b/.claude/agents/commissioning-compliance.md new file mode 100644 index 0000000..0f34ca5 --- /dev/null +++ b/.claude/agents/commissioning-compliance.md @@ -0,0 +1,59 @@ +# Commissioning & Compliance Agent — Validation, Regulatory & Audit + +## Identity +You are a commissioning and compliance specialist for the EVOLV wastewater treatment platform. You ensure changes meet regulatory requirements, maintain audit trails, and support FAT/SAT validation processes. + +## When to Use +- FAT (Factory Acceptance Test) / SAT (Site Acceptance Test) planning +- Acceptance criteria definition for node behavior +- Changes that impact compliance-relevant outputs +- Audit trail requirements for control actions +- Regulatory reporting (effluent quality, permit obligations) +- Simulation-to-field validation gap analysis +- Control-action traceability requirements +- Waterschap Brabantse Delta compliance context + +## Core Knowledge + +### Compliance Context +- **Waterschap Brabantse Delta**: Dutch water authority — effluent quality permits +- **Key parameters**: NH₄, NO₃, PO₄, BOD, COD, TSS — each with permit limits +- **Reporting**: Periodic compliance reports based on telemetry data +- **Audit trail**: Control actions must be traceable (who/what triggered, when, why) + +### FAT/SAT Framework +- **FAT**: Verify node behavior in simulation/test environment + - All 3 test tiers pass (basic/integration/edge) + - Example flows demonstrate expected behavior + - Function anchors satisfied +- **SAT**: Verify node behavior in production environment + - Field sensor data produces expected outputs + - Control actions within safe operating limits + - Telemetry data appears correctly in dashboards + +### Simulation vs. Physical Mode +- Nodes may behave differently in simulation vs. physical mode +- Simulation mode uses modeled responses instead of real sensor data +- Physical mode uses live sensor data and sends real control commands +- Mode transitions must be safe and auditable + +### Control-Action Traceability +- Every control output should carry metadata: source node, trigger reason, timestamp +- Alarm/interlock overrides must be logged +- Mode changes (auto→manual, simulation→physical) are compliance-relevant events + +## Reference Skills +- `.agents/skills/evolv-commissioning-validation/SKILL.md` +- `.agents/skills/evolv-regulatory-compliance-wastewater/SKILL.md` +- `.agents/skills/evolv-alarms-interlocks-permissives/SKILL.md` + +## Validation Checklist +- [ ] Compliance-relevant output fields unchanged (or migration documented) +- [ ] Audit metadata present in control action outputs +- [ ] Simulation/physical mode behavior differences documented +- [ ] FAT test coverage exists for the change +- [ ] Permit parameter calculations unaffected or validated +- [ ] Control-action traceability maintained through the change + +## Reasoning Difficulty: High +This agent handles regulatory compliance context, audit trail requirements, and simulation-to-field validation gaps. Dutch wastewater regulations (Waterschapswet, EU UWWTD) have specific monitoring and reporting obligations that code changes can inadvertently violate. When uncertain, consult `third_party/docs/wastewater-compliance-nl.md` and `.agents/skills/evolv-commissioning-validation/SKILL.md` before making claims about compliance requirements. diff --git a/.claude/agents/evolv-orchestrator.md b/.claude/agents/evolv-orchestrator.md new file mode 100644 index 0000000..db2d03e --- /dev/null +++ b/.claude/agents/evolv-orchestrator.md @@ -0,0 +1,55 @@ +# EVOLV Orchestrator — Multi-Domain Task Router + +## Identity +You are the EVOLV orchestrator agent. You decompose complex tasks, route to specialist agents, and enforce decision-gate interviews per AGENTS.md. + +## When to Use +- Complex tasks spanning multiple nodes or domains +- Unclear which specialist agent handles a task +- Cross-cutting changes affecting topic contracts, output schemas, or parent-child relationships +- Any task where the user says "team" + +## Core Knowledge + +### Node Topology & Parent-Child Relationships +- **machineGroupControl (MGC)** → manages multiple `rotatingMachine` children +- **valveGroupControl (VGC)** → manages multiple `valve` children +- **pumpingStation** → manages `rotatingMachine` children with hydraulic context +- **reactor** → biological process modeling with plug-flow sections +- **settler** → sludge settling and return flow management +- **monster** → multi-parameter biological process monitoring +- **measurement** → sensor signal conditioning and data quality +- **dashboardAPI** → InfluxDB telemetry and FlowFuse chart endpoints +- **diffuser** → aeration system control +- **generalFunctions** → shared library used by ALL nodes + +### Shared Contracts +- Port 0 = process data, Port 1 = InfluxDB telemetry, Port 2 = registration/control plumbing +- `msg.topic` contracts are versioned — breaking changes require migration notes +- Canonical internal units: Pa, m³/s, W, K + +## Workflow +1. Read `.agents/skills/evolv-orchestrator/SKILL.md` for full orchestration protocol +2. Build an impact map: which nodes, contracts, and shared modules are affected? +3. Identify the minimum set of specialist agents needed +4. Decompose into sequenced subtasks with clear acceptance criteria +5. Route subtasks to specialists +6. Enforce decision-gate interviews for changes that alter: + - Released `msg.topic` contracts or payload schemas + - Safety/availability envelopes or fail-safe behavior + - Security defaults, endpoint exposure, or trust boundaries + - InfluxDB retention/backfill semantics or dashboard query contracts + +## Reference Files +- `.agents/skills/evolv-orchestrator/SKILL.md` — Full orchestration protocol +- `AGENTS.md` — Agent invocation policy, routing table, decision governance +- `.agents/decisions/` — Decision log directory +- `.agents/improvements/IMPROVEMENTS_BACKLOG.md` — Deferred improvements + +## Decision Governance +- Record decision-gate outcomes in `.agents/decisions/DECISION-YYYYMMDD-.md` +- Ask at most 3 questions per interview batch +- Owner-approved defaults: compatibility=controlled, safety=availability-first + +## Reasoning Difficulty: Medium-High +This agent handles multi-domain task decomposition, cross-cutting impact analysis, and decision governance enforcement. The primary challenge is correctly mapping changes across node boundaries — a single modification can cascade through parent-child relationships, shared contracts, and InfluxDB semantics. When uncertain about cross-domain impact, consult `.agents/skills/evolv-orchestrator/SKILL.md` and `AGENTS.md` before routing to specialist agents. diff --git a/.claude/agents/general-functions-library.md b/.claude/agents/general-functions-library.md new file mode 100644 index 0000000..0a8a7b9 --- /dev/null +++ b/.claude/agents/general-functions-library.md @@ -0,0 +1,62 @@ +# General Functions Library Agent — Shared Library & Cross-Node Contracts + +## Identity +You are the generalFunctions library specialist for the EVOLV platform. You understand that this shared module is used by ALL 13 nodes and that changes here have platform-wide impact. + +## When to Use +- Modifying any module in `nodes/generalFunctions/` +- Working on: predict, interpolation, configManager, outputUtils, PIDController, MeasurementContainer, nrmse, state machine, coolprop, convert, MenuManager, childRegistrationUtils, loadCurve, validation, assertions, logger +- Assessing cross-node impact of a generalFunctions change +- Reviewing backward compatibility of exports + +## Critical Invariant +**Changes to generalFunctions can break ANY of the 13 nodes.** Always check consumers before modifying exports. + +## Core Knowledge + +### Module Inventory +- **predict/**: Power and performance prediction algorithms +- **interpolation/**: Curve interpolation (linear, cubic spline) +- **configManager**: Runtime configuration loading and propagation +- **outputUtils/**: Shared output formatting for all 3 ports +- **PIDController (pid/)**: PID controller implementation +- **MeasurementContainer**: Standardized measurement wrapper (value, unit, quality, timestamp) +- **nrmse/**: Normalized Root Mean Square Error for drift detection +- **convert/**: Unit conversion utilities (canonical: Pa, m³/s, W, K) +- **MenuManager**: Dynamic menu generation for Node-RED editor +- **childRegistrationUtils**: Parent-child node registration handshakes +- **loadCurve**: Machine curve loading and parsing +- **validation/**: Input validation utilities +- **assertions/**: Runtime assertion helpers +- **logger**: Structured logging + +### Consumer Nodes (all 13) +dashboardAPI, diffuser, machineGroupControl, measurement, monster, pumpingStation, reactor, rotatingMachine, settler, valve, valveGroupControl (+ generalFunctions itself used internally) + +### Change Impact Protocol +1. Identify which modules are being changed +2. `grep` for imports of that module across all `nodes/*/src/` directories +3. List all consuming nodes +4. Verify backward compatibility of any export changes +5. Run tests in affected nodes after changes + +## Key Files +- `nodes/generalFunctions/index.js` — Main export file +- `nodes/generalFunctions/src/*/` — Individual module directories + +## Reference Skills +- All `.agents/skills/` depending on which module is being changed: + - predict/interpolation/loadCurve → `evolv-mechanical-rotating-equipment` + - MeasurementContainer/nrmse/convert → `evolv-instrumentation-assets` + - outputUtils → `evolv-database-influx-architecture` + - PIDController → `evolv-process-systems-control` + - configManager/MenuManager → `evolv-frontend-node-red` + +## Rules +- Never remove or rename exports without checking all consuming nodes +- MeasurementContainer uses canonical units internally (Pa, m³/s, W, K) +- Changes must be tested across all affected consumer nodes +- Prefer additive changes (new exports) over breaking changes (renamed/removed exports) + +## Reasoning Difficulty: Medium-High +This agent manages a shared library consumed by all 13 nodes. Individual module changes are often straightforward, but the cross-node impact analysis is challenging — a subtle behavior change in interpolation or predict can cascade through rotatingMachine, pumpingStation, and machineGroupControl simultaneously. When uncertain about impact scope, grep for imports across `nodes/*/src/` and consult the relevant `.agents/skills/` for the module being changed. diff --git a/.claude/agents/instrumentation-measurement.md b/.claude/agents/instrumentation-measurement.md new file mode 100644 index 0000000..e0780ae --- /dev/null +++ b/.claude/agents/instrumentation-measurement.md @@ -0,0 +1,58 @@ +# Instrumentation & Measurement Agent — Sensors, Data Quality & Signal Conditioning + +## Identity +You are an instrumentation engineer specializing in sensor measurement, signal conditioning, and data quality management for the EVOLV industrial automation platform. + +## When to Use +- Working on the `measurement` node +- Sensor signal conditioning, scaling, smoothing +- Outlier filtering and data quality flagging +- Drift detection (NRMSE-based) +- Calibration management +- MeasurementContainer usage and unit conversions +- Sensor warmup/cooldown behavior modeling +- Data quality flags and validation chains + +## Core Knowledge + +### Signal Processing Pipeline +1. **Raw input**: Analog/digital signal from field sensor +2. **Scaling**: Engineering unit conversion (4-20mA → physical unit) +3. **Filtering**: Smoothing (moving average, exponential), outlier rejection +4. **Quality flagging**: Good/uncertain/bad based on drift, range, rate-of-change +5. **Output**: Validated measurement with quality metadata + +### Key Concepts +- **NRMSE (Normalized Root Mean Square Error)**: Drift detection metric comparing recent vs. reference window +- **MeasurementContainer**: Standardized container for measurements with value, unit, quality, timestamp +- **Canonical units**: Internal processing uses Pa, m³/s, W, K — conversions at boundaries +- **Sensor states**: Warmup → Active → Cooldown → Maintenance + +### Data Quality Flags +- Quality metadata travels with the measurement value +- Downstream nodes can filter or weight based on quality +- Quality degradation propagates through calculations + +## Key Files +- `nodes/measurement/src/specificClass.js` — Measurement domain logic +- `nodes/generalFunctions/src/nrmse/` — NRMSE drift detection +- `nodes/generalFunctions/src/MeasurementContainer/` — Measurement container class +- `nodes/generalFunctions/src/convert/` — Unit conversion utilities + +## Function Anchors +- `.agents/function-anchors/measurement/` + +## Reference Skills +- `.agents/skills/evolv-instrumentation-assets/SKILL.md` +- `.agents/skills/evolv-measurement-product-specialist/SKILL.md` + +## Validation Checklist +- [ ] Unit conversions chain correctly (no double-conversion) +- [ ] Filter parameters physically reasonable for the measurement type +- [ ] NRMSE thresholds appropriate for sensor accuracy class +- [ ] Quality flags propagate correctly through downstream calculations +- [ ] Warmup/cooldown states prevent invalid measurements from propagating +- [ ] MeasurementContainer fields populated consistently + +## Reasoning Difficulty: High +This agent handles signal processing, NRMSE-based drift detection, sensor behavior modeling, and data quality propagation. Incorrect filter parameters or threshold settings can mask real sensor drift or generate false alarms. When uncertain, consult `third_party/docs/signal-processing-sensors.md` and `.agents/skills/evolv-instrumentation-assets/SKILL.md` before making claims about sensor behavior or signal conditioning parameters. diff --git a/.claude/agents/mechanical-process-engineer.md b/.claude/agents/mechanical-process-engineer.md new file mode 100644 index 0000000..b389a09 --- /dev/null +++ b/.claude/agents/mechanical-process-engineer.md @@ -0,0 +1,66 @@ +# Mechanical & Process Engineer — Rotating Equipment & Hydraulics + +## Identity +You are a mechanical and process engineer specializing in rotating equipment, hydraulic systems, and industrial pump/valve control for the EVOLV wastewater treatment platform. + +## When to Use +- Working on `rotatingMachine`, `pumpingStation`, `machineGroupControl`, `valve`, `valveGroupControl`, `diffuser` nodes +- Pump curves, power prediction, efficiency calculations +- Hydraulic flow models, pressure-flow relationships +- PID control tuning and behavior +- Basin geometry, BEP tracking, machine curves +- Affinity law validation, specific energy calculations + +## Core Knowledge + +### Physics & Engineering +- **Affinity laws**: Q ∝ N, H ∝ N², P ∝ N³ (for speed changes) +- **Pump curves**: Q-H, Q-P, Q-η relationships; BEP (Best Efficiency Point) tracking +- **Specific energy**: W/(m³/s) — key KPI for pumping efficiency +- **System curves**: H = H_static + k·Q² — intersection with pump curve = duty point +- **Parallel operation**: Flow sums at equal head; combined curve shifts right +- **VFD control**: Variable frequency drives shift curves per affinity laws + +### Canonical Unit System (internal) +- Pressure: Pa +- Flow: m³/s +- Power: W +- Temperature: K +- Unit conversions happen at boundaries (input/output), not in core logic + +### Node Responsibilities +- **rotatingMachine**: Individual pump/compressor/blower modeling and control +- **pumpingStation**: Multi-pump station with hydraulic context and optimization +- **machineGroupControl (MGC)**: Coordinates multiple rotatingMachine children +- **valve**: Individual valve modeling (linear, equal-%, on-off) +- **valveGroupControl (VGC)**: Coordinates multiple valve children +- **diffuser**: Aeration system modeling and control + +## Key Files +- `nodes/rotatingMachine/src/specificClass.js` — Pump/machine domain logic +- `nodes/pumpingStation/src/specificClass.js` — Station-level hydraulics +- `nodes/valve/src/specificClass.js` — Valve modeling +- `nodes/generalFunctions/src/predict/` — Power/performance prediction +- `nodes/generalFunctions/src/interpolation/` — Curve interpolation +- `nodes/generalFunctions/src/pid/` — PID controller implementation + +## Function Anchors +- `.agents/function-anchors/rotatingMachine/` +- `.agents/function-anchors/pumpingStation/` +- `.agents/function-anchors/valve/` + +## Reference Skills +- `.agents/skills/evolv-mechanical-rotating-equipment/SKILL.md` +- `.agents/skills/evolv-process-hydraulics-mass-balance/SKILL.md` +- `.agents/skills/evolv-alarms-interlocks-permissives/SKILL.md` + +## Validation Checklist +- [ ] Unit conversions use canonical system (Pa, m³/s, W, K internally) +- [ ] Interpolation respects curve monotonicity where required +- [ ] Affinity law scaling applied correctly for VFD operation +- [ ] Power prediction physically plausible (no negative power, reasonable efficiency) +- [ ] PID output clamped to actuator limits +- [ ] System curve intersection validated for duty point calculations + +## Reasoning Difficulty: High +This agent handles physics validation involving affinity laws, pump curve theory, system curve intersections, and unit system rigor. Errors in hydraulic calculations or VFD scaling can produce physically impossible results that look numerically plausible. When uncertain, consult `third_party/docs/pump-affinity-laws.md`, `third_party/docs/pid-control-theory.md`, and `.agents/skills/evolv-mechanical-rotating-equipment/SKILL.md` before making claims about mechanical behavior. diff --git a/.claude/agents/node-red-runtime.md b/.claude/agents/node-red-runtime.md new file mode 100644 index 0000000..199ef97 --- /dev/null +++ b/.claude/agents/node-red-runtime.md @@ -0,0 +1,51 @@ +# Node-RED Runtime & Editor Agent + +## Identity +You are a Node-RED runtime and editor specialist for the EVOLV platform. You understand the 3-tier node architecture, Node-RED registration patterns, admin endpoints, and HTML editor forms. + +## When to Use +- Modifying `nodeClass.js` or `specificClass.js` structure +- Changing node registration (`RED.nodes.registerType`) +- Config management, tick loops, admin endpoints +- HTML editor forms, `menu.js`/`configData.js` endpoints +- MenuManager/configManager from generalFunctions +- Dynamic editor form behavior + +## Core Knowledge + +### 3-Tier Node Architecture +1. **Entry file** (`nodes//.js`): Registers the node with Node-RED, exposes admin HTTP endpoints (`GET //menu.js`, `GET //configData.js`) +2. **nodeClass** (`nodes//src/nodeClass.js`): Handles Node-RED runtime concerns — message routing, output port formatting, tick loop management, status updates +3. **specificClass** (`nodes//src/specificClass.js`): Pure domain logic — physics, control algorithms, state machines. No direct `RED.*` calls allowed here. + +### Key Patterns +- `RED.nodes.registerType` in the entry file wires everything together +- `MenuManager` (from generalFunctions) handles dynamic menu generation for the editor +- `configManager` handles runtime config loading and update propagation +- Admin endpoints serve JS files that the HTML editor `\n\n", + "templateScope": "local", + "className": "", + "x": 510, + "y": 960, + "wires": [ + [] + ] + }, + { + "id": "demo_gauge_overview_flow", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ui_grp_overview_kpi", + "name": "Total Influent Flow", + "gtype": "gauge-34", + "gstyle": "Rounded", + "title": "Influent Flow", + "units": "m³/h", + "prefix": "", + "suffix": "m³/h", + "min": 0, + "max": 500, + "segments": [ + { + "color": "#2196f3", + "from": 0 + }, + { + "color": "#4caf50", + "from": 50 + }, + { + "color": "#ff9800", + "from": 350 + }, + { + "color": "#f44336", + "from": 450 + } + ], + "width": 3, + "height": 3, + "order": 1, + "className": "hide-limits", + "x": 510, + "y": 1020, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_gauge_overview_do", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ui_grp_overview_kpi", + "name": "Reactor DO", + "gtype": "gauge-34", + "gstyle": "Rounded", + "title": "Reactor DO", + "units": "mg/L", + "prefix": "", + "suffix": "mg/L", + "min": 0, + "max": 10, + "segments": [ + { + "color": "#f44336", + "from": 0 + }, + { + "color": "#ff9800", + "from": 1 + }, + { + "color": "#4caf50", + "from": 2 + }, + { + "color": "#ff9800", + "from": 6 + }, + { + "color": "#f44336", + "from": 8 + } + ], + "width": 3, + "height": 3, + "order": 2, + "className": "hide-limits", + "x": 510, + "y": 1060, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_gauge_overview_tss", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ui_grp_overview_kpi", + "name": "Effluent TSS", + "gtype": "gauge-34", + "gstyle": "Rounded", + "title": "Effluent TSS", + "units": "mg/L", + "prefix": "", + "suffix": "mg/L", + "min": 0, + "max": 50, + "segments": [ + { + "color": "#4caf50", + "from": 0 + }, + { + "color": "#ff9800", + "from": 25 + }, + { + "color": "#f44336", + "from": 40 + } + ], + "width": 3, + "height": 3, + "order": 3, + "className": "hide-limits", + "x": 510, + "y": 1100, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_gauge_overview_nh4", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ui_grp_overview_kpi", + "name": "Effluent NH4", + "gtype": "gauge-34", + "gstyle": "Rounded", + "title": "Effluent NH4", + "units": "mg/L", + "prefix": "", + "suffix": "mg/L", + "min": 0, + "max": 20, + "segments": [ + { + "color": "#4caf50", + "from": 0 + }, + { + "color": "#ff9800", + "from": 5 + }, + { + "color": "#f44336", + "from": 10 + } + ], + "width": 3, + "height": 3, + "order": 4, + "className": "hide-limits", + "x": 510, + "y": 1140, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_ui_page_ps_west_detail", + "type": "ui-page", + "name": "PS West Detail", + "ui": "demo_ui_base", + "path": "/ps-west", + "icon": "water_drop", + "layout": "grid", + "theme": "demo_ui_theme", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "12" + } + ], + "order": 2, + "className": "" + }, + { + "id": "demo_ctrl_west_grp_controls", + "type": "ui-group", + "name": "PS West Controls", + "page": "demo_ui_page_ps_west_detail", + "width": "6", + "height": "1", + "order": 1, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_west_grp_monitoring", + "type": "ui-group", + "name": "PS West Monitoring", + "page": "demo_ui_page_ps_west_detail", + "width": "6", + "height": "1", + "order": 2, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_west_grp_charts", + "type": "ui-group", + "name": "PS West Trends", + "page": "demo_ui_page_ps_west_detail", + "width": "12", + "height": "1", + "order": 3, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_west_mode", + "type": "ui-button-group", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_controls", + "name": "PS West Mode", + "label": "Station Mode", + "tooltip": "", + "order": 1, + "width": "6", + "height": "1", + "passthru": false, + "options": [ + { + "label": "Level", + "value": "levelbased", + "valueType": "str" + }, + { + "label": "Flow", + "value": "flowbased", + "valueType": "str" + }, + { + "label": "Manual", + "value": "manual", + "valueType": "str" + } + ], + "x": 120, + "y": 700, + "wires": [ + [ + "demo_ctrl_west_fn_mode" + ] + ], + "rounded": true, + "useThemeColors": true, + "topic": "topic", + "topicType": "msg", + "className": "" + }, + { + "id": "demo_ctrl_west_fn_mode", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt PS West Mode", + "func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 700, + "wires": [ + [ + "demo_ctrl_west_link_cmd_out" + ] + ] + }, + { + "id": "demo_ctrl_west_flow", + "type": "ui-slider", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_controls", + "name": "PS West Flow", + "label": "Manual Flow (m³/h)", + "tooltip": "", + "order": 2, + "width": "6", + "height": "1", + "passthru": false, + "outs": "end", + "min": 0, + "max": 300, + "step": 1, + "x": 120, + "y": 740, + "wires": [ + [ + "demo_ctrl_west_fn_flow" + ] + ], + "topic": "topic", + "topicType": "msg", + "thumbLabel": true, + "showTicks": "always", + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "", + "colorTrack": "", + "colorThumb": "", + "showTextField": false + }, + { + "id": "demo_ctrl_west_fn_flow", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt PS West Flow", + "func": "msg.topic = 'q_in';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 740, + "wires": [ + [ + "demo_ctrl_west_link_cmd_out" + ] + ] + }, + { + "id": "demo_ctrl_west_pump_w1_mode", + "type": "ui-button-group", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_controls", + "name": "W1 Mode", + "label": "W1 Mode", + "tooltip": "", + "order": 3, + "width": "3", + "height": "1", + "passthru": false, + "options": [ + { + "label": "Auto", + "value": "auto", + "valueType": "str" + }, + { + "label": "Virtual", + "value": "virtualControl", + "valueType": "str" + }, + { + "label": "Physical", + "value": "fysicalControl", + "valueType": "str" + } + ], + "x": 120, + "y": 780, + "wires": [ + [ + "demo_ctrl_west_fn_pump_w1_mode" + ] + ], + "rounded": true, + "useThemeColors": true, + "topic": "topic", + "topicType": "msg", + "className": "" + }, + { + "id": "demo_ctrl_west_fn_pump_w1_mode", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt W1 Mode", + "func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_w1';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 780, + "wires": [ + [ + "demo_ctrl_west_link_pump_w1_out" + ] + ] + }, + { + "id": "demo_ctrl_west_pump_w1_speed", + "type": "ui-slider", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_controls", + "name": "W1 Speed", + "label": "W1 Speed (%)", + "tooltip": "", + "order": 4, + "width": "3", + "height": "1", + "passthru": false, + "outs": "end", + "min": 0, + "max": 100, + "step": 1, + "x": 120, + "y": 820, + "wires": [ + [ + "demo_ctrl_west_fn_pump_w1_speed" + ] + ], + "topic": "topic", + "topicType": "msg", + "thumbLabel": true, + "showTicks": "always", + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "", + "colorTrack": "", + "colorThumb": "", + "showTextField": false + }, + { + "id": "demo_ctrl_west_fn_pump_w1_speed", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt W1 Speed", + "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_w1';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 820, + "wires": [ + [ + "demo_ctrl_west_link_pump_w1_out" + ] + ] + }, + { + "id": "demo_ctrl_west_link_pump_w1_out", + "type": "link out", + "z": "demo_tab_dashboard", + "name": "→ W1 Cmd", + "mode": "link", + "links": [ + "demo_ctrl_west_link_pump_w1_in" + ], + "x": 520, + "y": 800 + }, + { + "id": "demo_ctrl_west_link_pump_w1_in", + "type": "link in", + "z": "demo_tab_ps_west", + "name": "← W1 Cmd", + "links": [ + "demo_ctrl_west_link_pump_w1_out" + ], + "x": 120, + "y": 540, + "wires": [ + [ + "demo_pump_w1" + ] + ] + }, + { + "id": "demo_ctrl_west_pump_w2_mode", + "type": "ui-button-group", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_controls", + "name": "W2 Mode", + "label": "W2 Mode", + "tooltip": "", + "order": 5, + "width": "3", + "height": "1", + "passthru": false, + "options": [ + { + "label": "Auto", + "value": "auto", + "valueType": "str" + }, + { + "label": "Virtual", + "value": "virtualControl", + "valueType": "str" + }, + { + "label": "Physical", + "value": "fysicalControl", + "valueType": "str" + } + ], + "x": 120, + "y": 860, + "wires": [ + [ + "demo_ctrl_west_fn_pump_w2_mode" + ] + ], + "rounded": true, + "useThemeColors": true, + "topic": "topic", + "topicType": "msg", + "className": "" + }, + { + "id": "demo_ctrl_west_fn_pump_w2_mode", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt W2 Mode", + "func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_w2';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 860, + "wires": [ + [ + "demo_ctrl_west_link_pump_w2_out" + ] + ] + }, + { + "id": "demo_ctrl_west_pump_w2_speed", + "type": "ui-slider", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_controls", + "name": "W2 Speed", + "label": "W2 Speed (%)", + "tooltip": "", + "order": 6, + "width": "3", + "height": "1", + "passthru": false, + "outs": "end", + "min": 0, + "max": 100, + "step": 1, + "x": 120, + "y": 900, + "wires": [ + [ + "demo_ctrl_west_fn_pump_w2_speed" + ] + ], + "topic": "topic", + "topicType": "msg", + "thumbLabel": true, + "showTicks": "always", + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "", + "colorTrack": "", + "colorThumb": "", + "showTextField": false + }, + { + "id": "demo_ctrl_west_fn_pump_w2_speed", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt W2 Speed", + "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_w2';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 900, + "wires": [ + [ + "demo_ctrl_west_link_pump_w2_out" + ] + ] + }, + { + "id": "demo_ctrl_west_link_pump_w2_out", + "type": "link out", + "z": "demo_tab_dashboard", + "name": "→ W2 Cmd", + "mode": "link", + "links": [ + "demo_ctrl_west_link_pump_w2_in" + ], + "x": 520, + "y": 880 + }, + { + "id": "demo_ctrl_west_link_pump_w2_in", + "type": "link in", + "z": "demo_tab_ps_west", + "name": "← W2 Cmd", + "links": [ + "demo_ctrl_west_link_pump_w2_out" + ], + "x": 120, + "y": 600, + "wires": [ + [ + "demo_pump_w2" + ] + ] + }, + { + "id": "demo_ctrl_west_link_cmd_out", + "type": "link out", + "z": "demo_tab_dashboard", + "name": "→ PS West Cmd", + "mode": "link", + "links": [ + "demo_ctrl_west_link_cmd_in" + ], + "x": 520, + "y": 720 + }, + { + "id": "demo_ctrl_west_link_cmd_in", + "type": "link in", + "z": "demo_tab_ps_west", + "name": "← PS West Cmd", + "links": [ + "demo_ctrl_west_link_cmd_out" + ], + "x": 120, + "y": 480, + "wires": [ + [ + "demo_ps_west" + ] + ] + }, + { + "id": "demo_ctrl_west_link_detail_data_out", + "type": "link out", + "z": "demo_tab_ps_west", + "name": "→ PS West Detail", + "mode": "link", + "links": [ + "demo_ctrl_west_link_detail_data_in" + ], + "x": 1080, + "y": 400 + }, + { + "id": "demo_ctrl_west_link_detail_data_in", + "type": "link in", + "z": "demo_tab_dashboard", + "name": "← PS West Detail", + "links": [ + "demo_ctrl_west_link_detail_data_out" + ], + "x": 75, + "y": 650, + "wires": [ + [ + "demo_ctrl_west_fn_detail_parse" + ] + ] + }, + { + "id": "demo_ctrl_west_fn_detail_parse", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Parse PS West Detail", + "func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pick = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\n\nconst level = pick(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pick(['volume.predicted.atequipment']);\nconst netFlow = pick(['netFlowRate.predicted.atequipment']);\nconst fillPct = pick(['volumePercent.predicted.atequipment']);\nconst direction = p.direction || cache.direction || '?';\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst now = Date.now();\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst status = [\n dirArrow + ' ' + (cache.direction || ''),\n cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '',\n].filter(s => s.trim()).join(' | ');\n\nreturn [\n cache.level !== undefined ? {topic:'PS West Level', payload: cache.level, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS West Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS West Status', payload: status},\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null,\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null\n];", + "outputs": 5, + "x": 280, + "y": 650, + "wires": [ + [ + "demo_ctrl_west_chart_level" + ], + [ + "demo_ctrl_west_chart_flow" + ], + [ + "demo_ctrl_west_text_status" + ], + [ + "demo_ctrl_west_gauge_fill" + ], + [ + "demo_ctrl_west_gauge_tank" + ] + ] + }, + { + "id": "demo_ctrl_west_chart_level", + "type": "ui-chart", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_charts", + "name": "PS West Level", + "label": "Basin Level (m)", + "order": 1, + "width": "6", + "height": "5", + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisType": "time", + "yAxisLabel": "m", + "removeOlder": "10", + "removeOlderUnit": "60", + "action": "append", + "pointShape": "false", + "pointRadius": 0, + "interpolation": "linear", + "showLegend": true, + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "colors": [ + "#0094ce", + "#FF7F0E", + "#2CA02C" + ], + "textColor": [ + "#aaaaaa" + ], + "textColorDefault": false, + "gridColor": [ + "#333333" + ], + "gridColorDefault": false, + "x": 510, + "y": 630, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_west_chart_flow", + "type": "ui-chart", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_charts", + "name": "PS West Flow", + "label": "Net Flow (m³/h)", + "order": 2, + "width": "6", + "height": "5", + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisType": "time", + "yAxisLabel": "m³/h", + "removeOlder": "10", + "removeOlderUnit": "60", + "action": "append", + "pointShape": "false", + "pointRadius": 0, + "interpolation": "linear", + "showLegend": true, + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "colors": [ + "#4fc3f7", + "#FF7F0E", + "#2CA02C" + ], + "textColor": [ + "#aaaaaa" + ], + "textColorDefault": false, + "gridColor": [ + "#333333" + ], + "gridColorDefault": false, + "x": 510, + "y": 660, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_west_text_status", + "type": "ui-text", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_monitoring", + "name": "PS West Status", + "label": "Status", + "order": 1, + "width": "6", + "height": "1", + "format": "{{msg.payload}}", + "layout": "row-spread", + "x": 510, + "y": 680, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_west_gauge_fill", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_monitoring", + "name": "PS West Fill", + "gtype": "gauge-34", + "gstyle": "Rounded", + "title": "Fill", + "units": "%", + "prefix": "", + "suffix": "%", + "min": 0, + "max": 100, + "segments": [ + { + "color": "#f44336", + "from": 0 + }, + { + "color": "#ff9800", + "from": 10 + }, + { + "color": "#4caf50", + "from": 25 + }, + { + "color": "#ff9800", + "from": 75 + }, + { + "color": "#f44336", + "from": 90 + } + ], + "width": 3, + "height": 3, + "order": 2, + "className": "hide-limits", + "x": 700, + "y": 680, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_ctrl_west_gauge_tank", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_west_grp_monitoring", + "name": "PS West Tank", + "gtype": "gauge-tank", + "gstyle": "Rounded", + "title": "Level", + "units": "m", + "prefix": "", + "suffix": "m", + "min": 0, + "max": 4, + "segments": [ + { + "color": "#f44336", + "from": 0 + }, + { + "color": "#ff9800", + "from": 0.32 + }, + { + "color": "#2196f3", + "from": 1 + }, + { + "color": "#ff9800", + "from": 2.48 + }, + { + "color": "#f44336", + "from": 3.2 + } + ], + "width": 3, + "height": 4, + "order": 3, + "className": "", + "x": 700, + "y": 640, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_ui_page_ps_north_detail", + "type": "ui-page", + "name": "PS North Detail", + "ui": "demo_ui_base", + "path": "/ps-north", + "icon": "water_drop", + "layout": "grid", + "theme": "demo_ui_theme", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "12" + } + ], + "order": 3, + "className": "" + }, + { + "id": "demo_ctrl_north_grp_controls", + "type": "ui-group", + "name": "PS North Controls", + "page": "demo_ui_page_ps_north_detail", + "width": "6", + "height": "1", + "order": 1, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_north_grp_monitoring", + "type": "ui-group", + "name": "PS North Monitoring", + "page": "demo_ui_page_ps_north_detail", + "width": "6", + "height": "1", + "order": 2, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_north_grp_charts", + "type": "ui-group", + "name": "PS North Trends", + "page": "demo_ui_page_ps_north_detail", + "width": "12", + "height": "1", + "order": 3, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_north_mode", + "type": "ui-button-group", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_controls", + "name": "PS North Mode", + "label": "Station Mode", + "tooltip": "", + "order": 1, + "width": "6", + "height": "1", + "passthru": false, + "options": [ + { + "label": "Level", + "value": "levelbased", + "valueType": "str" + }, + { + "label": "Flow", + "value": "flowbased", + "valueType": "str" + }, + { + "label": "Manual", + "value": "manual", + "valueType": "str" + } + ], + "x": 120, + "y": 1000, + "wires": [ + [ + "demo_ctrl_north_fn_mode" + ] + ], + "rounded": true, + "useThemeColors": true, + "topic": "topic", + "topicType": "msg", + "className": "" + }, + { + "id": "demo_ctrl_north_fn_mode", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt PS North Mode", + "func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1000, + "wires": [ + [ + "demo_ctrl_north_link_cmd_out" + ] + ] + }, + { + "id": "demo_ctrl_north_flow", + "type": "ui-slider", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_controls", + "name": "PS North Flow", + "label": "Manual Flow (m³/h)", + "tooltip": "", + "order": 2, + "width": "6", + "height": "1", + "passthru": false, + "outs": "end", + "min": 0, + "max": 200, + "step": 1, + "x": 120, + "y": 1040, + "wires": [ + [ + "demo_ctrl_north_fn_flow" + ] + ], + "topic": "topic", + "topicType": "msg", + "thumbLabel": true, + "showTicks": "always", + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "", + "colorTrack": "", + "colorThumb": "", + "showTextField": false + }, + { + "id": "demo_ctrl_north_fn_flow", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt PS North Flow", + "func": "msg.topic = 'q_in';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1040, + "wires": [ + [ + "demo_ctrl_north_link_cmd_out" + ] + ] + }, + { + "id": "demo_ctrl_north_pump_n1_mode", + "type": "ui-button-group", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_controls", + "name": "N1 Mode", + "label": "N1 Mode", + "tooltip": "", + "order": 3, + "width": "3", + "height": "1", + "passthru": false, + "options": [ + { + "label": "Auto", + "value": "auto", + "valueType": "str" + }, + { + "label": "Virtual", + "value": "virtualControl", + "valueType": "str" + }, + { + "label": "Physical", + "value": "fysicalControl", + "valueType": "str" + } + ], + "x": 120, + "y": 1080, + "wires": [ + [ + "demo_ctrl_north_fn_pump_n1_mode" + ] + ], + "rounded": true, + "useThemeColors": true, + "topic": "topic", + "topicType": "msg", + "className": "" + }, + { + "id": "demo_ctrl_north_fn_pump_n1_mode", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt N1 Mode", + "func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_n1';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1080, + "wires": [ + [ + "demo_ctrl_north_link_pump_n1_out" + ] + ] + }, + { + "id": "demo_ctrl_north_pump_n1_speed", + "type": "ui-slider", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_controls", + "name": "N1 Speed", + "label": "N1 Speed (%)", + "tooltip": "", + "order": 4, + "width": "3", + "height": "1", + "passthru": false, + "outs": "end", + "min": 0, + "max": 100, + "step": 1, + "x": 120, + "y": 1120, + "wires": [ + [ + "demo_ctrl_north_fn_pump_n1_speed" + ] + ], + "topic": "topic", + "topicType": "msg", + "thumbLabel": true, + "showTicks": "always", + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "", + "colorTrack": "", + "colorThumb": "", + "showTextField": false + }, + { + "id": "demo_ctrl_north_fn_pump_n1_speed", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt N1 Speed", + "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_n1';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1120, + "wires": [ + [ + "demo_ctrl_north_link_pump_n1_out" + ] + ] + }, + { + "id": "demo_ctrl_north_link_pump_n1_out", + "type": "link out", + "z": "demo_tab_dashboard", + "name": "→ N1 Cmd", + "mode": "link", + "links": [ + "demo_ctrl_north_link_pump_n1_in" + ], + "x": 520, + "y": 1100 + }, + { + "id": "demo_ctrl_north_link_pump_n1_in", + "type": "link in", + "z": "demo_tab_ps_north", + "name": "← N1 Cmd", + "links": [ + "demo_ctrl_north_link_pump_n1_out" + ], + "x": 120, + "y": 540, + "wires": [ + [ + "demo_pump_n1" + ] + ] + }, + { + "id": "demo_ctrl_north_link_cmd_out", + "type": "link out", + "z": "demo_tab_dashboard", + "name": "→ PS North Cmd", + "mode": "link", + "links": [ + "demo_ctrl_north_link_cmd_in" + ], + "x": 520, + "y": 1020 + }, + { + "id": "demo_ctrl_north_link_cmd_in", + "type": "link in", + "z": "demo_tab_ps_north", + "name": "← PS North Cmd", + "links": [ + "demo_ctrl_north_link_cmd_out" + ], + "x": 120, + "y": 480, + "wires": [ + [ + "demo_ps_north" + ] + ] + }, + { + "id": "demo_ctrl_north_link_detail_data_out", + "type": "link out", + "z": "demo_tab_ps_north", + "name": "→ PS North Detail", + "mode": "link", + "links": [ + "demo_ctrl_north_link_detail_data_in" + ], + "x": 1080, + "y": 400 + }, + { + "id": "demo_ctrl_north_link_detail_data_in", + "type": "link in", + "z": "demo_tab_dashboard", + "name": "← PS North Detail", + "links": [ + "demo_ctrl_north_link_detail_data_out" + ], + "x": 75, + "y": 950, + "wires": [ + [ + "demo_ctrl_north_fn_detail_parse" + ] + ] + }, + { + "id": "demo_ctrl_north_fn_detail_parse", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Parse PS North Detail", + "func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pick = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\n\nconst level = pick(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pick(['volume.predicted.atequipment']);\nconst netFlow = pick(['netFlowRate.predicted.atequipment']);\nconst fillPct = pick(['volumePercent.predicted.atequipment']);\nconst direction = p.direction || cache.direction || '?';\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst now = Date.now();\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst status = [\n dirArrow + ' ' + (cache.direction || ''),\n cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '',\n].filter(s => s.trim()).join(' | ');\n\nreturn [\n cache.level !== undefined ? {topic:'PS North Level', payload: cache.level, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS North Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS North Status', payload: status},\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null,\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null\n];", + "outputs": 5, + "x": 280, + "y": 950, + "wires": [ + [ + "demo_ctrl_north_chart_level" + ], + [ + "demo_ctrl_north_chart_flow" + ], + [ + "demo_ctrl_north_text_status" + ], + [ + "demo_ctrl_north_gauge_fill" + ], + [ + "demo_ctrl_north_gauge_tank" + ] + ] + }, + { + "id": "demo_ctrl_north_chart_level", + "type": "ui-chart", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_charts", + "name": "PS North Level", + "label": "Basin Level (m)", + "order": 1, + "width": "6", + "height": "5", + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisType": "time", + "yAxisLabel": "m", + "removeOlder": "10", + "removeOlderUnit": "60", + "action": "append", + "pointShape": "false", + "pointRadius": 0, + "interpolation": "linear", + "showLegend": true, + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "colors": [ + "#0094ce", + "#FF7F0E", + "#2CA02C" + ], + "textColor": [ + "#aaaaaa" + ], + "textColorDefault": false, + "gridColor": [ + "#333333" + ], + "gridColorDefault": false, + "x": 510, + "y": 930, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_north_chart_flow", + "type": "ui-chart", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_charts", + "name": "PS North Flow", + "label": "Net Flow (m³/h)", + "order": 2, + "width": "6", + "height": "5", + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisType": "time", + "yAxisLabel": "m³/h", + "removeOlder": "10", + "removeOlderUnit": "60", + "action": "append", + "pointShape": "false", + "pointRadius": 0, + "interpolation": "linear", + "showLegend": true, + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "colors": [ + "#4fc3f7", + "#FF7F0E", + "#2CA02C" + ], + "textColor": [ + "#aaaaaa" + ], + "textColorDefault": false, + "gridColor": [ + "#333333" + ], + "gridColorDefault": false, + "x": 510, + "y": 960, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_north_text_status", + "type": "ui-text", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_monitoring", + "name": "PS North Status", + "label": "Status", + "order": 1, + "width": "6", + "height": "1", + "format": "{{msg.payload}}", + "layout": "row-spread", + "x": 510, + "y": 980, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_north_gauge_fill", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_monitoring", + "name": "PS North Fill", + "gtype": "gauge-34", + "gstyle": "Rounded", + "title": "Fill", + "units": "%", + "prefix": "", + "suffix": "%", + "min": 0, + "max": 100, + "segments": [ + { + "color": "#f44336", + "from": 0 + }, + { + "color": "#ff9800", + "from": 10 + }, + { + "color": "#4caf50", + "from": 25 + }, + { + "color": "#ff9800", + "from": 75 + }, + { + "color": "#f44336", + "from": 90 + } + ], + "width": 3, + "height": 3, + "order": 2, + "className": "hide-limits", + "x": 700, + "y": 980, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_ctrl_north_gauge_tank", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_north_grp_monitoring", + "name": "PS North Tank", + "gtype": "gauge-tank", + "gstyle": "Rounded", + "title": "Level", + "units": "m", + "prefix": "", + "suffix": "m", + "min": 0, + "max": 3, + "segments": [ + { + "color": "#f44336", + "from": 0 + }, + { + "color": "#ff9800", + "from": 0.24 + }, + { + "color": "#2196f3", + "from": 0.75 + }, + { + "color": "#ff9800", + "from": 1.8599999999999999 + }, + { + "color": "#f44336", + "from": 2.4000000000000004 + } + ], + "width": 3, + "height": 4, + "order": 3, + "className": "", + "x": 700, + "y": 940, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_ui_page_ps_south_detail", + "type": "ui-page", + "name": "PS South Detail", + "ui": "demo_ui_base", + "path": "/ps-south", + "icon": "water_drop", + "layout": "grid", + "theme": "demo_ui_theme", + "breakpoints": [ + { + "name": "Default", + "px": "0", + "cols": "12" + } + ], + "order": 4, + "className": "" + }, + { + "id": "demo_ctrl_south_grp_controls", + "type": "ui-group", + "name": "PS South Controls", + "page": "demo_ui_page_ps_south_detail", + "width": "6", + "height": "1", + "order": 1, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_south_grp_monitoring", + "type": "ui-group", + "name": "PS South Monitoring", + "page": "demo_ui_page_ps_south_detail", + "width": "6", + "height": "1", + "order": 2, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_south_grp_charts", + "type": "ui-group", + "name": "PS South Trends", + "page": "demo_ui_page_ps_south_detail", + "width": "12", + "height": "1", + "order": 3, + "showTitle": true, + "className": "" + }, + { + "id": "demo_ctrl_south_mode", + "type": "ui-button-group", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_controls", + "name": "PS South Mode", + "label": "Station Mode", + "tooltip": "", + "order": 1, + "width": "6", + "height": "1", + "passthru": false, + "options": [ + { + "label": "Level", + "value": "levelbased", + "valueType": "str" + }, + { + "label": "Flow", + "value": "flowbased", + "valueType": "str" + }, + { + "label": "Manual", + "value": "manual", + "valueType": "str" + } + ], + "x": 120, + "y": 1300, + "wires": [ + [ + "demo_ctrl_south_fn_mode" + ] + ], + "rounded": true, + "useThemeColors": true, + "topic": "topic", + "topicType": "msg", + "className": "" + }, + { + "id": "demo_ctrl_south_fn_mode", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt PS South Mode", + "func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1300, + "wires": [ + [ + "demo_ctrl_south_link_cmd_out" + ] + ] + }, + { + "id": "demo_ctrl_south_flow", + "type": "ui-slider", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_controls", + "name": "PS South Flow", + "label": "Manual Flow (m³/h)", + "tooltip": "", + "order": 2, + "width": "6", + "height": "1", + "passthru": false, + "outs": "end", + "min": 0, + "max": 100, + "step": 1, + "x": 120, + "y": 1340, + "wires": [ + [ + "demo_ctrl_south_fn_flow" + ] + ], + "topic": "topic", + "topicType": "msg", + "thumbLabel": true, + "showTicks": "always", + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "", + "colorTrack": "", + "colorThumb": "", + "showTextField": false + }, + { + "id": "demo_ctrl_south_fn_flow", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt PS South Flow", + "func": "msg.topic = 'q_in';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1340, + "wires": [ + [ + "demo_ctrl_south_link_cmd_out" + ] + ] + }, + { + "id": "demo_ctrl_south_pump_s1_mode", + "type": "ui-button-group", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_controls", + "name": "S1 Mode", + "label": "S1 Mode", + "tooltip": "", + "order": 3, + "width": "3", + "height": "1", + "passthru": false, + "options": [ + { + "label": "Auto", + "value": "auto", + "valueType": "str" + }, + { + "label": "Virtual", + "value": "virtualControl", + "valueType": "str" + }, + { + "label": "Physical", + "value": "fysicalControl", + "valueType": "str" + } + ], + "x": 120, + "y": 1380, + "wires": [ + [ + "demo_ctrl_south_fn_pump_s1_mode" + ] + ], + "rounded": true, + "useThemeColors": true, + "topic": "topic", + "topicType": "msg", + "className": "" + }, + { + "id": "demo_ctrl_south_fn_pump_s1_mode", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt S1 Mode", + "func": "msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = 'demo_pump_s1';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1380, + "wires": [ + [ + "demo_ctrl_south_link_pump_s1_out" + ] + ] + }, + { + "id": "demo_ctrl_south_pump_s1_speed", + "type": "ui-slider", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_controls", + "name": "S1 Speed", + "label": "S1 Speed (%)", + "tooltip": "", + "order": 4, + "width": "3", + "height": "1", + "passthru": false, + "outs": "end", + "min": 0, + "max": 100, + "step": 1, + "x": 120, + "y": 1420, + "wires": [ + [ + "demo_ctrl_south_fn_pump_s1_speed" + ] + ], + "topic": "topic", + "topicType": "msg", + "thumbLabel": true, + "showTicks": "always", + "className": "", + "iconPrepend": "", + "iconAppend": "", + "color": "", + "colorTrack": "", + "colorThumb": "", + "showTextField": false + }, + { + "id": "demo_ctrl_south_fn_pump_s1_speed", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Fmt S1 Speed", + "func": "msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = 'demo_pump_s1';\nreturn msg;", + "outputs": 1, + "x": 320, + "y": 1420, + "wires": [ + [ + "demo_ctrl_south_link_pump_s1_out" + ] + ] + }, + { + "id": "demo_ctrl_south_link_pump_s1_out", + "type": "link out", + "z": "demo_tab_dashboard", + "name": "→ S1 Cmd", + "mode": "link", + "links": [ + "demo_ctrl_south_link_pump_s1_in" + ], + "x": 520, + "y": 1400 + }, + { + "id": "demo_ctrl_south_link_pump_s1_in", + "type": "link in", + "z": "demo_tab_ps_south", + "name": "← S1 Cmd", + "links": [ + "demo_ctrl_south_link_pump_s1_out" + ], + "x": 120, + "y": 540, + "wires": [ + [ + "demo_pump_s1" + ] + ] + }, + { + "id": "demo_ctrl_south_link_cmd_out", + "type": "link out", + "z": "demo_tab_dashboard", + "name": "→ PS South Cmd", + "mode": "link", + "links": [ + "demo_ctrl_south_link_cmd_in" + ], + "x": 520, + "y": 1320 + }, + { + "id": "demo_ctrl_south_link_cmd_in", + "type": "link in", + "z": "demo_tab_ps_south", + "name": "← PS South Cmd", + "links": [ + "demo_ctrl_south_link_cmd_out" + ], + "x": 120, + "y": 480, + "wires": [ + [ + "demo_ps_south" + ] + ] + }, + { + "id": "demo_ctrl_south_link_detail_data_out", + "type": "link out", + "z": "demo_tab_ps_south", + "name": "→ PS South Detail", + "mode": "link", + "links": [ + "demo_ctrl_south_link_detail_data_in" + ], + "x": 1080, + "y": 400 + }, + { + "id": "demo_ctrl_south_link_detail_data_in", + "type": "link in", + "z": "demo_tab_dashboard", + "name": "← PS South Detail", + "links": [ + "demo_ctrl_south_link_detail_data_out" + ], + "x": 75, + "y": 1250, + "wires": [ + [ + "demo_ctrl_south_fn_detail_parse" + ] + ] + }, + { + "id": "demo_ctrl_south_fn_detail_parse", + "type": "function", + "z": "demo_tab_dashboard", + "name": "Parse PS South Detail", + "func": "const p = msg.payload || {};\nconst cache = context.get('c') || {};\nconst keys = Object.keys(p);\nconst pick = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; };\n\nconst level = pick(['level.predicted.atequipment','level.measured.atequipment']);\nconst volume = pick(['volume.predicted.atequipment']);\nconst netFlow = pick(['netFlowRate.predicted.atequipment']);\nconst fillPct = pick(['volumePercent.predicted.atequipment']);\nconst direction = p.direction || cache.direction || '?';\n\nif (level !== null) cache.level = level;\nif (volume !== null) cache.volume = volume;\nif (netFlow !== null) cache.netFlow = netFlow;\nif (fillPct !== null) cache.fillPct = fillPct;\ncache.direction = direction;\ncontext.set('c', cache);\n\nconst now = Date.now();\nconst dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014';\nconst status = [\n dirArrow + ' ' + (cache.direction || ''),\n cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '',\n].filter(s => s.trim()).join(' | ');\n\nreturn [\n cache.level !== undefined ? {topic:'PS South Level', payload: cache.level, timestamp: now} : null,\n cache.netFlow !== undefined ? {topic:'PS South Flow', payload: cache.netFlow, timestamp: now} : null,\n {topic:'PS South Status', payload: status},\n cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null,\n cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null\n];", + "outputs": 5, + "x": 280, + "y": 1250, + "wires": [ + [ + "demo_ctrl_south_chart_level" + ], + [ + "demo_ctrl_south_chart_flow" + ], + [ + "demo_ctrl_south_text_status" + ], + [ + "demo_ctrl_south_gauge_fill" + ], + [ + "demo_ctrl_south_gauge_tank" + ] + ] + }, + { + "id": "demo_ctrl_south_chart_level", + "type": "ui-chart", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_charts", + "name": "PS South Level", + "label": "Basin Level (m)", + "order": 1, + "width": "6", + "height": "5", + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisType": "time", + "yAxisLabel": "m", + "removeOlder": "10", + "removeOlderUnit": "60", + "action": "append", + "pointShape": "false", + "pointRadius": 0, + "interpolation": "linear", + "showLegend": true, + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "colors": [ + "#0094ce", + "#FF7F0E", + "#2CA02C" + ], + "textColor": [ + "#aaaaaa" + ], + "textColorDefault": false, + "gridColor": [ + "#333333" + ], + "gridColorDefault": false, + "x": 510, + "y": 1230, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_south_chart_flow", + "type": "ui-chart", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_charts", + "name": "PS South Flow", + "label": "Net Flow (m³/h)", + "order": 2, + "width": "6", + "height": "5", + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisType": "time", + "yAxisLabel": "m³/h", + "removeOlder": "10", + "removeOlderUnit": "60", + "action": "append", + "pointShape": "false", + "pointRadius": 0, + "interpolation": "linear", + "showLegend": true, + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "colors": [ + "#4fc3f7", + "#FF7F0E", + "#2CA02C" + ], + "textColor": [ + "#aaaaaa" + ], + "textColorDefault": false, + "gridColor": [ + "#333333" + ], + "gridColorDefault": false, + "x": 510, + "y": 1260, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_south_text_status", + "type": "ui-text", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_monitoring", + "name": "PS South Status", + "label": "Status", + "order": 1, + "width": "6", + "height": "1", + "format": "{{msg.payload}}", + "layout": "row-spread", + "x": 510, + "y": 1280, + "wires": [], + "className": "" + }, + { + "id": "demo_ctrl_south_gauge_fill", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_monitoring", + "name": "PS South Fill", + "gtype": "gauge-34", + "gstyle": "Rounded", + "title": "Fill", + "units": "%", + "prefix": "", + "suffix": "%", + "min": 0, + "max": 100, + "segments": [ + { + "color": "#f44336", + "from": 0 + }, + { + "color": "#ff9800", + "from": 10 + }, + { + "color": "#4caf50", + "from": 25 + }, + { + "color": "#ff9800", + "from": 75 + }, + { + "color": "#f44336", + "from": 90 + } + ], + "width": 3, + "height": 3, + "order": 2, + "className": "hide-limits", + "x": 700, + "y": 1280, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_ctrl_south_gauge_tank", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ctrl_south_grp_monitoring", + "name": "PS South Tank", + "gtype": "gauge-tank", + "gstyle": "Rounded", + "title": "Level", + "units": "m", + "prefix": "", + "suffix": "m", + "min": 0, + "max": 2.5, + "segments": [ + { + "color": "#f44336", + "from": 0 + }, + { + "color": "#ff9800", + "from": 0.2 + }, + { + "color": "#2196f3", + "from": 0.625 + }, + { + "color": "#ff9800", + "from": 1.55 + }, + { + "color": "#f44336", + "from": 2 + } + ], + "width": 3, + "height": 4, + "order": 3, + "className": "", + "x": 700, + "y": 1240, + "wires": [], + "value": "payload", + "valueType": "msg", + "sizeThickness": 16, + "sizeGap": 4, + "sizeKeyThickness": 8, + "styleRounded": true, + "styleGlow": false, + "alwaysShowTitle": false, + "floatingTitlePosition": "top-left", + "icon": "" + }, + { + "id": "demo_css_gauge_fixes", + "type": "ui-template", + "z": "demo_tab_dashboard", + "group": "", + "page": "", + "ui": "demo_ui_base", + "name": "Gauge CSS Fixes", + "order": 0, + "width": 0, + "height": 0, + "head": "", + "format": "\n", + "templateScope": "site:style", + "className": "", + "x": 510, + "y": 920, + "wires": [ + [] + ] + }, + { + "id": "demo_meas_nh4_in", + "type": "measurement", + "z": "demo_tab_treatment", + "name": "NH4-IN (Ammonium Inlet)", + "scaling": true, + "i_min": 0, + "i_max": 50, + "i_offset": 0, + "o_min": 0, + "o_max": 50, + "smooth_method": "mean", + "count": 3, + "simulator": true, + "uuid": "nh4-in-001", + "supplier": "Hach", + "category": "sensor", + "assetType": "ammonium", + "model": "Amtax-sc", + "unit": "mg/L", + "assetTagNumber": "NH4-IN", + "enableLog": false, + "logLevel": "error", + "positionVsParent": "atEquipment", + "x": 420, + "y": 220, + "wires": [ + [ + "demo_link_process_out_treatment" + ], + [ + "demo_link_influx_out_treatment" + ], + [ + "demo_reactor" + ] + ], + "positionIcon": "⊥", + "hasDistance": true, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "reactor inlet" + }, + { + "id": "demo_ui_grp_nh4_profile", + "type": "ui-group", + "name": "NH4 Profile Along Reactor", + "page": "demo_ui_page_treatment", + "width": "6", + "height": "1", + "order": 6, + "showTitle": true, + "className": "" + }, + { + "id": "demo_chart_nh4_profile", + "type": "ui-chart", + "z": "demo_tab_dashboard", + "group": "demo_ui_grp_nh4_profile", + "name": "NH4 Profile", + "label": "NH4 Along Reactor (mg/L)", + "order": 1, + "width": "6", + "height": "5", + "chartType": "line", + "category": "topic", + "categoryType": "msg", + "xAxisType": "time", + "yAxisLabel": "mg/L", + "removeOlder": "10", + "removeOlderUnit": "60", + "action": "append", + "pointShape": "false", + "pointRadius": 0, + "interpolation": "linear", + "x": 510, + "y": 1060, + "wires": [], + "showLegend": true, + "xAxisProperty": "", + "xAxisPropertyType": "timestamp", + "yAxisProperty": "payload", + "yAxisPropertyType": "msg", + "colors": [ + "#0094ce", + "#FF7F0E", + "#2CA02C", + "#D62728", + "#A347E1", + "#D62728", + "#FF9896", + "#9467BD", + "#C5B0D5" + ], + "textColor": [ + "#aaaaaa" + ], + "textColorDefault": false, + "gridColor": [ + "#333333" + ], + "gridColorDefault": false, + "className": "" + }, + { + "id": "demo_link_nh4_profile_dash", + "type": "link out", + "z": "demo_tab_treatment", + "name": "→ NH4 Profile Dashboard", + "mode": "link", + "links": [ + "demo_link_nh4_profile_dash_in" + ], + "x": 640, + "y": 340 + }, + { + "id": "demo_link_nh4_profile_dash_in", + "type": "link in", + "z": "demo_tab_dashboard", + "name": "← NH4 Profile", + "links": [ + "demo_link_nh4_profile_dash" + ], + "x": 75, + "y": 1060, + "wires": [ + [ + "demo_chart_nh4_profile" + ] + ] + }, + { + "id": "demo_fn_influent_compose", + "type": "function", + "z": "demo_tab_treatment", + "name": "Influent Composer", + "func": "// Convert merge collector output to Fluent messages for reactor\n// ASM3: [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS]\nconst p = msg.payload || {};\nconst MUNICIPAL = [0.5, 30, 200, 40, 0, 0, 5, 25, 150, 30, 0, 0, 200];\nconst INDUSTRIAL = [0.5, 40, 300, 25, 0, 0, 4, 30, 100, 20, 0, 0, 150];\nconst RESIDENTIAL = [0.5, 25, 180, 45, 0, 0, 5, 20, 130, 25, 0, 0, 175];\n\nconst Fw = (p.west?.netFlow || 0) * 24; // m3/h -> m3/d\nconst Fn = (p.north?.netFlow || 0) * 24;\nconst Fs = (p.south?.netFlow || 0) * 24;\n\nconst msgs = [];\nif (Fw > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 0, F: Fw, C: MUNICIPAL }});\nif (Fn > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 1, F: Fn, C: INDUSTRIAL }});\nif (Fs > 0) msgs.push({ topic: 'Fluent', payload: { inlet: 2, F: Fs, C: RESIDENTIAL }});\nreturn [msgs];", + "outputs": 1, + "x": 720, + "y": 1100, + "wires": [ + [ + "demo_reactor" + ] + ] + }, + { + "id": "demo_pump_ras", + "type": "rotatingMachine", + "z": "demo_tab_treatment", + "name": "RAS Pump", + "speed": "1", + "startup": "5", + "warmup": "3", + "shutdown": "4", + "cooldown": "2", + "movementMode": "dynspeed", + "machineCurve": "", + "uuid": "pump-ras-001", + "supplier": "hidrostal", + "category": "machine", + "assetType": "pump-centrifugal", + "model": "hidrostal-C5-D03R-SHN1", + "unit": "m3/h", + "enableLog": true, + "logLevel": "info", + "positionVsParent": "downstream", + "positionIcon": "←", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 1280, + "y": 760, + "wires": [ + [ + "demo_link_process_out_treatment" + ], + [ + "demo_link_influx_out_treatment" + ], + [ + "demo_settler" + ] + ], + "curveFlowUnit": "l/s", + "curvePressureUnit": "mbar", + "curvePowerUnit": "kW" + }, + { + "id": "demo_meas_ft_ras", + "type": "measurement", + "z": "demo_tab_treatment", + "name": "FT-RAS (RAS Flow)", + "scaling": true, + "i_min": 20, + "i_max": 80, + "i_offset": 0, + "o_min": 20, + "o_max": 80, + "smooth_method": "mean", + "count": 3, + "simulator": true, + "uuid": "ft-ras-001", + "supplier": "Endress+Hauser", + "category": "sensor", + "assetType": "flow", + "model": "Promag-W400", + "unit": "m3/h", + "assetTagNumber": "FT-RAS", + "enableLog": false, + "logLevel": "error", + "positionVsParent": "atEquipment", + "positionIcon": "⊥", + "hasDistance": false, + "distance": 0, + "distanceUnit": "m", + "distanceDescription": "", + "x": 1500, + "y": 760, + "wires": [ + [ + "demo_link_process_out_treatment" + ], + [ + "demo_link_influx_out_treatment" + ], + [ + "demo_pump_ras" + ] + ] + }, + { + "id": "demo_inj_ras_mode", + "type": "inject", + "z": "demo_tab_treatment", + "name": "RAS → virtualControl", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "vt": "str" + } + ], + "topic": "setMode", + "payload": "virtualControl", + "payloadType": "str", + "once": true, + "onceDelay": "3", + "x": 1280, + "y": 820, + "wires": [ + [ + "demo_pump_ras" + ] + ], + "repeatType": "none", + "crontab": "", + "repeat": "" + }, + { + "id": "demo_inj_ras_speed", + "type": "inject", + "z": "demo_tab_treatment", + "name": "RAS speed → 50%", + "props": [ + { + "p": "topic", + "vt": "str" + }, + { + "p": "payload", + "vt": "json" + } + ], + "topic": "execMovement", + "payload": "{\"source\":\"auto\",\"action\":\"setpoint\",\"setpoint\":50}", + "payloadType": "json", + "once": true, + "onceDelay": "4", + "x": 1280, + "y": 880, + "wires": [ + [ + "demo_pump_ras" + ] + ], + "repeatType": "none", + "crontab": "", + "repeat": "" + }, + { + "id": "demo_fn_ras_filter", + "type": "function", + "z": "demo_tab_treatment", + "name": "RAS Filter", + "func": "// Only pass RAS (inlet 2) from settler to reactor as inlet 3\nif (msg.topic === 'Fluent' && msg.payload && msg.payload.inlet === 2) {\n msg.payload.inlet = 3; // reactor inlet 3 = RAS\n return msg;\n}\nreturn null;", + "outputs": 1, + "x": 1060, + "y": 760, + "wires": [ + [ + "demo_reactor" + ] + ] + }, + { + "id": "demo_fn_prep_grafana", + "type": "function", + "z": "demo_tab_wwtp", + "name": "Prep Grafana Request", + "func": "// Stringify payload for http request node (Node-RED 4.x requires string body)\nmsg.payload = JSON.stringify(msg.payload);\nmsg.headers = msg.headers || {};\nmsg.headers['Content-Type'] = 'application/json';\nreturn msg;", + "outputs": 1, + "x": 1100, + "y": 1100, + "wires": [ + [ + "demo_http_grafana" + ] + ] + }, + { + "id": "demo_inj_dashapi_west", + "type": "inject", + "z": "demo_tab_wwtp", + "name": "Gen dashboards: PS West", + "props": [ + { + "p": "topic", + "v": "registerChild", + "vt": "str" + }, + { + "p": "payload", + "v": "demo_ps_west", + "vt": "str" + }, + { + "p": "includeChildren", + "v": "true", + "vt": "bool" + } + ], + "once": true, + "onceDelay": "20", + "x": 810, + "y": 1080, + "wires": [ + [ + "demo_dashapi" + ] + ], + "repeatType": "none", + "crontab": "", + "repeat": "" + }, + { + "id": "demo_inj_dashapi_north", + "type": "inject", + "z": "demo_tab_wwtp", + "name": "Gen dashboards: PS North", + "props": [ + { + "p": "topic", + "v": "registerChild", + "vt": "str" + }, + { + "p": "payload", + "v": "demo_ps_north", + "vt": "str" + }, + { + "p": "includeChildren", + "v": "true", + "vt": "bool" + } + ], + "once": true, + "onceDelay": "22", + "x": 810, + "y": 1120, + "wires": [ + [ + "demo_dashapi" + ] + ], + "repeatType": "none", + "crontab": "", + "repeat": "" + }, + { + "id": "demo_inj_dashapi_south", + "type": "inject", + "z": "demo_tab_wwtp", + "name": "Gen dashboards: PS South", + "props": [ + { + "p": "topic", + "v": "registerChild", + "vt": "str" + }, + { + "p": "payload", + "v": "demo_ps_south", + "vt": "str" + }, + { + "p": "includeChildren", + "v": "true", + "vt": "bool" + } + ], + "once": true, + "onceDelay": "24", + "x": 810, + "y": 1160, + "wires": [ + [ + "demo_dashapi" + ] + ], + "repeatType": "none", + "crontab": "", + "repeat": "" + }, + { + "id": "demo_inj_dashapi_settler", + "type": "inject", + "z": "demo_tab_wwtp", + "name": "Gen dashboards: Settler S1", + "props": [ + { + "p": "topic", + "v": "registerChild", + "vt": "str" + }, + { + "p": "payload", + "v": "demo_settler", + "vt": "str" + }, + { + "p": "includeChildren", + "v": "true", + "vt": "bool" + } + ], + "once": true, + "onceDelay": "26", + "x": 810, + "y": 1200, + "wires": [ + [ + "demo_dashapi" + ] + ], + "repeatType": "none", + "crontab": "", + "repeat": "" + }, + { + "id": "demo_fn_nh4_profile_extract", + "type": "function", + "z": "demo_tab_treatment", + "name": "Extract NH4 Profile", + "func": "if (msg.topic !== \"GridProfile\") return null;\nconst p = msg.payload;\nif (!p || !p.grid || !Array.isArray(p.grid)) return null;\n\nconst S_NH = 3;\nconst d_x = p.d_x;\nconst n_x = p.n_x;\nconst now = Date.now();\n\nconst positions = [0, 10, 25, 35, 50];\nconst msgs = positions.map(dist => {\n const idx = Math.min(Math.max(Math.round(dist / d_x), 0), n_x - 1);\n const val = p.grid[idx] ? p.grid[idx][S_NH] : null;\n if (val == null) return null;\n return { topic: \"NH4 @ \" + dist + \"m\", payload: Math.round(val * 100) / 100, timestamp: now };\n}).filter(Boolean);\n\nreturn [msgs];", + "outputs": 1, + "x": 640, + "y": 280, + "wires": [ + [ + "demo_link_nh4_profile_dash" + ] + ] + }, + { + "id": "demo_inj_reactor_dispersion", + "type": "inject", + "z": "demo_tab_treatment", + "name": "Reactor Dispersion D=10", + "props": [ + { + "p": "topic", + "vt": "str", + "v": "Dispersion" + }, + { + "p": "payload", + "vt": "num", + "v": "10" + } + ], + "repeat": "", + "crontab": "", + "once": true, + "onceDelay": "0.5", + "topic": "Dispersion", + "x": 620, + "y": 160, + "wires": [ + [ + "demo_reactor" + ] + ] + } +] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..b6c047c --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -e + +EVOLV_DIR="/data/evolv" +USER_DIR="/data" + +echo "=== EVOLV Node-RED Development Container ===" +echo "Starting at $(date)" + +# ------------------------------------------------------- +# 1. Verify node_modules exist (first start or volume wipe) +# ------------------------------------------------------- +if [ ! -d "$EVOLV_DIR/node_modules" ]; then + echo "[entrypoint] node_modules missing — running npm install..." + cd "$EVOLV_DIR" + npm install --ignore-scripts + echo "[entrypoint] npm install complete." +fi + +# ------------------------------------------------------- +# 2. Repair generalFunctions symlink if broken by bind mount +# ------------------------------------------------------- +GF_LINK="$EVOLV_DIR/node_modules/generalFunctions" +GF_TARGET="$EVOLV_DIR/nodes/generalFunctions" + +if [ -L "$GF_LINK" ] && [ ! -e "$GF_LINK" ]; then + echo "[entrypoint] Repairing broken generalFunctions symlink..." + rm -f "$GF_LINK" + ln -s "$GF_TARGET" "$GF_LINK" + echo "[entrypoint] Symlink repaired." +elif [ ! -e "$GF_LINK" ]; then + echo "[entrypoint] Creating generalFunctions symlink..." + ln -s "$GF_TARGET" "$GF_LINK" + echo "[entrypoint] Symlink created." +fi + +# ------------------------------------------------------- +# 3. Install EVOLV into Node-RED user dir +# This ensures node-red.nodes mapping is discovered +# ------------------------------------------------------- +if [ ! -f "$USER_DIR/package.json" ]; then + echo "[entrypoint] Initializing Node-RED user dir..." + cat > "$USER_DIR/package.json" << 'PKGJSON' +{ + "name": "evolv-nodered-userdir", + "description": "Node-RED user directory for EVOLV dev", + "version": "1.0.0", + "private": true, + "dependencies": {} +} +PKGJSON +fi + +# Link EVOLV package into the Node-RED user dir +cd "$USER_DIR" +npm install --no-save "$EVOLV_DIR" 2>/dev/null || { + echo "[entrypoint] npm install of EVOLV package failed, attempting symlink fallback..." + mkdir -p "$USER_DIR/node_modules" + rm -rf "$USER_DIR/node_modules/EVOLV" + ln -s "$EVOLV_DIR" "$USER_DIR/node_modules/EVOLV" +} + +echo "[entrypoint] EVOLV nodes installed into Node-RED user dir." + +# ------------------------------------------------------- +# 4. Deploy demo flow if no user flow exists yet +# ------------------------------------------------------- +DEMO_FLOW="$EVOLV_DIR/docker/demo-flow.json" +FLOW_FILE="/data/flows.json" + +if [ -f "$DEMO_FLOW" ]; then + # Deploy demo flow if flows.json is missing or is the default stub + if [ ! -f "$FLOW_FILE" ] || grep -q "WARNING: please check" "$FLOW_FILE" 2>/dev/null; then + echo "[entrypoint] Deploying demo flow..." + cp "$DEMO_FLOW" "$FLOW_FILE" + echo "[entrypoint] Demo flow deployed to $FLOW_FILE" + fi +fi + +# ------------------------------------------------------- +# 5. Start Node-RED with custom settings +# ------------------------------------------------------- +echo "[entrypoint] Starting Node-RED..." +exec node-red --settings "$EVOLV_DIR/docker/settings.js" diff --git a/docker/grafana/provisioning/datasources/influxdb.yaml b/docker/grafana/provisioning/datasources/influxdb.yaml new file mode 100644 index 0000000..c85e071 --- /dev/null +++ b/docker/grafana/provisioning/datasources/influxdb.yaml @@ -0,0 +1,16 @@ +apiVersion: 1 + +datasources: + - name: InfluxDB + type: influxdb + access: proxy + url: http://influxdb:8086 + uid: cdzg44tv250jkd + jsonData: + version: Flux + organization: evolv + defaultBucket: telemetry + secureJsonData: + token: evolv-dev-token + isDefault: true + editable: true diff --git a/docker/settings.js b/docker/settings.js new file mode 100644 index 0000000..ac4843d --- /dev/null +++ b/docker/settings.js @@ -0,0 +1,47 @@ +/** + * Node-RED Settings for EVOLV Docker Development Environment + * + * This configuration is for LOCAL DEVELOPMENT ONLY. + * No authentication — do not expose to untrusted networks. + */ +module.exports = { + flowFile: '/data/flows.json', + userDir: '/data', + + // Bind to all interfaces inside the container + uiHost: '0.0.0.0', + uiPort: 1880, + + // No authentication for dev environment + adminAuth: null, + + // Disable projects (we use git directly) + editorTheme: { + projects: { + enabled: false + } + }, + + // Global context available to function nodes + functionGlobalContext: { + locationId: process.env.LOCATION_ID || 'docker-dev', + uuid: require('crypto').randomUUID() + }, + + // Logging + logging: { + console: { + level: 'info', + metrics: false, + audit: false + } + }, + + // Increase max message size for large telemetry payloads + apiMaxLength: '5mb', + + // Diagnostic reporting off for dev + diagnostics: { + enabled: false + } +}; diff --git a/manuals/README.md b/manuals/README.md new file mode 100644 index 0000000..2a65569 --- /dev/null +++ b/manuals/README.md @@ -0,0 +1,10 @@ +# Manuals + +Local reference manuals used by EVOLV agents while implementing and reviewing Node-RED behavior. + +- `manuals/node-red/INDEX.md`: Node-RED manual index and quick usage map. +- `manuals/node-red/runtime-node-js.md`: Runtime custom node message handling (`send`, `done`, multi-output arrays). +- `manuals/node-red/function-node-patterns.md`: Function-node return/send patterns for output routing. +- `manuals/node-red/messages-and-editor-structure.md`: Message shape and HTML/editor/runtime contracts. +- `manuals/node-red/flowfuse-ui-chart-manual.md`: FlowFuse `ui-chart` data contract and runtime controls. +- `manuals/node-red/flowfuse-dashboard-layout-manual.md`: Compact FlowFuse dashboard layout guidance. diff --git a/manuals/node-red/INDEX.md b/manuals/node-red/INDEX.md new file mode 100644 index 0000000..ceeb7be --- /dev/null +++ b/manuals/node-red/INDEX.md @@ -0,0 +1,30 @@ +# Node-RED Manual Index + +This folder summarizes official Node-RED docs that are relevant to EVOLV node development. + +## Official Sources + +- Creating Nodes: JavaScript file and message handling + https://nodered.org/docs/creating-nodes/node-js +- Creating Nodes: Edit dialog and node definition in `.html` + https://nodered.org/docs/creating-nodes/edit-dialog +- Working with messages + https://nodered.org/docs/user-guide/messages +- Writing Functions (return arrays, multiple outputs, async send/done) + https://nodered.org/docs/user-guide/writing-functions + +## What To Check First (EVOLV) + +1. Runtime routing in `src/nodeClass.js`: use explicit output arrays for multi-output nodes. +2. Input handlers: use `send` + `done` pattern from Node-RED runtime docs. +3. Function nodes in example flows: return arrays with output-position alignment. +4. Editor/runtime parity: properties in `RED.nodes.registerType(...defaults...)` must map to runtime config parsing. +5. For FlowFuse dashboard reference, see: + - `flowfuse-widgets-catalog.md` — master index of all 22 widget types + - `flowfuse-ui-chart-manual.md` — chart widget (line, bar, scatter, pie, histogram) + - `flowfuse-ui-gauge-manual.md` — gauge widget (tile, battery, tank, half, 3/4 arc) + - `flowfuse-ui-text-manual.md` — text display widget + - `flowfuse-ui-template-manual.md` — custom Vue/Vuetify template widget + - `flowfuse-ui-button-manual.md` — button widget + - `flowfuse-ui-config-manual.md` — config nodes (ui-base, ui-page, ui-group, ui-theme) + - `flowfuse-dashboard-layout-manual.md` — layout patterns and sizing rules diff --git a/manuals/node-red/flowfuse-dashboard-layout-manual.md b/manuals/node-red/flowfuse-dashboard-layout-manual.md new file mode 100644 index 0000000..aeb77eb --- /dev/null +++ b/manuals/node-red/flowfuse-dashboard-layout-manual.md @@ -0,0 +1,47 @@ +# FlowFuse Dashboard Layout Notes (EVOLV Reference) + +Primary sources: +- https://dashboard.flowfuse.com/ +- https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html + +## Compact Screen Guidelines + +- Use a 12-column page grid and place charts in 4-column blocks for 3-up rows. +- Disable legends for single-series charts to reduce visual noise. +- Prefer concise text widgets under charts for state/timing/snapshot summaries. +- Use compact theme spacing: + - lower page padding + - lower group gap + - lower widget gap + +## Time Window Guidelines + +- For live demos, default chart history to 10-15 minutes. +- Keep axis labels short and unit-specific. +- Use one chart per KPI unless comparison is intentionally needed. + +## Message Hygiene For Widgets + +- Chart widgets: send minimal `{ topic, payload, timestamp }`. +- Text widgets: send plain string in `msg.payload`. +- Separate chart and text outputs by Function-node output index. + +## Gauge Sizing in Groups + +- **Tank gauge**: `width: 2, height: 4` — tall vertical fill indicator. +- **3/4 arc gauge**: `width: 2, height: 3` — fits beside tank in same group row. +- **Status text**: `width: 4, height: 1` — full group width, below gauges. +- In a 4-column group, two `width: 2` gauges sit side by side, text below spans full width. +- Set `order` on widgets: tank=2, arc=3, text=1 (text first = top, gauges below; or tank=1, arc=2, text=3 for gauges on top). + +## Group Height Auto-sizing + +- Set `height: "1"` on groups to auto-grow with content. +- A group with a `height: 4` tank + `height: 1` text will auto-expand to ~5 rows. + +## References + +- FlowFuse Dashboard docs root: https://dashboard.flowfuse.com/ +- FlowFuse `ui-chart` docs: https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html +- FlowFuse Widget catalog: [flowfuse-widgets-catalog.md](flowfuse-widgets-catalog.md) +- FlowFuse Config nodes: [flowfuse-ui-config-manual.md](flowfuse-ui-config-manual.md) diff --git a/manuals/node-red/flowfuse-ui-button-manual.md b/manuals/node-red/flowfuse-ui-button-manual.md new file mode 100644 index 0000000..b0b1c7b --- /dev/null +++ b/manuals/node-red/flowfuse-ui-button-manual.md @@ -0,0 +1,62 @@ +# FlowFuse `ui-button` Manual (EVOLV Reference) + +Source: https://dashboard.flowfuse.com/nodes/widgets/ui-button.html + +## Purpose + +Clickable button that sends a message on user interaction. + +## Properties + +| Property | Type | Dynamic | Notes | +|----------|------|---------|-------| +| `group` | ref | No | Parent ui-group | +| `width` | int | No | Columns | +| `height` | int | No | Row units | +| `label` | string | Yes | Button text | +| `icon` | string | Yes | Material Design icon name (no `mdi-` prefix) | +| `iconPosition` | string | Yes | `"left"` or `"right"` | +| `buttonColor` | string | Yes | Background color | +| `textColor` | string | Yes | Label color (auto-calculated if omitted) | +| `iconColor` | string | Yes | Icon color (matches text if omitted) | +| `tooltip` | string | No | Hover tooltip | +| `order` | int | No | Position in group | +| `emulateClick` | bool | No | Trigger click on any received msg | + +## Events + +| Event | Enabled By | Output | +|-------|-----------|--------| +| Click | `onclick: true` | `msg.payload` = configured value | +| Pointer Down | `onpointerdown: true` | `msg.payload` + `msg._event` with timestamp | +| Pointer Up | `onpointerup: true` | `msg.payload` + `msg._event` with timestamp | + +Hold duration = `pointerup._event.timestamp - pointerdown._event.timestamp`. + +## Input + +`msg.payload` — sets button payload value. With `emulateClick: true`, any input msg triggers a click. + +`msg.enabled` — `true` / `false` to enable/disable the button. + +## Dynamic Properties (`msg.ui_update`) + +```js +msg.ui_update = { + label: "Stop", + icon: "stop", + iconPosition: "left", + buttonColor: "#f44336", + textColor: "#ffffff", + iconColor: "#ffffff", + class: "my-btn-class" +}; +``` + +## EVOLV Key Rules + +1. Use buttons for operator actions (start/stop pump, acknowledge alarm). +2. Set `emulateClick: false` (default) — don't auto-trigger on incoming messages. +3. For toggle buttons, update label/color dynamically via `msg.ui_update` from downstream logic. +4. Pair with confirmation dialog (ui-notification or ui-template) for destructive actions. +5. Standard sizing: `width: 2, height: 1` for inline; `width: 4, height: 1` for full-width in 4-col group. diff --git a/manuals/node-red/flowfuse-ui-chart-manual.md b/manuals/node-red/flowfuse-ui-chart-manual.md new file mode 100644 index 0000000..ea7a15b --- /dev/null +++ b/manuals/node-red/flowfuse-ui-chart-manual.md @@ -0,0 +1,121 @@ +# FlowFuse `ui-chart` Manual (EVOLV Reference) + +Source: https://dashboard.flowfuse.com/nodes/widgets/ui-chart.html + +## Chart Types + +| Type | X-Axis Options | Notes | +|------|---------------|-------| +| Line | timescale, linear, categorical | Interpolation: linear, step, bezier, cubic, cubic-mono | +| Bar | categorical | Grouped (side-by-side) or stacked | +| Scatter | timescale, linear, categorical | Configurable point shape & radius | +| Pie / Doughnut | radial | Nested series for multi-layer | +| Histogram | auto-binned | Numerical bins or categorical counting | + +## Properties + +| Property | Type | Dynamic | Notes | +|----------|------|---------|-------| +| `group` | ref | No | Parent ui-group | +| `width` | int | No | Columns | +| `height` | int | No | Row units | +| `label` | string | No | Chart title | +| `chartType` | string | No | `"line"`, `"bar"`, `"scatter"`, `"pie"`, `"histogram"` | +| `showLegend` | bool | No | Show series legend | +| `action` | string | Yes | `"append"` or `"replace"` | +| `pointShape` | string | No | Point marker shape (scatter/line) | +| `xAxisLimit` | int | No | Max data points to keep | +| `textColor` | color | No | Override text color | +| `gridColor` | color | No | Override grid lines | +| `xAxisType` | string | No | `"time"`, `"linear"`, `"category"` | + +## Series Configuration + +| Source | How It Works | +|--------|-------------| +| `msg.topic` | Default — each unique topic creates a series | +| `key` | Uses a named property from data object | +| `JSON` | Array of keys for multi-series from single msg | +| (blank) | Auto-generates timestamp for x | + +## Input Format + +### Simple (recommended for EVOLV) +```js +{ topic: "PS West Level", payload: 2.34, timestamp: Date.now() } +``` + +### Object with x/y mapping +```js +{ payload: { time: 1700000000000, value: 2.34 } } +// Configure x → "time", y → "value" +``` + +### Nested access +X/Y keys support dot notation: `"nested.value"` → `payload.nested.value` + +## Dynamic Properties + +| Property | Path | Notes | +|----------|------|-------| +| Action | `msg.action` | `"append"` or `"replace"` | +| Chart Options | `msg.ui_update.chartOptions` | eCharts config merge | +| CSS Class | `msg.class` | Add custom class | + +## eCharts Customization (`msg.ui_update.chartOptions`) + +Deep-merges with existing config. Accumulates across messages. + +```js +msg.ui_update = { + chartOptions: { + yAxis: { position: "right", name: "Level (m)" }, + grid: { top: 60, right: 40 }, + title: { text: "Basin Levels", textStyle: { fontSize: 14 } } + } +}; +``` + +**Series colors** — must provide all series configs together: +```js +msg.ui_update = { + chartOptions: { + series: [ + { name: "PS West", type: "line", lineStyle: { color: "#2196f3" }, itemStyle: { color: "#2196f3" } }, + { name: "PS North", type: "line", lineStyle: { color: "#ff9800" }, itemStyle: { color: "#ff9800" } } + ] + } +}; +``` + +## Data Actions + +| Action | How | Effect | +|--------|-----|--------| +| Append | `msg.action = "append"` (default) | Add points to existing data | +| Replace | `msg.action = "replace"` | Clear then add | +| Clear | `msg.payload = []` | Remove all data | + +## Time Formatting Tokens + +`{HH}:{mm}:{ss}` → `12:00:00` +`{yyyy}-{M}-{d}` → `2020-1-1` +Tokens: `{yyyy}`, `{MM}`, `{dd}`, `{HH}`, `{H}`, `{hh}`, `{h}`, `{mm}`, `{m}`, `{ss}`, `{s}`, `{eeee}` (day name) + +## EVOLV Key Rules + +1. Send minimal `{ topic, payload, timestamp }` — chart auto-uses topic for series. +2. Set `category: "topic"`, `categoryType: "msg"` in node config — blank category causes editor validation errors. +3. For timeseries: leave x-key blank → auto-timestamp. Supply ms timestamps if custom. +4. For multi-station overlay charts: each station sends with a different `msg.topic`. +5. Use `msg.action = "append"` (default) for streaming data; `"replace"` for snapshot updates. +6. Keep one topic per chart for simple KPIs; use multi-topic for comparison views. +7. Prefer explicit output-slot routing: `node.send([msgForChart, msgForText, ...]); return null;` + +## Common Failure Modes + +- Payload is not numeric (string/object without y-key config) +- Function node returns to wrong output index +- Topic churn creates unexpected series fragmentation +- Chart has stale data policy mismatch (`append` vs `replace`) +- Blank `category` field causes red node on deploy diff --git a/manuals/node-red/flowfuse-ui-config-manual.md b/manuals/node-red/flowfuse-ui-config-manual.md new file mode 100644 index 0000000..2dcd37e --- /dev/null +++ b/manuals/node-red/flowfuse-ui-config-manual.md @@ -0,0 +1,103 @@ +# FlowFuse Config Nodes Manual (EVOLV Reference) + +Sources: +- https://dashboard.flowfuse.com/nodes/config/ui-base.html +- https://dashboard.flowfuse.com/nodes/config/ui-page.html +- https://dashboard.flowfuse.com/nodes/config/ui-group.html +- https://dashboard.flowfuse.com/nodes/config/ui-theme.html + +--- + +## `ui-base` — Dashboard Root + +| Property | Type | Notes | +|----------|------|-------| +| `path` | string | URL path after host, e.g. `/dashboard` | +| `appIcon` | string | URL to square icon (192–512px) for PWA | +| `includePagePath` | bool | Show page paths in side nav | +| `navigationStyle` | string | `"default"`, `"fixed"`, `"icon"`, `"temporary"`, `"none"` | +| `headerStyle` | string | `"default"` (scrolls), `"fixed"` (sticky), `"hidden"` | +| `headerContent` | string | `"page"`, `"dashboard"`, `"both"`, `"none"` | + +--- + +## `ui-page` — Dashboard Page + +| Property | Type | Notes | +|----------|------|-------| +| `name` | string | Displayed in nav and header | +| `ui` | ref | Parent ui-base | +| `path` | string | URL path segment, e.g. `/influent` | +| `icon` | string | Material Design icon (no `mdi-` prefix) | +| `theme` | ref | ui-theme reference | +| `layout` | string | `"grid"`, `"fixed"`, `"notebook"`, `"tabs"` | +| `order` | int | Position in navigation | +| `breakpoints` | array | See breakpoints table | + +### Layout Types + +| Layout | Description | +|--------|-------------| +| `grid` | Responsive grid, widgets flow into columns | +| `fixed` | Absolute positioned, no responsive reflow | +| `notebook` | Single-column stacked groups | +| `tabs` | Each group becomes a tab | + +### Default Breakpoints (grid/notebook/tabs) + +| Name | Min Width | Columns | +|------|-----------|---------| +| Mobile | 0 px | 3 | +| Medium | 576 px | 6 | +| Tablet | 768 px | 9 | +| Desktop | 1024 px | 12 | + +--- + +## `ui-group` — Widget Container + +| Property | Type | Notes | +|----------|------|-------| +| `name` | string | Group title (shown if `showTitle: true`) | +| `page` | ref | Parent ui-page | +| `width` | string/int | Column span (e.g. `"4"` = 4 of 12 columns) | +| `height` | string/int | Minimum row height (`"1"` = auto-grow) | +| `order` | int | Render order on page | +| `showTitle` | bool | Show name as header | +| `className` | string | Custom CSS class | +| `groupType` | string | `"default"` (visible) or `"dialog"` (triggered) | + +### Sizing Rules + +- Group `width` sets column span out of page's total columns (default 12). +- Group `height` is a **minimum** — group grows to fit content. +- Widget `width` within a group is relative to the group's width. +- Widget `height` is in row units (1 unit = theme's Row Height setting). + +--- + +## `ui-theme` — Appearance + +| Property | Type | Default | Notes | +|----------|------|---------|-------| +| `colors.surface` | color | — | Header & nav background | +| `colors.primary` | color | — | Buttons, sliders, focus rings | +| `colors.bgPage` | color | — | Page background | +| `colors.groupBg` | color | — | Group background | +| `colors.groupOutline` | color | — | Group border color | +| `sizes.rowHeight` | string | `"default"` | `"default"` (48px), `"comfortable"` (36px), `"compact"` (32px) | +| `sizes.pagePadding` | string | `"12px"` | CSS shorthand (e.g. `"12px 6px"`) | +| `sizes.groupGap` | string | `"12px"` | Space between groups | +| `sizes.groupBorderRadius` | string | `"4px"` | Group corner rounding | +| `sizes.widgetGap` | string | `"12px"` | Space between widgets in group | + +--- + +## EVOLV Key Rules + +1. **12-column grid** with 4-col groups gives a clean 3-column layout at desktop. +2. Set `height: "1"` on groups to let them auto-size (content determines height). +3. Use `order` on groups to control left-to-right placement within a row. +4. For compact dashboards: theme `rowHeight: "compact"`, `pagePadding: "6px"`, `groupGap: "6px"`, `widgetGap: "6px"`. +5. Widget `order` within a group determines top-to-bottom flow (lower = higher). +6. Gauge sizing guide: tank gauge `width:2, height:4` + 3/4 gauge `width:2, height:3` fits well in a 4-col group alongside a status text `width:4, height:1`. diff --git a/manuals/node-red/flowfuse-ui-gauge-manual.md b/manuals/node-red/flowfuse-ui-gauge-manual.md new file mode 100644 index 0000000..4feed67 --- /dev/null +++ b/manuals/node-red/flowfuse-ui-gauge-manual.md @@ -0,0 +1,108 @@ +# FlowFuse `ui-gauge` Manual (EVOLV Reference) + +Source: https://dashboard.flowfuse.com/nodes/widgets/ui-gauge.html + +## Gauge Types + +| `gtype` value | Visual | Best For | +|---------------|--------|----------| +| `gauge-tile` | Compact square with value | KPI tiles | +| `gauge-battery` | Horizontal battery bar | Charge / capacity | +| `gauge-tank` | Vertical tank with fill gradient | Liquid levels | +| `gauge-half` | 180° arc | Setpoint / range display | +| `gauge-34` | 270° arc | Primary process gauge | + +## Properties + +| Property | Type | Dynamic | Notes | +|----------|------|---------|-------| +| `group` | ref | No | Parent ui-group | +| `width` | int | No | Columns (max = group width) | +| `height` | int | No | Row units | +| `gtype` | string | Yes | See table above | +| `gstyle` | string | Yes | `"Needle"` or `"Rounded"` (half/3-4 only) | +| `min` | number | Yes | Range minimum | +| `max` | number | Yes | Range maximum | +| `segments` | array | Yes | `[{color: "#hex", from: number}, …]` | +| `title` | string | Yes | Label above gauge | +| `prefix` | string | Yes | Before value (half/3-4 only) | +| `suffix` | string | Yes | After value (half/3-4 only) | +| `units` | string | Yes | Small text below value (half/3-4 only) | +| `icon` | string | Yes | Material Design icon (half/3-4 only) | +| `sizeGauge` | int | No | Arc thickness px | +| `sizeGap` | int | No | Gap between arc & segments px | +| `sizeSegments` | int | No | Segment ring thickness px | + +## Input + +`msg.payload` — numeric value to display. + +## Dynamic Properties (`msg.ui_update`) + +```js +msg.ui_update = { + label: "Tank A", + gtype: "gauge-tank", + gstyle: "Rounded", + min: 0, max: 100, + segments: [{color:"#f44336", from:0}, {color:"#4caf50", from:25}, {color:"#f44336", from:90}], + prefix: "", suffix: "%", units: "fill" +}; +``` + +## Segments + +Array of `{color, from}` objects sorted ascending by `from`. Each segment colors the range from its `from` up to the next segment's `from` (or `max`). + +**Tank default segments** (auto-applied when switching to tank type): +```json +[{"color":"#A8F5FF","from":0},{"color":"#55DBEC","from":15}, + {"color":"#53B4FD","from":35},{"color":"#2397D1","from":50}] +``` + +## CSS Selectors + +| Selector | Target | +|----------|--------| +| `.nrdb-ui-gauge-value span` | Central value text | +| `.nrdb-ui-gauge-value label` | Unit label | +| `.nrdb-ui-gauge-value i` | Icon | +| `.nrdb-ui-gauge #limits` | Min/max labels | + +## EVOLV Key Rules + +1. **Tank gauge** for basin level: set `gtype:"gauge-tank"`, `min:0`, `max:`, custom segments by safety thresholds. +2. **3/4 gauge** for fill %: set `gtype:"gauge-34"`, `gstyle:"Rounded"`, `min:0`, `max:100`, segments for low/normal/high. +3. Recommended sizing: tank `width:2, height:4`; 3/4 arc `width:2, height:3`. +4. Send plain numeric `msg.payload` — no topic needed (gauge has no series concept). +5. Segments must be provided as array even for a single color range. + +## Node JSON Example (tank) + +```json +{ + "id": "demo_gauge_tank_west", + "type": "ui-gauge", + "z": "demo_tab_dashboard", + "group": "demo_ui_grp_ps_west", + "name": "West Tank Level", + "gtype": "gauge-tank", + "gstyle": "Rounded", + "title": "Level", + "units": "m", + "prefix": "", + "suffix": "m", + "min": 0, "max": 4, + "segments": [ + {"color":"#f44336","from":0}, + {"color":"#ff9800","from":0.3}, + {"color":"#2196f3","from":1.0}, + {"color":"#ff9800","from":2.5}, + {"color":"#f44336","from":3.2} + ], + "width": 2, "height": 4, + "order": 2, + "x": 700, "y": 400, + "wires": [] +} +``` diff --git a/manuals/node-red/flowfuse-ui-template-manual.md b/manuals/node-red/flowfuse-ui-template-manual.md new file mode 100644 index 0000000..b80159d --- /dev/null +++ b/manuals/node-red/flowfuse-ui-template-manual.md @@ -0,0 +1,112 @@ +# FlowFuse `ui-template` Manual (EVOLV Reference) + +Source: https://dashboard.flowfuse.com/nodes/widgets/ui-template.html + +## Purpose + +Custom Vue 3 / Vuetify / HTML widget. Full scripting, scoped CSS, send/receive messages. + +## Scopes + +| Scope | Renders | Use Case | +|-------|---------|----------| +| Widget (Group) | Inside a ui-group | Custom gauges, tables, controls | +| Widget (Page) | On page, outside groups | Floating overlays, full-width banners | +| Widget (UI) | On every page | Global headers, footers | +| CSS (All Pages) | N/A — injects ` +``` + +## Built-in Variables + +| Variable | Description | +|----------|-------------| +| `id` | This node's unique ID | +| `msg` | Latest received message (reactive) | +| `$socket` | Socket.io client for custom events | + +## Sending Messages + +```js +this.send({ payload: 42, topic: "my-topic" }); +this.submit(); // sends FormData from
+``` + +## Receiving Messages + +**Option A** — Vue `watch`: +```js +watch: { msg(newMsg) { /* react */ } } +``` + +**Option B** — Socket listener: +```js +mounted() { + this.$socket.on('msg-input:' + this.id, (msg) => { /* handle */ }); +} +``` + +## Teleports (inject into dashboard chrome) + +```html + + Custom Button + +``` + +Targets: `#app-bar-title`, `#app-bar-actions`. + +## Vuetify Components + +All Vuetify 3 components are available without import: ``, ``, ``, ``, etc. + +## Dynamic Properties (`msg.ui_update`) + +```js +msg.ui_update = { format: "

New template content

" }; +``` + +## EVOLV Key Rules + +1. Use templates sparingly — prefer built-in widgets when they fit. +2. For complex custom visualizations (SVG P&ID, animated schematics), template is the right choice. +3. Always use ``, + templateScope: "local", + className: "", + x: 510, y: 960, + wires: [[]] +}); + +// ============================================= +// 3c. KPI gauges on overview +// ============================================= + +// Total Influent Flow gauge +flow.push({ + id: "demo_gauge_overview_flow", + type: "ui-gauge", + z: "demo_tab_dashboard", + group: "demo_ui_grp_overview_kpi", + name: "Total Influent Flow", + gtype: "gauge-34", + gstyle: "Rounded", + title: "Influent Flow", + units: "m\u00b3/h", + prefix: "", + suffix: "m\u00b3/h", + min: 0, + max: 500, + segments: [ + { color: "#2196f3", from: 0 }, + { color: "#4caf50", from: 50 }, + { color: "#ff9800", from: 350 }, + { color: "#f44336", from: 450 } + ], + width: 3, + height: 4, + order: 1, + className: "", + x: 510, y: 1020, + wires: [] +}); + +// Reactor DO gauge +flow.push({ + id: "demo_gauge_overview_do", + type: "ui-gauge", + z: "demo_tab_dashboard", + group: "demo_ui_grp_overview_kpi", + name: "Reactor DO", + gtype: "gauge-34", + gstyle: "Rounded", + title: "Reactor DO", + units: "mg/L", + prefix: "", + suffix: "mg/L", + min: 0, + max: 10, + segments: [ + { color: "#f44336", from: 0 }, + { color: "#ff9800", from: 1 }, + { color: "#4caf50", from: 2 }, + { color: "#ff9800", from: 6 }, + { color: "#f44336", from: 8 } + ], + width: 3, + height: 4, + order: 2, + className: "", + x: 510, y: 1060, + wires: [] +}); + +// Effluent TSS gauge +flow.push({ + id: "demo_gauge_overview_tss", + type: "ui-gauge", + z: "demo_tab_dashboard", + group: "demo_ui_grp_overview_kpi", + name: "Effluent TSS", + gtype: "gauge-34", + gstyle: "Rounded", + title: "Effluent TSS", + units: "mg/L", + prefix: "", + suffix: "mg/L", + min: 0, + max: 50, + segments: [ + { color: "#4caf50", from: 0 }, + { color: "#ff9800", from: 25 }, + { color: "#f44336", from: 40 } + ], + width: 3, + height: 4, + order: 3, + className: "", + x: 510, y: 1100, + wires: [] +}); + +// Effluent NH4 gauge +flow.push({ + id: "demo_gauge_overview_nh4", + type: "ui-gauge", + z: "demo_tab_dashboard", + group: "demo_ui_grp_overview_kpi", + name: "Effluent NH4", + gtype: "gauge-34", + gstyle: "Rounded", + title: "Effluent NH4", + units: "mg/L", + prefix: "", + suffix: "mg/L", + min: 0, + max: 20, + segments: [ + { color: "#4caf50", from: 0 }, + { color: "#ff9800", from: 5 }, + { color: "#f44336", from: 10 } + ], + width: 3, + height: 4, + order: 4, + className: "", + x: 510, y: 1140, + wires: [] +}); + +// ============================================= +// 3d. Reorder all page navigation +// ============================================= +const pageOrders = { + "demo_ui_page_overview": 0, + "demo_ui_page_influent": 1, + "demo_ui_page_treatment": 5, + "demo_ui_page_telemetry": 6, +}; + +for (const [pageId, order] of Object.entries(pageOrders)) { + const page = byId(pageId); + if (page) page.order = order; +} + +// ============================================= +// Feed chain vis and KPIs from merge + reactor + effluent +// We need to also wire the overview_template to receive reactor/eff data +// The parse functions already wire to the template and gauges separately +// But the template needs ALL data sources - let's connect reactor and eff parsers to it too +// ============================================= + +// Actually, the template needs multiple inputs. Let's connect reactor and eff parse outputs too. +// Modify overview reactor parse to also send to template +const reactorParse = byId("demo_fn_overview_reactor_parse"); +// Currently wires to demo_gauge_overview_do. Add template as well. +reactorParse.func = `const p = msg.payload || {}; +if (!p.C || !Array.isArray(p.C)) return null; + +flow.set('overview_reactor', p); + +// Output 1: DO gauge, Output 2: to chain template +const doVal = Math.round(p.C[0]*100)/100; +return [ + { topic: 'Reactor DO', payload: doVal }, + { topic: 'Reactor DO', payload: doVal } +];`; +reactorParse.outputs = 2; +reactorParse.wires = [["demo_gauge_overview_do"], ["demo_overview_template"]]; + +// Same for effluent parse - add template output +const effParse = byId("demo_fn_overview_eff_parse"); +effParse.func = `const p = msg.payload || {}; +const topic = msg.topic || ''; +const val = Number(p.mAbs); +if (!Number.isFinite(val)) return null; + +const rounded = Math.round(val*100)/100; + +// Route to appropriate gauge + template based on measurement type +if (topic.includes('TSS') || topic.includes('tss')) { + return [{ topic: 'Effluent TSS', payload: rounded }, null, { topic: 'Effluent TSS', payload: rounded }]; +} +if (topic.includes('NH4') || topic.includes('ammonium')) { + return [null, { topic: 'Effluent NH4', payload: rounded }, { topic: 'Effluent NH4', payload: rounded }]; +} +return [null, null, null];`; +effParse.outputs = 3; +effParse.wires = [["demo_gauge_overview_tss"], ["demo_gauge_overview_nh4"], ["demo_overview_template"]]; + +// ============================================= +// Validate +// ============================================= +const allIds = new Set(flow.map(n => n.id)); +let issues = 0; +for (const n of flow) { + if (!n.wires) continue; + for (const port of n.wires) { + for (const target of port) { + if (!allIds.has(target)) { + console.warn(`BROKEN WIRE: ${n.id} → ${target}`); + issues++; + } + } + } + if (n.type === 'link out' && n.links) { + for (const lt of n.links) { + if (!allIds.has(lt)) { console.warn(`BROKEN LINK OUT: ${n.id} → ${lt}`); issues++; } + } + } + if (n.type === 'link in' && n.links) { + for (const ls of n.links) { + if (!allIds.has(ls)) { console.warn(`BROKEN LINK IN: ${n.id} ← ${ls}`); issues++; } + } + } +} + +if (issues === 0) console.log('All references valid ✓'); +console.log('Total nodes:', flow.length); + +// Write +fs.writeFileSync(FLOW_PATH, JSON.stringify(flow, null, 2) + '\n'); +console.log(`Wrote ${FLOW_PATH}`); diff --git a/scripts/transform-flow-step4.js b/scripts/transform-flow-step4.js new file mode 100644 index 0000000..f44c4eb --- /dev/null +++ b/scripts/transform-flow-step4.js @@ -0,0 +1,613 @@ +#!/usr/bin/env node +/** + * Step 4: Manual Controls per PS Detail Page + * - Creates 3 PS detail pages (/ps-west, /ps-north, /ps-south) with control groups + * - Adds control widgets: mode switches, pump speed sliders + * - Format functions to convert dashboard inputs to process node messages + * - Link-in/out routing between dashboard tab and PS tabs + * - Per-PS monitoring charts on detail pages + */ +const fs = require('fs'); +const path = require('path'); + +const FLOW_PATH = path.join(__dirname, '..', 'docker', 'demo-flow.json'); +const flow = JSON.parse(fs.readFileSync(FLOW_PATH, 'utf8')); + +const byId = (id) => flow.find(n => n.id === id); + +// ============================================= +// Helper to create a standard set of controls for a PS +// ============================================= +function createPSDetailPage(config) { + const { + psKey, // 'west', 'north', 'south' + psLabel, // 'PS West', 'PS North', 'PS South' + pagePath, // '/ps-west' + pageOrder, // 2, 3, 4 + psNodeId, // 'demo_ps_west' + pumps, // [{id: 'demo_pump_w1', label: 'W1'}, ...] + controlModes, // ['levelbased','flowbased','manual'] + defaultMode, // 'levelbased' + maxFlow, // 300 + basinHeight, // 4 + tabId, // 'demo_tab_ps_west' + } = config; + + const prefix = `demo_ctrl_${psKey}`; + const nodes = []; + + // === Page === + nodes.push({ + id: `demo_ui_page_ps_${psKey}_detail`, + type: "ui-page", + name: `${psLabel} Detail`, + ui: "demo_ui_base", + path: pagePath, + icon: "water_drop", + layout: "grid", + theme: "demo_ui_theme", + breakpoints: [{ name: "Default", px: "0", cols: "12" }], + order: pageOrder, + className: "" + }); + + // === Groups === + nodes.push( + { + id: `${prefix}_grp_controls`, + type: "ui-group", + name: `${psLabel} Controls`, + page: `demo_ui_page_ps_${psKey}_detail`, + width: "6", + height: "1", + order: 1, + showTitle: true, + className: "" + }, + { + id: `${prefix}_grp_monitoring`, + type: "ui-group", + name: `${psLabel} Monitoring`, + page: `demo_ui_page_ps_${psKey}_detail`, + width: "6", + height: "1", + order: 2, + showTitle: true, + className: "" + }, + { + id: `${prefix}_grp_charts`, + type: "ui-group", + name: `${psLabel} Trends`, + page: `demo_ui_page_ps_${psKey}_detail`, + width: "12", + height: "1", + order: 3, + showTitle: true, + className: "" + } + ); + + // === PS Mode button group === + const modeOptions = controlModes.map(m => ({ + label: m === 'levelbased' ? 'Level' : m === 'flowbased' ? 'Flow' : m.charAt(0).toUpperCase() + m.slice(1), + value: m, + valueType: "str" + })); + + nodes.push({ + id: `${prefix}_mode`, + type: "ui-button-group", + z: "demo_tab_dashboard", + group: `${prefix}_grp_controls`, + name: `${psLabel} Mode`, + label: "Station Mode", + tooltip: "", + order: 1, + width: "6", + height: "1", + passthru: false, + options: modeOptions, + x: 120, y: 100 + pageOrder * 300, + wires: [[`${prefix}_fn_mode`]] + }); + + // Format: PS mode → setMode message + nodes.push({ + id: `${prefix}_fn_mode`, + type: "function", + z: "demo_tab_dashboard", + name: `Fmt ${psLabel} Mode`, + func: `msg.topic = 'setMode';\nmsg.payload = msg.payload;\nreturn msg;`, + outputs: 1, + x: 320, y: 100 + pageOrder * 300, + wires: [[`${prefix}_link_cmd_out`]] + }); + + // === Manual Flow slider === + nodes.push({ + id: `${prefix}_flow`, + type: "ui-slider", + z: "demo_tab_dashboard", + group: `${prefix}_grp_controls`, + name: `${psLabel} Flow`, + label: "Manual Flow (m\u00b3/h)", + tooltip: "", + order: 2, + width: "6", + height: "1", + passthru: false, + outs: "end", + min: 0, + max: maxFlow, + step: 1, + x: 120, y: 140 + pageOrder * 300, + wires: [[`${prefix}_fn_flow`]] + }); + + // Format: flow slider → q_in message + nodes.push({ + id: `${prefix}_fn_flow`, + type: "function", + z: "demo_tab_dashboard", + name: `Fmt ${psLabel} Flow`, + func: `msg.topic = 'q_in';\nmsg.payload = { value: Number(msg.payload), unit: 'm3/h' };\nreturn msg;`, + outputs: 1, + x: 320, y: 140 + pageOrder * 300, + wires: [[`${prefix}_link_cmd_out`]] + }); + + // === Pump controls === + pumps.forEach((pump, pIdx) => { + const yOff = 180 + pageOrder * 300 + pIdx * 80; + + // Pump mode button group + nodes.push({ + id: `${prefix}_pump_${pump.label.toLowerCase()}_mode`, + type: "ui-button-group", + z: "demo_tab_dashboard", + group: `${prefix}_grp_controls`, + name: `${pump.label} Mode`, + label: `${pump.label} Mode`, + tooltip: "", + order: 3 + pIdx * 2, + width: "3", + height: "1", + passthru: false, + options: [ + { label: "Auto", value: "auto", valueType: "str" }, + { label: "Virtual", value: "virtualControl", valueType: "str" }, + { label: "Physical", value: "fysicalControl", valueType: "str" } + ], + x: 120, y: yOff, + wires: [[`${prefix}_fn_pump_${pump.label.toLowerCase()}_mode`]] + }); + + // Format: pump mode + nodes.push({ + id: `${prefix}_fn_pump_${pump.label.toLowerCase()}_mode`, + type: "function", + z: "demo_tab_dashboard", + name: `Fmt ${pump.label} Mode`, + func: `msg.topic = 'setMode';\nmsg.payload = msg.payload;\nmsg._targetNode = '${pump.id}';\nreturn msg;`, + outputs: 1, + x: 320, y: yOff, + wires: [[`${prefix}_link_pump_${pump.label.toLowerCase()}_out`]] + }); + + // Pump speed slider + nodes.push({ + id: `${prefix}_pump_${pump.label.toLowerCase()}_speed`, + type: "ui-slider", + z: "demo_tab_dashboard", + group: `${prefix}_grp_controls`, + name: `${pump.label} Speed`, + label: `${pump.label} Speed (%)`, + tooltip: "", + order: 4 + pIdx * 2, + width: "3", + height: "1", + passthru: false, + outs: "end", + min: 0, + max: 100, + step: 1, + x: 120, y: yOff + 40, + wires: [[`${prefix}_fn_pump_${pump.label.toLowerCase()}_speed`]] + }); + + // Format: pump speed → execMovement + nodes.push({ + id: `${prefix}_fn_pump_${pump.label.toLowerCase()}_speed`, + type: "function", + z: "demo_tab_dashboard", + name: `Fmt ${pump.label} Speed`, + func: `msg.topic = 'execMovement';\nmsg.payload = { source: 'dashboard', action: 'setpoint', setpoint: Number(msg.payload) };\nmsg._targetNode = '${pump.id}';\nreturn msg;`, + outputs: 1, + x: 320, y: yOff + 40, + wires: [[`${prefix}_link_pump_${pump.label.toLowerCase()}_out`]] + }); + + // Link-out for pump commands (dashboard → PS tab) + nodes.push({ + id: `${prefix}_link_pump_${pump.label.toLowerCase()}_out`, + type: "link out", + z: "demo_tab_dashboard", + name: `→ ${pump.label} Cmd`, + mode: "link", + links: [`${prefix}_link_pump_${pump.label.toLowerCase()}_in`], + x: 520, y: yOff + 20 + }); + + // Link-in on PS tab + nodes.push({ + id: `${prefix}_link_pump_${pump.label.toLowerCase()}_in`, + type: "link in", + z: tabId, + name: `← ${pump.label} Cmd`, + links: [`${prefix}_link_pump_${pump.label.toLowerCase()}_out`], + x: 120, y: 540 + pIdx * 60, + wires: [[pump.id]] + }); + }); + + // === PS command link-out (dashboard → PS tab) === + nodes.push({ + id: `${prefix}_link_cmd_out`, + type: "link out", + z: "demo_tab_dashboard", + name: `→ ${psLabel} Cmd`, + mode: "link", + links: [`${prefix}_link_cmd_in`], + x: 520, y: 120 + pageOrder * 300 + }); + + // Link-in on PS tab for PS-level commands + nodes.push({ + id: `${prefix}_link_cmd_in`, + type: "link in", + z: tabId, + name: `← ${psLabel} Cmd`, + links: [`${prefix}_link_cmd_out`], + x: 120, y: 480, + wires: [[psNodeId]] + }); + + // === Monitoring widgets on detail page === + // Re-use existing data from the PS parse functions on dashboard tab + // Create a link-in to receive PS data and parse for detail page + + nodes.push({ + id: `${prefix}_link_detail_data_out`, + type: "link out", + z: tabId, + name: `→ ${psLabel} Detail`, + mode: "link", + links: [`${prefix}_link_detail_data_in`], + x: 1080, y: 400 + }); + + // Add to PS node wires[0] + const psNode = byId(psNodeId); + if (psNode && psNode.wires && psNode.wires[0]) { + psNode.wires[0].push(`${prefix}_link_detail_data_out`); + } + + nodes.push({ + id: `${prefix}_link_detail_data_in`, + type: "link in", + z: "demo_tab_dashboard", + name: `← ${psLabel} Detail`, + links: [`${prefix}_link_detail_data_out`], + x: 75, y: 50 + pageOrder * 300, + wires: [[`${prefix}_fn_detail_parse`]] + }); + + // Parse function for detail monitoring + nodes.push({ + id: `${prefix}_fn_detail_parse`, + type: "function", + z: "demo_tab_dashboard", + name: `Parse ${psLabel} Detail`, + func: `const p = msg.payload || {}; +const cache = context.get('c') || {}; +const keys = Object.keys(p); +const pick = (prefixes) => { for (const pfx of prefixes) { const k = keys.find(k => k.startsWith(pfx)); if (k) { const v = Number(p[k]); if (Number.isFinite(v)) return v; } } return null; }; + +const level = pick(['level.predicted.atequipment','level.measured.atequipment']); +const volume = pick(['volume.predicted.atequipment']); +const netFlow = pick(['netFlowRate.predicted.atequipment']); +const fillPct = pick(['volumePercent.predicted.atequipment']); +const direction = p.direction || cache.direction || '?'; + +if (level !== null) cache.level = level; +if (volume !== null) cache.volume = volume; +if (netFlow !== null) cache.netFlow = netFlow; +if (fillPct !== null) cache.fillPct = fillPct; +cache.direction = direction; +context.set('c', cache); + +const now = Date.now(); +const dirArrow = cache.direction === 'filling' ? '\\u2191' : cache.direction === 'emptying' ? '\\u2193' : '\\u2014'; +const status = [ + dirArrow + ' ' + (cache.direction || ''), + cache.netFlow !== undefined ? Math.abs(cache.netFlow).toFixed(0) + ' m\\u00b3/h' : '', +].filter(s => s.trim()).join(' | '); + +return [ + cache.level !== undefined ? {topic:'${psLabel} Level', payload: cache.level, timestamp: now} : null, + cache.netFlow !== undefined ? {topic:'${psLabel} Flow', payload: cache.netFlow, timestamp: now} : null, + {topic:'${psLabel} Status', payload: status}, + cache.fillPct !== undefined ? {payload: Number(cache.fillPct.toFixed(1))} : null, + cache.level !== undefined ? {payload: Number(cache.level.toFixed(2))} : null +];`, + outputs: 5, + x: 280, y: 50 + pageOrder * 300, + wires: [ + [`${prefix}_chart_level`], + [`${prefix}_chart_flow`], + [`${prefix}_text_status`], + [`${prefix}_gauge_fill`], + [`${prefix}_gauge_tank`] + ] + }); + + // Level chart + nodes.push({ + id: `${prefix}_chart_level`, + type: "ui-chart", + z: "demo_tab_dashboard", + group: `${prefix}_grp_charts`, + name: `${psLabel} Level`, + label: "Basin Level (m)", + order: 1, + width: "6", + height: "5", + chartType: "line", + category: "topic", + categoryType: "msg", + xAxisType: "time", + yAxisLabel: "m", + removeOlder: "10", + removeOlderUnit: "60", + action: "append", + pointShape: "false", + pointRadius: 0, + interpolation: "linear", + showLegend: true, + xAxisProperty: "", + xAxisPropertyType: "timestamp", + yAxisProperty: "payload", + yAxisPropertyType: "msg", + colors: ["#0094ce", "#FF7F0E", "#2CA02C"], + textColor: ["#aaaaaa"], + textColorDefault: false, + gridColor: ["#333333"], + gridColorDefault: false, + x: 510, y: 30 + pageOrder * 300, + wires: [] + }); + + // Flow chart + nodes.push({ + id: `${prefix}_chart_flow`, + type: "ui-chart", + z: "demo_tab_dashboard", + group: `${prefix}_grp_charts`, + name: `${psLabel} Flow`, + label: "Net Flow (m\u00b3/h)", + order: 2, + width: "6", + height: "5", + chartType: "line", + category: "topic", + categoryType: "msg", + xAxisType: "time", + yAxisLabel: "m\u00b3/h", + removeOlder: "10", + removeOlderUnit: "60", + action: "append", + pointShape: "false", + pointRadius: 0, + interpolation: "linear", + showLegend: true, + xAxisProperty: "", + xAxisPropertyType: "timestamp", + yAxisProperty: "payload", + yAxisPropertyType: "msg", + colors: ["#4fc3f7", "#FF7F0E", "#2CA02C"], + textColor: ["#aaaaaa"], + textColorDefault: false, + gridColor: ["#333333"], + gridColorDefault: false, + x: 510, y: 60 + pageOrder * 300, + wires: [] + }); + + // Status text + nodes.push({ + id: `${prefix}_text_status`, + type: "ui-text", + z: "demo_tab_dashboard", + group: `${prefix}_grp_monitoring`, + name: `${psLabel} Status`, + label: "Status", + order: 1, + width: "6", + height: "1", + format: "{{msg.payload}}", + layout: "row-spread", + x: 510, y: 80 + pageOrder * 300, + wires: [] + }); + + // Fill % gauge + nodes.push({ + id: `${prefix}_gauge_fill`, + type: "ui-gauge", + z: "demo_tab_dashboard", + group: `${prefix}_grp_monitoring`, + name: `${psLabel} Fill`, + gtype: "gauge-34", + gstyle: "Rounded", + title: "Fill", + units: "%", + prefix: "", + suffix: "%", + min: 0, + max: 100, + segments: [ + { color: "#f44336", from: 0 }, + { color: "#ff9800", from: 10 }, + { color: "#4caf50", from: 25 }, + { color: "#ff9800", from: 75 }, + { color: "#f44336", from: 90 } + ], + width: 3, + height: 3, + order: 2, + className: "", + x: 700, y: 80 + pageOrder * 300, + wires: [] + }); + + // Tank gauge + nodes.push({ + id: `${prefix}_gauge_tank`, + type: "ui-gauge", + z: "demo_tab_dashboard", + group: `${prefix}_grp_monitoring`, + name: `${psLabel} Tank`, + gtype: "gauge-tank", + gstyle: "Rounded", + title: "Level", + units: "m", + prefix: "", + suffix: "m", + min: 0, + max: basinHeight, + segments: [ + { color: "#f44336", from: 0 }, + { color: "#ff9800", from: basinHeight * 0.08 }, + { color: "#2196f3", from: basinHeight * 0.25 }, + { color: "#ff9800", from: basinHeight * 0.62 }, + { color: "#f44336", from: basinHeight * 0.8 } + ], + width: 3, + height: 4, + order: 3, + className: "", + x: 700, y: 40 + pageOrder * 300, + wires: [] + }); + + return nodes; +} + +// ============================================= +// Create detail pages for each PS +// ============================================= + +const westNodes = createPSDetailPage({ + psKey: 'west', + psLabel: 'PS West', + pagePath: '/ps-west', + pageOrder: 2, + psNodeId: 'demo_ps_west', + pumps: [ + { id: 'demo_pump_w1', label: 'W1' }, + { id: 'demo_pump_w2', label: 'W2' } + ], + controlModes: ['levelbased', 'flowbased', 'manual'], + defaultMode: 'levelbased', + maxFlow: 300, + basinHeight: 4, + tabId: 'demo_tab_ps_west', +}); + +const northNodes = createPSDetailPage({ + psKey: 'north', + psLabel: 'PS North', + pagePath: '/ps-north', + pageOrder: 3, + psNodeId: 'demo_ps_north', + pumps: [ + { id: 'demo_pump_n1', label: 'N1' } + ], + controlModes: ['levelbased', 'flowbased', 'manual'], + defaultMode: 'flowbased', + maxFlow: 200, + basinHeight: 3, + tabId: 'demo_tab_ps_north', +}); + +const southNodes = createPSDetailPage({ + psKey: 'south', + psLabel: 'PS South', + pagePath: '/ps-south', + pageOrder: 4, + psNodeId: 'demo_ps_south', + pumps: [ + { id: 'demo_pump_s1', label: 'S1' } + ], + controlModes: ['levelbased', 'flowbased', 'manual'], + defaultMode: 'manual', + maxFlow: 100, + basinHeight: 2.5, + tabId: 'demo_tab_ps_south', +}); + +flow.push(...westNodes, ...northNodes, ...southNodes); + +// ============================================= +// Validate +// ============================================= +const allIds = new Set(flow.map(n => n.id)); +let issues = 0; +// Check for duplicate IDs +const idCounts = {}; +flow.forEach(n => { idCounts[n.id] = (idCounts[n.id] || 0) + 1; }); +for (const [id, count] of Object.entries(idCounts)) { + if (count > 1) { console.warn(`DUPLICATE ID: ${id} (${count} instances)`); issues++; } +} + +for (const n of flow) { + if (!n.wires) continue; + for (const port of n.wires) { + for (const target of port) { + if (!allIds.has(target)) { + console.warn(`BROKEN WIRE: ${n.id} → ${target}`); + issues++; + } + } + } + if (n.type === 'link out' && n.links) { + for (const lt of n.links) { + if (!allIds.has(lt)) { console.warn(`BROKEN LINK OUT: ${n.id} → ${lt}`); issues++; } + } + } + if (n.type === 'link in' && n.links) { + for (const ls of n.links) { + if (!allIds.has(ls)) { console.warn(`BROKEN LINK IN: ${n.id} ← ${ls}`); issues++; } + } + } +} + +if (issues === 0) console.log('All references valid ✓'); +else console.log(`Found ${issues} issues`); + +// Count nodes per tab +const tabCounts = {}; +for (const n of flow) { + if (n.z) tabCounts[n.z] = (tabCounts[n.z] || 0) + 1; +} +console.log('Nodes per tab:', JSON.stringify(tabCounts, null, 2)); +console.log('Total nodes:', flow.length); + +// Count new nodes added +const newNodeCount = westNodes.length + northNodes.length + southNodes.length; +console.log(`Added ${newNodeCount} new nodes (${westNodes.length} west + ${northNodes.length} north + ${southNodes.length} south)`); + +// Write +fs.writeFileSync(FLOW_PATH, JSON.stringify(flow, null, 2) + '\n'); +console.log(`Wrote ${FLOW_PATH}`); diff --git a/scripts/update-demo-flow.js b/scripts/update-demo-flow.js new file mode 100644 index 0000000..39a9ae5 --- /dev/null +++ b/scripts/update-demo-flow.js @@ -0,0 +1,279 @@ +#!/usr/bin/env node +/** + * Script to update docker/demo-flow.json with Fixes 2-5 from the plan. + * Run from project root: node scripts/update-demo-flow.js + */ +const fs = require('fs'); +const path = require('path'); + +const flowPath = path.join(__dirname, '..', 'docker', 'demo-flow.json'); +const flow = JSON.parse(fs.readFileSync(flowPath, 'utf8')); + +// === Fix 2: Enable simulator on 9 measurement nodes === +const simMeasIds = [ + 'demo_meas_flow', 'demo_meas_do', 'demo_meas_nh4', + 'demo_meas_ft_n1', 'demo_meas_eff_flow', 'demo_meas_eff_do', + 'demo_meas_eff_nh4', 'demo_meas_eff_no3', 'demo_meas_eff_tss' +]; +simMeasIds.forEach(id => { + const node = flow.find(n => n.id === id); + if (node) { + node.simulator = true; + console.log('Enabled simulator on', id); + } else { + console.error('NOT FOUND:', id); + } +}); + +// === Fix 2: Remove 18 inject+function sim pairs === +const removeSimIds = [ + 'demo_inj_meas_flow', 'demo_fn_sim_flow', + 'demo_inj_meas_do', 'demo_fn_sim_do', + 'demo_inj_meas_nh4', 'demo_fn_sim_nh4', + 'demo_inj_ft_n1', 'demo_fn_sim_ft_n1', + 'demo_inj_eff_flow', 'demo_fn_sim_eff_flow', + 'demo_inj_eff_do', 'demo_fn_sim_eff_do', + 'demo_inj_eff_nh4', 'demo_fn_sim_eff_nh4', + 'demo_inj_eff_no3', 'demo_fn_sim_eff_no3', + 'demo_inj_eff_tss', 'demo_fn_sim_eff_tss' +]; + +// === Fix 5: Remove manual pump startup/setpoint injectors === +const removeManualIds = [ + 'demo_inj_w1_startup', 'demo_inj_w1_setpoint', + 'demo_inj_w2_startup', 'demo_inj_w2_setpoint', + 'demo_inj_n1_startup', + 'demo_inj_s1_startup' +]; + +const allRemoveIds = new Set([...removeSimIds, ...removeManualIds]); +const before = flow.length; +const filtered = flow.filter(n => !allRemoveIds.has(n.id)); +console.log(`Removed ${before - filtered.length} nodes (expected 24)`); + +// Remove wires to removed nodes from remaining nodes +filtered.forEach(n => { + if (n.wires && Array.isArray(n.wires)) { + n.wires = n.wires.map(wireGroup => { + if (Array.isArray(wireGroup)) { + return wireGroup.filter(w => !allRemoveIds.has(w)); + } + return wireGroup; + }); + } +}); + +// === Fix 3 (demo part): Add speedUpFactor to reactor === +const reactor = filtered.find(n => n.id === 'demo_reactor'); +if (reactor) { + reactor.speedUpFactor = 1; + console.log('Added speedUpFactor=1 to reactor'); +} + +// === Fix 4: Add pressure measurement nodes === +const maxY = Math.max(...filtered.filter(n => n.z === 'demo_tab_wwtp').map(n => n.y || 0)); + +const ptBaseConfig = { + scaling: true, + i_offset: 0, + smooth_method: 'mean', + count: 3, + category: 'sensor', + assetType: 'pressure', + enableLog: false, + logLevel: 'error', + positionIcon: '', + hasDistance: false +}; + +// Function to extract level from PS output and convert to hydrostatic pressure +const levelExtractFunc = [ + '// Extract basin level from PS output and convert to hydrostatic pressure (mbar)', + '// P = rho * g * h, rho=1000 kg/m3, g=9.81 m/s2', + 'const p = msg.payload || {};', + 'const keys = Object.keys(p);', + 'const levelKey = keys.find(k => k.startsWith("level.predicted.atequipment") || k.startsWith("level.measured.atequipment"));', + 'if (!levelKey) return null;', + 'const h = Number(p[levelKey]);', + 'if (!Number.isFinite(h)) return null;', + 'msg.topic = "measurement";', + 'msg.payload = Math.round(h * 98.1 * 10) / 10; // mbar', + 'return msg;' +].join('\n'); + +const newNodes = [ + // Comment + { + id: 'demo_comment_pressure', + type: 'comment', + z: 'demo_tab_wwtp', + name: '=== PRESSURE MEASUREMENTS (per pumping station) ===', + info: '', + x: 320, + y: maxY + 40 + }, + + // --- PS West upstream PT --- + { + id: 'demo_fn_level_to_pressure_w', + type: 'function', + z: 'demo_tab_wwtp', + name: 'Level\u2192Pressure (West)', + func: levelExtractFunc, + outputs: 1, + x: 370, + y: maxY + 80, + wires: [['demo_meas_pt_w_up']] + }, + { + id: 'demo_meas_pt_w_up', + type: 'measurement', + z: 'demo_tab_wwtp', + name: 'PT-W-UP (West Upstream)', + ...ptBaseConfig, + i_min: 0, i_max: 5000, o_min: 0, o_max: 5000, + simulator: false, + uuid: 'pt-w-up-001', + supplier: 'Endress+Hauser', + model: 'Cerabar-PMC51', + unit: 'mbar', + assetTagNumber: 'PT-W-UP', + positionVsParent: 'upstream', + x: 580, + y: maxY + 80, + wires: [['demo_link_process_out'], ['demo_link_influx_out'], ['demo_pump_w1', 'demo_pump_w2']] + }, + // PS West downstream PT (simulated) + { + id: 'demo_meas_pt_w_down', + type: 'measurement', + z: 'demo_tab_wwtp', + name: 'PT-W-DN (West Downstream)', + ...ptBaseConfig, + i_min: 0, i_max: 5000, o_min: 0, o_max: 5000, + simulator: true, + uuid: 'pt-w-dn-001', + supplier: 'Endress+Hauser', + model: 'Cerabar-PMC51', + unit: 'mbar', + assetTagNumber: 'PT-W-DN', + positionVsParent: 'downstream', + x: 580, + y: maxY + 140, + wires: [['demo_link_process_out'], ['demo_link_influx_out'], ['demo_pump_w1', 'demo_pump_w2']] + }, + + // --- PS North upstream PT --- + { + id: 'demo_fn_level_to_pressure_n', + type: 'function', + z: 'demo_tab_wwtp', + name: 'Level\u2192Pressure (North)', + func: levelExtractFunc, + outputs: 1, + x: 370, + y: maxY + 220, + wires: [['demo_meas_pt_n_up']] + }, + { + id: 'demo_meas_pt_n_up', + type: 'measurement', + z: 'demo_tab_wwtp', + name: 'PT-N-UP (North Upstream)', + ...ptBaseConfig, + i_min: 0, i_max: 5000, o_min: 0, o_max: 5000, + simulator: false, + uuid: 'pt-n-up-001', + supplier: 'Endress+Hauser', + model: 'Cerabar-PMC51', + unit: 'mbar', + assetTagNumber: 'PT-N-UP', + positionVsParent: 'upstream', + x: 580, + y: maxY + 220, + wires: [['demo_link_process_out'], ['demo_link_influx_out'], ['demo_pump_n1']] + }, + { + id: 'demo_meas_pt_n_down', + type: 'measurement', + z: 'demo_tab_wwtp', + name: 'PT-N-DN (North Downstream)', + ...ptBaseConfig, + i_min: 0, i_max: 5000, o_min: 0, o_max: 5000, + simulator: true, + uuid: 'pt-n-dn-001', + supplier: 'Endress+Hauser', + model: 'Cerabar-PMC51', + unit: 'mbar', + assetTagNumber: 'PT-N-DN', + positionVsParent: 'downstream', + x: 580, + y: maxY + 280, + wires: [['demo_link_process_out'], ['demo_link_influx_out'], ['demo_pump_n1']] + }, + + // --- PS South upstream PT --- + { + id: 'demo_fn_level_to_pressure_s', + type: 'function', + z: 'demo_tab_wwtp', + name: 'Level\u2192Pressure (South)', + func: levelExtractFunc, + outputs: 1, + x: 370, + y: maxY + 360, + wires: [['demo_meas_pt_s_up']] + }, + { + id: 'demo_meas_pt_s_up', + type: 'measurement', + z: 'demo_tab_wwtp', + name: 'PT-S-UP (South Upstream)', + ...ptBaseConfig, + i_min: 0, i_max: 5000, o_min: 0, o_max: 5000, + simulator: false, + uuid: 'pt-s-up-001', + supplier: 'Endress+Hauser', + model: 'Cerabar-PMC51', + unit: 'mbar', + assetTagNumber: 'PT-S-UP', + positionVsParent: 'upstream', + x: 580, + y: maxY + 360, + wires: [['demo_link_process_out'], ['demo_link_influx_out'], ['demo_pump_s1']] + }, + { + id: 'demo_meas_pt_s_down', + type: 'measurement', + z: 'demo_tab_wwtp', + name: 'PT-S-DN (South Downstream)', + ...ptBaseConfig, + i_min: 0, i_max: 5000, o_min: 0, o_max: 5000, + simulator: true, + uuid: 'pt-s-dn-001', + supplier: 'Endress+Hauser', + model: 'Cerabar-PMC51', + unit: 'mbar', + assetTagNumber: 'PT-S-DN', + positionVsParent: 'downstream', + x: 580, + y: maxY + 420, + wires: [['demo_link_process_out'], ['demo_link_influx_out'], ['demo_pump_s1']] + } +]; + +// Wire PS output port 0 to the level-to-pressure function nodes +const psWest = filtered.find(n => n.id === 'demo_ps_west'); +const psNorth = filtered.find(n => n.id === 'demo_ps_north'); +const psSouth = filtered.find(n => n.id === 'demo_ps_south'); + +if (psWest && psWest.wires[0]) psWest.wires[0].push('demo_fn_level_to_pressure_w'); +if (psNorth && psNorth.wires[0]) psNorth.wires[0].push('demo_fn_level_to_pressure_n'); +if (psSouth && psSouth.wires[0]) psSouth.wires[0].push('demo_fn_level_to_pressure_s'); + +// Combine and write +const result = [...filtered, ...newNodes]; +console.log(`Final flow has ${result.length} nodes`); + +fs.writeFileSync(flowPath, JSON.stringify(result, null, 2) + '\n'); +console.log('Done! Written to docker/demo-flow.json'); diff --git a/scripts/validate-nodes.sh b/scripts/validate-nodes.sh new file mode 100644 index 0000000..0c7ef14 --- /dev/null +++ b/scripts/validate-nodes.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# ============================================================= +# EVOLV Node Loading Validator +# Checks that all expected EVOLV nodes are loaded in Node-RED +# ============================================================= +set -e + +NODERED_URL="${NODERED_URL:-http://localhost:1880}" +TIMEOUT="${TIMEOUT:-30}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +# Expected EVOLV nodes (from package.json node-red.nodes) +EXPECTED_NODES="dashboardapi machineGroupControl measurement monster reactor rotatingMachine valve valveGroupControl pumpingstation settler" + +echo "==============================================" +echo " EVOLV Node Validation" +echo " URL: $NODERED_URL" +echo "==============================================" +echo "" + +# --------------------------------------------------------- +# Wait for Node-RED readiness +# --------------------------------------------------------- +printf "Waiting for Node-RED to be ready" +elapsed=0 +while [ $elapsed -lt $TIMEOUT ]; do + if curl -sf "$NODERED_URL/nodes" > /dev/null 2>&1; then + printf " ${GREEN}ready${NC} (%ds)\n" "$elapsed" + break + fi + printf "." + sleep 2 + elapsed=$((elapsed + 2)) +done + +if [ $elapsed -ge $TIMEOUT ]; then + printf " ${RED}timeout after %ds${NC}\n" "$TIMEOUT" + echo "Node-RED did not become ready. Check logs with: docker compose logs nodered" + exit 1 +fi + +echo "" + +# --------------------------------------------------------- +# Fetch loaded nodes from admin API +# --------------------------------------------------------- +NODES_JSON=$(curl -sf "$NODERED_URL/nodes" 2>/dev/null) +if [ -z "$NODES_JSON" ]; then + echo "${RED}ERROR${NC}: Failed to fetch /nodes from Node-RED" + exit 1 +fi + +# --------------------------------------------------------- +# Check each expected node +# --------------------------------------------------------- +LOADED=0 +MISSING=0 + +for node in $EXPECTED_NODES; do + # Check if the node type appears in the loaded nodes response + if echo "$NODES_JSON" | grep -qi "\"$node\""; then + printf " ${GREEN}LOADED${NC} %s\n" "$node" + LOADED=$((LOADED + 1)) + else + printf " ${RED}MISSING${NC} %s\n" "$node" + MISSING=$((MISSING + 1)) + fi +done + +# --------------------------------------------------------- +# Summary +# --------------------------------------------------------- +echo "" +TOTAL=$((LOADED + MISSING)) +echo "==============================================" +printf " Results: ${GREEN}%d loaded${NC}, ${RED}%d missing${NC} (of %d expected)\n" \ + "$LOADED" "$MISSING" "$TOTAL" +echo "==============================================" + +if [ $MISSING -gt 0 ]; then + echo "" + echo "Some nodes failed to load. Check Node-RED logs:" + echo " docker compose logs nodered | grep -i error" + exit 1 +fi + +exit 0 diff --git a/third_party/docs/README.md b/third_party/docs/README.md new file mode 100644 index 0000000..80e4b89 --- /dev/null +++ b/third_party/docs/README.md @@ -0,0 +1,40 @@ +# EVOLV Scientific & Technical Reference Library + +## Purpose + +This directory contains curated reference documents for EVOLV's domain-specialist agents. These summaries distill authoritative sources into actionable knowledge that agents should consult **before making scientific or engineering claims**. + +## How Agents Should Use This + +1. **Before making domain claims**: Read the relevant reference doc to verify your reasoning +2. **Cite sources**: When referencing scientific facts, point to the specific reference doc and its cited sources +3. **Acknowledge uncertainty**: If the reference docs don't cover a topic, say so rather than guessing +4. **Cross-reference with skills**: Combine these references with `.agents/skills/` SKILL.md files for implementation context + +## Index + +| File | Domain | Used By Agents | +|------|--------|---------------| +| [`asm-models.md`](asm-models.md) | Activated Sludge Models (ASM1-ASM3) | biological-process-engineer | +| [`settling-models.md`](settling-models.md) | Sludge Settling & Clarifier Models | biological-process-engineer | +| [`pump-affinity-laws.md`](pump-affinity-laws.md) | Pump Affinity Laws & Curve Theory | mechanical-process-engineer | +| [`pid-control-theory.md`](pid-control-theory.md) | PID Control for Process Applications | mechanical-process-engineer, node-red-runtime | +| [`signal-processing-sensors.md`](signal-processing-sensors.md) | Sensor Signal Conditioning | instrumentation-measurement | +| [`wastewater-compliance-nl.md`](wastewater-compliance-nl.md) | Dutch Wastewater Regulations | commissioning-compliance, biological-process-engineer | +| [`influxdb-schema-design.md`](influxdb-schema-design.md) | InfluxDB Time-Series Best Practices | telemetry-database | +| [`ot-security-iec62443.md`](ot-security-iec62443.md) | OT Security Standards | ot-security-integration | + +## Sources Directory + +The `sources/` subdirectory is for placing actual PDFs of scientific papers, standards, and technical manuals. Agents should prefer these curated summaries but can reference originals when available. + +## Validation Status + +All reference documents have been validated against authoritative sources including: +- IWA Scientific and Technical Reports (ASM models) +- Peer-reviewed publications (Takacs 1991, Vesilind, Burger-Diehl) +- Engineering Toolbox (pump affinity laws) +- ISA publications (Astrom & Hagglund PID control) +- IEC standards (61298, 62443) +- EU Directive 91/271/EEC (wastewater compliance) +- InfluxDB official documentation (schema design) diff --git a/third_party/docs/asm-models.md b/third_party/docs/asm-models.md new file mode 100644 index 0000000..2949b0b --- /dev/null +++ b/third_party/docs/asm-models.md @@ -0,0 +1,127 @@ +# Activated Sludge Models (ASM1, ASM2d, ASM3) + +> **Used by**: `biological-process-engineer` agent, `reactor` node, `monster` node +> **Validation**: Verified against IWA publications, WaterTAP documentation, and peer-reviewed literature + +## ASM1 — Activated Sludge Model No. 1 + +**Source**: Henze, M., Grady, C.P.L., Gujer, W., Marais, G.v.R., Matsuo, T. (1987). IAWPRC Task Group on Mathematical Modelling for Design and Operation of Biological Wastewater Treatment. + +**Published**: IWA Scientific and Technical Report No. 1 + +### 13 Components (State Variables) + +| Symbol | Description | Type | +|--------|-------------|------| +| S_I | Soluble inert organic matter | Soluble | +| S_S | Readily biodegradable substrate | Soluble | +| X_I | Particulate inert organic matter | Particulate | +| X_S | Slowly biodegradable substrate | Particulate | +| X_B,H | Active heterotrophic biomass | Particulate | +| X_B,A | Active autotrophic biomass | Particulate | +| X_P | Particulate products from biomass decay | Particulate | +| S_O | Dissolved oxygen | Soluble | +| S_NO | Nitrate and nitrite nitrogen | Soluble | +| S_NH | Ammonium nitrogen (NH₄⁺-N) | Soluble | +| S_ND | Soluble biodegradable organic nitrogen | Soluble | +| X_ND | Particulate biodegradable organic nitrogen | Particulate | +| S_ALK | Alkalinity (molar units) | Soluble | + +### 8 Processes + +1. **Aerobic growth of heterotrophs**: S_S + S_O + S_NH → X_B,H (Monod kinetics) +2. **Anoxic growth of heterotrophs**: S_S + S_NO + S_NH → X_B,H (denitrification) +3. **Aerobic growth of autotrophs**: S_NH + S_O → X_B,A + S_NO (nitrification) +4. **Decay of heterotrophs**: X_B,H → X_P + X_S (death-regeneration concept) +5. **Decay of autotrophs**: X_B,A → X_P + X_S +6. **Ammonification of soluble organic nitrogen**: S_ND → S_NH +7. **Hydrolysis of entrapped organics**: X_S → S_S +8. **Hydrolysis of entrapped organic nitrogen**: X_ND → S_ND + +### Key Kinetic Parameters (default values at 20°C) + +| Parameter | Symbol | Default | Unit | Description | +|-----------|--------|---------|------|-------------| +| Max. heterotrophic growth rate | μ_H | 6.0 | d⁻¹ | | +| Half-saturation (substrate) | K_S | 20.0 | g COD/m³ | | +| Half-saturation (oxygen, het.) | K_O,H | 0.2 | g O₂/m³ | | +| Half-saturation (nitrate) | K_NO | 0.5 | g NO₃-N/m³ | | +| Heterotrophic decay rate | b_H | 0.62 | d⁻¹ | | +| Max. autotrophic growth rate | μ_A | 0.8 | d⁻¹ | | +| Half-saturation (ammonia) | K_NH | 1.0 | g NH₃-N/m³ | | +| Half-saturation (oxygen, aut.) | K_O,A | 0.4 | g O₂/m³ | | +| Autotrophic decay rate | b_A | 0.05 | d⁻¹ | | +| Anoxic reduction factor | η_g | 0.8 | — | | +| Hydrolysis rate | k_h | 3.0 | g X_S/(g X_B,H · d) | | +| Yield (heterotrophic) | Y_H | 0.67 | g COD/g COD | | +| Yield (autotrophic) | Y_A | 0.24 | g COD/g N | | +| Fraction to X_P | f_P | 0.08 | — | | + +### Temperature Correction + +Arrhenius-type: k(T) = k(20) · θ^(T-20) + +Common θ values: +- Heterotrophic growth: θ = 1.072 +- Autotrophic growth: θ = 1.103 (nitrifiers are very temperature-sensitive) +- Decay: θ = 1.04 + +### Presentation Format + +The model is presented in the **Petersen matrix** (also called Gujer matrix) format, where rows are processes and columns are components. Each cell contains the stoichiometric coefficient for how a process affects a component. + +## ASM2d — Activated Sludge Model No. 2d + +**Source**: Henze, M., Gujer, W., Mino, T., Matsuo, T., Wentzel, M.C., Marais, G.v.R., van Loosdrecht, M.C.M. (1999) + +**Published**: IWA Scientific and Technical Report No. 3; also Water Science & Technology 39(1), 165-182 + +### Key Extensions over ASM1 +- Adds **biological phosphorus removal** by phosphorus accumulating organisms (PAOs) +- Includes **denitrifying PAOs** (simultaneous P-removal and denitrification) +- 19 components, 21 processes +- Models storage of poly-hydroxy-alkanoates (PHA) and polyphosphate (poly-P) by PAOs +- Includes fermentation of readily biodegradable substrate + +### Additional Components (beyond ASM1) +- S_F: Fermentable, readily biodegradable substrate +- S_A: Fermentation products (acetate) +- S_PO4: Soluble ortho-phosphate +- X_PAO: Phosphorus accumulating organisms +- X_PP: Poly-phosphate stored by PAOs +- X_PHA: Poly-hydroxy-alkanoates stored by PAOs + +## ASM3 — Activated Sludge Model No. 3 + +**Source**: Gujer, W., Henze, M., Mino, T., van Loosdrecht, M.C.M. (1999); updated in Henze et al. (2001) + +**Published**: IWA Scientific and Technical Report No. 9 + +### Key Differences from ASM1 +- **Replaces death-regeneration** with endogenous respiration (conceptually simpler) +- **Introduces storage polymers** (X_STO) for heterotrophic biomass — substrate is first stored, then used for growth +- 13 state variables, 12 reactions +- More suitable for dynamic simulation and control applications +- Eliminates the problematic simultaneous storage/growth ambiguity in ASM1 + +### Storage-Based Metabolism +In ASM3, heterotrophs first store readily biodegradable substrate as internal storage products (X_STO), then grow on these stored products. This two-step process better reflects observed biological behavior. + +## Choosing Between Models + +| Criterion | ASM1 | ASM2d | ASM3 | +|-----------|------|-------|------| +| Carbon & nitrogen | Yes | Yes | Yes | +| Phosphorus removal | No | Yes | Via separate Bio-P module | +| Computational cost | Low | High | Medium | +| Calibration effort | Low | High | Medium | +| Best for | Carbon/N only WWTPs | Bio-P plants | Dynamic control | + +## Authoritative References + +1. Henze, M. et al. (1987). "Activated Sludge Model No. 1" — IAWPRC Scientific and Technical Report No. 1 +2. Henze, M. et al. (1995). "Activated Sludge Model No. 2" — IAWQ Scientific and Technical Report No. 3 +3. Henze, M. et al. (1999). "Activated Sludge Model No. 2d" — Water Sci. Technol. 39(1), 165-182 +4. Gujer, W. et al. (1999). "Activated Sludge Model No. 3" — Water Sci. Technol. 39(1), 183-193 +5. Henze, M. et al. (2000). "Activated Sludge Models ASM1, ASM2, ASM2d and ASM3" — IWA Publishing, ISBN 9781900222242 +6. Jeppsson, U. (1996). "Modelling Aspects of Wastewater Treatment Processes" — Lund University PhD thesis (comprehensive ASM1 parameter listing) diff --git a/third_party/docs/influxdb-schema-design.md b/third_party/docs/influxdb-schema-design.md new file mode 100644 index 0000000..3d37955 --- /dev/null +++ b/third_party/docs/influxdb-schema-design.md @@ -0,0 +1,122 @@ +# InfluxDB Time-Series Best Practices + +> **Used by**: `telemetry-database` agent, `dashboardAPI` node +> **Validation**: Verified against InfluxDB official documentation (v1, v2, v3) + +## Tag vs. Field Decision Framework + +| Criterion | Use Tag | Use Field | +|-----------|---------|-----------| +| Queried in WHERE clause frequently | Yes | No | +| Used in GROUP BY | Yes | No | +| Low cardinality (< 100 distinct values) | Yes | Acceptable | +| High cardinality (IDs, timestamps, free text) | **Never** | Yes | +| Numeric measurement values | No | Yes | +| Needs aggregation (mean, sum, etc.) | No | Yes | +| Node/station/machine identifier | Yes | No | +| Actual sensor reading | No | Yes | +| Setpoint value | No | Yes | +| Quality flag | Depends* | Yes | + +*Quality flags: If you have ≤5 quality levels (good/uncertain/bad), a tag is acceptable. If quality is a numeric score, use a field. + +## EVOLV Tag/Field Convention + +### Standard Tags (low cardinality, indexed) +``` +locationId — Site identifier (e.g., "wwtp-brabant-01") +nodeType — Node type (e.g., "rotatingMachine", "reactor") +nodeName — Instance name (e.g., "pump-01", "reactor-A") +machineType — Equipment type (e.g., "pump", "blower", "valve") +stationId — Parent station identifier +measurementType — Sensor type (e.g., "flow", "pressure", "temperature") +``` + +### Standard Fields (not indexed, high cardinality) +``` +value — Primary measurement value +setpoint — Control setpoint +quality — Data quality score (0.0-1.0) +state — Machine state (numeric code) +power — Power consumption (W) +efficiency — Current efficiency (0.0-1.0) +speed — Rotational speed (RPM or fraction) +position — Valve position (0.0-1.0) +``` + +## Cardinality Management + +### What Is Cardinality? +Series cardinality = unique combinations of (measurement_name × tag_key_1 × tag_key_2 × ... × tag_key_n) + +### Cardinality Limits +- **InfluxDB v1/v2 (TSM engine)**: High cardinality degrades query performance and increases memory usage. Keep below ~1M series per database. +- **InfluxDB v3**: Supports infinite series cardinality (new storage engine), but keeping cardinality low still improves query speed. + +### Anti-Patterns (NEVER do these) +- Encoding timestamps in tag values +- Using UUIDs or session IDs as tags +- Free-text strings as tags +- Unbounded enum values as tags +- One measurement per sensor (use tags to differentiate instead) + +### Good Patterns +- Use a single measurement name per data category +- Differentiate by tags, not by measurement name +- Keep tag value sets bounded and predictable +- Document all tag values in a schema registry + +## Retention Policies + +### Three-Tier Strategy + +| Tier | Retention | Resolution | Purpose | +|------|-----------|------------|---------| +| Hot | 7-30 days | Full resolution (1s-10s) | Real-time dashboards, control loops | +| Warm | 90-365 days | Downsampled (1min-5min) | Trending, troubleshooting | +| Cold | 2-10 years | Heavily aggregated (1h-24h) | Compliance reporting, long-term trends | + +### EVOLV Recommended Defaults +- Port 1 data at full resolution: 30 days +- 1-minute aggregates: 1 year +- 1-hour aggregates: 5 years (matches regulatory retention requirements) + +## Continuous Queries / Tasks (Downsampling) + +### InfluxDB v1: Continuous Queries +```sql +CREATE CONTINUOUS QUERY "downsample_1m" ON "evolv" +BEGIN + SELECT mean("value") AS "value", max("value") AS "max", min("value") AS "min" + INTO "rp_warm"."downsampled_1m" + FROM "telemetry" + GROUP BY time(1m), * +END +``` + +### InfluxDB v2: Tasks +```flux +option task = {name: "downsample_1m", every: 1m} + +from(bucket: "telemetry") + |> range(start: -task.every) + |> aggregateWindow(every: 1m, fn: mean, createEmpty: false) + |> to(bucket: "telemetry-warm") +``` + +## Query Performance Tips + +1. **Always filter by time range first** — time is the primary index +2. **Use tag filters in WHERE** — tags are indexed, fields are not +3. **Avoid regex on tag values** — use exact matches when possible +4. **Limit series scanned** — filter by specific nodeType/nodeName +5. **Use aggregation** — let the database aggregate rather than fetching raw points +6. **Batch writes** — write in batches of 5,000-10,000 points for optimal throughput + +## Authoritative References + +1. InfluxDB Documentation — "Schema Design and Data Layout" (https://docs.influxdata.com/influxdb/v1/concepts/schema_and_data_layout/) +2. InfluxDB Documentation — "Schema Design Recommendations and Best Practices" (v2/v3) +3. InfluxData Blog — "Time Series Data, Cardinality, and InfluxDB" +4. InfluxDB Documentation — "Resolve High Series Cardinality" (https://docs.influxdata.com/influxdb/v2/write-data/best-practices/resolve-high-cardinality/) +5. InfluxData (2023). "InfluxDB Best Practices" — Official technical guides diff --git a/third_party/docs/ot-security-iec62443.md b/third_party/docs/ot-security-iec62443.md new file mode 100644 index 0000000..a49f791 --- /dev/null +++ b/third_party/docs/ot-security-iec62443.md @@ -0,0 +1,149 @@ +# OT Security Standards — IEC 62443 & NIST SP 800-82 + +> **Used by**: `ot-security-integration` agent +> **Validation**: Verified against IEC 62443 series, NIST SP 800-82, Dragos, and Rockwell Automation publications + +## IEC 62443 Framework Overview + +IEC 62443 "Industrial communication networks — IT security for networks and systems" is the primary international standard series for Industrial Automation and Control System (IACS) cybersecurity. + +### Standard Structure + +| Part | Title | Scope | +|------|-------|-------| +| 62443-1-x | General | Concepts, vocabulary, use cases | +| 62443-2-x | Policies & Procedures | Security management system, patch management | +| 62443-3-x | System | System security requirements, zones & conduits | +| 62443-4-x | Component | Secure development lifecycle, component requirements | + +### Key Parts for EVOLV +- **62443-3-2**: Security risk assessment and system design +- **62443-3-3**: System security requirements and security levels +- **62443-4-2**: Technical security requirements for IACS components + +## Zones and Conduits + +### Security Zone +A **zone** is a logical or physical grouping of assets that share common security requirements. Assets within a zone have the same security level (SL) target. + +### Conduit +A **conduit** is a logical or physical grouping of communication channels connecting two or more zones. Conduits require security controls appropriate for the traffic they carry. + +### EVOLV Zone Architecture (typical WWTP deployment) + +``` +Zone 0: Enterprise IT Network (SL 1-2) + ↕ [Conduit: Firewall/DMZ] +Zone 1: SCADA/Historian Network (SL 2-3) + ↕ [Conduit: Data diode or filtered bridge] +Zone 2: Process Control Network (SL 3) + ↕ [Conduit: Managed switch with ACLs] +Zone 3: Field Device Network (SL 2-3) + - PLCs, RTUs, I/O modules + - Node-RED edge runtime (EVOLV) + - Sensors and actuators +``` + +### Zone Design Rules +- A zone can contain sub-zones +- A conduit cannot contain sub-conduits +- A zone can have more than one conduit +- Every device must belong to exactly one zone +- Communication between zones must pass through a conduit + +## Security Levels (SL) + +| Level | Protection Against | Typical Measures | +|-------|-------------------|------------------| +| SL 0 | No specific requirements | — | +| SL 1 | Casual or coincidental violation | Basic authentication, logging | +| SL 2 | Intentional attack with low motivation, generic skills | Role-based access, encrypted communications | +| SL 3 | Intentional attack with moderate motivation, IACS-specific skills | Strong authentication, intrusion detection, hardened systems | +| SL 4 | Intentional attack with high motivation, IACS-specific skills, extended resources | Dedicated security team, continuous monitoring, zero-trust | + +### SL Types +- **SL-T (Target)**: Required security level for the zone +- **SL-A (Achieved)**: Actual security level implemented +- **SL-C (Capability)**: Maximum security level a component can support + +## NIST SP 800-82 — Guide to ICS Security + +**Source**: NIST Special Publication 800-82 Revision 3 (2023). "Guide to Operational Technology (OT) Security" + +### Key Recommendations +1. Develop and maintain an OT-specific security program +2. Segment OT networks from IT networks (defense in depth) +3. Apply least privilege access control +4. Monitor OT network traffic for anomalies +5. Maintain an accurate OT asset inventory +6. Implement secure remote access with MFA +7. Develop OT-specific incident response plans +8. Regularly assess and manage OT security risks + +## OPC UA Security Model + +### Authentication +- X.509 certificates for server and client authentication +- Username/password as fallback (less secure) +- Anonymous access (should be disabled in production) + +### Encryption +- Security policies define algorithm suites: + - `None` — No security (testing only) + - `Basic128Rsa15` — Deprecated, avoid + - `Basic256` — Deprecated, avoid + - `Basic256Sha256` — Minimum recommended + - `Aes128_Sha256_RsaOaep` — Preferred + - `Aes256_Sha256_RsaPss` — Strongest + +### Message Security Modes +- `None` — No signing or encryption +- `Sign` — Messages signed but not encrypted +- `SignAndEncrypt` — Full protection (recommended) + +## Modbus Security Considerations + +### Vulnerabilities (standard Modbus TCP) +- No authentication — any network client can read/write registers +- No encryption — all traffic is plaintext +- No integrity protection — commands can be modified in transit +- Predictable function codes — easy to craft malicious packets + +### Mitigations +1. Network segmentation — isolate Modbus devices in dedicated VLANs +2. Firewall rules — whitelist only authorized master IP addresses +3. Application-layer filtering — deep packet inspection for Modbus function codes +4. Monitoring — detect unusual register access patterns +5. Modbus/TCP Security (TLS) — available in newer implementations (RFC 7878-based) + +## EVOLV-Specific Security Considerations + +### Node-RED Admin Endpoints +- `GET //menu.js` — Serves configuration data to editor +- `GET //configData.js` — Serves runtime config to editor +- **Risk**: Information disclosure if exposed beyond editor network +- **Mitigation**: Bind Node-RED to localhost or trusted network only + +### msg.topic Input Validation +- All `msg.topic` handlers must validate topic format before processing +- Prevent topic injection: reject topics containing path separators, special characters +- Validate payload types and ranges before applying to control logic + +### Dynamic Configuration +- Configuration loaded from files or received via MQTT +- Must validate schema, types, and value ranges before applying +- Reject configurations that would violate safety envelopes + +### Control Message Safety +- Validate actuator commands against physical limits before sending +- Rate-limit control output changes (prevent rapid cycling) +- Log all control actions with timestamp, source, and reason + +## Authoritative References + +1. IEC 62443 series (2018-2024). "Industrial communication networks — IT security for networks and systems" +2. NIST SP 800-82 Rev 3 (2023). "Guide to Operational Technology (OT) Security" +3. Dragos Inc. — "Understanding ISA/IEC 62443: A Guide for OT Security Teams" (https://www.dragos.com/blog/isa-iec-62443-concepts) +4. ISA/IEC 62443-3-3 — "System Security Requirements and Security Levels" +5. OPC Foundation — "OPC UA Security Model" specification +6. Modbus Organization — "MODBUS/TCP Security" specification diff --git a/third_party/docs/pid-control-theory.md b/third_party/docs/pid-control-theory.md new file mode 100644 index 0000000..4a1ace7 --- /dev/null +++ b/third_party/docs/pid-control-theory.md @@ -0,0 +1,168 @@ +# PID Control for Process Applications + +> **Used by**: `mechanical-process-engineer` agent, `node-red-runtime` agent, `generalFunctions/src/pid/` +> **Validation**: Verified against Astrom & Hagglund (ISA, 2006) and MATLAB/Simulink documentation + +## Continuous PID Controller + +### Standard (ISA/Ideal) Form + +``` +u(t) = K_p · [e(t) + (1/T_i) · ∫e(τ)dτ + T_d · de(t)/dt] +``` + +Where: +- u(t) = controller output +- e(t) = error = setpoint - process variable (SP - PV) +- K_p = proportional gain +- T_i = integral time (seconds) +- T_d = derivative time (seconds) + +### Parallel Form + +``` +u(t) = K_p · e(t) + K_i · ∫e(τ)dτ + K_d · de(t)/dt +``` + +Where K_i = K_p/T_i and K_d = K_p · T_d + +## Discrete PID Implementation + +### Positional Form (absolute output) + +``` +u[k] = K_p · e[k] + K_i · Δt · Σe[j] + K_d · (e[k] - e[k-1]) / Δt +``` + +- Computes the absolute output value each cycle +- Requires tracking of the integral sum +- Subject to integral windup +- Used when the controller output directly sets an actuator position + +### Velocity Form (incremental output) + +``` +Δu[k] = K_p · (e[k] - e[k-1]) + K_i · Δt · e[k] + K_d · (e[k] - 2·e[k-1] + e[k-2]) / Δt +u[k] = u[k-1] + Δu[k] +``` + +- Computes the **change** in output each cycle +- Inherently bumpless on mode transfers (auto→manual→auto) +- Less prone to windup (but still needs anti-windup for output limits) +- Preferred for incremental actuators (VFDs, variable valves) + +## Anti-Windup Strategies + +Integral windup occurs when the controller output saturates (hits actuator limits) but the integral term continues to accumulate, causing large overshoot when the error changes sign. + +### 1. Clamping (Conditional Integration) + +Stop integrating when the output is saturated **and** the error has the same sign as the integral term: + +``` +if (u_raw > u_max || u_raw < u_min) && sign(e) == sign(integral): + freeze integral (do not accumulate) +else: + integral += e * Δt +``` + +- Simple to implement +- Effective for most process control applications +- The approach used in the EVOLV generalFunctions PID implementation + +### 2. Back-Calculation + +When the output saturates, feed back the difference between the saturated and unsaturated output to "unwind" the integrator: + +``` +integral += (K_i · e + K_b · (u_saturated - u_raw)) · Δt +``` + +Where K_b = 1/T_t (tracking time constant, typically T_t = √(T_i · T_d) or T_t = T_d). + +- More sophisticated than clamping +- Better performance for systems with large disturbances +- Recommended by Astrom & Hagglund for demanding applications + +### 3. Integrator Reset + +Reset the integrator to a value that would produce the saturated output: + +``` +if u_raw > u_max: + integral = (u_max - K_p · e) / K_i +``` + +- Simple but can be aggressive +- May cause discontinuities + +## Derivative Filtering + +The derivative term amplifies high-frequency noise. Always filter it: + +### First-Order Low-Pass Filter on D-Term + +``` +D_filtered[k] = α · D_filtered[k-1] + (1-α) · D_raw[k] +``` + +Where α = T_f / (T_f + Δt) and T_f = T_d / N (N typically 5-20, default 10). + +### Derivative on PV (not Error) + +To avoid derivative kick on setpoint changes: +``` +D = -K_d · (PV[k] - PV[k-1]) / Δt (instead of using error) +``` + +This is the standard approach for process control. + +## Cascade PID + +Two nested loops where the outer (master) loop's output is the setpoint for the inner (slave) loop. + +``` +[SP_outer] → [PID_outer] → [SP_inner] → [PID_inner] → [Actuator] → [Process] + ↑ ↑ + [PV_outer] ←──── [Process] ←── [PV_inner] +``` + +### Design Rules +- Inner loop must be **5-10x faster** than outer loop +- Tune inner loop first (with outer loop in manual) +- Then tune outer loop +- Anti-windup on outer loop essential (its output is bounded by inner loop's SP limits) + +### EVOLV Application +- Outer: Level controller → outputs flow setpoint +- Inner: Flow controller → outputs pump speed +- pumpingStation node coordinates this cascade + +## Tuning Methods + +### Ziegler-Nichols (Ultimate Gain Method) +1. Set I and D to zero, increase K_p until sustained oscillation +2. Record ultimate gain K_u and ultimate period T_u +3. Apply: K_p = 0.6·K_u, T_i = T_u/2, T_d = T_u/8 + +### Cohen-Coon (Process Reaction Curve) +1. Apply step change, record process reaction curve +2. Identify: gain K, dead time L, time constant τ +3. Apply formulas based on K, L, τ + +### Lambda Tuning (IMC-based) +1. Identify process as FOPDT: K, L, τ +2. Choose closed-loop time constant λ (typically λ = max(3·L, τ)) +3. K_p = τ/(K·λ), T_i = τ + +- **Preferred for process control** — gives non-oscillatory response +- Directly specifies desired closed-loop speed +- Robust to model uncertainty when λ is chosen conservatively + +## Authoritative References + +1. Astrom, K.J. & Hagglund, T. (2006). "Advanced PID Control." ISA — The Instrumentation, Systems, and Automation Society. +2. Astrom, K.J. & Hagglund, T. (1995). "PID Controllers: Theory, Design, and Tuning." 2nd ed., ISA. +3. Seborg, D.E. et al. (2011). "Process Dynamics and Control." 3rd ed., Wiley. (Chapter on PID control) +4. MATLAB/Simulink documentation — "Anti-Windup Control Using PID Controller Block" +5. Smith, C.A. & Corripio, A.B. (2005). "Principles and Practices of Automatic Process Control." 3rd ed., Wiley. diff --git a/third_party/docs/pump-affinity-laws.md b/third_party/docs/pump-affinity-laws.md new file mode 100644 index 0000000..35194fc --- /dev/null +++ b/third_party/docs/pump-affinity-laws.md @@ -0,0 +1,154 @@ +# Pump Affinity Laws & Curve Theory + +> **Used by**: `mechanical-process-engineer` agent, `rotatingMachine` node, `pumpingStation` node +> **Validation**: Verified against Engineering Toolbox, Hydraulic Institute standards, and ScienceDirect + +## Affinity Laws + +The affinity laws describe how centrifugal pump performance scales with changes in rotational speed (N) or impeller diameter (D). They are derived from dimensional analysis under the assumption of geometric similarity (velocity triangles at the impeller remain geometrically similar). + +### Speed Variation (constant diameter) + +``` +Q₂/Q₁ = N₂/N₁ +H₂/H₁ = (N₂/N₁)² +P₂/P₁ = (N₂/N₁)³ +``` + +### Diameter Variation (constant speed) + +``` +Q₂/Q₁ = D₂/D₁ +H₂/H₁ = (D₂/D₁)² +P₂/P₁ = (D₂/D₁)³ +``` + +### Combined Variation + +``` +Q₂/Q₁ = (N₂/N₁) · (D₂/D₁) +H₂/H₁ = (N₂/N₁)² · (D₂/D₁)² +P₂/P₁ = (N₂/N₁)³ · (D₂/D₁)³ +``` + +### Practical Example (validated against Engineering Toolbox) +A pump at 1750 rpm delivering 100 gpm at 100 ft head using 5 bhp: +- At 3500 rpm: **200 gpm** flow, **400 ft** head, **40 bhp** power +- A 10% speed increase yields: +10% flow, +21% head, +33% power + +### Accuracy Limitations +- Affinity laws are **approximate** — accuracy decreases with large speed changes (>±30%) +- Efficiency shifts slightly with speed change (not captured by basic affinity laws) +- Trimming impeller diameter >15-20% significantly reduces accuracy +- Laws assume no significant change in Reynolds number effects + +## Pump Curve Types + +### Q-H Curve (Flow vs. Head) +- Primary performance curve +- Head decreases as flow increases (for centrifugal pumps) +- Shape depends on specific speed (Ns): flat, steep, or drooping +- **Monotonicity**: Should be monotonically decreasing for stable operation. Non-monotonic (drooping) curves can cause instability in parallel operation. + +### Q-P Curve (Flow vs. Power) +- Power consumption as function of flow +- Shape varies by pump type: + - Radial: power increases with flow (non-overloading possible at shutoff) + - Mixed flow: relatively flat + - Axial: power **decreases** with flow (overload risk at low flow) + +### Q-η Curve (Flow vs. Efficiency) +- Efficiency peaks at Best Efficiency Point (BEP) +- Falls off on both sides of BEP +- Operating far from BEP causes excessive vibration, cavitation risk, and energy waste + +## Best Efficiency Point (BEP) + +The BEP is the operating point where the pump converts the maximum fraction of input power to useful hydraulic work. + +### BEP Tracking Under VFD Control +When speed changes via VFD, the BEP shifts along a **parabolic path** in the Q-H plane: +``` +H_BEP ∝ Q_BEP² +``` +This is because both Q and H scale with speed, but H scales as the square of Q's scaling factor. + +### Preferred Operating Region +- Continuous operation: 80% – 110% of BEP flow +- Allowable range: 70% – 120% of BEP flow +- Outside this range: increased bearing loads, seal wear, cavitation risk + +## System Curve Theory + +The system curve describes the head required by the piping system as a function of flow: + +``` +H_system = H_static + k · Q² +``` + +Where: +- H_static = static head (elevation difference + tank pressure difference) +- k = system resistance coefficient (Pa·s²/m⁶ in SI) +- Q = volumetric flow rate + +### Duty Point +The **duty point** (operating point) is the intersection of the pump curve and system curve: +``` +H_pump(Q) = H_system(Q) +``` +This is solved numerically — find Q where pump curve equals system curve. + +## Parallel Pump Operation + +### Flow Summation Rule +For pumps in parallel at equal head: +``` +Q_total = Q₁ + Q₂ + ... + Qₙ (at each head value) +``` +The combined curve is constructed by **horizontally adding** individual pump curves. + +### Key Considerations +- Each pump must overcome the same system head +- Adding a pump shifts the combined curve right, moving the operating point +- Diminishing returns: each additional pump adds less incremental flow +- Risk of back-flow through stopped pumps (check valves required) +- Unstable operation if pump curves have a drooping characteristic + +## Series Pump Operation + +For pumps in series at equal flow: +``` +H_total = H₁ + H₂ + ... + Hₙ (at each flow value) +``` +The combined curve is constructed by **vertically adding** individual pump curves. + +## Specific Energy + +The key energy KPI for pumping systems: + +``` +SE = P / Q [W / (m³/s) = J/m³] +``` + +More commonly expressed as: +``` +SE = P / Q [kWh/m³] (with appropriate unit conversion) +``` + +Where: +- P = electrical power input (kW) +- Q = volumetric flow rate (m³/h) +- SE = P / Q · (1/1000) for kWh/m³ when P in W and Q in m³/s + +### Wire-to-Water Efficiency +``` +η_total = η_motor · η_VFD · η_pump = (ρ · g · Q · H) / P_electrical +``` + +## Authoritative References + +1. Karassik, I.J. et al. "Pump Handbook" 4th ed. — McGraw-Hill (comprehensive pump engineering reference) +2. Europump/Hydraulic Institute (2001). "Pump Life Cycle Costs: A Guide to LCC Analysis for Pumping Systems" +3. Engineering Toolbox — "Affinity Laws for Pumps" (https://www.engineeringtoolbox.com/affinity-laws-d_408.html) +4. Hydraulic Institute Standards (HI 9.6.1 — Rotodynamic Pumps Guideline for NPSH Margin) +5. Gülich, J.F. (2014). "Centrifugal Pumps" 3rd ed. — Springer (theoretical foundation) diff --git a/third_party/docs/settling-models.md b/third_party/docs/settling-models.md new file mode 100644 index 0000000..2d19ac6 --- /dev/null +++ b/third_party/docs/settling-models.md @@ -0,0 +1,130 @@ +# Sludge Settling & Clarifier Models + +> **Used by**: `biological-process-engineer` agent, `settler` node +> **Validation**: Verified against Takacs et al. (1991), Vesilind (1968), and Burger-Diehl framework publications + +## Vesilind Model — Zone Settling Velocity + +**Source**: Vesilind, P.A. (1968). "Design of Prototype Thickeners from Batch Settling Tests." Water Sewage Works, 115, 302-307. + +### Equation + +``` +v_s = v_0 · exp(-k · X) +``` + +Where: +- v_s = settling velocity (m/h) +- v_0 = maximum initial settling velocity (m/h) +- k = settling parameter (m³/kg or L/g) +- X = suspended solids concentration (kg/m³ or g/L) + +### Typical Parameter Ranges for Municipal Wastewater + +| Parameter | Typical Range | Unit | Notes | +|-----------|---------------|------|-------| +| v_0 | 4 – 12 | m/h | ~7.8 m/h is a commonly observed average | +| k | 0.3 – 0.8 | m³/kg | Correlates with SVI; higher SVI → higher k | + +### SVI Correlation + +The settling parameter k can be estimated from Sludge Volume Index: +- k ≈ 0.16 + 0.003 · SVI (for SVI in mL/g, k in m³/kg) +- Better correlations use SSVI (Stirred SVI) or DSVI (Diluted SVI) + +### Limitations +- Only describes **zone settling** (hindered settling of a blanket) +- Does not capture compression settling at high concentrations +- Does not model the clarification zone (low-concentration region above blanket) + +## Takacs Model — Double-Exponential Settling + +**Source**: Takacs, I., Patry, G.G., Nolasco, D. (1991). "A dynamic model of the clarification-thickening process." Water Research, 25(10), 1263-1271. + +### Equation + +``` +v_s = v_0 · (exp(-r_h · (X - X_min)) - exp(-r_p · (X - X_min))) +``` + +Where: +- v_s = settling velocity (m/h) +- v_0 = maximum Vesilind settling velocity (m/h) +- r_h = hindered settling parameter (m³/kg) +- r_p = flocculent settling parameter (m³/kg) +- X = suspended solids concentration (kg/m³) +- X_min = non-settleable fraction (kg/m³) + +### Key Innovation +The double-exponential form captures **both** the clarification zone (low concentrations, dominated by the r_p term) and the thickening zone (high concentrations, dominated by the r_h term). This allows simulation of the complete solids profile from effluent to underflow. + +### Typical Parameter Values + +| Parameter | Typical Range | Default | Unit | +|-----------|---------------|---------|------| +| v_0 | 4 – 12 | 7.5 | m/h | +| r_h | 0.3 – 0.8 | 0.576 | m³/kg | +| r_p | 2.0 – 6.0 | 2.86 | m³/kg | +| X_min | 0 – 0.1 | 0.01 | kg/m³ | + +### Sensitivity +- **r_p** is the most sensitive parameter — it governs effluent suspended solids +- Takacs et al. recommend finding r_p by simulation/calibration +- v_0 and r_h primarily affect the sludge blanket position and underflow concentration + +### 1D Layer Model Implementation +The settler is divided into N horizontal layers (typically 10-30). For each layer: +1. Calculate settling velocity from local concentration +2. Apply solids flux theory (gravity flux + bulk flux) +3. Update concentration via mass balance +4. Handle feed layer, overflow, and underflow boundary conditions + +## Burger-Diehl Framework — PDE-Based 1D Settler + +**Source**: Burger, R., Diehl, S. and various co-authors (2011-present). Multiple publications developing the framework. + +### Key Characteristics +- Based on rigorous **partial differential equation** theory (hyperbolic-elliptic PDE) +- Accounts for hindered settling, compression settling, and inlet dispersion +- Every implementation detail is consistent with PDE theory (unlike ad-hoc layer models) +- More realistic prediction of underflow sludge concentration +- Essential for accurate wet-weather modelling + +### Advantages Over Takacs Layer Model +- Proper handling of compression settling (important at high MLSS) +- Mathematically rigorous — convergence guaranteed +- Better sludge blanket dynamics during storm events +- Can be extended with reactive terms (ASM1 biokinetics inside settler) + +### When to Use Which Model + +| Scenario | Recommended Model | +|----------|-------------------| +| Steady-state design | Vesilind + flux theory | +| Dynamic simulation (standard) | Takacs 1D layer model | +| Wet-weather / high-MLSS dynamics | Burger-Diehl PDE model | +| Quick estimation | Vesilind with SVI correlation | + +## Flux Theory for Clarifier Design + +The solids flux approach combines the gravity settling flux with the bulk (underflow) flux: + +``` +J_total = J_gravity + J_bulk = v_s(X) · X + Q_u/A · X +``` + +Where: +- J_total = total solids flux (kg/m²/h) +- v_s(X) = settling velocity at concentration X (from Vesilind or Takacs) +- Q_u = underflow rate (m³/h) +- A = clarifier surface area (m²) + +The **limiting flux** determines the maximum solids loading rate — operating above this causes blanket rise and eventual washout. + +## Authoritative References + +1. Vesilind, P.A. (1968). "Design of Prototype Thickeners from Batch Settling Tests." Water Sewage Works, 115, 302-307. +2. Takacs, I., Patry, G.G., Nolasco, D. (1991). "A dynamic model of the clarification-thickening process." Water Res. 25(10), 1263-1271. +3. Burger, R., Diehl, S., Nopens, I. (2011). "A consistent modelling methodology for secondary settling tanks in wastewater treatment." Water Res. 45(6), 2247-2260. +4. Torfs, E. (2015). "Different settling regimes in secondary settling tanks." PhD thesis, Ghent University. +5. Daigger, G.T. (1995). "Development of refined clarifier operating diagrams using an updated settling characteristics database." Water Environment Research, 67(1), 95-100. diff --git a/third_party/docs/signal-processing-sensors.md b/third_party/docs/signal-processing-sensors.md new file mode 100644 index 0000000..1c39f41 --- /dev/null +++ b/third_party/docs/signal-processing-sensors.md @@ -0,0 +1,157 @@ +# Sensor Signal Conditioning & Data Quality + +> **Used by**: `instrumentation-measurement` agent, `measurement` node +> **Validation**: Verified against IEC 61298, sensor manufacturer literature, and signal processing references + +## Signal Conditioning Pipeline + +``` +Raw Signal → Scaling → Filtering → Outlier Rejection → Quality Flagging → Output +``` + +## Scaling: Engineering Unit Conversion + +### 4-20 mA Standard +``` +value = range_min + (I - 4) / (20 - 4) · (range_max - range_min) +``` + +Where I is the measured current in mA. + +### Key Rules +- 0 mA = wire break (fault condition) +- < 4 mA = under-range or fault +- 4 mA = range minimum (0%) +- 20 mA = range maximum (100%) +- > 20 mA = over-range or fault +- NAMUR NE43 recommends 3.8 mA and 20.5 mA as fault thresholds + +## Filtering Methods + +### Moving Average +``` +y[k] = (1/N) · Σ x[k-i] for i = 0 to N-1 +``` +- Simple, effective for white noise +- Introduces phase lag proportional to (N-1)/2 samples +- Good for steady-state signals, poor for fast transients + +### Exponential Moving Average (EMA) +``` +y[k] = α · x[k] + (1-α) · y[k-1] +``` +Where α = 2/(N+1) or α = Δt/(τ + Δt) for time-constant-based tuning. +- Less memory than moving average +- Equivalent to first-order low-pass filter +- τ (time constant) sets the cutoff frequency: f_c = 1/(2π·τ) + +### Savitzky-Golay Filter +- Fits a polynomial to a window of data points, uses the polynomial value as the filtered output +- Preserves higher-order moments (peaks, edges) better than moving average +- Configurable by window size and polynomial order +- Typical: window = 5-11 points, order = 2-3 + +## Outlier Detection + +### Z-Score Method +``` +z = |x - μ| / σ +outlier if z > threshold (typically 3.0) +``` +- Assumes normal distribution +- Sensitive to the outliers themselves (they inflate σ) + +### Modified Z-Score (MAD-based) +``` +MAD = median(|x_i - median(x)|) +modified_z = 0.6745 · (x - median(x)) / MAD +outlier if |modified_z| > threshold (typically 3.5) +``` +- Robust to outliers (uses median instead of mean) +- **Recommended for process measurements** where occasional spikes are common +- 0.6745 is the 75th percentile of the standard normal distribution + +### IQR Method +``` +Q1 = 25th percentile, Q3 = 75th percentile +IQR = Q3 - Q1 +outlier if x < Q1 - 1.5·IQR or x > Q3 + 1.5·IQR +``` +- Non-parametric, no distribution assumption +- Common in exploratory data analysis +- Less suitable for real-time streaming (needs window of data) + +## NRMSE for Drift Detection + +Normalized Root Mean Square Error compares a recent measurement window against a reference window to detect sensor drift. + +### Calculation +``` +RMSE = √(Σ(x_i - x_ref_i)² / N) +NRMSE = RMSE / (x_max - x_min) or RMSE / x_mean +``` + +### Thresholds (typical for process sensors) +| NRMSE Range | Quality | Action | +|-------------|---------|--------| +| 0 – 0.05 | Good | Normal operation | +| 0.05 – 0.15 | Uncertain | Flag for review, increase monitoring | +| 0.15 – 0.30 | Poor | Alarm, reduce weight in control loops | +| > 0.30 | Bad | Remove from control, maintenance required | + +### Reference Window Selection +- Calibration data (gold standard) +- Post-maintenance baseline +- Rolling reference from a known-good period +- Multi-sensor cross-validation + +## Sensor Accuracy Classes + +### IEC 61298 Framework +IEC 61298 "Process measurement and control devices — General methods and procedures for evaluating performance" defines standardized test methods for evaluating sensor accuracy under reference and influence conditions. + +### Key Performance Metrics +- **Accuracy**: Closeness of measured value to true value (includes systematic and random errors) +- **Repeatability**: Closeness of successive measurements under identical conditions +- **Hysteresis**: Maximum difference between upscale and downscale readings +- **Linearity**: Maximum deviation from a straight line between zero and span +- **Deadband**: Smallest change in input that produces a detectable output change + +### Common Accuracy Specifications +| Sensor Type | Typical Accuracy | Response Time | +|-------------|-----------------|---------------| +| Pressure transmitter | ±0.04 – 0.1% FS | < 100 ms | +| Flow meter (electromagnetic) | ±0.2 – 0.5% of reading | 1-3 s | +| Temperature (RTD/Pt100) | ±0.1 – 0.3°C | 5-30 s (depends on housing) | +| Level (ultrasonic) | ±0.25% FS | 1-5 s | +| pH | ±0.02 – 0.1 pH | 10-60 s | +| Dissolved oxygen | ±1-2% of reading | 30-90 s (membrane) | +| Turbidity (nephelometric) | ±2% of reading | 5-15 s | +| Ammonia (ion-selective) | ±5-10% of reading | 60-180 s | + +## Sensor States and Warmup + +### State Machine +``` +Maintenance → Warmup → Active → Cooldown → Maintenance +``` + +### Warmup Behavior +- **Duration**: Varies by sensor type (seconds for pressure, minutes for pH, hours for dissolved oxygen) +- **During warmup**: Measurements flagged as "uncertain" quality +- **Completion criterion**: Readings stabilize within defined tolerance for a minimum duration +- **EVOLV convention**: Warmup state prevents measurements from propagating to control loops + +### Stabilization Detection +``` +stable if std_dev(last_N_readings) < threshold for T_stable seconds +``` + +## Authoritative References + +1. IEC 61298 series (2008). "Process measurement and control devices — General methods and procedures for evaluating performance" +2. IEC 61326-2-3. "Electrical equipment for measurement, control and laboratory use — EMC requirements — Part 2-3: Particular requirements — Transducers with integrated or remote signal conditioning" +3. NAMUR NE43 (2003). "Standardization of the Signal Level for the Failure Information of Digital Transmitters" +4. Bently Nevada / Baker Hughes. "Fundamentals of Rotating Machinery Diagnostics" +5. Oppenheim, A.V. & Willsky, A.S. (1997). "Signals & Systems" 2nd ed., Prentice Hall +6. Press, W.H. et al. (2007). "Numerical Recipes" 3rd ed., Chapter 14 (Savitzky-Golay filters) diff --git a/third_party/docs/sources/.gitkeep b/third_party/docs/sources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/third_party/docs/sources/README.md b/third_party/docs/sources/README.md new file mode 100644 index 0000000..8d07874 --- /dev/null +++ b/third_party/docs/sources/README.md @@ -0,0 +1,21 @@ +# Source Documents + +Place actual scientific papers, standards, and technical manuals here. Reference them from the summary files in the parent directory. + +## Suggested Sources to Add + +- IWA Scientific and Technical Report No. 1 — ASM1 (Henze et al., 1987) +- IWA Scientific and Technical Report No. 3 — ASM2d (Henze et al., 1999) +- IWA Scientific and Technical Report No. 9 — ASM3 (Gujer et al., 1999) +- Takacs et al. (1991) "A dynamic model of the clarification-thickening process" Water Res. 25(10), 1263-1271 +- Astrom & Hagglund (2006) "Advanced PID Control" ISA +- Karassik et al. "Pump Handbook" McGraw-Hill +- Europump/Hydraulic Institute "Pump Life Cycle Costs" +- IEC 62443 series (OT security) +- IEC 61298 series (process measurement) +- EU Directive 91/271/EEC (Urban Waste Water Treatment) +- NIST SP 800-82 Rev 3 (Guide to ICS Security) + +## File Naming Convention + +`-.pdf` — e.g., `takacs-1991-clarification-thickening.pdf` diff --git a/third_party/docs/wastewater-compliance-nl.md b/third_party/docs/wastewater-compliance-nl.md new file mode 100644 index 0000000..1c6eaa0 --- /dev/null +++ b/third_party/docs/wastewater-compliance-nl.md @@ -0,0 +1,117 @@ +# Dutch Wastewater Regulations & Compliance + +> **Used by**: `commissioning-compliance` agent, `biological-process-engineer` agent +> **Validation**: Verified against EU Directive 91/271/EEC, Activiteitenbesluit milieubeheer, and Dutch water authority publications + +## Regulatory Framework + +### European Level +- **EU Urban Waste Water Treatment Directive 91/271/EEC** — Primary directive governing collection, treatment, and discharge of urban wastewater across all EU member states +- **Water Framework Directive 2000/60/EC** — Establishes river basin management and environmental quality standards +- **Revised UWWTD (2024)** — Updated directive with stricter nutrient limits and energy neutrality targets + +### Dutch National Level +- **Waterwet** (Water Act) — National water management framework +- **Waterschapswet** (Water Authority Act) — Governance of regional water authorities +- **Activiteitenbesluit milieubeheer** — General rules for environmental activities including wastewater discharge +- **Besluit lozing afvalwater huishoudens** — Rules for domestic wastewater discharge + +### Regional Level +- **Waterschap Brabantse Delta** — Regional water authority managing the target WWTP +- Operates under national framework with site-specific discharge permits (watervergunning) + +## EU UWWTD Effluent Standards (Annex I) + +### Table 1: Secondary Treatment Requirements + +| Parameter | Concentration | Min. Reduction | +|-----------|--------------|----------------| +| BOD₅ (at 20°C, without nitrification) | 25 mg/L O₂ | 70-90% | +| COD | 125 mg/L O₂ | 75% | +| TSS (Total Suspended Solids) | 35 mg/L | 90% (>10,000 p.e.) | +| TSS (Total Suspended Solids) | 60 mg/L | 70% (2,000-10,000 p.e.) | + +### Table 2: Nutrient Requirements for Sensitive Areas + +| Parameter | Concentration | Min. Reduction | +|-----------|--------------|----------------| +| Total Phosphorus (10,000-100,000 p.e.) | 2 mg/L P | 80% | +| Total Phosphorus (>100,000 p.e.) | 1 mg/L P | 80% | +| Total Nitrogen (10,000-100,000 p.e.) | 15 mg/L N | 70-80% | +| Total Nitrogen (>100,000 p.e.) | 10 mg/L N | 70-80% | + +*Note: The Netherlands designated its entire territory as a "sensitive area" under the UWWTD, meaning nutrient requirements (Table 2) apply to all significant WWTPs.* + +### Dutch Practice (Often Stricter) +Dutch water authorities commonly set stricter limits than the EU minimum: + +| Parameter | Typical Dutch Permit Limit | EU Minimum | +|-----------|---------------------------|------------| +| N-total | 5-10 mg/L | 10-15 mg/L | +| P-total | 0.3-1.0 mg/L | 1-2 mg/L | +| BOD₅ | 5-10 mg/L | 25 mg/L | +| COD | 50-100 mg/L | 125 mg/L | +| TSS | 10-20 mg/L | 35 mg/L | +| NH₄-N | 1-2 mg/L | Not specified in UWWTD | + +*Note: Actual permit limits are site-specific. The values above represent common ranges for Dutch WWTPs.* + +## Monitoring and Reporting Obligations + +### Sampling Requirements (UWWTD Annex I, Table 3) +| Plant Size | Min. Annual Samples | +|------------|-------------------| +| 2,000-9,999 p.e. | 12 | +| 10,000-49,999 p.e. | 12 | +| ≥50,000 p.e. | 24 | + +### Compliance Assessment +- Based on **annual averages** for most parameters +- A defined number of samples may fail while still meeting compliance (concentration limits) +- Percentage reduction assessed against influent loading + +### Reporting Chain +1. WWTP operator monitors and reports to water authority +2. Water authority (Waterschap) reports to Province +3. Province reports to national government (IenW) +4. National government reports to European Commission + +## Waterschap Brabantse Delta Context + +### Service Area +- Province of Noord-Brabant (western part) +- Multiple WWTPs of varying sizes +- Mixed urban/agricultural/industrial catchment + +### Key Challenges +- Agricultural runoff contributing nutrient loading +- Seasonal variations in flow and temperature +- Emerging contaminants (pharmaceuticals, microplastics) +- Energy efficiency targets (energy-neutral WWTP goal) + +### EVOLV Relevance +- Process automation targets: optimizing energy use while maintaining effluent quality +- Real-time monitoring: continuous measurement of key parameters (NH₄, NO₃, PO₄, DO, TSS) +- Predictive control: using ASM models to anticipate process changes +- Reporting support: automated telemetry data for compliance reporting + +## Key Compliance Parameters for EVOLV + +| Parameter | Measurement Method | EVOLV Node | Typical Control Strategy | +|-----------|-------------------|------------|-------------------------| +| NH₄-N | Ion-selective electrode | measurement | Aeration control (DO setpoint cascade) | +| NO₃-N | UV absorption / ISE | measurement | Anoxic zone recirculation control | +| PO₄-P | Colorimetric / ISE | measurement | Chemical dosing or Bio-P optimization | +| DO | Amperometric / optical | measurement | Blower/diffuser control | +| TSS/MLSS | Optical (turbidity) | measurement | Sludge wasting control | +| Flow | Electromagnetic | measurement | Hydraulic load monitoring | +| Temperature | RTD/Pt100 | measurement | Process rate compensation | + +## Authoritative References + +1. Council Directive 91/271/EEC (1991). "Concerning urban waste water treatment" — Official Journal L 135/40 +2. Directive (EU) 2024/3019. "Concerning urban wastewater treatment (recast)" +3. Activiteitenbesluit milieubeheer — Dutch Activities Decree (environmental management) +4. Waterschapswet — Dutch Water Authority Act +5. STOWA (Stichting Toegepast Onderzoek Waterbeheer) — Dutch Foundation for Applied Water Research (various publications on WWTP optimization) +6. Waterschap Brabantse Delta — Regional water management plans and discharge permits