wip: sinus-driven pumping station demo + PS levelbased control to MGC
Some checks failed
CI / lint-and-test (push) Has been cancelled
Some checks failed
CI / lint-and-test (push) Has been cancelled
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) <noreply@anthropic.com>
This commit is contained in:
@@ -481,26 +481,10 @@ def build_process_tab():
|
|||||||
"── MGC ── (orchestrates the 3 pumps via optimalcontrol)",
|
"── MGC ── (orchestrates the 3 pumps via optimalcontrol)",
|
||||||
"Receives Qd from cmd:demand link-in. Distributes flow across pumps."
|
"Receives Qd from cmd:demand link-in. Distributes flow across pumps."
|
||||||
))
|
))
|
||||||
nodes.append(link_in(
|
# MGC no longer receives direct Qd from the dashboard — PS drives it
|
||||||
"lin_demand_to_mgc", TAB_PROCESS, LANE_X[0], y_mgc + 60,
|
# via level-based control or manual Qd forwarding. The demand_fanout
|
||||||
CH_DEMAND,
|
# has been replaced by: sinus → q_in → PS (levelbased), and
|
||||||
source_out_ids=[f"lout_demand_drivers", f"lout_demand_dash"],
|
# slider → Qd → PS (manual mode only).
|
||||||
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]],
|
|
||||||
))
|
|
||||||
nodes.append({
|
nodes.append({
|
||||||
"id": MGC_ID, "type": "machineGroupControl", "z": TAB_PROCESS,
|
"id": MGC_ID, "type": "machineGroupControl", "z": TAB_PROCESS,
|
||||||
"name": "MGC — Pump Group",
|
"name": "MGC — Pump Group",
|
||||||
@@ -552,8 +536,34 @@ def build_process_tab():
|
|||||||
# ---------------- PS ----------------
|
# ---------------- PS ----------------
|
||||||
y_ps = 100 + 4 * SECTION_GAP
|
y_ps = 100 + 4 * SECTION_GAP
|
||||||
nodes.append(comment("c_ps", TAB_PROCESS, LANE_X[2], y_ps,
|
nodes.append(comment("c_ps", TAB_PROCESS, LANE_X[2], y_ps,
|
||||||
"── Pumping Station ── (basin model, manual control mode)",
|
"── Pumping Station ── (basin model, levelbased control)",
|
||||||
"Receives q_in from demand fanout. Emits formatted basin state."
|
"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({
|
nodes.append({
|
||||||
"id": PS_ID, "type": "pumpingStation", "z": TAB_PROCESS,
|
"id": PS_ID, "type": "pumpingStation", "z": TAB_PROCESS,
|
||||||
@@ -566,13 +576,19 @@ def build_process_tab():
|
|||||||
"hasDistance": False, "distance": 0, "distanceUnit": "m",
|
"hasDistance": False, "distance": 0, "distanceUnit": "m",
|
||||||
"distanceDescription": "",
|
"distanceDescription": "",
|
||||||
"processOutputFormat": "process", "dbaseOutputFormat": "influxdb",
|
"processOutputFormat": "process", "dbaseOutputFormat": "influxdb",
|
||||||
# PS in manual mode + safeties off — see top-level README for why.
|
# PS in levelbased mode — sinus inflow fills the basin, pumps start
|
||||||
"controlMode": "manual",
|
# when level > startLevel, stop when level < stopLevel.
|
||||||
"basinVolume": 50, "basinHeight": 4,
|
"controlMode": "levelbased",
|
||||||
"enableDryRunProtection": False,
|
"basinVolume": 10, "basinHeight": 3,
|
||||||
"enableOverfillProtection": False,
|
"startLevel": 1.2, "stopLevel": 0.6,
|
||||||
"dryRunThresholdPercent": 0,
|
"minFlowLevel": 0.6, "maxFlowLevel": 2.8,
|
||||||
"overfillThresholdPercent": 100,
|
"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,
|
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
||||||
"x": LANE_X[3], "y": y_ps + 80,
|
"x": LANE_X[3], "y": y_ps + 80,
|
||||||
"wires": [
|
"wires": [
|
||||||
@@ -721,49 +737,39 @@ def build_ui_tab():
|
|||||||
"── Process Demand ──", ""))
|
"── Process Demand ──", ""))
|
||||||
nodes.append(ui_slider(
|
nodes.append(ui_slider(
|
||||||
"ui_demand_slider", TAB_UI, LANE_X[0], y + 40, g_demand,
|
"ui_demand_slider", TAB_UI, LANE_X[0], y + 40, g_demand,
|
||||||
"Process demand slider", "Process Demand (m³/h)",
|
"Manual demand (manual mode only)", "Manual demand (m³/h) — active in manual mode only",
|
||||||
0, 300, 5.0, "manualDemand",
|
0, 100, 5.0, "manualDemand",
|
||||||
wires=["lout_demand_dash"]
|
wires=["lout_demand_dash"]
|
||||||
))
|
))
|
||||||
nodes.append(link_out(
|
nodes.append(link_out(
|
||||||
"lout_demand_dash", TAB_UI, LANE_X[1], y + 40,
|
"lout_demand_dash", TAB_UI, LANE_X[1], y + 40,
|
||||||
CH_DEMAND, target_in_ids=["lin_demand_to_mgc", "lin_demand_to_text"]
|
"cmd:Qd", target_in_ids=["lin_qd_at_ps"]
|
||||||
))
|
|
||||||
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"]
|
|
||||||
))
|
))
|
||||||
nodes.append(ui_text(
|
nodes.append(ui_text(
|
||||||
"ui_demand_text", TAB_UI, LANE_X[3], y + 40, g_demand,
|
"ui_demand_text", TAB_UI, LANE_X[3], y + 40, g_demand,
|
||||||
"Current demand", "Current demand", "{{msg.payload}} m³/h"
|
"Manual demand (active in manual mode)", "Manual 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"]
|
|
||||||
))
|
))
|
||||||
|
# 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 =====
|
# ===== SECTION: Mode + Station Buttons =====
|
||||||
y = 320
|
y = 320
|
||||||
nodes.append(comment("c_ui_station", TAB_UI, LANE_X[2], y,
|
nodes.append(comment("c_ui_station", TAB_UI, LANE_X[2], y,
|
||||||
"── Mode + Station-wide buttons ──", ""))
|
"── 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(
|
nodes.append(ui_switch(
|
||||||
"ui_mode_toggle", TAB_UI, LANE_X[0], y + 40, g_station,
|
"ui_mode_toggle", TAB_UI, LANE_X[0], y + 40, g_station,
|
||||||
"Auto/Manual mode",
|
"Station mode",
|
||||||
"Mode (Auto = MGC orchestrates · Manual = dashboard per-pump)",
|
"Station mode (Auto = level-based control · Manual = slider demand)",
|
||||||
on_value="auto", off_value="virtualControl", topic="setMode",
|
on_value="levelbased", off_value="manual", topic="changemode",
|
||||||
wires=["lout_mode_dash"]
|
wires=["lout_ps_mode_dash"]
|
||||||
))
|
))
|
||||||
nodes.append(link_out(
|
nodes.append(link_out(
|
||||||
"lout_mode_dash", TAB_UI, LANE_X[1], y + 40,
|
"lout_ps_mode_dash", TAB_UI, LANE_X[1], y + 40,
|
||||||
CH_MODE, target_in_ids=["lin_mode"]
|
"cmd:ps-mode", target_in_ids=["lin_ps_mode_at_ps"]
|
||||||
))
|
))
|
||||||
|
|
||||||
for k, (text, payload, color, icon, lout_id, channel) in enumerate([
|
for k, (text, payload, color, icon, lout_id, channel) in enumerate([
|
||||||
@@ -1052,53 +1058,52 @@ def build_drivers_tab():
|
|||||||
"id": TAB_DRIVERS, "type": "tab",
|
"id": TAB_DRIVERS, "type": "tab",
|
||||||
"label": "🎛️ Demo Drivers",
|
"label": "🎛️ Demo Drivers",
|
||||||
"disabled": False,
|
"disabled": False,
|
||||||
"info": "Auto stimulus for the demo. Random demand generator + state holder "
|
"info": "Simulated inflow for the demo. A slow sinusoid generates "
|
||||||
"for the dashboard's randomToggle switch. In production, delete this "
|
"inflow into the pumping station basin, which then drives "
|
||||||
"tab and feed cmd:demand from your real demand source.",
|
"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,
|
nodes.append(comment("c_drv_title", TAB_DRIVERS, LANE_X[2], 20,
|
||||||
"🎛️ DEMO DRIVERS — auto stimulus only",
|
"🎛️ DEMO DRIVERS — simulated basin inflow",
|
||||||
"Removable: in production, replace this tab with the real demand source."
|
"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
|
y = 100
|
||||||
nodes.append(comment("c_drv_state", TAB_DRIVERS, LANE_X[2], y,
|
nodes.append(comment("c_drv_sinus", TAB_DRIVERS, LANE_X[2], y,
|
||||||
"── Random toggle state ──", ""))
|
"── Sinusoidal inflow generator ──",
|
||||||
nodes.append(link_in(
|
"Produces a smooth inflow curve (m³/s) and sends to pumpingStation\n"
|
||||||
"lin_random_to_drivers", TAB_DRIVERS, LANE_X[0], y + 40,
|
"via the cmd:q_in link channel. Period = 120s."
|
||||||
CH_RANDOM_TOGGLE, source_out_ids=["lout_random_dash"],
|
|
||||||
downstream=["random_state"]
|
|
||||||
))
|
))
|
||||||
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(
|
nodes.append(inject(
|
||||||
"rand_tick", TAB_DRIVERS, LANE_X[0], y + 40,
|
"sinus_tick", TAB_DRIVERS, LANE_X[0], y + 40,
|
||||||
"tick (random demand)",
|
"tick (1s inflow)",
|
||||||
topic="randomTick", payload="", payload_type="date",
|
topic="sinusTick", payload="", payload_type="date",
|
||||||
repeat="3", wires=["random_demand_fn"]
|
repeat="1", wires=["sinus_fn"]
|
||||||
))
|
))
|
||||||
nodes.append(function_node(
|
nodes.append(function_node(
|
||||||
"random_demand_fn", TAB_DRIVERS, LANE_X[1] + 220, y + 40,
|
"sinus_fn", TAB_DRIVERS, LANE_X[1] + 220, y + 40,
|
||||||
"random demand",
|
"sinus inflow (m³/s)",
|
||||||
"if (!flow.get('randomOn')) return null;\n"
|
"const base = 0.005; // m³/s (~18 m³/h always)\n"
|
||||||
"const v = Math.round(40 + Math.random() * 200);\n"
|
"const amplitude = 0.03; // m³/s (~108 m³/h peak)\n"
|
||||||
"return { topic: 'manualDemand', payload: v };",
|
"const period = 120; // seconds per full cycle\n"
|
||||||
outputs=1, wires=[["lout_demand_drivers"]]
|
"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(
|
nodes.append(link_out(
|
||||||
"lout_demand_drivers", TAB_DRIVERS, LANE_X[3], y + 40,
|
"lout_qin_drivers", TAB_DRIVERS, LANE_X[3], y + 40,
|
||||||
CH_DEMAND, target_in_ids=["lin_demand_to_mgc", "lin_demand_to_text"]
|
"cmd:q_in", target_in_ids=["lin_qin_at_ps"]
|
||||||
))
|
))
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
@@ -1126,8 +1131,8 @@ def build_setup_tab():
|
|||||||
y = 100
|
y = 100
|
||||||
nodes.append(inject(
|
nodes.append(inject(
|
||||||
"setup_mgc_scaling", TAB_SETUP, LANE_X[0], y,
|
"setup_mgc_scaling", TAB_SETUP, LANE_X[0], y,
|
||||||
"MGC scaling = absolute",
|
"MGC scaling = normalized",
|
||||||
topic="setScaling", payload="absolute", payload_type="str",
|
topic="setScaling", payload="normalized", payload_type="str",
|
||||||
once=True, once_delay="1.5",
|
once=True, once_delay="1.5",
|
||||||
wires=["lout_setup_to_mgc"]
|
wires=["lout_setup_to_mgc"]
|
||||||
))
|
))
|
||||||
@@ -1170,18 +1175,8 @@ def build_setup_tab():
|
|||||||
CH_STATION_START, target_in_ids=["lin_station_start"]
|
CH_STATION_START, target_in_ids=["lin_station_start"]
|
||||||
))
|
))
|
||||||
|
|
||||||
y = 450
|
# (Random demand removed — sinus inflow drives the demo automatically.
|
||||||
nodes.append(inject(
|
# No explicit "random on" inject needed.)
|
||||||
"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"]
|
|
||||||
))
|
|
||||||
|
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|||||||
@@ -689,45 +689,6 @@
|
|||||||
"y": 700,
|
"y": 700,
|
||||||
"wires": []
|
"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",
|
"id": "mgc_pumps",
|
||||||
"type": "machineGroupControl",
|
"type": "machineGroupControl",
|
||||||
@@ -797,12 +758,79 @@
|
|||||||
"id": "c_ps",
|
"id": "c_ps",
|
||||||
"type": "comment",
|
"type": "comment",
|
||||||
"z": "tab_process",
|
"z": "tab_process",
|
||||||
"name": "\u2500\u2500 Pumping Station \u2500\u2500 (basin model, manual control mode)",
|
"name": "\u2500\u2500 Pumping Station \u2500\u2500 (basin model, levelbased control)",
|
||||||
"info": "Receives q_in from demand fanout. Emits formatted basin state.",
|
"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,
|
"x": 640,
|
||||||
"y": 900,
|
"y": 900,
|
||||||
"wires": []
|
"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",
|
"id": "ps_basin",
|
||||||
"type": "pumpingStation",
|
"type": "pumpingStation",
|
||||||
@@ -824,13 +852,20 @@
|
|||||||
"distanceDescription": "",
|
"distanceDescription": "",
|
||||||
"processOutputFormat": "process",
|
"processOutputFormat": "process",
|
||||||
"dbaseOutputFormat": "influxdb",
|
"dbaseOutputFormat": "influxdb",
|
||||||
"controlMode": "manual",
|
"controlMode": "levelbased",
|
||||||
"basinVolume": 50,
|
"basinVolume": 10,
|
||||||
"basinHeight": 4,
|
"basinHeight": 3,
|
||||||
"enableDryRunProtection": false,
|
"startLevel": 1.2,
|
||||||
"enableOverfillProtection": false,
|
"stopLevel": 0.6,
|
||||||
"dryRunThresholdPercent": 0,
|
"minFlowLevel": 0.6,
|
||||||
"overfillThresholdPercent": 100,
|
"maxFlowLevel": 2.8,
|
||||||
|
"heightInlet": 2.5,
|
||||||
|
"heightOutlet": 0.2,
|
||||||
|
"heightOverflow": 2.8,
|
||||||
|
"enableDryRunProtection": true,
|
||||||
|
"enableOverfillProtection": true,
|
||||||
|
"dryRunThresholdPercent": 5,
|
||||||
|
"overfillThresholdPercent": 95,
|
||||||
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
||||||
"x": 900,
|
"x": 900,
|
||||||
"y": 980,
|
"y": 980,
|
||||||
@@ -1361,8 +1396,8 @@
|
|||||||
"type": "ui-slider",
|
"type": "ui-slider",
|
||||||
"z": "tab_ui",
|
"z": "tab_ui",
|
||||||
"group": "ui_grp_demand",
|
"group": "ui_grp_demand",
|
||||||
"name": "Process demand slider",
|
"name": "Manual demand (manual mode only)",
|
||||||
"label": "Process Demand (m\u00b3/h)",
|
"label": "Manual demand (m\u00b3/h) \u2014 active in manual mode only",
|
||||||
"tooltip": "",
|
"tooltip": "",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"width": "0",
|
"width": "0",
|
||||||
@@ -1372,7 +1407,7 @@
|
|||||||
"topic": "manualDemand",
|
"topic": "manualDemand",
|
||||||
"topicType": "str",
|
"topicType": "str",
|
||||||
"min": "0",
|
"min": "0",
|
||||||
"max": "300",
|
"max": "100",
|
||||||
"step": "5.0",
|
"step": "5.0",
|
||||||
"showLabel": true,
|
"showLabel": true,
|
||||||
"showValue": true,
|
"showValue": true,
|
||||||
@@ -1393,63 +1428,15 @@
|
|||||||
"id": "lout_demand_dash",
|
"id": "lout_demand_dash",
|
||||||
"type": "link out",
|
"type": "link out",
|
||||||
"z": "tab_ui",
|
"z": "tab_ui",
|
||||||
"name": "cmd:demand",
|
"name": "cmd:Qd",
|
||||||
"mode": "link",
|
"mode": "link",
|
||||||
"links": [
|
"links": [
|
||||||
"lin_demand_to_mgc",
|
"lin_qd_at_ps"
|
||||||
"lin_demand_to_text"
|
|
||||||
],
|
],
|
||||||
"x": 380,
|
"x": 380,
|
||||||
"y": 140,
|
"y": 140,
|
||||||
"wires": []
|
"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",
|
"id": "ui_demand_text",
|
||||||
"type": "ui-text",
|
"type": "ui-text",
|
||||||
@@ -1458,8 +1445,8 @@
|
|||||||
"order": 1,
|
"order": 1,
|
||||||
"width": "0",
|
"width": "0",
|
||||||
"height": "0",
|
"height": "0",
|
||||||
"name": "Current demand",
|
"name": "Manual demand (active in manual mode)",
|
||||||
"label": "Current demand",
|
"label": "Manual demand",
|
||||||
"format": "{{msg.payload}} m\u00b3/h",
|
"format": "{{msg.payload}} m\u00b3/h",
|
||||||
"layout": "row-left",
|
"layout": "row-left",
|
||||||
"style": false,
|
"style": false,
|
||||||
@@ -1470,23 +1457,6 @@
|
|||||||
"y": 140,
|
"y": 140,
|
||||||
"wires": []
|
"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",
|
"id": "c_ui_station",
|
||||||
"type": "comment",
|
"type": "comment",
|
||||||
@@ -1502,24 +1472,24 @@
|
|||||||
"type": "ui-switch",
|
"type": "ui-switch",
|
||||||
"z": "tab_ui",
|
"z": "tab_ui",
|
||||||
"group": "ui_grp_station",
|
"group": "ui_grp_station",
|
||||||
"name": "Auto/Manual mode",
|
"name": "Station mode",
|
||||||
"label": "Mode (Auto = MGC orchestrates \u00b7 Manual = dashboard per-pump)",
|
"label": "Station mode (Auto = level-based control \u00b7 Manual = slider demand)",
|
||||||
"tooltip": "",
|
"tooltip": "",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"width": "0",
|
"width": "0",
|
||||||
"height": "0",
|
"height": "0",
|
||||||
"passthru": true,
|
"passthru": true,
|
||||||
"decouple": "false",
|
"decouple": "false",
|
||||||
"topic": "setMode",
|
"topic": "changemode",
|
||||||
"topicType": "str",
|
"topicType": "str",
|
||||||
"style": "",
|
"style": "",
|
||||||
"className": "",
|
"className": "",
|
||||||
"evaluate": "true",
|
"evaluate": "true",
|
||||||
"onvalue": "auto",
|
"onvalue": "levelbased",
|
||||||
"onvalueType": "str",
|
"onvalueType": "str",
|
||||||
"onicon": "auto_mode",
|
"onicon": "auto_mode",
|
||||||
"oncolor": "#0f52a5",
|
"oncolor": "#0f52a5",
|
||||||
"offvalue": "virtualControl",
|
"offvalue": "manual",
|
||||||
"offvalueType": "str",
|
"offvalueType": "str",
|
||||||
"officon": "back_hand",
|
"officon": "back_hand",
|
||||||
"offcolor": "#888888",
|
"offcolor": "#888888",
|
||||||
@@ -1527,18 +1497,18 @@
|
|||||||
"y": 360,
|
"y": 360,
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
"lout_mode_dash"
|
"lout_ps_mode_dash"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lout_mode_dash",
|
"id": "lout_ps_mode_dash",
|
||||||
"type": "link out",
|
"type": "link out",
|
||||||
"z": "tab_ui",
|
"z": "tab_ui",
|
||||||
"name": "cmd:mode",
|
"name": "cmd:ps-mode",
|
||||||
"mode": "link",
|
"mode": "link",
|
||||||
"links": [
|
"links": [
|
||||||
"lin_mode"
|
"lin_ps_mode_at_ps"
|
||||||
],
|
],
|
||||||
"x": 380,
|
"x": 380,
|
||||||
"y": 360,
|
"y": 360,
|
||||||
@@ -3346,76 +3316,33 @@
|
|||||||
"type": "tab",
|
"type": "tab",
|
||||||
"label": "\ud83c\udf9b\ufe0f Demo Drivers",
|
"label": "\ud83c\udf9b\ufe0f Demo Drivers",
|
||||||
"disabled": false,
|
"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",
|
"id": "c_drv_title",
|
||||||
"type": "comment",
|
"type": "comment",
|
||||||
"z": "tab_drivers",
|
"z": "tab_drivers",
|
||||||
"name": "\ud83c\udf9b\ufe0f DEMO DRIVERS \u2014 auto stimulus only",
|
"name": "\ud83c\udf9b\ufe0f DEMO DRIVERS \u2014 simulated basin inflow",
|
||||||
"info": "Removable: in production, replace this tab with the real demand source.",
|
"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,
|
"x": 640,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
"wires": []
|
"wires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "c_drv_state",
|
"id": "c_drv_sinus",
|
||||||
"type": "comment",
|
"type": "comment",
|
||||||
"z": "tab_drivers",
|
"z": "tab_drivers",
|
||||||
"name": "\u2500\u2500 Random toggle state \u2500\u2500",
|
"name": "\u2500\u2500 Sinusoidal inflow generator \u2500\u2500",
|
||||||
"info": "",
|
"info": "Produces a smooth inflow curve (m\u00b3/s) and sends to pumpingStation\nvia the cmd:q_in link channel. Period = 120s.",
|
||||||
"x": 640,
|
"x": 640,
|
||||||
"y": 100,
|
"y": 100,
|
||||||
"wires": []
|
"wires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lin_random_to_drivers",
|
"id": "sinus_tick",
|
||||||
"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",
|
|
||||||
"type": "inject",
|
"type": "inject",
|
||||||
"z": "tab_drivers",
|
"z": "tab_drivers",
|
||||||
"name": "tick (random demand)",
|
"name": "tick (1s inflow)",
|
||||||
"props": [
|
"props": [
|
||||||
{
|
{
|
||||||
"p": "topic",
|
"p": "topic",
|
||||||
@@ -3427,52 +3354,51 @@
|
|||||||
"vt": "date"
|
"vt": "date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"topic": "randomTick",
|
"topic": "sinusTick",
|
||||||
"payload": "",
|
"payload": "",
|
||||||
"payloadType": "date",
|
"payloadType": "date",
|
||||||
"repeat": "3",
|
"repeat": "1",
|
||||||
"crontab": "",
|
"crontab": "",
|
||||||
"once": false,
|
"once": false,
|
||||||
"onceDelay": "0.5",
|
"onceDelay": "0.5",
|
||||||
"x": 120,
|
"x": 120,
|
||||||
"y": 290,
|
"y": 140,
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
"random_demand_fn"
|
"sinus_fn"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "random_demand_fn",
|
"id": "sinus_fn",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"z": "tab_drivers",
|
"z": "tab_drivers",
|
||||||
"name": "random demand",
|
"name": "sinus inflow (m\u00b3/s)",
|
||||||
"func": "if (!flow.get('randomOn')) return null;\nconst v = Math.round(40 + Math.random() * 200);\nreturn { topic: 'manualDemand', payload: v };",
|
"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,
|
"outputs": 1,
|
||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
"finalize": "",
|
"finalize": "",
|
||||||
"libs": [],
|
"libs": [],
|
||||||
"x": 600,
|
"x": 600,
|
||||||
"y": 290,
|
"y": 140,
|
||||||
"wires": [
|
"wires": [
|
||||||
[
|
[
|
||||||
"lout_demand_drivers"
|
"lout_qin_drivers"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "lout_demand_drivers",
|
"id": "lout_qin_drivers",
|
||||||
"type": "link out",
|
"type": "link out",
|
||||||
"z": "tab_drivers",
|
"z": "tab_drivers",
|
||||||
"name": "cmd:demand",
|
"name": "cmd:q_in",
|
||||||
"mode": "link",
|
"mode": "link",
|
||||||
"links": [
|
"links": [
|
||||||
"lin_demand_to_mgc",
|
"lin_qin_at_ps"
|
||||||
"lin_demand_to_text"
|
|
||||||
],
|
],
|
||||||
"x": 900,
|
"x": 900,
|
||||||
"y": 290,
|
"y": 140,
|
||||||
"wires": []
|
"wires": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3496,7 +3422,7 @@
|
|||||||
"id": "setup_mgc_scaling",
|
"id": "setup_mgc_scaling",
|
||||||
"type": "inject",
|
"type": "inject",
|
||||||
"z": "tab_setup",
|
"z": "tab_setup",
|
||||||
"name": "MGC scaling = absolute",
|
"name": "MGC scaling = normalized",
|
||||||
"props": [
|
"props": [
|
||||||
{
|
{
|
||||||
"p": "topic",
|
"p": "topic",
|
||||||
@@ -3504,12 +3430,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"p": "payload",
|
"p": "payload",
|
||||||
"v": "absolute",
|
"v": "normalized",
|
||||||
"vt": "str"
|
"vt": "str"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"topic": "setScaling",
|
"topic": "setScaling",
|
||||||
"payload": "absolute",
|
"payload": "normalized",
|
||||||
"payloadType": "str",
|
"payloadType": "str",
|
||||||
"repeat": "",
|
"repeat": "",
|
||||||
"crontab": "",
|
"crontab": "",
|
||||||
@@ -3654,49 +3580,5 @@
|
|||||||
"x": 380,
|
"x": 380,
|
||||||
"y": 350,
|
"y": 350,
|
||||||
"wires": []
|
"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": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Submodule nodes/pumpingStation updated: 9f430cebb5...f869296832
Reference in New Issue
Block a user