Air quality has been a heated topic recently here in Poland. It all started a couple of years ago in Kraków, which is arguably the most polluted of big Polish cities. With growing awareness of the air pollution and its impact on health, people started designing solutions to monitor and improve air quality, while adjusting their daily routine to accommodate for the smog.

When I later moved (back) to Łódź, I noticed that pollution problem, although valid across the country, is far less recognized anywhere outside Kraków and Lesser Poland area. The air surely smells better here, but smog incidents still happen, yet informing and acting upon them is close to non-existent.

This was enough an excuse for me to try and set up the air quality monitoring system on my own while making first steps in the field of IoT.


This is the introductory post about the Air Quality Monitor. Check out also the follow-up blog post where I write about switching from WiPy to LoPy4 and designing a custom-made PCB.


Checklist

Here comes a short summary of the project components:

  • particulate matter sensor – providing the key metric
  • relative air humidity sensor – to get better insight into PM readings
  • microcontroller – the heart of the system
  • battery – to enable taking measurement outdoors
  • packaging – to move away from the prototype board and make the project somewhat shippable.

Particulate matter sensor

The sensor would take PM10 and PM2.5 readings (particulate matter of different granularity - those unfamiliar with the topic can read more about it on the internet). Air quality measurements would be stored in a database and allow me to view results in a fairly real-time manner as well as analyze historical data.

I decided to go with PMS5003 air quality sensor that is a pretty popular low-cost device, communicates via UART and is good enough for personal use. For data storage and visualization I went with InfluxDB and Grafana, as I had this setup already on my Raspberry Pi.

Relative air humidity sensor

PMS5003 uses laser light scattering for measuring PM concentration and is reported to produce inaccurate data in high humidity conditions. That's because the laser sensor identifies water particles as dust. To have a general idea of the impact of humidity on PM measurement reliability I needed to incorporate a humidity sensor in the system.

I went with SHT10 digital temperature and humidity sensor that handles temperature between -40°C and 125°C and full 0-100% relative humidity range, and communicates via a custom 2-wire interface.

Microcontroller

This is what I decided very early on. I bought a WiPy development board from Pycom someday and this looked like a perfect project to give it a spin. I also got an expansion board and a nice small transparent case. All Pycom products are based on the ESP32 chipset and incorporate MicroPython environment. I was told I was the first customer to try out the updated WiPy 3.0 with 4MB RAM and 8MB flash memory, built-in real-time clock and a handful of interfaces – head to the product page for details.

Power source and packaging

For a real-world application, I would need to take measurements outdoors. It still makes sense to measure indoor air quality, e.g. when you try to evaluate the need of installing a home air purifier. Still, I wanted to put it on the balcony, where I don't happen to have a power outlet available. The system needs to run on battery power – this changes a lot and takes the project to a whole different level. Which is great news to me, because the ultimate goal of this project is to learn things while doing cool and possibly useful stuff.

Speaking of battery, the WiPy Expansion Board comes with a LiPo battery connector and even a battery charger! And the Pycase has enough room to hold a small battery – so far so good.

More on battery

Voltage

So these are the components with their power input/output ratings:

  • WiPy runs on any voltage between 3.3V and 5.5V. It has a 3V3 output capable of sourcing up to 550mA, but no 5V output.
  • PMS5003 air quality sensor transfers data using 3.3V UART, but requires 5V power supply.
  • SHT1x series temperature/humidity sensors work with 3.3V
  • a 1-cell Li-Poly battery has a nominal voltage of 3.7V, and a 2-cell battery rated at 7.4V would be too much. And too bulky too.

Confirmed, the air quality sensor won't even start when powered by a 1-cell battery. To convert the LiPo battery voltage to 5V I used this tiny step-up voltage regulator from Pololu, delivering up to 200mA output current which is just enough for PMS5003, according to the datasheet. Its small size ensured it would easily fit into the Pycase.

Capacity

By fitting a small battery into Pycase I meant as small as 550mAh. At least that's what I was able to find online, matching the dimensions suggested on Pycom forum. Given that PMS5003 could drain up to 200mA it becomes clear that the battery would last less than 3 hours powering just the sensor, let alone the microcontroller.

On the other hand, no one needs a constant measurement of air quality. Industry standard monitoring stations report data every hour or every couple of hours, but that is probably an hourly average of a couple of measurements. I thought I'd be reading the data every hour, but for testing purposes, I started doing it every 10 minutes, so that I didn't have to wait a long time to observe some meaningful set of data. Sensor's idle mode current rating of 5mA would ensure a couple dozens of hours on battery, which is kinda fine.

First run

