From 0cbd6a40778abda7b9379e70177750555d84ad45 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Tue, 14 Apr 2026 08:42:22 +0200 Subject: [PATCH] wip: sinus-driven pumping station demo + PS levelbased control to MGC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture change: demo is now driven by a sinusoidal inflow into the pumping station basin, rather than a random demand generator. The basin fills from the sinus, and PS's levelbased control should start/stop pumps via MGC when level crosses start/stop thresholds. Changes: - Demo Drivers tab: sinus generator (period 120s, base 0.005 + amp 0.03 m³/s) replaces the random demand. Sends q_in to PS via link channel. - PS config: levelbased mode, 10 m³ basin, startLevel 1.2 m / stopLevel 0.6 m. Volume-based safeties on, time-based off. - MGC scaling = normalized (was absolute) so PS's percent-based level control maps correctly. - Dashboard mode toggle now drives PS mode (levelbased ↔ manual) instead of per-pump setMode. Slider sends Qd to PS (only effective in manual). - PS code (committed separately): _controlLevelBased now calls _applyMachineGroupLevelControl + new Qd topic + forwardDemandToChildren. KNOWN ISSUE: Basin fills correctly (visible on dashboard), but pumps don't start when level exceeds startLevel. Likely cause: _pickVariant for 'level' in _controlLevelBased may not be resolving the predicted level correctly, or the safetyController is interfering despite time-threshold being 0. Needs source-level tracing of the PS tick → _safetyController → _controlLogic → _controlLevelBased path with logging enabled. To be debugged in the next session. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../build_flow.py | 205 +++++----- .../pumpingstation-3pumps-dashboard/flow.json | 364 ++++++------------ nodes/pumpingStation | 2 +- 3 files changed, 224 insertions(+), 347 deletions(-) diff --git a/examples/pumpingstation-3pumps-dashboard/build_flow.py b/examples/pumpingstation-3pumps-dashboard/build_flow.py index bd2bce8..1c510dc 100644 --- a/examples/pumpingstation-3pumps-dashboard/build_flow.py +++ b/examples/pumpingstation-3pumps-dashboard/build_flow.py @@ -481,26 +481,10 @@ def build_process_tab(): "── MGC ── (orchestrates the 3 pumps via optimalcontrol)", "Receives Qd from cmd:demand link-in. Distributes flow across pumps." )) - nodes.append(link_in( - "lin_demand_to_mgc", TAB_PROCESS, LANE_X[0], y_mgc + 60, - CH_DEMAND, - source_out_ids=[f"lout_demand_drivers", f"lout_demand_dash"], - downstream=["demand_fanout_mgc_ps"], - )) - # Single fanout: one demand value → MGC (Qd) + PS (q_in). - # Skips when v <= 0 to avoid auto-shutdown. - nodes.append(function_node( - "demand_fanout_mgc_ps", TAB_PROCESS, LANE_X[1] + 220, y_mgc + 60, - "demand → MGC + PS", - "const v = Number(msg.payload);\n" - "if (!Number.isFinite(v) || v <= 0) return null;\n" - "// MGC accepts Qd in m3/h directly when scaling=absolute.\n" - "const qd = { topic: 'Qd', payload: v };\n" - "// PS accepts q_in in m3/s (canonical) via the 'unit' field.\n" - "const qin = { topic: 'q_in', payload: v / 3600, unit: 'm3/s' };\n" - "return [qd, qin];", - outputs=2, wires=[[MGC_ID], [PS_ID]], - )) + # MGC no longer receives direct Qd from the dashboard — PS drives it + # via level-based control or manual Qd forwarding. The demand_fanout + # has been replaced by: sinus → q_in → PS (levelbased), and + # slider → Qd → PS (manual mode only). nodes.append({ "id": MGC_ID, "type": "machineGroupControl", "z": TAB_PROCESS, "name": "MGC — Pump Group", @@ -552,8 +536,34 @@ def build_process_tab(): # ---------------- PS ---------------- y_ps = 100 + 4 * SECTION_GAP nodes.append(comment("c_ps", TAB_PROCESS, LANE_X[2], y_ps, - "── Pumping Station ── (basin model, manual control mode)", - "Receives q_in from demand fanout. Emits formatted basin state." + "── Pumping Station ── (basin model, levelbased control)", + "Receives q_in (simulated inflow) from Demo Drivers tab.\n" + "Level-based control starts/stops pumps via MGC when level crosses start/stop thresholds." + )) + # link-in for simulated inflow from Drivers tab + nodes.append(link_in( + "lin_qin_at_ps", TAB_PROCESS, LANE_X[0], y_ps + 40, + "cmd:q_in", source_out_ids=["lout_qin_drivers"], + downstream=[PS_ID], + )) + # link-in for manual Qd demand from Dashboard slider (only effective in manual mode) + nodes.append(link_in( + "lin_qd_at_ps", TAB_PROCESS, LANE_X[0], y_ps + 80, + "cmd:Qd", source_out_ids=["lout_demand_dash"], + downstream=["qd_to_ps_wrap"], + )) + nodes.append(function_node( + "qd_to_ps_wrap", TAB_PROCESS, LANE_X[1], y_ps + 80, + "wrap slider → PS Qd", + "msg.topic = 'Qd';\n" + "return msg;", + outputs=1, wires=[[PS_ID]], + )) + # link-in for PS mode toggle from Dashboard + nodes.append(link_in( + "lin_ps_mode_at_ps", TAB_PROCESS, LANE_X[0], y_ps + 120, + "cmd:ps-mode", source_out_ids=["lout_ps_mode_dash"], + downstream=[PS_ID], )) nodes.append({ "id": PS_ID, "type": "pumpingStation", "z": TAB_PROCESS, @@ -566,13 +576,19 @@ def build_process_tab(): "hasDistance": False, "distance": 0, "distanceUnit": "m", "distanceDescription": "", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", - # PS in manual mode + safeties off — see top-level README for why. - "controlMode": "manual", - "basinVolume": 50, "basinHeight": 4, - "enableDryRunProtection": False, - "enableOverfillProtection": False, - "dryRunThresholdPercent": 0, - "overfillThresholdPercent": 100, + # PS in levelbased mode — sinus inflow fills the basin, pumps start + # when level > startLevel, stop when level < stopLevel. + "controlMode": "levelbased", + "basinVolume": 10, "basinHeight": 3, + "startLevel": 1.2, "stopLevel": 0.6, + "minFlowLevel": 0.6, "maxFlowLevel": 2.8, + "heightInlet": 2.5, "heightOutlet": 0.2, "heightOverflow": 2.8, + # Volume-based safeties ON, time-based OFF (time guard fires too + # aggressively with the sinus demo's small basin + high peak inflow). + "enableDryRunProtection": True, + "enableOverfillProtection": True, + "dryRunThresholdPercent": 5, + "overfillThresholdPercent": 95, "timeleftToFullOrEmptyThresholdSeconds": 0, "x": LANE_X[3], "y": y_ps + 80, "wires": [ @@ -721,49 +737,39 @@ def build_ui_tab(): "── Process Demand ──", "")) nodes.append(ui_slider( "ui_demand_slider", TAB_UI, LANE_X[0], y + 40, g_demand, - "Process demand slider", "Process Demand (m³/h)", - 0, 300, 5.0, "manualDemand", + "Manual demand (manual mode only)", "Manual demand (m³/h) — active in manual mode only", + 0, 100, 5.0, "manualDemand", wires=["lout_demand_dash"] )) nodes.append(link_out( "lout_demand_dash", TAB_UI, LANE_X[1], y + 40, - CH_DEMAND, target_in_ids=["lin_demand_to_mgc", "lin_demand_to_text"] - )) - nodes.append(ui_switch( - "ui_random_toggle", TAB_UI, LANE_X[0], y + 100, g_demand, - "Random demand", "Random demand generator (auto)", - on_value="on", off_value="off", topic="randomToggle", - wires=["lout_random_dash"] - )) - nodes.append(link_out( - "lout_random_dash", TAB_UI, LANE_X[1], y + 100, - CH_RANDOM_TOGGLE, target_in_ids=["lin_random_to_drivers"] + "cmd:Qd", target_in_ids=["lin_qd_at_ps"] )) nodes.append(ui_text( "ui_demand_text", TAB_UI, LANE_X[3], y + 40, g_demand, - "Current demand", "Current demand", "{{msg.payload}} m³/h" - )) - # Echo the demand back for the text widget - nodes.append(link_in( - "lin_demand_to_text", TAB_UI, LANE_X[2], y + 40, - CH_DEMAND, source_out_ids=["lout_demand_dash", "lout_demand_drivers"], - downstream=["ui_demand_text"] + "Manual demand (active in manual mode)", "Manual demand", + "{{msg.payload}} m³/h" )) + # The slider value routes to PS as Qd — only effective in manual mode. + # Route is: slider → link-out cmd:Qd → process tab link-in → PS. # ===== SECTION: Mode + Station Buttons ===== y = 320 nodes.append(comment("c_ui_station", TAB_UI, LANE_X[2], y, "── Mode + Station-wide buttons ──", "")) + # Mode toggle now drives the PUMPING STATION mode (levelbased ↔ manual) + # instead of per-pump setMode. In levelbased mode, PS drives pumps + # automatically. In manual mode, the demand slider takes over. nodes.append(ui_switch( "ui_mode_toggle", TAB_UI, LANE_X[0], y + 40, g_station, - "Auto/Manual mode", - "Mode (Auto = MGC orchestrates · Manual = dashboard per-pump)", - on_value="auto", off_value="virtualControl", topic="setMode", - wires=["lout_mode_dash"] + "Station mode", + "Station mode (Auto = level-based control · Manual = slider demand)", + on_value="levelbased", off_value="manual", topic="changemode", + wires=["lout_ps_mode_dash"] )) nodes.append(link_out( - "lout_mode_dash", TAB_UI, LANE_X[1], y + 40, - CH_MODE, target_in_ids=["lin_mode"] + "lout_ps_mode_dash", TAB_UI, LANE_X[1], y + 40, + "cmd:ps-mode", target_in_ids=["lin_ps_mode_at_ps"] )) for k, (text, payload, color, icon, lout_id, channel) in enumerate([ @@ -1052,53 +1058,52 @@ def build_drivers_tab(): "id": TAB_DRIVERS, "type": "tab", "label": "🎛️ Demo Drivers", "disabled": False, - "info": "Auto stimulus for the demo. Random demand generator + state holder " - "for the dashboard's randomToggle switch. In production, delete this " - "tab and feed cmd:demand from your real demand source.", + "info": "Simulated inflow for the demo. A slow sinusoid generates " + "inflow into the pumping station basin, which then drives " + "the level-based pump control automatically.\n\n" + "In production, delete this tab — real inflow comes from " + "upstream measurement sensors.", }) nodes.append(comment("c_drv_title", TAB_DRIVERS, LANE_X[2], 20, - "🎛️ DEMO DRIVERS — auto stimulus only", - "Removable: in production, replace this tab with the real demand source." + "🎛️ DEMO DRIVERS — simulated basin inflow", + "Sinus generator → q_in to pumpingStation. Basin fills → level-based\n" + "control starts pumps → basin drains → pumps stop → cycle repeats." )) - # Random toggle state holder (set by the dashboard switch) + # Sinus inflow generator: produces a flow value (m³/s) that + # simulates incoming wastewater. Period ~120s so the fill/drain + # cycle is visible on the dashboard. Amplitude scaled so 3 pumps + # can handle the peak. + # Q_in = base + amplitude * (1 + sin(2π t / period)) / 2 + # base = 0.005 m³/s (~18 m³/h) — always some inflow + # amplitude = 0.03 m³/s (~108 m³/h peak) + # period = 120 s y = 100 - nodes.append(comment("c_drv_state", TAB_DRIVERS, LANE_X[2], y, - "── Random toggle state ──", "")) - nodes.append(link_in( - "lin_random_to_drivers", TAB_DRIVERS, LANE_X[0], y + 40, - CH_RANDOM_TOGGLE, source_out_ids=["lout_random_dash"], - downstream=["random_state"] + nodes.append(comment("c_drv_sinus", TAB_DRIVERS, LANE_X[2], y, + "── Sinusoidal inflow generator ──", + "Produces a smooth inflow curve (m³/s) and sends to pumpingStation\n" + "via the cmd:q_in link channel. Period = 120s." )) - nodes.append(function_node( - "random_state", TAB_DRIVERS, LANE_X[1], y + 40, - "store random on/off", - "flow.set('randomOn', msg.payload === 'on');\n" - "return null;", - outputs=1, wires=[[]] - )) - - # Random demand generator: every 3s pick a value in [40, 240] m³/h - y = 250 - nodes.append(comment("c_drv_random", TAB_DRIVERS, LANE_X[2], y, - "── Random demand generator ── (every 3 s)", "")) nodes.append(inject( - "rand_tick", TAB_DRIVERS, LANE_X[0], y + 40, - "tick (random demand)", - topic="randomTick", payload="", payload_type="date", - repeat="3", wires=["random_demand_fn"] + "sinus_tick", TAB_DRIVERS, LANE_X[0], y + 40, + "tick (1s inflow)", + topic="sinusTick", payload="", payload_type="date", + repeat="1", wires=["sinus_fn"] )) nodes.append(function_node( - "random_demand_fn", TAB_DRIVERS, LANE_X[1] + 220, y + 40, - "random demand", - "if (!flow.get('randomOn')) return null;\n" - "const v = Math.round(40 + Math.random() * 200);\n" - "return { topic: 'manualDemand', payload: v };", - outputs=1, wires=[["lout_demand_drivers"]] + "sinus_fn", TAB_DRIVERS, LANE_X[1] + 220, y + 40, + "sinus inflow (m³/s)", + "const base = 0.005; // m³/s (~18 m³/h always)\n" + "const amplitude = 0.03; // m³/s (~108 m³/h peak)\n" + "const period = 120; // seconds per full cycle\n" + "const t = Date.now() / 1000; // seconds since epoch\n" + "const q = base + amplitude * (1 + Math.sin(2 * Math.PI * t / period)) / 2;\n" + "return { topic: 'q_in', payload: q, unit: 'm3/s', timestamp: Date.now() };", + outputs=1, wires=[["lout_qin_drivers"]] )) nodes.append(link_out( - "lout_demand_drivers", TAB_DRIVERS, LANE_X[3], y + 40, - CH_DEMAND, target_in_ids=["lin_demand_to_mgc", "lin_demand_to_text"] + "lout_qin_drivers", TAB_DRIVERS, LANE_X[3], y + 40, + "cmd:q_in", target_in_ids=["lin_qin_at_ps"] )) return nodes @@ -1126,8 +1131,8 @@ def build_setup_tab(): y = 100 nodes.append(inject( "setup_mgc_scaling", TAB_SETUP, LANE_X[0], y, - "MGC scaling = absolute", - topic="setScaling", payload="absolute", payload_type="str", + "MGC scaling = normalized", + topic="setScaling", payload="normalized", payload_type="str", once=True, once_delay="1.5", wires=["lout_setup_to_mgc"] )) @@ -1170,18 +1175,8 @@ def build_setup_tab(): CH_STATION_START, target_in_ids=["lin_station_start"] )) - y = 450 - nodes.append(inject( - "setup_random_on", TAB_SETUP, LANE_X[0], y, - "auto-enable random demand", - topic="randomToggle", payload="on", payload_type="str", - once=True, once_delay="5", - wires=["lout_setup_random"] - )) - nodes.append(link_out( - "lout_setup_random", TAB_SETUP, LANE_X[1], y, - CH_RANDOM_TOGGLE, target_in_ids=["lin_random_to_drivers"] - )) + # (Random demand removed — sinus inflow drives the demo automatically. + # No explicit "random on" inject needed.) return nodes diff --git a/examples/pumpingstation-3pumps-dashboard/flow.json b/examples/pumpingstation-3pumps-dashboard/flow.json index 590ab77..7ba6064 100644 --- a/examples/pumpingstation-3pumps-dashboard/flow.json +++ b/examples/pumpingstation-3pumps-dashboard/flow.json @@ -689,45 +689,6 @@ "y": 700, "wires": [] }, - { - "id": "lin_demand_to_mgc", - "type": "link in", - "z": "tab_process", - "name": "cmd:demand", - "links": [ - "lout_demand_drivers", - "lout_demand_dash" - ], - "x": 120, - "y": 760, - "wires": [ - [ - "demand_fanout_mgc_ps" - ] - ] - }, - { - "id": "demand_fanout_mgc_ps", - "type": "function", - "z": "tab_process", - "name": "demand \u2192 MGC + PS", - "func": "const v = Number(msg.payload);\nif (!Number.isFinite(v) || v <= 0) return null;\n// MGC accepts Qd in m3/h directly when scaling=absolute.\nconst qd = { topic: 'Qd', payload: v };\n// PS accepts q_in in m3/s (canonical) via the 'unit' field.\nconst qin = { topic: 'q_in', payload: v / 3600, unit: 'm3/s' };\nreturn [qd, qin];", - "outputs": 2, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 600, - "y": 760, - "wires": [ - [ - "mgc_pumps" - ], - [ - "ps_basin" - ] - ] - }, { "id": "mgc_pumps", "type": "machineGroupControl", @@ -797,12 +758,79 @@ "id": "c_ps", "type": "comment", "z": "tab_process", - "name": "\u2500\u2500 Pumping Station \u2500\u2500 (basin model, manual control mode)", - "info": "Receives q_in from demand fanout. Emits formatted basin state.", + "name": "\u2500\u2500 Pumping Station \u2500\u2500 (basin model, levelbased control)", + "info": "Receives q_in (simulated inflow) from Demo Drivers tab.\nLevel-based control starts/stops pumps via MGC when level crosses start/stop thresholds.", "x": 640, "y": 900, "wires": [] }, + { + "id": "lin_qin_at_ps", + "type": "link in", + "z": "tab_process", + "name": "cmd:q_in", + "links": [ + "lout_qin_drivers" + ], + "x": 120, + "y": 940, + "wires": [ + [ + "ps_basin" + ] + ] + }, + { + "id": "lin_qd_at_ps", + "type": "link in", + "z": "tab_process", + "name": "cmd:Qd", + "links": [ + "lout_demand_dash" + ], + "x": 120, + "y": 980, + "wires": [ + [ + "qd_to_ps_wrap" + ] + ] + }, + { + "id": "qd_to_ps_wrap", + "type": "function", + "z": "tab_process", + "name": "wrap slider \u2192 PS Qd", + "func": "msg.topic = 'Qd';\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 380, + "y": 980, + "wires": [ + [ + "ps_basin" + ] + ] + }, + { + "id": "lin_ps_mode_at_ps", + "type": "link in", + "z": "tab_process", + "name": "cmd:ps-mode", + "links": [ + "lout_ps_mode_dash" + ], + "x": 120, + "y": 1020, + "wires": [ + [ + "ps_basin" + ] + ] + }, { "id": "ps_basin", "type": "pumpingStation", @@ -824,13 +852,20 @@ "distanceDescription": "", "processOutputFormat": "process", "dbaseOutputFormat": "influxdb", - "controlMode": "manual", - "basinVolume": 50, - "basinHeight": 4, - "enableDryRunProtection": false, - "enableOverfillProtection": false, - "dryRunThresholdPercent": 0, - "overfillThresholdPercent": 100, + "controlMode": "levelbased", + "basinVolume": 10, + "basinHeight": 3, + "startLevel": 1.2, + "stopLevel": 0.6, + "minFlowLevel": 0.6, + "maxFlowLevel": 2.8, + "heightInlet": 2.5, + "heightOutlet": 0.2, + "heightOverflow": 2.8, + "enableDryRunProtection": true, + "enableOverfillProtection": true, + "dryRunThresholdPercent": 5, + "overfillThresholdPercent": 95, "timeleftToFullOrEmptyThresholdSeconds": 0, "x": 900, "y": 980, @@ -1361,8 +1396,8 @@ "type": "ui-slider", "z": "tab_ui", "group": "ui_grp_demand", - "name": "Process demand slider", - "label": "Process Demand (m\u00b3/h)", + "name": "Manual demand (manual mode only)", + "label": "Manual demand (m\u00b3/h) \u2014 active in manual mode only", "tooltip": "", "order": 1, "width": "0", @@ -1372,7 +1407,7 @@ "topic": "manualDemand", "topicType": "str", "min": "0", - "max": "300", + "max": "100", "step": "5.0", "showLabel": true, "showValue": true, @@ -1393,63 +1428,15 @@ "id": "lout_demand_dash", "type": "link out", "z": "tab_ui", - "name": "cmd:demand", + "name": "cmd:Qd", "mode": "link", "links": [ - "lin_demand_to_mgc", - "lin_demand_to_text" + "lin_qd_at_ps" ], "x": 380, "y": 140, "wires": [] }, - { - "id": "ui_random_toggle", - "type": "ui-switch", - "z": "tab_ui", - "group": "ui_grp_demand", - "name": "Random demand", - "label": "Random demand generator (auto)", - "tooltip": "", - "order": 1, - "width": "0", - "height": "0", - "passthru": true, - "decouple": "false", - "topic": "randomToggle", - "topicType": "str", - "style": "", - "className": "", - "evaluate": "true", - "onvalue": "on", - "onvalueType": "str", - "onicon": "auto_mode", - "oncolor": "#0f52a5", - "offvalue": "off", - "offvalueType": "str", - "officon": "back_hand", - "offcolor": "#888888", - "x": 120, - "y": 200, - "wires": [ - [ - "lout_random_dash" - ] - ] - }, - { - "id": "lout_random_dash", - "type": "link out", - "z": "tab_ui", - "name": "cmd:randomToggle", - "mode": "link", - "links": [ - "lin_random_to_drivers" - ], - "x": 380, - "y": 200, - "wires": [] - }, { "id": "ui_demand_text", "type": "ui-text", @@ -1458,8 +1445,8 @@ "order": 1, "width": "0", "height": "0", - "name": "Current demand", - "label": "Current demand", + "name": "Manual demand (active in manual mode)", + "label": "Manual demand", "format": "{{msg.payload}} m\u00b3/h", "layout": "row-left", "style": false, @@ -1470,23 +1457,6 @@ "y": 140, "wires": [] }, - { - "id": "lin_demand_to_text", - "type": "link in", - "z": "tab_ui", - "name": "cmd:demand", - "links": [ - "lout_demand_dash", - "lout_demand_drivers" - ], - "x": 640, - "y": 140, - "wires": [ - [ - "ui_demand_text" - ] - ] - }, { "id": "c_ui_station", "type": "comment", @@ -1502,24 +1472,24 @@ "type": "ui-switch", "z": "tab_ui", "group": "ui_grp_station", - "name": "Auto/Manual mode", - "label": "Mode (Auto = MGC orchestrates \u00b7 Manual = dashboard per-pump)", + "name": "Station mode", + "label": "Station mode (Auto = level-based control \u00b7 Manual = slider demand)", "tooltip": "", "order": 1, "width": "0", "height": "0", "passthru": true, "decouple": "false", - "topic": "setMode", + "topic": "changemode", "topicType": "str", "style": "", "className": "", "evaluate": "true", - "onvalue": "auto", + "onvalue": "levelbased", "onvalueType": "str", "onicon": "auto_mode", "oncolor": "#0f52a5", - "offvalue": "virtualControl", + "offvalue": "manual", "offvalueType": "str", "officon": "back_hand", "offcolor": "#888888", @@ -1527,18 +1497,18 @@ "y": 360, "wires": [ [ - "lout_mode_dash" + "lout_ps_mode_dash" ] ] }, { - "id": "lout_mode_dash", + "id": "lout_ps_mode_dash", "type": "link out", "z": "tab_ui", - "name": "cmd:mode", + "name": "cmd:ps-mode", "mode": "link", "links": [ - "lin_mode" + "lin_ps_mode_at_ps" ], "x": 380, "y": 360, @@ -3346,76 +3316,33 @@ "type": "tab", "label": "\ud83c\udf9b\ufe0f Demo Drivers", "disabled": false, - "info": "Auto stimulus for the demo. Random demand generator + state holder for the dashboard's randomToggle switch. In production, delete this tab and feed cmd:demand from your real demand source." + "info": "Simulated inflow for the demo. A slow sinusoid generates inflow into the pumping station basin, which then drives the level-based pump control automatically.\n\nIn production, delete this tab \u2014 real inflow comes from upstream measurement sensors." }, { "id": "c_drv_title", "type": "comment", "z": "tab_drivers", - "name": "\ud83c\udf9b\ufe0f DEMO DRIVERS \u2014 auto stimulus only", - "info": "Removable: in production, replace this tab with the real demand source.", + "name": "\ud83c\udf9b\ufe0f DEMO DRIVERS \u2014 simulated basin inflow", + "info": "Sinus generator \u2192 q_in to pumpingStation. Basin fills \u2192 level-based\ncontrol starts pumps \u2192 basin drains \u2192 pumps stop \u2192 cycle repeats.", "x": 640, "y": 20, "wires": [] }, { - "id": "c_drv_state", + "id": "c_drv_sinus", "type": "comment", "z": "tab_drivers", - "name": "\u2500\u2500 Random toggle state \u2500\u2500", - "info": "", + "name": "\u2500\u2500 Sinusoidal inflow generator \u2500\u2500", + "info": "Produces a smooth inflow curve (m\u00b3/s) and sends to pumpingStation\nvia the cmd:q_in link channel. Period = 120s.", "x": 640, "y": 100, "wires": [] }, { - "id": "lin_random_to_drivers", - "type": "link in", - "z": "tab_drivers", - "name": "cmd:randomToggle", - "links": [ - "lout_random_dash" - ], - "x": 120, - "y": 140, - "wires": [ - [ - "random_state" - ] - ] - }, - { - "id": "random_state", - "type": "function", - "z": "tab_drivers", - "name": "store random on/off", - "func": "flow.set('randomOn', msg.payload === 'on');\nreturn null;", - "outputs": 1, - "noerr": 0, - "initialize": "", - "finalize": "", - "libs": [], - "x": 380, - "y": 140, - "wires": [ - [] - ] - }, - { - "id": "c_drv_random", - "type": "comment", - "z": "tab_drivers", - "name": "\u2500\u2500 Random demand generator \u2500\u2500 (every 3 s)", - "info": "", - "x": 640, - "y": 250, - "wires": [] - }, - { - "id": "rand_tick", + "id": "sinus_tick", "type": "inject", "z": "tab_drivers", - "name": "tick (random demand)", + "name": "tick (1s inflow)", "props": [ { "p": "topic", @@ -3427,52 +3354,51 @@ "vt": "date" } ], - "topic": "randomTick", + "topic": "sinusTick", "payload": "", "payloadType": "date", - "repeat": "3", + "repeat": "1", "crontab": "", "once": false, "onceDelay": "0.5", "x": 120, - "y": 290, + "y": 140, "wires": [ [ - "random_demand_fn" + "sinus_fn" ] ] }, { - "id": "random_demand_fn", + "id": "sinus_fn", "type": "function", "z": "tab_drivers", - "name": "random demand", - "func": "if (!flow.get('randomOn')) return null;\nconst v = Math.round(40 + Math.random() * 200);\nreturn { topic: 'manualDemand', payload: v };", + "name": "sinus inflow (m\u00b3/s)", + "func": "const base = 0.005; // m\u00b3/s (~18 m\u00b3/h always)\nconst amplitude = 0.03; // m\u00b3/s (~108 m\u00b3/h peak)\nconst period = 120; // seconds per full cycle\nconst t = Date.now() / 1000; // seconds since epoch\nconst q = base + amplitude * (1 + Math.sin(2 * Math.PI * t / period)) / 2;\nreturn { topic: 'q_in', payload: q, unit: 'm3/s', timestamp: Date.now() };", "outputs": 1, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 600, - "y": 290, + "y": 140, "wires": [ [ - "lout_demand_drivers" + "lout_qin_drivers" ] ] }, { - "id": "lout_demand_drivers", + "id": "lout_qin_drivers", "type": "link out", "z": "tab_drivers", - "name": "cmd:demand", + "name": "cmd:q_in", "mode": "link", "links": [ - "lin_demand_to_mgc", - "lin_demand_to_text" + "lin_qin_at_ps" ], "x": 900, - "y": 290, + "y": 140, "wires": [] }, { @@ -3496,7 +3422,7 @@ "id": "setup_mgc_scaling", "type": "inject", "z": "tab_setup", - "name": "MGC scaling = absolute", + "name": "MGC scaling = normalized", "props": [ { "p": "topic", @@ -3504,12 +3430,12 @@ }, { "p": "payload", - "v": "absolute", + "v": "normalized", "vt": "str" } ], "topic": "setScaling", - "payload": "absolute", + "payload": "normalized", "payloadType": "str", "repeat": "", "crontab": "", @@ -3654,49 +3580,5 @@ "x": 380, "y": 350, "wires": [] - }, - { - "id": "setup_random_on", - "type": "inject", - "z": "tab_setup", - "name": "auto-enable random demand", - "props": [ - { - "p": "topic", - "vt": "str" - }, - { - "p": "payload", - "v": "on", - "vt": "str" - } - ], - "topic": "randomToggle", - "payload": "on", - "payloadType": "str", - "repeat": "", - "crontab": "", - "once": true, - "onceDelay": "5", - "x": 120, - "y": 450, - "wires": [ - [ - "lout_setup_random" - ] - ] - }, - { - "id": "lout_setup_random", - "type": "link out", - "z": "tab_setup", - "name": "cmd:randomToggle", - "mode": "link", - "links": [ - "lin_random_to_drivers" - ], - "x": 380, - "y": 450, - "wires": [] } ] diff --git a/nodes/pumpingStation b/nodes/pumpingStation index 9f430ce..f869296 160000 --- a/nodes/pumpingStation +++ b/nodes/pumpingStation @@ -1 +1 @@ -Subproject commit 9f430cebb5cd3f5321fdd4ad4679a5cc09b936e5 +Subproject commit f869296832245894447b8a3aba9205c08f5a903b