diff --git a/loop/SARA_send_data_v2.py b/loop/SARA_send_data_v2.py index 83696c8..0b50fc5 100755 --- a/loop/SARA_send_data_v2.py +++ b/loop/SARA_send_data_v2.py @@ -120,6 +120,7 @@ import traceback import threading import sys import sqlite3 +import struct import RPi.GPIO as GPIO from threading import Thread from datetime import datetime @@ -138,8 +139,6 @@ if uptime_seconds < 120: #Payload CSV to be sent to data.nebuleair.fr payload_csv = [None] * 30 -#Payload UPD to be sent to miotiq -payload_udp = [None] * 30 #Payload JSON to be sent to uSpot payload_json = { @@ -240,7 +239,6 @@ NOISE_sensor = config.get('NOISE', False) #update device id in the payload json payload_json["nebuleairid"] = device_id -payload_udp[0] = device_id # Skip execution if modem_config_mode is true if modem_config_mode: @@ -256,6 +254,115 @@ ser_sara = serial.Serial( timeout = 2 ) +class SensorPayload: + """ + Class to manage a fixed 100-byte sensor payload + All positions are predefined, no CSV intermediary + """ + + def __init__(self, device_id): + # Initialize 100-byte array with 0xFF (no data marker) + self.payload = bytearray(100) + for i in range(100): + self.payload[i] = 0xFF + + # Set device ID (bytes 0-7) + device_id_bytes = device_id.encode('ascii')[:8].ljust(8, b'\x00') + #device_id_bytes = bytes.fromhex(device_id)[:8].ljust(8, b'\x00') + + self.payload[0:8] = device_id_bytes + + # Set protocol version (byte 9) + self.payload[9] = 0x01 + + def set_signal_quality(self, value): + """Set 4G signal quality (byte 8)""" + if value is not None: + self.payload[8] = min(value, 255) + + def set_npm_core(self, pm1, pm25, pm10): + """Set NPM core values (bytes 10-15)""" + if pm1 is not None: + self.payload[10:12] = struct.pack('>H', int(pm1 * 10)) + if pm25 is not None: + self.payload[12:14] = struct.pack('>H', int(pm25 * 10)) + if pm10 is not None: + self.payload[14:16] = struct.pack('>H', int(pm10 * 10)) + + def set_bme280(self, temperature, humidity, pressure): + """Set BME280 values (bytes 16-21)""" + if temperature is not None: + self.payload[16:18] = struct.pack('>h', int(temperature * 10)) # Signed + if humidity is not None: + self.payload[18:20] = struct.pack('>H', int(humidity * 10)) + if pressure is not None: + self.payload[20:22] = struct.pack('>H', int(pressure)) + + def set_noise(self, avg_noise, max_noise=None, min_noise=None): + """Set noise values (bytes 22-27)""" + if avg_noise is not None: + self.payload[22:24] = struct.pack('>H', int(avg_noise * 10)) + if max_noise is not None: + self.payload[24:26] = struct.pack('>H', int(max_noise * 10)) + if min_noise is not None: + self.payload[26:28] = struct.pack('>H', int(min_noise * 10)) + + def set_envea(self, no2, h2s, nh3, co, o3): + """Set ENVEA gas sensor values (bytes 28-37)""" + if no2 is not None: + self.payload[28:30] = struct.pack('>H', int(no2)) + if h2s is not None: + self.payload[30:32] = struct.pack('>H', int(h2s)) + if nh3 is not None: + self.payload[32:34] = struct.pack('>H', int(nh3)) + if co is not None: + self.payload[34:36] = struct.pack('>H', int(co)) + if o3 is not None: + self.payload[36:38] = struct.pack('>H', int(o3)) + + def set_npm_5channels(self, ch1, ch2, ch3, ch4, ch5): + """Set NPM 5 channel values (bytes 38-47)""" + channels = [ch1, ch2, ch3, ch4, ch5] + for i, value in enumerate(channels): + if value is not None: + self.payload[38 + i*2:40 + i*2] = struct.pack('>H', int(value)) + + def set_npm_internal(self, temperature, humidity): + """Set NPM internal temp/humidity (bytes 48-51)""" + if temperature is not None: + self.payload[48:50] = struct.pack('>h', int(temperature * 10)) # Signed + if humidity is not None: + self.payload[50:52] = struct.pack('>H', int(humidity * 10)) + + def set_mppt(self, battery_voltage, battery_current, solar_voltage, solar_power, charger_status): + """Set MPPT charger values (bytes 52-61)""" + if battery_voltage is not None: + self.payload[52:54] = struct.pack('>H', int(battery_voltage * 10)) + if battery_current is not None: + self.payload[54:56] = struct.pack('>h', int(battery_current * 10)) # Signed + if solar_voltage is not None: + self.payload[56:58] = struct.pack('>H', int(solar_voltage * 10)) + if solar_power is not None: + self.payload[58:60] = struct.pack('>H', int(solar_power)) + if charger_status is not None: + self.payload[60:62] = struct.pack('>H', int(charger_status)) + + def set_wind(self, speed, direction): + """Set wind meter values (bytes 62-65)""" + if speed is not None: + self.payload[62:64] = struct.pack('>H', int(speed * 10)) + if direction is not None: + self.payload[64:66] = struct.pack('>H', int(direction)) + + def get_bytes(self): + """Get the complete 100-byte payload""" + return bytes(self.payload) + + def get_base64(self): + """Get base64 encoded payload for transmission""" + import base64 + return base64.b64encode(self.payload).decode('ascii') + def read_complete_response(serial_connection, timeout=2, end_of_response_timeout=2, wait_for_lines=None, debug=True): ''' Fonction très importante !!! @@ -608,6 +715,12 @@ try: ''' print('

