Problème de Connexion HTTPS avec SIMCom A7670E après Reconnexion GPRS (Échec de la Liaison SSL)

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 :

  1. Démarrage : La configuration initiale (connexion réseau, téléchargement du certificat, configuration SSL) se déroule sans aucune erreur.
  2. 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).
  3. 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".
  4. 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.

Avez vous essayé de réinitialiser le contexte SSL ou HTTP interne du modem ?

➜ par exemple ce serait intéressant de voir ce que donne un AT+HTTPTERM (ou similaire je ne connais pas le SIMCom A7670E ) qui devrait terminer la session HTTP active sur le modem et libérer les ressources et fermer proprement la connexion HTTP en cours.

Ensuite vous essayez de relancer une nouvelle session avec AT+HTTPINIT avant de reconfigurer SSL et d’envoyer la requête.

Bonjour
Apres m'etre penché sur votre idee j'ai revu mon code mais en vain. le resultat est toujours le meme. il aurait il une position particuliere pour le contexte afin quil s'execute correctement?

pas d'autres idées... désolé

éventuellement prévoir un transistor pour couper l'alim du module et le rebooter ?

Salut ntx972, j'ai le même module que toi, SIMCom A7670E. As tu pu résoudre le problème ? Je peux peut être aider. @tte :wink:

Hypothèse : fonctionnement bancal entre deux profils : celui que tu cherches à utilser (index 0 ?) el le profil par défaut (index 1) que le modem utilise par conception au démarrage voire dans certaines situations par la suite…

En travaillant en permanence avec le profil PDP 1 ça arrange peut être les choses.

Les notes d’Application de SIMCOM montrent souvent des séquence de commandes AT avec le profil 0 (profil de test ?)

Au vu du comportement du A7670E Je commence à penser qu’en ‘production’ mieux vaut se cantonner au profil par défaut , le profil 1 en y renseignant en amont l’APN, une fois pout toutes, pour la SIM utilisée.

AT+CGDCONT=1,”IP”,”apn” ……etc…..

Par la suite le modem LTE se connecte automatiquement au réseau cellulaire et active les ‘Data’ en utilsant systématiquement le profil 1 enregistré dans sa mémoire non volatile . Travailler en permanence avec ce profil privilégié , parmi les 4 profils disponibles, permet l’éviter le risque de discontinuité.