P9.3 + examples: fresh 3-tier flows + pilot wiki Home.md
examples/ (new — was empty except standalone-demo.js):
01-Basic.json 14 nodes, inject + dashboard, no parent
02-Integration.json 32 nodes, 2 tabs, measurement + MGC + 2 pumps,
link-out/link-in channels per node-red-flow-layout.md
03-Dashboard.json 63 nodes, 3 tabs (process + UI + setup),
FlowFuse charts + sliders, trend-split pattern
README.md load instructions
tools/build-examples.js regenerator
All canonical topic names only (set.*, cmd.*, data.*, child.*). No
legacy aliases. Every ui-* widget has x/y. Every chart has the full
mandatory key set from node-red-flow-layout.md §4.
wiki/Home.md (new) — pilot page for the 14-section visual-first template.
Sections 5 (topic-contract) + 9 (data-model) are auto-generated via the
new npm run wiki:* scripts; everything else hand-written following
.claude/refactor/WIKI_TEMPLATE.md.
package.json — added wiki:contract / wiki:datamodel / wiki:all scripts
wired to ../generalFunctions/scripts/wikiGen.js.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
340
examples/01-Basic.json
Normal file
340
examples/01-Basic.json
Normal file
@@ -0,0 +1,340 @@
|
||||
[
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"nodes": [
|
||||
"ps_basic_node",
|
||||
"ps_basic_format"
|
||||
],
|
||||
"x": 1290,
|
||||
"y": 230,
|
||||
"w": 500,
|
||||
"h": 140
|
||||
}
|
||||
]
|
||||
686
examples/02-Integration.json
Normal file
686
examples/02-Integration.json
Normal file
@@ -0,0 +1,686 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
1325
examples/03-Dashboard.json
Normal file
1325
examples/03-Dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
99
examples/README.md
Normal file
99
examples/README.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# pumpingStation - Example Flows
|
||||
|
||||
Three Node-RED flows demonstrating the Phase-2 pumpingStation node on the
|
||||
canonical topic API (`set.mode`, `set.inflow`, `set.demand`,
|
||||
`cmd.calibrate.volume`, `cmd.calibrate.level`). Legacy aliases
|
||||
(`changemode`, `q_in`, `Qd`, `calibratePredictedVolume`,
|
||||
`calibratePredictedLevel`, `registerChild`) still work but log a
|
||||
one-time deprecation warning; these fresh flows use the canonical names only.
|
||||
|
||||
## Files
|
||||
|
||||
| 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). |
|
||||
|
||||
## 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).
|
||||
|
||||
## How to load
|
||||
|
||||
```bash
|
||||
# Drop a file into a running Node-RED instance using its Admin API.
|
||||
curl -X POST -H 'Content-Type: application/json' \
|
||||
--data @nodes/pumpingStation/examples/01-Basic.json \
|
||||
http://localhost:1880/flows
|
||||
```
|
||||
|
||||
Or in the editor: **Menu -> Import -> select file -> Import**. The flows
|
||||
import into their own tabs and can be deployed immediately.
|
||||
|
||||
## 01-Basic - what to try
|
||||
|
||||
1. Deploy.
|
||||
2. Inject `set.mode = manual`.
|
||||
3. Inject `set.inflow = 60 m3/h` - the basin starts filling. Watch the
|
||||
formatted Port 0 payload in the debug sidebar.
|
||||
4. Inject `set.demand = 40 %` - in manual mode this would feed any
|
||||
registered children; here there are no pump children so it is logged
|
||||
and shown on Port 0.
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
These flows follow the EVOLV layout rule set in
|
||||
`.claude/rules/node-red-flow-layout.md`:
|
||||
|
||||
- Tabs split by **concern**: Process Plant (EVOLV nodes) / Dashboard UI
|
||||
(`ui-*` widgets) / Setup (once-true injects).
|
||||
- Cross-tab wiring via **named link out / link in channels**:
|
||||
`setup:to-ps-mode`, `setup:to-ps-inflow`, `setup:to-mgc-mode`,
|
||||
`cmd:ps-mode`, `cmd:ps-demand`, `evt:flow`, `evt:level`,
|
||||
`evt:volpct`, `evt:state`, `evt:perc`, `evt:dir`, `evt:tempty`.
|
||||
- **Lane positions** L0-L7 = `[120, 360, 600, 840, 1080, 1320, 1560, 1800]`,
|
||||
driven by each node's S88 level (Process Cell on L5, Unit on L4,
|
||||
Equipment on L3, Control Module on L2).
|
||||
- **Group boxes** wrap each parent + its direct children, coloured by the
|
||||
parent's S88 level.
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user