""" ____ _ ____ _ ____ _ ____ _ / ___| / \ | _ \ / \ / ___| ___ _ __ __| | | _ \ __ _| |_ __ _ \___ \ / _ \ | |_) | / _ \ \___ \ / _ \ '_ \ / _` | | | | |/ _` | __/ _` | ___) / ___ \| _ < / ___ \ ___) | __/ | | | (_| | | |_| | (_| | || (_| | |____/_/ \_\_| \_\/_/ \_\ |____/ \___|_| |_|\__,_| |____/ \__,_|\__\__,_| Main loop to gather data from sensor inside SQLite database: * NPM * Envea * I2C BME280 * Noise sensor and send it to AirCarto servers via SARA R4 HTTP post requests also send the timestamp (already stored inside the DB) ! /usr/bin/python3 /var/www/nebuleair_pro_4g/loop/SARA_send_data_v2.py ATTENTION: # This script is triggered every minutes by /var/www/nebuleair_pro_4g/master.py (as a service) CSV PAYLOAD (AirCarto Servers) Endpoint: data.nebuleair.fr /pro_4G/data.php?sensor_id={device_id}×tamp={rtc_module_time} ATTENTION : do not change order ! CSV size: 18 {PM1},{PM25},{PM10},{temp},{hum},{press},{avg_noise},{max_noise},{min_noise},{envea_no2},{envea_h2s},{envea_o3},{4g_signal_quality} 0 -> PM1 (μg/m3) 1 -> PM25 (μg/m3) 2 -> PM10 (μg/m3) 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, 13 -> PM 0.2μm to 0.5μm quantity (Nb/L) 14 -> PM 0.5μm to 1.0μm quantity (Nb/L) 15 -> PM 1.0μm to 2.5μm quantity (Nb/L) 16 -> PM 2.5μm to 5.0μm quantity (Nb/L) 17 -> PM 5.0μm to 10μm quantity (Nb/L) 18 -> NPM temp inside 19 -> NPM hum inside 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":"BME280_temperature","value":"28.47"}, {"value_type":"BME280_humidity","value":"28.47"}, {"value_type":"BME280_pressure","value":"28.47"}, {"value_type":"CAIRSENS_NO2","value":"54"}, {"value_type":"CAIRSENS_H2S","value":"54"}, {"value_type":"CAIRSENS_O3","value":"54"} ] } """ import board import json import serial import time import busio import re import os import traceback import sys import sqlite3 import RPi.GPIO as GPIO from threading import Thread # 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(f"System just booted ({uptime_seconds:.2f} seconds uptime), skipping execution.") sys.exit() #Payload CSV to be sent to data.nebuleair.fr payload_csv = [None] * 25 #Payload JSON to be sent to uSpot payload_json = { "nebuleairid": "XXX", "software_version": "ModuleAirV2-V1-042022", "sensordatavalues": [] # Empty list to start with } # SARA R4 UHTTPC profile IDs aircarto_profile_id = 0 uSpot_profile_id = 1 # database connection conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db") cursor = conn.cursor() def blink_led(pin, blink_count, delay=1): """ Blink an LED on a specified GPIO pin. Args: pin (int): GPIO pin number (BCM mode) to which the LED is connected. blink_count (int): Number of times the LED should blink. delay (float): Time in seconds for the LED to stay ON or OFF (default is 1 second). """ # GPIO setup GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) # Use BCM numbering GPIO.setup(pin, GPIO.OUT) # Set the specified pin as an output try: for _ in range(blink_count): GPIO.output(pin, GPIO.HIGH) # Turn the LED on #print(f"LED on GPIO {pin} is ON") time.sleep(delay) # Wait for the specified delay GPIO.output(pin, GPIO.LOW) # Turn the LED off #print(f"LED on GPIO {pin} is OFF") time.sleep(delay) # Wait for the specified delay finally: GPIO.cleanup(pin) # Clean up the specific pin to reset its state print(f"GPIO {pin} cleaned up") #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 send_aircarto = config.get('send_aircarto', True) #envoi sur AirCarto (data.nebuleair.fr) send_uSpot = config.get('send_uSpot', False) #envoi sur MicroSpot () 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 ) def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True): ''' Fonction très importante !!! Reads the complete response from a serial connection and waits for specific lines. ''' if wait_for_lines is None: wait_for_lines = [] # Default to an empty list if not provided 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 any target line decoded_response = response.decode('utf-8', errors='replace') for target_line in wait_for_lines: if target_line in decoded_response: if debug: print(f"[DEBUG] 🔎 Found target line: {target_line}") return decoded_response # Return response immediately if a target line is found elif time.time() > end_time: if debug: 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 if debug: print(f"[DEBUG] ⏱️ elapsed time: {total_elapsed_time:.2f}s. ⏱️") # Check if the elapsed time exceeded 10 seconds if total_elapsed_time > 10 and debug: print(f"[ALERT] 🚨 The operation took too long 🚨") print(f'[ALERT] ⚠️{total_elapsed_time:.2f}s⚠️') return response.decode('utf-8', errors='replace') # Return the full response if no target line is found try: ''' _ ___ ___ ____ | | / _ \ / _ \| _ \ | | | | | | | | | |_) | | |__| |_| | |_| | __/ |_____\___/ \___/|_| ''' print('
') print(response2) print("
") match = re.search(r'\+CSQ:\s*(\d+),', response2) if match: signal_quality = int(match.group(1)) payload_csv[12]=signal_quality time.sleep(0.1) # On vérifie si le signal n'est pas à 99 pour déconnexion # si c'est le cas on essaie de se reconnecter if signal_quality == 99: print('⚠️ATTENTION: Signal Quality indicates no signal (99)⚠️') print("TRY TO RECONNECT:") command = f'AT+COPS=1,2,"{selected_networkID}"\r' ser_sara.write(command.encode('utf-8')) responseReconnect = read_complete_response(ser_sara, timeout=20, end_of_response_timeout=20) print('') print(responseReconnect) print("
") print('🛑STOP LOOP🛑') print("') print(response_SARA_3) print("
") # si on recoit la réponse UHTTPCR 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:') print(response_SARA_9) print("
") ''' +UHTTPER: profile_id,error_class,error_code error_class 0 OK, no error 3 HTTP Protocol error class 10 Wrong HTTP API USAGE error_code (for error_class 3) 0 No error 11 Server connection error 73 Secure socket connect error ''' #Pas forcément un moyen de résoudre le soucis #print("resetting the URL (domain name):") #command = f'AT+UHTTP={aircarto_profile_id},1,"{url_nebuleair}"\r' #ser_sara.write(command.encode('utf-8')) #response_SARA_31 = read_complete_response(ser_sara) #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("Blink blue LED") led_thread = Thread(target=blink_led, args=(23, 5, 0.5)) led_thread.start() #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_lines=["OK"], debug=False) print('') print(response_SARA_4) print('
') #Si non ne recoit pas de réponse UHTTPCR #on a peut etre une ERROR de type "+CME ERROR: No connection to phone" else: print('No UUHTTPCR response') print("Blink red LED") # Run LED blinking in a separate thread led_thread = Thread(target=blink_led, args=(24, 5, 0.5)) led_thread.start() #Vérification de l'erreur print("Getting type of error") # Split the response into lines and search for "+CME ERROR:" lines2 = response_SARA_3.strip().splitlines() for line in lines2: if "+CME ERROR" in line: error_message = line.split("+CME ERROR:")[1].strip() print("*****") print('⚠️ATTENTION: CME ERROR⚠️') print(f"Error type: {error_message}") print("*****") # Handle "No connection to phone" error if error_message == "No connection to phone": print('📞Try reconnect to network📞') #IMPORTANT! # Reconnexion au réseau (AT+COPS) #command = f'AT+COPS=1,2,{selected_networkID}\r' command = f'AT+COPS=0\r' ser_sara.write(command.encode('utf-8')) responseReconnect = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=120, wait_for_lines=["OK", "+CME ERROR"], debug=True) print('') print(responseReconnect) print("
") # Handle "Operation not allowed" error if error_message == "Operation not allowed": print('❓Try Resetting the HTTP Profile❓') command = f'AT+UHTTP={aircarto_profile_id},1,"data.nebuleair.fr"\r' ser_sara.write(command.encode('utf-8')) responseResetHTTP_profile = read_complete_response(ser_sara, timeout=5, end_of_response_timeout=5, wait_for_lines=["OK", "+CME ERROR"], debug=True) print('') print(responseResetHTTP_profile) print("
") #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_lines=["OK"], debug=False) print(response_SARA_5) # Calculate and print the elapsed time elapsed_time = time.time() - start_time_script print(f"Elapsed time: {elapsed_time:.2f} seconds") print("