Some time in the past several months, the PMS5003 Air quality sensor came to my attention. This sensor allows you to monitor PM1.0, PM2.5, and PM10 particles in the air.
Some times the air quality around these parts gets quite bad due to dust storms or fires. Not only would I like to know just how bad the AQ (air quality) is at my house, I’d like to keep history. I’d also like to send myself a notification if it gets too bad so I know I need to shut up the house and turn on the HEPA filter.
I purchased this sensor at AdaFruit:
At $40, it isn’t cheap, but it is a pretty complex little device.
To start with, I wanted to play with this sensor using a windows PC. I needed a FTDI cable to do so. The FTDI cable converts a USB cable to the TTL serial protocol used by this board. Some quick research and I found this guy had connected the PMS5003 to his PC using an AdaFruit FT232H breakout board which can be found here:
I already cover how to connect this FTDI breakout board to your PC here:
Connecting AQ Sensor to FTDI Breakout Board
Since the sensor communicates via a normal serial interface, connection is very simple. You just need to connect power, ground, and TX & RX.
On the FTDI breakout, D0 is TX and D1 is RX. So connect D0 to RX and D1 to TX of the PMS board. For my project, I don’t intend to transmit to the sensor, but I’ll still connect the lead.
To see if you got the connections correct, just use putty to connect to the FTDI cable and you will see ‘garabage’ being transmitted. The data being transmitted is in binary so you won’t see anything useful at this point.
Reading and Formatting the Sensor Output
Once the sensor was functioning, I needed to read the binary values and convert them into something useful.
Further, having the data spit out every second wasn’t real useful. I decided I needed to collect 5 minutes of data, average it, and output the average to get a better sense of what the air quality really was.
The program I wrote to do this is in Lazarus / Free Pascal. If you’ve seen my other posts, you know this is my language of choice. If you know C/C++ you won’t have much trouble reading the source code to understand how to implement the code in C.
The Pascal source and executable can be found at:
The Pascal source code for the program is in the file aqmonitor.lpr.
If you wish to compile this program, you will also need the synapse ‘synaser’ serial library which can be found at
The sensor outputs a packet of data about once a second. I read that data and put it into this data structure:
aqPacketT = record hdr1 : byte; hdr2 : byte; frameLen : word; pm10Std : word; pm25Std : word; pm100Std : word; pm10Env : word; pm25Env : word; pm100Env : word; particles03um : word; particles05um : word; particles10um : word; particles25um : word; particles50um : word; particles100um : word; filler : word; checksum : word; end; // aqPacketT
In Pascal, ‘word’ is a 16bit unsigned integer.
There are 2 categories of ‘particulate matter’: Standard and environmental. So pm10Std is the PM1.0 standard data and pm10Env is the PM1.0 environmental data.
From others’ discussions, it appears that the standard values have something to do with calibration and it is the atmospheric environment values that should be used for actual measurements.
The variables for PM1.0, PM2.5, and PM10 will contain data for all particles <= that size. If it senses values for PM1.0, then those values will also show up in the PM2.5 and PM10 variables.
To create the data structure, I read the data stream until I see the ox42 and ox4D header bytes. Once I have those, I start reading data byte by byte and creating the data structure.
.. pm10Std := getword(comCB, calcChecksum); pm25Std := getword(comCB, calcChecksum); pm100Std := getword(comCB, calcChecksum); ..
As I’m populating the data structure, I’m also keeping track of the checksum so I can compare at the end of the packet. If they don’t match, I generate an exception. If the program gets 10 checksum errors in a row, it will assume a serious problem and abort.
if finalChecksum <> checksum then begin // checksums don't match, throw exception raise EChecksum.Create(erMsgChecksum); end;
As each packet is read, it is added to a FIFO list which I have set to hold 360 samples (about 6 minutes) of data:
Then every 5 minutes, I compute the average of all of the values for the past 5 minutes (300 seconds), and print those averages:
if SecondsBetween(now, avgTimer) >= 300 then begin packet := historyAvg(300, history); printPacket(packet); avgTimer := now; end;
Here is an example of the program’s output. The air has been very clean here in my office since I started the monitor last night, so I lit a match at about 11:53 and let it smoke the room a bit:
Here is a much longer run from a few days ago when the wind was kicking up dust, then a rain storm cleaned the air. This graphs just the PM2.5 values:
The plan is to migrate this project to a dedicated Raspberry Pi. The monitor program used here will be modified to write the data captured to a SQLite database. Once I have the data in a database, another program will periodically query the data and generate some graphics for a web page. Finally, I will need to fabricate some kind of enclosure.
I am planning to use a USB cable and the documented FTDI breakout board for my final project because the Raspberry Pi will be in my garage and the sensor outside. It will be a long enough run of cable between the two, I don’t want to use straight serial TTL protocol. But if you want to fashion your own cable, this guy documents the correct connector you will need:
This guy connects the PMS5003 to an arduino and reports the current AQ on an LED display:
Pretty good article on the performance of the sensor:
Adafruit post regarding difference between Standard and Environmental values: