|
| 1 | +""" |
| 2 | + Wrapper classes for the Plantower PMS5003. |
| 3 | + Philip Basford |
| 4 | + 12/02/2018 |
| 5 | +""" |
| 6 | + |
| 7 | +import logging |
| 8 | +from datetime import datetime, timedelta |
| 9 | +from serial import Serial, SerialException |
| 10 | + |
| 11 | +DEFAULT_SERIAL_PORT = "/dev/ttyUSB0" # Serial port to use if no other specified |
| 12 | +DEFAULT_BAUD_RATE = 9600 # Serial baud rate to use if no other specified |
| 13 | +DEFAULT_SERIAL_TIMEOUT = 2 # Serial timeout to use if not specified |
| 14 | +DEFAULT_READ_TIMEOUT = 1 #How long to sit looking for the correct character sequence. |
| 15 | + |
| 16 | +DEFAULT_LOGGING_LEVEL = logging.DEBUG |
| 17 | + |
| 18 | +MSG_CHAR_1 = b'\x42' # First character to be recieved in a valid packet |
| 19 | +MSG_CHAR_2 = b'\x4d' # Second character to be recieved in a valid packet |
| 20 | + |
| 21 | +class PlantowerReading(object): |
| 22 | + """ |
| 23 | + Describes a single reading from the PMS5003 sensor |
| 24 | + """ |
| 25 | + def __init__(self, line): |
| 26 | + """ |
| 27 | + Takes a line from the Plantower serial port and converts it into |
| 28 | + an object containing the data |
| 29 | + """ |
| 30 | + self.timestamp = datetime.utcnow() |
| 31 | + self.pm10_cf1 = line[4] * 256 + line[5] |
| 32 | + self.pm25_cf1 = line[6] * 256 + line[7] |
| 33 | + self.pm100_cf1 = line[8] * 256 + line[9] |
| 34 | + self.pm10_std = line[10] * 256 + line[11] |
| 35 | + self.pm25_std = line[12] * 256 + line[13] |
| 36 | + self.pm100_std = line[14] * 256 + line[15] |
| 37 | + self.gr03um = line[16] * 256 + line[17] |
| 38 | + self.gr05um = line[18] * 256 + line[19] |
| 39 | + self.gr10um = line[20] * 256 + line[21] |
| 40 | + self.gr25um = line[22] * 256 + line[23] |
| 41 | + self.gr50um = line[24] * 256 + line[25] |
| 42 | + self.gr100um = line[26] * 256 + line[27] |
| 43 | + |
| 44 | + def __str__(self): |
| 45 | + return ( |
| 46 | + "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s," % |
| 47 | + (self.pm10_cf1, self.pm10_std, self.pm25_cf1, self.pm25_std, |
| 48 | + self.pm100_cf1, self.pm100_std, self.gr03um, self.gr05um, |
| 49 | + self.gr10um, self.gr25um, self.gr50um, self.gr100um)) |
| 50 | + |
| 51 | +class PlantowerException(Exception): |
| 52 | + """ |
| 53 | + Exception to be thrown if any problems occur |
| 54 | + """ |
| 55 | + pass |
| 56 | + |
| 57 | +class Plantower(object): |
| 58 | + """ |
| 59 | + Actual interface to the PMS5003 sensor |
| 60 | + """ |
| 61 | + def __init__( |
| 62 | + self, port=DEFAULT_SERIAL_PORT, baud=DEFAULT_BAUD_RATE, |
| 63 | + serial_timeout=DEFAULT_SERIAL_TIMEOUT, |
| 64 | + read_timeout=DEFAULT_READ_TIMEOUT, |
| 65 | + log_level=DEFAULT_LOGGING_LEVEL): |
| 66 | + """ |
| 67 | + Setup the interface for the sensor |
| 68 | + """ |
| 69 | + self.logger = logging.getLogger("PMS5003 Interface") |
| 70 | + logging.basicConfig( |
| 71 | + format='%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s') |
| 72 | + self.logger.setLevel(log_level) |
| 73 | + self.port = port |
| 74 | + self.logger.info("Serial port: %s", self.port) |
| 75 | + self.baud = baud |
| 76 | + self.logger.info("Baud rate: %s", self.baud) |
| 77 | + self.serial_timeout = serial_timeout |
| 78 | + self.logger.info("Serial Timeout: %s", self.serial_timeout) |
| 79 | + self.read_timeout = read_timeout |
| 80 | + self.logger.info("Read Timeout: %s", self.read_timeout) |
| 81 | + try: |
| 82 | + self.serial = Serial( |
| 83 | + port=self.port, baudrate=self.baud, |
| 84 | + timeout=self.serial_timeout) |
| 85 | + self.logger.debug("Port Opened Successfully") |
| 86 | + except SerialException as exp: |
| 87 | + self.logger.error(str(exp)) |
| 88 | + raise PlantowerException(str(exp)) |
| 89 | + |
| 90 | + def set_log_level(self, log_level): |
| 91 | + """ |
| 92 | + Enables the class logging level to be changed after it's created |
| 93 | + """ |
| 94 | + self.logger.setLevel(log_level) |
| 95 | + |
| 96 | + def _verify(self, recv): |
| 97 | + """ |
| 98 | + Uses the last 2 bytes of the data packet from the Plantower sensor |
| 99 | + to verify that the data recived is correct |
| 100 | + """ |
| 101 | + calc = 0 |
| 102 | + ord_arr = [] |
| 103 | + for c in bytearray(recv[:-2]): #Add all the bytes together except the checksum bytes |
| 104 | + calc += c |
| 105 | + ord_arr.append(c) |
| 106 | + self.logger.debug(str(ord_arr)) |
| 107 | + sent = (recv[-2] << 8) | recv[-1] # Combine the 2 bytes together |
| 108 | + if sent != calc: |
| 109 | + self.logger.error("Checksum failure %d != %d", sent, calc) |
| 110 | + raise PlantowerException("Checksum failure") |
| 111 | + |
| 112 | + def read(self, perform_flush=True): |
| 113 | + """ |
| 114 | + Reads a line from the serial port and return |
| 115 | + if perform_flush is set to true it will flush the serial buffer |
| 116 | + before performing the read, otherwise, it'll just read the first |
| 117 | + item in the buffer |
| 118 | + """ |
| 119 | + recv = b'' |
| 120 | + start = datetime.utcnow() #Start timer |
| 121 | + if perform_flush: |
| 122 | + self.serial.flush() #Flush any data in the buffer |
| 123 | + while( |
| 124 | + datetime.utcnow() < |
| 125 | + (start + timedelta(seconds=self.read_timeout))): |
| 126 | + inp = self.serial.read() # Read a character from the input |
| 127 | + if inp == MSG_CHAR_1: # check it matches |
| 128 | + recv += inp # if it does add it to recieve string |
| 129 | + inp = self.serial.read() # read the next character |
| 130 | + if inp == MSG_CHAR_2: # check it's what's expected |
| 131 | + recv += inp # att it to the recieve string |
| 132 | + recv += self.serial.read(30) # read the remaining 30 bytes |
| 133 | + self._verify(recv) # verify the checksum |
| 134 | + return PlantowerReading(recv) # convert to reading object |
| 135 | + #If the character isn't what we are expecting loop until timeout |
0 commit comments