From 4779f426d9336933fa26fc5c37e5c35227eac0b7 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Thu, 18 Sep 2025 16:43:49 +0100
Subject: [PATCH] update
---
loop/SARA_send_data_v2.py | 213 ++++++++++++++++++++++++++++++++------
1 file changed, 184 insertions(+), 29 deletions(-)
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)