Skip to content

Instantly share code, notes, and snippets.

@kadamski
Last active March 31, 2024 02:03
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save kadamski/92653913a53baf9dd1a8 to your computer and use it in GitHub Desktop.
Save kadamski/92653913a53baf9dd1a8 to your computer and use it in GitHub Desktop.
SDS011 dust sensor reading
#!/usr/bin/python
# coding=utf-8
# "DATASHEET": http://cl.ly/ekot
from __future__ import print_function
import serial, struct, sys, time
DEBUG = 1
CMD_MODE = 2
CMD_QUERY_DATA = 4
CMD_DEVICE_ID = 5
CMD_SLEEP = 6
CMD_FIRMWARE = 7
CMD_WORKING_PERIOD = 8
MODE_ACTIVE = 0
MODE_QUERY = 1
ser = serial.Serial()
ser.port = sys.argv[1]
ser.baudrate = 9600
ser.open()
ser.flushInput()
byte, data = 0, ""
def dump(d, prefix=''):
print(prefix + ' '.join(x.encode('hex') for x in d))
def construct_command(cmd, data=[]):
assert len(data) <= 12
data += [0,]*(12-len(data))
checksum = (sum(data)+cmd-2)%256
ret = "\xaa\xb4" + chr(cmd)
ret += ''.join(chr(x) for x in data)
ret += "\xff\xff" + chr(checksum) + "\xab"
if DEBUG:
dump(ret, '> ')
return ret
def process_data(d):
r = struct.unpack('<HHxxBB', d[2:])
pm25 = r[0]/10.0
pm10 = r[1]/10.0
checksum = sum(ord(v) for v in d[2:8])%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 process_version(d):
r = struct.unpack('<BBBHBB', d[3:])
checksum = sum(ord(v) for v in d[2:8])%256
print("Y: {}, M: {}, D: {}, ID: {}, CRC={}".format(r[0], r[1], r[2], hex(r[3]), "OK" if (checksum==r[4] and r[5]==0xab) else "NOK"))
def read_response():
byte = 0
while byte != "\xaa":
byte = ser.read(size=1)
d = ser.read(size=9)
if DEBUG:
dump(d, '< ')
return byte + d
def cmd_set_mode(mode=MODE_QUERY):
ser.write(construct_command(CMD_MODE, [0x1, mode]))
read_response()
def cmd_query_data():
ser.write(construct_command(CMD_QUERY_DATA))
d = read_response()
if d[1] == "\xc0":
process_data(d)
def cmd_set_sleep(sleep=1):
mode = 0 if sleep else 1
ser.write(construct_command(CMD_SLEEP, [0x1, mode]))
read_response()
def cmd_set_working_period(period):
ser.write(construct_command(CMD_WORKING_PERIOD, [0x1, period]))
read_response()
def cmd_firmware_ver():
ser.write(construct_command(CMD_FIRMWARE))
d = read_response()
process_version(d)
def cmd_set_id(id):
id_h = (id>>8) % 256
id_l = id % 256
ser.write(construct_command(CMD_DEVICE_ID, [0]*10+[id_l, id_h]))
read_response()
if __name__ == "__main__":
cmd_set_sleep(0)
cmd_set_mode(1);
cmd_firmware_ver()
time.sleep(3)
cmd_query_data();
cmd_set_mode(0);
cmd_set_sleep()
@binh-bk
Copy link

binh-bk commented Jan 12, 2019

Thanks for your work. I forked your code and added with paho-mqtt-publish, save data to the local drive and calculate AQI based on US EPA

@mpnesta
Copy link

mpnesta commented Sep 30, 2019

thanks a lot! is it possible to use it also for Nova sds018 sensor ?

@kadamski
Copy link
Author

@mpnesta I have never tried it but from the manual I have found it seems to be using the same data format so should work.

@tjiagoM
Copy link

tjiagoM commented Oct 13, 2019

Hello @kadamski,

Thanks for this code! I've executed your code and when the air gets saturated your code reads values over 999 (it looks like it maxes out at 1999.9).

According to all the specifications I find online, both PM2.5 and PM10 should always be between the range 0-999 μg/m3, which doesn't seem the case when executing your code.

Do you have any idea why this is happening?

@kadamski
Copy link
Author

@tjiagoM the code does not do anything with the data given returned by the sensor - for both values we are getting 2 bytes which gives us the value in tens of ug/m³ so it is divded by 10 just as the datasheet says:
"PM2.5 value: PM2.5 (μg /m 3 ) = ((PM2.5 High byte *256) + PM2.5 low byte)/10"