Although WiPy's MicroPython environment is fairly easy to embrace and arguably allows for faster development compared e.g. to C, it took me some time to get all the components working. Especially when it comes to SHT10 temperature/humidity sensor that required implementing its custom communication protocol with CRC checksum computation algorithm.

The high-level outline of the first working version looked like this:

  1. Boot up
  2. Set up WiFi connection and get current time from NTP
  3. Start air quality sensor
  4. Read 11 measurements of PM2.5 and PM10 concentration
    • skip the first one since it's a bit inaccurate (I noticed it's always much lower than the rest)
    • average the rest 10 readings
  5. Read temperature and relative humidity
  6. Read battery voltage
  7. Post all the data to InfluxDB
  8. Turn off air quality sensor
  9. Sleep for 10 minutes
  10. Go to 3.

First disappointment

I was very surprised to see that fully charged battery allowed for just around 3 hours of reading the data. Well, it's the first time in my life when I'm putting serious stuff on battery power and apparently time.sleep() is not how you normally sleep in the embedded world.

Enter the Deep Sleep.

The very cool feature that is implemented in the hardware and doesn't require additional configuration. According to Pycom documentation, it stops the CPU and all peripherals (including networking interfaces, if any). Execution is resumed from the main script, just as with a reset. Simply replacing time.sleep() with machine.deepsleep() does the job and we're golden. According to Pycom, the chip needs as little as 25uA in deep sleep mode.

The script looks like this now:

  1. Boot up
  2. Set up WiFi connection and get current time from NTP
  3. Start air quality sensor
  4. Read 11 measurements of PM2.5 and PM10 concentration
    • skip the first one since it's a bit inaccurate (I noticed it's always much lower than the rest)
    • average the rest 10 readings
  5. Turn off air quality sensor
  6. Read temperature and relative humidity
  7. Read battery voltage
  8. Post all the data to InfluxDB
  9. Deep Sleep for 10 minutes. After that time the device would reboot.

Digging deeper into battery drain

While I was at battery usage optimization, I took the multimeter to figure out the actual current of the sensor. It turned out that it drains only around 75-90mA when active, so less than half of the datasheet value. On the other hand, it consumed as much as 8mA when in idle mode (with its EN pin set to low). My instant idea was "Hey, can I just turn off the power supply of the sensor instead of setting it to idle?".

Sure I can, but it comes at additional cost.

I can't just set the microcontroller pin to low to cut off the sensor because I'm not driving it from the microcontroller (it's 3.3V and the sensor needs 5V, plus microcontroller GPIO pins are not suited for driving loads such as 100mA). The only way I could do it would be adding a transistor switch driven by the microcontroller pin. This requires putting more components into an already tight fitting case.  I went with an n-MOSFET transistor with small 100Ω gate resistor and a diode placed parallel to the sensor, to protect from reverse current while switching. Perhaps I could have skipped the resistor and possibly the diode altogether. But it's been a while since I did any electronics stuff, and after all, I had just one sensor, so better safe than sorry.

This is the schematic of a power supply for the whole system. The NCP1402 is a boost voltage regulator.

Screen Shot 2017-11-27 at 08.39.46.png

And here's more or less how it looked like in real world:

IMG_2975 2.JPG

Here the sensor is supplied directly with 5V from my universal power adapter made of iPad charger and stripped USB cable :)

I couldn't measure the leakage current with my multimeter, but according to the datasheet, it should be much less than 100uA which is over 100 times less compared to the sensor running in idle mode. That's quite an impressive achievement, but getting it to production would require some non-trivial soldering job, considering limited space.

Second run

This time I went out to test the circuit in the wild.

[gallery ids="62,63" type="columns" link="file"]

I connected the battery-powered WiPy to the boost voltage regulator circuit on a prototype breadboard and put them in a lunchbox just to protect from heavy wind. The sensors were sticking out the box so that they would measure real data.

Measuring air quality, humidity, temperature and battery voltage every 10 minutes, the system ran for 42 hours straight! Oh well, straight-ish – it could do better if it didn't break in the meantime. I implemented the humidity sensor in a way that it throws an exception whenever the CRC checksum is wrong or the ACK bit was not properly read. It turned out that due to a syntax error in the exception handler the script stopped and left the chip alive for 17 minutes until I noticed that something was wrong. The air quality sensor was already turned off at this phase of the script, and if you look at the graph below, you'll notice how big impact the active WiFi can have on power consumption. Within just 17 minutes it managed to drain 1/4 of the whole battery charge:

Screen Shot 2017-11-20 at 21.43.09.png

Please disregard the absolute values of the battery voltage as at that time I had been inaccurately interpreting ADC readings. The actual voltage span should have been approximately 4200-3600mV.


