binh-bk
1/12/2019 - 4:26 AM

SDS011 dust sensor reading

SDS011 dust sensor reading

#!/usr/bin/python2  # only works with python2.7
# coding=utf-8
'''
forked from A. Adamski's gist: https://gist.github.com/kadamski/92653913a53baf9dd1a8
"DATASHEET": http://cl.ly/ekot
Binh Nguyen, November 2018
adapted from kadamski script with additional features:
1. logging
2. calculate AQI (US)
3. publish to MQTT broker.
4. Schedule interval run mode
- required pyserial, install by: sudo apt install python-serial 
or pip install pyserial
'''
from __future__ import print_function
import serial, struct, sys, time, json
import paho.mqtt.publish as publish

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 = "/dev/ttyUSB0"
ser.baudrate = 9600

ser.open()
ser.flushInput()

byte, data = 0, ""
global lastTime
lastTime = 0
logFile = '/home/pi/Desktop/sambaShare/weather/sds01.csv'
AQIs = {'Good':{'aqi':[0,50], 'PM2.5':[0,12.0], 'PM10':[0,54]},
            'Moderate':{'aqi':[51,100], 'PM2.5':[12.1,35.4], 'PM10':[54,154]},
            'Unhealthy for Sensitive Groups':{'aqi':[101,150], 'PM2.5':[35.5,55.4], 'PM10':[155,254]},
            'Unhealthy':{'aqi':[151,200], 'PM2.5':[55.5,150.4], 'PM10':[255,354]},
            'Very Unhealthy':{'aqi':[201,300], 'PM2.5':[150.5,250.4], 'PM10':[355,425]},
            'Hazardous':{'aqi':[301,400], 'PM2.5':[250.4,350.4], 'PM10':[425,504]},
            'Very Harzadous':{'aqi':[401,500], 'PM2.5':[350.5,500.4], 'PM10':[504,604]}
            }

def findRange(pollulant, conc):
    '''return key of the category, PM2.5, 30 ug/m3'''
    for key,value in AQIs.items():
            range_ = value[pollulant]
            if conc >= range_[0] and conc <= range_[1]:
                    return key

def calAQI(pollulant,conc):
    '''return AQI based US EPA for PM2.5 and PM10, p16, 2013 Technical Asst.'''
    key = findRange(pollulant, conc)
    I_Lo = AQIs[key]['aqi'][0]
    I_Hi = AQIs[key]['aqi'][1]
    BP_Lo = AQIs[key][pollulant][0]
    BP_Hi = AQIs[key][pollulant][1]
    AQI_X = (I_Hi-I_Lo)*(conc-BP_Lo)/(BP_Hi-BP_Lo) + I_Lo
    return round(AQI_X,1), key

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()
    
  def run():
    ''' turn on, stablize for 25 seconds before reading and then sleep'''
    cmd_set_sleep(0)
    cmd_set_mode(1)
    cmd_firmware_ver()
    time.sleep(25)

    cmd_query_data()
    cmd_set_mode(0)
    time.sleep(5)
    cmd_set_sleep()
    return None

def push_MQTT(mesg):
    '''push message to a local broker'''
    topic = 'sensors/nova_sds011'
    host = '192.168.1.xx
    auth = {'username':'janeoe', 'password':'password'}
    try:
        publish.single(topic, mesg, hostname=host, auth=auth)
    except socket.error:
        pass
    return None

def schedule(snapTime=600):
    global lastTime
    if time.time()-lastTime >=snapTime:
        ts = time.strftime('%x %X', time.localtime())
        print("Run script at: {}, lastRun was {}".format(ts, lastTime))
        lastTime=time.time()
        run()
        aqi_pm25, status_pm25 = calAQI('PM2.5', pm25)
        aqi_pm10, status_pm10 = calAQI('PM10', pm10)

        if aqi_pm25 > aqi_pm10:
            air_quality = status_pm25
            air_aqi = aqi_pm25
        else:
            air_quality = status_pm10
            air_aqi = aqi_pm10
        mesg = {'sensor':'Nova_SDS011','timestamp':ts, 'pm25':pm25, 'pm10':pm10,\
                'aqi25':aqi_pm25,'aqi10':aqi_pm10,'level':air_quality}
        mesg = json.dumps(mesg,'UTF-8')
        push_MQTT(mesg)
        print('Pushed MSG {}'.format(mesg))
        payload = ','.join([ts, str(pm25), str(pm10), str(aqi_pm25), str(aqi_pm10), air_quality])+ '\n'
        with open(logFile, 'a+') as f:
            f.write(payload)
            print('Save data: {}'.format(payload))
            print('Log file: {}'.format(logFile))
    return None

if __name__ == "__main__":
    while True:
        '''keep loop running and read sensor every 5 minutes
        schedule(300)