|Project LoRaWAN dust Sensor|
|LoRaWAN airborne particulate matter sensor|
What is it?
This is a companion project of LoraLuftdatenForwarder.
The concept consists of:
- a sensor that measures airborne particulate matter and sends the measurement data using LoRaWAN to TheThingsNetwork (TTN)
- a forwarder application that collects the data from TTN and forwards it to sensor.community (formerly luftdaten), opensensemap, mycayenne dashboard, etc.
- LoRaWAN-enabled particulate matter sensor node, based on off-the-shelf ESP32+LoRa hardware, specifically the TTGO LoRa32 v1 and Heltec LoRa32 v2 board.
- Works with the most recent version (v3) of TheThingsNetwork infrastructure
- Uses over-the-air-activation (OTAA), parameters have a sensible default and are stored in EEPROM:
- Device EUI defaults to the ESP32 built-in unique id
- APP EUI and APP key can be configured using the serial port
- Uses LoRaWAN automatic-data-rate (ADR) to optimise data rate and transmit power
- Data upload interval defaults to 5 minutes (SF7), scales with spreading-factor to satisfy the fair-use-policy of TheThingsNetwork
- Data is encoded as Cayenne LPP, no custom payload decoder needed
- Supported sensors (auto-detected):
- Nova Fitness SDS011 particulate matter sensor, provides PM10, PM2.5
- Sensirion SPS30 particulate matter sensor, provides PM10, PM4.0, PM2.5, PM1.0
- BME280 humidity/temperature/pressure sensor, provides humidity, temperature, barometric pressure
- Firmware can be upgraded over-the-air (WiFi)
I am publishing all source code on github and documentation on this wiki.
A similar thing has been done by:
- TTN Ulm, see https://github.com/verschwoerhaus/ttn-ulm-feinstaub (the sensor code) and https://github.com/verschwoerhaus/ttn-ulm-muecke (the forwarder, in python)
One thing in particular that my concept does better than existing solutions is to use proper OTAA for the LoRa connection to TTN. OTAA means over-the-air-activation and is a mechanism to dynamically negotiate encryption keys and communication settings.
- Base send interval on the exact time spent sending data, investigate if the LoRaWAN device stack supports this
- Implement a simple command set through LoRaWAN downlink commands, for example:
- a command to trigger an immediate reboot of the node
- a command to assist in geolocation, performing a WiFi AP scan:
- Per AP we send 8 bytes: 1) MAC 2) RSSI 3) channel, so a total of 6 APs can be send in 48 bytes
- create a basic command structure: command port, payload encoding, e.g. 1st byte is command id
- Implement the ESP-Now lamp/display protocol, for easy visualisation, see https://revspace.nl/StofAnanas#Next_generation.
- Start with sending the particulate matter measurements
- Add a suggested colour indication later
- Improve firmware update:
- Show current software version in the welcome screen
- Show hardware model (TTGO or Heltec) in the welcome screen
- Add support for Davis Airlink local API, more information at https://weatherlink.github.io/airlink-local-api/
Useful links for the TTGO LoRa board:
- Example code that joins TTN by OTAA and saves the OTAA parameters
The node is based on the Arduino framework:
- For the processor board, either the TTGO ESP32 board ("ttgo-lora32-v1") or the Heltec LoRa32 V2 can be used.
- The particulate matter sensor is the SDS011 or the SPS30, the sensor type is auto-detected.
- The humidity/temperature sensor is the BME280 (superior to the DHT11/22).
|TTGO LoRa v1||Heltec LoRa v2||Sensor||Remark|
|5V||5V||5V (SDS pin 3)||SPS30 5v (red), triple-check this, swapping 5V/GND destroys the SDS011|
|GND||GND||GND (SDS pin 5)||SPS30 gnd (black), triple-check this, swapping 5V/GND destroys the SDS011|
|GPIO23||GPIO23||RXD (SDS pin 6)||SPS30 RX (white?)|
|GPIO22||GPIO22||TXD (SDS pin 7)||SPS30 TX (purple?)|
|3.3V||3.3V/Vext||BME280 3V||Both Vext and 3.3V can be used|
- NOTE: https://www.thethingsnetwork.org/community/berlin/post/warning-attention-users-of-ttgo21-v16-boards-labeled-t3_v16-on-pcb-battery-exploded-and-got-on-fire
Compile and upload the firmware
Source code is hosted on github:
- Arduino node, written in C/Arduino, built using platformio. This firmware joins TTN by OTAA and sends the measurement data using Cayenne.
- TTN-to-luftdaten forwarder, written in Java, built using gradle. This picks up the Cayenne encoded data and forwards it to the Luftdaten API.
On Linux, with platformio (the command line tool), instructions for Debian Linux are:
- install platformio:
sudo apt install python3-pip sudo pip3 install platformio pio update
- get the source from github:
- enter the correct directory:
cd LoraWanPmSensor cd Esp32PmSensor
- compile and upload (for TTGO LoRa32 v1 board):
pio run -e ttgov1 -t upload
or (for Heltec LoRa32 v2 board):
pio run -e heltecv2 -t upload
Once the initial firmware has been loaded into the device, further firmware updates can be done over WiFi:
- Depending on the board you have, check that you have either a ttgov1.bin or heltecv2.bin firmware file
- Connect to the access point set up by the node, it's called 'ESP-XXXXXXXXXXXX', where XX is the same as the LoRaWAN device EUI
- Using a browser, go to http://192.168.4.1 , a firmware selection screen appears as shown on the right
- Select the appropriate BIN file, the will be updated and reboot
My firmware uses the Cayenne LPP (low power payload) encoding.
It's reasonably compact, it's a standard format, you can get a preview of the data in the TTN console. Interacts nicely with other platforms.
Over-the-air payload encoding:
- 4 bytes: PM10: analog input (type 2), channel id 1, with value in units of 0.01 ug/m3, saturated to 327.67 ug/m3
- 4 bytes: PM2.5: analog input (type 2), channel id 2, with value in units of 0.01 ug/m3, saturated to 327.67 ug/m3
- 4 bytes: Temperature: temperature (type 103), with value in units of 0.1 degrees celcius
- 3 bytes: Humidity: humidity (type 104), with value in units of 0.5 %
Particulate matter concentrations higher than 327.67 ug/m3 are saturated to 327.67, this is the maximum that can be represented as analog value in Cayenne.
- 4 bytes: PM1.0: analog input (type 2), channel id 0, with value in units of 0.01 ug/m3, saturated to 327.67 ug/m3
- 4 bytes: Pressure: barometer (type 115), with value in units of 0.1 mbar, or 10 Pa (optional)
A nice thing about Cayenne is that you can simply leave items out if you don't support them, which results in a shorter yet still valid message.
0x01 0x01 0xXX 0xXX 0x02 0x01 0xYY 0xYY 0x03 0x67 0xTT 0xTT 0x04 0x68 0xHH <== PM10 value ===> <== PM2.5 value ==> <== temperature ==> <= humidity =>
Total payload size is 15 bytes. The LoRaWAN header adds 13 bytes (at least).
Particulate matter values encoded are averaged over the measurement interval.
Update: discovered that the Cayenne standard *does* actually support a "particle concentration" id, this is 125, derived from IPSO id 3325 http://www.openmobilealliance.org/tech/profiles/lwm2m/3325.xml . No idea yet though how to specify the type of particle measurement, if there's any convention for PM10, PM2.5, etc. The resolution is only 1 ppm, with typical sensors delivering 0.1 ppm resolution.
How other projects encode the data:
- TTN Apeldoorn (?): https://github.com/tijnonlijn/RFM-node/blob/master/dustduino_PPD42NS_example.ino#L327 sends 5 bytes
- 1 byte : 0x04
- 2 bytes: PM25(?) big endian
- 2 bytes: PM10(?) big endian
- TTN Ulm: https://github.com/verschwoerhaus/ttn-ulm-feinstaub/blob/master/ttnulmdust/ttnulmdust.ino#L225 sends 8 bytes:
- 2 bytes: PM10, big endian (unit 0.01 ug/m3)
- 2 bytes: PM2.5, big endian (unit 0.01 ug/m3)
- 2 bytes: humidity (unit of 0.01%)
- 2 bytes: temperature (unit of 0.01 degree Celcius)
- RIVM node, sends 20 bytes
- 1 byte temperature (unit deg Celcius ?)
- 1 byte relative humidity (unit % ?)
- 2 bytes pressure (unit?)
- 2 bytes pm10 (unit?)
- 2 bytes pm25 (unit?)
- 2 bytes op1 (unit?)
- 2 bytes op2 (unit?)
- 4 bytes latitude (unit?)
- 4 bytes longitude (unit?)
- Apeldoorn in data: https://github.com/nijmeijer/TTN_Apeldoorn_in_Data_2018/blob/master/AiD_Dust_2018/AiD_Dust_2018.ino#L184 sends 16 bytes:
- 4 bytes: pm2_5 float big endian (unit?)
- 4 bytes: pm10 float big endian (unit?)
- 4 bytes: humidity float big endian (unit?)
- 4 bytes: temperature float big endian (unit?)
A smaller payload means less time in the air, smaller chance of collision with other LoRaWAN packets and more packets per hour. However, there is always an overhead from the LoRaWAN package (minimum 13 bytes), so using the smallest encoding (5 bytes) compared to the largest (16 bytes), reduces the on-air-time by only 23%.
LoraWan time budget
The number of bytes per telemetry packet and the number of packets sent per day determine how much of the available "airtime" we use. TheThingsNetwork states a fair-use-policy of 30 seconds per day total uplink time.
Consider the following:
- radio regulations generally have a 1% duty cycle requirement for the two bands used by the 8 LoRaWAN (EU) frequencies, so according to the legal limits, there is about (86400s x 1% x 2bands) = 1728 seconds per day send time at the best case.
- TheThingsNetwork has a FUP of 30s of data upload per day. This is actually a *lot* more strict than allowed purely by radio regulations.
- The sensor.community backend runs on a 5 minute interval, or 288 measurements per day.
- The default sensor.community WiFi firmware sends new data every 145s by default
With the TTN FUP of 30s upload per day and 5 minute upload interval, we can spend 30s / 288 = 104 ms on each transmission.
Using the LoRaWAN airtime calculator we can determine which modes can be used. At 15 bytes payload, this is only possible at SF7, the highest LoRa speed. Stretching the TTN guideline a bit, say by a factor 2, we can achieve those transfers still only at SF7 and SF8. So you need to be relatively close to a gateway.
In terms of interval:
- SF7: payload 17 bytes -> 71.9 ms/transmission -> every 3m27s
- SF8: payload 17 bytes -> 123,4 ms/transmission -> every 5m55s
- SF9: payload 17 bytes -> 226,3 ms/transmission -> every 10m32s
- SF10: payload 17 bytes -> 452.6 ms/transmission -> every 21m43s
With a smaller payload, can we use higher spreading factors? :
- smallest possible binary payload is 4 bytes (PM10 and PM2.5, no temperature, no humidity): SF7 takes 51.5 ms, SF8 takes 92.7 ms, SF9 takes 164.9 ms
- smallest Cayenne payload is 8 bytes (PM10 and PM2.5, no temperature, no humidity): SF7 takes 56.6 ms, SF8 takes 102.9 ms, SF9 takes 185.3 ms
So, in conclusion: With the FUP of TTN and use of Cayenne encoding, you can just barely send enough data to transport PM data! Also note that the payload size does not actually differ *that* much, this is because of the LoRaWAN overhead of 13 bytes minimum and pre-amble symbols which are sent anyway.
I plan the following downlink commands:
- "info", retrieves information on the platform, hardware and firmware version
- "reboot", performs a remote reboot, OTAA will be performed. This command should only be sent UNconfirmed, as the reboot is performed *before* sending the reply back. This could result in a reboot-loop. A reboot request with confirmed bit is therefore ignored.
- "locate", performs a wifi scan so the node can be located, response data contains an 8-byte structure per AP: 6-byte MAC address, 1-byte RSSI, 1-byte channel. A max of 6 APs is reported.
- command requests are sent over a specific port, say port 100
- responses are sent back on the same port
- commands can be confirmed or unconfirmed, the response will mimic the command in this respect
- commands and responses have a simple structure: first byte is command id, rest is command/response-specific