Skip to content

Instantly share code, notes, and snippets.

@kadamski kadamski/sds011
Last active Mar 11, 2020

Embed
What would you like to do?
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()
@filmat

This comment has been minimized.

Copy link

filmat commented Nov 28, 2016

Very useful. Thanks!

@dzaczek

This comment has been minimized.

Copy link

dzaczek commented Feb 28, 2018

thanks great idea 👍
i grab you code to my project

@ssz157

This comment has been minimized.

Copy link

ssz157 commented Mar 7, 2018

Hi there,
I am using ide for programming the sds011. All goes well only issue is that sds011 never sleeps and keep streaming.
can anyone please suggest what could be the problem.
I am totally new to programming so I have no idea if there is something wrong with the code?

@mvrueden

This comment has been minimized.

Copy link

mvrueden commented Aug 28, 2018

Just a guess: @ssz157 did you make sure the Rx and Tx pins are connected only using 3,3V?
Most examples in the web use NodeMCU, which runs at 3,3V by default, but e.g. Arduino Nano etc. usually run at 5V, thus Rx and Tx must be lowered down in order to communicate with the sensor.

@binh-bk

This comment has been minimized.

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

This comment has been minimized.

Copy link

mpnesta commented Sep 30, 2019

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

@kadamski

This comment has been minimized.

Copy link
Owner Author

kadamski commented Sep 30, 2019

@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

This comment has been minimized.

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

This comment has been minimized.

Copy link
Owner Author

kadamski commented Oct 14, 2019

@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

This comment has been minimized.

Copy link

tjiagoM commented Oct 14, 2019

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

@grass5150

This comment has been minimized.

Copy link

grass5150 commented Nov 20, 2019

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

This comment has been minimized.

Copy link
Owner Author

kadamski commented Nov 20, 2019

@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

This comment has been minimized.

Copy link

grass5150 commented Nov 20, 2019

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

This comment has been minimized.

Copy link
Owner Author

kadamski commented Nov 20, 2019

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

@grass5150

This comment has been minimized.

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

This comment has been minimized.

Copy link
Owner 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

This comment has been minimized.

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

This comment has been minimized.

Copy link

all-days commented Mar 11, 2020

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

This comment has been minimized.

Copy link
Owner Author

kadamski commented Mar 11, 2020

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

@all-days

This comment has been minimized.

Copy link

all-days commented Mar 11, 2020

@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

This comment has been minimized.

Copy link
Owner Author

kadamski commented Mar 11, 2020

@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

This comment has been minimized.

Copy link

all-days commented Mar 11, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.