v1.4.2 — Fix bug AT+USOWR leak dans payload UDP Miotiq

Corrige une desynchronisation serie qui causait l'envoi de la commande
AT+USOWR comme donnees UDP au lieu du payload capteurs. Ajout de flush
buffer serie, verification du prompt @, et abort propre a chaque etape.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
paul_vua
2026-03-14 22:53:59 +01:00
parent 7ab06f3413
commit d5b2e9c6c3
4 changed files with 203 additions and 61 deletions

View File

@@ -1 +1 @@
1.4.1 1.4.2

View File

@@ -1,5 +1,22 @@
{ {
"versions": [ "versions": [
{
"version": "1.4.2",
"date": "2026-03-14",
"changes": {
"features": [],
"improvements": [],
"fixes": [
"Fix envoi UDP Miotiq: desynchronisation serie causant l'envoi de la commande AT+USOWR comme payload au lieu des donnees capteurs",
"Ajout flush buffer serie (reset_input_buffer) avant chaque etape UDP critique",
"Verification du prompt @ du modem avant envoi des donnees binaires",
"Abort propre de l'envoi UDP si creation socket, connexion ou prompt @ echoue",
"Retry creation socket apres reset PDP reussi"
],
"compatibility": []
},
"notes": "Corrige un bug ou le modem SARA envoyait la commande AT+USOWR comme donnees UDP, causant des erreurs UNKNOWN_DEVICE sur le parser Miotiq."
},
{ {
"version": "1.4.1", "version": "1.4.1",
"date": "2026-03-12", "date": "2026-03-12",

View File

@@ -0,0 +1,107 @@
# Audit SARA_send_data_v2.py
Date: 2026-03-14
## Correction deja appliquee
**Bug AT+USOWR leak dans payload UDP Miotiq** — Le device_id recu par Miotiq etait `41542b55534f5752` = `AT+USOWR` (la commande AT elle-meme).
Cause: desynchronisation serie entre le script et le modem. Le code envoyait les donnees binaires sans verifier que le modem avait bien envoye le prompt `@`.
Corrections appliquees dans la section UDP (send_miotiq):
- `ser_sara.reset_input_buffer()` avant chaque commande AT critique
- Verification que `"@" in response` avant d'envoyer les donnees binaires
- Abort propre a chaque etape via `socket_id = None` si creation socket, connexion, ou prompt `@` echoue
- Retry `AT+USOCR=17` apres un PDP reset reussi
---
## Bugs critiques restants
### 1. Double `\r` sur plusieurs commandes AT
Certaines commandes AT ont `\r` dans le f-string ET un `+ '\r'` lors du write, ce qui envoie `\r\r` au modem.
**Lignes concernees:**
- **Ligne 988**: `command = f'AT+CSQ\r'` puis `ser_sara.write((command + '\r')...)`
- **Lignes 588, 628, 646, 656, 666, 674, 682, 690, 698**: fonctions `reset_server_hostname` et `reset_server_hostname_https`
- **Lignes 1403, 1541, 1570, 1694, 1741**: sections AirCarto et uSpot
Le modem tolere souvent le double `\r`, mais ca peut generer des reponses parasites dans le buffer serie et contribuer a des bugs de desynchronisation.
**Correction**: retirer le `\r` du f-string OU retirer le `+ '\r'` dans le write. Choisir une convention unique.
### 2. Double guillemet dans AT+URDFILE (ligne 1402)
```python
command = f'AT+URDFILE="aircarto_server_response.txt""\r'
# ^^ double "
```
Le `"` en trop peut causer une erreur AT ou une reponse inattendue.
**Correction**: `command = f'AT+URDFILE="aircarto_server_response.txt"\r'`
### 3. Crash si table SQLite vide (lignes 781-786, 820-826, 868-880)
```python
rows = cursor.fetchall()
data_values = [row[2:] for row in rows]
averages = [round(sum(col) / len(col),1) for col in zip(*data_values)]
```
Si `data_NPM`, `data_NPM_5channels` ou `data_envea` est vide, `data_values` sera `[]` et le calcul d'average crashera (IndexError / division par zero).
**Correction**: ajouter un check `if rows:` avant le calcul, comme c'est deja fait pour BME280 (ligne 840), wind (ligne 912) et MPPT (ligne 936).
### 4. Overflow struct.pack sur valeurs negatives (class SensorPayload)
```python
def set_npm_core(self, pm1, pm25, pm10):
self.payload[10:12] = struct.pack('>H', int(pm1 * 10)) # H = unsigned 16-bit
```
Si un capteur retourne une valeur negative (erreur capteur, -1, etc.), `struct.pack('>H', -10)` leve `struct.error`. Concerne: `set_npm_core`, `set_noise`, `set_envea`, `set_npm_5channels`, `set_wind`, `set_mppt` (sauf battery_current et temperatures qui utilisent `'>h'` signe).
**Correction**: clamper les valeurs avant pack: `max(0, int(value * 10))` pour les champs unsigned, ou verifier `value >= 0` avant le pack.
---
## Problemes importants
### 5. Port serie et SQLite jamais fermes
`ser_sara` (ligne 248) et `conn` (ligne 155) sont ouverts mais jamais fermes, meme dans le bloc `except` final (ligne 1755). Si le script crash, le port serie peut rester verrouille pour le prochain cycle.
**Correction**: ajouter un bloc `finally` apres le `except` (ligne 1757):
```python
finally:
ser_sara.close()
conn.close()
```
### 6. Code mort dans reset_server_hostname_https (lignes 717-722)
```python
if profile_id == 1: # ligne 613
...
elif profile_id == 1: # ligne 718 — jamais atteint car meme condition
pass
```
Copie-colle de `reset_server_hostname`. Le elif est mort.
**Correction**: supprimer le bloc elif (lignes 717-722).
---
## Resume
| # | Type | Description | Lignes |
|----|----------|------------------------------------------|----------------|
| 1 | Bug | Double \r sur commandes AT | 988, 588+, ... |
| 2 | Bug | Double guillemet AT+URDFILE | 1402 |
| 3 | Crash | Table SQLite vide -> IndexError | 781, 820, 868 |
| 4 | Crash | struct.pack overflow valeur negative | SensorPayload |
| 5 | Cleanup | Serial/SQLite jamais fermes (finally) | 248, 155 |
| 6 | Cleanup | Code mort elif profile_id==1 | 717-722 |

View File

@@ -1095,6 +1095,9 @@ try:
print(f"Binary payload: {len(binary_data)} bytes") print(f"Binary payload: {len(binary_data)} bytes")
#print(f"Binary payload: {binary_data}") #print(f"Binary payload: {binary_data}")
# Flush serial buffer to avoid stale data from previous operations
ser_sara.reset_input_buffer()
#create UDP socket (will return socket number) -> 17 is UDP protocol and 6 is TCP protocol #create UDP socket (will return socket number) -> 17 is UDP protocol and 6 is TCP protocol
# IF ERROR -> need to create the PDP connection # IF ERROR -> need to create the PDP connection
print("Create Socket:", end="") print("Create Socket:", end="")
@@ -1110,23 +1113,32 @@ try:
psd_csd_resets = reset_PSD_CSD_connection() psd_csd_resets = reset_PSD_CSD_connection()
if psd_csd_resets: if psd_csd_resets:
print("✅PSD CSD connection reset successfully") print("✅PSD CSD connection reset successfully")
# Retry socket creation after PDP reset
ser_sara.reset_input_buffer()
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)
print("Retry create socket:", end="")
print(response_SARA_1)
else: else:
print("⛔There were issues with the modem CSD PSD reinitialize process") print("⛔There were issues with the modem CSD PSD reinitialize process")
# Clignotement LED rouge en cas d'erreur # Clignotement LED rouge en cas d'erreur
led_thread = Thread(target=blink_led, args=(24, 5, 0.5)) led_thread = Thread(target=blink_led, args=(24, 5, 0.5))
led_thread.start() led_thread.start()
#Retreive Socket ID #Retreive Socket ID
socket_id = None
match = re.search(r'\+USOCR:\s*(\d+)', response_SARA_1) match = re.search(r'\+USOCR:\s*(\d+)', response_SARA_1)
if match: if match:
socket_id = match.group(1) socket_id = match.group(1)
print(f"Socket ID: {socket_id}", end="") print(f"Socket ID: {socket_id}", end="")
else: else:
print("Failed to extract socket ID") print('<span style="color: red;font-weight: bold;">⚠Failed to extract socket ID - skip UDP send⚠</span>')
#Connect to UDP server (USOCO) #Connect to UDP server (USOCO)
if socket_id is not None:
print("Connect to server:", end="") print("Connect to server:", end="")
ser_sara.reset_input_buffer()
command = f'AT+USOCO={socket_id},"192.168.0.20",4242\r' command = f'AT+USOCO={socket_id},"192.168.0.20",4242\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False)
@@ -1134,9 +1146,16 @@ try:
print(response_SARA_2) print(response_SARA_2)
print("</p>", end="") print("</p>", end="")
# Write data and send if "+CME ERROR" in response_SARA_2 or "ERROR" in response_SARA_2:
print('<span style="color: red;font-weight: bold;">⚠ATTENTION: Error connecting socket - skip UDP send⚠</span>')
ser_sara.write(f'AT+USOCL={socket_id}\r'.encode('utf-8'))
read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], timeout=1, end_of_response_timeout=1, debug=False)
socket_id = None
# Write data and send
if socket_id is not None:
print(f"Write data: {len(binary_data)} bytes", end="") print(f"Write data: {len(binary_data)} bytes", end="")
ser_sara.reset_input_buffer()
command = f'AT+USOWR={socket_id},{len(binary_data)}\r' command = f'AT+USOWR={socket_id},{len(binary_data)}\r'
ser_sara.write(command.encode('utf-8')) ser_sara.write(command.encode('utf-8'))
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False)
@@ -1144,9 +1163,18 @@ try:
print(response_SARA_2) print(response_SARA_2)
print("</p>", end="") print("</p>", end="")
# Verify modem sent @ prompt (ready for binary data)
if "@" not in response_SARA_2:
print('<span style="color: red;font-weight: bold;">⚠Modem did not send @ prompt - skip data send to avoid AT+USOWR leak⚠</span>')
ser_sara.reset_input_buffer()
ser_sara.write(f'AT+USOCL={socket_id}\r'.encode('utf-8'))
read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], timeout=1, end_of_response_timeout=1, debug=False)
socket_id = None
if socket_id is not None:
# Send the raw payload bytes (already prepared) # Send the raw payload bytes (already prepared)
ser_sara.write(binary_data) ser_sara.write(binary_data)
response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["@","OK", "+CME ERROR", "ERROR"], debug=False) response_SARA_2 = read_complete_response(ser_sara, wait_for_lines=["OK", "+CME ERROR", "ERROR"], debug=False)
print('<p class="text-danger-emphasis">', end="") print('<p class="text-danger-emphasis">', end="")
print(response_SARA_2) print(response_SARA_2)
print("</p>", end="") print("</p>", end="")
@@ -1169,16 +1197,6 @@ try:
#end loop #end loop
sys.exit() sys.exit()
#Read reply from server (USORD)
#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('<p class="text-danger-emphasis">')
#print(response_SARA_2)
#print("</p>", end="")
#Close socket #Close socket
print("Close socket:", end="") print("Close socket:", end="")
command = f'AT+USOCL={socket_id}\r' command = f'AT+USOCL={socket_id}\r'