diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..afed073 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.csv diff --git a/Alphasense/.gitkeep b/Alphasense/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/Alphasense/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/Alphasense/OPC_Simple_v2.py b/Alphasense/OPC_Simple_v2.py new file mode 100644 index 0000000..baa91db --- /dev/null +++ b/Alphasense/OPC_Simple_v2.py @@ -0,0 +1,174 @@ +#Created by: Kaleb Nails, Erik Liebergall, Marc Compere +#created : 10/6/2023 +#modified: 13 Dec 2023 +#from time import sleep +import sys +from usbiss.spi import SPI +import opcng as opc +import os +import time +import signal +import subprocess +from datetime import datetime # datetime.now() + + +#this is a slightly ugly way to make sure the while loops are killed and the program can exit propperly +global DataLoop +global InitializeLoop +InitializeLoop = True +DataLoop = True + +# This is to handle interuptions and turn the alphasense +def handle_interrupt(signal, frame): + global DataLoop + global InitializeLoop + print("Ctrl+C pressed. Performing cleanup or other actions...") + + #exits the while loops + DataLoop = False + InitializeLoop = False + time.sleep(.5) + dev.off() + print('sensor off') + exit(0) # Terminate the script gracefully + +signal.signal(signal.SIGINT, handle_interrupt) + + +device='/dev/ttyACM0' +#device='/dev/ttyUSB1' + + + +if len(sys.argv)==1: + #print('sys.argv[0]={0}'.format(sys.argv[0])) + print('provide a device name to read, like:') + print(' python3 pm25_SPS30_Senirion_Run.py /dev/ttyACM0' +"\n") + print('\033[91mWARNING: DEFAULT PORT WILL BE ACM0\033[0m' +"\n") + print('this default setting was left for developement' + "\n" + "\n") + time.sleep(2.5) + + + +if len(sys.argv)>1: + print('using command line arg, and provided device!') + device=sys.argv[1] + + +#setting up more sensor stuff from the library +spi = SPI(device) +spi.mode = 1 +spi.max_speed_hz = 500000 +spi.lsbfirst = False + +#This loop initializes the sensor and should only run at the start +while InitializeLoop == True: + try: + #This detects which model of opc and prints the relevent data + dev = opc.detect(spi) + # print(type(dev)) + print(f'device information: {dev.info()}') + print(f'serial: {dev.serial()}') + + #this just formats the serial number string to look better + SerialNumberStr = str(dev.serial()) + SerialNumberStr = SerialNumberStr.replace("N3","N3-") + SerialNumberStr = SerialNumberStr.replace(" ","") + + print(f'firmware version: {dev.serial()}') + print('sucessfully connected to device') + break + + except Exception as e: + print('ERROR connecting to sensor, trying again........') + print(e) + time.sleep(1) + + +#This exits one directory at a time so its on the local computer so you dont get csvs on your repository +os.chdir("..") +os.chdir("..") + +#This creates the file and the first row and and the labels +fname = '{0}_Alphasense_{1}.csv'.format(datetime.now().strftime("%Y_%m_%d__%H_%M_%S"), SerialNumberStr) +file = open(fname,'w') +titleStr = 'Serial Number,Date Label, Dates (YMD), Bin 0, Bin 1, Bin 2, Bin 3, Bin 4, Bin 5, Bin 6, Bin 7, Bin 8, Bin 9, Bin 10, Bin 11, Bin 12, Bin 13, Bin 14, Bin 15, Bin 16, Bin 17, Bin 18, Bin 19, Bin 20, Bin 21, Bin 22, Bin 23, Bin1 MToF,Bin3 MToF,Bin5 MToF,Bin7 MToF,Sampling Period,SFR,Temperature C,Relative humidity,PM1 ug/m3,PM2.5 ug/m3,PM10 ug/m3,#RejectGlitch,#RejectLongTOF,#RejectRatio,#RejectOutOfRange,Fan rev count,Laser status,Checksum' +file.write(titleStr +"\n") +file.flush() +dev.on() + +#This loop controls data collection +while DataLoop ==True: + try: + # query particle mass readings + time.sleep(1) + #print(dev.pm()) + recorded_data = dev.histogram() + print(recorded_data) + + dataStr = ', {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}, {12}, {13}, {14}, {15}, {16}, {17}, {18}, {19}, {20}, {21}, {22}, {23},\ + {24}, {25}, {26}, {27},\ + {28}, {29}, {30}, {31},\ + {32}, {33}, {34}, \ + {35}, {36}, {37}, {38}, {39}, {40}, {41}'.format( + recorded_data.get('Bin 0', ''), + recorded_data.get('Bin 1', ''), + recorded_data.get('Bin 2', ''), + recorded_data.get('Bin 3', ''), + recorded_data.get('Bin 4', ''), + recorded_data.get('Bin 5', ''), + recorded_data.get('Bin 6', ''), + recorded_data.get('Bin 7', ''), + recorded_data.get('Bin 8', ''), + recorded_data.get('Bin 9', ''), + recorded_data.get('Bin 10', ''), + recorded_data.get('Bin 11', ''), + recorded_data.get('Bin 12', ''), + recorded_data.get('Bin 13', ''), + recorded_data.get('Bin 14', ''), + recorded_data.get('Bin 15', ''), + recorded_data.get('Bin 16', ''), + recorded_data.get('Bin 17', ''), + recorded_data.get('Bin 18', ''), + recorded_data.get('Bin 19', ''), + recorded_data.get('Bin 20', ''), + recorded_data.get('Bin 21', ''), + recorded_data.get('Bin 22', ''), + recorded_data.get('Bin 23', ''), + recorded_data.get('Bin1 MToF', ''), # 24 + #recorded_data.get('Bin2 MToF', ''), + recorded_data.get('Bin3 MToF', ''), # 25 + #recorded_data.get('Bin4 MToF', ''), + recorded_data.get('Bin5 MToF', ''), # 26 + #recorded_data.get('Bin6 MToF', ''), + recorded_data.get('Bin7 MToF', ''), # 27 + + recorded_data.get('Sampling Period', ''), # 28 + recorded_data.get('SFR', ''), # 29 + recorded_data.get('Temperature', ''), # 30 + recorded_data.get('Relative humidity', ''), # 31 + + recorded_data.get('PM1', ''), # 32 (ug/m3), datasheet pdf page 13 + recorded_data.get('PM2.5', ''), # 33 (ug/m3) + recorded_data.get('PM10', ''), # 34 (ug/m3) + + recorded_data.get('#RejectGlitch', ''), # 35 + recorded_data.get('#RejectLongTOF', ''), # 36 + recorded_data.get('#RejectRatio', ''), # 37 + recorded_data.get('#RejectOutOfRange', ''), # 38 + recorded_data.get('Fan rev count', ''), # 39 + recorded_data.get('Laser status', ''), # 40 + recorded_data.get('Checksum', '')) # 41 + + dateStr =', Date:, {0}'.format(datetime.now()) + + print("\n" + SerialNumberStr + dateStr + dataStr + "\n") + file.write(SerialNumberStr + dateStr + dataStr + "\n") + file.flush() + + except BaseException as e: + print(e) + except RuntimeError: + print("Unable to read from sensor, retrying...") + continue + diff --git a/Alphasense/README.md b/Alphasense/README.md index 8b13789..f52d16c 100644 --- a/Alphasense/README.md +++ b/Alphasense/README.md @@ -1 +1,7 @@ +This is code to read the alphasense through USB. There is also code to read it through the aurduino if thats possible. + + + +Below is an image from the datasheet: +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/1c60443e-5b40-4c41-8a4c-d5d731657155) diff --git a/Alphasense/start_Alphasense_loggers.sh b/Alphasense/start_Alphasense_loggers.sh new file mode 100644 index 0000000..3a54778 --- /dev/null +++ b/Alphasense/start_Alphasense_loggers.sh @@ -0,0 +1,20 @@ +#!/bin/bash -x +# +# bash shell script to start multiple logger codes +# +# mdc +# created : 13 Dec 2023 +# modified: 13 Dec 2023 + +alpha1_device='/dev/ttyACM0' +alpha2_device='/dev/ttyACM1' + +echo "starting alpha1 logger" +screen -dm -S alpha1 python3 OPC_Simple_v2.py $alpha1_device + +echo "starting alpha2 logger" +screen -dm -S alpha2 python3 OPC_Simple_v2.py $alpha2_device + + + + diff --git a/Alphasense_Start_Script.sh b/Alphasense_Start_Script.sh new file mode 100644 index 0000000..20ed22c --- /dev/null +++ b/Alphasense_Start_Script.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +alpha0_device="/dev/ttyACM0" +echo "Starting alpha0 logger" +screen -dm -S alpha0 python3 OPC_Simple_v2.py $alpha0_device + +alpha1_device="/dev/ttyACM1" +echo "Starting alpha1 logger" +screen -dm -S alpha1 python3 OPC_Simple_v2.py $alpha1_device + +alpha2_device="/dev/ttyACM2" +echo "Starting alpha2 logger" +screen -dm -S alpha2 python3 OPC_Simple_v2.py $alpha2_device + diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..e270796 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,9 @@ +cff-version: 1.0.0 +message: "If you use this software, please cite it as below. Additional authors and contributors are cited in individual files or READMEs." +authors: + - family-names: Nails + given-names: Kaleb + orcid: https://orcid.org/0009-0007-8786-837X +title: "MOVEUAS PM sensor software" +version: patch-1 +date-released: 2023-07-19 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1c739a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 MOVEUAS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PMS plantower/README.md b/PMS plantower/README.md new file mode 100644 index 0000000..5a6289f --- /dev/null +++ b/PMS plantower/README.md @@ -0,0 +1,23 @@ +# Documentation # +Official Documentation: +https://www.aqmd.gov/docs/default-source/aq-spec/resources-page/plantower-pms5003-manual_v2-3.pdf + +Necessary uncommon imports pip commands are found on the line they are imported in. +(THIS IMAGE IS NOT THE EXACT SENSOR, BUT THEY ARE THE SAME AT A GLANCE, just for reference) +![Alt Text](https://m.media-amazon.com/images/I/71+cgzmX+pL._AC_SX679_.jpg) + + + +# Personal Knowledge # +For this code run: python3 pm25_simpletest.py +this will default to USB0 of your computer, and you will get a warning as well. + +If you have multiple sensors or its just on another port use: +python3 pm25_simpletest.py /dev/ttyUSB# + +The # is the port it is on. + + +BELOW ARE IMAGES ON HOW WE WIRE THEM (not for everyone): +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/d1159ea7-c33e-477f-a33f-659150fc29de) +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/ca50b5bd-99b6-46cc-95a3-b07300ec5d48) diff --git a/PMS plantower/pm25_simpletest.py b/PMS plantower/pm25_simpletest.py index 55d9712..c7b4358 100644 --- a/PMS plantower/pm25_simpletest.py +++ b/PMS plantower/pm25_simpletest.py @@ -15,20 +15,30 @@ # Kaleb Nails, nailsk@my.erau.edu # Marc Compere, comperem@erau.edu # created : 10 Feb 2023 -# modified: 06 Apr 2023 +# modified: 11 Oct 2023 """ Example sketch to connect to PM2.5 sensor with either I2C or UART. """ # pylint: disable=unused-import + + import time import board -import busio +import busio #pip3 install adafruit-blinka +import os +import sys from datetime import datetime # datetime.now() from digitalio import DigitalInOut, Direction, Pull #from adafruit_pm25.i2c import PM25_I2C +# For use with USB-to-serial cable: +import serial + +# go back 2 directories to store CSV's +os.chdir("..") +os.chdir("..") reset_pin = None # If you have a GPIO, its not a bad idea to connect it to the RESET pin @@ -49,29 +59,61 @@ # import serial # uart = serial.Serial("/dev/ttyS0", baudrate=9600, timeout=0.25) -# For use with USB-to-serial cable: -import serial -dev1='/dev/ttyUSB1' -dev2='/dev/ttyUSB2' +dev1='/dev/ttyUSB0' +devLabel = 'unlabeled' +# dev2='/dev/ttyUSB2' + + +if len(sys.argv)==1: + #print('sys.argv[0]={0}'.format(sys.argv[0])) + print('provide a device name to read, like:') + print(' python3 pm25_simpletest.py /dev/ttyUSB0' +"\n") + print('\033[91mWARNING: DEFAULT PORT WILL BE USB0\033[0m' +"\n") + print('this default setting was left for developement' + "\n" + "\n") + time.sleep(2.5) + + +if len(sys.argv)>1: + print('using command line arg, and provided device!') + dev1=sys.argv[1] + +# Plantower does not report unique serial number, so must label reporting device on the command line +if len(sys.argv)>2: + devLabel=sys.argv[2] + print('using command line arg 2, device label: {}'.format(devLabel)) # make this match the handwritten label using the cmd line arg #2 + +devName=os.path.basename(dev1) # get device name for logfile name +print('using devName=[{0}]'.format(devName)) + uart = serial.Serial(dev1, baudrate=9600, timeout=0.25) -uart2 = serial.Serial(dev2, baudrate=9600, timeout=0.25) +# uart2 = serial.Serial(dev2, baudrate=9600, timeout=0.25) # Connect to a PM2.5 sensor over UART -from adafruit_pm25.uart import PM25_UART +from adafruit_pm25.uart import PM25_UART #pip3 install adafruit-circuitpython-pm25 pm25 = PM25_UART(uart, reset_pin) -pm25_2 = PM25_UART(uart2, reset_pin) +# pm25_2 = PM25_UART(uart2, reset_pin) # Create library object, use 'slow' 100KHz frequency! #i2c = busio.I2C(board.SCL, board.SDA, frequency=100000) # Connect to a PM2.5 sensor over I2C #pm25 = PM25_I2C(i2c, reset_pin) -fname = '{0}_pm25_simplest_CSV.csv'.format(datetime.now().strftime("%Y_%m_%d__%H_%M_%S") ) +#fname = '{0}_pm25_simplest_plantower_CSV.csv'.format(datetime.now().strftime("%Y_%m_%d__%H_%M_%S") ) +fname = '{0}_pm25_simplest_plantower_{1}_CSV.csv'.format(datetime.now().strftime("%Y_%m_%d__%H_%M_%S"),devLabel ) # <-- handwritten label (not usb device) + file = open(fname,'w') - + print("Found PM2.5 sensor, reading data...") +#adds heading columns +#Add Labels to the top of the file TEST this works +titleStr = 'Date Label, Dates (YMD), Sensor 1, pm1.0 standard ug/m3, pm2.5 standard ug/m3, pm10.0 standard ug/m3, pm1.0 env ug/m3, pm2.5 env ug/m3, pm10.0 env ug/m3,particles 0.3um, particles 0.5um, particles 1.0um, particles 2.5um, particles 5.0um, particles 10.0um'#, Dates (YMD), Sensor 2, pm10 standard, pm25 standard, pm100 standard, pm10 env, pm25 env, pm100 env,particles 03um, particles 05um, particles 10um, particles 25um, particles 50um, particles 100um' +file.write(titleStr +"\n") +file.flush() + + + while True: time.sleep(1) @@ -84,6 +126,7 @@ print() print("Concentration Units (standard)") + print(f"TIME: {time.strftime('%H:%M:%S', time.localtime())}") print("---------------------------------------") print( "PM 1.0: %d\tPM2.5: %d\tPM10: %d" @@ -103,100 +146,76 @@ print("Particles > 5.0um / 0.1L air:", aqdata["particles 50um"]) print("Particles > 10 um / 0.1L air:", aqdata["particles 100um"]) print("---------------------------------------") - + dateStr1 =', Date:, {0}'.format(datetime.now()) # while False: - time.sleep(1) - - try: - aqdata2 = pm25_2.read() - # print(aqdata) - except RuntimeError: - print("Unable to read from sensor, retrying...") - continue - - print() - print("Concentration Units (standard)") - print("---------------------------------------") - print( - "PM 1.0: %d\tPM2.5: %d\tPM10: %d" - % (aqdata2["pm10 standard"], aqdata2["pm25 standard"], aqdata2["pm100 standard"]) - ) - print("Concentration Units (environmental)") - print("---------------------------------------") - print( - "PM 1.0: %d\tPM2.5: %d\tPM10: %d" - % (aqdata2["pm10 env"], aqdata2["pm25 env"], aqdata2["pm100 env"]) - ) - print("---------------------------------------") - print("Particles2 > 0.3um / 0.1L air:", aqdata2["particles 03um"]) - print("Particles2 > 0.5um / 0.1L air:", aqdata2["particles 05um"]) - print("Particles2 > 1.0um / 0.1L air:", aqdata2["particles 10um"]) - print("Particles2 > 2.5um / 0.1L air:", aqdata2["particles 25um"]) - print("Particles2 > 5.0um / 0.1L air:", aqdata2["particles 50um"]) - print("Particles2 > 10 um / 0.1L air:", aqdata2["particles 100um"]) - print("---------------------------------------") - - dateStr =', Date:, {0}'.format(datetime.now()) + #time.sleep(1) + + # try: + # aqdata2 = pm25_2.read() + # # print(aqdata) + # except RuntimeError: + # print("Unable to read from sensor, retrying...") + # continue + + # print() + # print("Concentration Units (standard)") + # print("---------------------------------------") + # print( + # "PM 1.0: %d\tPM2.5: %d\tPM10: %d" + # % (aqdata2["pm10 standard"], aqdata2["pm25 standard"], aqdata2["pm100 standard"]) + # ) + # print("Concentration Units (environmental)") + # print("---------------------------------------") + # print( + # "PM 1.0: %d\tPM2.5: %d\tPM10: %d" + # % (aqdata2["pm10 env"], aqdata2["pm25 env"], aqdata2["pm100 env"]) + # ) + # print("---------------------------------------") + # print("Particles2 > 0.3um / 0.1L air:", aqdata2["particles 03um"]) + # print("Particles2 > 0.5um / 0.1L air:", aqdata2["particles 05um"]) + # print("Particles2 > 1.0um / 0.1L air:", aqdata2["particles 10um"]) + # print("Particles2 > 2.5um / 0.1L air:", aqdata2["particles 25um"]) + # print("Particles2 > 5.0um / 0.1L air:", aqdata2["particles 50um"]) + # print("Particles2 > 10 um / 0.1L air:", aqdata2["particles 100um"]) + # print("---------------------------------------") + + # dateStr2 =', Date:, {0}'.format(datetime.now()) #dataStr1 = ', Data1:, {0}, {1}, {2}, {3}, {4}, {5}'.format(aqdata["particles 03um"],aqdata["particles 05um"],aqdata["particles 10um"],aqdata["particles 25um"],aqdata["particles 50um"],aqdata["particles 100um"]) #dataStr2 = ', Data2:, {0}, {1}, {2}, {3}, {4}, {5}'.format(aqdata2["particles 03um"],aqdata2["particles 05um"],aqdata2["particles 10um"],aqdata2["particles 25um"],aqdata2["particles 50um"],aqdata2["particles 100um"]) - - dataStr1 = ', Data1:, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}'.format( \ + + dataStr1 = ' Data1:, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}'.format( \ aqdata["pm10 standard"], \ aqdata["pm25 standard"], \ aqdata["pm100 standard"], \ - + aqdata["pm10 env"], \ aqdata["pm25 env"], \ aqdata["pm100 env"], \ - + aqdata["particles 03um"], \ aqdata["particles 05um"], \ aqdata["particles 10um"], \ aqdata["particles 25um"], \ aqdata["particles 50um"], \ aqdata["particles 100um"]) - - dataStr2 = ', Data2:, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}'.format( \ - aqdata2["pm10 standard"], \ - aqdata2["pm25 standard"], \ - aqdata2["pm100 standard"], \ - - aqdata2["pm10 env"], \ - aqdata2["pm25 env"], \ - aqdata2["pm100 env"], \ - - aqdata2["particles 03um"], \ - aqdata2["particles 05um"], \ - aqdata2["particles 10um"], \ - aqdata2["particles 25um"], \ - aqdata2["particles 50um"], \ - aqdata2["particles 100um"]) - file.write(dateStr + dataStr1 + dataStr2 + "\n") - file.flush() - - - - - - - - - - - - - - - - - - - - - - + # dataStr2 = ', Data2:, {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}, {10}, {11}'.format( \ + # aqdata2["pm10 standard"], \ + # aqdata2["pm25 standard"], \ + # aqdata2["pm100 standard"], \ + # aqdata2["pm10 env"], \ + # aqdata2["pm25 env"], \ + # aqdata2["pm100 env"], \ + # aqdata2["particles 03um"], \ + # aqdata2["particles 05um"], \ + # aqdata2["particles 10um"], \ + # aqdata2["particles 25um"], \ + # aqdata2["particles 50um"], \ + # aqdata2["particles 100um"]) + # file.write(dateStr1 + dataStr1 + dateStr2 + dataStr2 +"\n") + file.write(dateStr1 + dataStr1 + "\n") + file.flush() diff --git a/PMS plantower/start_Plantower_loggers.sh b/PMS plantower/start_Plantower_loggers.sh new file mode 100644 index 0000000..1d3fb3a --- /dev/null +++ b/PMS plantower/start_Plantower_loggers.sh @@ -0,0 +1,27 @@ +#!/bin/bash -x +# +# bash shell script to start multiple logger codes +# +# mdc +# created : 13 Dec 2023 +# modified: 13 Dec 2023 + +device1='/dev/ttyUSB2' +device2='/dev/ttyUSB3' +device3='/dev/ttyUSB4' +device4='/dev/ttyUSB5' + +echo "starting plantower1 logger" +screen -dm -S pt1 python3 pm25_simpletest.py $device1 pt1 + +echo "starting plantower2 logger" +screen -dm -S pt2 python3 pm25_simpletest.py $device2 pt2 + +echo "starting plantower3 logger" +screen -dm -S pt3 python3 pm25_simpletest.py $device3 pt3 + +echo "starting plantower4 logger" +screen -dm -S pt4 python3 pm25_simpletest.py $device4 pt4 + + + diff --git a/Post_Processing/Post_Processing.py b/Post_Processing/Post_Processing.py new file mode 100644 index 0000000..4910541 --- /dev/null +++ b/Post_Processing/Post_Processing.py @@ -0,0 +1,120 @@ +#Created by Kaleb Nails, Ahsan Ali + + +import pandas as pd +import os +from datetime import datetime +import numpy as np + + + + +def Process(df): + + #print(list(df.keys())[0]) + column_length= len(list(df.values())[0].keys()) + print(f"each sensor has {column_length} columns") + + # Create column headers based on the number of sensors + columns = [ + f"{col}_{i}" + for i in range(len(df)) + for col in df[list(df.keys())[0]].keys() + ] + #print(columns) + new_column = [] + + #new_column += [element for name in columns for element in columns if name[:-2] in element] this might do it in one line but i dont care + #print(new_column) + + #reoganize you columns + for name in columns: + + #print(name[:-2]) + result = [element for element in columns if name[:-2] in element] + new_column += result #it was in a double matrix in a matrix, this stops it from being that way + + new_column = np.unique(new_column) #I need the unique, is the easiest way to removing some duplicates I accidentally created + print(f"\nthe new organized column is \n {new_column}") + columns = new_column + + # Create an empty DataFrame with the specified columns + result = pd.DataFrame(columns=columns) + + # Iterate over the sensor data + sensor_count = 0 + for df_name, df_data in df.items(): + # Iterate over the columns in the sensor data + #print(f"df_data {df_name} is:{df_data}") + + for col in df_data.keys(): + # Append columns from each sensor to the interleaved_columns list + result[f"{col}_{sensor_count}"] = df_data[col] + sensor_count = sensor_count + 1 + + + #print(result) + result.to_csv(f"{datetime.now().strftime('%Y_%m_%d')}_{list(df.keys())[0][:-2]}.csv", index=False) + print(f"Saved {datetime.now().strftime('%Y_%m_%d')}_{list(df.keys())[0][:-2]}.csv") + return result + + + +#define initial variables +Sensirion_df = {} +Sensirion_count = 0 + +plantower_df = {} +plantower_count = 0 + +Alphasense_df = {} +Alphasense_count = 0 + +#Go to the directory above it to look for the csv's +current_dir = os.path.abspath(os.curdir) +parent_dir = os.path.dirname(current_dir) + +for filename in os.listdir(parent_dir): + if os.path.isfile(os.path.join(parent_dir, filename)) and filename.endswith(".csv"): + + if "Sensirion" in filename: + print("Sensirion file being read:", filename) + df_name = f"Sensirion_{Sensirion_count}" + Sensirion_count += 1 + Sensirion_df[df_name] = pd.read_csv(os.path.join(parent_dir, filename)) + + #Below is hard coded column headers I am just deleting + Sensirion_df[df_name].drop(columns=['Date Label'], inplace=True) + + elif "plantower" in filename: + print("plantower file being read:", filename) + df_name = f"Plantower_{plantower_count}" + plantower_count += 1 + plantower_df[df_name] = pd.read_csv(os.path.join(parent_dir, filename)) + + #remove extra columns: + plantower_df[df_name].drop(columns=['Date Label', ' Sensor 1'], inplace=True) + + elif "Alphasense" in filename: + print("Alphasense file being read:", filename) + df_name = f"Alphasense_{Alphasense_count}" + Alphasense_count += 1 + Alphasense_df[df_name] = pd.read_csv(os.path.join(parent_dir, filename)) + + #print(Alphasense_df[df_name].keys()) + + #remove extra columns: + Alphasense_df[df_name].drop(columns=['Date Label'," Bin 0", " Bin 1", " Bin 2", " Bin 3", " Bin 4", " Bin 5", " Bin 6", " Bin 7"," Bin 8", " Bin 9", " Bin 10", " Bin 11", " Bin 12", " Bin 13", " Bin 14", " Bin 15"," Bin 16", " Bin 17", " Bin 18", " Bin 19", " Bin 20", " Bin 21", " Bin 22", " Bin 23"," Bin1 MToF", "Bin3 MToF", "Bin5 MToF", "Bin7 MToF", "Sampling Period", "SFR", "Checksum", "Fan rev count","Laser status",'#RejectGlitch','#RejectLongTOF','#RejectOutOfRange','#RejectRatio'], inplace=True) + +#This runs post processing if the files exist, this a simple solution +if Sensirion_count > 0: + print("\n\033[1;31m" + "Processing Sensirion:" + "\033[0m") + Process(Sensirion_df) + +if plantower_count > 0: + print("\n\033[1;31m" + "Processing plantower:" + "\033[0m") + Process(plantower_df) + +if Alphasense_count > 0: + print("\n\033[1;31m" + "Processing Alphasense:" + "\033[0m") + Process(Alphasense_df) diff --git a/Post_Processing/README.md b/Post_Processing/README.md new file mode 100644 index 0000000..0b2cb5a --- /dev/null +++ b/Post_Processing/README.md @@ -0,0 +1,131 @@ +# Sensor Data Post-Processing + +## Overview + +This Python script is designed for post-processing sensor data files. It reads CSV files containing sensor data, organizes the data by sensor type, combines the data, and saves the combined data into new CSV files. The script is configured to work with Sensirion, Plantower, and Alphasense sensor data. +This is robust, but if sensor heading are changed from the sensors it will not work. + +Note the sensors automatically make their csv in the directories directly above /sensor_Code, to run post processing move them into Sensor_Code + + +### 📝 File Structure Before post processing after collecting data, after moving the relevant CSV's into Sensor_Code. + +```text +📦Sensor_Code + ┣ 📂Alphasense + ┃ ┣ 📄OPC_Simple_v2.py + ┃ ┣ 📄README.md + ┃ ┗ 📄start_Alphasense_loggers.sh + ┣ 📂PMS plantower + ┃ ┣ 📄.gitkeep + ┃ ┣ 📄README.md + ┃ ┣ 📄 pm25_simpletest.py + ┃ ┗ 📄start_Plantower_loggers.sh + ┣ 📂Post_Processing + ┃ ┣ 📄Post_Processing.py + ┃ ┗ 📄README.md + ┣ 📂Sensirion + ┃ ┣ 📄.gitkeep + ┃ ┣ 📄 README.md + ┃ ┣ 📄SPS30_Senirion_run.py + ┃ ┣ 📄exampleReadSPS30.py + ┃ ┣ 📄sps30.py + ┃ ┣ 📄start_Sensirion_loggers.sh + ┃ ┗ 📄stop_Sensirion_loggers.sh + ┣ 📂Viasala + ┃ ┗ 📄.gitkeep + ┣ 📄Alphasense_Start_Script.sh + ┣ 📄Sensor_Start_Script.sh + ┣ 📄gui_popup.py + ┣ 📄requirements.txt + ┣ 📄YYYY_MM_DD_##_##_##_Sensirion_sps30_{serial code}.csv + ┣ 📄YYYY_MM_DD_##_##_##_pm25_simplest_plantower_pt#_CSV.csv + ┣ 📄YYYY_MM_DD_##_##_##_Alphasense_OPC-N3-{serial code}.csv + ┗ 📄README.md +``` + +### File structure after post processing, it should look like this, and new files should appear in Post_Processing. Once you are done delete the CSV's in Sensor_Code after you backed them up. + +```text +📦Sensor_Code + ┣ 📂Alphasense + ┃ ┣ 📄OPC_Simple_v2.py + ┃ ┣ 📄README.md + ┃ ┗ 📄start_Alphasense_loggers.sh + ┣ 📂PMS plantower + ┃ ┣ 📄.gitkeep + ┃ ┣ 📄README.md + ┃ ┣ 📄 pm25_simpletest.py + ┃ ┗ 📄start_Plantower_loggers.sh + ┣ 📂Post_Processing + ┃ ┣ 📄Post_Processing.py + ┃ ┣ 📄YYYY_MM_DD_Sensirion.csv + ┃ ┣ 📄YYYY_MM_DD_plantower.csv + ┃ ┣ 📄YYYY_MM_DD_Alphasense.csv + ┃ ┣ 📄Post_Processing.py + ┃ ┗ 📄README.md + ┣ 📂Sensirion + ┃ ┣ 📄.gitkeep + ┃ ┣ 📄 README.md + ┃ ┣ 📄SPS30_Senirion_run.py + ┃ ┣ 📄exampleReadSPS30.py + ┃ ┣ 📄sps30.py + ┃ ┣ 📄start_Sensirion_loggers.sh + ┃ ┗ 📄stop_Sensirion_loggers.sh + ┣ 📂Viasala + ┃ ┗ 📄.gitkeep + ┣ 📄Alphasense_Start_Script.sh + ┣ 📄Sensor_Start_Script.sh + ┣ 📄gui_popup.py + ┣ 📄requirements.txt + ┣ 📄sensor_UART_configs.json + ┣ 📄YYYY_MM_DD_##_##_##_Sensirion_sps30_{serial code}.csv + ┣ 📄YYYY_MM_DD_##_##_##_pm25_simplest_plantower_pt#_CSV.csv + ┣ 📄YYYY_MM_DD_##_##_##_Alphasense_OPC-N3-{serial code}.csv + ┗ 📄README.md +``` + + + + + +## Features + +- **Sensor Data Organization:** The script organizes sensor data by sensor type and combines it into a single DataFrame. +- **Column Interleaving:** Columns from each sensor are interleaved in the output DataFrame. +- **CSV Output:** The combined and cleaned sensor data is saved as a CSV file with a timestamp in the filename. +- **Variable Column Omision:** If you are missing a column or you have one you dont need, there is a + ```bash + .drop(columns=['...']) + ``` + + for each sensor. If you add something inside these brackets they will be removed from the final product. + +## Usage + +1. **Prepare Data Files:** Place the sensor data CSV files in the same directory as the script. +2. **Run the Script:** Execute the script, and it will process the data files and save the combined data in CSV format. + +## Dependencies + +- pandas +- os +- datetime + +## Usage Example + +```bash +cd ~/Sensor_Code/Post_Processing +python post_processing.py +``` +## Dependencies + +- pandas +- os +- datetime + +--- +# plot_generator.py +This is an independent program from what is mentioned above. This uses current file formating as of 2024 to create graphs for a specific amount of sensors listed in the hardcoded program. This should help quick data generation. It is important to note it downsamples down to 20 minutes, so don't use it on very short data sets. Below is a sample image of the type of graph it will generate given the correct inputs. +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/8cd80975-528e-47b7-bceb-4e5dbd0442fd) + diff --git a/Post_Processing/plot_generator.py b/Post_Processing/plot_generator.py new file mode 100644 index 0000000..9f85ebf --- /dev/null +++ b/Post_Processing/plot_generator.py @@ -0,0 +1,115 @@ +# Author: Kaleb Nails +#This is a simple plotter to plot you data, note that is does down sample down to 20 minute intervals. + +import pandas as pd +import matplotlib.pyplot as plt + +# Read the CSV data into DataFrames +df1 = pd.read_csv('2024_03_21__14_25_57_Alphasense_OPC-N3-177380512.csv') +df1 = df1[[' Dates (YMD)', 'PM2.5 ug/m3']] +df1[' Dates (YMD)'] = pd.to_datetime(df1[' Dates (YMD)']) +df1['PM2.5 ug/m3'] = pd.to_numeric(df1['PM2.5 ug/m3'], errors='coerce') +df1.set_index(' Dates (YMD)', inplace=True) +df1_resampled = df1.resample('20min').mean().reset_index() + + +df3 = pd.read_csv('2024_03_21__14_25_57_Alphasense_OPC-N3-177380511.csv') +df3 = df3[[' Dates (YMD)', 'PM2.5 ug/m3']] +df3[' Dates (YMD)'] = pd.to_datetime(df3[' Dates (YMD)']) +df3['PM2.5 ug/m3'] = pd.to_numeric(df3['PM2.5 ug/m3'], errors='coerce') +df3.set_index(' Dates (YMD)', inplace=True) +df3_resampled = df3.resample('20min').mean().reset_index() + + +df2 = pd.read_csv('2024_03_21__10_34_46_vaisala_aqt420.csv', header=None, skipinitialspace=True) +df2 = df2[[0, 12]] # Date is in the first column, PM10 is in the 11th column +print(df2) +df2.columns = [' Dates (YMD)', 'PM2.5 ug/m3'] +df2[' Dates (YMD)'] = pd.to_datetime(df2[' Dates (YMD)']) + +########################################################################### +df4 = pd.read_csv('2024_03_21__14_48_19_pm25_simplest_plantower_pt1_CSV.csv') +print(df4.keys()) +df4 = df4[[' Dates (YMD)', ' pm2.5 standard ug/m3']] +print(df4) +df4[' Dates (YMD)'] = pd.to_datetime(df4[' Dates (YMD)']) +df4[' pm2.5 standard ug/m3'] = pd.to_numeric(df4[' pm2.5 standard ug/m3'], errors='coerce') +df4.set_index(' Dates (YMD)', inplace=True) +df4_resampled = df4.resample('20min').mean().reset_index() + +df5 = pd.read_csv('2024_03_21__14_48_19_pm25_simplest_plantower_pt2_CSV.csv') +df5 = df5[[' Dates (YMD)', ' pm2.5 standard ug/m3']] +df5[' Dates (YMD)'] = pd.to_datetime(df5[' Dates (YMD)']) +df5[' pm2.5 standard ug/m3'] = pd.to_numeric(df5[' pm2.5 standard ug/m3'], errors='coerce') +df5.set_index(' Dates (YMD)', inplace=True) +df5_resampled = df5.resample('20min').mean().reset_index() + +df6 = pd.read_csv('2024_03_21__14_48_19_pm25_simplest_plantower_pt3_CSV.csv') +df6 = df6[[' Dates (YMD)', ' pm2.5 standard ug/m3']] +df6[' Dates (YMD)'] = pd.to_datetime(df6[' Dates (YMD)']) +df6[' pm2.5 standard ug/m3'] = pd.to_numeric(df6[' pm2.5 standard ug/m3'], errors='coerce') +df6.set_index(' Dates (YMD)', inplace=True) +df6_resampled = df6.resample('20min').mean().reset_index() + +df7 = pd.read_csv('2024_03_21__14_48_19_pm25_simplest_plantower_pt4_CSV.csv') +df7 = df7[[' Dates (YMD)', ' pm2.5 standard ug/m3']] +df7[' Dates (YMD)'] = pd.to_datetime(df7[' Dates (YMD)']) +df7[' pm2.5 standard ug/m3'] = pd.to_numeric(df7[' pm2.5 standard ug/m3'], errors='coerce') +df7.set_index(' Dates (YMD)', inplace=True) +df7_resampled = df7.resample('20min').mean().reset_index() + +############################################### +df8 = pd.read_csv('2024_03_21__14_17_18_Sensirion_sps30_533D09A32681430C.csv') +df8 = df8[[' Dates (YMD)', 'Mass Concentration PM2.5 (µg/m³)']] +df8[' Dates (YMD)'] = pd.to_datetime(df8[' Dates (YMD)']) +df8['Mass Concentration PM2.5 (µg/m³)'] = pd.to_numeric(df8['Mass Concentration PM2.5 (µg/m³)'], errors='coerce') +df8.set_index(' Dates (YMD)', inplace=True) +df8_resampled = df8.resample('20min').mean().reset_index() + +df9 = pd.read_csv('2024_03_21__14_04_09_Sensirion_sps30_674C8CAB9A3123F6.csv') +df9 = df9[[' Dates (YMD)', 'Mass Concentration PM2.5 (µg/m³)']] +df9[' Dates (YMD)'] = pd.to_datetime(df9[' Dates (YMD)']) +df9['Mass Concentration PM2.5 (µg/m³)'] = pd.to_numeric(df9['Mass Concentration PM2.5 (µg/m³)'], errors='coerce') +df9.set_index(' Dates (YMD)', inplace=True) +df9_resampled = df9.resample('20min').mean().reset_index() + + + +# Plot the data +plt.figure(figsize=(10, 6)) + +plt.plot(df2[' Dates (YMD)'], df2['PM2.5 ug/m3'], + marker='x', linestyle='-.', label='Vaisala', markersize=.5, linewidth=4) + +plt.plot(df3_resampled[' Dates (YMD)'], df3_resampled['PM2.5 ug/m3'], + marker='o', linestyle='-', label='Alphasense 2', markersize=.5, linewidth=4) + +plt.plot(df1_resampled[' Dates (YMD)'], df1_resampled['PM2.5 ug/m3'], + marker='o', linestyle='-', label='Alphasense 1', markersize=.5, linewidth=4) + +plt.plot(df4_resampled[' Dates (YMD)'], df4_resampled[' pm2.5 standard ug/m3'], + marker='*', linestyle='--', label='Plantower 1', markersize=.5, linewidth=4) + +plt.plot(df5_resampled[' Dates (YMD)'], df5_resampled[' pm2.5 standard ug/m3'], + marker='*', linestyle='--', label='Plantower 2', markersize=.5, linewidth=4) + +plt.plot(df6_resampled[' Dates (YMD)'], df6_resampled[' pm2.5 standard ug/m3'], + marker='*', linestyle='--', label='Plantower 3', markersize=.5, linewidth=4) + +plt.plot(df7_resampled[' Dates (YMD)'], df7_resampled[' pm2.5 standard ug/m3'], + marker='*', linestyle='--', label='Plantower 4', markersize=.5, linewidth=4) + +plt.plot(df8_resampled[' Dates (YMD)'], df8_resampled['Mass Concentration PM2.5 (µg/m³)'], + marker='*', linestyle=':', label='Sensirion 1', markersize=.5, linewidth=4) + +plt.plot(df9_resampled[' Dates (YMD)'], df9_resampled['Mass Concentration PM2.5 (µg/m³)'], + marker='*', linestyle=':', label='Sensirion 2', markersize=.5, linewidth=4) + +plt.xlabel('Time (MM-DD HH)',fontsize=20) +plt.ylabel('PM2.5 ug/m3',fontsize=20) +plt.grid(True) +plt.xticks(rotation=45,fontsize=18) +plt.yticks(fontsize=18) +plt.legend(fontsize=18) +plt.tight_layout() +plt.show() diff --git a/README.md b/README.md new file mode 100644 index 0000000..2423a6f --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +## GitHub Created and Managed By: Kaleb Nails ## + +# Air Quality Sensor Repository +This repository contains code for different air quality sensors. Each directory represents a specific sensor model, and the code within each directory is tailored to that sensor. Authors listed are immediate authors to our club, of course a lot of this code is found from other online, and the are cited within the programs files themselves. + +To pip install the dependent libraries run: + ```bash + pip install -r requirements.txt +``` +Here is the link to the AIAA paper that explains this system: https://arc.aiaa.org/doi/10.2514/6.2024-4293 + +Also I have made some YouTube tutorials. They explain some simple things and issues you might run into. I will continue to update as the need arises. +https://youtube.com/playlist?list=PLwAqsBL94ygYDSl61qIoNLhPo3zRYZZli&si=xE07LIelMbSo7oQt + +## 📝 File Structure +```text +📦Sensor_Code + ┣ 📂Alphasense + ┃ ┣ 📄OPC_Simple_v2.py + ┃ ┣ 📄README.md + ┃ ┗ 📄start_Alphasense_loggers.sh + ┣ 📂PMS plantower + ┃ ┣ 📄.gitkeep + ┃ ┣ 📄README.md + ┃ ┣ 📄 pm25_simpletest.py + ┃ ┗ 📄start_Plantower_loggers.sh + ┣ 📂Post_Processing + ┃ ┣ 📄Post_Processing.py + ┃ ┗ 📄README.md + ┣ 📂Sensirion + ┃ ┣ 📄.gitkeep + ┃ ┣ 📄 README.md + ┃ ┣ 📄SPS30_Senirion_run.py + ┃ ┣ 📄exampleReadSPS30.py + ┃ ┣ 📄sps30.py + ┃ ┣ 📄start_Sensirion_loggers.sh + ┃ ┗ 📄stop_Sensirion_loggers.sh + ┣ 📂Viasala + ┃ ┗ 📄.gitkeep + ┣ 📄Alphasense_Start_Script.sh + ┣ 📄Sensor_Start_Script.sh + ┣ 📄gui_popup.py + ┣ 📄requirements.txt + ┣ 📄.gitnore + ┣ 📄sensor_UART_configs.json + ┗ 📄README.md +``` +# Sensor Models # + +## ALPHASENSE OPC +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/2488073d-5645-47d8-9b6e-21c8f2c8ef74) + +**Authors:** Kaleb Nails, Erik Liebergall, Marc Compere +**Created:** 10/6/2023 + +### Description +This directory contains code for the ALSPHASENSE air quality sensor. The main script, `OPC_Simple_v2.py`, interfaces with the sensor, reads data, and logs it to a CSV file. This is using the USB adaptor for the Alphasenses + +### Instructions +1. Connect the ALSPHASENSE sensor to your system. +2. Run the `pm25_SPS30_Senirion_Run.py` script with the appropriate device name as a command-line argument (default is `/dev/ttyACM0`). + ```bash + python3 pm25_SPS30_Senirion_Run.py /dev/ttyUSB0 + +The script will log air quality data to a CSV file with a timestamp. + + +## PMS plantower +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/e1553597-c06a-49dd-b361-26345a3ea2d6) + +**Authors:** Erik Liebergall, Leah Smith, Kaleb Nails, Marc Compere +**Created:** 10 Feb 2023 + +### Description +This directory contains code for the PMS plantower air quality sensor. The `pm25_simpletest.py` script interfaces with the sensor, reads data, and outputs air quality information to the console. + +### Instructions +1. Connect the PMS plantower sensor to your system. +2. Run the `pm25_simpletest.py` script with the appropriate device name as a command-line argument (default is `/dev/ttyUSB0`). + ```bash + python3 pm25_simpletest.py /dev/ttyUSB0 + +### Output +The script provides air quality measurements in both standard and environmental concentration units. Below is an example output: + +```plaintext +Concentration Units (Standard) +--------------------------------------- +PM 1.0: 10 PM 2.5: 15 PM 10: 20 + +Concentration Units (Environmental) +--------------------------------------- +PM 1.0: 8 PM 2.5: 12 PM 10: 16 +--------------------------------------- +Particles > 0.3um / 0.1L air: 1000 +Particles > 0.5um / 0.1L air: 800 +Particles > 1.0um / 0.1L air: 500 +Particles > 2.5um / 0.1L air: 200 +Particles > 5.0um / 0.1L air: 100 +Particles > 10um / 0.1L air: 50 +``` + +## Sensirion SPS30 +**Authors:** Erik Liebergall, Marc Compere, Kaleb Nails +**Created:** 13 Oct 2023 + +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/af3127f4-a29f-4fac-9898-43c5310545e8) + +### Description +This directory contains code for the Sensirion SPS30 air quality sensor. The `pm25_SPS30_Senirion_Run.py` script interfaces with the sensor, reads data, and logs air quality information to a CSV file. + +### Instructions +1. Connect the Sensirion SPS30 sensor to your system. +2. Run the `pm25_SPS30_Senirion_Run.py` script with the appropriate device name as a command-line argument (default is `/dev/ttyUSB0`). + ```bash + python3 pm25_SPS30_Senirion_Run.py /dev/ttyUSB0 +The script will continuously read data from the sensor and log it to a CSV file. + +# CSV File Format +Note these are subject to change are are up to date as off 1/26/2024 + +## AlphaSense CSV File Format +The script outputs air quality information to a CSV file. The CSV format includes the following columns: +- **Serial Number** +- **Date Label** +- **Dates (YMD)** +- **Bin 0 to 23** +- **Bin1 MToF to Bin7 MToF** +- **Sampling Period** +- **SFR** +- **Temperature C** +- **Relative humidity** +- **PM1 ug/m³** +- **PM2.5 ug/m³** +- **PM10 ug/m³** +- **#RejectGlitch** +- **#RejectLongTOF** +- **#RejectRatio** +- **#RejectOutOfRange** +- **Fan rev count** +- **Laser status** +- **Checksum** + +Each row in the CSV file represents a set of air quality measurements at a specific date and time, with corresponding values for each parameter. + +## PMS plantower CSV File Format +The script outputs air quality information to a CSV file. The CSV format includes the following columns: + +- **Date Label** +- **Dates (YMD)** +- **Sensor 1** +- **pm1.0 standard ug/m³** +- **pm2.5 standard ug/m³** +- **pm10.0 standard ug/m³** +- **pm1.0 env ug/m³** +- **pm2.5 env ug/m³** +- **pm10.0 env ug/m³** +- **particles 0.3um** +- **particles 0.5um** +- **particles 1.0um** +- **particles 2.5um** +- **particles 5.0um** +- **particles 10.0um** + +Each row in the CSV file represents a set of air quality measurements at a specific date and time, with corresponding values for each parameter. + +## SENSIRION CSV File Format ## +The script outputs air quality information to a CSV file. The CSV format includes the following columns: +- **Serial Number** +- **Date Label** +- **Dates (YMD)** +- **Mass Concentration PM1.0 (µg/m³)** +- **Mass Concentration PM2.5 (µg/m³)** +- **Mass Concentration PM4.0 (µg/m³)** +- **Mass Concentration PM10.0 (µg/m³)** +- **Number Concentration PM0.5 (#/cm³)** +- **Number Concentration PM1.0 (#/cm³)** +- **Number Concentration PM2.5 (#/cm³)** +- **Number Concentration PM4.0 (#/cm³)** +- **Number Concentration PM10.0 (#/cm³)** +- **Typical Particle Size [µm]** +Each row in the CSV file represents a set of air quality measurements at a specific date and time, with corresponding values for each parameter. + + + + + + + + diff --git a/Sensirion/README.md b/Sensirion/README.md new file mode 100644 index 0000000..f592b7b --- /dev/null +++ b/Sensirion/README.md @@ -0,0 +1,38 @@ +# Sensirion Sensor # + +## UART VS I2C ## +The sensor can record using both uart and I2C, for this we will be using uart + +## sps30.py ## +This is a file that comes from https://github.com/binh-bk/Sensirion_SPS30/blob/master/sps30.py and is used to collect sps30 data. + +## exampleReadSPS30.py ## +This is a simple examle on how to read the Sensirion sensor + +## Reference Links ## +Data-sheet: https://cdn.sparkfun.com/assets/4/e/e/f/8/Sensirion_PM_Sensors_Datasheet_SPS30.pdf + +https://github.com/binh-bk/Sensirion_SPS30/blob/master/sps30.py + +https://binh-bk.github.io/Sensirion_SPS30/ + +## Hardware Interface & Ports ## +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/1d0e7864-0a3f-4848-bdcf-2cc4c035eff0) + + +NOTE FOR OUR USB TO UART, white is the reciever & green is the transmitter. + + +also note: +USB - SENSOR: +red - red +black -black +white - purple +green -white +nothing - green + +HERE IS THE OUTPUT: + + + +![image](https://github.com/MOVEUAS/Sensor_Code/assets/117048000/bbf6a458-fb85-43ca-ae11-5ab87164ae53) diff --git a/Sensirion/SPS30_Senirion_Run.py b/Sensirion/SPS30_Senirion_Run.py new file mode 100644 index 0000000..a0d1370 --- /dev/null +++ b/Sensirion/SPS30_Senirion_Run.py @@ -0,0 +1,106 @@ +#Kaleb Nails +# Moral Support: Erik Liebergall +#10/11/2023 +#!/usr/bin/env python3 +# +# UART interface to Sparkfun SPS-30: sparkfun.com/products/15103 +# documented here: https://binh-bk.github.io/Sensirion_SPS30/ +# https://github.com/binh-bk/Sensirion_SPS30 +# +# output format (is probably) documented in 4.3 Measurement Output Formats +# at datasheet: https://cdn.sparkfun.com/assets/4/e/e/f/8/Sensirion_PM_Sensors_Datasheet_SPS30.pdf +# +# needs paho-mqtt: +# install with: pip3 install paho-mqtt +# +# Erik Liebergall +# Marc Compere +# Kaleb Nails +# created : 13 Oct 2023 + +from sps30 import get_usb +from sps30 import SPS30 +import time +from datetime import datetime +import os +import sys + + +device='/dev/ttyUSB0' +#device='/dev/ttyUSB1' + + + +if len(sys.argv)==1: + #print('sys.argv[0]={0}'.format(sys.argv[0])) + print('provide a device name to read, like:') + print(' python3 pm25_SPS30_Senirion_Run.py /dev/ttyUSB0' +"\n") + print('\033[91mWARNING: DEFAULT PORT WILL BE USB0\033[0m' +"\n") + print('this default setting was left for developement' + "\n" + "\n") + time.sleep(2.5) + + + +if len(sys.argv)>1: + print('using command line arg, and provided device!') + device=sys.argv[1] + +devName=os.path.basename(device) # get device name for logfile name +print('using devName=[{0}]'.format(devName)+"\n") + +p = SPS30(port=device, push_mqtt=False) + +SerialNumberStr = p.read_serial_number() +print(SerialNumberStr) + + + +#This exits one directory at a time so its on the local computer so you dont get csvs on your repository +os.chdir("..") +os.chdir("..") + +#This creates the file and the first row and and the labels +fname = '{0}_Sensirion_sps30_{1}.csv'.format(datetime.now().strftime("%Y_%m_%d__%H_%M_%S"), SerialNumberStr) +file = open(fname,'w') +titleStr = 'Serial Number,Date Label, Dates (YMD), Mass Concentration PM1.0 (µg/m³),Mass Concentration PM2.5 (µg/m³),Mass Concentration PM4.0 (µg/m³),Mass Concentration PM10.0 (µg/m³),Number Concentration PM0.5 (µg/m³),Number Concentration PM1.0 (#/cm³),Number Concentration PM2.5 (#/cm³),Number Concentration PM4.0(#/cm³),Number Concentration PM10.0 (#/cm³),Typical Particle Size [µm]' +file.write(titleStr +"\n") +file.flush() + + + +p.start() +#p.stop() + +#This sleep is very important +print(' \n LOADING... \n') +time.sleep(5) + + + +while True: + + #read values + out=p.read_values() + print(out) + #data_struct = {'Mass Concentration PM1.0 (µg/m³)',out[0],'Mass Concentration PM2.5 (µg/m³)',out[1],'Mass Concentration PM4.0 (µg/m³)',out[2],'Mass Concentration PM10.0 (µg/m³)',out[3],'Number Concentration PM0.5 (µg/m³)',out[4],'Number Concentration PM1.0 (µg/m³)',out[5],'Number Concentration PM2.5 (µg/m³)',out[6],'Number Concentration PM4.0(µg/m³)',out[7],'Number Concentration PM10.0 (µg/m³)',out[8],'Typical Particle Size [µm]',out[9]} + #dataStr = '{0}, {1}, {2}, {3}, {4},{5},{6},{7},{7},{8},{9}'.format(data_struct.get('Mass Concentration PM1.0 (µg/m³)',''),data_struct.get('Mass Concentration PM2.5 (µg/m³)',''),data_struct.get('Mass Concentration PM10.0 (µg/m³)',''),data_struct.get('Mass Concentration PM1.0 (µg/m³)',''), + dataStr = ', {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7},{8},{9}'.format(out[0],out[1],out[2],out[3],out[4],out[5],out[6],out[7],out[8],out[9]) + dateStr =', Date:, {0}'.format(datetime.now()) + + + print("\n" + SerialNumberStr + dateStr + dataStr + "\n") + file.write(SerialNumberStr + dateStr + dataStr + "\n") + file.flush() + time.sleep(1) + + +# docStr.append('Mass Concentration PM1.0 (µg/m³)') +# docStr.append('Mass Concentration PM2.5 (µg/m³)') +# docStr.append('Mass Concentration PM4.0 (µg/m³)') +# docStr.append('Mass Concentration PM10.0 (µg/m³)') +# docStr.append('Number Concentration PM0.5 [#/cm³]') +# docStr.append('Number Concentration PM1.0 [#/cm³]') +# docStr.append('Number Concentration PM2.5 [#/cm³]') +# docStr.append('Number Concentration PM4.0 [#/cm³]') +# docStr.append('Number Concentration PM10.0 [#/cm³]') +# docStr.append('Typical Particle Size [µm]') diff --git a/Sensirion/__pycache__/sps30.cpython-310.pyc b/Sensirion/__pycache__/sps30.cpython-310.pyc new file mode 100644 index 0000000..2bfb204 Binary files /dev/null and b/Sensirion/__pycache__/sps30.cpython-310.pyc differ diff --git a/Sensirion/exampleReadSPS30.py b/Sensirion/exampleReadSPS30.py new file mode 100644 index 0000000..81eb10c --- /dev/null +++ b/Sensirion/exampleReadSPS30.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# UART interface to Sparkfun SPS-30: sparkfun.com/products/15103 +# documented here: https://binh-bk.github.io/Sensirion_SPS30/ +# https://github.com/binh-bk/Sensirion_SPS30 +# +# output format (is probably) documented in 4.3 Measurement Output Formats +# at datasheet: https://cdn.sparkfun.com/assets/4/e/e/f/8/Sensirion_PM_Sensors_Datasheet_SPS30.pdf +# +# needs paho-mqtt: +# install with: pip3 install paho-mqtt +# +# Erik Liebergall, lieberge@my.erau.edu +# Marc Compere, comperem@erau.edu +# Kaleb Nails +# created : 02 Feb 2023 +# modified: 02 Feb 2023 + +from sps30 import get_usb +from sps30 import SPS30 +import time + +device='/dev/ttyUSB0' +#device='/dev/ttyUSB1' + +p = SPS30(port=device, push_mqtt=False) +print(p.read_serial_number()) + +docStr=[] +docStr.append('Mass Concentration PM1.0 (µg/m³)') +docStr.append('Mass Concentration PM2.5 (µg/m³)') +docStr.append('Mass Concentration PM4.0 (µg/m³)') +docStr.append('Mass Concentration PM10.0 (µg/m³)') +docStr.append('Number Concentration PM0.5 [#/cm³]') +docStr.append('Number Concentration PM1.0 [#/cm³]') +docStr.append('Number Concentration PM2.5 [#/cm³]') +docStr.append('Number Concentration PM4.0 [#/cm³]') +docStr.append('Number Concentration PM10.0 [#/cm³]') +docStr.append('Typical Particle Size [µm]') + +for i in range(len(docStr)): + print('{0}, '.format(docStr[i]), end='') + +p.start() +#p.stop() + +#This sleep is very important +print(' \n LOADING... \n') +time.sleep(5) +while True: + + out=p.read_values() + for i in range(len(out)): + print('{0}, '.format(out[i]), end='') + print('') + time.sleep(1) diff --git a/Sensirion/sps30.py b/Sensirion/sps30.py new file mode 100644 index 0000000..7fc16b9 --- /dev/null +++ b/Sensirion/sps30.py @@ -0,0 +1,235 @@ +""" +Binh Nguyen, June 14, 2020 +- A fork from Szymon Jakubiak (2018) +- additional feature: + 1. push data to MQTT server + 2. log file to CSV format (both of them are optional) + 3. run multiple SPS30 with USB hub (quality check SPS30 sensors) +""" + +import serial, struct, time +import subprocess +from operator import invert +import os +import json +import socket +import paho.mqtt.publish as publish #pip install paho-mqtt + +# set localtime +os.environ['TZ'] = 'Asia/Ho_Chi_Minh' +time.tzset() + +# MQTT host, users +mqtt = '192.168.1.100' # change this +topic = 'sensor/sps30' # and this +auth = {'username': 'mqtt_user', 'password': 'mqtt_password'} # and these two + + +def get_usb(): + ''' + - list all devices connected to USB port + - make sure no other devices rather SDS011 sensors connected + ''' + + + try: + with subprocess.Popen(['ls /dev/ttyUSB*'], shell=True, stdout=subprocess.PIPE) as f: + usbs = f.stdout.read().decode('utf-8') + usbs = usbs.split('\n') + usbs = [usb for usb in usbs if len(usb) > 3] + except Exception as e: + print('No USB available') + return usbs + +def time_(): return int(time.time()) + + +def datetime_(): + return time.strftime('%x %X', time.localtime()) + + +def host_folder(): + """designate a folder to save data""" + this_month_folder = time.strftime('%b%Y') + basedir = os.path.abspath(os.path.dirname(__file__)) + # basedir = '/'.join(basedir.split('/')[: -1]) # make one level up + all_dirs = [d for d in os.listdir(basedir) if os.path.isdir(d)] + if len(all_dirs) == 0 or this_month_folder not in all_dirs: + os.makedirs(this_month_folder) + print('created: {}'.format(this_month_folder)) + return os.path.join(basedir, this_month_folder) + + +def internet_ready(): + '''check if internet connection is ready''' + + try: + _ = socket.create_connection((mqtt, 1883), 3) + return True + except Exception as e: + print("Error {}".format(e)) + return False + + +def record_data(data): + '''save data to CSV file''' + id_ = data.split(',')[0] + id_ = f'{id_}' + filename = os.path.join(host_folder(), f'{id_}.csv') + with open(filename, 'a+') as f: + f.write(f'{data}\n') + print(data) + return None + + +def push_mqtt_server(data): + '''push data to MQTT server''' + + header = ['sensor','time','PM1','PM25','PM4','PM10','b0305', + 'b031','b0325','b034','b0310','tsize'] + data = data.split(',') + if len(header) == len(data): + print(f'Process for MQTT {data}') + payload = dict(zip(header,data)) + payload['type'] = 'json' + print(f'MQTT: {payload}') + payload = json.dumps(payload) + try: + if internet_ready(): + publish.single(topic, payload, hostname=mqtt, auth=auth) + + except Exception as e: + print('Error: {}'.format(e)) + pass + return None + + + +class SPS30: + NAME = 'SPS30' + WARMUP = 20 # seconds + + def __init__(self, port, save_data=True, push_mqtt=False, INTERVAL=60): + self.port = port + self.interval = INTERVAL + self.warmup = SPS30.WARMUP + self.save_data = save_data + self.push_mqtt = push_mqtt + self.name = SPS30.NAME + self.lastSample = 0 + self.fanOn = 0 + self.is_started = False + self.ser = serial.Serial(self.port, baudrate=115200, stopbits=1, parity="N", timeout=2) + + def __str__(self): + return f'{self.port}, {self.name}, {self.fanOn}, {self.lastSample}' + + + def start(self): + self.ser.write([0x7E, 0x00, 0x00, 0x02, 0x01, 0x03, 0xF9, 0x7E]) + + def stop(self): + self.ser.write([0x7E, 0x00, 0x01, 0x00, 0xFE, 0x7E]) + + def read_values(self): + self.ser.flushInput() + # Ask for data + self.ser.write([0x7E, 0x00, 0x03, 0x00, 0xFC, 0x7E]) + toRead = self.ser.inWaiting() + # Wait for full response + # (may be changed for looking for the stop byte 0x7E) + while toRead < 47: + + toRead = self.ser.inWaiting() + print(f'Wait: {toRead}') + time.sleep(1) + raw = self.ser.read(toRead) + + # Reverse byte-stuffing + if b'\x7D\x5E' in raw: + raw = raw.replace(b'\x7D\x5E', b'\x7E') + if b'\x7D\x5D' in raw: + raw = raw.replace(b'\x7D\x5D', b'\x7D') + if b'\x7D\x31' in raw: + raw = raw.replace(b'\x7D\x31', b'\x11') + if b'\x7D\x33' in raw: + raw = raw.replace(b'\x7D\x33', b'\x13') + + # Discard header and tail + rawData = raw[5:-2] + + try: + data = struct.unpack(">ffffffffff", rawData) + except struct.error: + data = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + return data + + def read_serial_number(self): + self.ser.flushInput() + self.ser.write([0x7E, 0x00, 0xD0, 0x01, 0x03, 0x2B, 0x7E]) + toRead = self.ser.inWaiting() + while toRead < 7: #24 + toRead = self.ser.inWaiting() + print(f'Wait: {toRead}') + time.sleep(1) + raw = self.ser.read(toRead) + + # Reverse byte-stuffing + if b'\x7D\x5E' in raw: + raw = raw.replace(b'\x7D\x5E', b'\x7E') + if b'\x7D\x5D' in raw: + raw = raw.replace(b'\x7D\x5D', b'\x7D') + if b'\x7D\x31' in raw: + raw = raw.replace(b'\x7D\x31', b'\x11') + if b'\x7D\x33' in raw: + raw = raw.replace(b'\x7D\x33', b'\x13') + + # Discard header, tail and decode + serial_number = raw[5:-3].decode('ascii') + return serial_number + + def run_query(self): + if time_() - self.lastSample >= self.interval: + if not self.is_started: + self.start() + self.fanOn = time_() + self.is_started = True + if self.name == SPS30.NAME: + name_ = self.read_serial_number() + if len(name_) >0: + self.name = f'SPS_{name_}' + if time_() - self.fanOn >= self.warmup: + output = self.read_values() + sensorData = "" + for val in output: + sensorData += "{0:.2f},".format(val) + + output = ','.join([self.name, datetime_(),sensorData[:-1]]) + self.lastSample = time_() + if self.is_started: + self.stop() + self.is_started = False + if self.save_data: + record_data(output) + if self.push_mqtt: + push_mqtt_server(output) + else: + time.sleep(1) + return None + + def close_port(self): + self.ser.close() + + +if __name__ == '__main__': + # s1 = SPS30(port='/dev/ttyUSB0', push_mqtt=True) + usbs = get_usb() + print(usbs) + process = list() + for port in usbs: + p = SPS30(port=port, push_mqtt=False) + process.append(p) + print('Starting') + while True: + for p in process: + p.run_query() diff --git a/Sensirion/start_Sensirion_loggers.sh b/Sensirion/start_Sensirion_loggers.sh new file mode 100644 index 0000000..04b0b8d --- /dev/null +++ b/Sensirion/start_Sensirion_loggers.sh @@ -0,0 +1,21 @@ +#!/bin/bash -x +# +# bash shell script to start multiple logger codes +# +# mdc +# created : 13 Dec 2023 +# modified: 13 Dec 2023 + +device1='/dev/ttyUSB0' +device2='/dev/ttyUSB1' + +DATE=`date` +echo "starting sensirion1 logger at $DATE" +screen -dm -S sen1 python3 SPS30_Senirion_Run.py $device1 + +echo "starting sensirion2 logger" +screen -dm -S sen2 python3 SPS30_Senirion_Run.py $device2 + + + + diff --git a/Sensirion/stop_Sensirion_loggers.sh b/Sensirion/stop_Sensirion_loggers.sh new file mode 100644 index 0000000..18a3fe7 --- /dev/null +++ b/Sensirion/stop_Sensirion_loggers.sh @@ -0,0 +1,15 @@ +#!/bin/bash -x +# +# bash shell script to stop specific logger codes +# +# mdc +# created : 13 Dec 2023 +# modified: 13 Dec 2023 + +# stop sensorion loggers +pkill -f "python3 SPS30" + + + + + diff --git a/Sensor_Start_Script.sh b/Sensor_Start_Script.sh new file mode 100755 index 0000000..8b83402 --- /dev/null +++ b/Sensor_Start_Script.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +alpha0_device="/dev/ttyACM0" +echo "Starting alpha0 logger" +screen -dm -S alpha0 python3 OPC_Simple_v2.py $alpha0_device + +alpha1_device="/dev/ttyACM1" +echo "Starting alpha1 logger" +screen -dm -S alpha1 python3 OPC_Simple_v2.py $alpha1_device + +alpha2_device="/dev/ttyACM2" +echo "Starting alpha2 logger" +screen -dm -S alpha2 python3 OPC_Simple_v2.py $alpha2_device + +sensirion0_device="/dev/ttyUSB0" +echo "Starting sensirion0 logger" +screen -dm -S sensirion0 python3 SPS30_Sensirion_Run.py $sensirion0_device + +PMS0_device="/dev/ttyUSB3" +echo "Starting PMS0 logger" +screen -dm -S PMS0 python3 pm25_simpletest.py $PMS0_device + +PMS1_device="/dev/ttyUSB4" +echo "Starting PMS1 logger" +screen -dm -S PMS1 python3 pm25_simpletest.py $PMS1_device + +PMS2_device="/dev/ttyUSB5" +echo "Starting PMS2 logger" +screen -dm -S PMS2 python3 pm25_simpletest.py $PMS2_device + +sensirion1_device="/dev/ttyUSB1" +echo "Starting sensirion1 logger" +screen -dm -S sensirion1 python3 SPS30_Sensirion_Run.py $sensirion1_device + +PMS0_device="/dev/ttyUSB3" +echo "Starting PMS0 logger" +screen -dm -S PMS0 python3 pm25_simpletest.py $PMS0_device + +PMS1_device="/dev/ttyUSB4" +echo "Starting PMS1 logger" +screen -dm -S PMS1 python3 pm25_simpletest.py $PMS1_device + +PMS2_device="/dev/ttyUSB5" +echo "Starting PMS2 logger" +screen -dm -S PMS2 python3 pm25_simpletest.py $PMS2_device + +sensirion2_device="/dev/ttyUSB2" +echo "Starting sensirion2 logger" +screen -dm -S sensirion2 python3 SPS30_Sensirion_Run.py $sensirion2_device + +PMS0_device="/dev/ttyUSB3" +echo "Starting PMS0 logger" +screen -dm -S PMS0 python3 pm25_simpletest.py $PMS0_device + +PMS1_device="/dev/ttyUSB4" +echo "Starting PMS1 logger" +screen -dm -S PMS1 python3 pm25_simpletest.py $PMS1_device + +PMS2_device="/dev/ttyUSB5" +echo "Starting PMS2 logger" +screen -dm -S PMS2 python3 pm25_simpletest.py $PMS2_device + diff --git a/Vialsala/README.md b/Vialsala/README.md new file mode 100644 index 0000000..4ea0c94 --- /dev/null +++ b/Vialsala/README.md @@ -0,0 +1,3 @@ +Note this has the google drive API in it which is not in the pip requirements, you can just get rid of the google drive upload stuff if you want. It should cause an issue. + +We only have one of these so I'm not going to change it on the github for now diff --git a/Vialsala/serial_Vaisala_AQT_420.py b/Vialsala/serial_Vaisala_AQT_420.py new file mode 100644 index 0000000..af27792 --- /dev/null +++ b/Vialsala/serial_Vaisala_AQT_420.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +# +# read Vaisala AQT-420 air quality and particulate matter sensor +# +# 'minicom -s' works with suggestions here: https://help.ubuntu.com/community/Minicom +# set HW flow control=OFF +# +# exit minicom: ctrl-A, then x +# +# what works: minicom with 115200/8N1, hw flow control=OFF +# then: date +# meas +# $ meas +# NO2 (ppm): 0.012 +# SO2 (ppm): 0.023 +# CO (ppm): 0.310 +# O3 (ppm): -0.002 +# PM2.5 (ug/m3): 0.7 +# PM10 (ug/m3): 1.9 +# TEMP (C): 21.5 +# HUM (%RH): 46.1 +# PRES (mbar): 1016.2 +# Uptime (s): 1802374 +# Validity: TRUE +# +# meas --csv +# 0.012,0.023,0.308,0.000,0.000,0.000,0.000,0.000,0.7,1.9,21.5,45.9,1016.3,1802444 +# +# +# +# ------------------------------------------------------------------------------ +# MODBUS example that has not worked because it's unclear how MODBUS on AQT-420 works over usb cable: +# from: https://medium.com/@peterfitch/modbus-and-rs485-a-python-test-rig-1b5014f709ec +# +# Marc Compere, comperem@erau.edu +# created : 04 Jan 2020 +# modified: 20 Apr 2023 +# modified by David Benning, Erik, Gabe, Leah, Kaleb: 20 Jan 2023 +#Thoughts and Prayers +#modified by Kaleb: 20 April 2023 + +# this got it started: +# ser.write('meas\r') +# while True: +# res=ser.read_until() +# print(res) + +import time # sleep() +from datetime import datetime # datetime.now() +import serial +import os +from pydrive.auth import GoogleAuth +from pydrive.drive import GoogleDrive +import socket + +gauth = GoogleAuth() +drive = GoogleDrive(gauth) +ser = serial.Serial('/dev/ttyUSB1', baudrate=115200, bytesize=8, parity='N', stopbits=1) + +ser.reset_input_buffer() # flush input buffer +ser.reset_output_buffer() # flush output buffer, more may be in usb adapter which is separate from linux buffer +ser.timeout=2 # (s) timeout for serial.read_until() + +class myDataClass: + NO2 = -1.0 # (ppm) + SO2 = -1.0 # (ppm) + CO = -1.0 # (ppm) + O3 = -1.0 # (ppm) + PM2pt5 = -1.0 # (ug/m3) + PM10 = -1.0 # (ug/m3) + TEMP = -1.0 # (C) + HUM = -1.0 # (%RH) + PRES = -1.0 # (mbar) + Uptime = -1.0 # (s) + Validity = -1.0 # (True/False) + +data = myDataClass() + +fname='{0}_vaisala_aqt420.csv'.format( datetime.now().strftime("%Y_%m_%d__%H_%M_%S") ) +folder = '1-ZUsOWRls8vGFxDhgCK5cudBqNecKI-d' +file1 = drive.CreateFile({'parents':[{'id':folder}],'title' : fname}) + +file=open(fname,'w') + +dt=10 # (sec) measurement request interval +cnt=0 + +print('Reading serial device: Vaisala AQT-420...') +print('logging to [{0}] every {1} seconds'.format(fname,dt)) +print('\nPress Crtl-C to quit...') + +while True: + #print('[{0}] --- requesting measurement ---'.format(datetime.now())) + nBytes = ser.write(b'meas\r') # write these bytes to the AQT-420, note: the \r does the trick (it's the return) + done=False # not done yet + while done==False: + res=ser.read_until() # returns byte arrays upto newline: + #print(res) + # b'NO2 (ppm): 0.011\r\n' + # b'SO2 (ppm): 0.020\r\n' + # b'CO (ppm): 0.248\r\n' + # b'O3 (ppm): 0.000\r\n' + # b'PM2.5 (ug/m3): 1.9\r\n' + # b'PM10 (ug/m3): 2.8\r\n' + # b'TEMP (C): 22.1\r\n' + # b'HUM (%RH): 43.6\r\n' + # b'PRES (mbar): 1015.6\r\n' + # b'Uptime (s): 1806282\r\n' + # b'Validity: TRUE\r\n' + # b'\r\n' + myStr=res.decode('utf-8') + if len(myStr)>5: + if myStr.find('NO2')>=0: # myStr='NO2 (ppm): 0.011\r\n' + (desc,val) = myStr.strip().split(':') + data.NO2 = float(val) + + if myStr.find('SO2')>=0: # myStr='SO2 (ppm): 0.020\r\n' + (desc,val) = myStr.strip().split(':') + data.SO2 = float(val) + + if myStr.find('CO')>=0: # myStr='CO (ppm): 0.248\r\n' + (desc,val) = myStr.strip().split(':') + data.CO = float(val) + + if myStr.find('O3')>=0: # myStr='O3 (ppm): 0.000\r\n' + (desc,val) = myStr.strip().split(':') + data.O3 = float(val) + + if myStr.find('PM2.5')>=0: # myStr='PM2.5 (ug/m3): 2.3\r\n' + (desc,val) = myStr.strip().split(':') + data.PM2pt5 = float(val) + + if myStr.find('PM10')>=0: # myStr='PM10 (ug/m3): 2.8\r\n' + (desc,val) = myStr.strip().split(':') + data.PM10 = float(val) + + if myStr.find('TEMP')>=0: # myStr='TEMP (C): 22.1\r\n' + (desc,val) = myStr.strip().split(':') + data.TEMP = float(val) + + if myStr.find('HUM')>=0: # myStr='HUM (%RH): 43.6\r\n' + (desc,val) = myStr.strip().split(':') + data.HUM = float(val) + + if myStr.find('PRES')>=0: # myStr='PRES (mbar): 1015.6\r\n' + (desc,val) = myStr.strip().split(':') + data.PRES = float(val) + + if myStr.find('Uptime')>=0: # myStr='Uptime (s): 1806282\r\n' + (desc,val) = myStr.strip().split(':') + data.Uptime = float(val) + + if myStr.find('Validity')>=0: # myStr='Validity: TRUE\r\n' + (desc,val) = myStr.strip().split(':') + data.Validity = bool(val) + done=True # exit this measurement string parsing loop + #print('done={0}'.format(done)) + + # print 1 complete line composed of all values just decoded + dateStr='{0}, cnt=,{1}, '.format(datetime.now(),cnt) + dataStr='NO2 (ppm),{0:>12.3f}, SO2 (ppm),{1:>12.3f}, CO (ppm),{2:>12.3f}, O3 (ppm),{3:>12.3f}, PM2.5 (ug/m3),{4:>12.3f}, PM10 (ug/m3),{5:>12.2f}, TEMP (C),{6:>8.2f}, HUM (%),{7:>8.1f}, PRES (mbar),{8:>10.2f}, Uptime (s),{9:>12.2f}, Validity,{10}' \ + .format(data.NO2,data.SO2,data.CO,data.O3,data.PM2pt5,data.PM10,data.TEMP,data.HUM,data.PRES,data.Uptime,data.Validity) + print(dateStr + dataStr) + + # just in case these formatting choices delete information, tack on the identical but raw floating point values as a long string at the end in the file only + file.write(dateStr + dataStr + ',raw values,' + ','.join([ str(elem) for elem in [data.NO2,data.SO2,data.CO,data.O3,data.PM2pt5,data.PM10,data.TEMP,data.HUM,data.PRES,data.Uptime,data.Validity] ]) + '\n' ) + file.flush() + + file1.SetContentFile(fname) + + try: + #Try to send data to bokeh table over sockets + ServerAddress = ('169.254.26.44',2222) + bufferSize = 1024 + #This is fine for now, change if there are multiple vaisalas + devName = 'Vaisala0' + UDPClient = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + Msg_to_bokeh_str ='"Sensors":"' + devName + '","NO2 (ppm)":"{0:>12.3f}", "SO2 (ppm)":"{1:>12.3f}", "CO (ppm)":"{2:>12.3f}", "O3 (ppm)":"{3:>12.3f}", "PM2.5 (ug/m3)":"{4:>12.3f}", "PM10 (ug/m3)":"{5:>12.2f}", "TEMP (C)":"{6:>8.2f}", "HUM (%)":"{7:>8.1f}", "PRES (mbar)":"{8:>10.2f}", "Uptime (s)":"{9:>12.2f}", "Validity":"{10}"'.format(data.NO2,data.SO2,data.CO,data.O3,data.PM2pt5,data.PM10,data.TEMP,data.HUM,data.PRES,data.Uptime,data.Validity) + Msg_to_bokeh_str = '{' + Msg_to_bokeh_str + '}' + Msg_to_bokeh_bytes = Msg_to_bokeh_str.encode('utf-8') + UDPClient.sendto(Msg_to_bokeh_bytes,ServerAddress) + + + + + except Exception as e: + print(e) + pass + + + try: + #upload to Google Drive + file1.Upload() + except: + print('No internet connection. Drive upload Failed') + pass + + + time.sleep(dt) + cnt=cnt+1 + + #file1.Upload() + diff --git a/gui_popup.py b/gui_popup.py new file mode 100755 index 0000000..a4bcc72 --- /dev/null +++ b/gui_popup.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +#Kaleb Nails +from tkinter import * +import json +import subprocess +import os +import sys +root = Tk() # create parent window +#Set the geometry +root.geometry("750x280") +root.title("MOVEUAS_WIZARD.exe") + +#screen -dm bash -c "./OPC_Simple_v2.py;exec sh" + + +#This will actually run the sensors based on the json given +def run_sensors(sensor_ports): + #subprocess.run("pkill screen", shell=True,text=True) + + + bash_script_content = '#!/bin/bash\n\n' + + # Loop through each sensor device + for i, device in enumerate(sensor_ports['alphasense'], start=0): + # Set the variable for each sensor + variable_name = f'alpha{i}_device' + device_port = sensor_ports['alphasense'][i] + bash_script_content += f'{variable_name}="{device_port}"\n' + + # Start the logger for each sensor + bash_script_content += f'echo "Starting alpha{i} logger"\n' + bash_script_content += f'screen -dm -S alpha{i} python3 OPC_Simple_v2.py ${variable_name}\n\n' + #print(bash_script_content) + + + + + # Loop through each sensor device + for i, device in enumerate(sensor_ports['sensirion'], start=0): + # Set the variable for each sensor + variable_name = f'sensirion{i}_device' + device_port = sensor_ports['sensirion'][i] + bash_script_content += f'{variable_name}="{device_port}"\n' + + # Start the logger for each sensor + bash_script_content += f'echo "Starting sensirion{i} logger"\n' + bash_script_content += f'screen -dm -S sensirion{i} python3 SPS30_Sensirion_Run.py ${variable_name}\n\n' + #print(bash_script_content) + + + # Loop through each sensor device + for i, device in enumerate(sensor_ports['PMS'], start=0): + # Set the variable for each sensor + variable_name = f'PMS{i}_device' + device_port = sensor_ports['PMS'][i] + bash_script_content += f'{variable_name}="{device_port}"\n' + + # Start the logger for each sensor + bash_script_content += f'echo "Starting PMS{i} logger"\n' + bash_script_content += f'screen -dm -S PMS{i} python3 pm25_simpletest.py ${variable_name}\n\n' + + + + print(bash_script_content) + + # Write the Bash script to a file + with open('Sensor_Start_Script.sh', 'w') as bash_script_file: + bash_script_file.write(bash_script_content) + + # Make the Bash script executable + subprocess.run(['chmod', '+x', 'Sensor_Start_Script.sh']) + + +# for i in sensor_ports['alphasense']: +# print(i) +# try: +# command = "sleep 15;./Alphasense/OPC_Simple_v2.py" +" " + i + ";exec sh" + #p3 = subprocess.run(["sudo", "screen", "-dm", "./OPC_Simple_v2.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # you can also put stderr to stdout + #p3 = subprocess.run(["screen", "-dm","bash","-c" , "sleep 15;./Alphasense/OPC_Simple_v2.py;exec sh"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # you can also put stderr to stdout + +# p3 = subprocess.run(["screen", "-dm","bash","-c" , command], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) # you can also put stderr to stdout + + + + + +# print('----') +# print('p3 = {}'.format(p3)) +# print('----') + +# except subprocess.CalledProcessError as err: +# print( "\n\ncaught an rsync() subprocess error: {0}".format(err) ) +# print( "exiting.") +# sys.exit(-1) + +# print('p.stdout = {}'.format(p3.stdout.decode('utf-8'))) + + #print(command) + + # Expand the tilde (~) to the full home directory path + #home_directory = os.path.expanduser('~') + + # Specify the relative path from the home directory + #relative_path = 'Sensor_Code/Alphasense' + + # Combine the home directory and the relative path + #full_path = os.path.join(home_directory, relative_path) + + #subprocess.call(['/bin/bash', '-x', 'ls']) + + # Now, you can use the full path as the current working directory + #subprocess.run(['lxterminal', '-e', 'python3', '-c', "print('hiiiiii')"], cwd=full_path, shell=False) + #subprocess.run(["lxterminal","-e","cd" "Sensor_Code","cd" "Alphasense","sudo","python3","print('hi')"]) + + + #subprocess.call(["lxterminal","-e","sudo","nano","/boot/config.txt"]) + #subprocess.call(["lxterminal","-e","bash","sleep","10","cd" "Sensor_Code","cd" "Alphasense"]) + #subprocess.run(["lxterminal", "-e", "bash", "-c", "cd Sensor_Code/Alphasense && sudo python3 -c 'print(\"hi\")'"]) + + + + + #This opens a new screen + #open_screen_command = f"screen -dmS alphasense{i.replace('/', '_')}" + ##open_screen_command = f"screen -S alphasense{i.replace('/', '_')}" + + #print( "OPEN_SCREEN_COMMAND: " + open_screen_command) + #subprocess.call(f"screen -dmS alphasense{i.replace('/', '_')}",shell=True) + + #subprocess.call(f"screen -dmS alphasense{i.replace('/', '_')}", shell=True) + #subprocess.call(f"screen -S alphasense{i.replace('/', '_')} -X stuff 'echo test_hello\\15'", shell=True) + #subprocess.call(f"screen -S alphasense{i.replace('/', '_')} -X stuff 'ls'$(echo -ne '\015')", shell=True) + + #subprocess.call(f"screen -S alphasense{i.replace('/', '_')} -X stuff 'python3 OPC_Simple_v2.py'$(echo -ne '\015')", shell=True) + #subprocess.run(f"screen -S alphasense_{i.replace('/', '_')} -X stuff 'python3 OPC_Simple_v2.py > /path/to/output.log 2>&1'", shell=True) + + + + #run_code_command = f"screen -r alphasense{i.replace('/', '_')}" + #subprocess.run(f"screen -r alphasense{i.replace('/', '_')}", shell=True,text=True) + #subprocess.run("echo 'hello'", shell=True,text=True) + + #subprocess.run(f"screen -d", shell=True,text=True) + + + + + #run_code_command = f"screen -r alphasense{i.replace('/', '_')} -X stuff 'python3 OPC_Simple_v2.py'$(echo -ne '\\015')" + + #subprocess.run("echo 'Hello World'") + + #subprocess.run("chmod +x OPC_Simple_v2.py.py") + #subprocess.run(run_code_command, shell=True,text=True) + #subprocess.run("echo 'Hello World'") + + + + + + + + + + + + + #This gives the relative and full path for the subprocess.call() later on + #relative_path = 'Sensor_Code/Alphasense' + #full_path = os.path.join(home_directory, relative_path) + + #subprocess.call(['lxterminal' , '-e', 'python3', command], cwd=full_path, shell = False) + + + + + + + + + +#THIS IS THE YES OPTION +def retrieve_old_data(): + + shown_text.config(text="USING OLD CONFIGURATION") + + #Open the Json file + with open ('sensors_UART_configs.json','r') as file: + sensor_ports = json.load(file) + + print( sensor_ports) + + run_sensors(sensor_ports) + + + + + + #FIGURE OUT WHAT THE HECK I AM DOING + #turn_on.config(text= "NO LONGER YES") + +#THIS IS THE NO OPTION +def setup_tty(): + turn_off.config(text="QUIT",command=root.quit) + shown_text.config(text="UNPLUG ALL SENSORS, when done hit next") + + + print("HELLO") + +# use Button and Label widgets to create a simple TV remote +turn_on = Button(root, text="YES",command = retrieve_old_data) +#turn_on.pack(side=BOTTOM) + +turn_off = Button(root, text="NO", command=setup_tty) + +INITIALIZING_TEXT_PROMPT = "HELLO, welcome to the MOVEUAS program." +INITIALIZING_TEXT_PROMPT += " Would you like to use previous sensor USB port setups?" +INITIALIZING_TEXT_PROMPT += " (If this is your first time setting up hit NO)" + +shown_text = Label(root, text=INITIALIZING_TEXT_PROMPT, wraplength=300, justify=LEFT) +shown_text.grid(column=1,row=0) + + +turn_on.grid(column=0,row=1,sticky='se') +turn_off.grid(column=2,row=1,sticky='sw') +root.grid_columnconfigure(1,weight=1) +root.grid_rowconfigure(1,weight=1) + + + + +root.mainloop() + + + #turn_on.config(text= "NO LONGER YES") +# +# def ask_question(question): +# root = tk.Tk() +# +# root.withdraw() +# +# answer = simpledialog.askstring("Question", question) +# +# root.destroy() +# +# return answer +# +# if __name__ == "__main__": +# questions = [ +# "Question 1: What is your name?", +# "Question 2: Where are you from?", +# "Question 3: What is your favorite color?", +# "Question 4: What is your age?", +# "Question 5: What is your hobby?" +# ] +# +# answers = [] +# for question in questions: +# answer = ask_question(question) +# answers.append(answer) +# +# print("Answers:") +# for i, answer in enumerate(answers): +# print(f"Question {i + 1}: {answer}") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b918f96 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Adafruit_Blinka==8.24.0 +adafruit_circuitpython_pm25==2.1.15 +board==1.0 +paho_mqtt==1.6.1 +py_opc_ng==0.0.5 +pyserial==3.5 +pyusbiss==0.2.2 diff --git a/sensors_UART_configs.json b/sensors_UART_configs.json new file mode 100644 index 0000000..c4b6537 --- /dev/null +++ b/sensors_UART_configs.json @@ -0,0 +1,5 @@ +{ + "alphasense": ["/dev/ttyACM0","/dev/ttyACM1","/dev/ttyACM2"], + "sensirion": ["/dev/ttyUSB0","/dev/ttyUSB1","/dev/ttyUSB2"], + "PMS": ["/dev/ttyUSB3","/dev/ttyUSB4","/dev/ttyUSB5"] +}