diff --git a/.agents/improvements/EXAMPLE_FLOW_TEMPLATE.md b/.agents/improvements/EXAMPLE_FLOW_TEMPLATE.md new file mode 100644 index 0000000..f83588c --- /dev/null +++ b/.agents/improvements/EXAMPLE_FLOW_TEMPLATE.md @@ -0,0 +1,123 @@ +# EVOLV Example Flow Template Standard + +## Overview + +Every EVOLV node MUST have example flows in its `examples/` directory. Node-RED automatically discovers these and shows them in **Import > Examples > EVOLV**. + +## Naming Convention + +``` +examples/ + 01 - Basic Manual Control.json # Tier 1: inject-based, zero deps + 02 - Integration with Parent Node.json # Tier 2: parent-child wiring + 03 - Dashboard Visualization.json # Tier 3: FlowFuse dashboard (optional) +``` + +The filename (minus `.json`) becomes the menu label in Node-RED. + +## Tier 1: Basic (inject-based, zero external dependencies) + +**Purpose:** Demonstrate all key functionality using only core Node-RED nodes. + +**Required elements:** +- 1x `comment` node (top-left): title + 2-3 line description of what the flow demonstrates +- 1x `comment` node (near inputs): "HOW TO USE: 1. Deploy flow. 2. Click inject nodes..." +- `inject` nodes for each control action (labeled clearly) +- The EVOLV node under test with **realistic, working configuration** +- 3x `debug` nodes: "Port 0: Process", "Port 1: InfluxDB", "Port 2: Parent" +- Optional: 1x `function` node to format output readably (keep under 20 lines) + +**Forbidden:** No dashboard nodes. No FlowFuse widgets. No HTTP nodes. No third-party nodes. + +**Config rules:** +- All required config fields filled with realistic values +- Model/curve fields set to existing models in the library +- `enableLog: true, logLevel: "info"` so users can see what happens +- Unit fields explicitly set (not empty strings) + +**Layout rules:** +- Comment nodes: top-left +- Input section: left side (x: 100-400) +- EVOLV node: center (x: 500-600) +- Debug/output: right side (x: 700-900) +- Y spacing: ~60px between nodes + +## Tier 2: Integration (parent-child relationships) + +**Purpose:** Show how nodes connect as parent-child via Port 2. + +**Required elements:** +- 1x `comment` node: what relationship is being demonstrated +- Parent node + child node(s) properly wired +- Port 2 of child → Port 0 input of parent (registration pathway) +- `inject` nodes to send control commands to parent +- `inject` nodes to send measurement/state to children +- `debug` nodes on all ports of both parent and children + +**Node-specific integration patterns:** +- `machineGroupControl` → 2x `rotatingMachine` +- `pumpingStation` → 1x `rotatingMachine` + 1x `measurement` (assetType: "flow") +- `valveGroupControl` → 2x `valve` +- `reactor` → `settler` (downstream cascade) +- `measurement` → any parent node + +## Tier 3: Dashboard Visualization (optional) + +**Purpose:** Rich interactive demo with FlowFuse dashboard. + +**Allowed additional dependencies:** FlowFuse dashboard nodes only (`@flowfuse/node-red-dashboard`). + +**Required elements:** +- 1x `comment` node: "Requires @flowfuse/node-red-dashboard" +- Auto-initialization: `inject` node with "Inject once after 1 second" for default mode/state +- Dashboard controls clearly labeled +- Charts with proper axis labels and units +- Keep parser/formatter functions under 40 lines (split if needed) +- No null message outputs (filter before sending to charts) + +## Comment Node Standard + +Every comment node must use this format: + +``` +Title: [Node Name] - [Flow Tier] +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[2-3 line description] + +Prerequisites: [list any requirements] +``` + +## ID Naming Convention + +Use predictable, readable IDs for all nodes (not random hex): + +``` +{nodeName}_{tier}_{purpose} + +Examples: +- rm_basic_tab (rotatingMachine, basic flow, tab) +- rm_basic_node (the actual rotatingMachine node) +- rm_basic_debug_port0 (debug on port 0) +- rm_basic_inject_start (inject for startup) +- rm_basic_comment_title (title comment) +``` + +## Validation Checklist + +Before committing an example flow: + +- [ ] Can be imported into clean Node-RED + EVOLV (no other packages needed for Tier 1/2) +- [ ] All nodes show correct status after deploy (no red triangles) +- [ ] Comment nodes present and descriptive +- [ ] All 3 output ports wired to something (debug at minimum) +- [ ] IDs follow naming convention (no random hex) +- [ ] Node config uses realistic values (not empty strings or defaults) +- [ ] File named per convention (01/02/03 prefix) + +## Gitea Wiki Integration + +Each node's wiki gets an "Examples" page that: +1. Lists all available example flows with descriptions +2. Links to the raw .json file in the repo +3. Describes prerequisites and step-by-step usage +4. Shows expected behavior after deploy diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..5943b6a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,77 @@ +{ + "permissions": { + "allow": [ + "Bash(node --test:*)", + "Bash(node -c:*)", + "Bash(npm:*)", + "Bash(git:*)", + "Bash(ls:*)", + "Bash(tree:*)", + "Bash(wc:*)", + "Bash(head:*)", + "Bash(tail:*)", + "Bash(sort:*)", + "Bash(find:*)", + "Bash(echo:*)", + "Bash(cat:*)", + "Bash(cut:*)", + "Bash(xargs:*)", + "WebSearch", + "WebFetch(domain:nodered.org)", + "WebFetch(domain:docs.influxdata.com)", + "WebFetch(domain:github.com)", + "WebFetch(domain:docs.anthropic.com)", + "WebFetch(domain:nodejs.org)", + "WebFetch(domain:www.npmjs.com)", + "WebFetch(domain:developer.mozilla.org)", + "WebFetch(domain:flowfuse.com)", + "WebFetch(domain:www.coolprop.org)", + "WebFetch(domain:en.wikipedia.org)", + "WebFetch(domain:www.engineeringtoolbox.com)", + "mcp__ide__getDiagnostics", + "Bash(chmod +x:*)", + "Bash(docker compose:*)", + "Bash(docker:*)", + "Bash(npm run docker:*)", + "Bash(sh:*)", + "Bash(curl:*)", + "Bash(# Check Node-RED context for the parse function to see if it received data\ndocker compose exec -T nodered sh -c 'curl -sf \"http://localhost:1880/context/node/demo_fn_ps_west_parse\" 2>/dev/null' | python3 -c \"\nimport json, sys\ntry:\n data = json.load\\(sys.stdin\\)\n print\\(json.dumps\\(data, indent=2\\)[:800]\\)\nexcept Exception as e: print\\(f'Error: {e}'\\)\n\" 2>&1)", + "Bash(# Check what the deployed flow looks like for link out type nodes\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\n# All node types and their counts\nfrom collections import Counter\ntypes = Counter\\(n.get\\('type',''\\) for n in flows if 'type' in n\\)\nfor t, c in sorted\\(types.items\\(\\)\\):\n if 'link' in t.lower\\(\\):\n print\\(f'{t}: {c}'\\)\nprint\\('---'\\)\n# Show existing link out nodes\nfor n in flows:\n if n.get\\('type'\\) == 'link out':\n print\\(f' {n[\\\\\"id\\\\\"]}: links={n.get\\(\\\\\"links\\\\\",[]\\)}'\\)\n\" 2>&1)", + "Bash(# Full count of all deployed node types\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nfrom collections import Counter\ntypes = Counter\\(n.get\\('type',''\\) for n in flows if 'type' in n\\)\nfor t, c in sorted\\(types.items\\(\\)\\):\n print\\(f'{t:30s}: {c}'\\)\nprint\\(f'Total nodes: {len\\(flows\\)}'\\)\n\" 2>&1)", + "Bash(# Check exact registered node type names\ncurl -sf http://localhost:1880/nodes 2>/dev/null | python3 -c \"\nimport json, sys\nnodes = json.load\\(sys.stdin\\)\nfor mod in nodes:\n if 'EVOLV' in json.dumps\\(mod\\) or 'evolv' in json.dumps\\(mod\\).lower\\(\\):\n if isinstance\\(mod, dict\\) and 'types' in mod:\n for t in mod['types']:\n print\\(f'Registered type: {t}'\\)\n elif isinstance\\(mod, dict\\) and 'nodes' in mod:\n for n in mod['nodes']:\n for t in n.get\\('types', []\\):\n print\\(f'Registered type: {t}'\\)\n\" 2>&1)", + "Bash(# Get node types from the /nodes endpoint properly\ndocker compose exec -T nodered sh -c 'curl -sf http://localhost:1880/nodes' | python3 -c \"\nimport json, sys\ndata = json.load\\(sys.stdin\\)\n# Find EVOLV node types\nfor module in data:\n if isinstance\\(module, dict\\):\n name = module.get\\('name', module.get\\('module', ''\\)\\)\n if 'EVOLV' in str\\(name\\).upper\\(\\) or 'evolv' in str\\(name\\).lower\\(\\):\n print\\(f'Module: {name}'\\)\n for node_set in module.get\\('nodes', []\\):\n for t in node_set.get\\('types', []\\):\n print\\(f' Type: {t}'\\)\n\" 2>&1)", + "Bash(# Get raw flow data directly from inside the container\ndocker compose exec -T nodered sh -c 'curl -sf http://localhost:1880/flows 2>/dev/null' | python3 -c \"\nimport json, sys\ndata = json.load\\(sys.stdin\\)\nprint\\(f'Total entries: {len\\(data\\)}'\\)\nprint\\(f'Type: {type\\(data\\)}'\\)\nif isinstance\\(data, list\\):\n print\\('First 3:'\\)\n for n in data[:3]:\n print\\(f' {n.get\\(\\\\\"id\\\\\",\\\\\"?\\\\\"\\)}: type={n.get\\(\\\\\"type\\\\\",\\\\\"?\\\\\"\\)}'\\)\n # Count\n from collections import Counter\n types = Counter\\(n.get\\('type',''\\) for n in data\\)\n for t, c in sorted\\(types.items\\(\\)\\):\n print\\(f' {t}: {c}'\\)\nelif isinstance\\(data, dict\\):\n print\\(f'Keys: {list\\(data.keys\\(\\)\\)}'\\)\n if 'flows' in data:\n flows = data['flows']\n print\\(f'Flows count: {len\\(flows\\)}'\\)\n from collections import Counter\n types = Counter\\(n.get\\('type',''\\) for n in flows\\)\n for t, c in sorted\\(types.items\\(\\)\\):\n print\\(f' {t}: {c}'\\)\n\" 2>&1)", + "Bash(# Check individual tab flows\ndocker compose exec -T nodered sh -c 'curl -sf http://localhost:1880/flow/demo_tab_wwtp' | python3 -c \"\nimport json, sys\ndata = json.load\\(sys.stdin\\)\nif isinstance\\(data, dict\\):\n print\\(f'Tab: {data.get\\(\\\\\"label\\\\\",\\\\\"?\\\\\"\\)}'\\)\n nodes = data.get\\('nodes', []\\)\n print\\(f'Nodes: {len\\(nodes\\)}'\\)\n from collections import Counter\n types = Counter\\(n.get\\('type',''\\) for n in nodes\\)\n for t, c in sorted\\(types.items\\(\\)\\):\n print\\(f' {t}: {c}'\\)\nelse:\n print\\(data\\)\n\" 2>&1)", + "Bash(sleep 5:*)", + "Bash(sleep 15:*)", + "Bash(# Get all dashboard UIDs and update the bucket variable from lvl2 to telemetry\ncurl -sf -H \"Authorization: Bearer glsa_4tbdInvrkQ6c7J6N3InjSsH8de83vZ66_9db7efa3\" \\\\\n \"http://localhost:3000/api/search?type=dash-db\" | python3 -c \"\nimport json, sys\ndashboards = json.load\\(sys.stdin\\)\nfor d in dashboards:\n print\\(d['uid']\\)\n\" 2>&1)", + "Bash(sleep 20:*)", + "Bash(# Check reactor parse function context\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\n# Find parse functions by name\nfor n in flows:\n if n.get\\('type'\\) == 'function' and 'reactor' in n.get\\('name',''\\).lower\\(\\):\n print\\(f\\\\\"Reactor parse: id={n['id']}, name={n.get\\('name'\\)}\\\\\"\\)\" 2>&1)", + "Bash(# Check if reactor node is sending output — look at debug info\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\n# Find the reactor node and its wires\nfor n in flows:\n if n.get\\('type'\\) == 'reactor':\n print\\(f\\\\\"Reactor: id={n['id']}, name={n.get\\('name',''\\)}\\\\\"\\)\n wires = n.get\\('wires', []\\)\n for i, port in enumerate\\(wires\\):\n print\\(f' Port {i}: {port}'\\)\n if n.get\\('type'\\) == 'link out' and 'reactor' in n.get\\('name',''\\).lower\\(\\):\n print\\(f\\\\\"Link-out reactor: id={n['id']}, name={n.get\\('name',''\\)}, links={n.get\\('links',[]\\)}\\\\\"\\)\" 2>&1)", + "Bash(# Check measurement node wiring and output\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nfor n in flows:\n if n.get\\('type'\\) == 'measurement':\n print\\(f\\\\\"Measurement: id={n['id']}, name={n.get\\('name',''\\)}\\\\\"\\)\n wires = n.get\\('wires', []\\)\n for i, port in enumerate\\(wires\\):\n print\\(f' Port {i}: {port}'\\)\n if n.get\\('type'\\) == 'link out' and 'meas' in n.get\\('name',''\\).lower\\(\\):\n print\\(f\\\\\"Link-out meas: id={n['id']}, name={n.get\\('name',''\\)}, links={n.get\\('links',[]\\)}\\\\\"\\)\" 2>&1)", + "Bash(# Check reactor node config and measurement configs\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nfor n in flows:\n if n.get\\('type'\\) == 'reactor':\n print\\('=== REACTOR CONFIG ==='\\)\n for k,v in sorted\\(n.items\\(\\)\\):\n if k not in \\('wires','x','y','z'\\):\n print\\(f' {k}: {v}'\\)\n if n.get\\('type'\\) == 'measurement' and n.get\\('id'\\) == 'demo_meas_flow':\n print\\('=== MEASUREMENT FT-001 CONFIG ==='\\)\n for k,v in sorted\\(n.items\\(\\)\\):\n if k not in \\('wires','x','y','z'\\):\n print\\(f' {k}: {v}'\\)\" 2>&1)", + "Bash(# Check what inject/input nodes target the measurement nodes\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\n\n# Find all nodes that wire INTO the measurement nodes\nmeas_ids = {'demo_meas_flow', 'demo_meas_do', 'demo_meas_nh4'}\nfor n in flows:\n wires = n.get\\('wires', []\\)\n for port_idx, port_wires in enumerate\\(wires\\):\n for target in port_wires:\n if target in meas_ids:\n print\\(f'{n.get\\(\\\\\"type\\\\\"\\)}:{n.get\\(\\\\\"name\\\\\",\\\\\"\\\\\"\\)} \\(id={n.get\\(\\\\\"id\\\\\"\\)}\\) port {port_idx} → {target}'\\)\n\n# Check inject nodes that send to measurements \nprint\\(\\)\nprint\\('=== Inject nodes ==='\\)\nfor n in flows:\n if n.get\\('type'\\) == 'inject':\n wires = n.get\\('wires', []\\)\n all_targets = [t for port in wires for t in port]\n print\\(f'inject: {n.get\\(\\\\\"name\\\\\",\\\\\"\\\\\"\\)} id={n.get\\(\\\\\"id\\\\\"\\)} → targets={all_targets} repeat={n.get\\(\\\\\"repeat\\\\\",\\\\\"\\\\\"\\)} topic={n.get\\(\\\\\"topic\\\\\",\\\\\"\\\\\"\\)}'\\)\" 2>&1)", + "Bash(# Check the simulator function code for measurements\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nfor n in flows:\n if n.get\\('id'\\) in \\('demo_fn_sim_flow', 'demo_fn_sim_do', 'demo_fn_sim_nh4'\\):\n print\\(f'=== {n.get\\(\\\\\"name\\\\\"\\)} ==='\\)\n print\\(n.get\\('func',''\\)\\)\n print\\(\\)\" 2>&1)", + "Bash(# Check what the reactor tick inject sends\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nfor n in flows:\n if n.get\\('id'\\) == 'demo_inj_reactor_tick':\n print\\('=== Reactor tick inject ==='\\)\n for k,v in sorted\\(n.items\\(\\)\\):\n if k not in \\('x','y','z','wires'\\):\n print\\(f' {k}: {v}'\\)\n if n.get\\('id'\\) == 'demo_inj_meas_flow':\n print\\('=== Flow sensor inject ==='\\)\n for k,v in sorted\\(n.items\\(\\)\\):\n if k not in \\('x','y','z','wires'\\):\n print\\(f' {k}: {v}'\\)\" 2>&1)", + "Bash(# Check measurement parse function code\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nfor n in flows:\n if n.get\\('id'\\) == 'demo_fn_reactor_parse':\n print\\('=== Parse Reactor ==='\\)\n print\\(n.get\\('func',''\\)\\)\n print\\(\\)\n if n.get\\('id'\\) == 'demo_fn_meas_parse':\n print\\('=== Parse Measurements ==='\\)\n print\\(n.get\\('func',''\\)\\)\n print\\(\\)\n if n.get\\('type'\\) == 'function' and 'meas' in n.get\\('name',''\\).lower\\(\\) and 'parse' in n.get\\('name',''\\).lower\\(\\):\n print\\(f'=== {n.get\\(\\\\\"name\\\\\"\\)} \\(id={n.get\\(\\\\\"id\\\\\"\\)}\\) ==='\\)\n print\\(n.get\\('func',''\\)\\)\n print\\(\\)\" 2>&1)", + "Bash(# Check the link node pairs are properly paired\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nnodes = {n['id']: n for n in flows if 'id' in n}\n\nlink_outs = [n for n in flows if n.get\\('type'\\) == 'link out']\nlink_ins = [n for n in flows if n.get\\('type'\\) == 'link in']\n\nprint\\('=== Link-out nodes ==='\\)\nfor lo in link_outs:\n links = lo.get\\('links', []\\)\n targets = [nodes.get\\(l, {}\\).get\\('name', f'MISSING:{l}'\\) for l in links]\n tab = nodes.get\\(lo.get\\('z',''\\), {}\\).get\\('label', '?'\\)\n print\\(f' [{tab}] {lo.get\\(\\\\\"name\\\\\",\\\\\"\\\\\"\\)} \\(id={lo[\\\\\"id\\\\\"]}\\) → {targets}'\\)\n\nprint\\(\\)\nprint\\('=== Link-in nodes ==='\\) \nfor li in link_ins:\n links = li.get\\('links', []\\)\n tab = nodes.get\\(li.get\\('z',''\\), {}\\).get\\('label', '?'\\)\n print\\(f' [{tab}] {li.get\\(\\\\\"name\\\\\",\\\\\"\\\\\"\\)} \\(id={li[\\\\\"id\\\\\"]}\\) links={links}'\\)\" 2>&1)", + "Bash(sleep 8:*)", + "Bash(# Check the InfluxDB convert function and HTTP request config\ncurl -sf http://localhost:1880/flows 2>/dev/null | python3 -c \"\nimport json, sys\nflows = json.load\\(sys.stdin\\)\nfor n in flows:\n if n.get\\('id'\\) == 'demo_fn_influx_convert':\n print\\('=== InfluxDB Convert Function ==='\\)\n print\\(f'func: {n.get\\(\\\\\"func\\\\\",\\\\\"\\\\\"\\)}'\\)\n print\\(f'wires: {n.get\\(\\\\\"wires\\\\\",[]\\)}'\\)\n print\\(\\)\n if n.get\\('id'\\) == 'demo_http_influx':\n print\\('=== Write InfluxDB HTTP ==='\\)\n for k,v in sorted\\(n.items\\(\\)\\):\n if k not in \\('x','y','z','wires'\\):\n print\\(f' {k}: {v}'\\)\n print\\(f' wires: {n.get\\(\\\\\"wires\\\\\",[]\\)}'\\)\n\" 2>&1)", + "Bash(echo Grafana API not accessible:*)", + "Bash(python3 -c \":*)", + "Bash(__NEW_LINE_6565c53f4a65adcb__ echo \"\")", + "Bash(__NEW_LINE_43bd4a070667d63e__ echo \"\")", + "Bash(node:*)", + "Bash(python3:*)", + "WebFetch(domain:dashboard.flowfuse.com)", + "Bash(do echo:*)", + "Bash(__NEW_LINE_5a355214e3d8caae__ git:*)", + "Bash(git add:*)", + "Bash(__NEW_LINE_4762b8ca1fb65139__ for:*)", + "Bash(docker.exe ps:*)", + "Bash(docker.exe logs:*)", + "Bash(docker.exe compose:*)", + "Bash(docker.exe exec:*)" + ] + } +} diff --git a/nodes/dashboardAPI b/nodes/dashboardAPI index 547333b..89d2260 160000 --- a/nodes/dashboardAPI +++ b/nodes/dashboardAPI @@ -1 +1 @@ -Subproject commit 547333be7d6a442c24d0066bd8417bd151bb2dd2 +Subproject commit 89d2260351dc0b81909f432958ce5d1b8169dfdf diff --git a/nodes/generalFunctions b/nodes/generalFunctions index c60aa40..27a6d3c 160000 --- a/nodes/generalFunctions +++ b/nodes/generalFunctions @@ -1 +1 @@ -Subproject commit c60aa40666076ab003f4d837736a267c9a06b125 +Subproject commit 27a6d3c7098ccc80e4753c8d8c31e69f65c11461 diff --git a/nodes/machineGroupControl b/nodes/machineGroupControl index f8012c8..b337bf9 160000 --- a/nodes/machineGroupControl +++ b/nodes/machineGroupControl @@ -1 +1 @@ -Subproject commit f8012c8bad03bc64914e259333e4a0a3c16f6382 +Subproject commit b337bf9eb71a65ac735b5cabbbf07295a7aa0883 diff --git a/nodes/measurement b/nodes/measurement index c587ed9..43b5269 160000 --- a/nodes/measurement +++ b/nodes/measurement @@ -1 +1 @@ -Subproject commit c587ed9c7b674d9c304e20e53eebcdc06dad1ac1 +Subproject commit 43b5269f0b5996023ee16a7c726160f9aac22696 diff --git a/nodes/monster b/nodes/monster index 38013a8..32ebfd7 160000 --- a/nodes/monster +++ b/nodes/monster @@ -1 +1 @@ -Subproject commit 38013a86db637615ec638b8927c5e70d2ff748dd +Subproject commit 32ebfd715449d3efc0c98a2d60b16e01a47e270e diff --git a/nodes/reactor b/nodes/reactor index 460b872..2c69a5a 160000 --- a/nodes/reactor +++ b/nodes/reactor @@ -1 +1 @@ -Subproject commit 460b872053ba07f48e6daea29eaa5c920cdab0e1 +Subproject commit 2c69a5a0c1a615fc301d10adc56267c8d2992e36 diff --git a/nodes/rotatingMachine b/nodes/rotatingMachine index 33f3c2e..6b2a823 160000 --- a/nodes/rotatingMachine +++ b/nodes/rotatingMachine @@ -1 +1 @@ -Subproject commit 33f3c2ef618cf0f10a7af67521fd8fe2df76065c +Subproject commit 6b2a8239f2fae1a36608f3df8d8335f819ca55c5 diff --git a/nodes/valve b/nodes/valve index d56f8a3..6287708 160000 --- a/nodes/valve +++ b/nodes/valve @@ -1 +1 @@ -Subproject commit d56f8a382cd1331fddbaa21a4fdea19f013b37c5 +Subproject commit 6287708c1e4753c160d36a652701f72586281bd3 diff --git a/nodes/valveGroupControl b/nodes/valveGroupControl index cbe868a..5e1f394 160000 --- a/nodes/valveGroupControl +++ b/nodes/valveGroupControl @@ -1 +1 @@ -Subproject commit cbe868a148cff63dd28b31dc8ddde596611f943c +Subproject commit 5e1f3946bf8119d02293cd4e1b1ca3e7de8359ae