-
-
Save kadamski/92653913a53baf9dd1a8 to your computer and use it in GitHub Desktop.
#!/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() |
@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):
- Get sensor out of sleep,
- Sets sensor query mode to 1 (meaning, the sensor will wait for us to ask him for data instead of reporting it in intervals).
- Prints the firmware version.
- 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.
- Sends a command to query data and prints it.
- Set mode back to 0.
- 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.
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?
You don't need sys.exit() at the end. The script will terminate when all the steps are done.
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!
@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.
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.
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.
@all-days: I don't know how this is related to this gist.
@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.
@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?
@kadamski
Thank you.
I'll give it a try.
May I ask you again?
@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 🤦
@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
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:
- https://www.raspberrypi.org/forums/viewtopic.php?t=274549
- https://www.raspberrypi.org/forums/viewtopic.php?t=237119
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
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:
Cheers,
Remco
@remco have you tried.putting a capacitor between the 5v and gnd to smooth out the voltage drop?
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
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
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
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
What needs to change in the code if I would not use Sensor Hat?
@alokvermaiitr what you mean by Sensor Hat?
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
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?