Raspberry Pi Environmental Monitoring
Contents
Purpose
To monitor air temperature, humidity, and air quality.
Air Quality
Base Config for Sensor
A friend sent me a Nova PM Sensor. I do not know python yet, but looking over the script that is readily available[1][2] I should be able to modify this to work for my needs.
mkdir -p /home/pi/build/air && cd /home/pi/build/air sudo apt install git-core python-serial python-enum mariadb-server python-mysql.connector wget -O aqi.py https://raw.githubusercontent.com/zefanja/aqi/master/python/aqi.py echo [] > aqi.json sed -i 's|/var/www/html/aqi.json|/home/pi/build/air/aqi.json|' aqi.py
At this point we can run the script, see output on the screen, and see the history written to the json file.
./aqi.py Y: 18, M: 1, D: 18, ID: 0xbc2d, CRC=OK PM2.5: 0.0 , PM10: 0.0 PM2.5: 0.7 , PM10: 0.7 PM2.5: 0.6 , PM10: 0.6 PM2.5: 0.7 , PM10: 0.7
Configure for writing to database
However, I want the history written to a database. Configure mariadb anyway you want, if you have credentials keep them on hand.
create database air; create user air; grant all on air.* to 'air'; use air; create table aqi (pm25 FLOAT, pm10 FLOAT, date DATETIME); select * from aqi; Empty set (0.00 sec) insert into aqi values (2.1, 2.3, now()); Query OK, 1 row affected (0.02 sec) select * from aqi; +------+------+---------------------+ | pm25 | pm10 | date | +------+------+---------------------+ | 2.1 | 2.3 | 2019-12-20 13:32:18 | +------+------+---------------------+ 1 row in set (0.00 sec)
Now we edit the script to work with mysql[3]
Please note that of this writing I the script searches the entire database for 0 values and deletes them. I need to go back and update to simply not write 0 values in the first place.
Full Modified Script
#!/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, mysql.connector mydb = mysql.connector.connect( host="localhost", user="air", database="air" ) mycursor = mydb.cursor() 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 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) <= 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('<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('<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, '< ') 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() 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: print("PM2.5: ", values[0], ", PM10: ", values[1]) pm25=values[0] pm10=values[1] mycursor.execute('INSERT INTO aqi (pm25, pm10, date) VALUES (%s, %s, %s)' % (pm25,pm10,'now()')) mydb.commit() mycursor.execute("delete from aqi where pm10 = 0 and pm25 = 0") time.sleep(2) print("Going to sleep for 1 min...") cmd_set_sleep(1) time.sleep(60) mycursor.execute("delete from aqi where date < (now() - interval 1 year)")
Tempurature / Humidity
For this one I decided to use node-red to pull the values directly. It supports raspberry pi GPIO and the DHT-11 sensor I have on hand[4].
- Install the node-red-contrib-dht-sensor packages (I used the web gui)
sudo apt install wiringpi
- After adding the rpi-dht11 node to the flow, I gave it the following values
- Sensor : DHT11
- Pin numbering : WiringPi (rev.1)
- Pin number : 7
- This breaks out to a function node that changes C to F [5]
var tempc = msg.payload; tempf = tempc * 9/5 + 32; tempf = Math.round(tempf * 10) / 10; msg.payload = tempf; return msg;
- We also send the output of the rpi-dht11 to a set node to change the payload from msg.humidity.
- Then add two gauges to represent temperature and humidity.
Graph Display
The same pointed me to node-red for the display. What I really wanted was a nerdy ncurses display, but node-red should suffice until I learn ncurses.
Install Node-Red
[6] When running the install script, make sure the pi settings are applied.
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered) sudo systemctl enable --now nodered
Install the dashboard components[7]
Configure PM 2.5 Flow
My flow consists of
- An inject node with the timestamp injection using topic
select pm25 from aqi order by date desc limit 1;
- A mysql node with the database location and credentials.
- A change node with set to
msg.payload[0].pm25
- A gauge node set for values between 0.5 and 2.
Configure Pi for Kiosk Mode
[8]Please note that this is not the most secure configuration. In my case the terminal is located in my house, on a segregated wifi network and broadcast domain, and the information contained is air quality metrics so impact of compromise is low.
sudo apt-get install lightdm sudo raspi-config
At this point go to boot options, desktop /CLI, then select the desktop autologin. Close the tool and reboot.
- Note : I ran into a login loop where I was unable to auto-login and manually logging in did not work. I found a forum[9] mentioning to install a set of x11 packages as well as openbox. I went ahead with this as I used open box years ago and found it very light weight and useful for such tasks.
sudo apt-get install xserver-xorg-core xserver-xorg-input-all \ xserver-xorg-video-fbdev libx11-6 x11-common \ x11-utils x11-xkb-utils x11-xserver-utils xterm lightdm openbox
After running the previous the system now logs me into openbox. At this point we want openbox to start chrome in kiosk mode.
sudo apt install chromium-browser mkdir -p ~/.config/openbox && cp /etc/xdg/openbox/* ~/.config/openbox echo 'chromium-browser --kiosk http://172.0.0.1:1880/ui' >> .config/openbox/autostart
- ↑ https://hackernoon.com/how-to-measure-particulate-matter-with-a-raspberry-pi-75faa470ec35
- ↑ https://openschoolsolutions.org/measure-particulate-matter-with-a-raspberry-pi/
- ↑ https://www.w3schools.com/python/python_mysql_insert.asp
- ↑ https://flows.nodered.org/node/node-red-contrib-dht-sensor
- ↑ https://groups.google.com/forum/#!topic/node-red/bkmv6kMAFlM
- ↑ https://nodered.org/docs/getting-started/raspberrypi
- ↑ https://flows.nodered.org/node/node-red-dashboard
- ↑ https://pimylifeup.com/raspberry-pi-kiosk/
- ↑ https://www.raspberrypi.org/forums/viewtopic.php?t=154190