Compare commits
1 Commits
8507ee4e02
...
fe5fa3577b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5fa3577b |
@@ -1,340 +1,479 @@
|
||||
[
|
||||
{
|
||||
"id": "ps_basic_tab",
|
||||
"type": "tab",
|
||||
"label": "PumpingStation - Basic",
|
||||
"disabled": false,
|
||||
"info": "Tier 1: single pumpingStation node driven by inject nodes only. Demonstrates the canonical Phase-2 topic API: set.mode, set.inflow, set.demand."
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_title",
|
||||
"type": "comment",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "PumpingStation - Basic\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nA 50 m³ basin (3.5 m tall, inflow at 3.0 m, outflow at 0.2 m,\noverflow at 3.2 m). controlMode = levelbased, manual demand allowed\nonly when set.mode = manual.\n\nHOW TO USE:\n 1. Deploy the flow.\n 2. Click \"set.mode = manual\" so set.demand is honoured.\n 3. Click \"set.inflow = 60 m3/h\" to push wastewater into the basin.\n 4. Watch the basin fill on Port 0 (level, volume, percControl rise).\n 5. Click \"calibrate volume 25 m3\" to jump straight to half-full.\n\nAliases (changemode, q_in, Qd, …) still work but log a deprecation\nwarning - fresh flows use the canonical names.",
|
||||
"info": "",
|
||||
"x": 600,
|
||||
"y": 40,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_inj_mode",
|
||||
"type": "inject",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "set.mode = manual",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "manual",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"topic": "set.mode",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"x": 200,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_node"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_inj_mode_lvl",
|
||||
"type": "inject",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "set.mode = levelbased",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "levelbased",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"topic": "set.mode",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"x": 220,
|
||||
"y": 200,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_node"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_inj_inflow",
|
||||
"type": "inject",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "set.inflow = 60 m3/h",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "60",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"topic": "set.inflow",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"x": 200,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_node"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_inj_demand",
|
||||
"type": "inject",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "set.demand = 40 %",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "40",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"topic": "set.demand",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"x": 200,
|
||||
"y": 300,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_node"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_inj_calvol",
|
||||
"type": "inject",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "calibrate volume 25 m3",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "25",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"topic": "cmd.calibrate.volume",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"x": 220,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_node"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_inj_callvl",
|
||||
"type": "inject",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "calibrate level 1.5 m",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "1.5",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"topic": "cmd.calibrate.level",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"x": 220,
|
||||
"y": 400,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_node"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_node",
|
||||
"type": "pumpingStation",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "Pumping Station",
|
||||
"simulator": false,
|
||||
"basinVolume": 50,
|
||||
"basinHeight": 3.5,
|
||||
"inflowLevel": 3,
|
||||
"outflowLevel": 0.2,
|
||||
"overflowLevel": 3.2,
|
||||
"defaultFluid": "wastewater",
|
||||
"inletPipeDiameter": 0.3,
|
||||
"outletPipeDiameter": 0.3,
|
||||
"pipelineLength": 80,
|
||||
"maxDischargeHead": 24,
|
||||
"staticHead": 12,
|
||||
"maxInflowRate": 200,
|
||||
"temperatureReferenceDegC": 15,
|
||||
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
||||
"enableDryRunProtection": true,
|
||||
"enableOverfillProtection": true,
|
||||
"dryRunThresholdPercent": 2,
|
||||
"overfillThresholdPercent": 98,
|
||||
"minHeightBasedOn": "outlet",
|
||||
"processOutputFormat": "process",
|
||||
"dbaseOutputFormat": "influxdb",
|
||||
"refHeight": "NAP",
|
||||
"basinBottomRef": 1,
|
||||
"uuid": "example-ps-001",
|
||||
"supplier": "WBD-RD",
|
||||
"category": "station",
|
||||
"assetType": "pumpingstation",
|
||||
"model": "demo-50m3",
|
||||
"unit": "m3/h",
|
||||
"enableLog": true,
|
||||
"logLevel": "info",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": "",
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"controlMode": "levelbased",
|
||||
"startLevel": 1.2,
|
||||
"minLevel": 0.4,
|
||||
"maxLevel": 2.8,
|
||||
"flowSetpoint": null,
|
||||
"flowDeadband": null,
|
||||
"x": 1320,
|
||||
"y": 300,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_format"
|
||||
],
|
||||
[
|
||||
"ps_basic_dbg_influx"
|
||||
],
|
||||
[
|
||||
"ps_basic_dbg_parent"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_format",
|
||||
"type": "function",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "Merge deltas + format",
|
||||
"func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst cache = context.get('c') || {};\nObject.assign(cache, p);\ncontext.set('c', cache);\nfunction pick(prefix) {\n for (const k of Object.keys(cache)) if (k === prefix || k.indexOf(prefix + '.') === 0) {\n const v = Number(cache[k]); if (Number.isFinite(v)) return v;\n } return null;\n}\nconst vol = pick('volume.predicted.atequipment');\nconst lvl = pick('level.predicted.atequipment');\nconst flIn = pick('flow.predicted.in');\nmsg.payload = {\n state: cache.state || 'unknown',\n controlMode: cache.controlMode || cache.mode || 'n/a',\n direction: cache.direction || 'n/a',\n percControl: cache.percControl != null ? Number(cache.percControl).toFixed(1) + ' %' : 'n/a',\n volume: vol != null ? vol.toFixed(2) + ' m3' : 'n/a',\n volumePercent: cache.volumePercent != null ? Number(cache.volumePercent).toFixed(1) + ' %' : 'n/a',\n level: lvl != null ? lvl.toFixed(3) + ' m' : 'n/a',\n inflow: flIn != null ? (flIn * 3600).toFixed(1) + ' m3/h' : 'n/a',\n timeToFull: cache.timeToFull != null ? Number(cache.timeToFull).toFixed(0) + ' s' : 'n/a',\n timeToEmpty: cache.timeToEmpty != null ? Number(cache.timeToEmpty).toFixed(0) + ' s' : 'n/a'\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 1560,
|
||||
"y": 280,
|
||||
"wires": [
|
||||
[
|
||||
"ps_basic_dbg_process"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_dbg_process",
|
||||
"type": "debug",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "Port 0: Process",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"x": 1800,
|
||||
"y": 240,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_dbg_influx",
|
||||
"type": "debug",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "Port 1: InfluxDB",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1800,
|
||||
"y": 320,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_basic_dbg_parent",
|
||||
"type": "debug",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "Port 2: Parent reg",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1800,
|
||||
"y": 380,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "grp_ps_basic",
|
||||
"type": "group",
|
||||
"z": "ps_basic_tab",
|
||||
"name": "Pumping Station (PC)",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#0c99d9",
|
||||
"fill-opacity": "0.10"
|
||||
{
|
||||
"id": "77f00aef1c966167",
|
||||
"type": "tab",
|
||||
"label": "PumpingStation - Basic",
|
||||
"disabled": false,
|
||||
"info": "Tier 1: single pumpingStation node driven by inject nodes only. Demonstrates the canonical Phase-2 topic API: set.mode, set.inflow, set.demand."
|
||||
},
|
||||
"nodes": [
|
||||
"ps_basic_node",
|
||||
"ps_basic_format"
|
||||
],
|
||||
"x": 1290,
|
||||
"y": 230,
|
||||
"w": 500,
|
||||
"h": 140
|
||||
}
|
||||
]
|
||||
{
|
||||
"id": "aa3381b896eb2cfb",
|
||||
"type": "group",
|
||||
"z": "77f00aef1c966167",
|
||||
"name": "Pumping Station (Process Cell)",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#0c99d9",
|
||||
"fill-opacity": "0.10"
|
||||
},
|
||||
"nodes": [
|
||||
"8e78b6607deb33a7"
|
||||
],
|
||||
"x": 534,
|
||||
"y": 351.5,
|
||||
"w": 232,
|
||||
"h": 97
|
||||
},
|
||||
{
|
||||
"id": "4996420d47442fad",
|
||||
"type": "group",
|
||||
"z": "77f00aef1c966167",
|
||||
"name": "1. Control mode",
|
||||
"style": {
|
||||
"stroke": "#666666",
|
||||
"fill": "#ffdf7f",
|
||||
"fill-opacity": "0.15",
|
||||
"label": true,
|
||||
"color": "#333333"
|
||||
},
|
||||
"nodes": [
|
||||
"1155bbbde7c65363",
|
||||
"e9bea0f95b557f5d"
|
||||
],
|
||||
"x": 94,
|
||||
"y": 119,
|
||||
"w": 272,
|
||||
"h": 122
|
||||
},
|
||||
{
|
||||
"id": "a9f9b38b0e00c1d7",
|
||||
"type": "group",
|
||||
"z": "77f00aef1c966167",
|
||||
"name": "2. Flow signals (inflow / outflow)",
|
||||
"style": {
|
||||
"stroke": "#666666",
|
||||
"fill": "#ffdf7f",
|
||||
"fill-opacity": "0.15",
|
||||
"label": true,
|
||||
"color": "#333333"
|
||||
},
|
||||
"nodes": [
|
||||
"7b2b5eb919b1ab15",
|
||||
"3350187815774b95"
|
||||
],
|
||||
"x": 94,
|
||||
"y": 279,
|
||||
"w": 262,
|
||||
"h": 122
|
||||
},
|
||||
{
|
||||
"id": "42bf82c87d05f498",
|
||||
"type": "group",
|
||||
"z": "77f00aef1c966167",
|
||||
"name": "3. Operator demand (manual mode only)",
|
||||
"style": {
|
||||
"stroke": "#666666",
|
||||
"fill": "#ffdf7f",
|
||||
"fill-opacity": "0.15",
|
||||
"label": true,
|
||||
"color": "#333333"
|
||||
},
|
||||
"nodes": [
|
||||
"48c2262c345c46b9"
|
||||
],
|
||||
"x": 94,
|
||||
"y": 479,
|
||||
"w": 261,
|
||||
"h": 82
|
||||
},
|
||||
{
|
||||
"id": "234bdce20170061a",
|
||||
"type": "group",
|
||||
"z": "77f00aef1c966167",
|
||||
"name": "4. Calibration",
|
||||
"style": {
|
||||
"stroke": "#666666",
|
||||
"fill": "#ffdf7f",
|
||||
"fill-opacity": "0.15",
|
||||
"label": true,
|
||||
"color": "#333333"
|
||||
},
|
||||
"nodes": [
|
||||
"463eefdd54df89a5",
|
||||
"2e0642275899fc79"
|
||||
],
|
||||
"x": 94,
|
||||
"y": 599,
|
||||
"w": 272,
|
||||
"h": 122
|
||||
},
|
||||
{
|
||||
"id": "f4ba4542514ed853",
|
||||
"type": "group",
|
||||
"z": "77f00aef1c966167",
|
||||
"name": "Expected outputs",
|
||||
"style": {
|
||||
"stroke": "#666666",
|
||||
"fill": "#d1d1d1",
|
||||
"fill-opacity": "0.2",
|
||||
"label": true,
|
||||
"color": "#333333"
|
||||
},
|
||||
"nodes": [
|
||||
"b2450e5ee2eebfaa",
|
||||
"386af1ad8aa8ed12",
|
||||
"c27c2655f199b530"
|
||||
],
|
||||
"x": 874,
|
||||
"y": 299,
|
||||
"w": 252,
|
||||
"h": 202
|
||||
},
|
||||
{
|
||||
"id": "b30af582f935bcb7",
|
||||
"type": "comment",
|
||||
"z": "77f00aef1c966167",
|
||||
"name": "PumpingStation — Basic (Tier 1)",
|
||||
"info": "Single pumpingStation node driven by inject buttons. Shows the canonical msg.topic command surface.\n\nDefault controlMode = levelbased. Switch to manual to honour set.demand.\n\nHOW TO USE\n1. Deploy the flow.\n2. (optional) Click \"set.mode = manual\" if you want set.demand to forward; otherwise leave it on levelbased and the ramp drives demand from level.\n3. Click \"set.inflow = 60 m³/h\" to push wastewater into the basin.\n4. Watch the basin fill on Port 0 (level, volume rise) and Port 1 (InfluxDB-shaped payload).\n5. In manual mode: click \"set.demand = 40\" — the value surfaces as `manualDemand` on Port 0/1 and in the node status badge.\n6. Click \"calibrate volume 25 m³\" or \"calibrate level 1.5 m\" to snap the predicted-volume integrator.\n\nPORTS\n- Port 0: process output (changed fields only)\n- Port 1: InfluxDB-shaped {measurement, fields, tags, timestamp}\n- Port 2: parent registration (child handshake)",
|
||||
"x": 650,
|
||||
"y": 300,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "1155bbbde7c65363",
|
||||
"type": "inject",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "4996420d47442fad",
|
||||
"name": "set.mode = manual",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "manual",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"topic": "set.mode",
|
||||
"x": 230,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"8e78b6607deb33a7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e9bea0f95b557f5d",
|
||||
"type": "inject",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "4996420d47442fad",
|
||||
"name": "set.mode = levelbased",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "levelbased",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"topic": "set.mode",
|
||||
"x": 240,
|
||||
"y": 200,
|
||||
"wires": [
|
||||
[
|
||||
"8e78b6607deb33a7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7b2b5eb919b1ab15",
|
||||
"type": "inject",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "a9f9b38b0e00c1d7",
|
||||
"name": "set.inflow = 60 m3/h",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "60",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"topic": "set.inflow",
|
||||
"x": 240,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"8e78b6607deb33a7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "48c2262c345c46b9",
|
||||
"type": "inject",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "42bf82c87d05f498",
|
||||
"name": "set.demand = 40 %",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "40",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"topic": "set.demand",
|
||||
"x": 230,
|
||||
"y": 520,
|
||||
"wires": [
|
||||
[
|
||||
"8e78b6607deb33a7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "463eefdd54df89a5",
|
||||
"type": "inject",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "234bdce20170061a",
|
||||
"name": "calibrate volume 25 m3",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "25",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"topic": "cmd.calibrate.volume",
|
||||
"x": 240,
|
||||
"y": 640,
|
||||
"wires": [
|
||||
[
|
||||
"8e78b6607deb33a7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "2e0642275899fc79",
|
||||
"type": "inject",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "234bdce20170061a",
|
||||
"name": "calibrate level 1.5 m",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "1.5",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"topic": "cmd.calibrate.level",
|
||||
"x": 240,
|
||||
"y": 680,
|
||||
"wires": [
|
||||
[
|
||||
"8e78b6607deb33a7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b2450e5ee2eebfaa",
|
||||
"type": "debug",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "f4ba4542514ed853",
|
||||
"name": "Port 0: Process",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"x": 980,
|
||||
"y": 340,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "386af1ad8aa8ed12",
|
||||
"type": "debug",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "f4ba4542514ed853",
|
||||
"name": "Port 1: InfluxDB",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 980,
|
||||
"y": 400,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "c27c2655f199b530",
|
||||
"type": "debug",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "f4ba4542514ed853",
|
||||
"name": "Port 2: Parent reg",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 990,
|
||||
"y": 460,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "8e78b6607deb33a7",
|
||||
"type": "pumpingStation",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "aa3381b896eb2cfb",
|
||||
"name": "",
|
||||
"simulator": false,
|
||||
"basinVolume": 50,
|
||||
"basinHeight": 4,
|
||||
"inflowLevel": 1.5,
|
||||
"outflowLevel": 0.2,
|
||||
"overflowLevel": 3.8,
|
||||
"defaultFluid": "wastewater",
|
||||
"inletPipeDiameter": 0.3,
|
||||
"outletPipeDiameter": 0.3,
|
||||
"pipelineLength": 80,
|
||||
"maxDischargeHead": 24,
|
||||
"staticHead": 12,
|
||||
"maxInflowRate": 200,
|
||||
"temperatureReferenceDegC": 15,
|
||||
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
||||
"enableDryRunProtection": true,
|
||||
"enableHighVolumeSafety": true,
|
||||
"enableOverfillProtection": true,
|
||||
"dryRunThresholdPercent": 2,
|
||||
"highVolumeSafetyThresholdPercent": 98,
|
||||
"overfillThresholdPercent": 98,
|
||||
"minHeightBasedOn": "outlet",
|
||||
"processOutputFormat": "process",
|
||||
"dbaseOutputFormat": "influxdb",
|
||||
"refHeight": "NAP",
|
||||
"basinBottomRef": 1,
|
||||
"uuid": "",
|
||||
"supplier": "",
|
||||
"category": "",
|
||||
"assetType": "",
|
||||
"model": "",
|
||||
"unit": "",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "⊥",
|
||||
"hasDistance": false,
|
||||
"distance": "",
|
||||
"controlMode": "levelbased",
|
||||
"levelCurveType": "linear",
|
||||
"logCurveFactor": 9,
|
||||
"enableShiftedRamp": false,
|
||||
"shiftLevel": 0,
|
||||
"shiftArmPercent": 95,
|
||||
"startLevel": 1,
|
||||
"stopLevel": 0.5,
|
||||
"minLevel": 0.20400000000000001,
|
||||
"maxLevel": 3.8,
|
||||
"flowSetpoint": null,
|
||||
"flowDeadband": null,
|
||||
"x": 650,
|
||||
"y": 400,
|
||||
"wires": [
|
||||
[
|
||||
"b2450e5ee2eebfaa"
|
||||
],
|
||||
[
|
||||
"386af1ad8aa8ed12"
|
||||
],
|
||||
[
|
||||
"c27c2655f199b530"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "3350187815774b95",
|
||||
"type": "inject",
|
||||
"z": "77f00aef1c966167",
|
||||
"g": "a9f9b38b0e00c1d7",
|
||||
"name": "set.outflow= 80 m3/h",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"topic": "set.outflow",
|
||||
"payload": "80",
|
||||
"payloadType": "num",
|
||||
"x": 230,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"8e78b6607deb33a7"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ef77c1819422a098",
|
||||
"type": "global-config",
|
||||
"env": [],
|
||||
"modules": {
|
||||
"EVOLV": "1.0.29"
|
||||
}
|
||||
}
|
||||
]
|
||||
1070
examples/02-Dashboard.json
Normal file
1070
examples/02-Dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,686 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "ps_int_proc",
|
||||
"type": "tab",
|
||||
"label": "Process Plant",
|
||||
"disabled": false,
|
||||
"info": "Tier 2: pumpingStation + measurement child + machineGroupControl parent with two rotatingMachine pumps. Demonstrates Phase-2 parent/child handshakes and the canonical set.mode/set.inflow/set.demand topics."
|
||||
},
|
||||
{
|
||||
"id": "ps_int_setup",
|
||||
"type": "tab",
|
||||
"label": "Setup",
|
||||
"disabled": false,
|
||||
"info": "Deploy-time once-true injects that initialise control modes on the EVOLV nodes."
|
||||
},
|
||||
{
|
||||
"id": "ps_int_title",
|
||||
"type": "comment",
|
||||
"z": "ps_int_proc",
|
||||
"name": "PumpingStation - Integration\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nL0 link-ins | L2 level sensor (CM) | L3 pumps (EM) | L4 MGC (UN) | L5 station (PC).\nPumps register with MGC via Port 2; MGC and the level sensor register with the station via Port 2.\nCross-tab channels: setup:* drive once-true initialisation from the Setup tab.",
|
||||
"info": "",
|
||||
"x": 600,
|
||||
"y": 40,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "lin_setup_mode",
|
||||
"type": "link in",
|
||||
"z": "ps_int_proc",
|
||||
"name": "setup:to-ps-mode",
|
||||
"links": [],
|
||||
"x": 120,
|
||||
"y": 500,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_station"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lin_setup_inflow",
|
||||
"type": "link in",
|
||||
"z": "ps_int_proc",
|
||||
"name": "setup:to-ps-inflow",
|
||||
"links": [],
|
||||
"x": 120,
|
||||
"y": 560,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_station"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lin_setup_mgcmode",
|
||||
"type": "link in",
|
||||
"z": "ps_int_proc",
|
||||
"name": "setup:to-mgc-mode",
|
||||
"links": [],
|
||||
"x": 120,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_mgc"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "meas_level",
|
||||
"type": "measurement",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Basin level sensor",
|
||||
"mode": "analog",
|
||||
"channels": "[]",
|
||||
"scaling": false,
|
||||
"i_min": 0,
|
||||
"i_max": 0,
|
||||
"i_offset": 0,
|
||||
"o_min": 0,
|
||||
"o_max": 1,
|
||||
"simulator": true,
|
||||
"smooth_method": "mean",
|
||||
"count": 5,
|
||||
"processOutputFormat": "process",
|
||||
"dbaseOutputFormat": "influxdb",
|
||||
"uuid": "example-level-001",
|
||||
"supplier": "vega",
|
||||
"category": "sensor",
|
||||
"assetType": "level",
|
||||
"model": "VEGAPULS-31",
|
||||
"unit": "m",
|
||||
"assetTagNumber": "LT-001",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": 0,
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"x": 600,
|
||||
"y": 700,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_dbg_level"
|
||||
],
|
||||
[],
|
||||
[
|
||||
"ps_int_station"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_int_inj_level",
|
||||
"type": "inject",
|
||||
"z": "ps_int_proc",
|
||||
"name": "sim level 1.6 m",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "1.6",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"topic": "measurement",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": false,
|
||||
"onceDelay": "",
|
||||
"x": 120,
|
||||
"y": 700,
|
||||
"wires": [
|
||||
[
|
||||
"meas_level"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pump_a",
|
||||
"type": "rotatingMachine",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump A",
|
||||
"speed": "1",
|
||||
"startup": "2",
|
||||
"warmup": "1",
|
||||
"shutdown": "2",
|
||||
"cooldown": "1",
|
||||
"movementMode": "staticspeed",
|
||||
"machineCurve": "",
|
||||
"uuid": "example-pump-a",
|
||||
"supplier": "hidrostal",
|
||||
"category": "pump",
|
||||
"assetType": "pump-centrifugal",
|
||||
"model": "hidrostal-H05K-S03R",
|
||||
"unit": "m3/h",
|
||||
"curvePressureUnit": "mbar",
|
||||
"curveFlowUnit": "m3/h",
|
||||
"curvePowerUnit": "kW",
|
||||
"curveControlUnit": "%",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": "",
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"x": 840,
|
||||
"y": 320,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_dbg_pa"
|
||||
],
|
||||
[],
|
||||
[
|
||||
"ps_int_mgc"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "pump_b",
|
||||
"type": "rotatingMachine",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump B",
|
||||
"speed": "1",
|
||||
"startup": "2",
|
||||
"warmup": "1",
|
||||
"shutdown": "2",
|
||||
"cooldown": "1",
|
||||
"movementMode": "staticspeed",
|
||||
"machineCurve": "",
|
||||
"uuid": "example-pump-b",
|
||||
"supplier": "hidrostal",
|
||||
"category": "pump",
|
||||
"assetType": "pump-centrifugal",
|
||||
"model": "hidrostal-H05K-S03R",
|
||||
"unit": "m3/h",
|
||||
"curvePressureUnit": "mbar",
|
||||
"curveFlowUnit": "m3/h",
|
||||
"curvePowerUnit": "kW",
|
||||
"curveControlUnit": "%",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": "",
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"x": 840,
|
||||
"y": 400,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_dbg_pb"
|
||||
],
|
||||
[],
|
||||
[
|
||||
"ps_int_mgc"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_int_mgc",
|
||||
"type": "machineGroupControl",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump Group",
|
||||
"enableLog": true,
|
||||
"logLevel": "info",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": "",
|
||||
"distanceUnit": "m",
|
||||
"x": 1080,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_dbg_mgc"
|
||||
],
|
||||
[],
|
||||
[
|
||||
"ps_int_station"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_int_station",
|
||||
"type": "pumpingStation",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pumping Station",
|
||||
"simulator": false,
|
||||
"basinVolume": 50,
|
||||
"basinHeight": 3.5,
|
||||
"inflowLevel": 3,
|
||||
"outflowLevel": 0.2,
|
||||
"overflowLevel": 3.2,
|
||||
"defaultFluid": "wastewater",
|
||||
"inletPipeDiameter": 0.3,
|
||||
"outletPipeDiameter": 0.3,
|
||||
"pipelineLength": 80,
|
||||
"maxDischargeHead": 24,
|
||||
"staticHead": 12,
|
||||
"maxInflowRate": 200,
|
||||
"temperatureReferenceDegC": 15,
|
||||
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
||||
"enableDryRunProtection": true,
|
||||
"enableOverfillProtection": true,
|
||||
"dryRunThresholdPercent": 2,
|
||||
"overfillThresholdPercent": 98,
|
||||
"minHeightBasedOn": "outlet",
|
||||
"processOutputFormat": "process",
|
||||
"dbaseOutputFormat": "influxdb",
|
||||
"refHeight": "NAP",
|
||||
"basinBottomRef": 1,
|
||||
"uuid": "example-ps-001",
|
||||
"supplier": "WBD-RD",
|
||||
"category": "station",
|
||||
"assetType": "pumpingstation",
|
||||
"model": "demo-50m3",
|
||||
"unit": "m3/h",
|
||||
"enableLog": true,
|
||||
"logLevel": "info",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": "",
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"controlMode": "levelbased",
|
||||
"startLevel": 1.2,
|
||||
"minLevel": 0.4,
|
||||
"maxLevel": 2.8,
|
||||
"flowSetpoint": null,
|
||||
"flowDeadband": null,
|
||||
"x": 1320,
|
||||
"y": 520,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_format"
|
||||
],
|
||||
[
|
||||
"ps_int_dbg_influx"
|
||||
],
|
||||
[]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_int_format",
|
||||
"type": "function",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Merge deltas + format",
|
||||
"func": "const p = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst cache = context.get('c') || {}; Object.assign(cache, p); context.set('c', cache);\nfunction pick(prefix){ for (const k of Object.keys(cache)) if (k===prefix||k.indexOf(prefix+'.')===0){ const v=Number(cache[k]); if(Number.isFinite(v)) return v;} return null; }\nconst vol=pick('volume.predicted.atequipment'), lvl=pick('level.predicted.atequipment'), flIn=pick('flow.predicted.in'), flOut=pick('flow.predicted.out');\nmsg.payload = {\n state: cache.state || 'unknown',\n controlMode: cache.controlMode || cache.mode || 'n/a',\n direction: cache.direction || 'n/a',\n percControl: cache.percControl != null ? Number(cache.percControl).toFixed(1)+' %' : 'n/a',\n volume: vol != null ? vol.toFixed(2)+' m3' : 'n/a',\n volumePercent: cache.volumePercent != null ? Number(cache.volumePercent).toFixed(1)+' %' : 'n/a',\n level: lvl != null ? lvl.toFixed(3)+' m' : 'n/a',\n inflow: flIn != null ? (flIn*3600).toFixed(1)+' m3/h' : 'n/a',\n outflow: flOut != null ? (flOut*3600).toFixed(1)+' m3/h' : 'n/a',\n childCount: cache.childCount != null ? cache.childCount : 'n/a'\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 1560,
|
||||
"y": 520,
|
||||
"wires": [
|
||||
[
|
||||
"ps_int_dbg_process"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_int_dbg_process",
|
||||
"type": "debug",
|
||||
"z": "ps_int_proc",
|
||||
"name": "PS Port 0: Process",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"x": 1800,
|
||||
"y": 480,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_int_dbg_influx",
|
||||
"type": "debug",
|
||||
"z": "ps_int_proc",
|
||||
"name": "PS Port 1: InfluxDB",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1800,
|
||||
"y": 540,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_int_dbg_mgc",
|
||||
"type": "debug",
|
||||
"z": "ps_int_proc",
|
||||
"name": "MGC Port 0",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"x": 1800,
|
||||
"y": 360,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_int_dbg_pa",
|
||||
"type": "debug",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump A Port 0",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"x": 1800,
|
||||
"y": 320,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_int_dbg_pb",
|
||||
"type": "debug",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump B Port 0",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"x": 1800,
|
||||
"y": 400,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_int_dbg_level",
|
||||
"type": "debug",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Level Port 0",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "payload",
|
||||
"targetType": "msg",
|
||||
"x": 1800,
|
||||
"y": 700,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "grp_pumpa",
|
||||
"type": "group",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump A (EM)",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#86bbdd",
|
||||
"fill-opacity": "0.10"
|
||||
},
|
||||
"nodes": [
|
||||
"pump_a",
|
||||
"ps_int_dbg_pa"
|
||||
],
|
||||
"x": 815,
|
||||
"y": 275,
|
||||
"w": 1210,
|
||||
"h": 110
|
||||
},
|
||||
{
|
||||
"id": "grp_pumpb",
|
||||
"type": "group",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump B (EM)",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#86bbdd",
|
||||
"fill-opacity": "0.10"
|
||||
},
|
||||
"nodes": [
|
||||
"pump_b",
|
||||
"ps_int_dbg_pb"
|
||||
],
|
||||
"x": 815,
|
||||
"y": 355,
|
||||
"w": 1210,
|
||||
"h": 110
|
||||
},
|
||||
{
|
||||
"id": "grp_mgc",
|
||||
"type": "group",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pump Group MGC (UN)",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#50a8d9",
|
||||
"fill-opacity": "0.10"
|
||||
},
|
||||
"nodes": [
|
||||
"ps_int_mgc",
|
||||
"ps_int_dbg_mgc",
|
||||
"lin_setup_mgcmode"
|
||||
],
|
||||
"x": 95,
|
||||
"y": 315,
|
||||
"w": 1930,
|
||||
"h": 110
|
||||
},
|
||||
{
|
||||
"id": "grp_station",
|
||||
"type": "group",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Pumping Station (PC)",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#0c99d9",
|
||||
"fill-opacity": "0.10"
|
||||
},
|
||||
"nodes": [
|
||||
"ps_int_station",
|
||||
"ps_int_format",
|
||||
"ps_int_dbg_process",
|
||||
"ps_int_dbg_influx",
|
||||
"lin_setup_mode",
|
||||
"lin_setup_inflow"
|
||||
],
|
||||
"x": 95,
|
||||
"y": 435,
|
||||
"w": 1930,
|
||||
"h": 190
|
||||
},
|
||||
{
|
||||
"id": "grp_level",
|
||||
"type": "group",
|
||||
"z": "ps_int_proc",
|
||||
"name": "Level Sensor (CM)",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#a9daee",
|
||||
"fill-opacity": "0.10"
|
||||
},
|
||||
"nodes": [
|
||||
"meas_level",
|
||||
"ps_int_inj_level",
|
||||
"ps_int_dbg_level"
|
||||
],
|
||||
"x": 95,
|
||||
"y": 655,
|
||||
"w": 1930,
|
||||
"h": 110
|
||||
},
|
||||
{
|
||||
"id": "setup_title",
|
||||
"type": "comment",
|
||||
"z": "ps_int_setup",
|
||||
"name": "Deploy-time setup\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\nFires once after each deploy: pushes the canonical set.mode / set.inflow /\nset.demand topics across cross-tab channels into the Process Plant tab.",
|
||||
"info": "",
|
||||
"x": 600,
|
||||
"y": 40,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "setup_inj_mode",
|
||||
"type": "inject",
|
||||
"z": "ps_int_setup",
|
||||
"name": "set.mode = levelbased",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "levelbased",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"topic": "set.mode",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "0.5",
|
||||
"x": 120,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"lout_setup_mode"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "setup_inj_mgcmode",
|
||||
"type": "inject",
|
||||
"z": "ps_int_setup",
|
||||
"name": "MGC set.mode = auto",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "auto",
|
||||
"vt": "str"
|
||||
}
|
||||
],
|
||||
"topic": "set.mode",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "0.5",
|
||||
"x": 120,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"lout_setup_mgcmode"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "setup_inj_inflow",
|
||||
"type": "inject",
|
||||
"z": "ps_int_setup",
|
||||
"name": "seed inflow 60 m3/h",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload",
|
||||
"v": "60",
|
||||
"vt": "num"
|
||||
}
|
||||
],
|
||||
"topic": "set.inflow",
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "1.0",
|
||||
"x": 120,
|
||||
"y": 280,
|
||||
"wires": [
|
||||
[
|
||||
"lout_setup_inflow"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "lout_setup_mode",
|
||||
"type": "link out",
|
||||
"z": "ps_int_setup",
|
||||
"name": "setup:to-ps-mode",
|
||||
"mode": "link",
|
||||
"links": [
|
||||
"lin_setup_mode"
|
||||
],
|
||||
"x": 1800,
|
||||
"y": 160,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "lout_setup_mgcmode",
|
||||
"type": "link out",
|
||||
"z": "ps_int_setup",
|
||||
"name": "setup:to-mgc-mode",
|
||||
"mode": "link",
|
||||
"links": [
|
||||
"lin_setup_mgcmode"
|
||||
],
|
||||
"x": 1800,
|
||||
"y": 220,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "lout_setup_inflow",
|
||||
"type": "link out",
|
||||
"z": "ps_int_setup",
|
||||
"name": "setup:to-ps-inflow",
|
||||
"mode": "link",
|
||||
"links": [
|
||||
"lin_setup_inflow"
|
||||
],
|
||||
"x": 1800,
|
||||
"y": 280,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "grp_setup",
|
||||
"type": "group",
|
||||
"z": "ps_int_setup",
|
||||
"name": "Deploy-time setup",
|
||||
"style": {
|
||||
"label": true,
|
||||
"stroke": "#000000",
|
||||
"fill": "#dddddd",
|
||||
"fill-opacity": "0.10"
|
||||
},
|
||||
"nodes": [
|
||||
"setup_inj_mode",
|
||||
"setup_inj_mgcmode",
|
||||
"setup_inj_inflow",
|
||||
"lout_setup_mode",
|
||||
"lout_setup_mgcmode",
|
||||
"lout_setup_inflow"
|
||||
],
|
||||
"x": 95,
|
||||
"y": 115,
|
||||
"w": 1930,
|
||||
"h": 230
|
||||
}
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
# pumpingStation - Example Flows
|
||||
|
||||
Three Node-RED flows demonstrating the Phase-2 pumpingStation node on the
|
||||
canonical topic API (`set.mode`, `set.inflow`, `set.demand`,
|
||||
Node-RED flows demonstrating the Phase-2 pumpingStation node on the
|
||||
canonical topic API (`set.mode`, `set.inflow`, `set.outflow`, `set.demand`,
|
||||
`cmd.calibrate.volume`, `cmd.calibrate.level`). Legacy aliases
|
||||
(`changemode`, `q_in`, `Qd`, `calibratePredictedVolume`,
|
||||
(`changemode`, `q_in`, `q_out`, `Qd`, `calibratePredictedVolume`,
|
||||
`calibratePredictedLevel`, `registerChild`) still work but log a
|
||||
one-time deprecation warning; these fresh flows use the canonical names only.
|
||||
|
||||
@@ -12,15 +12,14 @@ one-time deprecation warning; these fresh flows use the canonical names only.
|
||||
| File | Tier | Tabs | Purpose |
|
||||
|---|---|---|---|
|
||||
| `01-Basic.json` | 1 | Process Plant | Single pumpingStation driven by inject nodes - no parent, no dashboard. |
|
||||
| `02-Integration.json` | 2 | Process Plant + Setup | Adds a `measurement` level child and a `machineGroupControl` parent with two `rotatingMachine` pumps. Demonstrates the Phase-2 parent/child handshake. |
|
||||
| `03-Dashboard.json` | 3 | Process Plant + Dashboard UI + Setup | Tier 2 plumbing plus a FlowFuse Dashboard 2.0 page with 3 charts (flow / level / volume %), text widgets, and 2 controls (mode dropdown + demand slider). |
|
||||
| `02-Dashboard.json` | 2 | Process Plant + Dashboard UI | Same command surface as Basic, but driven by FlowFuse Dashboard 2.0 widgets — `ui-button` controls + `ui-text` live status panel. |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node-RED with the EVOLV package installed (so the `pumpingStation`,
|
||||
`measurement`, `machineGroupControl`, and `rotatingMachine` node
|
||||
types are registered).
|
||||
- For `03-Dashboard.json`: `@flowfuse/node-red-dashboard` (Dashboard 2.0).
|
||||
- For `02-Dashboard.json`: `@flowfuse/node-red-dashboard` (Dashboard 2.0).
|
||||
|
||||
## How to load
|
||||
|
||||
@@ -46,28 +45,22 @@ import into their own tabs and can be deployed immediately.
|
||||
5. Inject `cmd.calibrate.volume = 25 m3` to jump the predicted-volume
|
||||
integrator to half-full.
|
||||
|
||||
## 02-Integration - what to try
|
||||
|
||||
1. Deploy. The Setup tab fires `set.mode = levelbased` to the station
|
||||
and `set.mode = auto` to the MGC.
|
||||
2. The two pumps register with the MGC via Port 2; the MGC and the level
|
||||
sensor register with the station via Port 2. Watch the registration
|
||||
debug taps to confirm.
|
||||
3. The level inject pushes a 1.6 m measurement so the station sees a
|
||||
non-zero starting level. Setup also seeds `set.inflow = 60 m3/h`.
|
||||
4. The station's `controlMode = levelbased` then drives the MGC, which
|
||||
dispatches to Pump A / Pump B.
|
||||
|
||||
## 03-Dashboard - what to try
|
||||
## 02-Dashboard - what to try
|
||||
|
||||
1. Deploy.
|
||||
2. Open the dashboard at `http://localhost:1880/dashboard/page/pumping-station`.
|
||||
3. Use the **Control mode** dropdown to switch between `manual`,
|
||||
`levelbased`, `flowbased`, `none`.
|
||||
4. In manual mode, drag the **Manual demand** slider - the demand cascades
|
||||
to the MGC and on to the pumps.
|
||||
5. The three charts (flow, level, volume %) plot live data; the four text
|
||||
widgets show state, percControl, direction, and time-to-empty.
|
||||
2. Open the dashboard at `http://localhost:1880/dashboard/pumpingstation-basic`.
|
||||
3. Click **Mode: Manual** or **Mode: Levelbased** in the Controls panel.
|
||||
4. Click **Inflow 60 m³/h** to push wastewater into the basin — the Status
|
||||
panel on the right shows level / volume / volume % rising.
|
||||
5. In manual mode, click **Demand 40 m³/h** — the value surfaces as
|
||||
`Manual demand` in the Status panel and in the node's status badge.
|
||||
6. Use **Calibrate V = 25 m³** or **Calibrate L = 1.5 m** to snap the
|
||||
predicted-volume integrator.
|
||||
|
||||
All buttons fire the same canonical `msg.topic` as the Basic flow's inject
|
||||
nodes; the only difference is the trigger. The Live status panel is fed by
|
||||
Port 0 via a small fan-out function that caches last-known values so
|
||||
delta-only updates never blank a row.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
@@ -88,12 +81,6 @@ These flows follow the EVOLV layout rule set in
|
||||
|
||||
## Regenerating
|
||||
|
||||
These flows are generated from `tools/build-examples.js`. Edit the
|
||||
generator, never the JSON, then:
|
||||
|
||||
```bash
|
||||
node nodes/pumpingStation/tools/build-examples.js
|
||||
```
|
||||
|
||||
The script writes `01-Basic.json`, `02-Integration.json`, and
|
||||
`03-Dashboard.json` into this directory.
|
||||
The current example JSON files are hand-maintained. If you re-introduce a
|
||||
generator, regenerate `01-Basic.json` and `02-Dashboard.json` from it
|
||||
rather than editing the JSON directly.
|
||||
|
||||
@@ -1,589 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": "ps_tab_basic_dashboard",
|
||||
"type": "tab",
|
||||
"label": "PumpingStation Dashboard",
|
||||
"disabled": false,
|
||||
"info": "Basic level-based pumpingStation dashboard with basin trends and safety state."
|
||||
},
|
||||
{
|
||||
"id": "ui_base_ps_basic",
|
||||
"type": "ui-base",
|
||||
"name": "EVOLV Demo",
|
||||
"path": "/dashboard",
|
||||
"appIcon": "",
|
||||
"includeClientData": true,
|
||||
"acceptsClientConfig": [
|
||||
"ui-notification",
|
||||
"ui-control"
|
||||
],
|
||||
"showPathInSidebar": false,
|
||||
"headerContent": "page",
|
||||
"navigationStyle": "default",
|
||||
"titleBarStyle": "default"
|
||||
},
|
||||
{
|
||||
"id": "ui_theme_ps_basic",
|
||||
"type": "ui-theme",
|
||||
"name": "EVOLV Pumping Theme",
|
||||
"colors": {
|
||||
"surface": "#ffffff",
|
||||
"primary": "#0c99d9",
|
||||
"bgPage": "#f1f3f5",
|
||||
"groupBg": "#ffffff",
|
||||
"groupOutline": "#cfd7de"
|
||||
},
|
||||
"sizes": {
|
||||
"density": "default",
|
||||
"pagePadding": "14px",
|
||||
"groupGap": "14px",
|
||||
"groupBorderRadius": "6px",
|
||||
"widgetGap": "12px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui_page_ps_basic",
|
||||
"type": "ui-page",
|
||||
"name": "PumpingStation",
|
||||
"ui": "ui_base_ps_basic",
|
||||
"path": "/pumping-station",
|
||||
"icon": "water_drop",
|
||||
"layout": "grid",
|
||||
"theme": "ui_theme_ps_basic",
|
||||
"breakpoints": [
|
||||
{
|
||||
"name": "Default",
|
||||
"px": "0",
|
||||
"cols": "12"
|
||||
}
|
||||
],
|
||||
"order": 1,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_ps_inputs",
|
||||
"type": "ui-group",
|
||||
"name": "Simulation Inputs",
|
||||
"page": "ui_page_ps_basic",
|
||||
"width": "4",
|
||||
"height": "1",
|
||||
"order": 1,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_ps_trends",
|
||||
"type": "ui-group",
|
||||
"name": "Basin Trends",
|
||||
"page": "ui_page_ps_basic",
|
||||
"width": "8",
|
||||
"height": "1",
|
||||
"order": 2,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_ps_state",
|
||||
"type": "ui-group",
|
||||
"name": "State",
|
||||
"page": "ui_page_ps_basic",
|
||||
"width": "12",
|
||||
"height": "1",
|
||||
"order": 3,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ps_node_basic",
|
||||
"type": "pumpingStation",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "PS Dashboard Demo",
|
||||
"basinVolume": 50,
|
||||
"basinHeight": 5,
|
||||
"inflowLevel": 3,
|
||||
"outflowLevel": 0.2,
|
||||
"overflowLevel": 4.5,
|
||||
"defaultFluid": "wastewater",
|
||||
"inletPipeDiameter": 0.4,
|
||||
"outletPipeDiameter": 0.3,
|
||||
"pipelineLength": 80,
|
||||
"maxDischargeHead": 24,
|
||||
"staticHead": 12,
|
||||
"maxInflowRate": 200,
|
||||
"temperatureReferenceDegC": 15,
|
||||
"timeleftToFullOrEmptyThresholdSeconds": 0,
|
||||
"enableDryRunProtection": true,
|
||||
"enableHighVolumeSafety": true,
|
||||
"enableOverfillProtection": true,
|
||||
"dryRunThresholdPercent": 2,
|
||||
"highVolumeSafetyThresholdPercent": 98,
|
||||
"overfillThresholdPercent": 98,
|
||||
"minHeightBasedOn": "outlet",
|
||||
"processOutputFormat": "process",
|
||||
"dbaseOutputFormat": "influxdb",
|
||||
"refHeight": "NAP",
|
||||
"basinBottomRef": 0,
|
||||
"unit": "m3/h",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": 0,
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"controlMode": "levelbased",
|
||||
"levelCurveType": "linear",
|
||||
"logCurveFactor": 9,
|
||||
"minLevel": 1,
|
||||
"startLevel": 2,
|
||||
"maxLevel": 4,
|
||||
"x": 720,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"ps_parse_output"
|
||||
],
|
||||
[
|
||||
"ps_debug_influx"
|
||||
],
|
||||
[
|
||||
"ps_debug_parent"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_calibrate_initial",
|
||||
"type": "inject",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Set start level 2 m",
|
||||
"props": [
|
||||
{
|
||||
"p": "topic",
|
||||
"vt": "str"
|
||||
},
|
||||
{
|
||||
"p": "payload"
|
||||
}
|
||||
],
|
||||
"repeat": "",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "0.5",
|
||||
"topic": "calibratePredictedLevel",
|
||||
"payload": "2",
|
||||
"payloadType": "num",
|
||||
"x": 180,
|
||||
"y": 180,
|
||||
"wires": [
|
||||
[
|
||||
"ps_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_auto_inflow",
|
||||
"type": "inject",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Auto inflow 0.008 m3/s",
|
||||
"props": [
|
||||
{
|
||||
"p": "payload"
|
||||
}
|
||||
],
|
||||
"repeat": "1",
|
||||
"crontab": "",
|
||||
"once": true,
|
||||
"onceDelay": "1",
|
||||
"topic": "",
|
||||
"payload": "0.008",
|
||||
"payloadType": "num",
|
||||
"x": 180,
|
||||
"y": 240,
|
||||
"wires": [
|
||||
[
|
||||
"ps_build_qin"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_inflow_input",
|
||||
"type": "ui-number-input",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_inputs",
|
||||
"name": "Inflow",
|
||||
"label": "Inflow (m3/s)",
|
||||
"order": 1,
|
||||
"width": "4",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"topic": "",
|
||||
"min": 0,
|
||||
"max": 0.05,
|
||||
"step": 0.001,
|
||||
"x": 190,
|
||||
"y": 300,
|
||||
"wires": [
|
||||
[
|
||||
"ps_build_qin"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_build_qin",
|
||||
"type": "function",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Build q_in",
|
||||
"func": "msg.topic = 'q_in';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 440,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"ps_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_outflow_input",
|
||||
"type": "ui-number-input",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_inputs",
|
||||
"name": "Outflow",
|
||||
"label": "Outflow (m3/s)",
|
||||
"order": 2,
|
||||
"width": "4",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"topic": "",
|
||||
"min": 0,
|
||||
"max": 0.05,
|
||||
"step": 0.001,
|
||||
"x": 190,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"ps_build_qout"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_build_qout",
|
||||
"type": "function",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Build q_out",
|
||||
"func": "msg.topic = 'q_out';\nmsg.unit = 'm3/s';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 440,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"ps_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_parse_output",
|
||||
"type": "function",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Parse PS output",
|
||||
"func": "// MeasurementContainer flat keys are `${type}.${variant}.${position}.${childId}`.\n// When PS writes without an explicit .child(), the childId is the literal\n// string 'default' — DON'T strip it. See generalFunctions/src/measurements/\n// MeasurementContainer.js getFlattenedOutput for details.\nconst fields = (msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst snapshot = Object.assign({}, context.get('snapshot') || {}, fields);\ncontext.set('snapshot', snapshot);\nconst firstFinite = (...keys) => {\n for (const key of keys) {\n const value = Number(snapshot[key]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\nconst level = firstFinite('level.predicted.atequipment.default', 'level.measured.atequipment.default');\nconst volume = firstFinite('volume.predicted.atequipment.default', 'volume.measured.atequipment.default');\nconst netFlow = firstFinite('netFlowRate.predicted.atequipment.default', 'netFlowRate.measured.atequipment.default');\nconst demand = firstFinite('percControl');\nconst safety = snapshot.safetyState || 'normal';\nconst direction = snapshot.direction || 'unknown';\nconst overflow = snapshot.isOverflowing === true || snapshot.isOverflowing === 'true';\nconst timeleft = Number(snapshot.timeleft);\nconst fmt = (value, digits = 2) => Number.isFinite(value) ? value.toFixed(digits) : '-';\nreturn [\n level == null ? null : { topic: 'level', payload: level },\n volume == null ? null : { topic: 'volume', payload: volume },\n demand == null ? null : { topic: 'demand', payload: demand },\n netFlow == null ? null : { topic: 'net_flow', payload: netFlow },\n { topic: 'safety', payload: `${safety} | overflowing=${overflow}` },\n { topic: 'snapshot', payload: `level=${fmt(level)} m | volume=${fmt(volume)} m3 | demand=${fmt(demand, 0)}% | direction=${direction} | t=${Number.isFinite(timeleft) ? Math.round(timeleft) + ' s' : '-'}` }\n];",
|
||||
"outputs": 6,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 980,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"ps_chart_level"
|
||||
],
|
||||
[
|
||||
"ps_chart_volume"
|
||||
],
|
||||
[
|
||||
"ps_chart_demand"
|
||||
],
|
||||
[
|
||||
"ps_chart_netflow"
|
||||
],
|
||||
[
|
||||
"ps_text_safety"
|
||||
],
|
||||
[
|
||||
"ps_text_snapshot"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_level",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Level",
|
||||
"label": "Level (m)",
|
||||
"order": 1,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1230,
|
||||
"y": 140,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "0",
|
||||
"ymax": "5",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#0c99d9"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_volume",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Volume",
|
||||
"label": "Volume (m3)",
|
||||
"order": 2,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m3",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1230,
|
||||
"y": 200,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "0",
|
||||
"ymax": "50",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#2ca02c"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_demand",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Demand",
|
||||
"label": "Demand (%)",
|
||||
"order": 3,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "%",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1230,
|
||||
"y": 260,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "0",
|
||||
"ymax": "120",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#d68910"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_chart_netflow",
|
||||
"type": "ui-chart",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_trends",
|
||||
"name": "Net Flow",
|
||||
"label": "Net flow (m3/s)",
|
||||
"order": 4,
|
||||
"width": 4,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "topic",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m3/s",
|
||||
"removeOlder": "15",
|
||||
"removeOlderUnit": "60",
|
||||
"x": 1240,
|
||||
"y": 320,
|
||||
"wires": [],
|
||||
"showLegend": false,
|
||||
"categoryType": "msg",
|
||||
"xAxisProperty": "",
|
||||
"xAxisPropertyType": "timestamp",
|
||||
"xAxisFormat": "",
|
||||
"xAxisFormatType": "auto",
|
||||
"yAxisProperty": "payload",
|
||||
"yAxisPropertyType": "msg",
|
||||
"xmin": "",
|
||||
"xmax": "",
|
||||
"ymin": "",
|
||||
"ymax": "",
|
||||
"bins": 10,
|
||||
"action": "append",
|
||||
"stackSeries": false,
|
||||
"pointShape": "circle",
|
||||
"pointRadius": 4,
|
||||
"interpolation": "linear",
|
||||
"className": "",
|
||||
"colors": [
|
||||
"#9467bd"
|
||||
],
|
||||
"textColor": [
|
||||
"#666666"
|
||||
],
|
||||
"textColorDefault": true,
|
||||
"gridColor": [
|
||||
"#e5e5e5"
|
||||
],
|
||||
"gridColorDefault": true
|
||||
},
|
||||
{
|
||||
"id": "ps_text_safety",
|
||||
"type": "ui-text",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_state",
|
||||
"name": "Safety",
|
||||
"label": "Safety",
|
||||
"order": 1,
|
||||
"width": 4,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1230,
|
||||
"y": 380,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_text_snapshot",
|
||||
"type": "ui-text",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"group": "ui_group_ps_state",
|
||||
"name": "Snapshot",
|
||||
"label": "Snapshot",
|
||||
"order": 2,
|
||||
"width": 8,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1240,
|
||||
"y": 440,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_debug_influx",
|
||||
"type": "debug",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Influx output",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 980,
|
||||
"y": 320,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "ps_debug_parent",
|
||||
"type": "debug",
|
||||
"z": "ps_tab_basic_dashboard",
|
||||
"name": "Parent output",
|
||||
"active": false,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 980,
|
||||
"y": 380,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* Standalone PumpingStation demo — run with `node examples/standalone-demo.js`.
|
||||
* Builds a station + one pump, calibrates predicted volume, ticks once.
|
||||
* Useful for sanity-checking the orchestrator without Node-RED.
|
||||
*/
|
||||
const PumpingStation = require('../src/specificClass');
|
||||
const RotatingMachine = require('../../rotatingMachine/src/specificClass');
|
||||
|
||||
function createPumpingStationConfig(name) {
|
||||
return {
|
||||
general: {
|
||||
logging: { enabled: true, logLevel: 'debug' },
|
||||
name,
|
||||
id: `${name}-${Date.now()}`,
|
||||
flowThreshold: 1e-4,
|
||||
},
|
||||
functionality: { softwareType: 'pumpingStation', role: 'stationcontroller' },
|
||||
basin: { volume: 43.75, height: 10, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 3.2 },
|
||||
hydraulics: { refHeight: 'NAP', basinBottomRef: 0 },
|
||||
safety: { enableDryRunProtection: false, enableOverfillProtection: false },
|
||||
};
|
||||
}
|
||||
|
||||
function createMachineConfig(name, position) {
|
||||
return {
|
||||
general: { name, logging: { enabled: false, logLevel: 'debug' } },
|
||||
functionality: { softwareType: 'machine', positionVsParent: position },
|
||||
asset: { supplier: 'Hydrostal', type: 'pump', category: 'centrifugal', model: 'hidrostal-H05K-S03R' },
|
||||
};
|
||||
}
|
||||
|
||||
function createMachineStateConfig() {
|
||||
return {
|
||||
general: { logging: { enabled: true, logLevel: 'debug' } },
|
||||
movement: { speed: 1 },
|
||||
time: { starting: 2, warmingup: 3, stopping: 2, coolingdown: 3 },
|
||||
};
|
||||
}
|
||||
|
||||
(async function demo() {
|
||||
const station = new PumpingStation(createPumpingStationConfig('PumpingStationDemo'));
|
||||
const pump1 = new RotatingMachine(createMachineConfig('Pump1', 'downstream'), createMachineStateConfig());
|
||||
|
||||
station.childRegistrationUtils.registerChild(pump1, 'machine');
|
||||
|
||||
setInterval(() => station.tick(), 1000);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
console.log('Initial state:', station.state);
|
||||
station.setManualInflow(300, Date.now(), 'l/s');
|
||||
station.calibratePredictedVolume(3.4);
|
||||
|
||||
console.log('Station state:', station.state);
|
||||
console.log('Station output:', station.getOutput());
|
||||
})().catch((err) => {
|
||||
console.error('Demo failed:', err);
|
||||
});
|
||||
@@ -29,11 +29,11 @@
|
||||
|
||||
// Define station-specific properties
|
||||
simulator: { value: false },
|
||||
basinVolume: { value: 1 }, // m³, total empty basin
|
||||
basinHeight: { value: 1 }, // m, floor to top
|
||||
inflowLevel: { value: 0.8 }, // m, bottom/invert of inlet pipe above floor
|
||||
basinVolume: { value: 50 }, // m³, total empty basin
|
||||
basinHeight: { value: 4 }, // m, floor to top
|
||||
inflowLevel: { value: 1.5 }, // m, bottom/invert of inlet pipe above floor
|
||||
outflowLevel: { value: 0.2 }, // m, top of outlet/suction pipe above floor
|
||||
overflowLevel: { value: 0.9 }, // m, overflow elevation
|
||||
overflowLevel: { value: 3.8 }, // m, overflow elevation
|
||||
defaultFluid: { value: "wastewater" },
|
||||
inletPipeDiameter: { value: 0.3 }, // m
|
||||
outletPipeDiameter: { value: 0.3 }, // m
|
||||
@@ -84,10 +84,10 @@
|
||||
enableShiftedRamp: { value: false },
|
||||
shiftLevel: { value: 0 },
|
||||
shiftArmPercent: { value: 95 },
|
||||
startLevel: { value: null },
|
||||
stopLevel: { value: null },
|
||||
minLevel: { value: null },
|
||||
maxLevel: { value: null },
|
||||
startLevel: { value: 1 }, // m, pump-on threshold (engagement edge)
|
||||
stopLevel: { value: 0.5 }, // m, pump-off threshold (hysteresis fall-back)
|
||||
minLevel: { value: 0.3 }, // m, hard-stop (just above outflow pipe top)
|
||||
maxLevel: { value: 3.8 }, // m, 100% demand saturation
|
||||
flowSetpoint: { value: null },
|
||||
flowDeadband: { value: null }
|
||||
|
||||
|
||||
@@ -90,14 +90,20 @@
|
||||
return pts.join(' ');
|
||||
};
|
||||
|
||||
// Up curve. Foot is startLevel (the configured pump-on threshold and
|
||||
// ramp foot per the runtime in _controlLevelBased). The OFF baseline
|
||||
// is drawn for level < startLevel; at startLevel demand jumps from
|
||||
// OFF to 0 % and ramps up to 100 % at maxLevel.
|
||||
// Up curve. Engagement edge is startLevel (pump-on threshold); the
|
||||
// ramp foot is inflowLevel — matching the runtime in
|
||||
// _controlLevelBased, which scales demand over [inflowLevel, maxLevel].
|
||||
// The OFF baseline is drawn for level < startLevel; between startLevel
|
||||
// and inflowLevel demand sits flat at 0 % (system armed but not yet
|
||||
// ramping); from inflowLevel demand ramps to 100 % at maxLevel.
|
||||
const up = document.getElementById('ps-mode-curve-up');
|
||||
const down = document.getElementById('ps-mode-curve-down');
|
||||
const downLabel = document.getElementById('ps-mode-curve-down-label');
|
||||
if (up) up.setAttribute('points', buildPath(start, start, max));
|
||||
// Runtime falls back to startLevel when inflowLevel is missing
|
||||
// (basin?.inflowLevel ?? cfg.inflowLevel ?? startLevel); mirror that
|
||||
// in the preview so the curve is still drawn instead of blank.
|
||||
const upFoot = Number.isFinite(inlet) && inlet > start ? inlet : start;
|
||||
if (up) up.setAttribute('points', buildPath(start, upFoot, max));
|
||||
|
||||
// Shifted-DOWN curve (only when shift enabled): represents the
|
||||
// worst-case held-then-ramp path drawn for hold=100 % (the SVG
|
||||
|
||||
@@ -44,6 +44,12 @@ class PumpingStation extends BaseDomain {
|
||||
this.controlState = { percControl: 0 };
|
||||
this.state = { direction: 'steady', netFlow: 0, flowSource: null, seconds: null, remainingSource: null };
|
||||
|
||||
// Last operator demand from set.demand in manual mode. Stored on the
|
||||
// host so getOutput()/status reflect it even when no children are
|
||||
// registered yet (otherwise forwardDemand is invisible on Port 0/1).
|
||||
// Cleared on mode change away from manual.
|
||||
this._manualDemand = null;
|
||||
|
||||
// Level-armed hysteresis state — ported from basin-docs `_controlLevelBased`.
|
||||
// Exposed as instance fields because the e2e/basic tests assert on them
|
||||
// directly. levelBased strategy reads/writes via the same names.
|
||||
@@ -172,6 +178,8 @@ class PumpingStation extends BaseDomain {
|
||||
if (this.config.control.allowedModes?.has?.(newMode)) {
|
||||
this.logger.info(`Control mode changing from ${this.mode} to ${newMode}`);
|
||||
this.mode = newMode;
|
||||
if (newMode !== 'manual') this._manualDemand = null;
|
||||
this.notifyOutputChanged();
|
||||
} else {
|
||||
this.logger.warn(`Attempted to change to unsupported control mode: ${newMode}`);
|
||||
}
|
||||
@@ -183,7 +191,11 @@ class PumpingStation extends BaseDomain {
|
||||
setManualInflow(value, ts = Date.now(), unit) { calibration.setManualInflow(this, value, ts, unit); }
|
||||
setManualOutflow(value, ts = Date.now(), unit) { calibration.setManualOutflow(this, value, ts, unit); }
|
||||
|
||||
forwardDemandToChildren(demand) { return control.manual.forwardDemand(this.context(), demand); }
|
||||
forwardDemandToChildren(demand) {
|
||||
this._manualDemand = Number.isFinite(demand) ? demand : null;
|
||||
this.notifyOutputChanged();
|
||||
return control.manual.forwardDemand(this.context(), demand);
|
||||
}
|
||||
|
||||
// Direct delegations preserved so existing tests can drive the strategy
|
||||
// without re-mocking the dispatch layer.
|
||||
@@ -220,6 +232,8 @@ class PumpingStation extends BaseDomain {
|
||||
out.flowSource = this.state.flowSource;
|
||||
out.timeleft = this.state.seconds;
|
||||
out.percControl = this.controlState.percControl;
|
||||
out.mode = this.mode;
|
||||
out.manualDemand = this._manualDemand;
|
||||
|
||||
// Derived safety thresholds — exposed so editor + dashboards can show
|
||||
// the dryRunLevel and highVolumeSafetyLevel without recomputing.
|
||||
@@ -247,15 +261,14 @@ class PumpingStation extends BaseDomain {
|
||||
steady: { arrow: '⏸️', fill: 'green' },
|
||||
};
|
||||
const { arrow = '❔', fill = 'grey' } = STYLES[this.state?.direction] || {};
|
||||
const vol = this.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3') ?? 0;
|
||||
const pct = this.measurements.type('volumePercent').variant('predicted').position('atequipment').getCurrentValue() ?? 0;
|
||||
const maxVol = this.basin?.maxVolAtOverflow ?? 0;
|
||||
const netFlowM3h = (this.state?.netFlow ?? 0) * 3600;
|
||||
const seconds = this.state?.seconds;
|
||||
const tStr = seconds != null ? `t≈${Math.round(seconds / 60)} min` : null;
|
||||
const mode = this.mode || '?';
|
||||
const manualPart = this.mode === 'manual' && Number.isFinite(this._manualDemand)
|
||||
? `Qd=${this._manualDemand.toFixed(0)} m³/h` : null;
|
||||
|
||||
return statusBadge.compose(
|
||||
[`${arrow} ${pct.toFixed(1)}%`, `V=${vol.toFixed(2)} / ${maxVol.toFixed(2)} m³`, `net: ${netFlowM3h.toFixed(0)} m³/h`, tStr],
|
||||
[mode, `${arrow} ${pct.toFixed(1)}%`, `net: ${netFlowM3h.toFixed(0)} m³/h`, manualPart],
|
||||
{ fill, shape: 'dot' }
|
||||
);
|
||||
}
|
||||
|
||||
60
wiki/Home.md
60
wiki/Home.md
@@ -20,17 +20,7 @@ A `pumpingStation` models a wet-well lift station: one basin with sensors, and o
|
||||
|
||||
## How it looks in Node-RED
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** Drop a `pumpingStation` node onto a fresh Node-RED canvas and capture:
|
||||
> - The node tile itself (its colour, badge text, label).
|
||||
> - The full edit dialog when you double-click it (basin geometry section visible).
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png` (PNG, target 1200×800, optimise to ≤ 200 KB).
|
||||
> Then replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -62,15 +52,7 @@ curl -X POST -H 'Content-Type: application/json' \
|
||||
http://localhost:1880/flow
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Flow screenshot needed.** Open the imported `01-Basic.json` flow in the Node-RED editor and capture the whole tab. The inject row should be visible on the left, the pumpingStation in the middle, the debug taps on the right.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/02-basic-flow.png` (PNG, target 1600×900, optimise to ≤ 250 KB).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||

|
||||
|
||||
What to click in the dashboard after deploy:
|
||||
|
||||
@@ -79,21 +61,7 @@ What to click in the dashboard after deploy:
|
||||
3. `cmd.calibrate.level = 1.5 m` → the volume integrator syncs to a known level.
|
||||
4. Watch Port 0 in the debug pane: level rises, predicted volume integrates, demand follows the curve.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **GIF needed.** Record the dashboard reacting to the four clicks above. 15–25 seconds is enough. Use `peek` (Linux), LICEcap (Win/Mac), or any screen recorder; convert to GIF and optimise:
|
||||
>
|
||||
> ```bash
|
||||
> # if you started from an mp4:
|
||||
> ffmpeg -i raw.mp4 -vf "fps=15,scale=720:-1" -loop 0 stage.gif
|
||||
> gifsicle -O3 --lossy=80 stage.gif -o final.gif
|
||||
> ```
|
||||
>
|
||||
> Save as `wiki/_partial-gifs/pumpingStation/01-basic-demo.gif` (target ≤ 1 MB).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -103,27 +71,11 @@ The two patterns you'll see most.
|
||||
|
||||
### Standalone (`01-Basic.json`)
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** From the imported `01-Basic.json`, crop a tight view of just the inject column → pumpingStation → debug nodes. Skip the comment header.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/03-wiring-standalone.png` (PNG, target 1400×700).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||

|
||||
|
||||
### With a measurement child and an MGC parent (`02-Integration.json`)
|
||||
### With a measurement child and an MGC parent
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** From the imported `02-Integration.json`, capture the whole tab. The measurement node feeding the pumpingStation should be visible on the left; the MGC with its two `rotatingMachine` pumps on the right.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/04-wiring-integrated.png` (PNG, target 1600×900).
|
||||
> Replace this callout with:
|
||||
>
|
||||
> ```markdown
|
||||
> 
|
||||
> ```
|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
|
||||
## Shipped examples
|
||||
|
||||
| File | Tier | Tabs | What it shows |
|
||||
|:---|:---:|:---|:---|
|
||||
| `examples/01-Basic.json` | 1 | Process Plant | Single pumpingStation driven by inject nodes — no parent, no dashboard. |
|
||||
| `examples/02-Integration.json` | 2 | Process Plant + Setup | Adds a `measurement` level child and a `machineGroupControl` parent with two `rotatingMachine` pumps. Demonstrates the Phase-2 parent / child handshake. |
|
||||
| `examples/03-Dashboard.json` | 3 | Process Plant + Dashboard + Setup | Tier-2 plumbing plus a FlowFuse Dashboard 2.0 page with 3 charts (flow / level / volume %), text widgets, and 2 controls (mode dropdown + demand slider). |
|
||||
| `examples/basic-dashboard.flow.json` | legacy | mixed | Pre-refactor flow kept for reference. Use `03-Dashboard.json` instead. |
|
||||
| File | Tier | What it shows |
|
||||
|:---|:---:|:---|
|
||||
| `examples/01-Basic.json` | 1 | Single pumpingStation driven by inject nodes — no parent, no dashboard. Numbered driver groups for Mode / Flow signals / Operator demand / Calibration. |
|
||||
| `examples/02-Dashboard.json` | 2 | Same command surface as Basic, driven by a FlowFuse Dashboard 2.0 page (Controls + live Status rows + 4 trend charts + raw-output table). |
|
||||
|
||||
---
|
||||
|
||||
@@ -39,93 +37,67 @@ curl -X POST -H 'Content-Type: application/json' \
|
||||
|
||||
## Example 01 — Basic standalone
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** After importing `01-Basic.json`, capture the full Process Plant tab.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/05-ex01-basic.png`.
|
||||
> Replace this callout with the image link.
|
||||

|
||||
|
||||
### Nodes on the tab
|
||||
|
||||
| Type | Purpose |
|
||||
|:---|:---|
|
||||
| `comment` | Tab header / instructions |
|
||||
| `inject` × 6 | Buttons to send `set.mode`, `set.inflow`, `set.demand`, `cmd.calibrate.volume`, `cmd.calibrate.level` |
|
||||
| `inject` × 7 | Buttons to send `set.mode` (manual / levelbased), `set.inflow`, `set.outflow`, `set.demand`, `cmd.calibrate.volume`, `cmd.calibrate.level` |
|
||||
| `pumpingStation` | The unit under test |
|
||||
| `function` | Merge Port-0 deltas into a single rolling snapshot |
|
||||
| `debug` × 3 | Port 0 (process), Port 1 (InfluxDB), Port 2 (parent reg) |
|
||||
|
||||
Driver injects are wrapped in four numbered groups: **1. Control mode**, **2. Flow signals (inflow / outflow)**, **3. Operator demand (manual mode only)**, **4. Calibration**. Debug nodes sit in a separate **Debug outputs (sidebar)** group on the right.
|
||||
|
||||
### What to do after deploy
|
||||
|
||||
1. Click `set.mode = levelbased`.
|
||||
2. Click `cmd.calibrate.level = 1.5 m` to anchor the volume integrator.
|
||||
3. Click `set.inflow = 60 m³/h`.
|
||||
4. Watch the Port-0 debug pane: `direction` flips to `filling`, `level` rises, `demand` follows the level curve, `etaSeconds` decreases.
|
||||
5. Click `set.demand = 40 %` (only honoured in manual mode — for level-based, the controller decides demand from level).
|
||||
1. (optional) Click `set.mode = manual` if you want `set.demand` to forward; otherwise leave it on the default `levelbased` and the ramp drives demand from level.
|
||||
2. Click `set.inflow = 60 m³/h` — the basin starts filling. Watch Port 0 in the debug pane: `direction` flips to `filling`, `level` rises, predicted volume integrates.
|
||||
3. In manual mode: click `set.demand = 40` — the value surfaces as `manualDemand` on Port 0/1 and in the node status badge.
|
||||
4. Click `cmd.calibrate.volume = 25 m³` (or `cmd.calibrate.level = 1.5 m`) to snap the predicted-volume integrator.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **GIF needed.** Record steps 1–4. Target 15–25 s, ≤ 1 MB after `gifsicle -O3 --lossy=80`.
|
||||
>
|
||||
> Save as `wiki/_partial-gifs/pumpingStation/02-ex01-demo.gif`.
|
||||
> Replace this callout with the image link.
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Example 02 — Integration with parent + children
|
||||
## Example 02 — Dashboard
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** After importing `02-Integration.json`, capture the full Process Plant tab.
|
||||
> **Screenshot needed.** Two captures from `02-Dashboard.json`:
|
||||
> 1. The editor tab (left controls column + pumpingStation + Live-status group on the right).
|
||||
> 2. The rendered dashboard at `http://localhost:1880/dashboard/pumpingstation-basic`.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/06-ex02-integration.png`.
|
||||
> Replace this callout with the image link.
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/05-ex02-editor.png` and `06-ex02-dashboard.png`.
|
||||
> Replace this callout with both image links.
|
||||
|
||||
### What it adds vs Example 01
|
||||
|
||||
| Addition | Why |
|
||||
|:---|:---|
|
||||
| `measurement` node feeding `level` | Replaces the inject-driven level path with a real measurement child |
|
||||
| `machineGroupControl` (MGC) parent | Demand goes upward to the MGC instead of being applied directly |
|
||||
| Two `rotatingMachine` pumps under the MGC | The MGC load-shares demand across them |
|
||||
| `Setup` tab | Initial calibration injects fire once via `once: true` |
|
||||
| FlowFuse `ui-base` + `ui-theme` + `ui-page` setup | One dashboard page hosting four widget groups |
|
||||
| `ui-button` × 7 (Controls group) | Replace the inject buttons one-for-one — each carries the canonical `msg.topic` directly |
|
||||
| `ui-text` × 7 (Status group) | Live readouts: Mode, Direction, Level, Volume, Volume %, percControl, Manual demand |
|
||||
| `ui-chart` × 4 (Trends group) | Level (m), Volume (m³), Volume % (0–100), Flow (m³/h, multi-series Inflow / Outflow / Net) |
|
||||
| `ui-template` (Raw output group) | Full key/value table of the latest Port 0 cache — every field the node emits, sorted |
|
||||
| Fan-out function | Caches last-known values so delta-only Port 0 updates never blank a status row, and forwards numeric values to the charts |
|
||||
|
||||
This exercises the Phase-2 parent / child handshake: `child.register` is sent on Port 2 of each child to its parent, and the parent's `commandRegistry` dispatches into `ChildRouter.onRegister(...)`.
|
||||
|
||||
### What to do after deploy
|
||||
|
||||
1. Setup tab fires once, calibrating volume and setting mode.
|
||||
2. The MGC reports its predicted flow back to the pumpingStation.
|
||||
3. Click any inject in the Process Plant tab to perturb the basin.
|
||||
4. Watch all three Port-0 debug taps: PS, MGC, both pumps.
|
||||
|
||||
---
|
||||
|
||||
## Example 03 — Dashboard
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Screenshot needed.** Two captures from `03-Dashboard.json`:
|
||||
> 1. The editor tab (Dashboard UI) showing the dashboard widgets and trend-feeder functions.
|
||||
> 2. The rendered dashboard at `http://localhost:1880/dashboard`.
|
||||
>
|
||||
> Save as `wiki/_partial-screenshots/pumpingStation/07-ex03-editor.png` and `08-ex03-dashboard.png`.
|
||||
> Replace this callout with both image links.
|
||||
|
||||
### What it adds vs Example 02
|
||||
|
||||
| Addition | Why |
|
||||
|:---|:---|
|
||||
| FlowFuse ui-base + ui-page + ui-group setup | One page, multiple grouped widgets |
|
||||
| 3 ui-chart widgets | flow / level / volume % trends |
|
||||
| ui-text widgets | live mode, demand, direction display |
|
||||
| ui-dropdown for mode | operator-facing mode switch |
|
||||
| ui-slider for demand | manual setpoint |
|
||||
| Trend-feeder function | splits Port-0 deltas into one msg per chart with `msg.topic` set as series label |
|
||||
The buttons fire the **same canonical `msg.topic`** as the inject nodes in Example 01 — there is no separate dashboard command surface to learn.
|
||||
|
||||
Required: `@flowfuse/node-red-dashboard` (Dashboard 2.0) installed in the Node-RED instance.
|
||||
|
||||
### What to do after deploy
|
||||
|
||||
1. Open `http://localhost:1880/dashboard/pumpingstation-basic`.
|
||||
2. Click `Mode: Manual` or `Mode: Levelbased`.
|
||||
3. Click `Inflow 60 m³/h` — Status panel level / volume / vol% rise; the Level / Volume / Flow charts plot the trends.
|
||||
4. In manual mode click `Demand 40 m³/h` — `Manual demand` row updates, node badge appends `Qd=40 m³/h`.
|
||||
5. Inspect the **Raw output** table at the bottom of the page for the full Port 0 surface (basin geometry, dryRunLevel, highVolumeSafetyLevel, predictedOverflowVolume, …).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **GIF needed.** Slide the demand control and watch the trend charts react. 20–30 s is enough.
|
||||
> **GIF needed.** Capture clicking through Mode → Inflow → Demand and the charts reacting. 20–30 s is enough.
|
||||
>
|
||||
> Save as `wiki/_partial-gifs/pumpingStation/03-ex03-dashboard.gif`.
|
||||
> Save as `wiki/_partial-gifs/pumpingStation/02-ex02-dashboard.gif`.
|
||||
> Replace this callout with the image link.
|
||||
|
||||
---
|
||||
@@ -159,7 +131,6 @@ Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/bran
|
||||
| Level rises but `volume` stays at `minVol` | Volume integrator hasn't been calibrated. Send `cmd.calibrate.level = <real level>` once. |
|
||||
| Demand stays at 0 % even though level is high | Mode might be `manual` — check `set.mode`. Or the safety layer is blocking (look at `safety.blocked` on Port 0). |
|
||||
| Predicted volume drifts | Net-flow source is wrong. Look at `flowSource` on Port 0; it should match the highest-level aggregator you have wired in. |
|
||||
| MGC and pumps don't see demand | `02-Integration.json` requires the MGC to register **before** the pumps. The Setup tab handles ordering. |
|
||||
| `enableLog: 'debug'` floods the container log | Toggle it off in the node's config. Never ship a demo with debug logging enabled. |
|
||||
|
||||
---
|
||||
|
||||
BIN
wiki/_partial-gifs/pumpingStation/01-basic-demo.gif
Normal file
BIN
wiki/_partial-gifs/pumpingStation/01-basic-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 MiB |
BIN
wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png
Normal file
BIN
wiki/_partial-screenshots/pumpingStation/01-node-and-editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
BIN
wiki/_partial-screenshots/pumpingStation/02-basic-flow.png
Normal file
BIN
wiki/_partial-screenshots/pumpingStation/02-basic-flow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 213 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
Reference in New Issue
Block a user