Advertisement
pcpszc

Plantower reading library

May 26th, 2024
416
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 8.83 KB | Source Code | 0 0
  1. import argparse
  2. import os
  3. import signal
  4. from datetime import datetime
  5. from time import time, sleep
  6.  
  7. import serial
  8. import serial.serialutil
  9. from influxdb_client import InfluxDBClient
  10.  
  11. _EXIT = False
  12.  
  13.  
  14. def handle_signal(signum, frame):
  15.     global _EXIT
  16.     print("\nreceived %d signal, prepare to exit" % signum)
  17.     _EXIT = True
  18.  
  19.  
  20. def parse_args():
  21.     parser = argparse.ArgumentParser()
  22.     parser.add_argument(dest='file', nargs='?', type=str, default='/dev/ttyUSB0',
  23.                         help="tty device, select your serial port (default: /dev/ttyUSB0)")
  24.     parser.add_argument('-b', '--baudrate', dest='baudrate', metavar='', type=int, default=9600,
  25.                         help="set serialport baudrate (default: 9600)")
  26.     parser.add_argument('-d', '--delay', dest='delay', metavar='', type=float, default=0.0,
  27.                         help="delay between readings (default: fastest possible)")
  28.     parser.add_argument('-c', '--count', dest='count', metavar='', type=int, default=0,
  29.                         help="how many readings to do (default: infinity)")
  30.     return parser.parse_args()
  31.  
  32.  
  33. class PlantowerRecord:
  34.     """
  35.    object contains all data from sensor,
  36.    """
  37.  
  38.     def __init__(self, data_dict: dict):
  39.         self.timestamp = datetime.now()
  40.         self.__dict__.update(data_dict)
  41.  
  42.     def __getitem__(self, item):
  43.         return self.__dict__[item]
  44.  
  45.     def __str__(self):
  46.         string = (
  47.             ">>> {timestamp:s} \n"
  48.             "(ug/m³)  normal     env  %normal    %env  norm\n"
  49.             "PM  1.0: {pm1_0:>6d}  {pm1_0_env:>6d}   ---.-%  ---.-%  None\n"
  50.             "PM  2.5: {pm2_5:>6d}  {pm2_5_env:>6d}  {pm2_5_normn:6.1f}% {pm2_5_norme:6.1f}%  25\n"
  51.             "PM 10.0: {pm10_0:>6d}  {pm10_0_env:>6d}  {pm10_0_normn:6.1f}% {pm10_0_norme:6.1f}%  50\n\n"
  52.             "particles count (um / 0.1 L)\n"
  53.             " 0.3 um: {um0_3:>8d}\n"
  54.             " 0.5 um: {um0_5:>8d}\n"
  55.             " 1.0 um: {um1_0:>8d}\n"
  56.             " 2.5 um: {um2_5:>8d}\n"
  57.             " 5.0 um: {um5_0:>8d}\n"
  58.             "10.0 um: {um10_0:>8d}\n\n"
  59.             "checksum: {check:s}\n"
  60.         )
  61.         return string.format(
  62.             timestamp=str(self.timestamp),
  63.             pm1_0=self.data1, pm2_5=self.data2, pm10_0=self.data3, pm1_0_env=self.data4, pm2_5_env=self.data5,
  64.             pm10_0_env=self.data6, um0_3=self.data7, um0_5=self.data8, um1_0=self.data9, um2_5=self.data10,
  65.             um5_0=self.data11, um10_0=self.data12,
  66.             pm2_5_norme=(self.data5 / 25) * 100, pm10_0_norme=(self.data6 / 50) * 100,
  67.             pm2_5_normn=(self.data2 / 25) * 100, pm10_0_normn=(self.data3 / 50) * 100,
  68.             check='Match' if self.checksum_match else "NOT MATCH (data error occurred)"
  69.         )
  70.  
  71.  
  72. def verify_offset(fd, data):
  73.     """take care of data position"""
  74.     if not (data[0], data[1]) == (0x42, 0x4d):  # check if first two bytes matches to device protocol specification
  75.         print('Data not in proper position, trying to set proper offset...')
  76.         set_fixed_position(fd)
  77.         data = fd.read(32)
  78.         if not (data[0], data[1]) == (0x42, 0x4d):
  79.             print(
  80.                 'Synchronisation failed, check if proper baudrate are set (default: 9600, see --help)')
  81.             exit(1)
  82.  
  83.  
  84. def set_fixed_position(fd):
  85.     """Simple method to set file pointer on proper position (where 0x424d becoming first two values)"""
  86.     data = fd.read(32)
  87.     if (data[0], data[1]) == (0x42, 0x4d):
  88.         return
  89.  
  90.     offset = 0
  91.     for i in range(31):
  92.         if (data[i], data[i + 1]) == (0x42, 0x4d):
  93.             offset = i
  94.     fd.read(offset)
  95.  
  96.  
  97. def read_data(fd) -> PlantowerRecord:
  98.     """Simple method that reads data from sensor and return prepared PlantowerRecord object"""
  99.     data = fd.read(32)
  100.  
  101.     verify_offset(fd, data)
  102.  
  103.     calculated_checksum = 0
  104.     for i in range(29):
  105.         calculated_checksum += data[i]  # calculated checksum on given data
  106.  
  107.     checksum = (data[30] << 8) + data[31]  # checksum provided by device
  108.  
  109.     data_dict = {
  110.         # frame data size, not useful for user
  111.         'frame': (data[2] << 8) + data[3],
  112.  
  113.         # pm1.0 unit ug/m3 (CF=1, standard particle) (?)
  114.         'data1': (data[4] << 8) + data[5],
  115.         # pm2.5 unit ug/m3 (CF=1, standard particle) (?)
  116.         'data2': (data[6] << 8) + data[7],
  117.         # pm10 unit ug/m3 (CF=1, standard particle) (?)
  118.         'data3': (data[8] << 8) + data[9],
  119.  
  120.         # pm1.0 unit ug/m3 (under atmospheric environment) (?)
  121.         'data4': (data[10] << 8) + data[11],
  122.         # pm2.5 unit ug/m3 (under atmospheric environment) (?)
  123.         'data5': (data[12] << 8) + data[13],
  124.         # pm10 unit ug/m3 (under atmospheric environment) (?)
  125.         'data6': (data[14] << 8) + data[15],
  126.  
  127.         # number of particles with diameter beyond 0.3 um in 0.1L of air.
  128.         'data7': (data[16] << 8) + data[17],
  129.         # number of particles with diameter beyond 0.5 um in 0.1L of air.
  130.         'data8': (data[18] << 8) + data[19],
  131.         # number of particles with diameter beyond 1.0 um in 0.1L of air.
  132.         'data9': (data[20] << 8) + data[21],
  133.         # number of particles with diameter beyond 2.5 um in 0.1L of air.
  134.         'data10': (data[22] << 8) + data[23],
  135.         # number of particles with diameter beyond 5.0 um in 0.1L of air.
  136.         'data11': (data[24] << 8) + data[25],
  137.         # the number of particles with diameter beyond 10.0 um in 0.1L of air.
  138.         'data12': (data[26] << 8) + data[27],
  139.         'data13': (data[28] << 8) + data[29],  # reserved, not used
  140.         'checksum_match': True if calculated_checksum == checksum else False
  141.     }
  142.  
  143.     return PlantowerRecord(data_dict)
  144.  
  145.  
  146. def save(client, record):
  147.     if not record.checksum_match:
  148.         print("checksum missmatch!")
  149.         return
  150.  
  151.     now = datetime.now()
  152.     t = int(now.timestamp() * 1000 * 1000 * 1000)
  153.     #print(now.isoformat())
  154.     data = [
  155.         {
  156.             "measurement": "plantower",
  157.             "time": t,
  158.             "tags": {
  159.                     "data_type": "CF1",
  160.                     "val_type": "mass",
  161.                 },
  162.             "fields": {
  163.                 "1.0": record.data1,
  164.                 "2.5": record.data2,
  165.                 "10": record.data3,
  166.             }
  167.         },
  168.         {
  169.             "measurement": "plantower",
  170.             "time": t,
  171.             "tags": {
  172.                     "data_type": "atmo",
  173.                     "val_type": "mass",
  174.                 },
  175.             "fields": {
  176.                 "1.0": record.data4,
  177.                 "2.5": record.data5,
  178.                 "10": record.data6,
  179.             }
  180.         },
  181.         {
  182.             "measurement": "plantower",
  183.             "time": t,
  184.             "tags": {
  185.                     "val_type": "count",
  186.                 },
  187.             "fields": {
  188.                 "0.3": record.data7,
  189.                 "0.5": record.data8,
  190.                 "1.0": record.data9,
  191.                 "2.5": record.data10,
  192.                 "5.0": record.data11,
  193.                 "10": record.data12,
  194.             }
  195.         },
  196.  
  197.     ]
  198.     client.write("grafana", "pi4black", data)
  199.  
  200.  
  201. def print_loop(fd, args):
  202.     client = InfluxDBClient(
  203.         url="http://pi4black:8086",
  204.         token="8WpxAr74led6pgP1i_lu4PyhVYUfuxPEP-3Zcth3GL4AehXFTHqomrFaRUYWb7u5DUzhzT1JB0ep9HX9KGLxQw==",
  205.         org="pi4black",
  206.     )
  207.     write_api = client.write_api()
  208.  
  209.     check_time = time() + args.delay
  210.  
  211.     reported_ok = False
  212.     while not _EXIT:
  213.         record = read_data(fd)
  214.         if time() >= check_time:
  215.             check_time = time() + args.delay
  216.             #print(record)
  217.             try:
  218.                 save(write_api, record)
  219.             except Exception as e:
  220.                 print("save failed:", e)
  221.             else:
  222.                 if not reported_ok:
  223.                     print("save success! from now only reporting on failures")
  224.                     reported_ok = True
  225.  
  226.  
  227. def connect(file, baudrate):
  228.     print('connecting to %s... ' % file, end='')
  229.     while not _EXIT:
  230.         if not os.path.exists(file):
  231.             sleep(0.1)
  232.             continue
  233.         try:
  234.             s = serial.Serial(port=file, baudrate=baudrate)
  235.         except serial.serialutil.SerialException:
  236.             sleep(0.1)
  237.             continue
  238.         else:
  239.             print('Connected.')
  240.             return s
  241.  
  242.  
  243. def main():
  244.     args = parse_args()
  245.     signal.signal(signal.SIGINT, handle_signal)
  246.     signal.signal(signal.SIGQUIT, handle_signal)
  247.  
  248.     fd = connect(args.file, args.baudrate)
  249.     while not _EXIT:
  250.         try:
  251.             print_loop(fd, args)
  252.         except serial.serialutil.SerialException:
  253.             print('Error ocurred, device disconnected? Trying to restore')
  254.             fd.close()
  255.             fd = connect(args.file, args.baudrate)
  256.  
  257.     if fd:
  258.         fd.close()
  259.     exit(0)
  260.  
  261.  
  262. if __name__ == "__main__":
  263.     main()
Tags: plantower
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement