""" Main loop to gather data from sensor: * NPM * Envea * I2C BME280 * Noise sensor and send it to AirCarto servers via SARA R4 HTTP post requests CSV PAYLOAD (AirCarto Servers) Endpoint: data.nebuleair.fr /pro_4G/data.php?sensor_id={device_id} ATTENTION : do not change order ! {PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality} 0 -> PM1 1 -> PM25 2 -> PM10 3 -> temp 4 -> hum 5 -> press 6 -> avg_noise 7 -> max_noise 8 -> min_noise 9 -> envea_no2 10 -> envea_h2s 11 -> envea_o3 12 -> 4G signal quality JSON PAYLOAD (Micro-Spot Servers) Same as NebuleAir wifi Endpoint: api-prod.uspot.probesys.net nebuleair?token=2AFF6dQk68daFZ port 443 {"nebuleairid": "82D25549434", "software_version": "ModuleAirV2-V1-042022", "sensordatavalues": [ {"value_type":"NPM_P0","value":"1.54"}, {"value_type":"NPM_P1","value":"1.54"}, {"value_type":"NPM_P2","value":"1.54"}, {"value_type":"NPM_N1","value":"0.02"}, {"value_type":"NPM_N10","value":"0.02"}, {"value_type":"NPM_N25","value":"0.02"}, {"value_type":"MHZ16_CO2","value":"793.00"}, {"value_type":"SGP40_VOC","value":"29915.00"}, {"value_type":"samples","value":"134400"}, {"value_type":"min_micro","value":"137"}, {"value_type":"max_micro","value":"155030"}, {"value_type":"interval","value":"145000"}, {"value_type":"signal","value":"-80"}, {"value_type":"latitude","value":"43.2964"}, {"value_type":"longitude","value":"5.36978"}, {"value_type":"state_npm","value":"State: 00000000"}, {"value_type":"th_npm","value":"28.47 / 37.54"}, {"value_type":"CAIRSENS_NO2","value":"54"}, {"value_type":"CAIRSENS_H2S","value":"54"}, ] } """ import board import json import serial import time import busio import re import os import traceback import RPi.GPIO as GPIO from adafruit_bme280 import basic as adafruit_bme280 # Record the start time of the script start_time_script = time.time() # Check system uptime with open('/proc/uptime', 'r') as f: uptime_seconds = float(f.readline().split()[0]) # Skip execution if uptime is less than 2 minutes (120 seconds) if uptime_seconds < 120: print("System just booted, skipping execution.") exit() url_nebuleair="data.nebuleair.fr" payload_csv = [None] * 20 payload_json = { "nebuleairid": "82D25549434", "software_version": "ModuleAirV2-V1-042022", "sensordatavalues": [] # Empty list to start with } # Set up GPIO mode (for Blue LED: network status) GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) # Use Broadcom pin numbering GPIO.setup(23, GPIO.OUT) # Set GPIO23 as an output pin #get data from config def load_config(config_file): try: with open(config_file, 'r') as file: config_data = json.load(file) return config_data except Exception as e: print(f"Error loading config file: {e}") return {} #Fonction pour mettre à jour le JSON de configuration def update_json_key(file_path, key, value): """ Updates a specific key in a JSON file with a new value. :param file_path: Path to the JSON file. :param key: The key to update in the JSON file. :param value: The new value to assign to the key. """ try: # Load the existing data with open(file_path, "r") as file: data = json.load(file) # Check if the key exists in the JSON file if key in data: data[key] = value # Update the key with the new value else: print(f"Key '{key}' not found in the JSON file.") return # Write the updated data back to the file with open(file_path, "w") as file: json.dump(data, file, indent=2) # Use indent for pretty printing print(f"updating '{key}' to '{value}'.") except Exception as e: print(f"Error updating the JSON file: {e}") # Define the config file path config_file = '/var/www/nebuleair_pro_4g/config.json' # Load the configuration data config = load_config(config_file) baudrate = config.get('SaraR4_baudrate', 115200) #baudrate du sara R4 device_id = config.get('deviceID', '').upper() #device ID en maj need_to_log = config.get('loop_log', False) #inscription des logs bme_280_config = config.get('i2c_BME', False) #présence du BME280 i2C_sound_config = config.get('i2C_sound', False) #présence du capteur son send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr) send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot () envea_sondes = config.get('envea_sondes', []) connected_envea_sondes = [sonde for sonde in envea_sondes if sonde.get('connected', False)] selected_networkID = config.get('SARA_R4_neworkID', '') #update device id in the payload json payload_json["nebuleairid"] = device_id ser_sara = serial.Serial( port='/dev/ttyAMA2', baudrate=baudrate, #115200 ou 9600 parity=serial.PARITY_NONE, #PARITY_NONE, PARITY_EVEN or PARITY_ODD stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout = 2 ) ser_NPM = serial.Serial( port='/dev/ttyAMA5', baudrate=115200, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout = 1 ) serial_connections = {} # Sondes Envea if connected_envea_sondes: # Pour chacune des sondes for device in connected_envea_sondes: port = device.get('port', 'Unknown') name = device.get('name', 'Unknown') connected = device.get('connected', False) serial_connections[name] = serial.Serial( port=f'/dev/{port}', # Format the port string baudrate=9600, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, bytesize=serial.EIGHTBITS, timeout = 1 ) def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_line=None): response = bytearray() serial_connection.timeout = timeout end_time = time.time() + end_of_response_timeout start_time = time.time() while True: elapsed_time = time.time() - start_time # Time since function start if serial_connection.in_waiting > 0: data = serial_connection.read(serial_connection.in_waiting) response.extend(data) end_time = time.time() + end_of_response_timeout # Reset timeout on new data # Decode and check for the specific line if wait_for_line: decoded_response = response.decode('utf-8', errors='replace') if wait_for_line in decoded_response: print(f"[DEBUG] 🔎Found target line: {wait_for_line}") break elif time.time() > end_time: print(f"[DEBUG] Timeout reached. No more data received.") break time.sleep(0.1) # Short sleep to prevent busy waiting # Final response and debug output total_elapsed_time = time.time() - start_time print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️") return response.decode('utf-8', errors='replace') # Open and read the JSON file try: # Send the command to request data (e.g., data for 60 seconds) print('

START LOOP

') print("Getting NPM values") ser_NPM.write(b'\x81\x12\x6D') # Read the response byte_data = ser_NPM.readline() #if npm is disconnected byte_data is empty # Extract the state byte and PM data from the response state_byte = int.from_bytes(byte_data[2:3], byteorder='big') state_bits = [int(bit) for bit in bin(state_byte)[2:].zfill(8)] PM1 = int.from_bytes(byte_data[9:11], byteorder='big') / 10 PM25 = int.from_bytes(byte_data[11:13], byteorder='big') / 10 PM10 = int.from_bytes(byte_data[13:15], byteorder='big') / 10 #Add data to payload CSV payload_csv[0] = PM1 payload_csv[1] = PM25 payload_csv[2] = PM10 #Add data to payload JSON payload_json["sensordatavalues"].append({"value_type": "NPM_P0", "value": str(PM1)}) payload_json["sensordatavalues"].append({"value_type": "NPM_P1", "value": str(PM10)}) payload_json["sensordatavalues"].append({"value_type": "NPM_P2", "value": str(PM25)}) # Sonde BME280 connected if bme_280_config: print("Getting BME280 values") #on récupère les infos du BME280 et on les ajoute au payload_csv i2c = busio.I2C(board.SCL, board.SDA) bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x76) bme280.sea_level_pressure = 1013.25 # Update this value for your location payload_csv[3] = round(bme280.temperature, 2) payload_csv[4] = round(bme280.humidity, 2) payload_csv[5] = round(bme280.pressure, 2) # Sonde Bruit connected if i2C_sound_config: #on récupère les infos de sound_metermoving et on les ajoute au message file_path_data_noise = "/var/www/nebuleair_pro_4g/sound_meter/moving_avg_minute.txt" # Read the file and extract the numbers try: with open(file_path_data_noise, "r") as file: content = file.read().strip() avg_noise, max_noise, min_noise = map(int, content.split()) # Append the variables to the payload_csv payload_csv[6] = avg_noise payload_csv[7] = max_noise payload_csv[8] = min_noise except FileNotFoundError: print(f"Error: File {file_path} not found.") except ValueError: print("Error: File content is not valid numbers.") # Sondes Envea if connected_envea_sondes: # Pour chacune des sondes for device in connected_envea_sondes: port = device.get('port', 'Unknown') name = device.get('name', 'Unknown') coefficient = device.get('coefficient', 'Unknown') print(f"Connected envea Sonde: {name} on port {port} and coefficient {coefficient} ") if name in serial_connections: serial_connection = serial_connections[name] try: # Write data to the device serial_connection.write( b"\xFF\x02\x13\x30\x01\x02\x03\x04\x05\x06\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x12\xAF\x88\x03" ) # Read data from the device data_envea = serial_connection.readline() if len(data_envea) >= 20: byte_20 = data_envea[19] byte_20 = byte_20 * coefficient # Update payload CSV based on device type if name == "h2s": payload_csv[10] = byte_20 if name == "no2": payload_csv[9] = byte_20 if name == "o3": payload_csv[11] = byte_20 print(f"Data from envea {name}: {byte_20}") else: print(f"Données reçues insuffisantes pour {name} pour extraire le 20ème octet.") except serial.SerialException as e: print(f"Error communicating with {name}: {e}") else: print(f"No serial connection for {name}") # Getting the LTE Signal print("-> Getting LTE signal <-") ser_sara.write(b'AT+CSQ\r') response2 = read_complete_response(ser_sara, wait_for_line="OK") print('