So.. well.. maybe the sensor can report values over 999ug/m³ but it is not reliable? Or maybe the firmware was changed somehow but the datasheet is not updated? I don't know. If you do not trust values over 999, you could just change lines 43 and 44 to:

pm25 = min(999.0, r[0]/10.0)
pm10 = min(999.0, r[1]/10.0)

@tjiagoM
Copy link

tjiagoM commented Oct 14, 2019

Thanks for your prompt reply! Well, fair enough, I guess, cheers!

@grass5150
Copy link

Could you give some breakdown of the variables for n00bs? I don't know much about python and I've done the crontab -e & then @reboot cd /home/pi/ && ./aqi.py mentioned in your article. It does start at boot, but only ever does 1 set of readings and then never updates again. I suspect it's the time or sleep functions?

@kadamski
Copy link
Author

@grass5150 what article? The "@reboot", according to the documentation (see the end of section "Extensions" in https://linux.die.net/man/5/crontab) is "Run once after reboot." so the script will only be run once. The script itself does this sequence (see the line 94-102):

  1. Get sensor out of sleep,
  2. Sets sensor query mode to 1 (meaning, the sensor will wait for us to ask him for data instead of reporting it in intervals).
  3. Prints the firmware version.
  4. Waits 3 seconds till fan gets up to speed. Otherwise the values we get are not reliable for me. You could experiment with this sleep - check if increasing it gives you more accurate (reliable) results or if lowering it will still give you good results to speed things up. I think this is the only "variable" that might require tweaking.
  5. Sends a command to query data and prints it.
  6. Set mode back to 0.
  7. Put device into sleep.

Note that _this is not repeated. The command does this only once so you have to run this command in intervals yourself.

@grass5150
Copy link

Thanks for the reply, so what I'm really looking for is to do "@hourly" and then put sys.exit() at the end of the script after it puts the device back to sleep?

@kadamski
Copy link
Author

You don't need sys.exit() at the end. The script will terminate when all the steps are done.

@grass5150
Copy link

grass5150 commented Nov 21, 2019

So the issue I'm seeing is that it just keeps running processes and not closing them:

$ ps -ef | grep aqi
pi 611 607 0 01:00 ? 00:00:00 /bin/sh -c python /home/pi/aqi.py
pi 612 611 0 01:00 ? 00:00:00 python /home/pi/aqi.py
pi 664 660 0 01:30 ? 00:00:00 /bin/sh -c python /home/pi/aqi.py
pi 665 664 0 01:30 ? 00:00:00 python /home/pi/aqi.py
pi 721 702 0 01:55 pts/0 00:00:00 grep --color=auto aqi

¯_(ツ)_/¯

Sorry for the n00b questions, but I'm learning!

@kadamski
Copy link
Author

kadamski commented Dec 3, 2019

@grass5150: Most likely it is waiting at the "read_response()" in the cmd_set_sleep() function. You could try disabling this call if we are putting the device into sleep by changing this line to:

if not sleep:
read_response()

and see if that helps.

@daneelolivaw42
Copy link

daneelolivaw42 commented Feb 13, 2020

thank you so much for the code. I just have a problem as far as I understand it is written in Python2, is there a way to transfer the code, to Python3? I wanted to integrate the code in a python3 script, but then it doesn't seem to work. Thank you so much for your help, sorry for the question. I'm pretty new to Python and still learning. The encoding hex code seems a bit too advanced at the moment.

@all-days
Copy link

Hello @kadamski,

Thanks for your work.

I am poor at this field.

How can I receive data from json to csv format?

I would be very grateful if you could let me know.

@kadamski
Copy link
Author

@all-days: I don't know how this is related to this gist.

@all-days
Copy link

@kadamski:
First of all, I appreciate your reply.

If I use this code, data of json form are stored.
But I would like to get data of csv form.
How should I modify it?
Please let me know.

@kadamski
Copy link
Author

@all-days: I haven't run this script for some years but it doesn't produce JSON for sure. It does not produce structured output at all, just a free form text. The output is produced by "print" statements in proces_version() and proces_data() functions. You can ignore (or remove from the script) output from proces_version() and just focus on the process_data() output. The line is:

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"))

You are interested only in the first argument to the print function, namely:

"PM 2.5: {} μg/m^3 PM 10: {} μg/m^3 CRC={}"

This specifies what will be printed. All the {} are placeholders which will be filled with proper values. You have to keep the number of those placeholders while changing this string (in other words, you have to keep 3 of them). If you want to have CSV, you could simply change this to:
"{},{},{}" so this line would end up beeing:
print("{},{},{}".format(pm25, pm10, "OK" if (checksum==r[2] and r[3]==0xab) else "NOK"))

Is this what you need?

@all-days
Copy link

@kadamski
Thank you.
I'll give it a try.
May I ask you again?

@tyeth
Copy link

tyeth commented Sep 8, 2020

@kadamski Thanks for this, it enabled me to make an air quality sensor for my own curiosity, and then again for a friend (which caused no end of hilarity as I realised I had to use python3).
I kind of failed to grasp the Python3 version of the same thing, ended up with this https://github.com/tyeth/john_air/blob/main/aqi_py3_win.py#L87 - not happy as was very close but just didn't quite grasp the encoding bit hence the 192 value for a byte 🤦

@RemBrandNL
Copy link

@kadamski, great stuff!

To keep a small form factor, I have connected an SDS011 directly to a Raspberry PI Zero W. This way you don't need a clunky USB UART things and ditto USB > MicroUSB converter. It all works like a charm, but once you wake it up after putting it to sleep, it reboots the RPI. Does anyone else have any experience with this? It's probably fixed with giving the sensor its own 5V feed I guess, but open to suggestion!
Remco
image

@tyeth
Copy link

tyeth commented Jan 2, 2021 via email

@RemBrandNL
Copy link

Thanks for the supports, after more hours of Googling and much trial&error I found out I am not the only one who ran into this issue. For who is interested, this topic summarizes it best: https://www.raspberrypi.org/forums/viewtopic.php?t=219026

Other posts on this topic:

The only option to make it somewhat more compact is to replace the USB > UART board with a angled micro USB so it doesn't stick out that much.

One last hope I have is to pick up power from the USB port to see if that doesn't reboot it, trying that later today. Setting the SDS011 to sleep and waking it up again does work via that USB dongle, so let's see what happens if you draw power from there instead of the GPIO pins directly ;-)
Cheers,
Remco

