Raspberry Pi Exhibition Demonstrator

In this post, we describe the development of a demonstrator for an exhibition using a Raspberry Pi. The Digital Environment team are planning to be in support of NERC in attending the American Association for the Advancement of Science (AAAS) annual conference and exhibition in Seattle, USA in February 2020, and are making preparations. One of our tasks 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 a digital environmental sensing demonstrator.

Here we draw on the parallel blogging site we maintain, http://www.geothread.net. In previous blogs there, 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 Model 4 that we are using, 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 with header pins soldered on
BME680 reverse side
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/).

The Raspberry Pi 4 runs quite hot, so in fact we need to locate the temperature sensor away from it – so although the sensor will plug directly into the GPIO pin connector, we found using a ribbon cable between the sensor and the Pi to separate the two led to better results.

For the display, we will use the official Raspberry Pi 7 inch touch screen display (https://www.raspberrypi.org/products/raspberry-pi-touch-display/). With a display resolution of 800 x 480 pixels, the unit is quite large enough to display a dashboard. The screen can be scrolled by touch to show a longer display of items. The screen takes 5v and the power cables from the display are connected to the two Pi 4 GPIO pins +5v and ground. The ribbon cable is connected between the display and the Pi’s DSI port.

Software

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

After setting up the Pi, we ran raspi-config and set the option ‘3 Boot Options’ > ‘B1 Desktop / CLI’ > ‘B4 Desktop Autologin’. This means on booting up, the Pi is logged in and displays the operating system on the connected touchscreen.

With the BME680 breakout board now connected to the Pi, a Python script is required that can read the 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 as the default (e.g. we didn’t need to solder the pads on the breakout board). We used the raspi-config tool to ensure the I2C was enabled on the Pi (‘5 Interface Options’ > ‘P5 I2C’). We then 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 data format based on JSON.

#!/usr/bin/env python

import bme680
import time

try:
    sensor = bme680.BME680(bme680.I2C_ADDR_PRIMARY)
except IOError:
    sensor = bme680.BME680(bme680.I2C_ADDR_SECONDARY)

# These oversampling settings can be tweaked to
# change the balance between accuracy and noise in
# the data.

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)

#print('\n\nInitial reading:')
#for name in dir(sensor.data):
#    value = getattr(sensor.data, name)
#    if not name.startswith('_'):
#        print('{}: {}'.format(name, value))

sensor.set_gas_heater_temperature(320)
sensor.set_gas_heater_duration(150)
sensor.select_gas_heater_profile(0)

# Up to 10 heater profiles can be configured, each
# with their own temperature and duration.
# sensor.set_gas_heater_profile(200, 150, nb_profile=1)
# sensor.select_gas_heater_profile(1)

#print('\n\nPolling:')
try:
    while True:
        if sensor.get_sensor_data():
            output = '{{"Temperature_oC":{0:.2f}}},{{"Pressure_HPA":{1:.2f}}},{{"RelativeHumidity_percent":{2:.2f}}}'.format(
                sensor.data.temperature,
                sensor.data.pressure,
                sensor.data.humidity)
            #output = '{0:.2f} C,{1:.2f} hPa,{2:.2f} %RH'.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))
                #print('{0},{1} Ohms'.format(
                #    output,
                #    sensor.data.gas_resistance))
            else:
                print(output)

        time.sleep(10)

except KeyboardInterrupt:
    pass

We then ran the script just to make sure it produced data, which it did thus:

