updates to rotating machine struct
This commit is contained in:
49
examples/README.md
Normal file
49
examples/README.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# RotatingMachine Example Flows
|
||||
|
||||
These flows are import-ready Node-RED examples focused on the `rotatingMachine` node.
|
||||
|
||||
## Files
|
||||
|
||||
- `basic.flow.json`
|
||||
Purpose: quick manual control + local visualization.
|
||||
Includes:
|
||||
- mode selection
|
||||
- startup/shutdown/emergency buttons
|
||||
- setpoint control
|
||||
- simulated upstream/downstream pressure input
|
||||
- local charts for predicted flow/power/ctrl
|
||||
|
||||
- `integration.flow.json`
|
||||
Purpose: richer scenario testing from dashboard controls.
|
||||
Includes:
|
||||
- sequence controls (startup/shutdown/maintenance)
|
||||
- direct setpoint + flowMovement commands
|
||||
- simulated pressure inputs
|
||||
- charts for flow, power, NCog%
|
||||
- state snapshot text
|
||||
|
||||
- `edge.flow.json`
|
||||
Purpose: intentionally send invalid/boundary inputs and observe behavior.
|
||||
Includes:
|
||||
- invalid mode command
|
||||
- negative setpoint command
|
||||
- disallowed source sequence command
|
||||
- unsupported simulated measurement type
|
||||
- debug outputs for process/influx/parent channels
|
||||
|
||||
## Requirements
|
||||
|
||||
- EVOLV rotatingMachine node installed/available in Node-RED.
|
||||
- FlowFuse Dashboard 2 nodes installed (`ui-base`, `ui-page`, `ui-group`, etc.).
|
||||
|
||||
## Import
|
||||
|
||||
1. In Node-RED, use `Import` and select one of the `*.flow.json` files.
|
||||
2. Deploy.
|
||||
3. Open the dashboard page path configured in the flow.
|
||||
|
||||
## Notes
|
||||
|
||||
- These examples are manual by design: no auto-start on deploy.
|
||||
- Pressure simulation uses `msg.topic = "simulateMeasurement"` handled by the rotatingMachine wrapper.
|
||||
- Output graphs are based on the rotatingMachine process output payload fields.
|
||||
644
examples/basic.flow.json
Normal file
644
examples/basic.flow.json
Normal file
@@ -0,0 +1,644 @@
|
||||
[
|
||||
{
|
||||
"id": "f1e8a6c8b2a4477f",
|
||||
"type": "tab",
|
||||
"label": "RotatingMachine Basic",
|
||||
"disabled": false,
|
||||
"info": "Basic manual control and visualization for rotatingMachine"
|
||||
},
|
||||
{
|
||||
"id": "ui_base_rm_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_rm_basic",
|
||||
"type": "ui-theme",
|
||||
"name": "EVOLV Basic Theme",
|
||||
"colors": {
|
||||
"surface": "#ffffff",
|
||||
"primary": "#0094ce",
|
||||
"bgPage": "#eeeeee",
|
||||
"groupBg": "#ffffff",
|
||||
"groupOutline": "#cccccc"
|
||||
},
|
||||
"sizes": {
|
||||
"density": "default",
|
||||
"pagePadding": "12px",
|
||||
"groupGap": "12px",
|
||||
"groupBorderRadius": "4px",
|
||||
"widgetGap": "12px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui_page_rm_basic",
|
||||
"type": "ui-page",
|
||||
"name": "RotatingMachine Basic",
|
||||
"ui": "ui_base_rm_basic",
|
||||
"path": "/rotating-machine-basic",
|
||||
"icon": "settings_input_component",
|
||||
"layout": "grid",
|
||||
"theme": "ui_theme_rm_basic",
|
||||
"breakpoints": [
|
||||
{
|
||||
"name": "Default",
|
||||
"px": "0",
|
||||
"cols": "12"
|
||||
}
|
||||
],
|
||||
"order": 1,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_ctrl",
|
||||
"type": "ui-group",
|
||||
"name": "Machine Controls",
|
||||
"page": "ui_page_rm_basic",
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"order": 1,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_sim",
|
||||
"type": "ui-group",
|
||||
"name": "Simulation Inputs",
|
||||
"page": "ui_page_rm_basic",
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"order": 2,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_obs",
|
||||
"type": "ui-group",
|
||||
"name": "Observed Output",
|
||||
"page": "ui_page_rm_basic",
|
||||
"width": "12",
|
||||
"height": "24",
|
||||
"order": 3,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "rm_node_basic",
|
||||
"type": "rotatingMachine",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "RM Basic",
|
||||
"speed": "1",
|
||||
"startup": "3",
|
||||
"warmup": "3",
|
||||
"shutdown": "3",
|
||||
"cooldown": "3",
|
||||
"movementMode": "staticspeed",
|
||||
"machineCurve": "",
|
||||
"uuid": "",
|
||||
"supplier": "hidrostal",
|
||||
"category": "machine",
|
||||
"assetType": "pump-centrifugal",
|
||||
"model": "hidrostal-H05K-S03R",
|
||||
"unit": "m3/h",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"positionIcon": "",
|
||||
"hasDistance": false,
|
||||
"distance": "",
|
||||
"distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"x": 1060,
|
||||
"y": 420,
|
||||
"wires": [
|
||||
[
|
||||
"rm_parse_output"
|
||||
],
|
||||
[
|
||||
"rm_debug_influx"
|
||||
],
|
||||
[
|
||||
"rm_debug_parent"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_mode_dropdown",
|
||||
"type": "ui-dropdown",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_ctrl",
|
||||
"name": "Mode",
|
||||
"label": "Mode",
|
||||
"order": 1,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"multiple": false,
|
||||
"options": [
|
||||
{
|
||||
"label": "auto",
|
||||
"value": "auto",
|
||||
"type": "str"
|
||||
},
|
||||
{
|
||||
"label": "virtualControl",
|
||||
"value": "virtualControl",
|
||||
"type": "str"
|
||||
},
|
||||
{
|
||||
"label": "fysicalControl",
|
||||
"value": "fysicalControl",
|
||||
"type": "str"
|
||||
}
|
||||
],
|
||||
"payload": "",
|
||||
"topic": "setMode",
|
||||
"x": 210,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"rm_set_mode"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_set_mode",
|
||||
"type": "change",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "topic=setMode",
|
||||
"rules": [
|
||||
{
|
||||
"t": "set",
|
||||
"p": "topic",
|
||||
"pt": "msg",
|
||||
"to": "setMode",
|
||||
"tot": "str"
|
||||
}
|
||||
],
|
||||
"x": 440,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_startup_btn",
|
||||
"type": "ui-button",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_ctrl",
|
||||
"name": "Startup",
|
||||
"label": "Startup",
|
||||
"order": 2,
|
||||
"width": "3",
|
||||
"height": "1",
|
||||
"emulateClick": false,
|
||||
"tooltip": "Run startup sequence",
|
||||
"color": "",
|
||||
"bgcolor": "",
|
||||
"icon": "play_arrow",
|
||||
"payload": "startup",
|
||||
"payloadType": "str",
|
||||
"topic": "",
|
||||
"x": 200,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"rm_exec_sequence_builder"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_shutdown_btn",
|
||||
"type": "ui-button",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_ctrl",
|
||||
"name": "Shutdown",
|
||||
"label": "Shutdown",
|
||||
"order": 3,
|
||||
"width": "3",
|
||||
"height": "1",
|
||||
"emulateClick": false,
|
||||
"tooltip": "Run shutdown sequence",
|
||||
"icon": "stop",
|
||||
"payload": "shutdown",
|
||||
"payloadType": "str",
|
||||
"topic": "",
|
||||
"x": 200,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"rm_exec_sequence_builder"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_emergency_btn",
|
||||
"type": "ui-button",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_ctrl",
|
||||
"name": "Emergency Stop",
|
||||
"label": "Emergency Stop",
|
||||
"order": 4,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"emulateClick": false,
|
||||
"tooltip": "Emergency stop",
|
||||
"color": "#ffffff",
|
||||
"bgcolor": "#cc0000",
|
||||
"icon": "warning",
|
||||
"payload": "{\"source\":\"GUI\",\"action\":\"emergencystop\"}",
|
||||
"payloadType": "json",
|
||||
"topic": "emergencystop",
|
||||
"x": 240,
|
||||
"y": 300,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_exec_sequence_builder",
|
||||
"type": "function",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "Build execSequence",
|
||||
"func": "msg.topic = 'execSequence';\nmsg.payload = {\n source: 'GUI',\n action: 'execSequence',\n parameter: msg.payload\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"x": 470,
|
||||
"y": 240,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_setpoint_input",
|
||||
"type": "ui-number-input",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_ctrl",
|
||||
"name": "Setpoint",
|
||||
"label": "Setpoint %",
|
||||
"order": 5,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"topic": "",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"x": 200,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"rm_exec_movement_builder",
|
||||
"rm_ctrl_setpoint_for_chart"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_exec_movement_builder",
|
||||
"type": "function",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "Build execMovement",
|
||||
"func": "msg.topic = 'execMovement';\nmsg.payload = {\n source: 'GUI',\n action: 'execMovement',\n setpoint: Number(msg.payload)\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"x": 480,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_pressure_down_input",
|
||||
"type": "ui-number-input",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_sim",
|
||||
"name": "Downstream Pressure",
|
||||
"label": "Downstream pressure (mbar)",
|
||||
"order": 1,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"topic": "",
|
||||
"min": 0,
|
||||
"max": 3000,
|
||||
"step": 10,
|
||||
"x": 230,
|
||||
"y": 460,
|
||||
"wires": [
|
||||
[
|
||||
"rm_sim_pressure_down_builder"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_pressure_up_input",
|
||||
"type": "ui-number-input",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_sim",
|
||||
"name": "Upstream Pressure",
|
||||
"label": "Upstream pressure (mbar)",
|
||||
"order": 2,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"topic": "",
|
||||
"min": 0,
|
||||
"max": 3000,
|
||||
"step": 10,
|
||||
"x": 220,
|
||||
"y": 500,
|
||||
"wires": [
|
||||
[
|
||||
"rm_sim_pressure_up_builder"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_sim_pressure_down_builder",
|
||||
"type": "function",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "simulate downstream pressure",
|
||||
"func": "msg.topic = 'simulateMeasurement';\nmsg.payload = {\n type: 'pressure',\n position: 'downstream',\n value: Number(msg.payload),\n unit: 'mbar'\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"x": 510,
|
||||
"y": 460,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_sim_pressure_up_builder",
|
||||
"type": "function",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "simulate upstream pressure",
|
||||
"func": "msg.topic = 'simulateMeasurement';\nmsg.payload = {\n type: 'pressure',\n position: 'upstream',\n value: Number(msg.payload),\n unit: 'mbar'\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"x": 500,
|
||||
"y": 500,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_basic"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_parse_output",
|
||||
"type": "function",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "Parse RM process output",
|
||||
"func": "const incoming = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst lastPayload = context.get('lastPayload') || {};\nconst merged = { ...lastPayload, ...incoming };\ncontext.set('lastPayload', merged);\n\nconst cache = context.get('metricCache') || {\n flow: 0,\n power: 0,\n ctrl: 0,\n nCog: 0,\n stateCode: 0,\n state: 'idle',\n mode: 'auto',\n runtime: 0,\n moveTimeleft: 0,\n maintenanceTime: 0\n};\n\nconst pickNumber = (...keys) => {\n for (const key of keys) {\n const value = Number(merged[key]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\n\nconst pickString = (key, fallback = null) => {\n const value = merged[key];\n if (value === undefined || value === null || value === '') return fallback;\n return String(value);\n};\n\nconst flow = pickNumber('flow.predicted.downstream.default', 'flow.predicted.downstream');\nconst power = pickNumber('power.predicted.atequipment.default', 'power.predicted.atequipment', 'power.predicted.atEquipment.default', 'power.predicted.atEquipment');\nconst ctrl = pickNumber('ctrl', 'ctrl.predicted.atequipment.default', 'ctrl.predicted.atequipment', 'ctrl.predicted.atEquipment.default', 'ctrl.predicted.atEquipment');\nconst nCog = pickNumber('NCogPercent', 'NCog');\nconst runtime = pickNumber('runtime');\nconst moveTimeleft = pickNumber('moveTimeleft');\nconst maintenanceTime = pickNumber('maintenanceTime');\nconst state = pickString('state', cache.state);\nconst mode = pickString('mode', cache.mode);\n\nconst stateCodeMap = { off: 0, idle: 1, starting: 2, warmingup: 3, operational: 4, accelerating: 5, decelerating: 6, stopping: 7, coolingdown: 8, maintenance: 9 };\nconst stateCode = stateCodeMap[state] ?? cache.stateCode;\n\nif (flow !== null) cache.flow = flow;\nif (power !== null) cache.power = power;\nif (ctrl !== null) cache.ctrl = ctrl;\nif (nCog !== null) cache.nCog = nCog;\nif (runtime !== null) cache.runtime = runtime;\nif (moveTimeleft !== null) cache.moveTimeleft = moveTimeleft;\nif (maintenanceTime !== null) cache.maintenanceTime = maintenanceTime;\ncache.state = state;\ncache.mode = mode;\ncache.stateCode = stateCode;\ncontext.set('metricCache', cache);\n\nreturn [\n { topic: 'actual_flow', payload: cache.flow },\n { topic: 'predicted_power', payload: cache.power },\n { topic: 'actual_ctrl', payload: cache.ctrl },\n { topic: 'nCog', payload: cache.nCog },\n { topic: 'stateCode', payload: cache.stateCode },\n { payload: 'state=' + cache.state + ', mode=' + cache.mode + ', ctrl=' + cache.ctrl.toFixed(2) + '%' },\n { payload: 'runtime=' + cache.runtime.toFixed(3) + ' h | moveTimeLeft=' + cache.moveTimeleft.toFixed(0) + ' s | maintenance=' + cache.maintenanceTime.toFixed(3) + ' h' },\n { payload: JSON.stringify(merged) }\n];",
|
||||
"outputs": 8,
|
||||
"noerr": 0,
|
||||
"x": 1310,
|
||||
"y": 420,
|
||||
"wires": [
|
||||
[
|
||||
"rm_chart_flow"
|
||||
],
|
||||
[
|
||||
"rm_chart_power"
|
||||
],
|
||||
[
|
||||
"rm_chart_ctrl"
|
||||
],
|
||||
[
|
||||
"rm_chart_ncog"
|
||||
],
|
||||
[
|
||||
"rm_chart_statecode"
|
||||
],
|
||||
[
|
||||
"rm_text_state"
|
||||
],
|
||||
[
|
||||
"rm_text_timing"
|
||||
],
|
||||
[
|
||||
"rm_text_snapshot"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_chart_flow",
|
||||
"type": "ui-chart",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "Predicted Flow",
|
||||
"label": "Flow (m3/h)",
|
||||
"order": 1,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"category": "",
|
||||
"xAxisLabel": "time",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m3/h",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"removeOlderPoints": "",
|
||||
"x": 1560,
|
||||
"y": 340,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_chart_power",
|
||||
"type": "ui-chart",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "Predicted Power",
|
||||
"label": "Power (kW)",
|
||||
"order": 2,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "kW",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1560,
|
||||
"y": 400,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_chart_ctrl",
|
||||
"type": "ui-chart",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "Control Position",
|
||||
"label": "Ctrl (%)",
|
||||
"order": 3,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "%",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1560,
|
||||
"y": 460,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_text_state",
|
||||
"type": "ui-text",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "State/Mode",
|
||||
"label": "Current State",
|
||||
"order": 6,
|
||||
"width": 12,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1560,
|
||||
"y": 520,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_debug_influx",
|
||||
"type": "debug",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "Influx output",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1290,
|
||||
"y": 560,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_debug_parent",
|
||||
"type": "debug",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "Parent output",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"console": false,
|
||||
"tostatus": false,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1280,
|
||||
"y": 600,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_chart_ncog",
|
||||
"type": "ui-chart",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "NCog",
|
||||
"label": "NCog (%)",
|
||||
"order": 4,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "%",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1560,
|
||||
"y": 520,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_chart_statecode",
|
||||
"type": "ui-chart",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "Machine State Code",
|
||||
"label": "State Code (off=0 .. maint=9)",
|
||||
"order": 5,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "state",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1560,
|
||||
"y": 580,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_text_timing",
|
||||
"type": "ui-text",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "Timing",
|
||||
"label": "Timing",
|
||||
"order": 7,
|
||||
"width": 12,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1560,
|
||||
"y": 700,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_text_snapshot",
|
||||
"type": "ui-text",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"group": "ui_group_rm_obs",
|
||||
"name": "Latest Payload",
|
||||
"label": "Latest Payload",
|
||||
"order": 8,
|
||||
"width": 12,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1560,
|
||||
"y": 740,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_ctrl_setpoint_for_chart",
|
||||
"type": "function",
|
||||
"z": "f1e8a6c8b2a4477f",
|
||||
"name": "ctrl setpoint series",
|
||||
"func": "msg.topic = 'setpoint_ctrl';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 610,
|
||||
"y": 280,
|
||||
"wires": [
|
||||
[
|
||||
"rm_chart_ctrl"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
325
examples/edge.flow.json
Normal file
325
examples/edge.flow.json
Normal file
@@ -0,0 +1,325 @@
|
||||
[
|
||||
{
|
||||
"id": "91a88f212fb34de8",
|
||||
"type": "tab",
|
||||
"label": "RotatingMachine Edge Cases",
|
||||
"disabled": false,
|
||||
"info": "Manual edge-case driving for rotatingMachine"
|
||||
},
|
||||
{
|
||||
"id": "ui_base_rm_edge",
|
||||
"type": "ui-base",
|
||||
"name": "EVOLV Demo",
|
||||
"path": "/dashboard",
|
||||
"includeClientData": true,
|
||||
"acceptsClientConfig": [
|
||||
"ui-notification",
|
||||
"ui-control"
|
||||
],
|
||||
"showPathInSidebar": false,
|
||||
"headerContent": "page",
|
||||
"navigationStyle": "default",
|
||||
"titleBarStyle": "default"
|
||||
},
|
||||
{
|
||||
"id": "ui_theme_rm_edge",
|
||||
"type": "ui-theme",
|
||||
"name": "EVOLV Edge Theme",
|
||||
"colors": {
|
||||
"surface": "#ffffff",
|
||||
"primary": "#0094ce",
|
||||
"bgPage": "#eeeeee",
|
||||
"groupBg": "#ffffff",
|
||||
"groupOutline": "#cccccc"
|
||||
},
|
||||
"sizes": {
|
||||
"density": "default",
|
||||
"pagePadding": "12px",
|
||||
"groupGap": "12px",
|
||||
"groupBorderRadius": "4px",
|
||||
"widgetGap": "12px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui_page_rm_edge",
|
||||
"type": "ui-page",
|
||||
"name": "RotatingMachine Edge",
|
||||
"ui": "ui_base_rm_edge",
|
||||
"path": "/rotating-machine-edge",
|
||||
"icon": "report_problem",
|
||||
"layout": "grid",
|
||||
"theme": "ui_theme_rm_edge",
|
||||
"breakpoints": [
|
||||
{
|
||||
"name": "Default",
|
||||
"px": "0",
|
||||
"cols": "12"
|
||||
}
|
||||
],
|
||||
"order": 3,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_edge_inputs",
|
||||
"type": "ui-group",
|
||||
"name": "Edge Input Generators",
|
||||
"page": "ui_page_rm_edge",
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"order": 1,
|
||||
"showTitle": true
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_edge_obs",
|
||||
"type": "ui-group",
|
||||
"name": "Observed Responses",
|
||||
"page": "ui_page_rm_edge",
|
||||
"width": "6",
|
||||
"height": "8",
|
||||
"order": 2,
|
||||
"showTitle": true
|
||||
},
|
||||
{
|
||||
"id": "rm_node_edge",
|
||||
"type": "rotatingMachine",
|
||||
"z": "91a88f212fb34de8",
|
||||
"name": "RM Edge",
|
||||
"speed": "1",
|
||||
"startup": "3",
|
||||
"warmup": "3",
|
||||
"shutdown": "3",
|
||||
"cooldown": "3",
|
||||
"movementMode": "staticspeed",
|
||||
"machineCurve": "",
|
||||
"uuid": "",
|
||||
"supplier": "hidrostal",
|
||||
"category": "machine",
|
||||
"assetType": "pump-centrifugal",
|
||||
"model": "hidrostal-H05K-S03R",
|
||||
"unit": "m3/h",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"x": 930,
|
||||
"y": 300,
|
||||
"wires": [
|
||||
[
|
||||
"rm_edge_parse",
|
||||
"rm_edge_process_debug"
|
||||
],
|
||||
[
|
||||
"rm_edge_debug_influx"
|
||||
],
|
||||
[
|
||||
"rm_edge_debug_parent"
|
||||
]
|
||||
],
|
||||
"hasDistance": false
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_invalid_mode_btn",
|
||||
"type": "ui-button",
|
||||
"z": "91a88f212fb34de8",
|
||||
"group": "ui_group_rm_edge_inputs",
|
||||
"name": "Invalid Mode",
|
||||
"label": "Send invalid mode",
|
||||
"order": 1,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"payload": "invalidMode",
|
||||
"payloadType": "str",
|
||||
"topic": "setMode",
|
||||
"x": 190,
|
||||
"y": 120,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_edge"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_neg_setpoint",
|
||||
"type": "ui-number-input",
|
||||
"z": "91a88f212fb34de8",
|
||||
"group": "ui_group_rm_edge_inputs",
|
||||
"name": "Negative Setpoint",
|
||||
"label": "Negative setpoint",
|
||||
"order": 2,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"min": -100,
|
||||
"max": 0,
|
||||
"step": 1,
|
||||
"x": 190,
|
||||
"y": 170,
|
||||
"wires": [
|
||||
[
|
||||
"rm_edge_exec_movement"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_exec_movement",
|
||||
"type": "function",
|
||||
"z": "91a88f212fb34de8",
|
||||
"name": "Build execMovement",
|
||||
"func": "msg.topic = 'execMovement';\nmsg.payload = {source:'GUI', action:'execMovement', setpoint:Number(msg.payload)};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 450,
|
||||
"y": 170,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_edge"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_bad_source_btn",
|
||||
"type": "ui-button",
|
||||
"z": "91a88f212fb34de8",
|
||||
"group": "ui_group_rm_edge_inputs",
|
||||
"name": "Bad Source",
|
||||
"label": "Disallowed source action",
|
||||
"order": 3,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"payload": "{}",
|
||||
"payloadType": "str",
|
||||
"x": 210,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"rm_edge_bad_source_builder"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_bad_source_builder",
|
||||
"type": "function",
|
||||
"z": "91a88f212fb34de8",
|
||||
"name": "Build blocked execSequence",
|
||||
"func": "msg.topic='execSequence';\nmsg.payload={source:'bad-source', action:'execSequence', parameter:'startup'};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 500,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_edge"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_bad_sim_type",
|
||||
"type": "ui-button",
|
||||
"z": "91a88f212fb34de8",
|
||||
"group": "ui_group_rm_edge_inputs",
|
||||
"name": "Unsupported Sim Type",
|
||||
"label": "simulateMeasurement (bad type)",
|
||||
"order": 4,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"payload": "{\"type\":\"unknown\",\"position\":\"downstream\",\"value\":123,\"unit\":\"mbar\"}",
|
||||
"payloadType": "json",
|
||||
"topic": "simulateMeasurement",
|
||||
"x": 220,
|
||||
"y": 270,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_edge"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_parse",
|
||||
"type": "function",
|
||||
"z": "91a88f212fb34de8",
|
||||
"name": "Summarize process output",
|
||||
"func": "const incoming = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst merged = { ...(context.get('lastPayload') || {}), ...incoming };\ncontext.set('lastPayload', merged);\n\nconst state = merged.state == null ? 'n/a' : String(merged.state);\nconst mode = merged.mode == null ? 'n/a' : String(merged.mode);\nconst ctrl = Number(merged.ctrl);\nconst stateCodeMap = { off: 0, idle: 1, starting: 2, warmingup: 3, operational: 4, accelerating: 5, decelerating: 6, stopping: 7, coolingdown: 8, maintenance: 9 };\nconst stateCode = stateCodeMap[state] ?? -1;\n\nreturn [\n { payload: `state=${state}, mode=${mode}, ctrl=${Number.isFinite(ctrl) ? ctrl.toFixed(2) : 'n/a'}%` },\n { topic: 'stateCode', payload: stateCode }\n];",
|
||||
"outputs": 2,
|
||||
"x": 1230,
|
||||
"y": 300,
|
||||
"wires": [
|
||||
[
|
||||
"rm_edge_state_text"
|
||||
],
|
||||
[
|
||||
"rm_edge_state_chart"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_state_text",
|
||||
"type": "ui-text",
|
||||
"z": "91a88f212fb34de8",
|
||||
"group": "ui_group_rm_edge_obs",
|
||||
"name": "State Text",
|
||||
"label": "Machine summary",
|
||||
"order": 2,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1450,
|
||||
"y": 300,
|
||||
"wires": [],
|
||||
"width": 6,
|
||||
"height": 1
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_process_debug",
|
||||
"type": "debug",
|
||||
"z": "91a88f212fb34de8",
|
||||
"name": "Process Output",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1220,
|
||||
"y": 340,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_debug_influx",
|
||||
"type": "debug",
|
||||
"z": "91a88f212fb34de8",
|
||||
"name": "Influx Output",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1220,
|
||||
"y": 380,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_debug_parent",
|
||||
"type": "debug",
|
||||
"z": "91a88f212fb34de8",
|
||||
"name": "Parent Output",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1220,
|
||||
"y": 420,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_edge_state_chart",
|
||||
"type": "ui-chart",
|
||||
"z": "91a88f212fb34de8",
|
||||
"group": "ui_group_rm_edge_obs",
|
||||
"name": "State Code",
|
||||
"label": "State Code",
|
||||
"order": 1,
|
||||
"width": 6,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "state",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1230,
|
||||
"y": 300,
|
||||
"wires": []
|
||||
}
|
||||
]
|
||||
569
examples/integration.flow.json
Normal file
569
examples/integration.flow.json
Normal file
@@ -0,0 +1,569 @@
|
||||
[
|
||||
{
|
||||
"id": "12f41a7b538c40db",
|
||||
"type": "tab",
|
||||
"label": "RotatingMachine Integration",
|
||||
"disabled": false,
|
||||
"info": "Manual integration-style scenario builder for rotatingMachine"
|
||||
},
|
||||
{
|
||||
"id": "ui_base_rm_int",
|
||||
"type": "ui-base",
|
||||
"name": "EVOLV Demo",
|
||||
"path": "/dashboard",
|
||||
"includeClientData": true,
|
||||
"acceptsClientConfig": [
|
||||
"ui-notification",
|
||||
"ui-control"
|
||||
],
|
||||
"showPathInSidebar": false,
|
||||
"headerContent": "page",
|
||||
"navigationStyle": "default",
|
||||
"titleBarStyle": "default"
|
||||
},
|
||||
{
|
||||
"id": "ui_theme_rm_int",
|
||||
"type": "ui-theme",
|
||||
"name": "EVOLV Integration Theme",
|
||||
"colors": {
|
||||
"surface": "#ffffff",
|
||||
"primary": "#0094ce",
|
||||
"bgPage": "#eeeeee",
|
||||
"groupBg": "#ffffff",
|
||||
"groupOutline": "#cccccc"
|
||||
},
|
||||
"sizes": {
|
||||
"density": "default",
|
||||
"pagePadding": "12px",
|
||||
"groupGap": "12px",
|
||||
"groupBorderRadius": "4px",
|
||||
"widgetGap": "12px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ui_page_rm_int",
|
||||
"type": "ui-page",
|
||||
"name": "RotatingMachine Integration",
|
||||
"ui": "ui_base_rm_int",
|
||||
"path": "/rotating-machine-integration",
|
||||
"icon": "lan",
|
||||
"layout": "grid",
|
||||
"theme": "ui_theme_rm_int",
|
||||
"breakpoints": [
|
||||
{
|
||||
"name": "Default",
|
||||
"px": "0",
|
||||
"cols": "12"
|
||||
}
|
||||
],
|
||||
"order": 2,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_int_ctrl",
|
||||
"type": "ui-group",
|
||||
"name": "Control Sequences",
|
||||
"page": "ui_page_rm_int",
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"order": 1,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_int_sim",
|
||||
"type": "ui-group",
|
||||
"name": "Process Simulation",
|
||||
"page": "ui_page_rm_int",
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"order": 2,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "ui_group_rm_int_vis",
|
||||
"type": "ui-group",
|
||||
"name": "Observed Behaviour",
|
||||
"page": "ui_page_rm_int",
|
||||
"width": "12",
|
||||
"height": "20",
|
||||
"order": 3,
|
||||
"showTitle": true,
|
||||
"className": ""
|
||||
},
|
||||
{
|
||||
"id": "rm_node_int",
|
||||
"type": "rotatingMachine",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "RM Integration",
|
||||
"speed": "1",
|
||||
"startup": "3",
|
||||
"warmup": "3",
|
||||
"shutdown": "3",
|
||||
"cooldown": "3",
|
||||
"movementMode": "staticspeed",
|
||||
"machineCurve": "",
|
||||
"uuid": "",
|
||||
"supplier": "hidrostal",
|
||||
"category": "machine",
|
||||
"assetType": "pump-centrifugal",
|
||||
"model": "hidrostal-H05K-S03R",
|
||||
"unit": "m3/h",
|
||||
"enableLog": false,
|
||||
"logLevel": "error",
|
||||
"positionVsParent": "atEquipment",
|
||||
"x": 1040,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_parse"
|
||||
],
|
||||
[
|
||||
"rm_int_debug_influx"
|
||||
],
|
||||
[
|
||||
"rm_int_debug_parent"
|
||||
]
|
||||
],
|
||||
"hasDistance": false
|
||||
},
|
||||
{
|
||||
"id": "rm_int_startup",
|
||||
"type": "ui-button",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_ctrl",
|
||||
"name": "Startup",
|
||||
"label": "Startup",
|
||||
"order": 1,
|
||||
"width": "3",
|
||||
"height": "1",
|
||||
"icon": "play_arrow",
|
||||
"payload": "startup",
|
||||
"payloadType": "str",
|
||||
"x": 190,
|
||||
"y": 120,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_exec_seq"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_shutdown",
|
||||
"type": "ui-button",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_ctrl",
|
||||
"name": "Shutdown",
|
||||
"label": "Shutdown",
|
||||
"order": 2,
|
||||
"width": "3",
|
||||
"height": "1",
|
||||
"icon": "stop",
|
||||
"payload": "shutdown",
|
||||
"payloadType": "str",
|
||||
"x": 190,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_exec_seq"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_maint",
|
||||
"type": "ui-button",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_ctrl",
|
||||
"name": "Maintenance",
|
||||
"label": "Enter Maintenance",
|
||||
"order": 3,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"icon": "build",
|
||||
"payload": "entermaintenance",
|
||||
"payloadType": "str",
|
||||
"x": 220,
|
||||
"y": 200,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_exec_seq"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_exec_seq",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "Build execSequence",
|
||||
"func": "msg.topic = 'execSequence';\nmsg.payload = {source:'GUI', action:'execSequence', parameter: msg.payload};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 450,
|
||||
"y": 160,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_int"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_setpoint",
|
||||
"type": "ui-number-input",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_ctrl",
|
||||
"name": "Setpoint",
|
||||
"label": "Setpoint %",
|
||||
"order": 4,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"x": 190,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_exec_movement",
|
||||
"rm_int_ctrl_setpoint_for_chart"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_exec_movement",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "Build execMovement",
|
||||
"func": "msg.topic='execMovement';\nmsg.payload={source:'GUI', action:'execMovement', setpoint:Number(msg.payload)};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 460,
|
||||
"y": 260,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_int"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_flow_target",
|
||||
"type": "ui-number-input",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_sim",
|
||||
"name": "Flow target",
|
||||
"label": "Flow target (m3/h)",
|
||||
"order": 1,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"min": 0,
|
||||
"max": 1000,
|
||||
"step": 5,
|
||||
"x": 200,
|
||||
"y": 340,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_flow_move",
|
||||
"rm_int_flow_setpoint_for_chart"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_flow_move",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "Build flowMovement",
|
||||
"func": "msg.topic='flowMovement';\nmsg.payload={source:'GUI', action:'flowMovement', setpoint:Number(msg.payload)};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 450,
|
||||
"y": 340,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_int"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_pressure_up",
|
||||
"type": "ui-number-input",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_sim",
|
||||
"name": "Upstream Pressure",
|
||||
"label": "Upstream pressure (mbar)",
|
||||
"order": 2,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"min": 0,
|
||||
"max": 3000,
|
||||
"step": 10,
|
||||
"x": 210,
|
||||
"y": 390,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_sim_up"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_pressure_down",
|
||||
"type": "ui-number-input",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_sim",
|
||||
"name": "Downstream Pressure",
|
||||
"label": "Downstream pressure (mbar)",
|
||||
"order": 3,
|
||||
"width": "6",
|
||||
"height": "1",
|
||||
"passthru": true,
|
||||
"min": 0,
|
||||
"max": 3000,
|
||||
"step": 10,
|
||||
"x": 220,
|
||||
"y": 430,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_sim_down"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_sim_up",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "simulate upstream pressure",
|
||||
"func": "msg.topic='simulateMeasurement';\nmsg.payload={type:'pressure', position:'upstream', value:Number(msg.payload), unit:'mbar'};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 500,
|
||||
"y": 390,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_int"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_sim_down",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "simulate downstream pressure",
|
||||
"func": "msg.topic='simulateMeasurement';\nmsg.payload={type:'pressure', position:'downstream', value:Number(msg.payload), unit:'mbar'};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"x": 510,
|
||||
"y": 430,
|
||||
"wires": [
|
||||
[
|
||||
"rm_node_int"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_parse",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "Parse RM output",
|
||||
"func": "const incoming = (msg && msg.payload && typeof msg.payload === 'object') ? msg.payload : {};\nconst lastPayload = context.get('lastPayload') || {};\nconst merged = { ...lastPayload, ...incoming };\ncontext.set('lastPayload', merged);\n\nconst cache = context.get('metricCache') || {\n flow: 0,\n power: 0,\n nCog: 0,\n ctrl: 0,\n stateCode: 0,\n state: 'idle',\n mode: 'auto',\n runtime: 0,\n moveTimeleft: 0,\n maintenanceTime: 0\n};\n\nconst pickNumber = (...keys) => {\n for (const key of keys) {\n const value = Number(merged[key]);\n if (Number.isFinite(value)) return value;\n }\n return null;\n};\n\nconst pickString = (key, fallback = null) => {\n const value = merged[key];\n if (value === undefined || value === null || value === '') return fallback;\n return String(value);\n};\n\nconst flow = pickNumber('flow.predicted.downstream.default', 'flow.predicted.downstream');\nconst power = pickNumber('power.predicted.atequipment.default', 'power.predicted.atequipment', 'power.predicted.atEquipment.default', 'power.predicted.atEquipment');\nconst nCog = pickNumber('NCogPercent', 'NCog');\nconst ctrl = pickNumber('ctrl', 'ctrl.predicted.atequipment.default', 'ctrl.predicted.atequipment', 'ctrl.predicted.atEquipment.default', 'ctrl.predicted.atEquipment');\nconst runtime = pickNumber('runtime');\nconst moveTimeleft = pickNumber('moveTimeleft');\nconst maintenanceTime = pickNumber('maintenanceTime');\nconst state = pickString('state', cache.state);\nconst mode = pickString('mode', cache.mode);\n\nconst stateCodeMap = { off: 0, idle: 1, starting: 2, warmingup: 3, operational: 4, accelerating: 5, decelerating: 6, stopping: 7, coolingdown: 8, maintenance: 9 };\nconst stateCode = stateCodeMap[state] ?? cache.stateCode;\n\nif (flow !== null) cache.flow = flow;\nif (power !== null) cache.power = power;\nif (nCog !== null) cache.nCog = nCog;\nif (ctrl !== null) cache.ctrl = ctrl;\nif (runtime !== null) cache.runtime = runtime;\nif (moveTimeleft !== null) cache.moveTimeleft = moveTimeleft;\nif (maintenanceTime !== null) cache.maintenanceTime = maintenanceTime;\ncache.state = state;\ncache.mode = mode;\ncache.stateCode = stateCode;\ncontext.set('metricCache', cache);\n\nreturn [\n { topic: 'actual_flow', payload: cache.flow },\n { topic: 'predicted_power', payload: cache.power },\n { topic: 'nCog', payload: cache.nCog },\n { topic: 'actual_ctrl', payload: cache.ctrl },\n { topic: 'stateCode', payload: cache.stateCode },\n { payload: JSON.stringify({ state: cache.state, mode: cache.mode, ctrl: cache.ctrl, runtime: cache.runtime, moveTimeleft: cache.moveTimeleft, maintenanceTime: cache.maintenanceTime }) }\n];",
|
||||
"outputs": 6,
|
||||
"x": 1260,
|
||||
"y": 360,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_chart_flow"
|
||||
],
|
||||
[
|
||||
"rm_int_chart_power"
|
||||
],
|
||||
[
|
||||
"rm_int_chart_nCog"
|
||||
],
|
||||
[
|
||||
"rm_int_chart_ctrl"
|
||||
],
|
||||
[
|
||||
"rm_int_chart_statecode"
|
||||
],
|
||||
[
|
||||
"rm_int_state_text"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_chart_flow",
|
||||
"type": "ui-chart",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_vis",
|
||||
"name": "Flow",
|
||||
"label": "Flow (m3/h)",
|
||||
"order": 1,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "m3/h",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1510,
|
||||
"y": 280,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_int_chart_power",
|
||||
"type": "ui-chart",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_vis",
|
||||
"name": "Power",
|
||||
"label": "Power (kW)",
|
||||
"order": 2,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "kW",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1510,
|
||||
"y": 340,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_int_chart_nCog",
|
||||
"type": "ui-chart",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_vis",
|
||||
"name": "NCog",
|
||||
"label": "NCog (%)",
|
||||
"order": 3,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "%",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1510,
|
||||
"y": 400,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_int_state_text",
|
||||
"type": "ui-text",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_vis",
|
||||
"name": "State",
|
||||
"label": "State Snapshot",
|
||||
"order": 6,
|
||||
"width": 12,
|
||||
"height": 1,
|
||||
"format": "{{msg.payload}}",
|
||||
"layout": "row-spread",
|
||||
"x": 1510,
|
||||
"y": 460,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_int_debug_influx",
|
||||
"type": "debug",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "Influx",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1250,
|
||||
"y": 520,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_int_debug_parent",
|
||||
"type": "debug",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "Parent",
|
||||
"active": true,
|
||||
"tosidebar": true,
|
||||
"complete": "true",
|
||||
"targetType": "full",
|
||||
"x": 1240,
|
||||
"y": 560,
|
||||
"wires": []
|
||||
},
|
||||
{
|
||||
"id": "rm_int_chart_ctrl",
|
||||
"type": "ui-chart",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_vis",
|
||||
"name": "Ctrl",
|
||||
"label": "Ctrl (%)",
|
||||
"order": 4,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "%",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1510,
|
||||
"y": 460,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_int_chart_statecode",
|
||||
"type": "ui-chart",
|
||||
"z": "12f41a7b538c40db",
|
||||
"group": "ui_group_rm_int_vis",
|
||||
"name": "State Code",
|
||||
"label": "State Code (off=0 .. maint=9)",
|
||||
"order": 5,
|
||||
"width": 12,
|
||||
"height": 4,
|
||||
"chartType": "line",
|
||||
"xAxisType": "time",
|
||||
"yAxisLabel": "state",
|
||||
"removeOlder": "1",
|
||||
"removeOlderUnit": "3600",
|
||||
"x": 1510,
|
||||
"y": 520,
|
||||
"wires": [],
|
||||
"showLegend": true
|
||||
},
|
||||
{
|
||||
"id": "rm_int_ctrl_setpoint_for_chart",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "ctrl setpoint series",
|
||||
"func": "msg.topic = 'setpoint_ctrl';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 560,
|
||||
"y": 220,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_chart_ctrl"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "rm_int_flow_setpoint_for_chart",
|
||||
"type": "function",
|
||||
"z": "12f41a7b538c40db",
|
||||
"name": "flow setpoint series",
|
||||
"func": "msg.topic = 'setpoint_flow';\nmsg.payload = Number(msg.payload);\nreturn Number.isFinite(msg.payload) ? msg : null;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
"finalize": "",
|
||||
"libs": [],
|
||||
"x": 570,
|
||||
"y": 330,
|
||||
"wires": [
|
||||
[
|
||||
"rm_int_chart_flow"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Control module rotatingMachine",
|
||||
"main": "rotatingMachine.js",
|
||||
"scripts": {
|
||||
"test": "node rotatingMachine.js"
|
||||
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -118,7 +118,7 @@ class nodeClass {
|
||||
const mode = m.currentMode;
|
||||
const state = m.state.getCurrentState();
|
||||
const flow = Math.round(m.measurements.type("flow").variant("predicted").position('downstream').getCurrentValue('m3/h'));
|
||||
const power = Math.round(m.measurements.type("power").variant("predicted").position('atequipment').getCurrentValue('kW'));
|
||||
const power = Math.round(m.measurements.type("power").variant("predicted").position('atEquipment').getCurrentValue('kW'));
|
||||
let symbolState;
|
||||
switch(state){
|
||||
case "off":
|
||||
@@ -189,7 +189,7 @@ class nodeClass {
|
||||
}
|
||||
return status;
|
||||
} catch (error) {
|
||||
node.error("Error in updateNodeStatus: " + error.message);
|
||||
this.node.error("Error in updateNodeStatus: " + error.message);
|
||||
return { fill: "red", shape: "ring", text: "Status Error" };
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,40 @@ class nodeClass {
|
||||
const { source: esSource, action: esAction } = msg.payload;
|
||||
m.handleInput(esSource, esAction);
|
||||
break;
|
||||
case 'simulateMeasurement':
|
||||
{
|
||||
const payload = msg.payload || {};
|
||||
const type = String(payload.type || '').toLowerCase();
|
||||
const position = payload.position || 'atEquipment';
|
||||
const value = Number(payload.value);
|
||||
const unit = payload.unit;
|
||||
const context = {
|
||||
timestamp: payload.timestamp || Date.now(),
|
||||
unit,
|
||||
childName: 'dashboard-sim',
|
||||
childId: 'dashboard-sim',
|
||||
};
|
||||
|
||||
if (!Number.isFinite(value)) {
|
||||
this.node.warn('simulateMeasurement payload.value must be a finite number');
|
||||
break;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'pressure':
|
||||
m.updateMeasuredPressure(value, position, context);
|
||||
break;
|
||||
case 'flow':
|
||||
m.updateMeasuredFlow(value, position, context);
|
||||
break;
|
||||
case 'temperature':
|
||||
m.updateMeasuredTemperature(value, position, context);
|
||||
break;
|
||||
default:
|
||||
this.node.warn(`Unsupported simulateMeasurement type: ${type}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'showWorkingCurves':
|
||||
m.showWorkingCurves();
|
||||
send({ topic : "Showing curve" , payload: m.showWorkingCurves() });
|
||||
|
||||
@@ -96,10 +96,15 @@ class Machine {
|
||||
this.measurements.type('temperature').variant('measured').position('atEquipment').value(15).unit('C');
|
||||
//assume standard atm pressure is at sea level
|
||||
this.measurements.type('atmPressure').variant('measured').position('atEquipment').value(101325).unit('Pa');
|
||||
//populate min and max
|
||||
//populate min and max when curve data is available
|
||||
const flowunit = this.config.general.unit;
|
||||
this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax, Date.now() , flowunit)
|
||||
this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin).unit(this.config.general.unit);
|
||||
if (this.predictFlow) {
|
||||
this.measurements.type('flow').variant('predicted').position('max').value(this.predictFlow.currentFxyYMax, Date.now() , flowunit);
|
||||
this.measurements.type('flow').variant('predicted').position('min').value(this.predictFlow.currentFxyYMin).unit(this.config.general.unit);
|
||||
} else {
|
||||
this.measurements.type('flow').variant('predicted').position('max').value(0, Date.now(), flowunit);
|
||||
this.measurements.type('flow').variant('predicted').position('min').value(0, Date.now(), flowunit);
|
||||
}
|
||||
}
|
||||
|
||||
_updateState(){
|
||||
@@ -705,7 +710,14 @@ _callMeasurementHandler(measurementType, value, position, context) {
|
||||
const atmPressure = this.measurements.type('atmPressure').variant('measured').position('atEquipment').getCurrentValue('Pa');
|
||||
|
||||
console.log(`--------------------calc efficiency : Pressure diff:${pressureDiff},${temp}, ${g} `);
|
||||
const rho = coolprop.PropsSI('D', 'T', temp, 'P', atmPressure, 'WasteWater');
|
||||
let rho = null;
|
||||
try {
|
||||
rho = coolprop.PropsSI('D', 'T', temp, 'P', atmPressure, 'WasteWater');
|
||||
} catch (error) {
|
||||
// coolprop can throw transient initialization errors; keep machine calculations running.
|
||||
this.logger.warn(`CoolProp density lookup failed: ${error.message}. Using fallback density.`);
|
||||
rho = 1000; // kg/m3 fallback for water-like fluids
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug(`temp: ${temp} atmPressure : ${atmPressure} rho : ${rho} pressureDiff: ${pressureDiff?.value || 0}`);
|
||||
|
||||
31
test/basic/constructor.basic.test.js
Normal file
31
test/basic/constructor.basic.test.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const Machine = require('../../src/specificClass');
|
||||
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||
|
||||
test('constructor initializes with valid curve model', () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
assert.equal(machine.hasCurve, true);
|
||||
assert.ok(machine.predictFlow);
|
||||
assert.ok(machine.predictPower);
|
||||
assert.ok(machine.predictCtrl);
|
||||
|
||||
const out = machine.getOutput();
|
||||
assert.ok('state' in out);
|
||||
assert.ok('mode' in out);
|
||||
assert.ok('ctrl' in out);
|
||||
});
|
||||
|
||||
test('constructor handles missing curve model without throwing', () => {
|
||||
const cfg = makeMachineConfig({ asset: { supplier: 'x', category: 'machine', type: 'pump', model: 'not-existing-model', unit: 'm3/h' } });
|
||||
const machine = new Machine(cfg, makeStateConfig());
|
||||
|
||||
assert.equal(machine.hasCurve, false);
|
||||
assert.equal(machine.predictFlow, null);
|
||||
assert.equal(machine.predictPower, null);
|
||||
assert.equal(machine.predictCtrl, null);
|
||||
|
||||
const out = machine.getOutput();
|
||||
assert.ok('state' in out);
|
||||
});
|
||||
35
test/basic/mode-and-input.basic.test.js
Normal file
35
test/basic/mode-and-input.basic.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const Machine = require('../../src/specificClass');
|
||||
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||
|
||||
test('setMode changes mode only for allowed values', () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
const original = machine.currentMode;
|
||||
|
||||
machine.setMode('virtualControl');
|
||||
assert.equal(machine.currentMode, 'virtualControl');
|
||||
|
||||
machine.setMode('invalid-mode');
|
||||
assert.equal(machine.currentMode, 'virtualControl');
|
||||
assert.notEqual(machine.currentMode, original);
|
||||
});
|
||||
|
||||
test('handleInput rejects non-string action safely', async () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
await assert.doesNotReject(async () => {
|
||||
await machine.handleInput('GUI', 123, null);
|
||||
});
|
||||
});
|
||||
|
||||
test('handleInput ignores disallowed source/action combination', async () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
machine.setMode('fysicalControl');
|
||||
|
||||
const before = machine.state.getCurrentState();
|
||||
await machine.handleInput('GUI', 'execSequence', 'startup');
|
||||
const after = machine.state.getCurrentState();
|
||||
|
||||
assert.equal(before, after);
|
||||
});
|
||||
31
test/edge/error-paths.edge.test.js
Normal file
31
test/edge/error-paths.edge.test.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const Machine = require('../../src/specificClass');
|
||||
const NodeClass = require('../../src/nodeClass');
|
||||
const { makeMachineConfig, makeStateConfig, makeNodeStub } = require('../helpers/factories');
|
||||
|
||||
test('setpoint rejects negative inputs without throwing', async () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||
await assert.doesNotReject(async () => {
|
||||
await machine.setpoint(-1);
|
||||
});
|
||||
});
|
||||
|
||||
test('nodeClass _updateNodeStatus returns error status on internal failure', () => {
|
||||
const inst = Object.create(NodeClass.prototype);
|
||||
const node = makeNodeStub();
|
||||
inst.node = node;
|
||||
inst.source = {
|
||||
currentMode: 'auto',
|
||||
state: {
|
||||
getCurrentState() {
|
||||
throw new Error('boom');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const status = inst._updateNodeStatus();
|
||||
assert.equal(status.text, 'Status Error');
|
||||
assert.equal(node._errors.length, 1);
|
||||
});
|
||||
60
test/edge/nodeClass-routing.edge.test.js
Normal file
60
test/edge/nodeClass-routing.edge.test.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const NodeClass = require('../../src/nodeClass');
|
||||
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
|
||||
|
||||
test('input handler routes topics to source methods', () => {
|
||||
const inst = Object.create(NodeClass.prototype);
|
||||
const node = makeNodeStub();
|
||||
|
||||
const calls = [];
|
||||
inst.node = node;
|
||||
inst.RED = makeREDStub({
|
||||
child1: {
|
||||
source: { id: 'child-source' },
|
||||
},
|
||||
});
|
||||
|
||||
inst.source = {
|
||||
childRegistrationUtils: {
|
||||
registerChild(childSource, pos) {
|
||||
calls.push(['registerChild', childSource, pos]);
|
||||
},
|
||||
},
|
||||
setMode(mode) {
|
||||
calls.push(['setMode', mode]);
|
||||
},
|
||||
handleInput(source, action, parameter) {
|
||||
calls.push(['handleInput', source, action, parameter]);
|
||||
},
|
||||
showWorkingCurves() {
|
||||
return { ok: true };
|
||||
},
|
||||
showCoG() {
|
||||
return { cog: 1 };
|
||||
},
|
||||
updateMeasuredPressure(value, position) {
|
||||
calls.push(['updateMeasuredPressure', value, position]);
|
||||
},
|
||||
updateMeasuredFlow(value, position) {
|
||||
calls.push(['updateMeasuredFlow', value, position]);
|
||||
},
|
||||
updateMeasuredTemperature(value, position) {
|
||||
calls.push(['updateMeasuredTemperature', value, position]);
|
||||
},
|
||||
};
|
||||
|
||||
inst._attachInputHandler();
|
||||
const onInput = node._handlers.input;
|
||||
|
||||
onInput({ topic: 'setMode', payload: 'auto' }, () => {}, () => {});
|
||||
onInput({ topic: 'execSequence', payload: { source: 'GUI', action: 'execSequence', parameter: 'startup' } }, () => {}, () => {});
|
||||
onInput({ topic: 'registerChild', payload: 'child1', positionVsParent: 'downstream' }, () => {}, () => {});
|
||||
onInput({ topic: 'simulateMeasurement', payload: { type: 'pressure', position: 'upstream', value: 250, unit: 'mbar' } }, () => {}, () => {});
|
||||
|
||||
assert.deepEqual(calls[0], ['setMode', 'auto']);
|
||||
assert.deepEqual(calls[1], ['handleInput', 'GUI', 'execSequence', 'startup']);
|
||||
assert.deepEqual(calls[2], ['registerChild', { id: 'child-source' }, 'downstream']);
|
||||
assert.deepEqual(calls[3], ['updateMeasuredPressure', 250, 'upstream']);
|
||||
});
|
||||
110
test/helpers/factories.js
Normal file
110
test/helpers/factories.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const { MeasurementContainer } = require('generalFunctions');
|
||||
|
||||
function makeMachineConfig(overrides = {}) {
|
||||
return {
|
||||
general: {
|
||||
id: 'rm-test-1',
|
||||
name: 'rotating-machine-test',
|
||||
unit: 'm3/h',
|
||||
logging: { enabled: false, logLevel: 'error' },
|
||||
},
|
||||
functionality: {
|
||||
positionVsParent: 'atEquipment',
|
||||
},
|
||||
asset: {
|
||||
supplier: 'hidrostal',
|
||||
category: 'machine',
|
||||
type: 'pump',
|
||||
model: 'hidrostal-H05K-S03R',
|
||||
unit: 'm3/h',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeStateConfig(overrides = {}) {
|
||||
return {
|
||||
general: {
|
||||
logging: { enabled: false, logLevel: 'error' },
|
||||
},
|
||||
state: {
|
||||
current: 'idle',
|
||||
},
|
||||
movement: {
|
||||
mode: 'staticspeed',
|
||||
speed: 1000,
|
||||
maxSpeed: 1000,
|
||||
interval: 10,
|
||||
},
|
||||
time: {
|
||||
starting: 0,
|
||||
warmingup: 0,
|
||||
stopping: 0,
|
||||
coolingdown: 0,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeChildMeasurement({ id = 'child-1', name = 'PT-1', positionVsParent = 'downstream', type = 'pressure', unit = 'mbar' } = {}) {
|
||||
const measurements = new MeasurementContainer({
|
||||
autoConvert: true,
|
||||
defaultUnits: {
|
||||
pressure: 'mbar',
|
||||
flow: 'm3/h',
|
||||
temperature: 'C',
|
||||
power: 'kW',
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
config: {
|
||||
general: { id, name },
|
||||
functionality: { positionVsParent },
|
||||
asset: { type, unit },
|
||||
},
|
||||
measurements,
|
||||
};
|
||||
}
|
||||
|
||||
function makeNodeStub() {
|
||||
const handlers = {};
|
||||
const sent = [];
|
||||
const statuses = [];
|
||||
const errors = [];
|
||||
const warns = [];
|
||||
return {
|
||||
id: 'node-1',
|
||||
source: null,
|
||||
send(msg) { sent.push(msg); },
|
||||
status(s) { statuses.push(s); },
|
||||
error(e) { errors.push(e); },
|
||||
warn(w) { warns.push(w); },
|
||||
on(event, cb) { handlers[event] = cb; },
|
||||
_handlers: handlers,
|
||||
_sent: sent,
|
||||
_statuses: statuses,
|
||||
_errors: errors,
|
||||
_warns: warns,
|
||||
};
|
||||
}
|
||||
|
||||
function makeREDStub(nodeMap = {}) {
|
||||
return {
|
||||
nodes: {
|
||||
getNode(id) {
|
||||
return nodeMap[id] || null;
|
||||
},
|
||||
createNode() {},
|
||||
registerType() {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makeMachineConfig,
|
||||
makeStateConfig,
|
||||
makeChildMeasurement,
|
||||
makeNodeStub,
|
||||
makeREDStub,
|
||||
};
|
||||
22
test/integration/coolprop.integration.test.js
Normal file
22
test/integration/coolprop.integration.test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const Machine = require('../../src/specificClass');
|
||||
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||
|
||||
test('calcEfficiency runs through coolprop path without mocks', () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||
|
||||
machine.measurements.type('pressure').variant('measured').position('downstream').value(1200, Date.now(), 'mbar');
|
||||
machine.measurements.type('pressure').variant('measured').position('upstream').value(800, Date.now(), 'mbar');
|
||||
machine.measurements.type('flow').variant('predicted').position('atEquipment').value(120, Date.now(), 'm3/h');
|
||||
machine.measurements.type('power').variant('predicted').position('atEquipment').value(12, Date.now(), 'kW');
|
||||
|
||||
assert.doesNotThrow(() => {
|
||||
machine.calcEfficiency(12, 120, 'predicted');
|
||||
});
|
||||
|
||||
const eff = machine.measurements.type('efficiency').variant('predicted').position('atEquipment').getCurrentValue();
|
||||
assert.equal(typeof eff, 'number');
|
||||
assert.ok(eff > 0);
|
||||
});
|
||||
27
test/integration/registration.integration.test.js
Normal file
27
test/integration/registration.integration.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const Machine = require('../../src/specificClass');
|
||||
const { makeMachineConfig, makeStateConfig, makeChildMeasurement } = require('../helpers/factories');
|
||||
|
||||
test('registerChild listens to measurement events and stores measured pressure', async () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
const child = makeChildMeasurement({ positionVsParent: 'downstream', type: 'pressure', unit: 'mbar' });
|
||||
|
||||
machine.registerChild(child, 'measurement');
|
||||
|
||||
child.measurements
|
||||
.type('pressure')
|
||||
.variant('measured')
|
||||
.position('downstream')
|
||||
.value(123, Date.now(), 'mbar');
|
||||
|
||||
const stored = machine.measurements
|
||||
.type('pressure')
|
||||
.variant('measured')
|
||||
.position('downstream')
|
||||
.getCurrentValue('mbar');
|
||||
|
||||
assert.equal(typeof stored, 'number');
|
||||
assert.equal(Math.round(stored), 123);
|
||||
});
|
||||
22
test/integration/sequences.integration.test.js
Normal file
22
test/integration/sequences.integration.test.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const Machine = require('../../src/specificClass');
|
||||
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
|
||||
|
||||
test('execSequence startup reaches operational with zero transition times', async () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig());
|
||||
|
||||
await machine.handleInput('parent', 'execSequence', 'startup');
|
||||
|
||||
assert.equal(machine.state.getCurrentState(), 'operational');
|
||||
});
|
||||
|
||||
test('execMovement updates controller position in operational state', async () => {
|
||||
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
|
||||
|
||||
await machine.handleInput('parent', 'execMovement', 10);
|
||||
|
||||
const pos = machine.state.getCurrentPosition();
|
||||
assert.ok(pos >= 9.9 && pos <= 10);
|
||||
});
|
||||
Reference in New Issue
Block a user