LE LABO HORS DES MURS

Le terrain mesure.
Votre station raconte.

Température et humidité avec le SHT85, un ESP32 et un SIM7080 LTE-M.

Parcours simplifié : M5Stamp CAT-M S003 avec alimentation 5 V et UART externe 3,3 V. POWERKEY/STATUS sont internes, sans fil de commande à ajouter. APN 1NCE à confirmer : sensor.net pour les nouvelles SIM plateforme 2.0, iot.1nce.net pour certains profils existants.

Votre parcours

  1. Mesurer en pleine nature
  2. Un montage qui ne chauffe pas sa mesure
  3. M5Stamp CAT-M : le parcours simplifié
  4. Le SHT85 : quatre broches, deux contrôles
  5. Acheter et préparer la SIM 1NCE
  6. Lire une conversation AT
  7. Le programme : des états et une preuve de livraison
  8. Un collecteur qui confirme après écriture
  9. Économiser sans promettre une autonomie
  10. Qualifier au banc puis sur le terrain

01 / PARTIR DU BESOIN

Mesurer en pleine nature

Objectif : observer la température et l’humidité de l’air avec un SHT85, puis transporter de petites mesures depuis un ESP32 classique au moyen d’un SIM7080 LTE-M. Le capteur mesure ; le microcontrôleur organise ; le modem se connecte ; le serveur conserve. Chaque maillon doit être vérifié séparément.

Le projet fournit un programme Arduino complet, un collecteur Python et une méthode de qualification. Il n’inclut ni SIM, ni matériel, ni service de transmission opérationnel. Le programme doit être configuré. Il n’a pas été validé sur le montage M5Stamp CatM + ESP32 + SHT85.

Définissez avant de câbler : lieu autorisé, hauteur de mesure, fréquence utile, durée souhaitée, accès pour maintenance, conditions de condensation et possibilité réelle de couverture LTE-M. Un relevé toutes les dix minutes est un choix pédagogique du programme, pas une exigence du SHT85.

Choisissez votre critère de succès : une mesure valide, enregistrée une seule fois par le collecteur, avec confirmation retournée à la station. Un voyant réseau, une adresse IP ou un OK du modem ne prouvent pas que la mesure a été sauvegardée.

Lecture du guide : commencez par le câblage et le capteur seul, validez ensuite le modem au terminal AT, puis activez le programme complet. Les annexes contiennent le code imprimé et les fichiers sources intégrés au PDF. En cas de lecteur PDF ne montrant pas les pièces jointes, utilisez le code imprimé ou les sources du tutoriel.

02 / QUALITÉ ENVIRONNEMENTALE

Un montage qui ne chauffe pas sa mesure

Placez le SHT85 dans un abri ventilé et protégé du rayonnement direct, à distance du régulateur, du modem et de la batterie. Un capteur enfermé dans la boîte électronique mesure surtout le microclimat de cette boîte. Notez la hauteur, le type d’abri, la végétation, les obstacles et toute modification du montage.

Séparez la protection de l’électronique et la circulation de l’air autour du capteur. Évitez les pièges à eau et prévoyez une boucle vers le bas pour les câbles. La membrane du SHT85 ne confère pas à la station entière une certification d’étanchéité. Ne plongez pas le montage et ne collez pas sur l’ouverture du capteur.

À la réception, contrôlez une mesure stable face à une référence connue, dans le même air et après équilibre thermique. Consignez l’écart, les conditions et la référence ; une simple comparaison ne constitue pas un étalonnage traçable. Deux décimales affichées ne donnent pas une exactitude de 0,01 degré.

Conservez les données brutes utiles et un indicateur de qualité. Une lecture I²C invalide ou un CRC incorrect est une absence de mesure, jamais une température de zéro. Le programme refuse les données défectueuses plutôt que de fabriquer une valeur de remplacement.

L’exemple horodate à la réception côté serveur. Il ne connaît pas l’heure UTC exacte de l’acquisition : après une coupure réseau, une mesure ancienne peut arriver plus tard. Pour une campagne scientifique, ajoutez une horloge synchronisée, un horodatage d’acquisition et une file durable dimensionnée à la durée de panne attendue.

03 / BRANCHEMENT À CONFIRMER

M5Stamp CAT-M : le parcours simplifié

Mesurer et transmettre en LTE-M
Schéma de branchement : cliquez pour agrandir. Suivez les noms des broches, les connecteurs sont déployés pour la lisibilité. SVG · Tous les branchements

Le montage principal utilise le M5Stack Stamp CatM S003 à SIM7080G, proposé par le propriétaire. Il intègre le régulateur, l’adaptation UART, le support MicroSIM et la connexion d’antenne. Il faut toujours l’ESP32, le SHT85 et une alimentation appropriée ; les fils ou barrettes doivent être fixés correctement. [15-16]

