Last time on Patriot Geek, our intrepid blogger built a MQTT client in Node-RED. Can we now present the incoming data in a useful and attractive manner? Stay tuned!
To visualize the data being sent by the BME280, we will use the "node-red-dashboard" module, which adds various nodes that generate interactive dashboards. Here's the dashboard we'll be building:
This post is divided into the following sections:
- Install the dashboard extension
- Understand dashboard nomenclature
- Design Overview
- The MQTT Subscriber Flow
- Create Dashboard Tabs and Groups
- The Dashboard Flow
- Test the Dashboard
- Conclusions and Source Code
Installation
The Node-RED IDE can be extended in various ways, from adding a single new node to adding a family of new nodes as well as adding significantly to the user interface. That's what the Node-RED Dashboard module does: it adds 16 new nodes, a new sidecar tab, and makes other changes to the IDE.
To install, click the Menu Button and choose "Manage palette". In the property sheet that opens, click the "Install" tab. Do a search for "Dashboard", and scroll until you see the "node-red-dashboard" package, and click the "install" button, then click the red "Done" button. Note: there is no need to install the "node-red-dashboard-es" package.
Dashboard Nomenclature
The use of the word "Dashboard" in Node-RED is kind of confusing, because it is used in several ways:
- The module we just installed
- The sidebar tab
- The section of the palette, and the nodes inside it
- The UIs that are built with those nodes
A tab is a top-level part of the dashboard, and every dashboard must have one. When a dashboard has more than one, a menu will be shown on the left side of the title bar. Each tab has one or more "groups", which are... groups of UI controls.
Here's a tab with three groups inside it:
Here's the menu showing the two available tabs:
Design Overview
We will implement the MQTT data dashboard using two workspace tabs: one called "MQTT Subscriber" which will be a flow similar to that in the previous blog post; the other called "MQTT Dashboard" which will be for the dashboard nodes.
This design approach follows the Model-View-Controller (MVC) design pattern. The dashboard user interface is a view, and the MQTT client is essentially a controller - it processes incoming events and passes data to the view.
How do we send messages from the controller tab over to the view tab? There are a pair of nodes for doing this: the Link Out node and the Link In node. These are found under the "output" and "input" sections of the palette, respectively.
The MQTT Subscriber Flow
Create two new tabs by pressing the New Tab button. Double click on the first, and set the name to be "MQTT Subscriber". Double click the second and name it "MQTT Dashboard". Click back on the "MQTT Subscriber" tab.
Again, the MQTT Subscriber flow is a modification of the one in the previous post - all we'll be doing is adding a Link Out node and removing the two Debug nodes. Here's what the final result will look like:
After deleting the two Debug nodes, add a new Link Out node, which is found in the "Input" section of the palette. Wire it as shown. Double click it and set the name to be "To Dashboard" and click "Done". For quick reference, here are the properties of all four nodes in this flow:
Node Type | Property | Value | ||
---|---|---|---|---|
Server: | 192.168.1.11:1883 | |||
Topic: | home/living-room | |||
QoS: | 0 | |||
Columns: | temperature,humidity,barometric-pressure | |||
Name: | Parse CSV into JSON | |||
Name: | Add Time Info | |||
Function: |
|
|||
Name: | To Dashboard |
Create the Dashboard's Tabs and Groups
Before drawing the view flow, set up the tabs and groups for the dashboard. We will have only one tab called "Living Room Data". Inside that tab will be three groups, called "Temperature", "Humidity", and "Barometric Pressure". Here's how this will look in the Layout subtab when we're done:
Click the dashboard sidebar tab, then choose "Layout" sub tab, and use the "+ tab" button to create the tab. Set the name to be "Living Room Data". This determines the text shown in the title bar of the dashboard.
Then, hover-over the tab and click the "+ group" button three times. Hover-over each and click the "edit" button and set the names to be "Temperature", "Humidity", and "Barometric Pressure". Make sure they're all under the "Living Room Data" tab.
Click the "Site" subtab, and set the web page's title to "MQTT Data". This will be shown in the browser title bar.
While we're here, click the Theme subtab, and set the style to be "Dark". Why? Because Dark UIs Matter:
The Dashboard Flow
Click the "MQTT Dashboard" tab so we can build the dashboard flow. On it, drop one Link In node, one Debug node, three Change nodes, two Chart nodes, one Gauge node, and three Text nodes. The Chart, Gauge, and Text nodes and found in the "dashboard" section of the palette, and the Link In node is found under the "Input" section. Wire them up as follows:
We want the Link In node to receive data from the Link Out node on the other tab. Double click the Link In node, and the name we set to the Link Out node ("To Dashboard") is listed there as a checkbox. Click that checkbox. Also, set the node's name to be "From Subscriber":
Click the Link In node. When it has been successfully connected to the Link Out in the other tab, it is displayed like this, but only when selected:
Double click the Debug node, and set the out to be "complete msg object"
Now, the Chart and Gauge nodes expect their input to be in a particular format: the input must be numeric and the incoming msg.payload must be set equal that numeric value. Trouble is, the data coming from the MQTT subscriber tab (via the Link In node) is a JSON object like this:
{ "temperature": 71.55, "humidity": 41.46, "barometric-pressure": 29.73 }
That's the purpose of the three Change nodes, to extract the numeric values from msg.payload, and replace msg.payload with those values.
Double click the first Change node, and set the properties as follows:
Update the second Change node so that it uses msg.payload.humidity. Give it the name "Extract Humidity", and set msg.payload to be msg.payload.humidity.
Name the third Change node to be "Extract Pressure", and set msg.payload to be msg.payload["barometric-pressure"]. Note the different syntax here - "barometric-pressure" is a perfectly fine JSON field name, but it is an invalid JavaScript field name!
Notice the dichotomy between visual programming languages and most traditional languages: altering msg.payload in the first Change node only has consequences for the "downstream" nodes!
The Gauge node and the Chart nodes are now getting the right data!
Now we configure the charts and the gauge. Double click the Chart node that's attached to the "Extract Temperature" Change node, and set the properties as follows:
The Chart attached to the "Extract Pressure" Change node will be set in a similar fashion, except that it will be set to appear in the "Barometric Pressure [Living Room Data]" group, and the name will be "Pressure Chart".
For the Gauge node, set the properties as shown:
Note that range for the Humidity Gauge is set to be from 0 to 100. This is because humidity runs from 0% to 100%. It is also possible to preconfigure the y-axis range on the Chart nodes but the chart will draw values outside that range as a flat line.
Our last task is to configure the Text nodes. Set their properties as follows:
Node | Property | Value |
---|---|---|
Group: | Temperature [Living Room Data] | |
Label: | Temperature: | |
Value format: | {{msg.payload}} °F | |
Layout: | "row-left" | |
Name: | Temperature Text | |
Group: | Humidity [Living Room Data] | |
Label: | Humidity: | |
Value format: | {{msg.payload}} % | |
Layout: | "row-left" | |
Name: | Humidity Text | |
Group: | Barometric Pressure [Living Room Data] | |
Label: | Barometric Pressure: | |
Value format: | {{msg.payload}} InHg | |
Layout: | "row-left" | |
Name: | Pressure Text |
Here's what the flow will now look like:
Test the Dashboard
To see the result of all this, be sure that the MQTT broker is running, and open a new browser window and go to:
http://127.0.0.1:1880/uiData will start appearing in the gauge and charts. If there's not a lot of variability in the data, try (get this) breathing on the BME280! Here's what the dashboard should look like:
It works on an iPhone 5c, too:
Conclusions and Source Code
This post demonstrated how to make a dashboard that displays MQTT data coming from the BME280 and ESP8266. Something else demonstrated was how to use two separate tabs for the application, one for the controller, the other for the user interface. This use of the Model-View-Controller (MVC) design pattern adds some clarity to our flows.
The source code for the modified MQTT Subscriber flow is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | [ { "id": "fdb325cd.8b5748", "type": "mqtt in", "z": "553bcc1e.c97b54", "name": "", "topic": "home/living-room", "qos": "0", "broker": "a481358c.031c18", "x": 128, "y": 218, "wires": [ [ "3c175b0f.97a134" ] ] }, { "id": "3c175b0f.97a134", "type": "csv", "z": "553bcc1e.c97b54", "name": "Parse CSV into JSON", "sep": ",", "hdrin": "", "hdrout": "", "multi": "one", "ret": "\\n", "temp": "temperature,humidity,barometric-pressure", "x": 340, "y": 218, "wires": [ [ "eae5505.8945fb" ] ] }, { "id": "eae5505.8945fb", "type": "function", "z": "553bcc1e.c97b54", "name": "Add Time Info", "func": "var now = new Date();\nmsg.payload[\"timestamp\"] = now.getTime();\nmsg.payload[\"fomratted-time\"] = now.toUTCString();\nreturn msg;", "outputs": 1, "noerr": 0, "x": 554, "y": 218, "wires": [ [ "3a476f0b.d6bea" ] ] }, { "id": "3a476f0b.d6bea", "type": "link out", "z": "553bcc1e.c97b54", "name": "To Dashboard", "links": [ "492fdf69.9cb38", "4e7d2e3c.c3427" ], "x": 684, "y": 218, "wires": [] }, { "id": "a481358c.031c18", "type": "mqtt-broker", "z": "", "broker": "192.168.1.11", "port": "1883", "clientid": "", "usetls": false, "compatmode": true, "keepalive": "60", "cleansession": true, "willTopic": "", "willQos": "0", "willPayload": "", "birthTopic": "", "birthQos": "0", "birthPayload": "" } ] |
Notice how there is an extra node in the JSON that is not shown in the workspace - this is where info about the MQTT broker is stored. Nodes like this are called "configuration nodes".
And here's the source code for the Dashboard flow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 | [ { "id": "4e7d2e3c.c3427", "type": "link in", "z": "6fc2e4b5.acb35c", "name": "", "links": [ "3a476f0b.d6bea" ], "x": 55, "y": 140, "wires": [ [ "43a49c20.b2b534", "2d87fae4.e4c7b6", "ec5f8a6c.7cef28", "432ea3c2.6ca94c" ] ] }, { "id": "43a49c20.b2b534", "type": "debug", "z": "6fc2e4b5.acb35c", "name": "", "active": true, "console": "false", "complete": "true", "x": 210, "y": 40, "wires": [] }, { "id": "2d87fae4.e4c7b6", "type": "change", "z": "6fc2e4b5.acb35c", "name": "Extract Temperature", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "payload.temperature", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 260, "y": 100, "wires": [ [ "5294196b.20d158", "d925dd10.68321" ] ] }, { "id": "ec5f8a6c.7cef28", "type": "change", "z": "6fc2e4b5.acb35c", "name": "Extract Humidity", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "payload.humidity", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 250, "y": 200, "wires": [ [ "f65a9085.ae81", "78782f2d.880f1" ] ] }, { "id": "432ea3c2.6ca94c", "type": "change", "z": "6fc2e4b5.acb35c", "name": "Extract Pressure", "rules": [ { "t": "set", "p": "payload", "pt": "msg", "to": "payload[\"barometric-pressure\"]", "tot": "msg" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 240, "y": 300, "wires": [ [ "ee70b8b0.828fc8", "c65aff2.f086b" ] ] }, { "id": "5294196b.20d158", "type": "ui_chart", "z": "6fc2e4b5.acb35c", "name": "Temperature Chart", "group": "637855dc.2229bc", "order": 0, "width": 0, "height": 0, "label": "", "chartType": "line", "legend": "false", "xformat": "HH:mm:ss", "interpolate": "linear", "nodata": "", "dot": false, "ymin": "", "ymax": "", "removeOlder": "5", "removeOlderPoints": "", "removeOlderUnit": "60", "cutout": 0, "useOneColor": false, "colors": [ "#1f77b4", "#aec7e8", "#ff7f0e", "#2ca02c", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5" ], "useOldStyle": false, "x": 530, "y": 80, "wires": [ [], [] ] }, { "id": "ee70b8b0.828fc8", "type": "ui_chart", "z": "6fc2e4b5.acb35c", "name": "Pressure Chart", "group": "9728196c.705ff8", "order": 0, "width": 0, "height": 0, "label": "", "chartType": "line", "legend": "false", "xformat": "HH:mm:ss", "interpolate": "linear", "nodata": "", "dot": false, "ymin": "", "ymax": "", "removeOlder": "5", "removeOlderPoints": "", "removeOlderUnit": "60", "cutout": 0, "useOneColor": false, "colors": [ "#1f77b4", "#aec7e8", "#ff7f0e", "#2ca02c", "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5" ], "useOldStyle": false, "x": 520, "y": 280, "wires": [ [], [] ] }, { "id": "f65a9085.ae81", "type": "ui_gauge", "z": "6fc2e4b5.acb35c", "name": "Humidity Gauge", "group": "f468b186.a264b", "order": 0, "width": 0, "height": 0, "gtype": "gage", "title": "Gauge", "label": "%", "format": "{{value}}", "min": 0, "max": "100", "colors": [ "#00b500", "#e6e600", "#ca3838" ], "seg1": "", "seg2": "", "x": 520, "y": 180, "wires": [] }, { "id": "d925dd10.68321", "type": "ui_text", "z": "6fc2e4b5.acb35c", "group": "637855dc.2229bc", "order": 0, "width": 0, "height": 0, "name": "Temperature Text", "label": "Temperature:", "format": "{{msg.payload}} °F", "layout": "row-left", "x": 530, "y": 120, "wires": [] }, { "id": "78782f2d.880f1", "type": "ui_text", "z": "6fc2e4b5.acb35c", "group": "f468b186.a264b", "order": 0, "width": 0, "height": 0, "name": "Humidity Text", "label": "", "format": "{{msg.payload}} %", "layout": "row-left", "x": 520, "y": 220, "wires": [] }, { "id": "c65aff2.f086b", "type": "ui_text", "z": "6fc2e4b5.acb35c", "group": "9728196c.705ff8", "order": 0, "width": 0, "height": 0, "name": "Pressure Text", "label": "Barometric Pressure:", "format": "{{msg.payload}} InHg", "layout": "row-left", "x": 520, "y": 320, "wires": [] }, { "id": "637855dc.2229bc", "type": "ui_group", "z": "", "name": "Temperature", "tab": "d376d602.4e3538", "order": 1, "disp": true, "width": "6" }, { "id": "9728196c.705ff8", "type": "ui_group", "z": "", "name": "Barometric Pressure", "tab": "d376d602.4e3538", "order": 3, "disp": true, "width": "6" }, { "id": "f468b186.a264b", "type": "ui_group", "z": "", "name": "Humidity", "tab": "d376d602.4e3538", "order": 2, "disp": true, "width": "6" }, { "id": "d376d602.4e3538", "type": "ui_tab", "z": "", "name": "Living Room Data", "icon": "dashboard", "order": 2 } ] |
Here, we have four extra configuration nodes: one for the tab and three for the groups.