On a side note (or on the main topic, perhaps?), those weren't the best 42 hours air-quality-wise for my neighbourhood. But not so terrible either. Particulate matter levels recommended by WHO are 50μg/m3 and 25μg/m3 for PM10 and PM2.5 respectively, but these are mean 24-hour values. Still, by looking at the graph you could easily tell that the mean value would fall outside that range.

The impact of high humidity on the readout remains to be determined though, and I should have some more insights into it as the sensor gathers more data in different weather conditions.

Still a long way to go

The 42 hours test gave pretty satisfactory results, especially that it could possibly have lasted even longer. I just needed to do something about the humidity sensor that was occasionally failing for me. I couldn't easily debug the issue and it appeared to be random, showing up for just one measurement and working perfectly fine next time.

Bulletproofing

Given that SHT1x series sensors are in not recommended for new design state according to the manufacturer, I decided not to spend any more time on it and move on to more interesting stuff. I just added a watchdog timer to my system – it would reset the device if it didn't fall asleep for 30 seconds since waking up. This has effectively dealt with the humidity sensor issues, as usually already on next boot it worked just fine.

Time profiling

0.1.0

I added another timer just to measure the overall time the system was alive. In the current state, i.e. following the procedure:

  1. Boot up
  2. Set up WiFi and RTC
  3. Measure air quality
  4. Measure temperature and humidity
  5. Measure battery voltage
  6. Upload data
  7. Sleep

the whole process lasted for around 16 seconds.

0.1.1 – Parallel measurements

I don't have the exact breakdown of time taken per phase of the script, but clearly, the most lengthy task was to read 11 samples of air quality data. Some other tasks could have been performed while waiting for the sensor to gather data samples. Using MicroPython's _thread module I could easily offload temperature and humidity measurement to a separate thread. This saved up some time ending up in measurement duration at around 15.5 seconds.

0.1.2 – Set up network in background thread

I could move the WiFi setup code to the background thread too since it wasn't required while reading sensors data. That shaved off a whole 3 seconds of the total time, to around 12.64s. Additionally, the WiFi would be on for a shorter time, since it's started further into the script, and that should save some battery life.

0.1.3 – Turn off printing timestamps

Initially, I used local timestamps in the log to have clear visibility into what happens when, but that could now be ditched. It saved a tiny 20ms off the time, to 12.62s.

0.1.4 – Average less air quality measurements

Instead of averaging 10 air quality readouts I decided to drop half of them and average only even ones. So I'm still averaging the data from similar time span, but only half of the data set. Skipping every second frame saved me almost another second, landing at 11.720s, over 4 seconds (or 27 percent) less than initially.

Here's the visual summary of the performance evening:

Screen Shot 2017-11-21 at 00.53.05.png

One more thing to notice is that putting WiFi setup code in a separate thread caused the measurement duration to become more stable. Connecting to the wireless network is the most indeterministic process of the whole script time-wise, and now it got to be executed in parallel with (still much longer) UART reading of air quality sensor data.

0.2.0 – Fix air quality measurement and send data less frequently

After doing some more testing of the air quality sensor itself, I noticed two things:

  • Readouts can vary widely during first 5 seconds since turning it on
  • After that time it shows pretty consistent data, so averaging a couple of measurements shouldn't be needed.

I redesigned the script a little bit to first power on the sensor, then wait for 7 seconds, and after that average 5 consecutive readouts (still, just in case). That brought measurement duration further down to 10.920s, so already 36% less than initially :)

The other thing I decided to implement was to read data every 10 minutes, but send it to the database only every hour (or precisely, whenever 6 data points are ready). This way the WiFi would only be used once every hour, roughly 6 times less frequently. The historical measurements needed a timestamp in order to be properly stored in the database (sending data points without a timestamp would have assigned all of them the current time and date), so I just had to use WiFi (to set up the real-time clock) once more, on every hard reset. I stored the data in a file on the chip's flash, which is kinda fine in this case, but for more frequent file access using the SD card would make more sense. Apart from the data file I also stored the available data points count in WiPy's NVRAM to ensure high performance, instead of reading the whole file from flash memory every time.

Shipping it

As I iteratively developed somewhat stable software for the system, I could move on towards putting the components into one box and doing another real-life test.

Power supply circuit

img29763

I had just about that much space to fit the voltage regulator and transistor switch. Given that Pycase holds a battery, and that it also contains inner walls in the bottom compartment (such as the ones holding the battery at the center), there were two options: try to fit within a very narrow 36x13mm space with some artisan two-layer PCB routing effort, or cut out the wall connecting screw holes and gain almost twice the width available. I was about to do some drilling anyway to route the cables, plus the yellow shape was unrealistic, and the two-layer PCB was not an option due to the 6mm height limit (mind the connectors!) so I went with destroying some part of the case for greater glory.