') print(response2) print("

") match = re.search(r'\+CSQ:\s*(\d+),', response2) if match: signal_quality = match.group(1) print("Signal Quality:", signal_quality) payload_csv[12]=signal_quality time.sleep(1) #print(payload_json) ''' SEND TO AIRCARTO ''' # Write Data to saraR4 # 1. Open sensordata_csv.json (with correct data size) csv_string = ','.join(str(value) if value is not None else '' for value in payload_csv) size_of_string = len(csv_string) print("Open JSON:") command = f'AT+UDWNFILE="sensordata_csv.json",{size_of_string}\r' ser_sara.write(command.encode('utf-8')) response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">") print(response_SARA_1) time.sleep(1) #2. Write to shell print("Write data to memory:") ser_sara.write(csv_string.encode()) response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK") print(response_SARA_2) #3. Send to endpoint (with device ID) print("Send data (POST REQUEST):") command= f'AT+UHTTPC=0,4,"/pro_4G/data.php?sensor_id={device_id}","server_response.txt","sensordata_csv.json",4\r' ser_sara.write(command.encode('utf-8')) response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=45, wait_for_line="+UUHTTPCR") print('

') print(response_SARA_3) print("

") # Wait for the +UUHTTPCR response #print("Waiting for +UUHTTPCR response...") #response_received = False #while not response_received: # response_SARA_3 = read_complete_response(ser_sara, timeout=5) # print(response_SARA_3.strip()) # if "+UUHTTPCR" in response_SARA_3: # response_received = True if "+UUHTTPCR" in response_SARA_3: print("✅ Received +UUHTTPCR response.") # Les types de réponse # 1.La commande n'a pas fonctionné # +CME ERROR: No connection to phone # +CME ERROR: Operation not allowed # 2.La commande fonctionne: elle renvoie un code # +UUHTTPCR: ,, # : 1 pour sucess et 0 pour fail # +UUHTTPCR: 0,4,1 -> OK # +UUHTTPCR: 0,4,0 -> error # Split response into lines lines = response_SARA_3.strip().splitlines() # 1.Vérifier si la réponse contient un message d'erreur CME if "+CME ERROR" in lines[-1]: print("*****") print('ATTENTION: CME ERROR') print("error:", lines[-1]) print("*****") #update status update_json_key(config_file, "SARA_R4_network_status", "disconnected") # Gestion de l'erreur spécifique if "No connection to phone" in lines[-1]: print("No connection to the phone. Retrying or reset may be required.") # Actions spécifiques pour ce type d'erreur (par exemple, réinitialiser ou tenter de reconnecter) # need to reconnect to network # and reset HTTP profile (AT+UHTTP=0) -> ne fonctionne pas.. # tester un reset avec CFUN 15 # 1.Reconnexion au réseau (AT+COPS) command = f'AT+COPS=1,2,"{selected_networkID}"\r' ser_sara.write(command.encode('utf-8')) responseReconnect = read_complete_response(ser_sara) print("Response reconnect:") print(responseReconnect) print("End response reconnect") elif "Operation not allowed" in lines[-1]: print("Operation not allowed. This may require a different configuration.") # Actions spécifiques pour ce type d'erreur # Clignotement LED en cas d'erreur GPIO.output(23, GPIO.LOW) # Éteindre la LED définitivement for _ in range(4): GPIO.output(23, GPIO.HIGH) # Allumer la LED time.sleep(0.1) GPIO.output(23, GPIO.LOW) # Éteindre la LED time.sleep(0.1) GPIO.output(23, GPIO.LOW) # Turn off the LED else: # 2.Si la réponse contient une réponse HTTP valide # Extract HTTP response code from the last line # ATTENTION: lines[-1] renvoie l'avant dernière ligne et il peut y avoir un soucis avec le OK # rechercher plutot http_response = lines[-1] # "+UUHTTPCR: 0,4,0" parts = http_response.split(',') # 2.1 code 0 (HTTP failed) if len(parts) == 3 and parts[-1] == '0': # The third value indicates success print("*****") print('ATTENTION: HTTP operation failed') update_json_key(config_file, "SARA_R4_network_status", "disconnected") print("*****") print("resetting the URL (domain name):") print("Turning off the blue LED...") for _ in range(4): # Faire clignoter 4 fois GPIO.output(23, GPIO.HIGH) # Allumer la LED time.sleep(0.1) # Attendre 100 ms GPIO.output(23, GPIO.LOW) # Éteindre la LED time.sleep(0.1) # Attendre 100 ms GPIO.output(23, GPIO.LOW) # Turn off the LED command = f'AT+UHTTP=0,1,"{url_nebuleair}"\r' ser_sara.write(command.encode('utf-8')) response_SARA_31 = read_complete_response(ser_sara) if need_to_log: print(response_SARA_31) # 2.2 code 1 (HHTP succeded) else: # Si la commande HTTP a réussi print('HTTP operation successful.') update_json_key(config_file, "SARA_R4_network_status", "connected") print("Turning on the blue LED...") for _ in range(4): # Faire clignoter 4 fois GPIO.output(23, GPIO.HIGH) # Allumer la LED time.sleep(0.1) # Attendre 100 ms GPIO.output(23, GPIO.LOW) # Éteindre la LED time.sleep(0.1) # Attendre 100 ms GPIO.output(23, GPIO.HIGH) # Turn on the LED #4. Read reply from server print("Reply from server:") ser_sara.write(b'AT+URDFILE="server_response.txt"\r') response_SARA_4 = read_complete_response(ser_sara, wait_for_line="OK") print('