Sur le connecteur J1 du schéma M5Stack : TX=2, RX=3, GND=4, 3V3=5 et 5V=6 ; ANT=1 est un signal radio. Alimenter le Stamp par 5V et GND. Laisser 3V3 libre dans ce montage et ne pas relier simultanément deux alimentations. Les lignes TX/RX externes sont adaptées au domaine 3,3 V : TX Stamp vers GPIO16, RX Stamp vers GPIO17. Ne pas appliquer 5V sur UART. [16]

Le schéma relie en interne PWRKEY (module, 39) à STATUS (module, 42) par R4=0 ohm. Aucun de ces deux signaux ne sort sur J1. Ne pas ajouter de GPIO POWERKEY/STATUS. Le sketch utilise CFUN=0 puis CFUN=1 et ne demande pas une extinction CPOWD qui nécessiterait un redémarrage adapté. GPIO25 et GPIO26 restent réservés, en entrée. Vérifier AT après mise sous tension. [16]

Alternative avancée : LCSC C2943992 est le SIM7080G nu à souder, pas cette carte intégrée. Il exige un circuit support avec alimentation, UART 1,8 V adapté à l’ESP32, support SIM, antenne et commande de démarrage. Le Hardware Design confirme UART1_TXD=1, RXD=2, PWRKEY=39, STATUS=42 et VBAT=34/35. Ne pas reproduire le branchement 5V du Stamp sur le module nu. [3,17]

Stamp CatM / J1ESP32 ou alimentationPrécision
6 / 5VAlimentation 5 V adaptéePas un GPIO
4 / GNDGND communIndispensable
2 / TXGPIO16 / RX2UART externe 3,3 V
3 / RXGPIO17 / TX2UART externe 3,3 V
5 / 3V3Non connecté iciSortie du régulateur
1 / ANTNe pas relier au GPIOUtiliser antenne IPEX
POWERKEY / STATUSAucun fil sur ESP32Non exposés sur J1
M5Stamp CAT-M avec UART et support MicroSIM intégrés
M5Stamp CAT-M : le parcours simplifié. Régulateur, adaptation UART et support MicroSIM intégrés.
SIM7080G nu avec contacts à souder, dessus et dessous
SIM7080G nu : intégration avancée. Circuit support, alimentation et niveaux logiques à concevoir.

04 / CAPTEUR TEMPÉRATURE ET HUMIDITÉ

Le SHT85 : quatre broches, deux contrôles

Le SHT85 utilise l’adresse I²C fixe 0x44. Dans le brochage fabricant, la broche 1 est SCL, 2 VDD, 3 VSS/GND et 4 SDA. Respectez la vue et le détrompage de la fiche Sensirion ; ne déduisez pas l’ordre des broches des couleurs du câble. Le brochage d’un adaptateur peut différer.

Pour ce montage ESP32, alimentez le SHT85 en 3,3 V et utilisez des résistances de rappel SDA/SCL vers 3,3 V, si elles ne sont pas déjà présentes sur l’adaptateur. Vérifiez leur dimensionnement en fonction du câblage et de la capacité du bus. Ajoutez le découplage 100 nF au plus près du capteur. Gardez le câble court pour la première validation.

Le programme déclenche une mesure simple haute répétabilité par 0x2400, attend 20 ms sans suspendre la boucle, puis lit six octets. Il contrôle séparément le CRC des deux mots, avec polynôme 0x31 et initialisation 0xFF. La conversion est T = -45 + 175 × brut/65535 et HR = 100 × brut/65535. Source : [4], sections 3 et 4.

Vérification au banc : modem débranché, vérifiez l’alimentation, l’adresse I²C et des valeurs cohérentes. Réchauffez doucement l’air autour de la sonde, puis attendez son retour à l’équilibre. Une erreur répétée appelle un contrôle du sens du connecteur, des masses, des résistances de rappel et du câble.

SHT85 nuESP32 classique
1 / SCLGPIO22
2 / VDD3V3
3 / VSSGND
4 / SDAGPIO21

05 / CONNECTIVITÉ AVANT LA FORÊT

Acheter et préparer la SIM 1NCE

Sur 1nce.com, choisissez votre pays et vérifiez l’éligibilité de votre statut au contrat proposé. Consultez la couverture par technologie, spécifiquement LTE-M, et les conditions de l’offre au moment de commander. Une présence 4G grand public ne garantit ni LTE-M, ni l’accès par les partenaires de roaming 1NCE au point de mesure.

Sélectionnez le format physique compatible avec la carte SIM7080, puis contrôlez panier, taxes, livraison, durée contractuelle et quotas. Le tarif et les quotas ne sont pas figés dans ce guide. Conservez les documents de commande et l’accès au portail dans un gestionnaire de mots de passe.

Dans le portail, retrouvez la SIM par son ICCID, contrôlez son statut activé/Enabled, son volume disponible et les éventuelles restrictions. Les intitulés peuvent changer selon la plateforme. Activez ou réactivez la connectivité si nécessaire, sans supposer que toute SIM reçue est déjà prête. [5]

Confirmez l’APN applicable dans votre portail ou auprès du support : les SIM nouvellement commandées sur la plateforme 2.0 utilisent sensor.net ; les anciennes peuvent conserver iot.1nce.net ou un APN personnalisé. Ne testez pas aveuglément plusieurs APN. L’APN doit être explicitement configuré. [6-8]

Pour les profils documentés cités : IPv4, utilisateur et mot de passe vides ; PAP si le modem requiert une méthode. N’utilisez pas votre mot de passe de portail comme mot de passe APN. Un volume épuisé peut empêcher les données alors que le modem reste enregistré. Coupez l’alimentation avant d’insérer ou retirer la SIM. [5-7]

06 / DU TERMINAL AU RÉSEAU

Lire une conversation AT

Ouvrez un terminal série vers l’UART du modem au débit effectivement configuré, avec fin de commande CR. Évitez de piloter simultanément le modem avec le terminal et le programme. Identifiez modèle et firmware ; comparez les commandes au manuel SIMCom V1.07 et aux notes du firmware. [2]

Séquence de diagnostic ci-dessous : APN_CONFIRME est à remplacer. Les index 1 de CGDCONT et 0 de CNCFG appartiennent à deux familles de commandes : ne les assimilez pas. Le programme utilise le profil applicatif 0 pour le socket. Vérifiez les formes de test avant d’imposer un paramètre.

CEREG : un statut 1 ou 5 permet de poursuivre ; 2 signifie recherche, 3 refus. Un OK valide la commande, pas la réception de votre donnée par le serveur. Une IP valide du profil actif prouve seulement que le contexte est prêt. Le code attend aussi un accusé signé du collecteur.

En cas de refus, relevez localement le motif d’erreur, contrôlez statut/quota SIM, APN, antenne et alimentation. Ne forcez pas une bande ou un opérateur au hasard. Les limites de temps de notre programme sont des choix de reprise, pas des garanties de temps d’enregistrement du réseau.

AT
ATE0
ATI
AT+GMR
AT+CMEE=2
AT+CPIN?
AT+CNMP=?
AT+CMNB=?
AT+CNMP=38
AT+CMNB=1
AT+CFUN=1
AT+CGDCONT=1,"IP","APN_CONFIRME"
AT+CNCFG=0,1,"APN_CONFIRME","","",1
AT+CEREG?
AT+CPSI?
AT+CSQ
AT+CNACT=0,1
AT+CNACT?

07 / ARDUINO IDE

Le programme : des états et une preuve de livraison

Installez Arduino IDE depuis Arduino et le paquet ESP32 depuis Espressif [9]. Choisissez le modèle réel de carte, son port et le moniteur série à 115200 bauds. Le sketch utilise les composants fournis par le core ESP32 : Wire, Preferences, mbedTLS et sommeil profond. Il ne dépend pas d’une bibliothèque SIM7080 supplémentaire.

Ouvrez le dossier TerrainSHT85 et son fichier de même nom. Renseignez APN, SERVER, PORT, DEVICE et KEY. SERVER doit être votre collecteur UDP joignable, pas simplement une page web. PORT=9001 est le choix de ce projet. KEY est un secret aléatoire de 64 caractères hex, utilisé comme texte ASCII identique sur les deux équipements. Une clé différente doit être créée par station.

HARDWARE_VERIFIED reste false jusqu’au contrôle de la carte. Une configuration incomplète laisse le programme au repos. Sur le Stamp CatM, POWERKEY et STATUS sont internes ; aucun GPIO ne leur est attribué. Relais et buzzer ne sont pas actionnés. Le modem doit répondre à AT après mise sous tension.

Parcours : mesure → contrôle CRC → sauvegarde NVS → vérification modem/SIM → enregistrement LTE-M → contexte IP → socket UDP → envoi signé → attente ACK → nettoyage → repos. Les réponses finales et les indications utiles sont attendues ensemble. La boucle traite des quantités bornées de caractères ; aucune boucle d’attente longue ni delay n’est employé.

Ce fonctionnement est coopératif, pas temps réel strict : les appels I²C et les écritures NVS prennent un temps borné ou variable. Le programme limite le temps de bus I²C. Une seule mesure reste en attente ; tant qu’elle n’est pas confirmée, il réessaie cette mesure sans en acquérir une nouvelle. Une campagne exigeant toutes les mesures pendant une panne doit ajouter une vraie file durable.

Le fichier est fourni en annexe et en pièce jointe. Sa compilation Arduino et sa validation sur le matériel cible sont à réaliser avant campagne. Ne déclarez jamais un essai radio ou une autonomie validés sur la seule base d’une relecture du code.

08 / DE LA STATION AU SERVEUR

Un collecteur qui confirme après écriture

Le collecteur Python fourni reçoit un message de la forme v1|station|sequence|temperature|humidite|signature. Le HMAC-SHA256 couvre tous les champs qui le précèdent. Le serveur vérifie la signature, les bornes et le numéro de séquence, sauvegarde dans SQLite puis renvoie un ACK signé. Il ne confirme pas avant la validation de l’écriture.

L’identifiant et le numéro de séquence forment la clé unique en base. Une retransmission exacte reçoit de nouveau son ACK sans créer une nouvelle mesure. Un numéro ancien jamais enregistré est refusé. Ne réinitialisez pas la mémoire NVS d’une station en conservant le même identifiant : changez d’identifiant et de clé, ou gérez explicitement cette réinitialisation dans le collecteur.

UDP ne garantit pas la livraison. Une commande CASEND réussie signifie seulement que le modem a accepté l’envoi. L’ACK applicatif signé est donc indispensable à la suppression de la mesure en attente. Un ACK perdu provoque une retransmission sans double comptage.

Ce transport est authentifié mais non chiffré : un observateur du chemin réseau peut lire les mesures et l’identifiant technique. N’y mettez ni nom de personne, ni coordonnées sensibles. Pour de la confidentialité, déployez le collecteur sur l’interface VPN privée prévue par votre offre 1NCE et validez le routage, ou développez une variante TLS/DTLS avec vérification des certificats. Ce guide ne présente pas HMAC comme un chiffrement.

Le collecteur écoute sur 127.0.0.1 par défaut et n’est pas installé automatiquement sur votre VPS. Pour un banc local, renseignez TERRAIN_KEYS et TERRAIN_DB puis lancez collector.py. Pour un accès cellulaire réel, choisissez délibérément l’adresse d’écoute privée/VPN ou publique, autorisez uniquement le port UDP choisi dans les pare-feu nécessaires et surveillez les volumes. Aucun nouveau port radio n’a été ouvert par la publication du tutoriel.

Le fichier de clés est un objet JSON privé, par exemple {"station01":"votre_cle_aleatoire_64_hex"}. Générez la clé avec secrets.token_hex(32) en Python, conservez-la hors du site public et du dépôt. Dans un terminal privé : export TERRAIN_KEYS=/chemin/prive/keys.json ; export TERRAIN_DB=/chemin/prive/terrain.sqlite ; python3 collector.py. Exportez aussi TERRAIN_BIND pour quitter volontairement le loopback.

09 / LOW-POWER ET REPRISE

Économiser sans promettre une autonomie

Après le cycle, le programme ferme le socket, désactive le contexte applicatif puis demande CFUN=0. Cela réduit l’activité radio ; ce n’est pas le PSM. En cas de succès, la pause suivante vaut dix minutes. Sans ACK, les reprises espacées passent progressivement d’environ une minute à trente minutes avec une petite gigue.

DEEP_SLEEP=false facilite le diagnostic au banc. En le passant à true après validation, l’ESP32 peut dormir entre cycles, tandis que le modem reste alimenté avec radio réduite. L’ESP32 redémarre à chaque réveil et conserve la mesure en NVS. La veille profonde n’est utilisée à la fin d’un cycle que si CFUN=0 a réussi. Le courant résiduel de l’ensemble doit être mesuré.

Pour aller plus loin : qualifier le PSM et l’eDRX avec le réseau et le firmware réels, vérifier les temporisateurs accordés, les signaux de réveil et les niveaux électriques. Les commandes CPSMS et CEDRXS ne garantissent pas à elles seules l’acceptation réseau. Ce projet ne force aucun temporisateur binaire supposé universel. [1-2]

Une coupure complète du modem exige une extinction et un redémarrage propres selon la carte. Ne coupez pas le relais GPIO25 à l’aveugle. Avec le Stamp, couper puis remettre le 5V impose aussi d’éviter toute rétro-alimentation par UART ; ne pas modifier les liaisons POWERKEY/STATUS internes. Le buzzer et une bobine de relais entretenue peuvent consommer davantage que les économies attendues.

Mesurez le courant à l’entrée batterie sur un cycle entier : démarrage, acquisition, recherche réseau, émission, attente et repos. Énergie journalière = somme des puissances × durées sur la journée. Autonomie estimée = énergie réellement utilisable de la batterie / énergie journalière. Intégrez température, vieillissement, rendement et longues recherches réseau. Ne multipliez pas un courant PSM du module nu par la durée de votre campagne.

La facilité du Stamp a un coût énergétique : la fiche M5Stack indique 46 mA sous 5 V en veille dans ses conditions de mesure. Ce n’est pas le courant PSM du SIM7080G nu. La LED et le convertisseur font partie du bilan ; une autonomie longue doit être mesurée sur la carte entière. [15]

10 / ESSAIS À RÉALISER

Qualifier au banc puis sur le terrain

1. Capteur : valider le sens du connecteur et une lecture cohérente ; débrancher SDA, vérifier qu’aucune mesure factice n’est envoyée, puis rétablir la connexion. Contrôler le comportement après une coupure d’alimentation.

2. Réseau : tester SIM absente, APN volontairement incorrect et absence de couverture dans un environnement de test. Le programme doit sortir de l’attente et espacer les reprises. Relever CEREG, CPSI et les motifs d’erreur sans publier ICCID, IMSI, clés ou mots de passe.

3. Livraison : bloquer le collecteur après envoi puis le rétablir. Observer que la même séquence est retransmise, stockée une seule fois et effacée de la NVS après l’ACK. Couper l’ESP32 avant l’ACK puis redémarrer ; la mesure en attente doit être conservée.

4. Authentification : utiliser une clé erronée et envoyer un ACK modifié. Le collecteur doit rejeter le premier cas et la station ne doit pas libérer la mesure dans le second. Tester séparément les doublons et les anciens numéros de séquence.

5. Énergie : comparer courant au repos, pendant la recherche réseau et après CFUN=0. Tester ensuite le sommeil profond. Si le modem n’est pas réellement au repos ou ne reprend pas correctement, conserver DEEP_SLEEP=false et corriger la séquence avec la notice fabricant.

6. Terrain : contrôler la réception au point exact et à la hauteur prévue, avec l’antenne et le coffret finaux. Consigner plusieurs cycles, la météo et les visites de maintenance. Ne dérangez ni faune ni végétation ; obtenez l’autorisation du propriétaire ou gestionnaire, et vérifiez les règles de la zone protégée concernée.

Limites à lever avant campagne : révision effective du Stamp CatM, compilation Arduino cible, essais CRC/radio réels, dimensionnement batterie et confidentialité du lien. La présente livraison apporte les fichiers et la méthode, pas une certification du montage.

Les fichiers pour le banc de test

Sources complètes à configurer, sans identifiants et sans prétendre à une validation matérielle.

Programme Arduino completCollecteur Python
Lire le programme Arduino
/* Quiet Bee - SHT85 + ESP32 classique + SIM7080, 24/10/2026.
   UART ESP32 RX16 <- TX modem ; TX17 -> RX modem (niveaux adaptes).
   Parcours M5Stamp CatM S003 : 5V/GND, TX/RX externes adaptes 3.3V.
   POWERKEY/STATUS internes non exposes ; aucun fil de commande a ajouter.
   GPIO25 (relais) et GPIO26 (buzzer) reserves, non pilotes.
   UDP signe HMAC-SHA256, NON chiffre. Voir guide et collector.py.
   Arduino ESP32 3.x : Wire, Preferences, mbedtls sont fournis par le core.
*/
#include <Arduino.h>
#include <Wire.h>
#include <Preferences.h>
#include <esp_sleep.h>
#include <esp_system.h>
#include <mbedtls/md.h>

// Renseigner APN du portail 1NCE : sensor.net OU iot.1nce.net.
constexpr char APN[] = "";
constexpr char SERVER[] = ""; // IPv4/nom DNS du collecteur fourni, pas un site HTTP.
constexpr uint16_t PORT = 9001; // choix de ce projet, a ouvrir en UDP sur le serveur.
constexpr char DEVICE[] = "station01"; // identifiant technique, sans localisation.
constexpr char KEY[] = ""; // 64 caracteres hex aleatoires, identiques au collecteur.
constexpr bool HARDWARE_VERIFIED = false; // apres controle schema et niveaux UART.
constexpr bool DEEP_SLEEP = false; // ESP32 seul ; modem reste alimente, radio CFUN=0.
constexpr uint32_t PERIOD_MS = 600000; // choix pedagogique : 10 minutes APRES cycle.
constexpr uint32_t UART_BAUD = 115200; // a confirmer sur votre modem.
constexpr int RX_PIN=16, TX_PIN=17, SDA_PIN=21, SCL_PIN=22;
// GPIO25 : reserve commande d'alimentation ; GPIO26 : reserve buzzer.
// Ils restent en entree. POWERKEY et STATUS ne sont pas assignes.

HardwareSerial modem(2);
Preferences prefs;
enum State {DISABLED, IDLE, SENSOR_WAIT, BOOT, REG, PDP, OPEN, SEND, ACK,
            CLOSE, DEACT, RADIO_OFF, REST};
State state=DISABLED;
uint32_t due=0, cycleStart=0, regStart=0, pdpStart=0, cmdAt=0, cmdTimeout=0, backoff=60000;
uint8_t step=0, sensorTries=0;
bool busy=false, done=false, good=false, okSeen=false, prompt=false, requireData=false;
bool dataSeen=false, registered=false, pdpActive=false, socketOpen=false, acknowledged=false;
String line, response, expected, pending, packet;
uint64_t sequence=0;

bool reached(uint32_t t) { return int32_t(millis()-t)>=0; }
uint8_t crc8(const uint8_t *p, size_t n) {
  uint8_t c=0xFF;
  while(n--) {c^=*p++; for(int i=0;i<8;i++) c=(c&0x80)?(c<<1)^0x31:c<<1;}
  return c;
}
String sign(const String& value) {
  uint8_t out[32]; char hex[65];
  const auto *info=mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
  if(!info || mbedtls_md_hmac(info,(const uint8_t*)KEY,strlen(KEY),
      (const uint8_t*)value.c_str(),value.length(),out)!=0) return "";
  for(int i=0;i<32;i++) snprintf(hex+2*i,3,"%02x",out[i]);
  return String(hex);
}
void finish(bool success) { busy=false; done=true; good=success; }
void onLine(String s) {
  s.trim(); if(!s.length()) return;
  // Pas de trace brute du trafic : la cle n'est jamais envoyee ni journalisee.
  if(s.startsWith("ack|")) {
    const String body="ack|"+String(DEVICE)+"|"+String((unsigned long long)sequence);
    const String mac=sign(body), want=body+"|"+mac;
    if(mac.length()==64 && s.length()==want.length()) {
      unsigned diff=0; for(size_t i=0;i<s.length();i++) diff|=s[i]^want[i];
      if(!diff) acknowledged=true;
    }
  }
  if(!busy) return;
  if(s=="ERROR" || s.startsWith("+CME ERROR:") || s.startsWith("+CMS ERROR:")) {
    finish(false); return;
  }
  if(response.length()+s.length()<1600) response+=s+"\n";
  else {finish(false);return;}
  if(expected.length() && s.startsWith(expected)) dataSeen=true;
  if(s=="OK") okSeen=true;
  if(okSeen && (!requireData || dataSeen)) finish(true);
}
void pollModem() {
  int budget=512;
  while(modem.available() && budget-->0) {
    char c=modem.read();
    if(busy && prompt && c=='>') {
      modem.write((const uint8_t*)packet.c_str(),packet.length());
      prompt=false; line=""; continue;
    }
    if(c=='\n') {onLine(line);line="";}
    else if(c!='\r') {if(line.length()<512) line+=c;else {line="";if(busy)finish(false);}}
  }
  if(busy && millis()-cmdAt>=cmdTimeout) finish(false);
}
void command(const String& s,uint32_t timeout=10000,const String& prefix="",bool askPrompt=false) {
  response="";line="";expected=prefix;requireData=prefix.length()>0;
  okSeen=false;dataSeen=false;done=false;good=false;busy=true;prompt=askPrompt;
  cmdAt=millis();cmdTimeout=timeout;modem.print(s);modem.print('\r');
}
void enter(State s) {state=s;step=0;done=false;due=millis();}
void rest(uint32_t waitMs) {
  if(DEEP_SLEEP) { Serial.flush();esp_sleep_enable_timer_wakeup(uint64_t(waitMs)*1000ULL);esp_deep_sleep_start(); }
  enter(REST);due=millis()+waitMs;
}
void failed() {
  Serial.println("Cycle interrompu : mesure conservee ; nouvelle tentative differee.");
  busy=false;enter(CLOSE); // nettoyage best effort puis radio reduite.
}
void sensorStart() {
  Wire.beginTransmission(0x44);Wire.write(0x24);Wire.write(0x00);
  if(Wire.endTransmission()!=0) {
    if(++sensorTries<3) {due=millis()+100;return;}
    Serial.println("SHT85 absent : aucune valeur fabriquee.");rest(PERIOD_MS);return;
  }
  enter(SENSOR_WAIT);due=millis()+20;
}
void saveSample() {
  uint8_t b[6];
  bool valid=Wire.requestFrom(uint8_t(0x44),uint8_t(6))==6;
  if(valid) for(auto &v:b) v=Wire.read();
  if(!valid || crc8(b,2)!=b[2] || crc8(b+3,2)!=b[5]) {
    Serial.println("Lecture SHT85 invalide (I2C/CRC).");
    if(++sensorTries<3) {enter(IDLE);due=millis()+100;}else rest(PERIOD_MS);
    return;
  }
  float t=-45.0f+175.0f*((b[0]<<8)|b[1])/65535.0f;
  float h=100.0f*((b[3]<<8)|b[4])/65535.0f;
  sequence=prefs.getULong64("seq",0)+1;
  if(!prefs.putULong64("seq",sequence)) {rest(PERIOD_MS);return;}
  pending="v1|"+String(DEVICE)+"|"+String((unsigned long long)sequence)+"|"+String(t,2)+"|"+String(h,2);
  if(!prefs.putString("pending",pending)) {pending="";rest(PERIOD_MS);return;}
  Serial.printf("Mesure valide : %.2f C, %.2f %%HR\n",t,h);
  enter(BOOT);
}
void setup() {
  Serial.begin(115200);pinMode(25,INPUT);pinMode(26,INPUT);
  bool configOK=HARDWARE_VERIFIED && strlen(APN)>0 && strlen(SERVER)>0 && strlen(SERVER)<=64 && strlen(KEY)==64;
  for(const char *p=KEY;*p;p++) if(!isxdigit((unsigned char)*p)) configOK=false;
  for(const char *p=APN;*p;p++) if(!isalnum((unsigned char)*p)&&*p!='.'&&*p!='-') configOK=false;
  for(const char *p=SERVER;*p;p++) if(!isalnum((unsigned char)*p)&&*p!='.'&&*p!='-') configOK=false;
  for(const char *p=DEVICE;*p;p++) if(!isalnum((unsigned char)*p)&&*p!='_'&&*p!='-') configOK=false;
  if(strlen(DEVICE)<1 || strlen(DEVICE)>32)configOK=false;
  if(!configOK) {Serial.println("Configurer carte, APN, collecteur et cle avant utilisation.");return;}
  Wire.begin(SDA_PIN,SCL_PIN);Wire.setClock(100000);Wire.setTimeOut(50);
  modem.begin(UART_BAUD,SERIAL_8N1,RX_PIN,TX_PIN);modem.setTimeout(50);
  if(!prefs.begin("terrain",false)) {Serial.println("NVS indisponible.");return;}
  pending=prefs.getString("pending","");sequence=prefs.getULong64("seq",0);
  backoff=constrain(prefs.getUInt("backoff",60000),uint32_t(60000),uint32_t(1800000));
  cycleStart=millis();enter(pending.length()?BOOT:IDLE);due=millis()+50;
}
void loop() {
  pollModem(); if(state==DISABLED)return;
  if(state>=BOOT && state<=ACK && millis()-cycleStart>240000) {failed();return;}
  if(busy)return;
  if(state==IDLE) {if(reached(due))sensorStart();return;}
  if(state==SENSOR_WAIT) {if(reached(due))saveSample();return;}
  if(state==BOOT) {
    if(done) {
      if(!good || (step==4 && response.indexOf("+CPIN: READY")<0)) {failed();return;}
      done=false;step++;
    }
    switch(step) {
      case 0:command("AT");break;
      case 1:command("ATE0");break;
      case 2:command("AT+CMEE=2");break;
      case 3:command("AT+CFUN=1",60000);break;
      case 4:command("AT+CPIN?",10000,"+CPIN:");break;
      case 5:command("AT+CNMP=38");break;
      case 6:command("AT+CMNB=1");break;
      case 7:command("AT+CEREG=0");break; // interrogation, pas d'URC ambigue.
      case 8:command("AT+CGDCONT=1,\"IP\",\""+String(APN)+"\"");break;
      case 9:command("AT+CNCFG=0,1,\""+String(APN)+"\",\"\",\"\",1");break;
      default:regStart=millis();enter(REG);break;
    }
    return;
  }
  if(state==REG) {
    if(done) {
      int n=-1,stat=-1;int at=response.indexOf("+CEREG:");
      if(good && at>=0)sscanf(response.c_str()+at,"+CEREG: %d,%d",&n,&stat);
      done=false;registered=stat==1||stat==5;
      if(registered){pdpStart=millis();enter(PDP);return;}
      if(stat==3 || millis()-regStart>180000){failed();return;}
      due=millis()+3000;
    }
    if(reached(due))command("AT+CEREG?",10000,"+CEREG:");return;
  }
  if(state==PDP) {
    if(done) {
      if(!good){failed();return;}done=false;
      if(step==1) {
        pdpActive=response.indexOf("+CNACT: 0,1,\"")>=0 && response.indexOf("+CNACT: 0,1,\"0.0.0.0\"")<0;
        if(pdpActive){enter(OPEN);return;}
        if(millis()-pdpStart>90000){failed();return;}
        due=millis()+2000;
      } else {
        step=1;due=millis()+500;
      }
    }
    if(step==0)command("AT+CNACT=0,1",60000);
    else if(reached(due))command("AT+CNACT?",10000,"+CNACT:");return;
  }
  if(state==OPEN) {
    if(done){int cid=-1,result=-1;int at=response.indexOf("+CAOPEN:");if(at>=0)sscanf(response.c_str()+at,"+CAOPEN: %d,%d",&cid,&result);socketOpen=good&&cid==0&&result==0;if(!socketOpen){failed();return;}enter(SEND);return;}
    command("AT+CAOPEN=0,0,\"UDP\",\""+String(SERVER)+"\","+String(PORT)+",1",60000,"+CAOPEN:");return;
  }
  if(state==SEND) {
    if(done){if(!good){failed();return;}enter(ACK);due=millis()+15000;return;}
    String mac=sign(pending);if(mac.length()!=64){failed();return;}
    packet=pending+"|"+mac+"\n";
    command("AT+CASEND=0,"+String(packet.length())+",5000",15000,"",true);return;
  }
  if(state==ACK) {
    if(acknowledged) {
      // Effacer seulement APRES ACK signe : coupure avant effacement => doublon deduplique.
      prefs.remove("pending");pending="";backoff=60000;prefs.putUInt("backoff",backoff);Serial.println("Mesure recue et confirmee par le serveur.");enter(CLOSE);
    } else if(reached(due))failed();
    return;
  }
  if(state==CLOSE) {
    if(done){enter(DEACT);return;}command("AT+CACLOSE=0",10000);return;
  }
  if(state==DEACT) {
    if(done){enter(RADIO_OFF);return;}command("AT+CNACT=0,0",20000);return;
  }
  if(state==RADIO_OFF) {
    if(done) {
      bool radioOff=good;uint32_t waitMs=acknowledged?PERIOD_MS:backoff+(esp_random()%10000);
      if(!acknowledged){backoff=min(backoff*2,uint32_t(1800000));prefs.putUInt("backoff",backoff);}
      // Pas de sommeil profond si la reduction radio n'a pas ete confirmee.
      if(DEEP_SLEEP && radioOff)rest(waitMs);
      else {enter(REST);due=millis()+waitMs;}
      return;
    }
    command("AT+CFUN=0",20000);return;
  }
  if(state==REST && reached(due)) {
    cycleStart=millis();sensorTries=0;acknowledged=false;
    registered=false;pdpActive=false;socketOpen=false;
    enter(pending.length()?BOOT:IDLE);
  }
}

À GARDER HORS CONNEXION

Le carnet de terrain en PDF · 1,99 €

Le parcours pédagogique M5Stamp CAT-M + SHT85, les tableaux, les diagnostics et les sources intégrées, dans un document imprimable. Paiement PayPal et téléchargement privé après validation manuelle. Les ventes restent fermées pour le moment, à la demande du propriétaire.

Voir le guide et les conditions →

Sources officielles

  1. 1 / SIMCom : SIM7080G et téléchargements constructeur
  2. 2 / SIMCom : manuel AT V1.07, copie distributeur
  3. 3 / SIMCom : Hardware Design V1.03, copie distributeur
  4. 4 / Sensirion : fiche SHT85, brochage et protocole
  5. 5 / 1NCE : tableau de bord et état SIM
  6. 6 / 1NCE : APN plateforme historique
  7. 7 / 1NCE : APN plateforme 2.0
  8. 8 / 1NCE : changements de plateforme
  9. 9 / Espressif : installation Arduino ESP32
  10. 10 / PayPal : PayPal.Me et montant dans la devise du profil
  11. 11 / Service Public : règles du commerce en ligne
  12. 12 / DGCCRF : rétractation et contenu numérique
  13. 13 / CNIL : information des clients
  14. 14 / 1NCE : couverture par technologie
  15. 15 / M5Stack : Stamp CatM S003 et spécifications
  16. 16 / M5Stack : schéma officiel Stamp CatM
  17. 17 / LCSC : SIM7080G nu C2943992

Module intégré M5Stamp CAT-M choisi pour le parcours simplifié. Édition du 24 octobre 2026. Les essais réels et la conformité de votre révision de carte restent à confirmer.