Measuring air quality with the Raspberry Pi

Air Quality Sensor Code

The code that reads the sensor is pretty straightforward. The AQS produces a voltage that needs to be converted to a digital form for analysis. Typically, this is accomplished with an analog-to-digital converter (ADC). One of the enduring issues for the Rasp Pi has been the lack of an on-board ADC. The Arduino family typically has a 6- to 16-channel ADC, usually at 10 bits of resolution, which is suitable for many applications. Even the lowly ESP8266 WiFi module has one channel of ADC, but because it is limited to 1.0V maximum, you often need to condition the signal (using an amplifier or a voltage divider) to read voltages.

The Rasp Pi has no ADC channels. At the Lab, we searched various Grove device manufacturers for a suitable ADC and could find no Grove solution that had multiple channels, had an Inter-Integrated Circuit (I2C) interface, would work at 3.3V and 5.0V, and had >10-bit resolution. So we built one (Figure 4). This Grove board is based on the TI ADS1115 ADC chip and has four channels of 16-bit resolution and can work at 3.3V and 5.0V. Now the Rasp Pi has the missing ADC functionality.

Figure 4: The Grove ADS1115 four-channel, 16-bit ADC.

The Pi2Grover board provides the physical interface between the Grove devices and the Rasp Pi, as shown in Figure 5.

Figure 5: The Pi2Grover board sits on the Raspberry Pi 3.

The ADS1115 is mostly compatible with the ADS1015 ADC, so drivers are readily available. The Adafruit Python driver for the ADS1x15 family will work with this board, but two issues required modification of the driver for this project. (The modified code,, is included in the DataLogging code repository [6].) First, I needed to read the raw data (rather than the voltage) from the driver; second, I noticed that the ADS1115 requires a bit more delay between samples than the stock driver, especially at 3.3V.

The code for reading the sensor is pretty simple (Listing 1). It sets up the gain and samples per second, reads all four channels into an array – first, the raw digital value (16 bits, unsigned) and then the value interpreted as a voltage – and stores them all in a MySQL database. The AQS sensor is connected to channel 1.

Listing 1

Reading the ADS1115 Sensor

01 gain = 6144 # +/- 6.144V
03 # Select the sample rate
04 sps = 250 # 250 samples per second
07 print "------------------------------"
08 rawData = []
09 Voltage = []
11 for x in range (0,4):
12   rawData.append(ads1115.readRAW_ADCSingleEnded(x, gain, sps))
13   Voltage.append(ads1115.readADCSingleEnded(x, gain, sps)/1000)

DataLogging Code

The most complex part of the software logs the data and graphs it for analysis. Recently SwitchDoc Labs wrote some code to turn the Rasp Pi into a data logger and graph generator using MySQL and Matplotlib. That project was the subject of an earlier SwitchDoc Labs column [7], and all of the code is included in the DataLogger repository on GitHub [6].

Although the code for saving the data to a MySQL database is pretty straightforward, the Matplotlib code is not so obvious, so you should refer the article mentioned earlier [7]. One of the interesting, and most useful, pieces of code revolves around the Advanced Python Scheduler (APScheduler) [8]. This package allows you to schedule execution of your Python code later, either once or periodically. You can add new jobs and remove old jobs on the fly as you please. I have used this in two other projects (Project Curacao and SunRover) and have been very pleased with the performance. Because your jobs are running in separate threads, sometimes you will see what you think is odd behavior, but you can work around it.

Listing 2 shows how to set up and add jobs to the scheduler in the DataLogging software. First, you set up the background scheduler, and then you schedule the jobs at the times you want them to execute. Although there are dozens of ways to schedule jobs, I use the 'interval' method here, which just starts a job x number of seconds in the future and then repeats. The killLogger job in line 6 quits the sampling at some time in the future (about 140 hours in the example sketch).

Listing 2

Scheduling Execution of Python Code

01 scheduler = BackgroundScheduler()
02 scheduler.add_job(INA3221Functions.readINA3221Data, 'interval', seconds=SampleTime, args=[password])
03 scheduler.add_job(INA3221Functions.buildINA3221Graph, 'interval', seconds=GraphRefresh, args=[password, GraphSampleCount])
04 scheduler.add_job(ADS1115Functions.readADS1115Data, 'interval', seconds=SampleTime, args=[password])
05 scheduler.add_job(ADS1115Functions.buildADS1115Graph, 'interval', seconds=GraphRefresh+15, args=[password, GraphSampleCount])
06 scheduler.add_job(killLogger, 'interval', seconds=LengthSample)
07 scheduler.add_job(tick, 'interval', seconds=60)
08 scheduler.start()
09 scheduler.print_jobs()
11 print('Press Ctrl+{0} to exit'.format('Break' if == 'nt' else 'C'))
13 try:
14   # This is here to simulate application activity (which keeps the main thread alive).
15   while True:
16     time.sleep(2)
17 except (KeyboardInterrupt, SystemExit):
18   # Not strictly necessary if daemonic mode is enabled but should be done if possible
19   scheduler.shutdown()

Buy Raspberry Pi Geek

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content