Um timestamp NTP no ESP32 para MQTT registra quando uma leitura foi realmente feita, e não quando ela chegou ao broker. Neste tutorial, você vai sincronizar o relógio do ESP32 via SNTP, formatar timestamps UTC em ISO 8601 e em epoch com milissegundos e publicá-los em JSON no broker.connectmq.com, incluindo os casos extremos que quebram implementações ingênuas.
Por que mensagens MQTT precisam de timestamp próprio
O MQTT não coloca timestamp no pacote. Um assinante sabe quando a mensagem chegou, mas isso pode ser bem diferente de quando o sensor foi lido:
- O dispositivo armazenou leituras durante uma queda de Wi-Fi e as publica depois, de uma vez.
- Uma sessão persistente entregou mensagens QoS 1 em fila a um assinante que estava offline.
- Uma mensagem retida é entregue a um novo assinante horas depois de publicada.
- A latência da rede e as retransmissões adicionam jitter que importa em eventos rápidos.
Se o seu dashboard ou banco de dados usa o horário de chegada, os quatro casos geram gráficos errados. A solução é simples: deixe o dispositivo carimbar cada leitura no momento da captura, usando um relógio sincronizado com UTC. O ESP32 tem um contador de tempo real, mas na maioria das placas não tem relógio com bateria, então após cada boot ele começa em 1º de janeiro de 1970 até o NTP corrigir.
Escolhendo o formato do timestamp
| Formato | Exemplo | Tamanho | Melhor para |
|---|---|---|---|
| ISO 8601 UTC com ms | 2026-10-16T09:41:07.382Z |
24 caracteres | Logs legíveis, a maioria dos consumidores JSON |
| Epoch em segundos | 1792143667 |
10 dígitos | Payloads compactos, telemetria de baixa resolução |
| Epoch em milissegundos | 1792143667382 |
13 dígitos | Bancos de séries temporais, ordenação de eventos |
| Horário local com offset | 2026-10-16T11:41:07+02:00 |
25 caracteres | Exibição vinculada a um único local |
Use UTC na transmissão. Converta para o horário local no dashboard, onde o fuso horário de quem visualiza é conhecido. Timestamps locais nos dispositivos causam horas duplicadas ou ausentes duas vezes por ano, quando muda o horário de verão. O sketch abaixo publica ISO 8601 e epoch em milissegundos para você escolher depois.
Pré-requisitos
- Uma placa ESP32 com Arduino core 3.x (o core 2.x também funciona para este sketch).
- PubSubClient de Nick O'Leary.
- Porta UDP 123 de saída liberada na sua rede para NTP.
mosquitto_subpara testes.
O sketch de timestamp NTP no ESP32 para 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"; // torne o id único
const time_t VALID_EPOCH = 1700000000; // qualquer valor anterior significa "ainda não sincronizado"
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);
}
// Escreve, por exemplo, 2026-10-16T09:41:07.382Z; retorna false se o relógio não for válido
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");
// Espera até 15 s pela primeira sincronização, mas nunca bloqueia para sempre
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. Captura a leitura e o timestamp juntos
float temperature = 21.5f + (esp_random() % 300) / 100.0f;
char iso[32];
uint64_t epochMs = 0;
bool valid = isoTimestamp(iso, sizeof(iso), &epochMs);
// 2. Monta o 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. Publica
mqtt.publish(TOPIC, payload);
Serial.println(payload);
}
Assine pelo seu computador para ver o resultado:
mosquitto_sub -h broker.connectmq.com -p 1883 -i ts-viewer-8r2c -t "connectmq/demo/ntp8r2c/#" -v
Cada linha fica assim: {"ts":"2026-10-16T09:41:07.382Z","tsMs":1792143667382,"timeSynced":true,"temperature":22.71}. O servidor MQTT público (gratuito) é aberto a todos e serve para testes, então publique apenas valores de demonstração, nunca dados de produção; a página dele lista seus limites.
Como o SNTP funciona no ESP32
configTime(0, 0, ...) inicia o cliente SNTP do lwIP com até três servidores e offset zero, de modo que o relógio do sistema roda em UTC. Após a primeira resposta, o ESP32 mantém a hora com seu timer interno e ressincroniza periodicamente, por padrão uma vez por hora. Você pode mudar isso com sntp_set_sync_interval(ms); intervalos menores que alguns minutos são desnecessários para telemetria e sobrecarregam os pools NTP públicos.
getLocalTime(&tm, timeoutMs) espera até que o ano seja plausível, o que é uma verificação bloqueante conveniente no boot. O callback registrado com sntp_set_time_sync_notification_cb é disparado a cada sincronização bem-sucedida, o que é útil se você quer registrar o desvio ou publicar um status "relógio ok". A API de horário do sistema subjacente está descrita no guia de horário do sistema do ESP-IDF.
Desvio do relógio entre sincronizações
O cristal principal do ESP32 é preciso o bastante para que sincronizações a cada hora mantenham o desvio bem abaixo de um segundo em telemetria comum. O deep sleep é diferente: por padrão, o timer RTC roda a partir de um oscilador RC interno que desvia muito mais. Se o seu dispositivo dorme por longos períodos, ressincronize após cada despertar ou instale um cristal externo de 32 kHz.
Tratando a janela sem sincronia
O bug de timestamp mais comum é publicar 1970-01-01T00:00:12Z logo após o boot. O sketch evita isso de três formas:
- Compara o relógio com um epoch recente conhecido (
VALID_EPOCH) antes de formatar. - Quando o relógio não é válido, envia
uptimeMsetimeSynced:falsepara que o consumidor decida o que fazer. - Não bloqueia para sempre se o NTP estiver inacessível, então o dispositivo continua reportando.
Um consumidor pode recorrer ao próprio horário de chegada nas mensagens sem sincronia ou descartá-las. Nunca deixe o dispositivo inventar um horário.
Exibindo o horário local no dispositivo
Às vezes o próprio dispositivo precisa do horário local, por exemplo para acionar um relé às 07:00 ou mostrar a hora em um pequeno display. Mantenha o payload em UTC e converta apenas para esses usos locais. A biblioteca C do ESP32 entende strings de fuso horário POSIX, que incluem as regras de horário de verão:
// Horário da Europa Central com horário de verão automático
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); // ex.: 2026-10-16T11:41:07+0200
O gmtime_r no sketch principal ignora a variável TZ, então definir um fuso para exibição local não altera os timestamps UTC que você publica. Procure a string POSIX da sua região em vez de calcular offsets manualmente; um offset fixo de +02:00 está errado durante metade do ano na maior parte da Europa.
Timestamps com buffer e tarefas
Timestamps se tornam essenciais quando as leituras vão para uma fila. Se você separa amostragem e publicação como no tutorial ESP32 FreeRTOS MQTT, armazene epochMs na struct enfileirada no momento da captura e formate o JSON apenas na tarefa de publicação. Um acúmulo enviado após uma queda então cai na posição correta de cada gráfico. Para payloads maiores, montar o JSON com uma biblioteca é menos sujeito a erros do que snprintf; veja ArduinoJson com MQTT no ESP32.
Mensagens do tipo evento também se beneficiam. Um valor retido de "visto por último" com timestamp real informa a qualquer novo assinante exatamente quando algo aconteceu pela última vez; o projeto com sensor de movimento PIR usa esse padrão no topic de último movimento.
Checklist de solução de problemas
| Sintoma | Causa provável | Solução |
|---|---|---|
| Ano 1970 nos payloads | Publicado antes da primeira sincronização | Verifique VALID_EPOCH antes de formatar |
getLocalTime sempre dá timeout |
UDP 123 bloqueada | Use um servidor NTP na sua LAN ou peça à TI para liberar |
| Timestamps com uma hora de diferença | Fuso horário aplicado duas vezes | Mantenha o dispositivo em UTC e converta só nos dashboards |
Milissegundos sempre .000 |
Formatado a partir de time() em vez de gettimeofday() |
Use struct timeval |
| Horário errado após deep sleep | Desvio do RTC durante o sono | Ressincronize após despertar ou adicione um cristal de 32 kHz |
%llu imprime lixo |
Cast errado | Faça cast para unsigned long long |
Configuração para produção
Quando for além dos testes, crie um broker em app.connectmq.com, passe para WiFiClientSecure na porta 8883 com o certificado ISRG Root X1 e conecte com YOUR_CLIENT_ID, YOUR_USERNAME e YOUR_PASSWORD do portal. Note que um horário sincronizado também é boa prática para TLS: a validação de certificados e a correlação de logs dependem de um relógio correto. O explorador de topics ao vivo do portal do ConnectMQ mostra então suas mensagens com timestamp à medida que chegam, o que facilita identificar um dispositivo cujo ts está atrasado.
Comece com o ConnectMQ
Timestamps precisos nos dispositivos mais um broker confiável resultam em dados em que você pode confiar. O ConnectMQ é um broker MQTT gerenciado com TLS, MQTT 3.1.1 e 5.0 e um plano gratuito Serverless que não exige cartão de crédito. Crie um broker, grave este sketch com suas credenciais e explore o restante da série tutorial de IoT com ESP32 para projetos com sensores que partem daqui.
Perguntas frequentes
O broker MQTT adiciona timestamp às mensagens?
O protocolo MQTT não tem campo de timestamp, então os assinantes só sabem o horário de chegada. Coloque o horário de captura no payload, ou em uma user property do MQTT 5, a partir de um relógio sincronizado no dispositivo.
Devo enviar ISO 8601 ou epoch em milissegundos?
ISO 8601 em UTC é legível e amplamente interpretado; epoch em milissegundos é compacto e ordena naturalmente. Muitos projetos enviam os dois durante o desenvolvimento e mantêm um em produção.
Com que frequência o ESP32 deve ressincronizar com NTP?
A ressincronização padrão a cada hora serve para a maioria das telemetrias. Ressincronize após longos períodos de deep sleep, porque o relógio do modo sleep desvia mais que o cristal principal.
E se meu dispositivo não tiver acesso à internet para NTP?
Rode um servidor NTP em um gateway ou roteador local e aponte o configTime para ele. Sem nenhuma fonte de horário, publique o uptime e deixe o consumidor registrar o horário de chegada.