At first, I was considering THT elements, but I quickly realized that the TO-220 MOSFET package is larger than the whole voltage regulator board, so I had to give it a try with SMD. After a couple of iterations with KiCad I came up with the following:

Screen Shot 2017-11-27 at 21.11.11

And the same evening I made it happen:

[gallery ids="70,71" type="square" columns="2" link="file"]

  1. Please spare me the harsh critique, this was my first ever SMD job :)
  2. I got the 700mAh battery that still fits the case!

Voilà!

20171125-IMG_3032-4

20171125-IMG_3041-2

The system velcroed to the wall as shown above, with version 0.1.4 of the script mentioned earlier, managed to run for 80 hours without interruptions this time. This is more than 3 days and nights, or almost half a week. Not too bad already, and I disconnected it once the battery voltage dropped below 3700mV to avoid over-discharging (according to this great article on LiPo batteries), but perhaps it could have run a bit longer since the battery itself has the circuit preventing it from over-discharging.

0.1.4_grafana

Weatherproofing and final test

20171128-IMG_3047.jpg

Although the sensors and the board are safe from the rain on my balcony, long exposure to high humidity could eventually do harm to the circuits. Putting everything inside a waterproof case is not really an option, because the air quality sensor, by definition, needs to work in the open air. The same applies to humidity sensor too.

I got an IP67-rated case and cut a huge hole to stick the sensor through its wall. The case is obviously not IP67 anymore, but sealing the vent with e.g. silicone or hot glue would work just fine, while also securing the sensor in place. I haven't yet decided what to use plus I have neither the glue gun nor the silicone, so I temporarily resorted to using the electrical tape (which is probably fine too but looks lame). Having put everything in the big case I could get rid of the Pycase and get even more space, but the battery and voltage regulator circuit were already so nicely integrated into the Pycase that I left it as is. That's how I become a victim of premature optimization ;)

The semi-weatherproof sensor circuit is now undergoing a test running the script version 0.2.0 (which transmits data hourly).

[gallery ids="95,96" type="rectangular" link="file"]

The results look promising so far, with ~60% maximum battery voltage after 44 hours, compared to ~40% during the previous test.


I'm going to stop here, because the more I write about it, the more ideas come to my mind, and this post has (literally) outgrown my expectations length- and knowledge-wise. Partially because I wrote it while actively working on that project, and given that I will for sure continue improving it, I would like to finally deliver this blog post some day. Let's recap then.

Lessons learned

This, in my case, could be a really long list, since these are my baby steps in hardware and embedded engineering. Let's try to be concise this time:

  • moving your projects a step further from the prototype board stage opens up a whole new tier of problems, not limited to software and hardware, but including some DIY challenges too,
  • if you think your project goes too easy, try powering it from battery and aiming for some decent running time between charges,
  • constant WiFi drains sooo much power! And then remember, that for your mobile phone using WiFi is still less battery-time-challenging than 3G/LTE data,
  • you can optimize performance forever, it becomes pretty addictive once you see the results of your work,
  • SMD soldering is a piece of cake – at least for a small number of simple items – if you haven't tried it, I totally recommend, although it works best with the right tools (most importantly a hot-air soldering station).

Next steps

Since I ended up putting the project in a quite big case where there's still some room available, I'll probably get some huge battery, e.g. 2600mAh, so that it runs for >10 days on a single charge.

I'd like to make the measurement data available publicly so that people in the closest area could have the info on air quality – especially that the nearest official AQ station is a few kilometres away and isn't very relevant to my neighbourhood. I'll have a look at some services providing MQTT API, like ThingSpeak and Adafruit IO.

Also, I preordered two LoPy4 boards from Pycom and I expect to receive them before Christmas :) I'd be very interested to see how LoRa compares to WiFi with regard to power consumption.

Apparently, removing the expansion board and creating my own board for WiPy with just what I need would further reduce the power consumption. It sounds nice, but will also be a bit time consuming. Also in the end, the air quality sensor draws the most power and there's nothing to optimize on that side, so working too long on the board performance won't help for this very project.

Useful links

Some of them were mentioned in the article already, but I'll gather them in one place:

For those curious about the implementation, the complete script that my WiPy runs is available from my GitHub repo.

It was a long read, but I still hope you enjoyed it. Be sure to let me know if you didn't, and in case of any questions.

Thanks for having a look!


02/02/2018 update: in the meantime, I did some more work on the air quality monitor, including moving to LoPy4 and a custom-made PCB. Check out this blog post for more information.