$ python demo_code.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 on the touchscreen and to do this we wanted to use the open source NodeRED tool (built on Node.JS). We have used NodeRED in earlier GeoThread blogs, see (http://www.geothread.net/node-red-and-the-internet-of-things/) to build interfaces and to gather data, and the installation instructions are also described there. The Node-RED installation installs Node.JS also.

Once the default Node-RED is installed, we need to install some additional functionality. The NodeRed Pallette (installed modules) will need updating – adding ‘node-red-contrib-pythonshell‘ and ‘node-red-dashboard‘.

Next we build the flow (script) to obtain the data, which it does by running the Python script, and then formatting the output into a Node-RED dashboard. When complete the flow looked like this:

Node-RED graphical Flow
Node-RED graphical Flow

In NodeRED, once developed, a flow itself can be saved off to a JSON text file, and used to recreate the flow. The exported code for the flow was as follows (this JSON can be imported to form a new flow):

[{"id":"d551e5.6a5cfe18","type":"tab","label":"BME680","disabled":false,"info":""},{"id":"45811201.43cc8c","type":"pythonshell in","z":"d551e5.6a5cfe18","name":"BME680 Reader","pyfile":"/home/pi/bme680/examples/digi-env.py","virtualenv":"","continuous":true,"stdInData":false,"x":320,"y":240,"wires":[["82fc0a5c.5303a"]]},{"id":"552a34f5.caaf84","type":"debug","z":"d551e5.6a5cfe18","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":710,"y":440,"wires":[]},{"id":"aca78c5f.5728a","type":"json","z":"d551e5.6a5cfe18","name":"","property":"payload","action":"obj","pretty":false,"x":410,"y":340,"wires":[["b323191b.6a76d"]]},{"id":"ddc4d3ef.871be","type":"debug","z":"d551e5.6a5cfe18","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":710,"y":340,"wires":[]},{"id":"82fc0a5c.5303a","type":"split","z":"d551e5.6a5cfe18","name":"","splt":",","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":290,"y":340,"wires":[["aca78c5f.5728a"]]},{"id":"9bce8f10.5d73a","type":"ui_gauge","z":"d551e5.6a5cfe18","name":"Pressure Gauge","group":"e66437e4.1a68e8","order":5,"width":"8","height":"4","gtype":"gage","title":"","label":"Pressure 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":"1277efb.aa0f69","type":"comment","z":"d551e5.6a5cfe18","name":"Process BME680 data","info":"","x":160,"y":180,"wires":[]},{"id":"e3e148eb.6c59d","type":"ui_gauge","z":"d551e5.6a5cfe18","name":"Temperature Gauge","group":"e66437e4.1a68e8","order":1,"width":"8","height":"4","gtype":"gage","title":"","label":"Temperature oCelsius","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":"5f6e7522.ca290c","type":"inject","z":"d551e5.6a5cfe18","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":130,"y":240,"wires":[["45811201.43cc8c"]]},{"id":"f7a4502f.f25b58","type":"ui_chart","z":"d551e5.6a5cfe18","name":"Temperature Chart (oCelsius)","group":"863f6151.2b3ca8","order":1,"width":"17","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":1328.5000858306885,"y":263.2778015136719,"wires":[[]]},{"id":"408a9812.361928","type":"function","z":"d551e5.6a5cfe18","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":[["f7a4502f.f25b58"]]},{"id":"e12687de.a17a","type":"function","z":"d551e5.6a5cfe18","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":[["e91b44a2.210da8"]]},{"id":"e91b44a2.210da8","type":"ui_chart","z":"d551e5.6a5cfe18","name":"Pressure Chart (HPA)","group":"b4c4e00d.cbf16","order":2,"width":"17","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":1317.5276718139648,"y":613.6111507415771,"wires":[[]]},{"id":"3434c172.45b926","type":"function","z":"d551e5.6a5cfe18","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":[["5fd30172.4d4c58"]]},{"id":"5b7698f6.faafc8","type":"ui_gauge","z":"d551e5.6a5cfe18","name":"Humidity Gauge","group":"e66437e4.1a68e8","order":6,"width":"8","height":"4","gtype":"gage","title":"","label":"Humidity %","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":"5fd30172.4d4c58","type":"ui_chart","z":"d551e5.6a5cfe18","name":"Humidity Chart (%)","group":"a7a0d7.6c039f28","order":1,"width":"17","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":1306.1944618225098,"y":493.38890838623047,"wires":[[]]},{"id":"8aee2381.d5cf3","type":"function","z":"d551e5.6a5cfe18","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":[["67b6c72e.d18d3"]]},{"id":"8c364894.a362a8","type":"ui_gauge","z":"d551e5.6a5cfe18","name":"VOC Gauge","group":"e66437e4.1a68e8","order":2,"width":"8","height":"4","gtype":"gage","title":"","label":"VOC 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":"67b6c72e.d18d3","type":"ui_chart","z":"d551e5.6a5cfe18","name":"VOC Chart (Ohms)","group":"ff8a4ab5.f3ed5","order":1,"width":"17","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":1300.5832595825195,"y":378.05558013916016,"wires":[[]]},{"id":"b3022fd7.ba64f8","type":"ui_template","z":"d551e5.6a5cfe18","group":"e66437e4.1a68e8","name":"Temperature Max and Min","order":3,"width":"8","height":"1","format":"<div layout=\"row\" layout-align=\"start center\">\n  <span flex>&emsp;&emsp;&emsp;&emsp;Min (oC):&emsp;<span style=\"color: green\">{{msg.temp.min}}</span>\n  &emsp;&emsp;Max:&emsp;<span flex style=\"color: red\">{{msg.temp.max}}</span></span>\n</div>","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1322.5,"y":340.583327293396,"wires":[[]]},{"id":"d24141b4.8ed88","type":"function","z":"d551e5.6a5cfe18","name":"Prepare data","func":"var newMsg={temp:{},pressure:{},humidity:{},voc:{}};\nvar temp_min=flow.get('temp_min') || 100;\nvar temp_max=flow.get('temp_max') || -100;\nvar pressure_min=flow.get('pressure_min') || 5000;\nvar pressure_max=flow.get('pressure_max') || -5000;\nvar humidity_min=flow.get('humidity_min') || 100;\nvar humidity_max=flow.get('humidity_max') || -100;\nvar voc_min=flow.get('voc_min') || 500000;\nvar voc_max=flow.get('voc_max') || -500000;\n//\nnewMsg.payload = msg.payload;\n\n//\n\nif (msg.payload.Temperature_oC < temp_min) {\n   newMsg.temp.min = msg.payload.Temperature_oC;\n   flow.set('temp_min', msg.payload.Temperature_oC);\n} else {\n   newMsg.temp.min = temp_min;\n}\nif (msg.payload.Temperature_oC > temp_max) {\n   newMsg.temp.max = msg.payload.Temperature_oC;\n   flow.set('temp_max', msg.payload.Temperature_oC);\n} else {\n   newMsg.temp.max = temp_max;\n}\n\n//\n\nif (msg.payload.Pressure_HPA < pressure_min) {\n   newMsg.pressure.min = msg.payload.Pressure_HPA;\n   flow.set('pressure_min', msg.payload.Pressure_HPA);\n} else {\n   newMsg.pressure.min = pressure_min;\n}\nif (msg.payload.Pressure_HPA > pressure_max) {\n   newMsg.pressure.max = msg.payload.Pressure_HPA;\n   flow.set('pressure_max', msg.payload.Pressure_HPA);\n} else {\n   newMsg.pressure.max = pressure_max;\n}\n\n//\n\nif (msg.payload.RelativeHumidity_percent < humidity_min) {\n   newMsg.humidity.min = msg.payload.RelativeHumidity_percent;\n   flow.set('humidity_min', msg.payload.RelativeHumidity_percent);\n} else {\n   newMsg.humidity.min = humidity_min;\n}\nif (msg.payload.RelativeHumidity_percent > humidity_max) {\n   newMsg.humidity.max = msg.payload.RelativeHumidity_percent;\n   flow.set('humidity_max', msg.payload.RelativeHumidity_percent);\n} else {\n   newMsg.humidity.max = humidity_max;\n}\n\n//\n\nif (msg.payload.Resistance_Ohms < voc_min) {\n   newMsg.voc.min = msg.payload.Resistance_Ohms;\n   flow.set('voc_min', msg.payload.Resistance_Ohms);\n} else {\n   newMsg.voc.min = voc_min;\n}\nif (msg.payload.Resistance_Ohms > voc_max) {\n   newMsg.voc.max = msg.payload.Resistance_Ohms;\n   flow.set('voc_max', msg.payload.Resistance_Ohms);\n} else {\n   newMsg.voc.max = voc_max;\n}\n\nreturn newMsg;","outputs":1,"noerr":0,"x":730,"y":380,"wires":[["552a34f5.caaf84","408a9812.361928","e3e148eb.6c59d","b3022fd7.ba64f8","8aee2381.d5cf3","3434c172.45b926","e12687de.a17a","8c364894.a362a8","5b7698f6.faafc8","9bce8f10.5d73a","9f7adbf3.d62af8","9d6fbcc2.49f4b8","85b65f56.e570b"]]},{"id":"b323191b.6a76d","type":"join","z":"d551e5.6a5cfe18","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":530,"y":340,"wires":[["d24141b4.8ed88","ddc4d3ef.871be"]]},{"id":"9f7adbf3.d62af8","type":"ui_template","z":"d551e5.6a5cfe18","group":"e66437e4.1a68e8","name":"Pressure max and min","order":7,"width":"8","height":"1","format":"<div layout=\"row\" layout-align=\"start center\">\n  <span flex>&emsp;&emsp;&emsp;&emsp;Min (HPA):&emsp;<span style=\"color: green\">{{msg.pressure.min}}</span>\n  &emsp;&emsp;Max:&emsp;<span flex style=\"color: red\">{{msg.pressure.max}}</span></span>\n</div>","storeOutMessages":false,"fwdInMessages":true,"templateScope":"local","x":1317.9166297912598,"y":701.2499589920044,"wires":[[]]},{"id":"9d6fbcc2.49f4b8","type":"ui_template","z":"d551e5.6a5cfe18","group":"e66437e4.1a68e8","name":"Humidity max and min","order":8,"width":"8","height":"1","format":"<div layout=\"row\" layout-align=\"start center\">\n  <span flex>&emsp;&emsp;&emsp;&emsp;Min (%):&emsp;<span style=\"color: green\">{{msg.humidity.min}}</span>\n  &emsp;&emsp;Max:&emsp;<span flex style=\"color: red\">{{msg.humidity.max}}</span></span>\n</div>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1316.250087738037,"y":574.5833158493042,"wires":[[]]},{"id":"85b65f56.e570b","type":"ui_template","z":"d551e5.6a5cfe18","group":"e66437e4.1a68e8","name":"VOC max and min","order":4,"width":"8","height":"1","format":"<div layout=\"row\" layout-align=\"start center\">\n  <span flex>&emsp;&emsp;&emsp;Min (Ohms):&emsp;<span style=\"color: green\">{{msg.voc.min}}</span>\n  &emsp;&emsp;Max:&emsp;<span flex style=\"color: red\">{{msg.voc.max}}</span></span>\n</div>","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":1304.5833892822266,"y":454.58332443237305,"wires":[[]]},{"id":"452a5198.520f6","type":"ui_button","z":"d551e5.6a5cfe18","name":"Halt","group":"b4c4e00d.cbf16","order":4,"width":"1","height":"1","passthru":false,"label":"Halt","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":1270,"y":760,"wires":[["64212229.98256c"]]},{"id":"64212229.98256c","type":"exec","z":"d551e5.6a5cfe18","command":"sudo poweroff #","append":"","useSpawn":"","name":"turn off Pi","x":1420,"y":760,"wires":[[],[],[]]},{"id":"4926710d.cc6af","type":"ui_button","z":"d551e5.6a5cfe18","name":"","group":"b4c4e00d.cbf16","order":2,"width":"1","height":"1","passthru":false,"label":"Reset","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":410,"y":380,"wires":[["82d547b0.18997"]]},{"id":"82d547b0.18997","type":"function","z":"d551e5.6a5cfe18","name":"Reset","func":"flow.set('temp_min', 100);\nflow.set('temp_max', -100);\nflow.set('pressure_min', 5000);\nflow.set('pressure_max', -5000);\nflow.set('humidity_min', 100);\nflow.set('humidity_max', -100);\nflow.set('voc_min', 5000000);\nflow.set('voc_max', -5000000);\nreturn msg;","outputs":1,"noerr":0,"x":530,"y":380,"wires":[["d24141b4.8ed88"]]},{"id":"e66437e4.1a68e8","type":"ui_group","z":"","name":"Dashboard","tab":"6c90afc.b3623d","order":1,"disp":false,"width":"16","collapse":false},{"id":"863f6151.2b3ca8","type":"ui_group","z":"","name":"Temperature (oCelsius)","tab":"6c90afc.b3623d","order":2,"disp":true,"width":"17","collapse":false},{"id":"b4c4e00d.cbf16","type":"ui_group","z":"","name":"Pressure (HPA)","tab":"6c90afc.b3623d","order":5,"disp":true,"width":"17","collapse":false},{"id":"a7a0d7.6c039f28","type":"ui_group","z":"","name":"Humidity (%)","tab":"6c90afc.b3623d","order":4,"disp":true,"width":"17","collapse":false},{"id":"ff8a4ab5.f3ed5","type":"ui_group","z":"","name":"VOC (Ohms)","tab":"6c90afc.b3623d","order":3,"disp":true,"width":"17","collapse":false},{"id":"6c90afc.b3623d","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 7 inch touch screen. The final settings used are shown below. Note that the settings depend on the screen monitor/TV used:

To configure the dashboard for the 7 inch touchscreen, we set the 1×1 widget pixel size to 45 x 45 pixels.

Dashboard site settings
Dashboard site settings

We then configured the dashboard sizings as follows:
Gauge = 8×3 units
Max and Min = 8×1 units
Chart = 17×4 units

and the order of display thus:

Dashboard layout settings
Dashboard layout settings

A spacer was also inserted to separate the two buttons. Also, we preferred the ‘dark theme’, thus:

Dashboard theme settings
Dashboard theme settings

We can now connect to the dashboard and see the real-time streaming sensor data. 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).

Node-RED Dashboard viewed via connected laptop browser
Node-RED Dashboard viewed via connected laptop browser

Connecting to the dashboard using a remote laptop browser is a neat trick, as one can edit the settings on the laptop and the dashboard rebuilds automatically on the connected 7 inch touch screen.

Configuring Kiosk mode for the dashboard

The final requirement was to enable the Pi to boot from cold and run the dashboard automatically. 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 we created a script to run the dashboard, saved to file ‘/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

Note the reference to the ‘localhost’ address (e.g. run the web URL on the current computer). 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
 Type=simple
 ExecStart=/bin/bash /home/pi/kiosk.sh
 Restart=on-abort
 User=pi
 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 automatically 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 should now automatically appear (it takes a while to appear).

Epilogue

We wanted to have a demonstrator for an exhibition that could run unattended, started automatically on power up, and 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 Node-RED configuration page on the Pi (<<IP address of Pi>>:1880) – then, as the code is redeployed, or the dashboard rebuilt – the attached touch screen 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 – was added at the foot of the dashboard, as it is generally not advised to turn off a Pi by just pulling the plug!! Another button resets the counters. If we are running without a mouse or keyboard, but using the Raspberry Pi screen, which is a touchscreen, a further ‘touch’ setting needed to be added to the call made to the Chromium browser, thus: (/usr/bin/chromium-browser –touch-events –noerrdialogs –disable-infobars –kiosk http://localhost:1880/ui).