ESP32 e Arduino

Timestamp NTP no ESP32 para MQTT: Adicione Horário Preciso a Cada Mensagem

Sincronize o relógio do ESP32 com NTP, formate timestamps ISO 8601 e epoch em milissegundos e anexe-os aos payloads MQTT para que as leituras mantenham o horário real de captura mesmo quando publicadas com atraso.

Ícone de relógio sincronizando uma placa ESP32 antes de publicar JSON com timestamp NTP via MQTT
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

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_sub para 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:

  1. Compara o relógio com um epoch recente conhecido (VALID_EPOCH) antes de formatar.
  2. Quando o relógio não é válido, envia uptimeMs e timeSynced:false para que o consumidor decida o que fazer.
  3. 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.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.