START LOOP

') + #payload = SensorPayload(device_id) + payload = SensorPayload("484AE134") + print("deviceID (ASCII):") + print(payload.get_bytes()[:8].hex()) + + #print(f'Modem version: {modem_version}') #Local timestamp @@ -655,12 +768,18 @@ try: num_columns = len(data_values[0]) averages = [round(sum(col) / len(col),1) for col in zip(*data_values)] + PM1 = averages[0] PM25 = averages[1] PM10 = averages[2] npm_temp = averages[3] npm_hum = averages[4] + print(f"PM1: {PM1}") + print(f"PM2.5: {PM25}") + print(f"PM10: {PM10}") + + #Add data to payload CSV payload_csv[0] = PM1 payload_csv[1] = PM25 @@ -669,9 +788,8 @@ try: payload_csv[19] = npm_hum #add data to payload UDP - payload_udp[2] = PM1 - payload_udp[3] = PM25 - payload_udp[4] = PM10 + payload.set_npm_core(PM1, PM25, PM10) + payload.set_npm_internal(npm_temp, npm_hum) #Add data to payload JSON payload_json["sensordatavalues"].append({"value_type": "NPM_P0", "value": str(PM1)}) @@ -714,9 +832,11 @@ try: payload_csv[5] = BME280_pressure #Add data to payload UDP - payload_udp[5] = BME280_temperature - payload_udp[6] = BME280_humidity - payload_udp[7] = BME280_pressure + payload.set_bme280( + temperature=last_row[1], + humidity=last_row[2], + pressure=last_row[3] + ) #Add data to payload JSON payload_json["sensordatavalues"].append({"value_type": "BME280_temperature", "value": str(BME280_temperature)}) @@ -749,6 +869,15 @@ try: payload_csv[27] = averages[3] # envea_CO payload_csv[28] = averages[4] # envea_O3 + #Add data to payload UDP + payload.set_envea( + no2=averages[0], + h2s=averages[1], + nh3=averages[2], + co=averages[3], + o3=averages[4] + ) + #Add data to payload JSON payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_NO2", "value": str(averages[0])}) payload_json["sensordatavalues"].append({"value_type": "CAIRSENS_H2S", "value": str(averages[1])}) @@ -768,6 +897,12 @@ try: payload_csv[25] = wind_speed payload_csv[26] = wind_direction + #Add data to payload UDP + payload.set_wind( + speed=last_row[1], + direction=last_row[2] + ) + else: print("No data available in the database.") @@ -791,6 +926,15 @@ try: payload_csv[22] = solar_voltage payload_csv[23] = solar_power payload_csv[24] = charger_status + + #Add data to payload UDP + payload.set_mppt( + battery_voltage=last_row[1], + battery_current=last_row[2], + solar_voltage=last_row[3], + solar_power=last_row[4], + charger_status=last_row[5] + ) else: print("No data available in the database.") @@ -805,7 +949,14 @@ try: DB_A_value = last_row[2] #Add data to payload CSV - payload_csv[6] = DB_A_value + payload_csv[6] = DB_A_value + + #Add data to payload UDP + payload.set_noise( + avg_noise=last_row[2], # DB_A_value + max_noise=None, # Add if available + min_noise=None # Add if available + ) #print("Verify SARA connection (AT)") @@ -879,6 +1030,8 @@ try: if match: signal_quality = int(match.group(1)) payload_csv[12]=signal_quality + payload.set_signal_quality(signal_quality) + time.sleep(0.1) # On vérifie si le signal n'est pas à 99 pour déconnexion @@ -917,9 +1070,14 @@ try: if send_miotiq: print('

➡️SEND TO MIOTIQ

', end="") + binary_data = payload.get_bytes() + + print(f"Binary payload: {len(binary_data)} bytes") + + #create UDP socket (will return socket number) -> 17 is UDP protocol and 6 is TCP protocol # IF ERROR -> need to create the PDP connection - print("Create Socket:") + print("Create Socket:", end="") command = f'AT+USOCR=17\r' ser_sara.write(command.encode('utf-8')) response_SARA_1 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False) @@ -927,7 +1085,7 @@ try: print(response_SARA_1) print("

", end="") - if "+CME ERROR" in response_SARA_1: + if "+CME ERROR" in response_SARA_1 or "ERROR" in response_SARA_1: print('⚠️ATTENTION: need to reset PDP connection⚠️') psd_csd_resets = reset_PSD_CSD_connection() if psd_csd_resets: @@ -945,7 +1103,7 @@ try: print("Failed to extract socket ID") #Connect to UDP server (USOCO) - print("Connect to server:") + print("Connect to server:", end="") command = f'AT+USOCO={socket_id},"192.168.0.20",4242\r' ser_sara.write(command.encode('utf-8')) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False) @@ -953,37 +1111,34 @@ try: print(response_SARA_2) print("

", end="") - #prepare data - csv_udp_string = ','.join(str(value) if value is not None else '' for value in payload_udp) - size_of_udp_string = len(csv_udp_string) + # Write data and send - # 4. Write data and send (USOWR) - print("Write data:") - print(csv_udp_string) - command = f'AT+USOWR={socket_id},{size_of_udp_string}\r' + print(f"Write data: {len(binary_data)} bytes") + command = f'AT+USOWR={socket_id},{len(binary_data)}\r' ser_sara.write(command.encode('utf-8')) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False) print('

') print(response_SARA_2) print("

", end="") - ser_sara.write(csv_udp_string.encode()) + # Send the raw payload bytes (already prepared) + ser_sara.write(binary_data) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False) print('

') print(response_SARA_2) print("

", end="") #Read reply from server (USORD) - print("Read reply:") - command = f'AT+USORD=0,100\r' - ser_sara.write(command.encode('utf-8')) - response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False) - print('

') - print(response_SARA_2) - print("

", end="") + #print("Read reply:", end="") + #command = f'AT+USORD=0,100\r' + #ser_sara.write(command.encode('utf-8')) + #response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False) + #print('

') + #print(response_SARA_2) + #print("

", end="") #Close socket - print("Close socket:") + print("Close socket:", end="") command = f'AT+USOCL={socket_id}\r' ser_sara.write(command.encode('utf-8')) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False)