diff --git a/doc/source/V2023.rst b/doc/source/V2023.rst index 860f2a5aa431f267364213afb19abb0fdca5054b..8cd2d46fa911adbe1f656b4b68e7cd81c1664771 100644 --- a/doc/source/V2023.rst +++ b/doc/source/V2023.rst @@ -246,7 +246,7 @@ An example of MQTT broker that can be used is `Mosquitto <https://mosquitto.org/ MQTT messages include logging messages from the OhmPi and commands sent to the OhmPi. These messages can be examined easily using a third party software such as `MQTT Explorer <http://mqtt-explorer.com/>`_. -Commands send on the broker are received by the ohmpi.py script that runs on the OhmPi (make sure ohmpi.py starts on reboot) and further processed. +Commands sent on the broker are received by the ohmpi.py script that runs on the OhmPi (make sure ohmpi.py starts on reboot) and further processed. MQTT commands are sent in JSON format following the Python API with kwargs as illustrated below: .. code-block:: json @@ -302,10 +302,21 @@ MQTT commands are sent in JSON format following the Python API with kwargs as il Custom processing of messages and tailor-made dashboards for monitoring experiments may be designed using a browser-based flow editor such as `Node-red <http://mqtt-explorer.com/>`_. This may help designing complex IoT experiments and monitoring systems in which OhmPi is a component. -An example of a simple flow incorporating execution commands and data outputs from OhmPi can be found in the OhmPi examples. +Examples incorporating execution commands and data outputs from OhmPi can be found in the OhmPi examples. Once Node-RED is installed on the OhmPi, these examples can be accessed separately by running a command in the console such as : + +.. code-block:: console +node-red basic_ohmpi_flows_node-red.json + +These examples may require installing some additional node packages in order to work properly. This can be done in the `Palette Manager <https://nodered.org/docs/user-guide/editor/palette/manager> within Node-RED. .. figure:: img/node-red-flow.png Example flow in node-red to interact with an OhmPi. + +.. figure:: img/node-red-interface-ui.png + + Example of a dashboard UI created with node-red to interact with an OhmPi. + + For more documentation dedicated to node-red, please refer to the Node-red `cookbooks <https://cookbook.nodered.org/>`_. diff --git a/examples/node-red_interface.json b/examples/node-red_interface.json new file mode 100644 index 0000000000000000000000000000000000000000..43a51692f06e9c0d98d6b4111662e65f46b6f81b --- /dev/null +++ b/examples/node-red_interface.json @@ -0,0 +1,2411 @@ +[ + { + "id": "b19c51e9d4d25a33", + "type": "tab", + "label": "Flow 1", + "disabled": false, + "info": "" + }, + { + "id": "c3dbd6595b0256a9", + "type": "subflow", + "name": "Send_commands", + "info": "", + "category": "", + "in": [], + "out": [ + { + "x": 1140, + "y": 240, + "wires": [ + { + "id": "71f435fecd185ab7", + "port": 0 + } + ] + } + ], + "env": [], + "meta": {}, + "color": "#DDAA99" + }, + { + "id": "1506d9deaa5de120", + "type": "subflow", + "name": "Update_settings", + "info": "", + "category": "", + "in": [], + "out": [ + { + "x": 1120, + "y": 120, + "wires": [ + { + "id": "1278358184e5b189", + "port": 0 + } + ] + } + ], + "env": [], + "meta": {}, + "color": "#DDAA99" + }, + { + "id": "4489e1d51c9522f3", + "type": "subflow", + "name": "Data_plots", + "info": "", + "category": "", + "in": [ + { + "x": 80, + "y": 300, + "wires": [ + { + "id": "9465b94aca687c9f" + }, + { + "id": "cc84bb5476128ec6" + }, + { + "id": "5bab870fe804aae8" + }, + { + "id": "f655ae35cc8d0036" + } + ] + } + ], + "out": [], + "env": [], + "meta": {}, + "color": "#DDAA99" + }, + { + "id": "b6b050428bd78f42", + "type": "subflow", + "name": "Data_table", + "info": "", + "category": "", + "in": [ + { + "x": 50, + "y": 30, + "wires": [ + { + "id": "61a6e472b87b237a" + } + ] + } + ], + "out": [], + "env": [], + "meta": {}, + "color": "#DDAA99" + }, + { + "id": "2924702c.b33a7", + "type": "subflow", + "name": "ui-table handler", + "info": "# ui-table handler\nUniversal handler for ui-table.\n## features\n- buffer table data\n- add or update individual rows or cells of the table\n- delete rows\n- clear tableData\n- handle column width\n- handle column order\n- hide und unhide columns\n- hide and unhide rows\n- records row order\n- support for nested columns [(column groups)](http://tabulator.info/examples/4.7#column-groups)\n- support for child rows (_children) [(nested data trees)](http://tabulator.info/examples/4.7#tree)\n\nFor real life example see:\n\n**syslog server** for logfile like table with filters\n\n**remote device table** for dynamically updated table with context menues\n\n**irrigation system** for sortable rows\n\n## sending data to ui-tabel\n\n- sending an `array` as discribed in ui-table will replace the complete table and delete all table edits\n \n if `msg.keepEdits=true` is added the existing edits will be kept.\n- send an `object` containing the updated properties of a table row by sending msg.<tableDataProp>.\n\n The table is updated using the `updateOrAddData` command. You can alter the command used by adding the `msg.tabulatorCommand` and `msg.tabulatorParameter`\n\n```\nmsg.tabulatorCommand=\"addData\";\nmsg.tabulatorParameter=[true];\n```\n## configuration\n- `tabulator` json formatted object containing configuration of the table. See ui-table for more details.\n- `property` property of the msg object that contains the data to be passed to ui-table. I.e. *state* `msg.state`\n- `index` index column to identify individual rows. Each message containing data must have a unique `msg.topic` to identify the row. Messages without this `msg.topic` will be droped. It is not nessesary but possible to display the index column in the table. Do not enable editing on this column otherwise you will loose the connection and another row will be added to the table as soon as a new message arrives!\n\n Defaults to *$topic* `msg.state.$topic`\n- `maxRows`maximum number of rows held by table widget. If grater than **0** the amount of rows in ui-table is limited. For this to work the index row must be a Number. ´rows < currentID-maxRows´ will be deleted.\n- `maxStore`maximum number of rows stored by this node for replay if a client connects. If grater than **0** the amount of rows in flow context is limited. for this to work the index row must be a Number. ´rows < currentID-maxStore´ will be deleted.\n- `dashboard` name of the dashboard tab to only update the table if the dashboard is visible. If empty the table will be updated on every tab change and connect.\n- `context` configuration of context data. The subflow will save or cache data in the flows context using `$parent.`. \n **tableData** caches the incoming data to restore it on `ui-control´ *change* messages.\n **tableConfig** saves column width and order to save the interactive table layot\n **tableEdit** saves edits on the table data otherwise it would be overwritten when new data arrives\n```json\n{\n \"tableData\": {\n \"name\": \"tableData\"\n },\n \"tableConfig\": {\n \"name\": \"tableConfig\",\n \"storage\": \"file\"\n },\n \"tableEdit\": {\n \"name\": \"tableEdit\",\n \"storage\": \"file\"\n }\n}\n```\n\n## commands\ncommands can be passed by sending a object as `msg.payload`\n\n```json\n{\n \"command\": \"delete\",\n \"object\": \"columnOrder\"\n}\n```\n\n- `deleteTable` tableCache\n- `deleteRow` delete single row. `object` matching index property\n- `ignoreRow` delete single row and put it on an ignore list. `object` matching index property\n- `unIgnoreRow`remove row from the ignore list. `object` matching index property\n- `unIgnoreRows`delte the ignore list. \n- `deleteRowOrder` delete custom row order\n- `deleteColumnOrder` delete custom column order\n This is important if you add or delete columns in the tabulator config otherwise the columns most likely don`t show up\n- `deleteColumnWidth` delete custom column width\n- `columnHide` hide a column. `object` matching column field\n- `columnUnHide` unhide a column. `object` matching column field\n- `columnsUnHide` unhide all hidden columns.\n- `setMaxStore` set maximum amount of rows in cache\n- `setMaxDisplay` set maximum amout of rows in ui-table\n- `getTable` get table data (as displayed) as an array (on 2nd output)\n \n## background\nui-table warps the powerfull tabluator library. This subflow makes it easier to unleash the powerfull features of ui-table", + "category": "dashboard", + "in": [ + { + "x": 54, + "y": 85, + "wires": [ + { + "id": "5eb0bd6b.74b794" + } + ] + } + ], + "out": [ + { + "x": 360, + "y": 85, + "wires": [ + { + "id": "5eb0bd6b.74b794", + "port": 1 + } + ] + }, + { + "x": 360, + "y": 136, + "wires": [ + { + "id": "5eb0bd6b.74b794", + "port": 2 + } + ] + } + ], + "env": [ + { + "name": "tabulator", + "type": "json", + "value": "{\"tabulator\":{\"responsiveLayout\":\"collapse\",\"responsiveLayoutCollapseStartOpen\":false,\"index\":\"$name\",\"layout\":\"fitColumns\",\"movableColumns\":true,\"groupBy\":\"\",\"columnResized\":\"function(column){ var newColumn = { field: column._column.field, visible: column._column.visible, width: column._column.width, widthFixed: column._column.widthFixed, widthStyled: column._column.widthStyled }; this.send({topic:this.config.topic,ui_control:{callback:'columnResized',columnWidths:newColumn}}); }\",\"columnMoved\":\"function(column, columns){ var newColumns=[]; columns.forEach(function (column) { newColumns.push({'field': column._column.definition.field, 'title': column._column.definition.title}); }); this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }\",\"rowFormatter\":\"function(row){ var data = row.getData(); switch (data.$state) { case \\\"lost\\\": row.getElement().style.backgroundColor = \\\"#9e2e66\\\"; row.getElement().style.color = \\\"#a6a6a6\\\"; break; case \\\"sleeping\\\": row.getElement().style.backgroundColor = \\\"#336699\\\"; break; case \\\"disconnected\\\": row.getElement().style.backgroundColor = \\\"#cc3300\\\"; row.getElement().style.color = \\\"#a6a6a6\\\"; break; case \\\"alert\\\": row.getElement().style.backgroundColor = \\\"#A6A6DF\\\"; break; case \\\"init\\\": row.getElement().style.backgroundColor = \\\"#f2f20d\\\"; break; case \\\"ready\\\": row.getElement().style.backgroundColor = \\\"\\\"; row.getElement().style.color = \\\"\\\"; break; } }\",\"columns\":[{\"formatter\":\"responsiveCollapse\",\"width\":30,\"minWidth\":30,\"align\":\"center\",\"resizable\":false,\"headerSort\":false,\"frozen\":true,\"title\":\"expand\",\"field\":\"expand\",\"headerVertical\":\"flip\"},{\"formatter\":\"function(cell, formatterParams, onRendered) { var html = cell.getValue(); return html; }\",\"title\":\"State\",\"field\":\"$stateIcon\",\"width\":100,\"frozen\":true,\"headerVertical\":\"flip\"},{\"formatter\":\"function(cell, formatterParams, onRendered) { var html = cell.getValue(); return html; }\",\"title\":\"Signal\",\"field\":\"signalIcon\",\"width\":100,\"frozen\":true,\"headerVertical\":\"flip\"},{\"title\":\"Name\",\"field\":\"$name\",\"width\":100,\"frozen\":true,\"headerVertical\":\"flip\"},{\"title\":\"State\",\"field\":\"$state\",\"width\":100,\"align\":\"center\",\"headerVertical\":\"flip\"},{\"title\":\"last-ready\",\"field\":\"lastSeenreadyFormatted\",\"width\":100,\"align\":\"left\",\"headerVertical\":\"flip\"},{\"title\":\"Homie\",\"field\":\"$homie\",\"width\":100,\"align\":\"left\",\"headerVertical\":\"flip\"},{\"title\":\"Platform\",\"field\":\"$implementation\",\"width\":100,\"align\":\"left\",\"headerVertical\":\"flip\"},{\"title\":\"Statistics\",\"columns\":[{\"title\":\"Interval\",\"field\":\"interval\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"outputFormat\":\"d hh:mm:ss\",\"inputFormat\":\"seconds\",\"invalidPlaceholder\":\"(unknown)\"},\"title\":\"Uptime\",\"field\":\"uptime\",\"formatter\":\"function(cell, formatterParams, onRendered){ var pad = function (num) { return (\\\"0\\\"+num).slice(-2); }; var secs = Number(cell.getValue()); if (Number.isNaN(secs)) return; var minutes = Math.floor(secs / 60); secs = secs%60; var hours = Math.floor(minutes/60); minutes = minutes%60; var days = Math.floor(hours/24); hours = hours%24; if (days>0) return days+\\\"d \\\"+pad(hours)+\\\":\\\"+pad(minutes); else return pad(hours)+\\\":\\\"+pad(minutes)+\\\":\\\"+pad(secs); }\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":0,\"max\":100,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) {if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" %</span>\\\"; else return; }\",\"legendColor\":\"#FFFFFF\",\"legendAlign\":\"center\"},\"title\":\"Signal\",\"field\":\"signal\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":2.5,\"max\":3.5,\"color\":[\"red\",\"green\",\"red\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" V</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"Supply\",\"field\":\"supply\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":0,\"max\":100,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" %</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"Battery\",\"field\":\"battery\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":0,\"max\":100000,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+(value/1024).toFixed(2)+\\\" kB</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"Memory\",\"field\":\"freeheap\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"target\":\"_blank\",\"min\":0,\"max\":100,\"color\":[\"red\",\"orange\",\"green\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" %</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"CPU load\",\"field\":\"cpuload\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"},{\"formatterParams\":{\"min\":20,\"max\":60,\"color\":[\"green\",\"orange\",\"red\"],\"legend\":\"function (value) { if (value>0) return \\\"<span style='color:#FFFFFF;'>\\\"+value+\\\" °C</span>\\\"; else return; }\",\"legendColor\":\"#101010\",\"legendAlign\":\"center\"},\"title\":\"CPU temp\",\"field\":\"cputemp\",\"formatter\":\"progress\",\"width\":100,\"headerVertical\":\"flip\"}]},{\"title\":\"Firmware\",\"columns\":[{\"formatter\":\"link\",\"formatterParams\":{\"labelField\":\"$localip\",\"urlPrefix\":\"http://\",\"target\":\"_blank\"},\"title\":\"IP\",\"field\":\"$localip\",\"width\":100},{\"title\":\"mac\",\"field\":\"$mac\",\"width\":100},{\"title\":\"Accsess Point\",\"field\":\"SSID\",\"width\":100},{\"title\":\"Firmware\",\"field\":\"name\",\"width\":100},{\"title\":\"Version\",\"field\":\"version\",\"width\":100},{\"title\":\"Last Boot Cause\",\"field\":\"lastBootCause\",\"width\":100},{\"title\":\"Reset Reason\",\"field\":\"resetReason\",\"width\":100}]}]},\"customHeight\":12}", + "ui": { + "icon": "font-awesome/fa-table", + "label": { + "en-US": "Tabulator" + }, + "type": "input", + "opts": { + "types": [ + "json", + "env" + ] + } + } + }, + { + "name": "tableDataProp", + "type": "str", + "value": "row", + "ui": { + "icon": "font-awesome/fa-tag", + "label": { + "en-US": "rowProperty" + } + } + }, + { + "name": "tableIndex", + "type": "str", + "value": "$topic", + "ui": { + "icon": "font-awesome/fa-indent", + "label": { + "en-US": "Index" + }, + "type": "input", + "opts": { + "types": [ + "str", + "json", + "env" + ] + } + } + }, + { + "name": "maxRows", + "type": "num", + "value": "0", + "ui": { + "icon": "font-awesome/fa-list-ol", + "type": "input", + "opts": { + "types": [ + "num", + "bool", + "env" + ] + } + } + }, + { + "name": "maxStore", + "type": "num", + "value": "0", + "ui": { + "icon": "font-awesome/fa-database", + "type": "input", + "opts": { + "types": [ + "num", + "env" + ] + } + } + }, + { + "name": "dashboard", + "type": "str", + "value": "Remote Device Table", + "ui": { + "icon": "font-awesome/fa-dashboard", + "label": { + "en-US": "Dashboard" + }, + "type": "input", + "opts": { + "types": [ + "str", + "env" + ] + } + } + }, + { + "name": "tableContext", + "type": "json", + "value": "{\"tableData\":{\"name\":\"tableData\"},\"tableConfig\":{\"name\":\"tableConfig\",\"storage\":\"file\"},\"tableEdit\":{\"name\":\"tableEdit\",\"storage\":\"file\"}}", + "ui": { + "icon": "font-awesome/fa-database", + "label": { + "en-US": "Context" + }, + "type": "input", + "opts": { + "types": [ + "json", + "env" + ] + } + } + } + ], + "color": "#3FADB5", + "icon": "node-red-dashboard/ui_slider.png", + "status": { + "x": 360, + "y": 34, + "wires": [ + { + "id": "5eb0bd6b.74b794", + "port": 0 + } + ] + } + }, + { + "id": "142ad6ae.d55e29", + "type": "ui_group", + "name": "Controller", + "tab": "5d888f29.07334", + "order": 1, + "disp": true, + "width": 8, + "collapse": false, + "className": "" + }, + { + "id": "5d888f29.07334", + "type": "ui_tab", + "name": "Control", + "icon": "dashboard", + "order": 1, + "disabled": false, + "hidden": false + }, + { + "id": "ea73b76b.ee0738", + "type": "ui_base", + "theme": { + "name": "theme-dark", + "lightTheme": { + "default": "#0094CE", + "baseColor": "#0094CE", + "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif", + "edited": true, + "reset": false + }, + "darkTheme": { + "default": "#097479", + "baseColor": "#097479", + "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif", + "edited": true, + "reset": false + }, + "customTheme": { + "name": "Untitled Theme 1", + "default": "#4B7930", + "baseColor": "#4B7930", + "baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif" + }, + "themeState": { + "base-color": { + "default": "#097479", + "value": "#097479", + "edited": true + }, + "page-titlebar-backgroundColor": { + "value": "#097479", + "edited": false + }, + "page-backgroundColor": { + "value": "#111111", + "edited": false + }, + "page-sidebar-backgroundColor": { + "value": "#333333", + "edited": false + }, + "group-textColor": { + "value": "#0eb8c0", + "edited": false + }, + "group-borderColor": { + "value": "#555555", + "edited": false + }, + "group-backgroundColor": { + "value": "#333333", + "edited": false + }, + "widget-textColor": { + "value": "#eeeeee", + "edited": false + }, + "widget-backgroundColor": { + "value": "#097479", + "edited": false + }, + "widget-borderColor": { + "value": "#333333", + "edited": false + }, + "base-font": { + "value": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif" + } + }, + "angularTheme": { + "primary": "indigo", + "accents": "blue", + "warn": "red", + "background": "grey", + "palette": "light" + } + }, + "site": { + "name": "OhmPi Node-RED interface", + "hideToolbar": "false", + "allowSwipe": "true", + "lockMenu": "true", + "allowTempTheme": "true", + "dateFormat": "DD/MM/YYYY", + "sizes": { + "sx": 48, + "sy": 48, + "gx": 10, + "gy": 10, + "cx": 6, + "cy": 6, + "px": 1, + "py": 1 + } + } + }, + { + "id": "5a09c1ee2a3419c3", + "type": "mqtt-broker", + "name": "ohmpi_local", + "broker": "127.0.0.1", + "port": "1883", + "clientid": "", + "autoConnect": true, + "usetls": false, + "protocolVersion": "4", + "keepalive": "60", + "cleansession": true, + "birthTopic": "", + "birthQos": "0", + "birthPayload": "", + "birthMsg": {}, + "closeTopic": "", + "closeQos": "0", + "closePayload": "", + "closeMsg": {}, + "willTopic": "", + "willQos": "0", + "willPayload": "", + "willMsg": {}, + "userProps": "", + "sessionExpiry": "" + }, + { + "id": "7792ecc419ecbb59", + "type": "ui_group", + "name": "Latest reading", + "tab": "32170516a52092f8", + "order": 2, + "disp": true, + "width": "6", + "collapse": true, + "className": "" + }, + { + "id": "b0990b3c5ff3c09a", + "type": "ui_group", + "name": "Messages", + "tab": "5d888f29.07334", + "order": 3, + "disp": true, + "width": 16, + "collapse": true, + "className": "" + }, + { + "id": "32170516a52092f8", + "type": "ui_tab", + "name": "Data", + "icon": "show_chart", + "order": 2, + "disabled": false, + "hidden": false + }, + { + "id": "d1631b3685c8e6de", + "type": "ui_link", + "name": "OhmPi Documentation", + "link": "https://reversaal.gitlab.irstea.page/OhmPi/", + "icon": "help", + "target": "newtab", + "order": 4 + }, + { + "id": "df2cbd8c9c9960e1", + "type": "ui_group", + "name": "Sequence", + "tab": "32170516a52092f8", + "order": 2, + "disp": true, + "width": 20, + "collapse": false, + "className": "" + }, + { + "id": "6e40c7571e22771c", + "type": "ui_group", + "name": "Readings", + "tab": "5d888f29.07334", + "order": 3, + "disp": true, + "width": "24", + "collapse": false, + "className": "" + }, + { + "id": "2ea9e102cdb2b8bc", + "type": "ui_spacer", + "z": "b19c51e9d4d25a33", + "name": "spacer", + "group": "142ad6ae.d55e29", + "order": 1, + "width": 2, + "height": 1 + }, + { + "id": "9726d2db53670a2f", + "type": "ui_spacer", + "z": "b19c51e9d4d25a33", + "name": "spacer", + "group": "142ad6ae.d55e29", + "order": 4, + "width": 2, + "height": 1 + }, + { + "id": "8604d2895bcb450c", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 1, + "width": 4, + "height": 1 + }, + { + "id": "7043dcaa8b77a1ee", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 3, + "width": 4, + "height": 1 + }, + { + "id": "a97edf86754fb6d6", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 4, + "width": 4, + "height": 1 + }, + { + "id": "20893cc8683fd811", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 5, + "width": 4, + "height": 1 + }, + { + "id": "f1e2260223be3173", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 6, + "width": 4, + "height": 1 + }, + { + "id": "efb0652208c0e313", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 7, + "width": 4, + "height": 1 + }, + { + "id": "3f9ed5d50127d447", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 8, + "width": 4, + "height": 1 + }, + { + "id": "a3ccc2e4e8da4bf9", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 9, + "width": 4, + "height": 1 + }, + { + "id": "a186e41072c92e53", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 10, + "width": 4, + "height": 1 + }, + { + "id": "94601de256407651", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 11, + "width": 4, + "height": 1 + }, + { + "id": "e3212dc1f098fb0e", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 12, + "width": 4, + "height": 1 + }, + { + "id": "6e9c8fe4a83d80bb", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 13, + "width": 4, + "height": 1 + }, + { + "id": "fce3dd5095e58052", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 14, + "width": 8, + "height": 1 + }, + { + "id": "8ca07c19d93e4907", + "type": "ui_spacer", + "z": "b6b050428bd78f42", + "name": "spacer", + "group": "df2cbd8c9c9960e1", + "order": 16, + "width": 8, + "height": 1 + }, + { + "id": "bd7e9f8b229fdb1a", + "type": "ui_link", + "name": "HTTP interface", + "link": "http://localhost:8080", + "icon": "open_in_browser", + "target": "newtab", + "order": 3 + }, + { + "id": "71f435fecd185ab7", + "type": "join", + "z": "c3dbd6595b0256a9", + "name": "", + "mode": "custom", + "build": "merged", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "accumulate": true, + "timeout": "", + "count": "3", + "reduceRight": false, + "reduceExp": "", + "reduceInit": "", + "reduceInitType": "", + "reduceFixup": "", + "x": 970, + "y": 240, + "wires": [ + [] + ] + }, + { + "id": "5eb07cfc6ea7e7e4", + "type": "change", + "z": "c3dbd6595b0256a9", + "name": "", + "rules": [ + { + "t": "set", + "p": "complete", + "pt": "msg", + "to": "true", + "tot": "bool" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 710, + "y": 240, + "wires": [ + [ + "71f435fecd185ab7" + ] + ] + }, + { + "id": "87336107859f0f4c", + "type": "delay", + "z": "c3dbd6595b0256a9", + "name": "", + "pauseType": "delay", + "timeout": "250", + "timeoutUnits": "milliseconds", + "rate": "1", + "nbRateUnits": "1", + "rateUnits": "second", + "randomFirst": "1", + "randomLast": "5", + "randomUnits": "seconds", + "drop": false, + "allowrate": false, + "outputs": 1, + "x": 510, + "y": 240, + "wires": [ + [ + "5eb07cfc6ea7e7e4" + ] + ] + }, + { + "id": "ef7cd86d21809032", + "type": "function", + "z": "c3dbd6595b0256a9", + "name": "set cmd_id", + "func": "var newMsg = { payload: {\"cmd_id\": msg.payload }};\nreturn newMsg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 610, + "y": 200, + "wires": [ + [ + "71f435fecd185ab7" + ] + ] + }, + { + "id": "028e5762e321dce2", + "type": "uuid", + "z": "c3dbd6595b0256a9", + "uuidVersion": "v1", + "namespaceType": "", + "namespace": "", + "namespaceCustom": "", + "name": "", + "field": "payload", + "fieldType": "msg", + "x": 430, + "y": 200, + "wires": [ + [ + "ef7cd86d21809032" + ] + ] + }, + { + "id": "23f549ae6ee82d23", + "type": "function", + "z": "c3dbd6595b0256a9", + "name": "get cmd", + "func": "return global.get(\"command_tmp\");", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 600, + "y": 280, + "wires": [ + [ + "71f435fecd185ab7" + ] + ] + }, + { + "id": "3027158d54f370fd", + "type": "function", + "z": "c3dbd6595b0256a9", + "name": "get kwargs", + "func": "return global.get(\"kwargs\");", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 610, + "y": 320, + "wires": [ + [ + "71f435fecd185ab7" + ] + ] + }, + { + "id": "2d858a81f7e7f2b3", + "type": "ui_button", + "z": "c3dbd6595b0256a9", + "name": "", + "group": "142ad6ae.d55e29", + "order": 8, + "width": 0, + "height": 0, + "passthru": false, + "label": "Send command", + "tooltip": "", + "color": "", + "bgcolor": "", + "className": "", + "icon": "", + "payload": "", + "payloadType": "str", + "topic": "topic", + "topicType": "msg", + "x": 200, + "y": 240, + "wires": [ + [ + "028e5762e321dce2", + "87336107859f0f4c", + "23f549ae6ee82d23", + "3027158d54f370fd" + ] + ] + }, + { + "id": "de9e803de4d28986", + "type": "ui_dropdown", + "z": "c3dbd6595b0256a9", + "name": "", + "label": "Command", + "tooltip": "", + "place": "Select option", + "group": "142ad6ae.d55e29", + "order": 6, + "width": 0, + "height": 0, + "passthru": true, + "multiple": false, + "options": [ + { + "label": "", + "value": "load_sequence", + "type": "str" + }, + { + "label": "", + "value": "reset_mux", + "type": "str" + }, + { + "label": "", + "value": "set_sequence", + "type": "str" + }, + { + "label": "", + "value": "update_settings", + "type": "str" + } + ], + "payload": "", + "topic": "command", + "topicType": "str", + "className": "", + "x": 190, + "y": 400, + "wires": [ + [ + "68222c7c6633d3fb" + ] + ] + }, + { + "id": "17f76c1517cb0285", + "type": "ui_text_input", + "z": "c3dbd6595b0256a9", + "name": "", + "label": "kwargs", + "tooltip": "", + "group": "142ad6ae.d55e29", + "order": 7, + "width": 0, + "height": 0, + "passthru": true, + "mode": "text", + "delay": "250", + "topic": "kwargs", + "sendOnBlur": false, + "className": "", + "topicType": "str", + "x": 179, + "y": 457, + "wires": [ + [ + "c25811ae6ee89f2d" + ] + ] + }, + { + "id": "c25811ae6ee89f2d", + "type": "json", + "z": "c3dbd6595b0256a9", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 329, + "y": 457, + "wires": [ + [ + "071930f0cabf3d94" + ] + ] + }, + { + "id": "68222c7c6633d3fb", + "type": "function", + "z": "c3dbd6595b0256a9", + "name": "set cmd", + "func": "var newMsg = { payload: {\"cmd\": msg.payload }};\nglobal.set(\"command_tmp\",newMsg)\nreturn global.get(\"command_tmp\");", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 400, + "y": 400, + "wires": [ + [] + ] + }, + { + "id": "071930f0cabf3d94", + "type": "function", + "z": "c3dbd6595b0256a9", + "name": "set kwargs", + "func": "var newMsg = { payload: {\"kwargs\": msg.payload }};\nglobal.set(\"kwargs\", newMsg)\nreturn global.get(\"kwargs\");", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 489, + "y": 457, + "wires": [ + [] + ] + }, + { + "id": "19e8f5b5b199d998", + "type": "ui_text", + "z": "c3dbd6595b0256a9", + "group": "142ad6ae.d55e29", + "order": 5, + "width": 0, + "height": 0, + "name": "", + "label": "<font size = 4.5>Manual command", + "format": "{{msg.payload}}", + "layout": "row-left", + "className": "", + "x": 260, + "y": 340, + "wires": [] + }, + { + "id": "1278358184e5b189", + "type": "join", + "z": "1506d9deaa5de120", + "name": "", + "mode": "custom", + "build": "merged", + "property": "payload", + "propertyType": "msg", + "key": "topic", + "joiner": "\\n", + "joinerType": "str", + "accumulate": true, + "timeout": "", + "count": "3", + "reduceRight": false, + "reduceExp": "", + "reduceInit": "", + "reduceInitType": "", + "reduceFixup": "", + "x": 970, + "y": 120, + "wires": [ + [] + ] + }, + { + "id": "5183385a4d4e6cce", + "type": "change", + "z": "1506d9deaa5de120", + "name": "", + "rules": [ + { + "t": "set", + "p": "complete", + "pt": "msg", + "to": "true", + "tot": "bool" + } + ], + "action": "", + "property": "", + "from": "", + "to": "", + "reg": false, + "x": 710, + "y": 120, + "wires": [ + [ + "1278358184e5b189" + ] + ] + }, + { + "id": "a91742bfc4517ad5", + "type": "delay", + "z": "1506d9deaa5de120", + "name": "", + "pauseType": "delay", + "timeout": "250", + "timeoutUnits": "milliseconds", + "rate": "1", + "nbRateUnits": "1", + "rateUnits": "second", + "randomFirst": "1", + "randomLast": "5", + "randomUnits": "seconds", + "drop": false, + "allowrate": false, + "outputs": 1, + "x": 510, + "y": 120, + "wires": [ + [ + "5183385a4d4e6cce" + ] + ] + }, + { + "id": "43a86ef16bc90f95", + "type": "function", + "z": "1506d9deaa5de120", + "name": "set cmd_id", + "func": "var newMsg = { payload: {\"cmd_id\": msg.payload }};\nreturn newMsg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 610, + "y": 80, + "wires": [ + [ + "1278358184e5b189" + ] + ] + }, + { + "id": "8d984843fded4a0b", + "type": "uuid", + "z": "1506d9deaa5de120", + "uuidVersion": "v1", + "namespaceType": "", + "namespace": "", + "namespaceCustom": "", + "name": "", + "field": "payload", + "fieldType": "msg", + "x": 430, + "y": 80, + "wires": [ + [ + "43a86ef16bc90f95" + ] + ] + }, + { + "id": "414271cceb77591d", + "type": "function", + "z": "1506d9deaa5de120", + "name": "set kwargs", + "func": "return global.get(\"kwargs_tmp\");", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 630, + "y": 200, + "wires": [ + [ + "1278358184e5b189" + ] + ] + }, + { + "id": "d1ba46c1d65348db", + "type": "ui_form", + "z": "1506d9deaa5de120", + "name": "", + "label": "Settings", + "group": "142ad6ae.d55e29", + "order": 9, + "width": 0, + "height": 0, + "options": [ + { + "label": "Nb electrodes", + "value": "nb_electrodes", + "type": "number", + "required": false, + "rows": null + }, + { + "label": "Injection duration", + "value": "injection_duration", + "type": "number", + "required": false, + "rows": null + }, + { + "label": "Nb stack", + "value": "nb_stack", + "type": "number", + "required": false, + "rows": null + } + ], + "formValue": { + "nb_electrodes": "", + "injection_duration": "", + "nb_stack": "" + }, + "payload": "", + "submit": "Update settings", + "cancel": "cancel", + "topic": "topic", + "topicType": "msg", + "splitLayout": "", + "className": "", + "x": 180, + "y": 540, + "wires": [ + [ + "8d984843fded4a0b", + "40e18df2a4f6f7bc", + "7c3f483dc0a97154", + "92a5062165281e27", + "f260d8e69fe63dbb" + ] + ] + }, + { + "id": "a28983bae6d15cb6", + "type": "function", + "z": "1506d9deaa5de120", + "name": "set kwargs", + "func": "var settings = JSON.parse((msg.payload));\nfor (var key in settings) {\n if (settings[key] == null){\n delete settings[key]}\n }\n\nvar newMsg = {\n payload: {\n \"kwargs\": { \"settings\": settings}}}\nglobal.set(\"kwargs_tmp\", newMsg)\nreturn global.get(\"kwargs_tmp\");", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 530, + "y": 540, + "wires": [ + [] + ] + }, + { + "id": "65d9c7d700116da5", + "type": "function", + "z": "1506d9deaa5de120", + "name": "set cmd", + "func": "return { payload: { \"cmd\": \"update_settings\" } };", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 600, + "y": 160, + "wires": [ + [ + "1278358184e5b189" + ] + ] + }, + { + "id": "40e18df2a4f6f7bc", + "type": "function", + "z": "1506d9deaa5de120", + "name": "blank", + "func": "return { payload: { } };", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 350, + "y": 240, + "wires": [ + [ + "a91742bfc4517ad5" + ] + ] + }, + { + "id": "7c3f483dc0a97154", + "type": "delay", + "z": "1506d9deaa5de120", + "name": "", + "pauseType": "delay", + "timeout": "250", + "timeoutUnits": "milliseconds", + "rate": "1", + "nbRateUnits": "1", + "rateUnits": "second", + "randomFirst": "1", + "randomLast": "5", + "randomUnits": "seconds", + "drop": false, + "allowrate": false, + "outputs": 1, + "x": 510, + "y": 400, + "wires": [ + [ + "414271cceb77591d" + ] + ] + }, + { + "id": "92a5062165281e27", + "type": "delay", + "z": "1506d9deaa5de120", + "name": "", + "pauseType": "delay", + "timeout": "250", + "timeoutUnits": "milliseconds", + "rate": "1", + "nbRateUnits": "1", + "rateUnits": "second", + "randomFirst": "1", + "randomLast": "5", + "randomUnits": "seconds", + "drop": false, + "allowrate": false, + "outputs": 1, + "x": 410, + "y": 320, + "wires": [ + [ + "65d9c7d700116da5" + ] + ] + }, + { + "id": "f260d8e69fe63dbb", + "type": "json", + "z": "1506d9deaa5de120", + "name": "", + "property": "payload", + "action": "", + "pretty": false, + "x": 350, + "y": 540, + "wires": [ + [ + "a28983bae6d15cb6" + ] + ] + }, + { + "id": "63021563a27e162b", + "type": "ui_chart", + "z": "4489e1d51c9522f3", + "name": "", + "group": "df2cbd8c9c9960e1", + "order": 2, + "width": 12, + "height": 6, + "label": "Measured resistances chart", + "chartType": "line", + "legend": "true", + "xformat": "HH:mm:ss", + "interpolate": "linear", + "nodata": "", + "dot": true, + "ymin": "", + "ymax": "", + "removeOlder": 1, + "removeOlderPoints": "", + "removeOlderUnit": "3600", + "cutout": 0, + "useOneColor": false, + "useUTC": false, + "colors": [ + "#1f77b4", + "#aec7e8", + "#ff7f0e", + "#2ca02c", + "#98df8a", + "#d62728", + "#ff9896", + "#9467bd", + "#c5b0d5" + ], + "outputs": 1, + "useDifferentColor": false, + "className": "", + "x": 660, + "y": 340, + "wires": [ + [] + ] + }, + { + "id": "f655ae35cc8d0036", + "type": "function", + "z": "4489e1d51c9522f3", + "name": "function 3", + "func": "var msg2 = { payload: JSON.parse(msg.payload.split(' | ')[2].slice(6,).split(`'`).join(`\"`))[\"R [ohm]\"] };\nreturn msg2;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 440, + "y": 380, + "wires": [ + [ + "d891753fb13281c2", + "63021563a27e162b" + ] + ] + }, + { + "id": "d891753fb13281c2", + "type": "debug", + "z": "4489e1d51c9522f3", + "name": "debug 6", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 600, + "y": 400, + "wires": [] + }, + { + "id": "8b35e61a7f7a37f8", + "type": "ui_gauge", + "z": "4489e1d51c9522f3", + "name": "", + "group": "7792ecc419ecbb59", + "order": 1, + "width": 6, + "height": 4, + "gtype": "gage", + "title": "Latest measured resistance", + "label": "R [Ohm]", + "format": "{{value}}", + "min": 0, + "max": "50", + "colors": [ + "#00b500", + "#e6e600", + "#ca3838" + ], + "seg1": "", + "seg2": "", + "className": "", + "x": 660, + "y": 100, + "wires": [] + }, + { + "id": "9465b94aca687c9f", + "type": "function", + "z": "4489e1d51c9522f3", + "name": "function 4", + "func": "var msg2 = { payload: JSON.parse(msg.payload.split(' | ')[2].slice(6,).split(`'`).join(`\"`))[\"R [ohm]\"] };\nreturn msg2;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 440, + "y": 100, + "wires": [ + [ + "8b35e61a7f7a37f8", + "763ea2c282b8b81a" + ] + ] + }, + { + "id": "763ea2c282b8b81a", + "type": "debug", + "z": "4489e1d51c9522f3", + "name": "debug 7", + "active": false, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "false", + "statusVal": "", + "statusType": "auto", + "x": 460, + "y": 40, + "wires": [] + }, + { + "id": "57fd4568a66aac99", + "type": "ui_gauge", + "z": "4489e1d51c9522f3", + "name": "", + "group": "7792ecc419ecbb59", + "order": 2, + "width": 6, + "height": 4, + "gtype": "gage", + "title": "Latest measured voltage", + "label": "R [Ohm]", + "format": "{{value}}", + "min": -1500, + "max": "1500", + "colors": [ + "#00b500", + "#e6e600", + "#ca3838" + ], + "seg1": "", + "seg2": "", + "className": "", + "x": 650, + "y": 160, + "wires": [] + }, + { + "id": "cc84bb5476128ec6", + "type": "function", + "z": "4489e1d51c9522f3", + "name": "function 5", + "func": "var msg2 = { payload: JSON.parse(msg.payload.split(' | ')[2].slice(6,).split(`'`).join(`\"`))[\"Vmn [mV]\"] };\nreturn msg2;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 440, + "y": 160, + "wires": [ + [ + "57fd4568a66aac99" + ] + ] + }, + { + "id": "035c20e764a1f057", + "type": "ui_gauge", + "z": "4489e1d51c9522f3", + "name": "", + "group": "7792ecc419ecbb59", + "order": 3, + "width": 6, + "height": 4, + "gtype": "gage", + "title": "Latest applied current", + "label": "R [Ohm]", + "format": "{{value}}", + "min": 0, + "max": "50", + "colors": [ + "#00b500", + "#e6e600", + "#ca3838" + ], + "seg1": "", + "seg2": "", + "className": "", + "x": 640, + "y": 220, + "wires": [] + }, + { + "id": "5bab870fe804aae8", + "type": "function", + "z": "4489e1d51c9522f3", + "name": "function 6", + "func": "var msg2 = { payload: JSON.parse(msg.payload.split(' | ')[2].slice(6,).split(`'`).join(`\"`))[\"I [mA]\"] };\nreturn msg2;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 440, + "y": 220, + "wires": [ + [ + "035c20e764a1f057" + ] + ] + }, + { + "id": "e0f21bb5ada6c4d7", + "type": "function", + "z": "b6b050428bd78f42", + "name": "Add data", + "func": "msg.payload = {\n command: \"addData\", \n arguments: [\n [msg.payload],\n true\n ],\n returnPromise: true\n }\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 660, + "y": 80, + "wires": [ + [ + "3d18b988d090ebf5" + ] + ] + }, + { + "id": "81a5a8e6.5418f8", + "type": "function", + "z": "b6b050428bd78f42", + "name": "store data", + "func": "let tabledata = flow.get('tabledata2') || [];\ntabledata.push(msg.payload);\nflow.set('tabledata2',tabledata);\n\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 400, + "y": 80, + "wires": [ + [ + "e0f21bb5ada6c4d7" + ] + ], + "icon": "node-red/db.svg" + }, + { + "id": "e873e03a03791394", + "type": "ui_ui_control", + "z": "b6b050428bd78f42", + "name": "", + "events": "change", + "x": 400, + "y": 160, + "wires": [ + [ + "6870f5124586ee5f" + ] + ] + }, + { + "id": "6870f5124586ee5f", + "type": "function", + "z": "b6b050428bd78f42", + "name": "get data", + "func": "msg.payload = flow.get('tabledata2') || [];\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 570, + "y": 160, + "wires": [ + [ + "3d18b988d090ebf5" + ] + ], + "icon": "node-red/db.svg" + }, + { + "id": "61a6e472b87b237a", + "type": "function", + "z": "b6b050428bd78f42", + "name": "data", + "func": "let counter = context.get('counter') || 0;\ncounter++;\ncontext.set('counter', counter);\n\nvar data = JSON.parse(\n msg.payload.split(' | ')[2].slice(6,).split(`'`).join(`\"`));\n\n//data[\"id\"] = counter\n\nfor (var key in data) {\n data[key] = data[key].toString()\n};\nmsg.payload = data\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 190, + "y": 80, + "wires": [ + [ + "81a5a8e6.5418f8" + ] + ] + }, + { + "id": "3d18b988d090ebf5", + "type": "ui_table", + "z": "b6b050428bd78f42", + "group": "df2cbd8c9c9960e1", + "name": "Latest readings", + "order": 18, + "width": 20, + "height": 11, + "columns": [ + { + "field": "time", + "title": "Time", + "width": "20%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "A", + "title": "A", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "B", + "title": "B", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "M", + "title": "M", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "N", + "title": "N", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "R [ohm]", + "title": "R [ohm]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Vmn [mV]", + "title": "Vmn [mV]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "I [mA]", + "title": "I [mA]", + "width": "8%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Ps [mV]", + "title": "Ps [mV]", + "width": "8%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "inj time [ms]", + "title": "Injection<br>Time [ms]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "nbStack", + "title": "Nb<br>Stack", + "width": "5%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Tx [V]", + "title": "Tx [V]", + "width": "4%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "CPU temp [degC]", + "title": "CPU<br>Temp [degC]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Nb samples [-]", + "title": "Nb<br>Samples", + "width": "3%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "cmd_id", + "title": "Cmd<br>ID", + "width": "6%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + } + ], + "outputs": 1, + "cts": true, + "x": 900, + "y": 120, + "wires": [ + [] + ] + }, + { + "id": "43717a0158183d95", + "type": "ui_text", + "z": "b6b050428bd78f42", + "group": "df2cbd8c9c9960e1", + "order": 15, + "width": 4, + "height": 1, + "name": "", + "label": "<font size = 4.5>Latest readings", + "format": "{{msg.payload}}", + "layout": "row-left", + "className": "", + "x": 170, + "y": 160, + "wires": [] + }, + { + "id": "f67644bcd94bee9b", + "type": "ui_button", + "z": "b6b050428bd78f42", + "name": "", + "group": "7792ecc419ecbb59", + "order": 7, + "width": 0, + "height": 0, + "passthru": false, + "label": "clear", + "tooltip": "clear all table data", + "color": "", + "bgcolor": "", + "className": "", + "icon": "", + "payload": "", + "payloadType": "date", + "topic": "", + "topicType": "str", + "x": 390, + "y": 220, + "wires": [ + [ + "ef121340aa158d36" + ] + ] + }, + { + "id": "ef121340aa158d36", + "type": "function", + "z": "b6b050428bd78f42", + "name": "clearData", + "func": "\nmsg.payload={\n command:\"clearData\",\n arguments: [],\n returnPromise: true\n}\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 621, + "y": 220, + "wires": [ + [ + "3d18b988d090ebf5" + ] + ], + "info": "# clear data\n\nunfortunately this command (currently) do not send a promise back! So we have to pass it directly to the table handler" + }, + { + "id": "5eb0bd6b.74b794", + "type": "function", + "z": "2924702c.b33a7", + "name": "handle tableData", + "func": "var status = {fill:\"red\",shape:\"dot\",text: \"payload \"};\nvar tableIndex = env.get(\"tableIndex\") || \"$topic\";\nvar tableDataProp = env.get(\"tableDataProp\") || \"row\";\nvar tableContext = env.get(\"tableContext\");\nvar dashboard = env.get(\"dashboard\");\nvar maxRows = env.get(\"maxRows\") || 0;\nvar maxStore = env.get(\"maxStore\") || 0;\n\nif (!tableContext.hasOwnProperty(\"tableData\") || !tableContext.hasOwnProperty(\"tableConfig\")) {\n status.text=\"tableContext not defined\";\n node.error(status.text);\n return [{payload:status},null];\n}\n\n// context store to cache table data (memoryOnly prefered)\nvar tableData = flow.get(\"$parent.\"+tableContext.tableData.name,tableContext.tableData.storage);\nif (tableData===undefined) {\n node.warn(\"[ui-table handler] tableData initialized!\");\n tableData={};\n flow.set(\"$parent.\"+tableContext.tableData.name,tableData,tableContext.tableData.storage);\n}\n\n// context Store to save table configuration (file)\nvar tableConfig = flow.get(\"$parent.\"+tableContext.tableConfig.name,tableContext.tableConfig.storage);\nif (tableConfig===undefined) {\n node.warn(\"[ui-table handler] tableConfig initialized!\");\n tableConfig={ResponsiveLayout:true};\n flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n}\n\nif (tableConfig.hasOwnProperty(\"maxStore\")) maxStore=tableConfig.maxStore;\nif (tableConfig.hasOwnProperty(\"maxRows\")) maxRows=tableConfig.maxRows;\n\n// context Store to save table configuration (file)\nvar tableEdit;\nif (tableContext.hasOwnProperty(\"tableEdit\")) {\n tableEdit = flow.get(\"$parent.\"+tableContext.tableEdit.name,tableContext.tableEdit.storage);\n if (tableEdit===undefined) {\n node.warn(\"[ui-table handler] tableEdit initialized!\");\n tableEdit={};\n flow.set(\"$parent.\"+tableContext.tableEdit.name,tableEdit,tableContext.tableEdit.storage);\n }\n}\n\n// function to merge partial data into existing table row\nvar mergeObject = function (destination, source, filter) {\n for (let currentSource in source) {\n if (source.hasOwnProperty(currentSource)) {\n if (filter!==undefined && tableEdit && tableEdit.hasOwnProperty(filter) && tableEdit[filter].hasOwnProperty(currentSource)) {\n destination[currentSource]= tableEdit[filter][currentSource];\n source[currentSource]=tableEdit[filter][currentSource];\n } else {\n destination[currentSource]= source[currentSource];\n }\n } \n }\n return source;\n};\n\n// merge edits into a destination object respecting _children\nvar mergeEdits = function(destination) {\n \n var mergeChildEdits = function(children) {\n children.forEach(child => {\n if (child.hasOwnProperty(tableIndex) && tableEdit.hasOwnProperty(child[tableIndex])) {\n// node.warn([\"mergeChild\",child])\n Object.keys(tableEdit[child[tableIndex]]).forEach(edit => {\n if (child.hasOwnProperty(edit)) {\n child[edit]=tableEdit[child[tableIndex]][edit];\n// node.warn([\"mergeChild edit \",edit,child[edit]])\n }\n });\n }\n if (child.hasOwnProperty(\"_children\")) {\n mergeChildEdits(child._children);\n }\n })\n }\n \n\n Object.keys(destination).forEach(row => {\n if (destination[row].hasOwnProperty(tableIndex)) {\n if (tableEdit.hasOwnProperty(row)) {\n Object.keys(tableEdit[row]).forEach(edit => {\n destination[row][edit]=tableEdit[row][edit];\n });\n }\n if (destination[row].hasOwnProperty(\"_children\")) {\n mergeChildEdits(destination[row]._children);\n }\n }\n });\n}\n\n// deep search for a column including nested columns\nvar searchTabulatorColumn = function (columns,key,match) {\n var result;\n for (let column of columns) {\n if (column.hasOwnProperty(\"columns\")) {\n result = searchTabulatorColumn(column.columns,key,match);\n if (result!==undefined) return result;\n } else if (column.hasOwnProperty(key) && column[key]===match) {\n return column;\n }\n }\n};\n\n// command message to update add or update data on ui-table\nvar msgToTable={};\nmsgToTable.payload={\n \"command\":msg.tabulatorCommand || \"updateOrAddData\",\n \"arguments\": [],\n \"returnPromise\": false\n};\n\n// store data in tableData\nif (msg.hasOwnProperty(tableDataProp)) {\n // store data for later recover\n if (!msg.hasOwnProperty(\"topic\")) { // check if index existst\n status.text=\"msg.topic not defined!\";\n return [{payload:status},null];\n }\n if (!tableData.hasOwnProperty(msg.topic)){ // first seen\n if (maxRows>0 && Object.keys(tableData).lenght===0) {\n tableConfig.currentFirstRow=msg.topic;\n }\n tableData[msg.topic]={};\n if (tableEdit && tableEdit.hasOwnProperty(msg.topic)) { // table edits available!\n Object.keys(tableEdit[msg.topic]).forEach((key) => {\n msg[tableDataProp][key]=tableEdit[msg.topic][key];\n tableData[msg.topic][key]=tableEdit[msg.topic][key];\n })\n }\n if (maxStore>0 && typeof msg.topic === \"number\") { // limit rows in tableData\n let rowKeys = Object.keys(tableData);\n if (rowKeys.length>maxStore) {\n for (let i=0; i<(rowKeys.length-maxStore); i++) {\n delete tableData[rowKeys[i]];\n }\n }\n }\n }\n if (!tableData[msg.topic].hasOwnProperty(tableIndex)) tableData[msg.topic][tableIndex]=msg.topic;\n msg[tableDataProp]=mergeObject(tableData[msg.topic],msg[tableDataProp],msg.topic);\n msg[tableDataProp][tableIndex]=msg.topic;\n msgToTable.payload.arguments=[[msg[tableDataProp]]];\n // add aditional parameters\n if (msg.hasOwnProperty(\"tabulatorParameter\") && Array.isArray(msg.tabulatorParameter)) {\n for (let arg in msg.tabulatorParameter) msgToTable.payload.arguments.push(arg);\n }\n // delete rows if rows exceed maxRows\n /*\n if (maxRows>0 && tableConfig.hasOwnProperty(\"currentFirstRow\") && typeof tableData[msg.topic][tableIndex]===\"number\") {\n //node.warn([maxRows,tableConfig.hasOwnProperty(\"currentFirstRow\"),typeof tableData[msg.topic][tableIndex],tableConfig.currentFirstRow,tableData[msg.topic][tableIndex]-maxRows])\n if (tableConfig.currentFirstRow<tableData[msg.topic][tableIndex]-maxRows) {\n node.warn([\"maxRowExeeded\",tableConfig.currentFirstRow]);\n node.send([null,{payload:{\"command\":\"deleteRow\",\"arguments\": [tableConfig.currentFirstRow],\"returnPromise\": false}},null]);\n tableConfig.currentFirstRow++;\n }\n }*/\n if (maxRows>0 && typeof tableData[msg.topic][tableIndex]===\"number\" && msg.topic-maxRows>0) {\n node.send([null,{payload:{\"command\":\"deleteRow\",\"arguments\": [msg.topic-maxRows],\"returnPromise\": false}},null]);\n }\n status.fill=\"green\";\n status.text=msg.topic+\" updated\";\n return [{payload:status},msgToTable,null];\n} if (msg.payload===\"connect\" || (msg.payload===\"change\" && msg.name===dashboard) || (msg.hasOwnProperty(\"payload\") && msg.payload.hasOwnProperty(\"command\"))) { \n if (!msg.hasOwnProperty(\"ui_control\")) {\n msg.ui_control = env.get('tabulator');\n status.text+=\" ui_control added\";\n }\n //process commands\n //node.warn({\"command\":msg.payload.command,\"msg\":msg,\"object\":msg.payload.object})\n if (msg.payload.hasOwnProperty(\"command\") && msg.payload.command!=='getTable') {\n status.fill=\"blue\";\n switch(msg.payload.command) {\n case 'deleteTable':\n flow.set(\"$parent.\"+tableContext.tableData.name,undefined,tableContext.tableData.storage);\n tableData={};\n status.text=\"tabledata deleted\";\n node.warn(\"[ui-table handler] \"+\"tabledata deleted\");\n break;\n case 'deleteRow':\n case 'deleteDevice':\n var deleteRow = function(id) {\n // check if row is in root\n if (tableData.hasOwnProperty(id)) {\n delete tableData[id]\n return true;\n }\n // check if row is a child\n let deleteChildRow = function(children, id) {\n for(let i = 0; i < children.length; i++){\n if (children[i].hasOwnProperty(tableIndex) && children[i][tableIndex]===id) {\n children.splice(i, 1); \n return true; \n }\n if (children[i].hasOwnProperty(\"_children\")) {\n if (deleteChildRow(children[i]._children,id)) {\n if (children[i]._children.length === 0) {\n delete children[i]._children;\n }\n return true;\n }\n }\n }\n return false;\n };\n \n for (let row in tableData) {\n if (tableData[row].hasOwnProperty(\"_children\")) {\n if (deleteChildRow(tableData[row]._children,id)) return true;\n }\n }\n return false;\n }\n \n if (deleteRow(msg.payload.object)) {\n status.text=msg.payload.object+\" deleted\";\n } else {\n status.fill=\"yellow\";\n status.text=msg.payload.object+\" undefined\";\n }\n break;\n case 'ignoreRow':\n case 'ignoreDevice':\n if (tableData.hasOwnProperty(msg.payload.object)) {\n delete tableData[msg.payload.object];\n status.text=msg.payload.object+\" will be ignored\";\n if (!tableConfig.hasOwnProperty('ignoreDevice')) tableConfig.ignoreDevice={};\n tableConfig.ignoreDevice[msg.payload.object]=true;\n }\n break;\n case 'unIgnoreRow':\n case 'unIgnoreDevice':\n if (tableConfig.hasOwnProperty('ignoreDevice')) {\n delete tableConfig.ignoreDevice[msg.payload.object];\n }\n break;\n case 'unIgnoreRows':\n case 'unIgnoreDevices':\n delete tableConfig.ignoreDevice;\n break;\n case 'updateData':\n status.text=\"column \"+msg.payload.column+\" updated\";\n delete msg.ui_control;\n return [{payload:status},msg];\n case 'updateTable':\n status.text=msg.payload.command+\": \";\n break;\n case 'columnHide':\n if (!tableConfig.hasOwnProperty('columnVisible')) tableConfig.columnVisible={};\n tableConfig.columnVisible[msg.payload.object]=false;\n break;\n case 'columnUnHide':\n if (!tableConfig.hasOwnProperty('columnVisible')) tableConfig.columnVisible={};\n tableConfig.columnVisible[msg.payload.object]=true;\n break;\n case 'columnsUnHide':\n for (let column in tableConfig.columnVisible) {\n if (tableConfig.columnVisible.hasOwnProperty(column)) tableConfig.columnVisible[column]=true;\n }\n break;\n case 'refreshTable':\n break;\n case 'deleteColumnOrder':\n case 'restoreColumnOrder':\n delete tableConfig.columns;\n break;\n case 'deleteColumnWidth':\n case 'resetColumnWidth':\n delete tableConfig.columnWidths;\n break;\n case 'setResponsiveLayout':\n tableConfig.ResponsiveLayout=!tableConfig.ResponsiveLayout;\n break;\n case 'deleteRowOrder':\n delete tableConfig.rowOrder;\n break;\n case 'setMaxStore':\n tableConfig.maxStore=msg.payload.object;\n maxStore=msg.payload.object;\n break;\n case 'setMaxRows':\n tableConfig.maxRows=msg.payload.object;\n maxRows=msg.payload.object;\n break;\n default:\n status.fill=\"red\";\n status.text=\"unknown command \"+msg.payload.command;\n node.warn(\"[ui-table handler] \"+status.text);\n break;\n }\n flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n node.send([{payload:status},null,null]);\n }\n\n // crawl through tabulator arrays and updated user defined values\n var crawlTabulator = function (columns,match,config,property) {\n for (let column of columns) {\n if (column.hasOwnProperty(\"columns\")) {\n crawlTabulator(column.columns,match,config,property);\n } else if (config.hasOwnProperty(column[match])) column[property]=config[column.field];\n }\n };\n \n // restore custom column width\n if (tableConfig.hasOwnProperty(\"columnWidths\") && msg.hasOwnProperty(\"ui_control\")) {\n crawlTabulator(msg.ui_control.tabulator.columns,\"field\",tableConfig.columnWidths,\"width\");\n }\n \n // restore custom column hide/show\n if (tableConfig.hasOwnProperty(\"columnVisible\") && msg.hasOwnProperty(\"ui_control\")) {\n crawlTabulator(msg.ui_control.tabulator.columns,\"field\",tableConfig.columnVisible,\"visible\");\n }\n \n // restore custom responsive / standard view\n if (tableConfig.hasOwnProperty(\"ResponsiveLayout\")) {\n if (!tableConfig.ResponsiveLayout) {\n msg.ui_control.tabulator.responsiveLayout=false;\n }\n msg.ui_control.tabulator.columns.forEach((column,index) => {\n if (column.formatter===\"responsiveCollapse\") { // hide expand column on any position\n column.visible=tableConfig.ResponsiveLayout;\n return;\n }\n });\n }\n\n // sort columns\n if (tableConfig.hasOwnProperty(\"columns\") && msg.hasOwnProperty(\"ui_control\") && msg.ui_control.hasOwnProperty(\"tabulator\")) {\n var addedColumns = 0;\n var sortColumnsByLayout = function (sortColumns, columnsLayout, targetColumns) {\n for (var layoutColumn=0; layoutColumn<columnsLayout.length; layoutColumn++) {\n for (var sortColumn in sortColumns) {\n if (sortColumns[sortColumn].hasOwnProperty(\"columns\")) {\n targetColumns.push({\"title\":sortColumns[sortColumn].title, \"columns\":[]});\n sortColumnsByLayout(sortColumns[sortColumn].columns,columnsLayout,targetColumns[targetColumns.length-1].columns);\n layoutColumn=addedColumns; // jump forward after childes added\n } else {\n if (columnsLayout[layoutColumn].field===sortColumns[sortColumn].field){\n targetColumns.push(sortColumns[sortColumn]);\n addedColumns++;\n break;\n }\n }\n }\n }\n }; \n var newColumns=[];\n sortColumnsByLayout(msg.ui_control.tabulator.columns,tableConfig.columns,newColumns);\n msg.ui_control.tabulator.columns=newColumns;\n }\n\n // restore stored lines after connect\n\n let command = msg.payload.command;\n var tableArray;\n if (command===\"getTable\") {\n msg.payload.tableArray=[];\n tableArray=msg.payload.tableArray\n } else {\n msg.payload=[];\n tableArray=msg.payload;\n }\n \n var pushRowData = function(rowData) {\n // ignore rows in ignoreRows array\n if (tableConfig && tableConfig.hasOwnProperty(\"ignoreDevice\") && tableConfig.ignoreDevice[rowData]) {\n // do nothing\n } else {\n // merge edits into table\n if (tableEdit && tableEdit.hasOwnProperty(rowData)) {\n let tableRow = RED.util.cloneMessage(tableData[rowData]);\n Object.keys(tableEdit[rowData]).forEach((field) => {\n tableRow[field]=tableEdit[rowData][field];\n });\n tableArray.push(tableRow);\n } else {\n tableArray.push(tableData[rowData]);\n }\n }\n }\n \n if (tableConfig.hasOwnProperty(\"rowOrder\")) {\n // first check if new rows exits which are not in rowOrder\n Object.keys(tableData).forEach((key) => {\n if (tableConfig.rowOrder.indexOf(tableData[key][tableIndex])<0) {\n tableConfig.rowOrder.push(tableData[key][tableIndex]); // add row to the end of rowOrder\n }\n });\n tableConfig.rowOrder.forEach((value,index) => {\n node.warn([\"pushRowOrder\",value,tableData.hasOwnProperty(value),tableData[value]]);\n if (tableData.hasOwnProperty(value)) { // push rows in rowOrder sequence\n pushRowData(value);\n } else { // delete not existing rows from rowOrder\n tableConfig.rowOrder.splice(index,1)\n }\n });\n } else {\n for (let rowData in tableData) {\n pushRowData(rowData);\n }\n }\n // store the first index if maxRows limits amount of displayed lines\n if (maxRows>0 && tableData) {\n let tableKeys=Object.keys(tableData);\n if (tableKeys.length>0 && typeof tableData[tableKeys[0]][tableIndex] === \"number\") {\n tableConfig.currentFirstRow=tableData[tableKeys[0]][tableIndex];\n }\n }\n \n if (command=='getTable'){\n status.fill=\"blue\";\n status.text+=\" \"+tableArray.length+\" rows emitted\";\n return [{payload:status},null,msg];\n } else {\n status.fill=\"blue\";\n status.text+=\" \"+tableArray.length+\" rows restored\";\n return [{payload:status},msg,[{topic:\"maxRows\",payload:maxRows},{topic:\"maxStore\",payload:maxStore}]];\n }\n} if (msg.hasOwnProperty(\"ui_control\")) {\n // callback from tabulator\n status.fill=\"blue\";\n status.text=\"callback \"+msg.ui_control.callback;\n switch(msg.ui_control.callback) {\n case \"columnResized\": // save new column width\n if (tableConfig.columnWidths===undefined) tableConfig.columnWidths={};\n tableConfig.columnWidths[msg.ui_control.columnWidths.field]=msg.ui_control.columnWidths.width;\n flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n status.text=msg.ui_control.columnWidths.field+\"=\"+msg.ui_control.columnWidths.width+\"px\";\n break;\n case \"columnMoved\": // save new column order\n if (tableConfig.columns===undefined) tableConfig.columns=[];\n tableConfig.columns=msg.ui_control.columns;\n flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n status.text=\"new column order\";\n break;\n case \"cellEdited\":\n if (tableEdit) {\n if (!tableEdit.hasOwnProperty(msg[tableIndex])) tableEdit[msg[tableIndex]]={};\n tableEdit[msg[tableIndex]][msg.field] = msg.payload; // save data and mark as edited field\n flow.set(\"$parent.\"+tableContext.tableEdit.name,tableEdit,tableContext.tableEdit.storage);\n mergeEdits(tableData);\n flow.set(\"$parent.\"+tableContext.tableData.name,tableData,tableContext.tableData.storage);\n status.text=msg[tableIndex]+\" \"+msg.field+\" edited to \"+msg.payload;\n msg[tableDataProp]={};\n msg[tableDataProp][tableIndex]=msg[tableIndex];\n msg[tableDataProp][msg.field]=msg.payload;\n msgToTable.payload.arguments=[[msg[tableDataProp]]];\n node.send([{payload:status},null,msg]); // was node.send([{payload:status},msgToTable,msg]);\n } else {\n node.error(\"[ui-table handler] no tableEdit store defined!\")\n }\n break;\n case \"rowContext\":\n msg.ignoredDevices=[];\n for (let rowData in tableConfig.ignoreDevice) {\n if (tableConfig.ignoreDevice.hasOwnProperty(rowData)) {\n msg.ignoredDevices.push({\"text\":rowData,\"icon\":\"fa fa-plug\",\"topic\":\"unIgnoreDevice\",\"payload\":rowData}) \n }\n }\n break;\n case \"headerContext\":\n msg.hiddenColumns=[];\n let tabulatorConfig = env.get('tabulator');\n for (let column in tableConfig.columnVisible) {\n if (tableConfig.columnVisible.hasOwnProperty(column) &&\n !tableConfig.columnVisible[column]) {\n let configColumn=searchTabulatorColumn(tabulatorConfig.tabulator.columns,\"field\",column);\n let icon;\n if (configColumn.hasOwnProperty('title') && configColumn.title.toLowerCase().includes('</i>')) {\n // <i class='fa fa-star-half-o'></i> State\n let start=configColumn.title.indexOf(\"'fa \");\n let end=configColumn.title.indexOf(\"'\",start+1);\n icon=configColumn.title.substring(start+4,end);\n }\n msg.hiddenColumns.push({\"text\":column,\"icon\":icon,\"topic\":\"columnUnHide\",\"payload\":configColumn.field}) \n }\n }\n break;\n case \"rowMoved\":\n if (tableConfig.rowOrder===undefined) tableConfig.rowOrder={};\n tableConfig.rowOrder=msg.ui_control.rowOrder;\n flow.set(\"$parent.\"+tableContext.tableConfig.name,tableConfig,tableContext.tableConfig.storage);\n status.text=\"new row order\";\n break;\n default:\n // if rowIndex exists pass complete object\n if (msg.hasOwnProperty(tableIndex)) {\n msg.rowData=tableData[msg[tableIndex]];\n }\n status.text=\"pass message\";\n }\n return [{payload:status},null,msg];\n} \nif (Array.isArray(msg.payload)) {\n tableData={};\n \n msg.payload.forEach((row) => {\n if (row.hasOwnProperty(tableIndex)) {\n tableData[row[tableIndex]]=row;\n }\n });\n if (msg.keepEdits) {\n mergeEdits(tableData);\n }\n\n \n flow.set(\"$parent.\"+tableContext.tableData.name,tableData,tableContext.tableData.storage);\n if (tableContext.hasOwnProperty(\"tableEdit\") && !msg.keepEdits) {\n tableEdit={};\n flow.set(\"$parent.\"+tableContext.tableEdit.name,tableEdit,tableContext.tableEdit.storage);\n }\n status.fill=\"blue\"\n status.text=\"table replaced \"+msg.payload.length+\" rows\";\n return [{payload:status},msg,null];\n} \n \n// nothing to do bejond this point\nstatus.text+=\" [\"+msg.payload+\"]\";\nreturn [{payload:status},null];\n", + "outputs": 3, + "noerr": 0, + "initialize": "", + "finalize": "", + "x": 192, + "y": 85, + "wires": [ + [], + [], + [] + ], + "icon": "font-awesome/fa-table" + }, + { + "id": "0f23781293c4b819", + "type": "mqtt in", + "z": "b19c51e9d4d25a33", + "name": "", + "topic": "ohmpi_0001/exec", + "qos": "2", + "datatype": "auto-detect", + "broker": "6ae7e77e.04c64", + "nl": false, + "rap": false, + "inputs": 0, + "x": 330, + "y": 40, + "wires": [ + [ + "f4b6096e60252b62" + ] + ] + }, + { + "id": "36ba500fac1d0f38", + "type": "mqtt in", + "z": "b19c51e9d4d25a33", + "name": "", + "topic": "ohmpi_0001/data", + "qos": "2", + "datatype": "auto", + "broker": "6ae7e77e.04c64", + "nl": false, + "rap": false, + "inputs": 0, + "x": 320, + "y": 140, + "wires": [ + [ + "f6075b441607acc4", + "2a196662df41046e", + "946869bda15d570c", + "d3229390343731e3" + ] + ] + }, + { + "id": "16b2937ad25f25d2", + "type": "mqtt out", + "z": "b19c51e9d4d25a33", + "name": "MQTT ctrl", + "topic": "ohmpi_0001/ctrl", + "qos": "2", + "retain": "false", + "respTopic": "", + "contentType": "", + "userProps": "", + "correl": "", + "expiry": "", + "broker": "6ae7e77e.04c64", + "x": 820, + "y": 400, + "wires": [] + }, + { + "id": "c7c725c7f947f5a8", + "type": "mqtt in", + "z": "b19c51e9d4d25a33", + "name": "", + "topic": "ohmpi_0001/soh", + "qos": "2", + "datatype": "auto", + "broker": "6ae7e77e.04c64", + "nl": false, + "rap": false, + "inputs": 0, + "x": 320, + "y": 240, + "wires": [ + [ + "6d9397b8b510b4b8" + ] + ] + }, + { + "id": "06e401792488500e", + "type": "ui_button", + "z": "b19c51e9d4d25a33", + "name": "Run sequence", + "group": "142ad6ae.d55e29", + "order": 2, + "width": 2, + "height": 1, + "passthru": false, + "label": "Run", + "tooltip": "run sequence", + "color": "red", + "bgcolor": "lightgrey", + "className": "", + "icon": "play_circle_fill", + "payload": "{\"cmd_id\" :\"0\", \"cmd\":\"run_sequence_async\"}", + "payloadType": "str", + "topic": "topic", + "topicType": "msg", + "x": 320, + "y": 340, + "wires": [ + [ + "16b2937ad25f25d2", + "649dbc02e71571e2" + ] + ] + }, + { + "id": "c427102c051828b6", + "type": "ui_button", + "z": "b19c51e9d4d25a33", + "name": "Interrupt", + "group": "142ad6ae.d55e29", + "order": 3, + "width": 2, + "height": 1, + "passthru": false, + "label": "Stop", + "tooltip": "interrupt sequence", + "color": "black", + "bgcolor": "lightgrey", + "className": "", + "icon": "stop", + "payload": "{\"cmd_id\" :\"0\", \"cmd\":\"interrupt\"}", + "payloadType": "str", + "topic": "topic", + "topicType": "msg", + "x": 300, + "y": 400, + "wires": [ + [ + "16b2937ad25f25d2", + "649dbc02e71571e2" + ] + ] + }, + { + "id": "f4b6096e60252b62", + "type": "ui_text", + "z": "b19c51e9d4d25a33", + "group": "b0990b3c5ff3c09a", + "order": 3, + "width": 16, + "height": 3, + "name": "MQTT exec", + "label": "Execution", + "format": "{{msg.payload}}", + "layout": "row-spread", + "className": "", + "x": 830, + "y": 40, + "wires": [] + }, + { + "id": "f6075b441607acc4", + "type": "ui_text", + "z": "b19c51e9d4d25a33", + "group": "b0990b3c5ff3c09a", + "order": 4, + "width": 16, + "height": 3, + "name": "MQTT Data", + "label": "Data", + "format": "{{msg.payload}}", + "layout": "row-spread", + "className": "", + "x": 830, + "y": 140, + "wires": [] + }, + { + "id": "6d9397b8b510b4b8", + "type": "ui_text", + "z": "b19c51e9d4d25a33", + "group": "b0990b3c5ff3c09a", + "order": 5, + "width": 0, + "height": 0, + "name": "MQTT SOH", + "label": "SOH", + "format": "{{msg.payload}}", + "layout": "row-spread", + "className": "", + "x": 830, + "y": 240, + "wires": [] + }, + { + "id": "455f7fa404a19428", + "type": "ui_button", + "z": "b19c51e9d4d25a33", + "name": "", + "group": "b0990b3c5ff3c09a", + "order": 1, + "width": 0, + "height": 0, + "passthru": false, + "label": "clear messages", + "tooltip": "", + "color": "", + "bgcolor": "", + "className": "<p style=\"background-color:Tomato;\">", + "icon": "", + "payload": " ", + "payloadType": "str", + "topic": "topic", + "topicType": "msg", + "x": 580, + "y": 200, + "wires": [ + [ + "f6075b441607acc4", + "f4b6096e60252b62", + "649dbc02e71571e2", + "6d9397b8b510b4b8" + ] + ] + }, + { + "id": "31071d204231a91b", + "type": "subflow:c3dbd6595b0256a9", + "z": "b19c51e9d4d25a33", + "name": "", + "x": 320, + "y": 540, + "wires": [ + [ + "16b2937ad25f25d2", + "649dbc02e71571e2" + ] + ] + }, + { + "id": "e62faba48a0960cc", + "type": "subflow:1506d9deaa5de120", + "z": "b19c51e9d4d25a33", + "name": "", + "x": 320, + "y": 640, + "wires": [ + [ + "16b2937ad25f25d2", + "649dbc02e71571e2" + ] + ] + }, + { + "id": "649dbc02e71571e2", + "type": "ui_text", + "z": "b19c51e9d4d25a33", + "group": "b0990b3c5ff3c09a", + "order": 2, + "width": 0, + "height": 0, + "name": "", + "label": "Command sent", + "format": "{{msg.payload}}", + "layout": "row-spread", + "className": "", + "x": 840, + "y": 300, + "wires": [] + }, + { + "id": "2a196662df41046e", + "type": "subflow:4489e1d51c9522f3", + "z": "b19c51e9d4d25a33", + "name": "", + "x": 1190, + "y": 60, + "wires": [] + }, + { + "id": "946869bda15d570c", + "type": "subflow:b6b050428bd78f42", + "z": "b19c51e9d4d25a33", + "name": "", + "x": 1190, + "y": 100, + "wires": [] + }, + { + "id": "d16e9ecebccafc37", + "type": "ui_table", + "z": "b19c51e9d4d25a33", + "group": "6e40c7571e22771c", + "name": "Latest reading", + "order": 1, + "width": 0, + "height": 0, + "columns": [ + { + "field": "time", + "title": "Time", + "width": "20%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "A", + "title": "A", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "B", + "title": "B", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "M", + "title": "M", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "N", + "title": "N", + "width": "2%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "R [ohm]", + "title": "R [ohm]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Vmn [mV]", + "title": "Vmn [mV]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "I [mA]", + "title": "I [mA]", + "width": "8%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Ps [mV]", + "title": "Ps [mV]", + "width": "8%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "inj time [ms]", + "title": "Injection<br>Time [ms]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "nbStack", + "title": "Nb<br>Stack", + "width": "5%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Tx [V]", + "title": "Tx [V]", + "width": "4%", + "align": "left", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "CPU temp [degC]", + "title": "CPU<br>Temp [degC]", + "width": "8%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "Nb samples [-]", + "title": "Nb<br>Samples", + "width": "3%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + }, + { + "field": "cmd_id", + "title": "Cmd<br>ID", + "width": "6%", + "align": "center", + "formatter": "plaintext", + "formatterParams": { + "target": "_blank" + } + } + ], + "outputs": 1, + "cts": true, + "x": 1140, + "y": 140, + "wires": [ + [] + ] + }, + { + "id": "d3229390343731e3", + "type": "function", + "z": "b19c51e9d4d25a33", + "name": "data", + "func": "var data = JSON.parse(\n msg.payload.split(' | ')[2].slice(6,).split(`'`).join(`\"`));\n\nfor (var key in data) {\n data[key] = data[key].toString()\n};\nmsg.payload = data\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 810, + "y": 180, + "wires": [ + [ + "eda4397c88dfcf6d" + ] + ] + }, + { + "id": "eda4397c88dfcf6d", + "type": "function", + "z": "b19c51e9d4d25a33", + "name": "Add data", + "func": "msg.payload = {\n command: \"addData\", \n arguments: [\n [msg.payload],\n true\n ],\n returnPromise: true\n }\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 960, + "y": 180, + "wires": [ + [ + "d16e9ecebccafc37" + ] + ] + }, + { + "id": "357d22b7680ff21c", + "type": "ui_button", + "z": "b19c51e9d4d25a33", + "name": "", + "group": "6e40c7571e22771c", + "order": 7, + "width": 0, + "height": 0, + "passthru": false, + "label": "clear", + "tooltip": "clear all table data", + "color": "", + "bgcolor": "", + "className": "", + "icon": "", + "payload": "", + "payloadType": "date", + "topic": "", + "topicType": "str", + "x": 990, + "y": 220, + "wires": [ + [ + "f46aca539df9df53" + ] + ] + }, + { + "id": "f46aca539df9df53", + "type": "function", + "z": "b19c51e9d4d25a33", + "name": "clearData", + "func": "\nmsg.payload={\n command:\"clearData\",\n arguments: [],\n returnPromise: true\n}\nreturn msg;", + "outputs": 1, + "noerr": 0, + "initialize": "", + "finalize": "", + "libs": [], + "x": 1121, + "y": 220, + "wires": [ + [ + "d16e9ecebccafc37" + ] + ], + "info": "# clear data\n\nunfortunately this command (currently) do not send a promise back! So we have to pass it directly to the table handler" + } +]