Monitoring air quality with a Nova PM2.5/PM10 Sensor and Python

Discontinuous mode

When the sensor is powered-up, it measures at the frequency of 1 times per second, which is  suitable for real-time data acquisition. However, if you want to run the device 24/7, you should keep in mind that its laser diode has a service life of only 8000 hours.

Although it is mentioned in the product data sheet that the sensor can be sent to sleep, it is not documented how to enable the discontinuous working method to prolong the service life. I figured out that it can be achieved by sending a special sleep command to the sensor, which disables both laser diode and fan.

To re-enable the sensor, it is enough to send one byte (\x01). I included a small delay of 12 seconds before measurement, to respect the sensor’s response time of about 10 seconds (with a hose attached to the sensor, a longer delay might be required).

In continuous mode, the module consumes in between 80 to 100 mA, but only 30 mA in sleep mode. Therefore, it should be possible to use it as a portable device for measuring outdoor air quality, i.e. next to a road with heavy traffic. With few more lines of code, you can log the data into  a MySQL(ite) database or CSV.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2017 <>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.

from __future__ import print_function
import serial, struct, sys, time

ser = serial.Serial()
ser.port = "/dev/ttyUSB0"
ser.baudrate = 9600

def dump_data(d):
 print(' '.join(x.encode('hex') for x in d))

def process_frame(d):
 #dump_data(d) #debug
 r = struct.unpack('<HHxxBBB', d[2:])
 pm25 = r[0]/10.0
 pm10 = r[1]/10.0
 checksum = sum(ord(v) for v in d[2:8]){c7f7cb1468c0d02af358b3ce02b96b7aadc0ce32ccb53258bc8958c0e25c05c4}256
 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 sensor_read():
 byte = 0
 while byte != "\xaa":
 byte =
 d =
 if d[0] == "\xc0":
 process_frame(byte + d)

def sensor_wake():

def sensor_sleep():
 bytes = ['\xaa', #head
 '\xb4', #command 1
 '\x06', #data byte 1
 '\x01', #data byte 2 (set mode)
 '\x00', #data byte 3 (sleep)
 '\x00', #data byte 4
 '\x00', #data byte 5
 '\x00', #data byte 6
 '\x00', #data byte 7
 '\x00', #data byte 8
 '\x00', #data byte 9
 '\x00', #data byte 10
 '\x00', #data byte 11
 '\x00', #data byte 12
 '\x00', #data byte 13
 '\xff', #data byte 14 (device id byte 1)
 '\xff', #data byte 15 (device id byte 2)
 '\x05', #checksum
 '\xab'] #tail

 for b in bytes:

def main(args):

if __name__ == '__main__':
 import sys

Download Python Script


Meanwhile, I created a simple UI for controlling the sensor. Window size is set to 480×320, in order to fit on a 3.5″ touch screen.

Python code can be cloned from my github repo.

Notify of
Inline Feedbacks
View all comments
6 years ago

What is the device ID (2 bytes)? Is it unique for each and every SDS sensor?

7 years ago

Hello, could you please add a link to your script. Many thanks.

7 years ago

Indents messed up in sample code