') print(response_SARA_4) print('

') else: print('No UUHTTPCR response') #5. empty json print("Empty SARA memory:") ser_sara.write(b'AT+UDELFILE="sensordata_csv.json"\r') response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK") print(response_SARA_5) ''' SEND TO MICRO SPOT ''' if send_uSpot: print(">>>>>>>>") print(">>>>>>>>") print(">>>>>>>>") print("SEND TO MICRO SPOT (HTTP):") profile_id = 1 #step 4: set url (op_code = 1) print("****") print("SET URL") command = f'AT+UHTTP={profile_id},1,"api-prod.uspot.probesys.net"\r' ser_sara.write((command + '\r').encode('utf-8')) response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK") print(response_SARA_5) time.sleep(1) #step 4: set url to SSL (op_code = 6) (http_secure = 1 for HTTPS)(USECMNG_PROFILE = 2) print("****") print("SET SSL") command = f'AT+UHTTP={profile_id},6,0\r' ser_sara.write(command.encode('utf-8')) response_SARA_5 = read_complete_response(ser_sara, wait_for_line="OK") print(response_SARA_5) time.sleep(1) #step 4: set PORT (op_code = 5) print("****") print("SET PORT") command = f'AT+UHTTP={profile_id},5,81\r' ser_sara.write((command + '\r').encode('utf-8')) response_SARA_55 = read_complete_response(ser_sara, wait_for_line="OK") print(response_SARA_55) time.sleep(1) # Write Data to saraR4 # 1. Open sensordata_json.json (with correct data size) print("Open JSON:") payload_string = json.dumps(payload_json) # Convert dict to JSON string size_of_string = len(payload_string) command = f'AT+UDWNFILE="sensordata_json.json",{size_of_string}\r' ser_sara.write((command + '\r').encode('utf-8')) response_SARA_1 = read_complete_response(ser_sara, wait_for_line=">") print(response_SARA_1) time.sleep(1) #2. Write to shell print("Write to memory:") ser_sara.write(payload_string.encode()) response_SARA_2 = read_complete_response(ser_sara, wait_for_line="OK") print(response_SARA_2) #step 4: trigger the request (http_command=1 for GET and http_command=1 for POST) print("****") print("Trigger POST REQUEST") command = f'AT+UHTTPC={profile_id},4,"/nebuleair?token=2AFF6dQk68daFZ","http.resp","sensordata_json.json",4\r' ser_sara.write(command.encode('utf-8')) response_SARA_3 = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=30, wait_for_line="+UUHTTPCR") print('

') print(response_SARA_3) print("

") #READ REPLY print("****") print("Read reply from server") ser_sara.write(b'AT+URDFILE="http.resp"\r') response_SARA_7 = read_complete_response(ser_sara, wait_for_line="OK") print('

') print(response_SARA_7) print('

') #5. empty json print("Empty SARA memory:") ser_sara.write(b'AT+UDELFILE="sensordata_json.json"\r') response_SARA_8 = read_complete_response(ser_sara, wait_for_line="OK") print(response_SARA_8) # Calculate and print the elapsed time elapsed_time = time.time() - start_time_script if need_to_log: print(f"Elapsed time: {elapsed_time:.2f} seconds") print("
") except Exception as e: print("An error occurred:", e) traceback.print_exc() # This prints the full traceback