Bonjour à tous,
Je développe un projet sur un ESP32 utilisant un modem SIMCom A7670E pour envoyer des données de capteurs à un serveur via une requête HTTPS POST. J'utilise les commandes AT directes pour un contrôle total de la transaction.
Le Scénario :
Mon appareil se connecte, configure le SSL, et fonctionne parfaitement au démarrage. Il collecte des données toutes les minutes et les envoie par lots toutes les 15 minutes.
Le problème survient spécifiquement après une perte et une reconnexion au réseau GPRS .
Le Problème en Détail :
- Démarrage : La configuration initiale (connexion réseau, téléchargement du certificat, configuration SSL) se déroule sans aucune erreur.
- Perte de Connexion : Après un certain temps (généralement avant l'envoi du premier lot de données), le modem perd la connexion GPRS (
!modem.isGprsConnected()retourne vrai). - Reconnexion : Mon code détecte la perte, se reconnecte avec succès au GPRS, synchronise l'heure, et ré-exécute la même fonction de configuration SSL qui avait parfaitement fonctionné au démarrage . Toutes les commandes de configuration (
AT+CSSLCFG) retournent "OK". - L'Échec : Immédiatement après, lorsque la fonction d'envoi de données est appelée, la toute première commande liée à la sécurité de la transaction HTTP (
AT+HTTPPARA="SSL",1,0) échoue systématiquement avec le message :[HTTPS AT] Échec de la liaison du contexte SSL.
Il semble que la reconnexion GPRS place le modem dans un état où, bien qu'il accepte les commandes de configuration SSL, le contexte de sécurité créé n'est plus utilisable par le service HTTP.
Ce que j'ai déjà essayé sans succès :
- Vérification du Certificat : J'utilise le bon certificat racine (
ISRG Root X1) pour mon serveur (qui utilise Let's Encrypt). Le certificat est bien téléchargé sur le modem. - Ordre des Commandes : J'ai essayé de multiples ordres pour les commandes
AT+CSSLCFG(authmode,cacert, etc.). - Timing de la Configuration : J'ai déplacé l'appel à
configureSSL()pour qu'il s'exécute juste avant chaque transaction HTTP, afin de garantir un contexte "frais". - Configuration du SNI : J'ai déplacé la configuration du SNI du contexte SSL global (
AT+CSSLCFG) vers les paramètres de la session HTTP (AT+HTTPPARA="SNI",...), ce qui a résolu les problèmes de configuration au démarrage, mais pas le problème après la reconnexion.
Ma Question :
Quelqu'un a-t-il déjà rencontré ce problème où un contexte SSL devient inutilisable après une reconnexion GPRS sur un A7670E (ou un module similaire) ? Y a-t-il une commande AT spécifique pour "nettoyer" ou "réinitialiser" complètement l'état SSL/HTTP après une reconnexion, que j'aurais manquée ?
Toute aide ou piste serait grandement appréciée.
Code pertinant:
/**
* @brief Configure le contexte SSL/TLS du modem pour les requêtes HTTPS.
* @note Doit être appelée dans setup() après l'initialisation du modem.
*/
void configureSSL() {
Serial.println("--- Configuration SSL/TLS pour HTTPS ---");
Serial.print("Configuration de la version SSL... ");
modem.sendAT("+CSSLCFG=\"sslversion\",0,4");
if (modem.waitResponse() != 1) {
Serial.println("ERREUR.");
} else {
Serial.println("OK.");
}
Serial.print("Configuration du mode d'authentification... ");
modem.sendAT("+CSSLCFG=\"authmode\",0,1");
if (modem.waitResponse() != 1) {
Serial.println("ERREUR.");
} else {
Serial.println("OK.");
}
Serial.print("Liaison du certificat CA... ");
modem.sendAT("+CSSLCFG=\"cacert\",0,\"", CERT_FILENAME, "\"");
if (modem.waitResponse() != 1) {
Serial.println("ERREUR.");
} else {
Serial.println("OK.");
}
Serial.println("Configuration SSL/TLS terminée.");
}
/**
* @brief Lit les données, les formate en JSON et les envoie via HTTPS avec des commandes AT.
*/
void readAndSendBatchData() {
File dataFile = SD.open(DATA_FILENAME, FILE_READ);
if (!dataFile) {
Serial.println("Impossible d'ouvrir le fichier de données pour l'envoi.");
return;
}
String lines[BATCH_SIZE];
int lineCount = 0;
while (dataFile.available()) {
String line = dataFile.readStringUntil('\n');
line.trim();
if (line.length() > 0 && line.indexOf("Timestamp") == -1) {
lines[lineCount % BATCH_SIZE] = line;
lineCount++;
}
}
dataFile.close();
if (lineCount == 0) {
Serial.println("Aucune donnée à envoyer.");
return;
}
bool gps_fixed = getGPSLocation(latitude, longitude, gps_altitude, speed, satellites);
DynamicJsonDocument doc(12288);
JsonArray jsonArray = doc.to<JsonArray>();
int linesToSend = min(lineCount, BATCH_SIZE);
for (int i = 0; i < linesToSend; i++) {
String currentLine = lines[(lineCount - linesToSend + i) % BATCH_SIZE];
JsonObject obj = jsonArray.createNestedObject();
char csvBuffer[250];
currentLine.toCharArray(csvBuffer, 250);
char timestamp[25];
float tempBMP, pressure, altitude, tempAHT, humidity, no2_ppm, no2_ppb, no2_ugm3;
char imei[20], serial[20];
int parsed_count = sscanf(csvBuffer, "%24[^,],%f,%f,%f,%f,%f,%f,%f,%f,%*f,%*f,%*f,%*f,%*d,%19[^,],%19s",
timestamp, &tempBMP, &pressure, &altitude, &tempAHT, &humidity,
&no2_ppm, &no2_ppb, &no2_ugm3, imei, serial);
if (parsed_count >= 9) {
obj["timestamp_text"] = timestamp;
obj["temp_ext_c"] = tempBMP;
obj["pressure_hpa"] = pressure;
obj["altitude_m"] = altitude;
obj["temp_int_c"] = tempAHT;
obj["humidity_percent"] = humidity;
obj["no2_ppm"] = no2_ppm;
obj["no2_ppb"] = no2_ppb;
obj["no2_ugm3"] = no2_ugm3;
obj["latitude"] = latitude;
obj["longitude"] = longitude;
obj["gps_altitude_m"] = gps_altitude;
obj["gps_speed_kmh"] = speed;
obj["gps_satellites"] = satellites;
obj["imei"] = imei;
obj["sensor_serial"] = serial;
}
}
String jsonPayload;
serializeJson(doc, jsonPayload);
Serial.println("\n[HTTPS AT] Envoi du lot de données à Xano...");
Serial.println(jsonPayload);
// 1. Initialiser le service HTTP
modem.sendAT("+HTTPINIT");
if (modem.waitResponse() != 1) {
Serial.println("[HTTPS AT] Échec de HTTPINIT");
return;
}
bool request_success = false;
do { // Bloc pour faciliter la sortie en cas d'erreur avec break
// CORRECTION : Re-configurer le contexte SSL juste avant la transaction
configureSSL();
// NOUVEAU : Lier la session HTTP au contexte SSL 0
modem.sendAT("+HTTPPARA=\"SSL\",1,0");
if (modem.waitResponse() != 1) {
Serial.println("[HTTPS AT] Échec de la liaison du contexte SSL");
break;
}
// NOUVEAU : Configurer le SNI pour cette session HTTP
modem.sendAT("+HTTPPARA=\"SNI\",\"", xano_server, "\"");
if (modem.waitResponse() != 1) {
Serial.println("[HTTPS AT] Échec de la configuration du SNI");
break;
}
// 2. Lier au contexte PDP (activé dans le setup)
modem.sendAT("+HTTPPARA=\"CID\",1");
if (modem.waitResponse() != 1) {
Serial.println("[HTTPS AT] Échec de la configuration du CID");
break;
}
// 3. Définir l'URL complète
String url = String("https://") + xano_server + xano_path;
modem.sendAT("+HTTPPARA=\"URL\",\"", url, "\"");
if (modem.waitResponse() != 1) {
Serial.println("[HTTPS AT] Échec de la configuration de l'URL");
break;
}
// 4. Définir le type de contenu
modem.sendAT("+HTTPPARA=\"CONTENT\",\"application/json\"");
if (modem.waitResponse() != 1) {
Serial.println("[HTTPS AT] Échec de la configuration du Content-Type");
break;
}
// 5. Préparer l'envoi des données POST (payload)
modem.sendAT("+HTTPDATA=", jsonPayload.length(), ",120000");
if (modem.waitResponse(10000L, "DOWNLOAD") != 1) {
Serial.println("[HTTPS AT] Échec de la commande HTTPDATA");
break;
}
// 6. Envoyer le corps de la requête (payload)
modem.stream.print(jsonPayload);
modem.stream.flush();
if (modem.waitResponse() != 1) { // Attendre le OK après l'envoi des données
Serial.println("[HTTPS AT] Échec de l'envoi du payload");
break;
}
// 7. Exécuter l'action POST (méthode 1)
modem.sendAT("+HTTPACTION=1");
String response;
if (modem.waitResponse(120000L, response) != 1 || response.indexOf("+HTTPACTION:") == -1) {
Serial.println("[HTTPS AT] Pas de réponse ou réponse invalide de HTTPACTION");
break;
}
int statusCode = 0;
// Parse: +HTTPACTION: <method>,<status>,<len>
char* resp_str = (char*)response.c_str();
strtok(resp_str, ",");
statusCode = atoi(strtok(NULL, ","));
Serial.print("[HTTPS AT] Statut de la réponse: "); Serial.println(statusCode);
if (statusCode >= 200 && statusCode < 300) {
request_success = true;
}
} while(false);
// 8. Toujours terminer la session HTTP pour libérer les ressources
modem.sendAT("+HTTPTERM");
modem.waitResponse();
// 9. Gérer le résultat après la fin de la session
if (request_success) {
Serial.println("[HTTPS AT] Lot de données envoyé avec succès !");
DateTime now = rtc.now();
char archiveName[30];
sprintf(archiveName, "/archive_%04d%02d%02d_%02d%02d.csv", now.year(), now.month(), now.day(), now.hour(), now.minute());
if (SD.rename(DATA_FILENAME, archiveName)) {
Serial.printf("Fichier de données archivé sous: %s\n", archiveName);
} else {
Serial.println("ERREUR: Impossible de renommer le fichier. Tentative de suppression...");
if(SD.remove(DATA_FILENAME)) {
Serial.println("Ancien fichier de données supprimé.");
} else {
Serial.println("ERREUR: Impossible de supprimer l'ancien fichier.");
}
}
} else {
Serial.println("[HTTPS AT] Échec de l'envoi du lot. Les données seront renvoyées au prochain cycle.");
}
}
Journal d'execution:
------ Cycle de mesure (1 minute) ------
Données enregistrées sur /data_minute.csv
--- Cycle de 15 minutes atteint. Préparation de l'envoi du lot. ---
Tentative de localisation GPS...
GPS activé. Attente d'un fix...
Échec de la localisation GPS après le délai imparti.
GPS désactivé.
[HTTPS AT] Envoi du lot de données à Xano...
[{"timestamp_text":"2025-07-23T21:04:20Z","temp_ext_c":29.78,"pressure_hpa":990.68,"altitude_m":-143.77,"temp_int_c":30.27,"humidity_percent":66.37,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:05:20Z","temp_ext_c":29.67,"pressure_hpa":990.66,"altitude_m":-143.77,"temp_int_c":30.2,"humidity_percent":66.51,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:06:20Z","temp_ext_c":29.55,"pressure_hpa":990.69,"altitude_m":-143.76,"temp_int_c":30.05,"humidity_percent":66.99,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:07:20Z","temp_ext_c":29.38,"pressure_hpa":990.68,"altitude_m":-143.77,"temp_int_c":29.88,"humidity_percent":67.59,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:08:20Z","temp_ext_c":29.13,"pressure_hpa":990.67,"altitude_m":-143.77,"temp_int_c":29.65,"humidity_percent":68.34,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:09:20Z","temp_ext_c":28.97,"pressure_hpa":990.66,"altitude_m":-143.77,"temp_int_c":29.49,"humidity_percent":68.88,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:10:20Z","temp_ext_c":28.84,"pressure_hpa":990.69,"altitude_m":-143.76,"temp_int_c":29.35,"humidity_percent":69.55,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:11:20Z","temp_ext_c":28.62,"pressure_hpa":990.68,"altitude_m":-143.77,"temp_int_c":29.15,"humidity_percent":70.48,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:12:20Z","temp_ext_c":28.44,"pressure_hpa":990.65,"altitude_m":-143.77,"temp_int_c":28.99,"humidity_percent":71.26,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:13:20Z","temp_ext_c":28.52,"pressure_hpa":990.68,"altitude_m":-143.77,"temp_int_c":29.06,"humidity_percent":71.23,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:14:20Z","temp_ext_c":28.56,"pressure_hpa":990.69,"altitude_m":-143.76,"temp_int_c":29.09,"humidity_percent":70.8,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:15:20Z","temp_ext_c":28.6,"pressure_hpa":990.69,"altitude_m":-143.76,"temp_int_c":29.13,"humidity_percent":70.71,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:16:20Z","temp_ext_c":28.6,"pressure_hpa":990.73,"altitude_m":-143.76,"temp_int_c":29.11,"humidity_percent":70.79,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:17:20Z","temp_ext_c":28.55,"pressure_hpa":990.72,"altitude_m":-143.76,"temp_int_c":29.08,"humidity_percent":71.07,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"},{"timestamp_text":"2025-07-23T21:18:20Z","temp_ext_c":28.68,"pressure_hpa":990.72,"altitude_m":-143.76,"temp_int_c":29.21,"humidity_percent":70.65,"no2_ppm":0,"no2_ppb":0,"no2_ugm3":0,"latitude":0,"longitude":0,"gps_altitude_m":0,"gps_speed_kmh":0,"gps_satellites":0,"imei":"862771075501844","sensor_serial":"HB-NO2-00001"}]
--- Configuration SSL/TLS pour HTTPS ---
Configuration de la version SSL... OK.
Configuration du mode d'authentification... OK.
Liaison du certificat CA... OK.
Configuration SSL/TLS terminée.
[HTTPS AT] Échec de la liaison du contexte SSL
[HTTPS AT] Échec de l'envoi du lot. Les données seront renvoyées au prochain cycle.
------ Fin du cycle. En attente de la prochaine minute. ------
Je vous remercie d'avance.