Un horodatage NTP ESP32 MQTT enregistre le moment où une mesure a réellement été prise, et non celui où elle a fini par atteindre le broker. Dans ce tutoriel, vous allez synchroniser l'horloge de l'ESP32 via SNTP, formater des horodatages UTC en ISO 8601 et en millisecondes epoch, puis les publier en JSON vers broker.connectmq.com, en couvrant les cas limites qui mettent en échec les implémentations naïves.
Pourquoi les messages MQTT ont besoin de leur propre horodatage
MQTT ne place aucun horodatage dans le paquet. Un abonné sait quand un message est arrivé, mais ce moment peut être très différent de celui de la lecture du capteur :
- L'appareil a mis des mesures en tampon pendant une coupure Wi-Fi et les publie plus tard en rafale.
- Une session persistante a livré des messages QoS 1 en attente à un abonné qui était hors ligne.
- Un retained message est livré à un nouvel abonné des heures après sa publication.
- La latence réseau et les retransmissions ajoutent une gigue qui compte pour les événements rapides.
Si votre tableau de bord ou votre base de données utilise l'heure d'arrivée, ces quatre cas produisent des graphiques faux. La solution est simple : laissez l'appareil horodater chaque mesure au moment de la capture, avec une horloge synchronisée sur l'UTC. L'ESP32 possède un compteur temps réel mais, sur la plupart des cartes, aucune horloge sauvegardée par pile : après chaque démarrage, il repart du 1er janvier 1970 jusqu'à ce que NTP le corrige.
Choisir un format d'horodatage
| Format | Exemple | Taille | Idéal pour |
|---|---|---|---|
| ISO 8601 UTC avec ms | 2026-10-16T09:41:07.382Z |
24 caractères | Logs lisibles, la plupart des consommateurs JSON |
| Secondes epoch | 1792143667 |
10 chiffres | Payloads compacts, télémétrie à faible résolution |
| Millisecondes epoch | 1792143667382 |
13 chiffres | Bases de séries temporelles, ordonnancement d'événements |
| Heure locale avec décalage | 2026-10-16T11:41:07+02:00 |
25 caractères | Affichages liés à un seul site |
Utilisez l'UTC sur le réseau. Convertissez en heure locale dans le tableau de bord, là où le fuseau horaire de l'utilisateur est connu. Les horodatages locaux sur les appareils provoquent des heures dupliquées ou manquantes deux fois par an lors des changements d'heure. Le sketch ci-dessous publie à la fois l'ISO 8601 et les millisecondes epoch pour que vous puissiez choisir en aval.
Prérequis
- Une carte ESP32 avec le core Arduino 3.x (le core 2.x fonctionne aussi pour ce sketch).
- PubSubClient de Nick O'Leary.
- Le port UDP 123 sortant autorisé sur votre réseau pour NTP.
mosquitto_subpour les tests.
Le sketch d'horodatage NTP ESP32 MQTT
#include <WiFi.h>
#include <PubSubClient.h>
#include <time.h>
#include <sys/time.h>
#include "esp_sntp.h"
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* TOPIC = "connectmq/demo/ntp8r2c/telemetry"; // rendez l'identifiant unique
const time_t VALID_EPOCH = 1700000000; // toute valeur antérieure signifie « pas encore synchronisé »
WiFiClient net;
PubSubClient mqtt(net);
String clientId;
volatile bool timeSynced = false;
unsigned long lastPublish = 0;
void onTimeSync(struct timeval* tv) {
timeSynced = true;
Serial.printf("NTP sync at epoch %ld\n", (long)tv->tv_sec);
}
// Écrit par ex. 2026-10-16T09:41:07.382Z, renvoie false si l'horloge n'est pas valide
bool isoTimestamp(char* buf, size_t len, uint64_t* epochMs) {
struct timeval tv;
gettimeofday(&tv, nullptr);
if (tv.tv_sec < VALID_EPOCH) return false;
struct tm utc;
gmtime_r(&tv.tv_sec, &utc);
size_t n = strftime(buf, len, "%Y-%m-%dT%H:%M:%S", &utc);
snprintf(buf + n, len - n, ".%03ldZ", (long)(tv.tv_usec / 1000));
*epochMs = (uint64_t)tv.tv_sec * 1000ULL + tv.tv_usec / 1000;
return true;
}
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(300);
Serial.printf("Wi-Fi connected, IP %s\n", WiFi.localIP().toString().c_str());
}
void startNtp() {
sntp_set_time_sync_notification_cb(onTimeSync);
configTime(0, 0, "pool.ntp.org", "time.cloudflare.com", "time.google.com");
// Attendre jusqu'à 15 s la première synchronisation, sans jamais bloquer indéfiniment
struct tm tmp;
if (getLocalTime(&tmp, 15000)) {
Serial.println("Clock is valid");
} else {
Serial.println("NTP not reachable yet, will publish with timeSynced=false");
}
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(clientId.c_str())) {
Serial.println("MQTT connected");
} else {
Serial.printf("MQTT failed, state %d\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
clientId = "esp32-ntp-" + String((uint32_t)ESP.getEfuseMac(), HEX);
connectWifi();
startNtp();
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setBufferSize(512);
connectMqtt();
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
if (millis() - lastPublish < 5000) return;
lastPublish = millis();
// 1. Capturer la mesure et son horodatage ensemble
float temperature = 21.5f + (esp_random() % 300) / 100.0f;
char iso[32];
uint64_t epochMs = 0;
bool valid = isoTimestamp(iso, sizeof(iso), &epochMs);
// 2. Construire le payload
char payload[200];
if (valid) {
snprintf(payload, sizeof(payload),
"{\"ts\":\"%s\",\"tsMs\":%llu,\"timeSynced\":true,\"temperature\":%.2f}",
iso, (unsigned long long)epochMs, temperature);
} else {
snprintf(payload, sizeof(payload),
"{\"uptimeMs\":%lu,\"timeSynced\":false,\"temperature\":%.2f}",
millis(), temperature);
}
// 3. Publier
mqtt.publish(TOPIC, payload);
Serial.println(payload);
}
Abonnez-vous depuis votre ordinateur pour voir le résultat :
mosquitto_sub -h broker.connectmq.com -p 1883 -i ts-viewer-8r2c -t "connectmq/demo/ntp8r2c/#" -v
Chaque ligne ressemble à {"ts":"2026-10-16T09:41:07.382Z","tsMs":1792143667382,"timeSynced":true,"temperature":22.71}. Le serveur MQTT public gratuit est ouvert à tous et destiné aux tests : ne publiez que des valeurs de démonstration. Ses limites sont détaillées sur sa page.
Comment fonctionne SNTP sur l'ESP32
configTime(0, 0, ...) démarre le client SNTP de lwIP avec jusqu'à trois serveurs et un décalage nul, de sorte que l'horloge système fonctionne en UTC. Après la première réponse, l'ESP32 maintient l'heure avec son timer interne et se resynchronise périodiquement, par défaut une fois par heure. Vous pouvez modifier cet intervalle avec sntp_set_sync_interval(ms) ; descendre sous quelques minutes est inutile pour de la télémétrie et charge inutilement les pools NTP publics.
getLocalTime(&tm, timeoutMs) attend que l'année soit plausible, ce qui constitue une vérification bloquante pratique au démarrage. Le callback enregistré avec sntp_set_time_sync_notification_cb se déclenche à chaque synchronisation réussie, ce qui est utile si vous voulez journaliser la dérive ou publier un statut « horloge OK ». L'API d'heure système sous-jacente est décrite dans le guide de l'heure système d'ESP-IDF.
Dérive de l'horloge entre deux synchronisations
Le quartz principal de l'ESP32 est assez précis pour que des synchronisations horaires maintiennent la dérive bien en dessous d'une seconde pour une télémétrie ordinaire. Le deep sleep est un autre cas : par défaut, le timer RTC fonctionne sur un oscillateur RC interne qui dérive beaucoup plus. Si votre appareil dort longtemps, resynchronisez-le après chaque réveil, ou ajoutez un quartz externe de 32 kHz.
Gérer la fenêtre non synchronisée
Le bug d'horodatage le plus courant consiste à publier 1970-01-01T00:00:12Z juste après le démarrage. Le sketch l'évite de trois façons :
- Il compare l'horloge à un epoch récent connu (
VALID_EPOCH) avant de formater. - Lorsque l'horloge n'est pas valide, il envoie
uptimeMsettimeSynced:falsepour que le consommateur décide quoi faire. - Il ne bloque pas indéfiniment si NTP est injoignable : l'appareil continue donc de remonter ses mesures.
Un consommateur peut se rabattre sur sa propre heure d'arrivée pour les messages non synchronisés, ou les ignorer. Ne laissez jamais l'appareil inventer une heure.
Afficher l'heure locale sur l'appareil
Parfois, l'appareil lui-même a besoin de l'heure locale, par exemple pour commuter un relais à 07:00 ou afficher l'heure sur un petit écran. Gardez le payload en UTC et ne convertissez que pour ces usages locaux. La bibliothèque C de l'ESP32 comprend les chaînes de fuseau horaire POSIX, qui intègrent les règles de changement d'heure :
// Heure d'Europe centrale avec passage automatique à l'heure d'été
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1);
tzset();
struct tm local;
time_t now = time(nullptr);
localtime_r(&now, &local);
char buf[32];
strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S%z", &local); // par ex. 2026-10-16T11:41:07+0200
Dans le sketch principal, gmtime_r ignore la variable TZ : définir un fuseau horaire pour l'affichage local ne modifie donc pas les horodatages UTC que vous publiez. Cherchez la chaîne POSIX de votre région plutôt que de calculer les décalages à la main ; un décalage fixe de +02:00 est faux la moitié de l'année dans la majeure partie de l'Europe.
Horodatages, mise en tampon et tâches
Les horodatages deviennent indispensables dès que les mesures sont mises en file d'attente. Si vous séparez échantillonnage et publication comme dans le tutoriel ESP32 FreeRTOS MQTT, stockez epochMs dans la structure mise en queue au moment de la capture et ne formatez le JSON que dans la tâche de publication. Un arriéré vidé après une coupure se place alors au bon endroit sur chaque graphique. Pour des payloads plus volumineux, construire le JSON avec une bibliothèque est moins sujet aux erreurs que snprintf ; voir ArduinoJson avec MQTT sur l'ESP32.
Les messages de type événement en profitent aussi. Une valeur retained « vu pour la dernière fois » assortie d'un vrai horodatage indique à tout nouvel abonné exactement quand quelque chose s'est produit pour la dernière fois ; le projet de capteur de mouvement PIR utilise ce modèle pour son topic de dernier mouvement.
Liste de vérification pour le dépannage
| Symptôme | Cause probable | Correctif |
|---|---|---|
| Année 1970 dans les payloads | Publication avant la première synchronisation | Vérifier VALID_EPOCH avant de formater |
getLocalTime expire toujours |
UDP 123 bloqué | Utiliser un serveur NTP sur votre LAN ou demander à l'IT de l'autoriser |
| Horodatages décalés d'une heure | Fuseau horaire appliqué deux fois | Garder l'appareil en UTC, convertir uniquement dans les tableaux de bord |
Millisecondes toujours à .000 |
Formatage à partir de time() au lieu de gettimeofday() |
Utiliser struct timeval |
| Heure fausse après un deep sleep | Dérive du RTC pendant le sommeil | Resynchroniser au réveil ou ajouter un quartz de 32 kHz |
%llu affiche n'importe quoi |
Mauvais cast | Caster en unsigned long long |
Configuration de production
Lorsque vous dépassez le stade des tests, créez un broker sur app.connectmq.com, passez à WiFiClientSecure sur le port 8883 avec le certificat ISRG Root X1 et connectez-vous avec YOUR_CLIENT_ID, YOUR_USERNAME et YOUR_PASSWORD issus du portail. Une heure synchronisée relève aussi de la bonne hygiène pour TLS : la vérification de validité des certificats et la corrélation des logs reposent toutes deux sur une horloge correcte. L'explorateur de topics en direct du portail ConnectMQ affiche ensuite vos messages horodatés au fil de leur arrivée, ce qui permet de repérer facilement un appareil dont le ts prend du retard.
Démarrer avec ConnectMQ
Des horodatages précis côté appareil associés à un broker fiable vous donnent des données dignes de confiance. ConnectMQ est un broker MQTT managé avec TLS, MQTT 3.1.1 et 5.0, et une offre gratuite Serverless sans carte bancaire. Créez un broker, flashez ce sketch avec vos identifiants et explorez le reste de la série tutoriel IoT ESP32 pour des projets de capteurs qui s'appuient dessus.
Questions fréquentes
Le broker MQTT ajoute-t-il un horodatage aux messages ?
Le protocole MQTT n'a pas de champ d'horodatage : les abonnés ne connaissent que l'heure d'arrivée. Placez l'heure de capture dans le payload, ou dans une user property MQTT 5, à partir d'une horloge d'appareil synchronisée.
Faut-il envoyer de l'ISO 8601 ou des millisecondes epoch ?
L'ISO 8601 en UTC est lisible et largement pris en charge ; les millisecondes epoch sont compactes et se trient naturellement. Beaucoup de projets envoient les deux en développement et n'en gardent qu'un en production.
À quelle fréquence l'ESP32 doit-il se resynchroniser avec NTP ?
La resynchronisation horaire par défaut convient à la plupart des télémétries. Resynchronisez après de longues périodes de deep sleep, car l'horloge de veille dérive davantage que le quartz principal.
Que faire si mon appareil n'a pas accès à Internet pour NTP ?
Faites tourner un serveur NTP sur une passerelle ou un routeur local et pointez configTime vers lui. Sans aucune source de temps, publiez l'uptime et laissez le consommateur horodater à l'arrivée.



