Ein ESP32-NTP-MQTT-Zeitstempel hält fest, wann ein Messwert tatsächlich erfasst wurde, nicht wann er zufällig beim Broker ankam. In diesem Tutorial synchronisieren Sie die ESP32-Uhr per SNTP, formatieren UTC-Zeitstempel in ISO 8601 und Epoch-Millisekunden und veröffentlichen sie als JSON an broker.connectmq.com – einschließlich der Sonderfälle, an denen naive Implementierungen scheitern.
Warum MQTT-Nachrichten einen eigenen Zeitstempel brauchen
MQTT fügt dem Paket keinen Zeitstempel hinzu. Ein Subscriber weiß, wann eine Nachricht angekommen ist, doch das kann sich stark vom Zeitpunkt der Sensormessung unterscheiden:
- Das Gerät hat Messwerte während eines WLAN-Ausfalls gepuffert und veröffentlicht sie später gebündelt.
- Eine persistente Session hat wartende QoS-1-Nachrichten an einen Subscriber zugestellt, der offline war.
- Eine Retained Message wird einem neuen Subscriber Stunden nach der Veröffentlichung zugestellt.
- Netzwerklatenz und Wiederholungen erzeugen Jitter, der bei schnellen Ereignissen ins Gewicht fällt.
Wenn Ihr Dashboard oder Ihre Datenbank die Ankunftszeit verwendet, liefern alle vier Fälle falsche Diagramme. Die Lösung ist einfach: Das Gerät versieht jeden Messwert zum Erfassungszeitpunkt mit einem Zeitstempel aus einer auf UTC synchronisierten Uhr. Der ESP32 hat zwar einen Echtzeitzähler, auf den meisten Boards aber keine batteriegepufferte Uhr, sodass er nach jedem Start beim 1. Januar 1970 beginnt, bis NTP ihn korrigiert.
In der Fertigung wiegt das doppelt: MES, Historian und OEE-Auswertungen korrelieren Ereignisse mehrerer Maschinen und Linien zeitlich, etwa einen Temperaturanstieg mit einer Störung an der nächsten Station. Das funktioniert nur, wenn jeder Knoten im Unified Namespace (UNS) seine Messwerte mit einem gemeinsamen, auf UTC synchronisierten Zeitbezug veröffentlicht. In Werksnetzen kommt dieser Zeitbezug meist von einem internen NTP-Server statt aus dem Internet.
Das Zeitstempelformat wählen
| Format | Beispiel | Größe | Am besten geeignet für |
|---|---|---|---|
| ISO 8601 UTC mit ms | 2026-10-16T09:41:07.382Z |
24 Zeichen | Gut lesbare Logs, die meisten JSON-Konsumenten |
| Epoch-Sekunden | 1792143667 |
10 Ziffern | Kompakte Payloads, grobe Telemetrie |
| Epoch-Millisekunden | 1792143667382 |
13 Ziffern | Zeitreihendatenbanken, Reihenfolge von Ereignissen |
| Ortszeit mit Offset | 2026-10-16T11:41:07+02:00 |
25 Zeichen | Anzeigen, die an einen einzigen Standort gebunden sind |
Verwenden Sie auf der Leitung UTC. Rechnen Sie im Dashboard in Ortszeit um, wo die Zeitzone des Betrachters bekannt ist. Lokale Zeitstempel auf Geräten führen zweimal im Jahr bei der Sommerzeitumstellung zu doppelten oder fehlenden Stunden. Der folgende Sketch veröffentlicht sowohl ISO 8601 als auch Epoch-Millisekunden, damit Sie nachgelagert wählen können.
Voraussetzungen
- Ein ESP32-Board mit Arduino Core 3.x (Core 2.x funktioniert für diesen Sketch ebenfalls).
- PubSubClient von Nick O'Leary.
- Ausgehender UDP-Port 123 für NTP in Ihrem Netzwerk freigegeben.
mosquitto_subzum Testen.
Der ESP32-NTP-MQTT-Zeitstempel-Sketch
#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"; // ID eindeutig machen
const time_t VALID_EPOCH = 1700000000; // alles davor heißt „noch nicht synchronisiert“
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);
}
// Schreibt z. B. 2026-10-16T09:41:07.382Z, gibt false zurück, wenn die Uhr ungültig ist
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");
// Bis zu 15 s auf die erste Synchronisierung warten, aber nie endlos blockieren
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. Messwert und Zeitstempel gemeinsam erfassen
float temperature = 21.5f + (esp_random() % 300) / 100.0f;
char iso[32];
uint64_t epochMs = 0;
bool valid = isoTimestamp(iso, sizeof(iso), &epochMs);
// 2. Payload zusammensetzen
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. Veröffentlichen
mqtt.publish(TOPIC, payload);
Serial.println(payload);
}
Abonnieren Sie von Ihrem Rechner aus, um das Ergebnis zu sehen:
mosquitto_sub -h broker.connectmq.com -p 1883 -i ts-viewer-8r2c -t "connectmq/demo/ntp8r2c/#" -v
Jede Zeile sieht so aus: {"ts":"2026-10-16T09:41:07.382Z","tsMs":1792143667382,"timeSynced":true,"temperature":22.71}. Der öffentliche MQTT-Broker ist für alle offen und für Tests und Prototypen gedacht, veröffentlichen Sie also nur Demowerte. Seine Grenzen sind auf der Seite zum öffentlichen MQTT-Broker aufgeführt.
So funktioniert SNTP auf dem ESP32
configTime(0, 0, ...) startet den lwIP-SNTP-Client mit bis zu drei Servern und einem Offset von null, sodass die Systemuhr in UTC läuft. Nach der ersten Antwort hält der ESP32 die Zeit mit seinem internen Timer und synchronisiert regelmäßig neu, standardmäßig einmal pro Stunde. Das können Sie mit sntp_set_sync_interval(ms) ändern; alles unter einigen Minuten ist für Telemetrie unnötig und belastet öffentliche NTP-Pools zusätzlich.
getLocalTime(&tm, timeoutMs) wartet, bis das Jahr plausibel ist – eine bequeme blockierende Prüfung beim Start. Der mit sntp_set_time_sync_notification_cb registrierte Callback wird bei jeder erfolgreichen Synchronisierung ausgelöst, was nützlich ist, wenn Sie die Drift protokollieren oder einen Status „Uhr ok“ veröffentlichen möchten. Die zugrunde liegende System-Time-API beschreibt der ESP-IDF-Leitfaden zur Systemzeit.
Uhrendrift zwischen Synchronisierungen
Der Hauptquarz des ESP32 ist genau genug, dass stündliche Synchronisierungen die Drift für normale Telemetrie deutlich unter einer Sekunde halten. Im Deep Sleep sieht es anders aus: Standardmäßig läuft der RTC-Timer dann über einen internen RC-Oszillator, der viel stärker driftet. Wenn Ihr Gerät lange schläft, synchronisieren Sie nach jedem Aufwachen neu oder bestücken Sie einen externen 32-kHz-Quarz.
Das Zeitfenster vor der Synchronisierung behandeln
Der häufigste Zeitstempel-Fehler ist die Veröffentlichung von 1970-01-01T00:00:12Z direkt nach dem Start. Der Sketch vermeidet ihn auf drei Arten:
- Er vergleicht die Uhr vor dem Formatieren mit einer bekannten, kürzlich vergangenen Epoch (
VALID_EPOCH). - Ist die Uhr ungültig, sendet er
uptimeMsundtimeSynced:false, damit der Konsument entscheiden kann, was zu tun ist. - Er blockiert nicht endlos, wenn NTP nicht erreichbar ist, sodass das Gerät trotzdem Daten meldet.
Ein Konsument kann bei nicht synchronisierten Nachrichten auf seine eigene Ankunftszeit zurückgreifen oder sie verwerfen. Lassen Sie das Gerät niemals eine Uhrzeit erfinden.
Ortszeit auf dem Gerät anzeigen
Manchmal braucht das Gerät selbst die Ortszeit, etwa um ein Relais um 07:00 Uhr zu schalten oder die Uhrzeit auf einem kleinen Display anzuzeigen. Halten Sie die Payload in UTC und rechnen Sie nur für diese lokalen Zwecke um. Die C-Bibliothek des ESP32 versteht POSIX-Zeitzonen-Strings einschließlich der Sommerzeitregeln:
// Mitteleuropäische Zeit mit automatischer Sommerzeit
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); // z. B. 2026-10-16T11:41:07+0200
gmtime_r im Haupt-Sketch ignoriert die Variable TZ, sodass das Setzen einer Zeitzone für die lokale Anzeige die veröffentlichten UTC-Zeitstempel nicht verändert. Schlagen Sie den POSIX-String für Ihre Region nach, statt Offsets von Hand zu berechnen; ein fester Offset von +02:00 ist in den meisten Teilen Europas ein halbes Jahr lang falsch.
Zeitstempel mit Pufferung und Tasks
Zeitstempel werden unverzichtbar, sobald Messwerte in Warteschlangen landen. Wenn Sie Erfassung und Veröffentlichung wie im ESP32-FreeRTOS-MQTT-Tutorial trennen, speichern Sie epochMs zum Erfassungszeitpunkt in der eingereihten Struct und formatieren das JSON erst im Publish-Task. Ein nach einem Ausfall abgearbeiteter Rückstau landet dann in jedem Diagramm an der richtigen Stelle. Bei größeren Payloads ist das Erzeugen von JSON mit einer Bibliothek weniger fehleranfällig als snprintf; siehe ArduinoJson mit MQTT auf dem ESP32.
Auch ereignisartige Nachrichten profitieren davon. Ein Retained-Wert „zuletzt gesehen“ mit echtem Zeitstempel verrät jedem neuen Subscriber genau, wann zuletzt etwas passiert ist; das Projekt mit PIR-Bewegungsmelder nutzt dieses Muster für sein Topic zur letzten Bewegung.
Checkliste zur Fehlerbehebung
| Symptom | Wahrscheinliche Ursache | Lösung |
|---|---|---|
| Jahr 1970 in Payloads | Vor der ersten Synchronisierung veröffentlicht | VALID_EPOCH vor dem Formatieren prüfen |
getLocalTime läuft immer in den Timeout |
UDP 123 blockiert | Einen NTP-Server im LAN nutzen oder die IT um Freigabe bitten |
| Zeitstempel um eine Stunde verschoben | Zeitzone doppelt angewendet | Gerät in UTC belassen, nur im Dashboard umrechnen |
Millisekunden immer .000 |
Aus time() statt aus gettimeofday() formatiert |
struct timeval verwenden |
| Falsche Zeit nach Deep Sleep | RTC-Drift während des Schlafs | Nach dem Aufwachen neu synchronisieren oder 32-kHz-Quarz ergänzen |
%llu gibt Unsinn aus |
Falscher Cast | Nach unsigned long long casten |
Einrichtung für die Produktion
Wenn Sie über das Testen hinausgehen, erstellen Sie einen Broker unter app.connectmq.com, wechseln auf WiFiClientSecure an Port 8883 mit dem Zertifikat ISRG Root X1 und verbinden sich mit YOUR_CLIENT_ID, YOUR_USERNAME und YOUR_PASSWORD aus dem Portal. Eine synchronisierte Uhr ist übrigens auch für TLS gute Praxis: Die Prüfung der Zertifikatsgültigkeit und die Korrelation von Logs hängen beide von einer korrekten Uhr ab. Der Live-Topic-Explorer im ConnectMQ-Portal zeigt Ihre Nachrichten mit Zeitstempel dann beim Eintreffen an, sodass Sie ein Gerät, dessen ts hinterherhinkt, leicht erkennen.
Starten Sie mit ConnectMQ
Genaue Gerätezeitstempel und ein zuverlässiger Broker liefern Ihnen Daten, denen Sie vertrauen können. ConnectMQ ist ein verwalteter MQTT-Broker mit TLS, MQTT 3.1.1 und 5.0 sowie einem kostenlosen Serverless-Plan ohne Kreditkarte. Erstellen Sie einen Broker, flashen Sie diesen Sketch mit Ihren Zugangsdaten und entdecken Sie in der Reihe zum ESP32-IoT-Tutorial weitere Sensorprojekte, die darauf aufbauen.
Häufig gestellte Fragen
Fügt der MQTT-Broker Nachrichten einen Zeitstempel hinzu?
Das MQTT-Protokoll hat kein Zeitstempelfeld, Subscriber kennen also nur die Ankunftszeit. Legen Sie den Erfassungszeitpunkt aus einer synchronisierten Geräteuhr in die Payload oder in eine MQTT-5-User-Property.
Sollte ich ISO 8601 oder Epoch-Millisekunden senden?
ISO 8601 in UTC ist lesbar und wird breit unterstützt; Epoch-Millisekunden sind kompakt und lassen sich natürlich sortieren. Viele Projekte senden während der Entwicklung beides und behalten in der Produktion eines davon.
Wie oft sollte sich der ESP32 per NTP neu synchronisieren?
Die standardmäßige stündliche Synchronisierung reicht für die meisten Telemetriedaten. Synchronisieren Sie nach langen Deep-Sleep-Phasen neu, weil die Schlafuhr stärker driftet als der Hauptquarz.
Was, wenn mein Gerät für NTP keinen Internetzugang hat?
Betreiben Sie einen NTP-Server auf einem lokalen Gateway oder Router und richten Sie configTime darauf aus. Ohne jede Zeitquelle veröffentlichen Sie die Uptime und lassen den Konsumenten beim Eintreffen den Zeitstempel setzen.



