Raspberry Pi Exhibition Demonstrator

In this post, we describe the development of a demonstrator for an exhibition using a Raspberry Pi. Here at Cranfield University we are fortunate to be attending the American Association for the Advancement of Science (AAAS) annual conference and exhibition in Seattle, USA in February 2020, and are making preparations. The task is to develop for this a stand-alone technical demonstrator, showcasing British science and technology – and in doing this what could be better than the use of a Raspberry Pi. We will develop an environmental sensing demonstrator.

In previous blogs, we have explored the use of the fantastic Bosch BME680 sensor from PiMoroni and now we will use it with a Raspberry Pi and a built-in data dashboard showing real time environmental data.

Hardware

The pin confirmation of the BME680 breakout board from PiMoroni is designed to match the order of the GPIO pins on the Raspberry Pi, meaning that the breakout board can be plugged directly into the PI. The first task was to solder up a BME680 breakout board with header pin sockets to allow it to plug into the Pi.

BME680 with header pins soldered on
BME680 reverse side

Once the pin connectors are soldered in, the unit can be simply plugged into the Pi’s GPIO pins, noting the configuration of the pins (https://www.raspberrypi.org/documentation/usage/gpio/).

Location of the GPIO pins on the Raspberry Pi
BME680 board plugged into the Raspberry Pi

Software

Previous blogs here have covered how to set the Raspberry Pi up from the start – see http://www.geothread.net/raspberry-pi-headless-setup/.

Now a Python script is required that can read the BME680 sensors and output the data. For this we used the excellent instructions on the PiMoroni webpage (good pirates!) – see https://learn.pimoroni.com/tutorial/sandyj/getting-started-with-bme680-breakout.

We left the I2C address the default one (e.g. didn’t solder the pads on the breakout board). We used raspi-config tool to ensure the I2C was enabled on the Pi. We used the simple installation script to install all the libraries required:

curl https://get.pimoroni.com/i2c | bash

Next we built a Python script to read the data off every 10 seconds. The formatting of the output was changed to a format based on JSON.

import bme680 import time sensor = bme680.BME680() sensor.set_humidity_oversample(bme680.OS_2X) sensor.set_pressure_oversample(bme680.OS_4X) sensor.set_temperature_oversample(bme680.OS_8X) sensor.set_filter(bme680.FILTER_SIZE_3) sensor.set_gas_status(bme680.ENABLE_GAS_MEAS) sensor.set_gas_heater_temperature(320) sensor.set_gas_heater_duration(150) sensor.select_gas_heater_profile(0) while True:
      if sensor.get_sensor_data():
          #output = "{0:.2f} C,{1:.2f} hPa,{2:.2f} %RH".format(sensor.data.temperature, sensor.data.pressure, sensor.data.humidity)
         output = '{{"Temperature_oC":{0:.2f}}},{{"Pressure_HPA":{1:.2f}}},{{"RelativeHumidity_percent":{2:.2f}}}'.format(sensor.data.temperature, sensor.data.pressure, sensor.data.humidity)
          if sensor.data.heat_stable:
             print('{0},{{"Resistance_Ohms":{1}}}'.format(output, sensor.data.gas _resistance))
         else:
              print(output)      time.sleep(10)

We could then run the script to make sure it produced data, which it did thus:

$ python read.py
{"Temperature_oC":29.42},{"Pressure_HPA":1018.88},{"RelativeHumidity_percent":31.68},{"Resistance_Ohms":154806}
{"Temperature_oC":29.35},{"Pressure_HPA":1018.93},{"RelativeHumidity_percent":31.58},{"Resistance_Ohms":174661}

Building a Dashboard

We now need a dashboard to display the data and to do this we wanted to use the open source NodeRED tool. We have used NodeRED in earlier blogs (http://www.geothread.net/node-red-and-the-internet-of-things/) to build interfaces and to gather data, and the installation instructions have been described there. The NodeRED installation installs Node.JS also.

The Node-red : json ‘palette’ should be installed, but if not that needs installing. Next we built flow (script) to obtain the data, which it does by running the Python script, and then formatting the output into a NodeRED dashboard. When complete the flow looked like this:

NodeRED Flow to read the values from the BME680 and create a dashboard

In NodeRED, the flow itself can be saved off to a text file, and used to recreate the flow. The exported code for this was as follows:

[{"id":"705fa714.2850b8","type":"tab","label":"BME680","disabled":false,"info":""},{"id":"75e72d28.122a8c","type":"pythonshell in","z":"705fa714.2850b8","name":"BME680 Reader","pyfile":"/home/pi/Pimoroni/bme680/examples/read.py","virtualenv":"","continuous":true,"stdInData":false,"x":324.9642677307129,"y":233.74208164215088,"wires":[["e686e72d.e74ce"]]},{"id":"cbf8dc79.826648","type":"debug","z":"705fa714.2850b8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":714.1309356689453,"y":445.6587562561035,"wires":[]},{"id":"4a810981.ca556","type":"json","z":"705fa714.2850b8","name":"","property":"payload","action":"obj","pretty":false,"x":409.9642639160156,"y":344.74209213256836,"wires":[["513ee2d2.58f264"]]},{"id":"f6e2168c.d430a","type":"debug","z":"705fa714.2850b8","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":714.2142715454102,"y":349.74207878112793,"wires":[]},{"id":"e686e72d.e74ce","type":"split","z":"705fa714.2850b8","name":"","splt":",","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":282.9642639160156,"y":344.74209213256836,"wires":[["4a810981.ca556"]]},{"id":"30eed397.31f024","type":"ui_gauge","z":"705fa714.2850b8","name":"Pressure Gauge","group":"1a8c95a4.eceb62","order":1,"width":"6","height":"3","gtype":"gage","title":"","label":"HPA","format":"{{msg.payload.Pressure_HPA}}","min":0,"max":"1500","colors":["#00b500","#e6e600","#ca3838"],"seg1":"500","seg2":"1000","x":1297.2421073913574,"y":656.3254203796387,"wires":[]},{"id":"624d0d63.3173fc","type":"comment","z":"705fa714.2850b8","name":"Process BME680 data","info":"","x":161.71430206298828,"y":179.71429634094238,"wires":[]},{"id":"74288516.75bfec","type":"ui_gauge","z":"705fa714.2850b8","name":"Temperature Gauge","group":"9f82b5b9.abac3","order":1,"width":"6","height":"3","gtype":"gage","title":"","label":"Degrees Celsius","format":"{{msg.payload.Temperature_oC}}","min":0,"max":"40","colors":["#00b500","#e6e600","#ca3838"],"seg1":"20","seg2":"30","x":1298.1309928894043,"y":301.5754270553589,"wires":[]},{"id":"da32905a.2f3218","type":"inject","z":"705fa714.2850b8","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":142.67855834960938,"y":234.31350898742676,"wires":[["75e72d28.122a8c"]]},{"id":"5bb7dfa7.fc9648","type":"ui_chart","z":"705fa714.2850b8","name":"Temperature chart","group":"9f82b5b9.abac3","order":2,"width":"24","height":"4","label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1298.5000858306885,"y":263.2778015136719,"wires":[[]]},{"id":"6def3ca.13f22c4","type":"function","z":"705fa714.2850b8","name":"Prepare Temperature","func":"var newMsg={};\nnewMsg.topic = 'Temperature_oC';\nnewMsg.payload = msg.payload.Temperature_oC;\nreturn newMsg;","outputs":1,"noerr":0,"x":1042.9444961547852,"y":262.08337211608887,"wires":[["5bb7dfa7.fc9648"]]},{"id":"b3f73ef1.5d3668","type":"function","z":"705fa714.2850b8","name":"Prepare Pressure","func":"var newMsg={};\nnewMsg.topic = 'Pressure_HPA';\nnewMsg.payload = msg.payload.Pressure_HPA\nreturn newMsg;","outputs":1,"noerr":0,"x":1029.7499771118164,"y":612.2778091430664,"wires":[["be2f7463.a220d"]]},{"id":"be2f7463.a220d","type":"ui_chart","z":"705fa714.2850b8","name":"Pressure chart","group":"1a8c95a4.eceb62","order":2,"width":"24","height":"4","label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1297.5276718139648,"y":613.6111507415771,"wires":[[]]},{"id":"4f78ba28.f9942c","type":"function","z":"705fa714.2850b8","name":"Prepare Humidity","func":"var newMsg={};\nnewMsg.topic = 'RelativeHumidity_percent';\nnewMsg.payload = msg.payload.RelativeHumidity_percent;\nreturn newMsg;","outputs":1,"noerr":0,"x":1028.8611221313477,"y":493.05558586120605,"wires":[["bf817e3.1b8e58"]]},{"id":"50d062af.19b81c","type":"ui_gauge","z":"705fa714.2850b8","name":"Humidity Gauge","group":"33d2c36c.c43efc","order":1,"width":"6","height":"3","gtype":"gage","title":"","label":"%","format":"{{msg.payload.RelativeHumidity_percent}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"30","seg2":"40","x":1298.8610877990723,"y":532.3889007568359,"wires":[]},{"id":"bf817e3.1b8e58","type":"ui_chart","z":"705fa714.2850b8","name":"Humidity chart","group":"33d2c36c.c43efc","order":2,"width":"24","height":"4","label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1296.1944618225098,"y":493.38890838623047,"wires":[[]]},{"id":"4b7ce9a7.a5111","type":"function","z":"705fa714.2850b8","name":"Prepare VOC","func":"var newMsg={};\nnewMsg.topic = 'Resistance_Ohms';\nnewMsg.payload = msg.payload.Resistance_Ohms;\nreturn newMsg;","outputs":1,"noerr":0,"x":1013.9166641235352,"y":378.61114501953125,"wires":[["a7a7ac5b.88a96"]]},{"id":"a35146bd.08ded","type":"ui_gauge","z":"705fa714.2850b8","name":"VOC Gauge","group":"75a1ea8e.7b0304","order":1,"width":"6","height":"3","gtype":"gage","title":"","label":"Ohms","format":"{{msg.payload.Resistance_Ohms}}","min":0,"max":"300000","colors":["#00b500","#e6e600","#ca3838"],"seg1":"100000","seg2":"200000","x":1283.361171722412,"y":415.2777862548828,"wires":[]},{"id":"a7a7ac5b.88a96","type":"ui_chart","z":"705fa714.2850b8","name":"VOC Chart","group":"75a1ea8e.7b0304","order":2,"width":"24","height":"4","label":"","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1280.5832595825195,"y":378.05558013916016,"wires":[[]]},{"id":"2535ceb0.2f89ca","type":"ui_template","z":"705fa714.2850b8","group":"9f82b5b9.abac3","name":"Temperature Max and Min","order":2,"width":"6","height":"1","format":"<br>\n  Min: \n  Max: \n<br>\n\n  {{msg.temp.min}}\n  {{msg.temp.max}}\n<br>","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1322.5,"y":340.583327293396,"wires":[[]]},{"id":"a209a10d.79aab","type":"function","z":"705fa714.2850b8","name":"Prepare data","func":"var newMsg={temp:{},pressure:{},humidity:{},voc:{}};\nvar temp_min=context.get('temp_min') || 100;\nvar temp_max=context.get('temp_max') || -100;\nvar pressure_min=context.get('pressure_min') || 5000;\nvar pressure_max=context.get('pressure_max') || -5000;\nvar humidity_min=context.get('humidity_min') || 100;\nvar humidity_max=context.get('humidity_max') || -100;\nvar voc_min=context.get('voc_min') || 500000;\nvar voc_max=context.get('voc_max') || -500000;\n//\nnewMsg.payload = msg.payload;\n\n//\n\nif (msg.payload.Temperature_oC &lt; temp_min) {\n   newMsg.temp.min = msg.payload.Temperature_oC;\n   context.set('temp_min', msg.payload.Temperature_oC);\n} else {\n   newMsg.temp.min = temp_min;\n}\nif (msg.payload.Temperature_oC &gt; temp_max) {\n   newMsg.temp.max = msg.payload.Temperature_oC;\n   context.set('temp_max', msg.payload.Temperature_oC);\n} else {\n   newMsg.temp.max = temp_max;\n}\n\n//\n\nif (msg.payload.Pressure_HPA &lt; pressure_min) {\n   newMsg.pressure.min = msg.payload.Pressure_HPA;\n   context.set('pressure_min', msg.payload.Pressure_HPA);\n} else {\n   newMsg.pressure.min = pressure_min;\n}\nif (msg.payload.Pressure_HPA &gt; pressure_max) {\n   newMsg.pressure.max = msg.payload.Pressure_HPA;\n   context.set('pressure_max', msg.payload.Pressure_HPA);\n} else {\n   newMsg.pressure.max = pressure_max;\n}\n\n//\n\nif (msg.payload.RelativeHumidity_percent &lt; humidity_min) {\n   newMsg.humidity.min = msg.payload.RelativeHumidity_percent;\n   context.set('humidity_min', msg.payload.RelativeHumidity_percent);\n} else {\n   newMsg.humidity.min = humidity_min;\n}\nif (msg.payload.RelativeHumidity_percent &gt; humidity_max) {\n   newMsg.humidity.max = msg.payload.RelativeHumidity_percent;\n   context.set('humidity_max', msg.payload.RelativeHumidity_percent);\n} else {\n   newMsg.humidity.max = humidity_max;\n}\n\n//\n\nif (msg.payload.Resistance_Ohms &lt; voc_min) {\n   newMsg.voc.min = msg.payload.Resistance_Ohms;\n   context.set('voc_min', msg.payload.Resistance_Ohms);\n} else {\n   newMsg.voc.min = voc_min;\n}\nif (msg.payload.Resistance_Ohms &gt; voc_max) {\n   newMsg.voc.max = msg.payload.Resistance_Ohms;\n   context.set('voc_max', msg.payload.Resistance_Ohms);\n} else {\n   newMsg.voc.max = voc_max;\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"x":734.2500076293945,"y":396.25000190734863,"wires":[["cbf8dc79.826648","6def3ca.13f22c4","74288516.75bfec","2535ceb0.2f89ca","4b7ce9a7.a5111","4f78ba28.f9942c","b3f73ef1.5d3668","a35146bd.08ded","50d062af.19b81c","30eed397.31f024","479f52fc.67b8a4","9537a7fa.9f40f","34d364ea.cf3b44"]]},{"id":"513ee2d2.58f264","type":"join","z":"705fa714.2850b8","name":"","mode":"custom","build":"merged","property":"payload","propertyType":"msg","key":"topic","joiner":"\n","joinerType":"str","accumulate":false,"timeout":"","count":"","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":538.25,"y":344.24999618530273,"wires":[["a209a10d.79aab","f6e2168c.d430a"]]},{"id":"479f52fc.67b8a4","type":"ui_template","z":"705fa714.2850b8","group":"1a8c95a4.eceb62","name":"Pressure max and min","order":2,"width":"6","height":"1","format":"\n  Min: \n  Max: \n<br>\n\n  {{msg.pressure.min}}\n  {{msg.pressure.max}}\n<br>","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1317.9166297912598,"y":701.2499589920044,"wires":[[]]},{"id":"9537a7fa.9f40f","type":"ui_template","z":"705fa714.2850b8","group":"33d2c36c.c43efc","name":"Humidity max and min","order":2,"width":"6","height":"1","format":"\n  Min: \n  Max: \n<br>\n\n  {{msg.humidity.min}}\n  {{msg.humidity.max}}\n<br>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1316.250087738037,"y":574.5833158493042,"wires":[[]]},{"id":"34d364ea.cf3b44","type":"ui_template","z":"705fa714.2850b8","group":"75a1ea8e.7b0304","name":"VOC max and min","order":2,"width":"6","height":"1","format":"\n  Min: \n  Max: \n<br>\n\n  {{msg.voc.min}}\n  {{msg.voc.max}}\n<br>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1304.5833892822266,"y":454.58332443237305,"wires":[[]]},{"id":"600348f2.eec0e","type":"ui_button","z":"705fa714.2850b8","name":"Halt","group":"1a8c95a4.eceb62","order":3,"width":"1","height":"1","passthru":false,"label":"Halt","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":1269.4445190429688,"y":749.9999961853027,"wires":[["2c1763b5.243e14"]]},{"id":"2c1763b5.243e14","type":"exec","z":"705fa714.2850b8","command":"sudo poweroff #","append":"","useSpawn":"","name":"turn off Pi","x":1429.5555686950684,"y":748.9999313354492,"wires":[[],[],[]]},{"id":"1a8c95a4.eceb62","type":"ui_group","z":"","name":"Pressure","tab":"1e784a62.f22dbe","order":4,"disp":true,"width":"30","collapse":false},{"id":"9f82b5b9.abac3","type":"ui_group","z":"","name":"Temperature","tab":"1e784a62.f22dbe","order":1,"disp":true,"width":"30","collapse":false},{"id":"33d2c36c.c43efc","type":"ui_group","z":"","name":"Humidity","tab":"1e784a62.f22dbe","order":3,"disp":true,"width":"30","collapse":false},{"id":"75a1ea8e.7b0304","type":"ui_group","z":"","name":"VOC","tab":"1e784a62.f22dbe","order":2,"disp":true,"width":"30","collapse":false},{"id":"1e784a62.f22dbe","type":"ui_tab","z":"","name":"BME680","icon":"dashboard","order":2,"disabled":false,"hidden":false}]

Some final configuration of the dashboard settings was required to ensure the dashboard items all fitted onto the screen. The final settings used are shown below. Note that the settings depend on the monitor/TV used (we intend to use Raspberry Pi’s own 7 inch monitor in the exhibition):

Configuring Kiosk mode for the dashboard

The final requirement was to enable the Pi to boot from cold and run the dashboard. The dashboard is a web page, and we can use the Pi’s own Chromium browser in ‘kiosk’ mode to do this. To set the dashboard up, we followed the excellent tutorial ‘Raspberry Pi Kiosk using Chromium’ at https://pimylifeup.com/raspberry-pi-kiosk/.

In summary, we first added some useful tools to the Pi (sudo apt-get install xdotool unclutter sed), then enabled autologin using rasps-config, and then created a script to run the dashboard, ‘/home/pi/kiosk.sh’, (see article linked above) thus:

#!/bin/bash
 xset s noblank
 xset s off
 xset -dpms
 unclutter -idle 2 -root &amp;
 sed -i 's/"exited cleanly":false/"exited_cleanly":true/' /home/pi/.config/chromium/Default/Preferences
 sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' /home/pi/.config/chromium/Default/Preferences
 /usr/bin/chromium-browser --noerrdialogs --disable-infobars --kiosk http://localhost:1880/ui

We then set up a service file (editing as root), ‘sudo nano /lib/systemd/system/kiosk.service’ as follows:

[Unit] Description=Chromium Kiosk
 Wants=graphical.target
 After=graphical.target

[Service]
 Environment=DISPLAY=:0.0 Environment=XAUTHORITY=/home/pi/.Xauthority<br> Type=simple ExecStart=/bin/bash /home/pi/kiosk.sh<br> Restart=on-abort User=pi<br> Group=pi

[Install]
 WantedBy=graphical.target

Finally, we could test the service started (sudo systemctl start kiosk.service) and stopped (sudo systemctl stop kiosk.service), before configuring it to run from boot (sudo systemctl enable kiosk.service). We checked the status was good (sudo systemctl status kiosk.service).

Then we can reboot the Pi and the dashboard appears (takes a while to appear).

Epilogue

We wanted to have a demonstrator for an exhibition that could run unattended, did not need network connections, and showcased the use of the Raspberry Pi as a means to collect and display environmental data. This configuration works well and shows what a versatile computer the Pi really is. In designing the dashboard, there were inevitably lots of editing required. We discovered that with the Pi connected to a monitor as configured above, the best means to do this was to connect with a separate networked laptop to the NodeRED configuration page on the Pi (<<IP address of Pi>>:1880) – then, then the code is redeployed, or the dashboard rebuilt – the attached monitor display automatically updates. This saved a LOT of time.

For the exhibition itself, the Pi can now be plugged in and powered up and should immediately start working. A ‘power down’ halt button – has been added at the foot of the dashboard, as it is generally not advised to turn off a Pi by just pulling the plug!! If we are running without a mouse or keyboard, but using the Raspberry Pi screen, which is a touchscreen, a further ‘touch’ setting needs to be added to the call to Chromium, thus: (/usr/bin/chromium-browser –touch-events –noerrdialogs –disable-infobars –kiosk http://localhost:1880/ui).

Building the hardware for a room sensor

The final sensor housing design

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are gathering environmental data from IoT devices and building data dashboards to show the data and related analyses. One of our projects will be to investigate air quality on the campus, in our lecture rooms and public spaces. Cranfield is a unique University in the UK for having its own airfield as part of the campus – we want to monitor any particular impacts that can arise from this. In this blog we discuss building the hardware for a room sensor to detect levels for temperature, humidity, barometric pressure and VOC (volatile organic compounds).

In previous blogs, we have explored the use of the fantastic Bosch BME680 sensor from PiMoroni with the equally fantastic Particle Photon board to detect environmental characteristics. This blog post is more about building the hardware for the sensor, describing a prototype design.

An initial issue is how to site the sensor in a room. We were considering at first fixing sensors to walls, and running power to the case with trunking going up the wall from power sockets. This is all pretty unsightly and obtrusive. However, an idea then emerged that offers a perfect solution. Each room has a WiFi router positioned centrally on the ceiling, and each router has a spare USB socket. The design goal was therefore to design a plug-in unit that can be positioned in this USB socket. There certainly shouldn’t be an issue with WiFi connectivity!

WiFi ceiling mounted routers
Routers have a spare USB socket

We therefore looked to find a suitable case to fit a Particle photon, and the sensor, with the ability to plug into the USB socket. We found the perfect case from Farnell, the Hammond 1551USB3TSK USB Plastic Enclosure. We also bought a right angled USB PCB plug. Together with the Photon and BME680, all the parts looked like this.

Components being assembled for build

The next stage involved soldering the PCB USB connector onto a piece of veroboard and cutting it to fit with the trusty Dremmel saw, drill and deburring tool.

After much fiddling with the components, the piece was starting to take shape. The first task was to fit the USB connector. We cut out a piece of veroboard and then cut it to fit. As can be seen this was a pretty fiddly task. The USB plug needed fixing in place so the case could plugged in and removed without the USB plugboard moving. Next time we realised we should use the case pillars better and cut a slot out to fit around them!

Finally, after the USB board was fitted, the rest of the components could be fitted and wired up. The other end of the case then had a small slot routed in to allow the sensor to be stuck through to poke outside. We found that in use the Photon can heat up, so we wanted the temperature sensor located as far away as possible from it and to have the sensor exposed to the air. We then needed to wire the components up permanently which meant soldering wires in rather than using the header pillars that our earlier prototype had used. We had our wiring diagram to work to from our prototype.

We realised there was not enough space in the case for adding header blocks on the Photon or the sensor, so soldering directly to their PCBs was the only way to proceed. Soldering makes a permanent join, but the electronics of both the photon and BME680 sensor are very delicate, so we were careful to use the absolute minimum heating time from the soldering iron to make the joins, and use a solder with plenty of flux. This resulted in satisfactory joins. Also, we needed to get the 5v power to the Photon from the USB plug unit. To to this we took the power lines off and soldered directly to the VIn and Gnd pins on the photon (bypassing the micro USB socket).

The final wiring in place, the unit is ready for assembly

Once the wiring was all in place and all the solder joints checked carefully under a powerful magnifying lens (to check there were no ‘dry’ joints), the components could be packed out with sticky pads to ensure there was no rattling around. Finally we could fit the lid on the case and screw it all into place.

The final assembly was very satisfactory. We could turn it on to test it by plugging it into a USB charger battery. The photon was brand new and so commenced the setup routine with the flashing blue light. We were able to then connect to it with our mobile, claim and name the deice, and then upload the source code previously written. able to save off a JSON structure with all the data included to the Particle cloud on a regular basis. The Photon’s excellent design means that even once one or more units are deployed, their code can be flashed remotely to update their functionality. Multiple devices can be grouped into ‘products’ to allow concurrent code maintenance. We realise we will have to work out how to have photons automatically identify themselves individually when multiple data sources are collated to one database – something to think about next!

The new sensor in use and generating data

A test of the electrical current for the completed unit shows a draw of about 530mAh, which is pretty small for such a sensor and acceptable in that it isn’t drawing off too much current.

Epilogue

The exercise here is more one of hardware than of software – all the code and software methodologies were sorted as described with the earlier prototype. Here we have a physical design which will do what we want, plugging unobtrusively into a spare USB socket located on a WiFi router on the ceiling. The device will be now fixed into place in a lecture room and tested. To extend the project to a campus-wide solution, ideally a custom PCB would be created, designed to fit the case perfectly (or a dedicated case could be designed and 3D printed). Also, to gather the data together, potentially from multiple sources we will need a ‘dashboard’ and a linked database in a system able to receive the data streams. This solution would be bigger than the ThingSpeak tooling we have used for recent prototypes. We have been experimenting with ThingsBoard for this system scale solution, with a provisional version already running on a test Raspberry Pi – perhaps this will become the subject for a future blog.

Particulates Sensing with the NOVA SDS011

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are gathering environmental data from IoT devices and building data dashboards to show the data and related analyses. One of our projects will be to investigate air quality on the campus, in our lecture rooms and public spaces. Cranfield is a unique University in the UK for having its own airfield as part of the campus – we want to monitor any particular impacts that can arise from this. To do this, one of the tools we will use is the amazing Nova SDS011 particulates sensor (http://www.inovafitness.com/en/a/index.html).

The sensor itself, available from many outlets for instance here, is extremely cheap for what it offers, and is widely reported on with many projects on the Internet. We followed the excellent tutorial laid out on Hackernoon (https://hackernoon.com/how-to-measure-particulate-matter-with-a-raspberry-pi-75faa470ec35). We used a Raspberry Pi Zero, and we used the USB interface to speed the process of prototyping.

Rather than repeat the instructions laid out so well by Hackernoon, here we have some observations, and then some small adaptations to enable notifications and data logging.

One thing to remember in using the Raspberry Pi is that you need adapters (shown above) to connect traditional USB plugs to the micro plugs on the Pi. Also you need to remember that of the two USB ports, one is for powering the device and one is for peripherals. Plugging them in the wrong way round led to lots of unnecessary head scratching!

That said, once the instructions were followed, and the code put in place, the system was up and running and we could access the simple dashboard Hackernoon have developed using lighttpd.

This could be the end of the blog, all worked well, we have readings and a simple dashboard showing AQI. The device is incredibly sensitive – we can attest that during building the setup a late night pizza was accidentally burned (too busy hacking)! But the machine picked up the spike in particulates very well.

So the next challenge was to log the data being generated. In earlier blogs, we have used and liked ThingSpeak as a quick means to log data and build dashboards, so we decided to use this. This meant editing the Python code that hacker noon provided.

To write to ThingSpeak in Python, one can use the ‘urllib2’ library. We followed the excellent Instructables blog to do this. First, at the top of the code we import the urllib2 library and set up a variable to hold the connection string to ThingSpeak (using the API key for writing to the Channel we have created to hold the data):

<code>import urllib2 baseURL = 'http://api.thingspeak.com/update?api_key=CHANNEL_WRITE_API_KEY'</code>

Next, we located in the code where the particulate values for PM2.5 and PM10 are extracted and sent off to the web dashboard (full code used at the end). Here we inserted code to also send the same data to ThingSpeak:

<code>f = urllib2.urlopen(baseURL + '&amp;field1=' + str(values[0]) + '&amp;field2=' + str(values[1]))
f.read()
f.close()</code>

This worked well and data was transmitted to ThingSpeak and with its timestamp, this enabled a more comprehensive dashboard to be created that monitored the data values detected by the device (rather than the AQI values shown in the Hackernoon dashboard – clearly one could write that conversion in python in future if needed).

We then followed Hackernoon’s instructions to make the process start up on boot by placing the script into the crontab file. However, in doing this we realised it isn’t always possible to know when the script has started. As the script only starts on boot, if something goes wrong, the script never runs. We found that this was not a unique issue as others have found this also in other blogs. Thanks to the instructions on the Raspberry Pi website, we realised we could add a sleep command in to the crontab to ensure that the script was only started when there was a good chance the rest of the system was up and running. This solved the problem and now the crontab command was:

<code>@reboot sleep 60 &amp;&amp; cd /home/pi/ &amp;&amp; ./aqi.py</code>

The time could be extended from 60 seconds if needed. In any case, we now wanted to know it had indeed started up OK. We wanted a message sent to a mobile phone to say the process had started up OK. To do this we used the push notification approach of Prowl used in earlier blogs on this site (you need an iPhone for this although there will be equivalents for other phones. To get prowl to work in Python, we used the Python module for Prowl iPhone notification service from jacobb at https://github.com/jacobb/prowlpy. Installing this means downloading the ‘prowlpy.py’ script, and then a further adaptation in the aqi script at the start to call it appropriately, thus:

<code>import prowlpy
 apikey = 'PROWL_API_KEY'
 p = prowlpy.Prowl(apikey)
 try:
     p.add('AirQual','Starting up',"System commencing", 1, None, "http://www.prowlapp.com/")
     print('Success')
 except Exception,msg:
     print(msg)</code>

Finally, were it required, the push notification approach could also be used to inform particulate readings. The values of pm can also be intercepted, as per the ThingSpeak export, to send to the mobile phone too, code to do this would be thus:

<code>_message = "pm25: %.2f, pm10: %.2f, at %s" % (values[0], values[1], time.strftime("%d.%m.%Y %H:%M:%S"))          
print(_message) # debug line 
try:
    p.add('AirQual','Reading', _message, 1, None, "http://www.prowlapp.com/") 
except Exception,msg:
    print(msg)</code>

Although this worked perfectly, the phone was immediately overwhelmed with the number of messages, and this was quickly turned off! Notifications could be used however to message the user’s phone if important air quality thresholds were breached – reminding the operator to, for example, take the pizza out of the oven!

The final code script used for ‘aqi.py’ was:

<code>#!/usr/bin/python -u
# coding=utf-8
# "DATASHEET": http://cl.ly/ekot
# https://gist.github.com/kadamski/92653913a53baf9dd1a8
from __future__ import print_function
import serial, struct, sys, time, json, subprocess

# Customisations ######
import urllib2
baseURL = 'http://api.thingspeak.com/update?api_key=THINGSPEAK_API'

import prowlpy
apikey = 'PROWL_API_CODE'
p = prowlpy.Prowl(apikey)
try:
    p.add('AirQual','Starting up',"System commencing", 1, None, "http://www.prowlapp.com/")
    print('Success')
except Exception,msg:
    print(msg)
####################

DEBUG = 0
CMD_MODE = 2
CMD_QUERY_DATA = 4
CMD_DEVICE_ID = 5
CMD_SLEEP = 6
CMD_FIRMWARE = 7
CMD_WORKING_PERIOD = 8
MODE_ACTIVE = 0
MODE_QUERY = 1
PERIOD_CONTINUOUS = 0

JSON_FILE = '/var/www/html/aqi.json'

MQTT_HOST = ''
MQTT_TOPIC = '/weather/particulatematter'

ser = serial.Serial()
ser.port = "/dev/ttyUSB0"
ser.baudrate = 9600

ser.open()
ser.flushInput()

byte, data = 0, ""

def dump(d, prefix=''):
    print(prefix + ' '.join(x.encode('hex') for x in d))

def construct_command(cmd, data=[]):
    assert len(data) &lt;= 12
    data += [0,]*(12-len(data))
    checksum = (sum(data)+cmd-2)%256
    ret = "\xaa\xb4" + chr(cmd)
    ret += ''.join(chr(x) for x in data)
    ret += "\xff\xff" + chr(checksum) + "\xab"

    if DEBUG:
        dump(ret, '> ')
    return ret

def process_data(d):
    r = struct.unpack('&lt;HHxxBB', d[2:])
    pm25 = r[0]/10.0
    pm10 = r[1]/10.0
    checksum = sum(ord(v) for v in d[2:8])%256
    return [pm25, pm10]
    #print("PM 2.5: {} μg/m^3  PM 10: {} μg/m^3 CRC={}".format(pm25, pm10, "OK" if (checksum==r[2] and r[3]==0xab) else "NOK"))

def process_version(d):
    r = struct.unpack('&lt;BBBHBB', d[3:])
    checksum = sum(ord(v) for v in d[2:8])%256
    print("Y: {}, M: {}, D: {}, ID: {}, CRC={}".format(r[0], r[1], r[2], hex(r[3]), "OK" if (checksum==r[4] and r[5]==0xab) else "NOK"))

def read_response():
    byte = 0
    while byte != "\xaa":
        byte = ser.read(size=1)

    d = ser.read(size=9)

    if DEBUG:
        dump(d, '&lt; ')
    return byte + d

def cmd_set_mode(mode=MODE_QUERY):
    ser.write(construct_command(CMD_MODE, [0x1, mode]))
    read_response()

def cmd_query_data():
    ser.write(construct_command(CMD_QUERY_DATA))
    d = read_response()
    values = []
    if d[1] == "\xc0":
        values = process_data(d)
    return values

def cmd_set_sleep(sleep):
    mode = 0 if sleep else 1
    ser.write(construct_command(CMD_SLEEP, [0x1, mode]))
    read_response()

def cmd_set_working_period(period):
    ser.write(construct_command(CMD_WORKING_PERIOD, [0x1, period]))
    read_response()

def cmd_firmware_ver():
    ser.write(construct_command(CMD_FIRMWARE))
    d = read_response()
    process_version(d)

def cmd_set_id(id):
    id_h = (id>>8) % 256
    id_l = id % 256
    ser.write(construct_command(CMD_DEVICE_ID, [0]*10+[id_l, id_h]))
    read_response()

def pub_mqtt(jsonrow):
    cmd = ['mosquitto_pub', '-h', MQTT_HOST, '-t', MQTT_TOPIC, '-s']
    print('Publishing using:', cmd)
    with subprocess.Popen(cmd, shell=False, bufsize=0, stdin=subprocess.PIPE).stdin as f:
        json.dump(jsonrow, f)


if __name__ == "__main__":
    cmd_set_sleep(0)
    cmd_firmware_ver()
    cmd_set_working_period(PERIOD_CONTINUOUS)
    cmd_set_mode(MODE_QUERY);
    while True:
        cmd_set_sleep(0)
        for t in range(15):
            values = cmd_query_data();
            if values is not None and len(values) == 2 and values[0] != 0 and values[1] != 0:
              print("PM2.5: ", values[0], ", PM10: ", values[1])
              time.sleep(2)

	      # ThingSpeak ######
	      f = urllib2.urlopen(baseURL + '&amp;field1=' + str(values[0]) + '&amp;field2=' + str(values[1]))
	      f.read()
	      f.close()
              ###################

              # Push notifications ######
              #_message = "pm25: %.2f, pm10: %.2f, at %s" % (values[0], values[1], time.strftime("%d.%m.%Y %H:%M:%S"))
              #print(_message)
              #try:
              #	p.add('AirQual','Reading', _message, 1, None, "http://www.prowlapp.com/")
              #except Exception,msg:
              #  print(msg)
              ####################


        # open stored data
        try:
            with open(JSON_FILE) as json_data:
                data = json.load(json_data)
        except IOError as e:
            data = []

        # check if length is more than 100 and delete first element
        if len(data) > 100:
            data.pop(0)

        # append new values
        jsonrow = {'pm25': values[0], 'pm10': values[1], 'time': time.strftime("%d.%m.%Y %H:%M:%S")}
        data.append(jsonrow)

        # save it
        with open(JSON_FILE, 'w') as outfile:
            json.dump(data, outfile)

        if MQTT_HOST != '':
            pub_mqtt(jsonrow)

        print("Going to sleep for 1 min...")
        cmd_set_sleep(1)
        time.sleep(60)</code>

Raspberry Pi – Headless Setup

It’s been some time since we wrote our earlier blog describing setting up a Raspberry Pi, and a lot has changed since, including the base operating system. Raspbian Stretch, the latest version of the Debian port for the Raspberry Pi has a lot of great new features and so it is time for an update.

In this blog, we are setting up a Pi in headless mode – that is to say we want it to work over the WiFi via an ssh session from a remote computer from the start – and don’t want to be plugging it into a monitor with a keyboard etc.

The first step is to visit the Raspberry Pi Downloads page. Here, we can either download the ‘Noobs’ installer, or as we will the full Raspbian image. Downloading the Raspbian image, there is a choice between a version with and without a set of recommended software packages installed, Python, Scratch, Sonic Pi, Java etc. Although that is a very useful facility, in this case, we wanted a clean version of Raspbian, so downloaded the file ‘2019-04-08-raspbian-stretch.img’ (the other image file would have ‘-full’ as a suffix. When the file is downloaded, it is a zip file. This is then unzipped to the ‘img’ file.

We now need to use the ‘Etcher’ tool to install the image on our new microSD card. Since last using etcher, we note there is also a new version of this excellent utility from Balena too. We inserted the MicroSD card into a USB reader, inserted into the laptop and ran Etcher. From here we select the image, the destination card and hit ‘Flash’. The image is copied to the card and verified.

The new Balena Etcher programme

Once the image is copied over, we need to make the edits to the new installation to make it work on out network. Using a MacBook laptop, we unplugged the USB reader, and then plugged it back in again. This led to a new volume ‘Boot’ being mounted – an icon appears on the desktop.

The MicroSD card, its USB reader – and for Mac users, the dongle to get the Mac to read the USB ‘A’ device (to USB ‘C’).

We opened a terminal and changed to the new volume:

cd /Volumes/boot

Now we need to add two things, a file in this location called ‘ssh’ to enable secure shell access, and secondly the WiFi credentials.

sudo touch ssh

and to create and edit the Wifi configuration file:

sudo nano wpa_supplicant.conf

In the new file, for Raspbian Stretch, we add the following:

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
 network={
     ssid="WIFI-SSID"
     psk="WIFI-PASSWORD"
     key_mgmt=WPA-PSK
 }
Inserting the MicroSD card in the Pi (already in its case)

The card is now ready to be inserted into the Pi, and the machine booted up for the first time. Hopefully, the Pi will authenticate correctly on the WiFi network.

We can check the Router utility to see what DHCP address the Pi was assigned, or run a command such as ‘ifconfig’ or ‘arp -a’ to inspect connected devices. In our case the IP address, on a local network, could be for example 192.168.1.100

We should now then be able to ssh onto the Pi

ssh pi@192.168.1.100

The default password is ‘raspberry’. Once we are logged in a few important things. First is to change the system password:

passwd

Next, update and upgrade the system (see link):

sudo apt-get update
sudo apt-get dist-upgrade

If necessary, raspi-config can be run to permit further configuration:

sudo raspi-config

We now have a functioning Raspberry Pi, ready for our next project.

BME680 and the Particle Photon

Introduction

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are gathering environmental data from IoT devices and building data dashboards to show the data and related analyses. One of our projects will be to place environmental sensors in our lecture rooms and public spaces to allow our facilities team to monitor conditions across the campus. In this blog, we show how this project is starting to take shape, and in so doing explain how we are connecting the Particle Photon device up with Bosch’s amazing multifunction BME680 sensor.

The controller we use is the Particle Photon, described in an earlier post. We started with a device without header poles, and then soldered in the ones we will use [D0, D1, +ve, Gnd]. The Photon was then connected to the WiFi network, following the instructions on the particle website Quick Start Guide.

Hardware

Particle Photon

Next we used a Bosch BME680 sensor. This is able to measure temperature, pressure, humidity, and indoor air quality (IAQ) – the device currently returns gas resistivity in KOhms, rather than IAQ. It is also able to use the i2C interface, which only needs two connections, plus power (4 cables). Here the connection sockets are shown having been soldered in.

Bosch BME680 multi-function sensor



We then prepared four appropriate cables.

Cables

and then wired the devices up. The wiring connections used were:

PhotonBME680
D0SDA
D1SCL
3.3V2-6V
GNDGND













Code

Next, we opened the Particle Photon oneline cloud Web IDE. We created a new app, and located the Adafruit BME680 library and sample code.

The full code is presented below.

/***************************************************************************
  This is a library for the BME680 gas, humidity, temperature &amp; pressure sensor
  Designed specifically to work with the Adafruit BME680 Breakout
  ----&gt; http://www.adafruit.com/products/3660
  These sensors use I2C or SPI to communicate, 2 or 4 pins are required
  to interface.
  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing products
  from Adafruit!
  Written by Limor Fried &amp; Kevin Townsend for Adafruit Industries.
  BSD license, all text above must be included in any redistribution
 ***************************************************************************/

#include "Adafruit_BME680.h"

#define BME_SCK 13
#define BME_MISO 12
#define BME_MOSI 11
#define BME_CS 10

#define SEALEVELPRESSURE_HPA (1013.25)

Adafruit_BME680 bme; // I2C
//Adafruit_BME680 bme(BME_CS); // hardware SPI
//Adafruit_BME680 bme(BME_CS, BME_MOSI, BME_MISO,  BME_SCK);

double temperatureInC = 0;
double relativeHumidity = 0;
double pressureHpa = 0;
double gasResistanceKOhms = 0;
double approxAltitudeInM = 0;

void setup() {
   
  if (!bme.begin(0x76)) {
    Particle.publish("Log", "Could not find a valid BME680 sensor, check wiring!");
  } else {
    Particle.publish("Log", "bme.begin() success =)");
    // Set up oversampling and filter initialization
    bme.setTemperatureOversampling(BME680_OS_8X);
    bme.setHumidityOversampling(BME680_OS_2X);
    bme.setPressureOversampling(BME680_OS_4X);
    bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
    bme.setGasHeater(320, 150); // 320*C for 150 ms

    Particle.variable("temperature", &amp;temperatureInC, DOUBLE);
    Particle.variable("humidity", &amp;relativeHumidity, DOUBLE);
    Particle.variable("pressure", &amp;pressureHpa, DOUBLE);
    Particle.variable("gas", &amp;gasResistanceKOhms, DOUBLE);
    Particle.variable("altitude", &amp;approxAltitudeInM, DOUBLE);
  }
}

void loop() {
  if (! bme.performReading()) {
    Particle.publish("Log", "Failed to perform reading :(");
  } else {
    temperatureInC = bme.temperature;
    relativeHumidity = bme.humidity;
    pressureHpa = bme.pressure / 100.0;
    gasResistanceKOhms = bme.gas_resistance / 1000.0;
    approxAltitudeInM = bme.readAltitude(SEALEVELPRESSURE_HPA);

    // ThingSpeak Channel Info                        
    unsigned long myChannelNumber =999999;      //  From your ThingSpeak Account Info
    const char * myWriteAPIKey = "YOURAPIKEY";  //  From your ThingSpeak Account Info (API KEYS tab)

    String data = String::format(
      "{"
        "\"temperatureInC\":%.2f,"
        "\"humidityPercentage\":%.2f,"
        "\"pressureHpa\":%.2f,"
        "\"gasResistanceKOhms\":%.2f,"
        "\"approxAltitudeInM\":%.2f,"
        "\"key\":\"%s\""
      "}",
      temperatureInC,
      relativeHumidity,
      pressureHpa,
      gasResistanceKOhms,
      approxAltitudeInM,
      myWriteAPIKey);

    Particle.publish("Sensor", data, 60, PRIVATE, NO_ACK);
  
  }
  delay(10 * 1000);
}

Receiving data

Note the inclusion of the ThingsBoard API key ‘myWriteAPIKey’ into the JSON structure. ThingsBoard is used below.

Once we verified and flashed this code to the new Photon, it was able to start generating data. It took about 20 minutes to stabilise readings. Data was then picked up from the Particle.publish command in the source code on the Particle Console view. The data JSON structure is shown being generated (key not shown).

Particle Console

ThingSpeak Dashboard

Finally, following the approach outlined in this earlier blog, we built a ‘Webhook’ integration from the Particle web Console to ThingSpeak, and added a new Channel to receive the data to create a dashboard.

ThingsSpeak Dashboard – data shown with spline and averaged over 10 minutes

and as before the range of visualisations can be customised, and indeed the power of Matlab analytics can be blended in also.

Alternate dashboard with gauges

Epilogue

This blog has shown how easy it is to get a Particle Photon working with a Bosch BME680 multifunction sensor. As can be seen, the sensor outputs a range of data streams, barometric pressure, humidity, temperature and gas, and of these it is the gas resistance level, from which an Indoor Air Quality (IAQ) can be calculated, that is of particular interest. To quote Bosch, ‘The gas sensor within the BME680 can detect a broad range of gases to measure indoor air quality for personal well being. Gases that can be detected by the BME680 include: Volatile Organic Compounds (VOC) from paints (such as formaldehyde), lacquers, paint strippers, cleaning supplies, furnishings, office equipment, glues, adhesives and alcohol.’ This opens up a range of applications for this sensor which, combined with our project to monitor continuously public areas around the campus, a lot of options. As the Bosch technical sheet notes, IAQ provides a value from 0-500, with the following classification:

IAQ IndexAir Quality
0 – 50good
51 – 100average
101 – 150little bad
151 – 200bad
201 – 300worse
301 – 500very bad

The current device and software library only returns gas resistance, but a future project can be to link this to the Bosch libraries that calculate IAQ. There is also an interesting thread on the Pi Moroni blog for achieving this oneself, and more information here also.

Node-RED and the Internet of Things

Here at Cranfield University we are putting in place plans related to the new ‘Living Laboratory’ project, part of our ‘Urban Observatory’. This project sits within the wider UKCRIC initiative, across a number of universities. Of the many experiments in development, we are gathering environmental data from IoT devices and building data dashboards to show the data and related analyses.

In this blog we investigate the use of Node-RED (https://nodered.org) as a programming tool for wiring together hardware devices, APIs and online services, using its browser-based editor to wire together flows using the wide range of nodes in the palette that can be deployed to its runtime in a single-click. Node-RED provides graphical programming tool for Node-JS that permits complex programs to be built pictorially with great ease. To undertake the project, we used a WIO Node device collecting temperature values, exposing these values via a web service, and the Node-RED receiving device being a Raspberry Pi.

Sourcing temperature data – the Wio Node

The Wio Node temperature sensor was described in an earlier blog here (http://www.geothread.net/voice-activated-wio-node-temperature-sensor). Temperature values are extracted via a web-based API call, with the REST URL taking the form, thus:

https://us.wio.seeed.io/v1/node/GroveTemp1WireD1/temp?access_token=TOKEN_GOES_HERE

The temperature values are then returned as a JSON string, appearing thus:

{"temperature":19.1800000000001}

Preparing the Raspberry Pi – installing Node-RED

To prepare the Raspberry Pi and install Node-RED, we first followed instructions to install Node-JS on the Pi at https://www.w3schools.com/nodejs/nodejs_raspberrypi.asp. Next we followed the instructions on the Node-RED site (https://nodered.org/docs/hardware/raspberrypi). In brief, we ran the Node-RED upgrade script:

bash <(curl -sL https://raw.githubusercontent.com/node-red/raspbian-deb-package/master/resources/update-nodejs-and-nodered)

We then set Node-RED to start automatically on boot, with:

sudo systemctl enable nodered.service

Running Node-RED

The Raspberry Pi was then rebooted. We were then able to start using the Node-RED editor (https://nodered.org/docs/hardware/raspberrypi#using-the-editor), calling the web-based interface with the URL (the IP address being that if the Raspberry Pi):

http://{the-ip-address-returned}:1880/

The general Node-RED interface, ‘palette’ to the left, properties to the right, and design canvas centrally.

Node-RED allows installation of many modules, one of which permits data dashboards. The data dashboard module is described at https://flows.nodered.org/node/node-red-dashboard. Installation can be via npm, as described at the link above. However, we used the ‘Manage Palette’ option within the graphical interface to install the new functions.

With this installed, the next task was to develop the ‘flow’, or programme. This starts with a HTTP GET call to the WIO Node as described above. For this the ‘http request’ node is called, and configured with the URI to the temperature value. After consideration of the various configuration options, we elected to return a ‘parsed JSON object’.

To drive the process whereby the URI is called continuously, the http request call is preceded with an ‘inject node’, set to run continuously on a timed basis (shown here at 5 seconds, although that could be a longer period).

The data that is returned from this process, the ‘payload’, can now be passed directly to the first element of the dashboard – the gauge. The payload JSON object has a member ‘temperature’, referenced via the value format {{payload.temperature}}.

The next dashboard elements we wanted are firstly a line graph of temperature over time, and secondly a custom node recording the ‘minimum’ and ‘maximum’ temperatures over time. These nodes will need data prepared in a particular way. The graph, or chart, needs data in the form described at https://github.com/node-red/node-red-dashboard/blob/master/Charts.md.

{topic:"temperature", payload:22}

In addition, further JSON elements for minimum and maximum values will be required. In order to construct a revised message payload, a custom script is required. Explanations are in the code below:

// Create a new empty object 'newMsg' to return at the end
// then fill it with another empty object 'bounds'
var newMsg={bounds:{}}; // create

// Create two local variables min and max initialised from the persistent 
// context variables of the same names where these values exist, or else
// seed with values we know are off the scale
var min=context.get('min') || 100;
var max=context.get('max') || -100;

// Set an element 'topic' and give the value the string 'temperature'
newMsg.topic = 'temperature';
// Set the payload element to the incoming message payload temperature
newMsg.payload = msg.payload.temperature

// update the min and max, comparing the incoming values to the context
if (msg.payload.temperature < min) {
   newMsg.bounds.min = msg.payload.temperature;
   context.set('min', msg.payload.temperature);
} else {
   newMsg.bounds.min = min;
}
if (msg.payload.temperature > max) {
   newMsg.bounds.max = msg.payload.temperature;
   context.set('max', msg.payload.temperature);
} else {
   newMsg.bounds.max = max;
}

// and finally return the new object 'newMsg'
return newMsg;

What is always a good idea when processing data is to have a debug that shows the whole message object constructed by this process. To do this, a ‘debug node’ is added and configured – here to show the ‘complete msg object’. We can see the min and max are contained in the bounds node, and that the ‘topic’ and ‘payload’ elements are correctly configured.

As a result, the two additional dashboard node widgets can be added, first the chart node. The line interpolation is set here to ‘bezier’ to provide a smoother visualisation. The time interval is set to 15 minutes.

Next we wanted to add a new custom node widget to show a running maximum and minimum value. To do this, we added a ‘Template node’ and configured it thus:

<div layout="row" layout-align="start center">
  <span flex>Temp Min: </span>
  <span flex>Temp Max: </span>
</div>
<div layout="row" layout-align="start center" ng-repeat="bounds in msg">
  <span flex style="color: green">{{bounds.min}}</span>
  <span flex style="color: red">{{bounds.max}}</span>
</div>

Once these elements are all in place, the ‘flow’ programme can be deployed. This commences the running of the code, and then the dashboard can be accessed. The easiest means to do this is to follow the link in the properties section as shown:

The result is the display of the dashboard. To get this to display as required, one can change the visual style (e.g. to ‘dark’), and the dimensions of the canvas. Node dashboard widgets are always rendered to the top left according to the layout properties.

Epilogue

In this blog, we have shown how the Node-RED environment can be used to streamline Node-JS code, with customised elements, and inclusion of libraries of functionality (dashboard). Node-RED is a powerful yet easy to configure environment that is cable of a whole range of functionality though its graphical ‘flows’. There are many example flows available on websites that can be downloaded and tested. Flows are designed to be easily imported and exported. Below is the export for the flow described above – to load it, select ‘Import’ and ‘Clipboard’ from the main menu options and paste in the following.

<div layout="row" layout-align="start center">[{"id":"d988539b.52bdc8","type":"tab","label":"Temperature","disabled":false,"info":""},{"id":"35963a2e.6aa056","type":"tab","label":"Temperature","disabled":false,"info":""},{"id":"166841a0.b19cce","type":"mqtt-broker","z":"","broker":"192.168.0.6","port":"1883","clientid":"Teste","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"a76a54d5.4c5998","type":"ui_tab","z":"d988539b.52bdc8","name":"ESP_DTH11","icon":"dashboard","order":3,"disabled":false,"hidden":false},{"id":"519167a8.570e5","type":"ui_group","z":"d988539b.52bdc8","name":"DHT11","tab":"a76a54d5.4c5998","order":1,"disp":true,"width":"12","collapse":false},{"id":"1785bc54.de4d24","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":false},"page-titlebar-backgroundColor":{"value":"#097479","edited":false},"page-backgroundColor":{"value":"#111111","edited":false},"page-sidebar-backgroundColor":{"value":"#000000","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"}},"site":{"name":"Node-RED Dashboard","hideToolbar":"false","allowSwipe":"false","lockMenu":"false","allowTempTheme":"true","dateFormat":"DD/MM/YYYY","sizes":{"sx":48,"sy":48,"gx":6,"gy":6,"cx":6,"cy":6,"px":0,"py":0}}},{"id":"749056a0.0a1d28","type":"ui_group","z":"","name":"Chart","tab":null,"order":2,"disp":true,"width":"12","collapse":false},{"id":"684a7caa.4db0f4","type":"ui_group","z":"","name":"Chart","tab":"a76a54d5.4c5998","order":2,"disp":true,"width":"12","collapse":false},{"id":"84ea1128.ec6fd","type":"ui_tab","z":"35963a2e.6aa056","name":"ESP_DTH11","icon":"dashboard","order":3,"disabled":false,"hidden":false},{"id":"c86b0ed1.65efc8","type":"ui_group","z":"35963a2e.6aa056","name":"DHT11","tab":"84ea1128.ec6fd","order":1,"disp":true,"width":"12","collapse":false},{"id":"a7d331dd.9d8078","type":"debug","z":"d988539b.52bdc8","name":"Message object","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","x":1129.75,"y":286.9166564941406,"wires":[]},{"id":"aa810265.1f789","type":"ui_gauge","z":"d988539b.52bdc8","name":"Gauge","group":"519167a8.570e5","order":0,"width":"6","height":"2","gtype":"gage","title":"Temperature","label":"Celsius","format":"{{payload.temperature}}","min":0,"max":"60","colors":["#00b500","#e6e600","#ca3838"],"seg1":"25","seg2":"28","x":1086.833251953125,"y":432.8055419921875,"wires":[]},{"id":"aa922201.f96eb8","type":"inject","z":"d988539b.52bdc8","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":401.5,"y":394,"wires":[["cbeca854.f6174"]]},{"id":"cbeca854.f6174","type":"http request","z":"d988539b.52bdc8","name":"Wio Temperature","method":"GET","ret":"obj","paytoqs":false,"url":"https://us.wio.seeed.io/v1/node/GroveTemp1WireD1/temp?access_token=7c6297dfa2e48793c58a53269bc23ef0","tls":"","proxy":"","authType":"basic","x":610.5,"y":394,"wires":[["aa810265.1f789","cd69dcd.1c5d3a"]]},{"id":"af52c259.fecbd8","type":"ui_chart","z":"d988539b.52bdc8","name":"Chart","group":"684a7caa.4db0f4","order":2,"width":"12","height":"7","label":"Temperature chart","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"bezier","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"15                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ","removeOlderPoints":"50","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1087.8333740234375,"y":384.22222900390625,"wires":[[]]},{"id":"cd69dcd.1c5d3a","type":"function","z":"d988539b.52bdc8","name":"Process temperature","func":"var newMsg={bounds:{}};\nvar min=context.get('min') || 100;\nvar max=context.get('max') || -100;\n\n// http://www.steves-internet-guide.com/node-red-variables/\nnewMsg.topic = 'temperature';\nnewMsg.payload = msg.payload.temperature\n\nif (msg.payload.temperature &lt; min) {\n   newMsg.bounds.min = msg.payload.temperature;\n   context.set('min', msg.payload.temperature);\n} else {\n   newMsg.bounds.min = min;\n}\nif (msg.payload.temperature &gt; max) {\n   newMsg.bounds.max = msg.payload.temperature;\n   context.set('max', msg.payload.temperature);\n} else {\n   newMsg.bounds.max = max;\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"x":875.5,"y":336,"wires":[["af52c259.fecbd8","a7d331dd.9d8078","81bda4f5.6f104"]]},{"id":"259fa218.53bdbe","type":"comment","z":"d988539b.52bdc8","name":"Useful links","info":"see:\nhttps://github.com/node-red/node-red-dashboard/blob/master/Charts.md\nhttp://noderedguide.com/tutorial-node-red-dashboards-multiple-lines-on-a-chart/#more-1612\nhttp://www.steves-internet-guide.com/node-red-functions/\nhttp://www.steves-internet-guide.com/node-red-dashboard/","x":400.5,"y":337,"wires":[]},{"id":"81bda4f5.6f104","type":"ui_template","z":"d988539b.52bdc8","group":"519167a8.570e5","name":"Max and Min","order":2,"width":"6","height":"2","format":"</p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="">\n  <span flex="">Temp Min: </span>\n  <span flex="">Temp Max: </span>\n</div>
<p>\n</p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="" ng-repeat="\&quot;bounds" in="" msg\"="">\n  <span flex="" style="\&quot;color:" green\"="">{{bounds.min}}</span>\n  <span flex="" style="\&quot;color:" red\"="">{{bounds.max}}</span>\n</div>
<p>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1109.4444122314453,"y":336.6666717529297,"wires":[[]]},{"id":"f5bb5785.45e55","type":"debug","z":"35963a2e.6aa056","name":"Message object","active":true,"tosidebar":true,"console":true,"tostatus":false,"complete":"true","targetType":"full","x":1129.75,"y":286.9166564941406,"wires":[]},{"id":"27187c33.85c07c","type":"ui_gauge","z":"35963a2e.6aa056","name":"Gauge","group":"c86b0ed1.65efc8","order":0,"width":"6","height":"2","gtype":"gage","title":"Temperature","label":"Celsius","format":"{{payload.temperature}}","min":0,"max":"60","colors":["#00b500","#e6e600","#ca3838"],"seg1":"25","seg2":"28","x":1086.833251953125,"y":432.8055419921875,"wires":[]},{"id":"cd94fffc.6f0da8","type":"inject","z":"35963a2e.6aa056","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":401.5,"y":394,"wires":[["ab8d1686.0264d"]]},{"id":"ab8d1686.0264d","type":"http request","z":"35963a2e.6aa056","name":"Wio Temperature","method":"GET","ret":"obj","paytoqs":false,"url":"https://us.wio.seeed.io/v1/node/GroveTemp1WireD1/temp?access_token=&lt;<your token="" here="">&gt;","tls":"","proxy":"","authType":"basic","x":610.5,"y":394,"wires":[["27187c33.85c07c","ce9f0009.faab98"]]},{"id":"dbe1013a.863be8","type":"ui_chart","z":"35963a2e.6aa056","name":"Chart","group":"684a7caa.4db0f4","order":2,"width":"12","height":"7","label":"Temperature chart","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"bezier","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"15                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               ","removeOlderPoints":"50","removeOlderUnit":"60","cutout":0,"useOneColor":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1087.8333740234375,"y":384.22222900390625,"wires":[[]]},{"id":"ce9f0009.faab98","type":"function","z":"35963a2e.6aa056","name":"Process temperature","func":"var newMsg={bounds:{}};\nvar min=context.get('min') || 100;\nvar max=context.get('max') || -100;\n\nnewMsg.topic = 'temperature';\nnewMsg.payload = msg.payload.temperature\n\nif (msg.payload.temperature &lt; min) {\n   newMsg.bounds.min = msg.payload.temperature;\n   context.set('min', msg.payload.temperature);\n} else {\n   newMsg.bounds.min = min;\n}\nif (msg.payload.temperature &gt; max) {\n   newMsg.bounds.max = msg.payload.temperature;\n   context.set('max', msg.payload.temperature);\n} else {\n   newMsg.bounds.max = max;\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"x":875.5,"y":336,"wires":[["dbe1013a.863be8","f5bb5785.45e55","b809b11b.7f47c8"]]},{"id":"b809b11b.7f47c8","type":"ui_template","z":"35963a2e.6aa056","group":"c86b0ed1.65efc8","name":"Max and Min","order":2,"width":"6","height":"2","format":"</your></p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="">\n  <span flex="">Temp Min: </span>\n  <span flex="">Temp Max: </span>\n</div>
<p>\n</p><div layout="\&quot;row\&quot;" layout-align="\&quot;start" center\"="" ng-repeat="\&quot;bounds" in="" msg\"="">\n  <span flex="" style="\&quot;color:" green\"="">{{bounds.min}}</span>\n  <span flex="" style="\&quot;color:" red\"="">{{bounds.max}}</span>\n</div>
<p>\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1109.4444122314453,"y":336.6666717529297,"wires":[[]]}]