@RemBrandNL
Copy link

OK, not sure if it's the 'correct' way to go, but I did get it working withouth that stupid dongle. Wiring needs some TLC, but it's working now. I can put the sensor to sleep and wake it up again without my RPI rebooting. Complete lenghty lengthy write-up with more pics on https://tinker.rembrand.nl/2021/01/02/using-sds011-via-uart/

I combined it with a DHT22 sensor for now and it's happily recording every 5 minutes:
image

Cheers,
Remco

image

@kadamski
Copy link
Author

kadamski commented Jan 2, 2021

@remco have you tried.putting a capacitor between the 5v and gnd to smooth out the voltage drop?

@RemBrandNL
Copy link

Wouldn't even know which value to pick; my knowledge of electronics is limited to none ;-) I did read on several posts/boards that others did try that, including circuits with diodes and all, but to no avail. Best I can do is logic and this seems to work as expected. Until I find some other solution, I'll tidy this up and move it to an enclosure outside to do further testing with the scripts I am working on. I want to collect the data to a database, do some graphs, send it properly formated to luftdaten. All knowledge gaps so I have quite some challenges. Your scripts helped me to get started with the SDS011 though, so many thanks for that :-)
Remco

@tyeth
Copy link

tyeth commented Jan 2, 2021

Wicked, also check the /var/log/syslog file or dmesg, i remember low voltage warnings in one of them when I was doing too much with my weak 5v supply

@RemBrandNL
Copy link

Wicked, also check the /var/log/syslog file or dmesg, i remember low voltage warnings in one of them when I was doing too much with my weak 5v supply

image

It's been running in the backyard since yesterday evening, 0 hits on 'voltage' issues it seems. Ever since then it has measured 10 times with 1 second intervals, put the SDS011 to sleep for about 5 minutes, wake up and measure again. So I am still optimistic this works ;-)
Pics of the enclosure you can find here. Currently using a standard weatherproof junction box which was always my entention for the outdoor enclosure.
Remco

@alokvermaiitr
Copy link

What needs to change in the code if I would not use Sensor Hat?

@kadamski
Copy link
Author

@alokvermaiitr what you mean by Sensor Hat?

@FaisalAhmed123
Copy link

For those still having the issue where it maxes out, I wrote some new code which seems to work past the 20,000 mark https://github.com/FaisalAhmed123/Nova-PM2.5-Sensor-Reading-Interpreter/blob/main/main.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment