NTP e SNTP
O NTP (Network Time Protocol) é um protocolo de sincronização de relógios, implementado sobre o UDP . Foi aperfeiçoado ao longo dos anos, a versão atual é a 4 e a especificação formal está na RFC5905. Implementado de forma completa pode ser obtida uma sincronização da ordem de milissegundos.
O NSTP (Simple Network Time Protocol) é um subconjunto do NTP, que usa o mesmo formato de pacotes porém ignora alguns campos e dispensa os protocolos mais sofisticados. Atualmente é também descrito na RFC5905, porém a versão anterior (RFC4330) me pareceu a mais legível de todas.
Daqui para frente vou falar no SNTP do ponto de vista do cliente, conforme a RFC4330. O formato do pacote que será enviado e recebido é o seguinte:
Os timestamps possuem 64 bits. Os 32 primeiros correspondem ao número inteiro de segundos desde 1/1/1900 às 00:00:00 horas e os 32 seguintes à parte fracionária, sempre em relação a UTC. Merece destaque que o contador de segundos irá "dar a volta" em 2036 (a versão mais atual do NTP suporta também timestamps de 128 bits para superar este limite).
Para solicitar ao servidor o timestamp atual enviamos o pacote com a maioria dos campos zerados, exceto por:
- Mode: 3 (indica cliente)
- VN: 4 (versão suportada)
- Transmit Timestamp: embora este campo possa ser zerado, é conveniente colocar aqui o timestamp local para validar a resposta.
Algumas consistências permitem descartar pacotes inválidos (pouco provável) ou que cheguem fora de ordem (o UDP não garante entrega na ordem de envio):
- Mode deve ser 4 (servidor)
- VN deve ser 4 (o mesmo que solicitamos)
- LI deve ser 0 a 2.
- Stratum e Transmit Timestamp não devem ser zero
- Originate Timestamp deve ser igual ao Transmit Timestamp da nossa solicitação
Para manter a sincronização, o processo deve ser repetido periodicamente. A RF4330 recomenda um tempo mínimo de 60 segundos entre solicitações e "proíbe" tempos menores que 14 segundos.
Biblioteca NTPClient do Arduino
A maioria dos exemplos que encontrei na internet usam esta biblioteca, escrita por Fabrice Weinberg. Ela pode ser vista no GitHub e instalada diretamente pela IDE do Arduino. Implementa na realidade um cliente SNTP (com algumas esquisitices no preenchimento da solicitação). Os métodos principais desta biblioteca são:
- Construtor, onde podem ser especificados o objeto para envio e recepção via UDP, o nome do servidor, um offset em segundos (para levar em conta o fuso horário) e o intervalo entre solicitações.
- begin(), que faz a iniciação do objeto.
- update(), que deve ser chamada no loop principal e faz a consulta ao servidor respeitando o intervalo definido no construtor
- getEpochTime() que retorna a hora local (considerando o offset definido no construtor) no formato Unix (segundos a partir de 1/1/1970 às 00:00:00)
A biblioteca tem funções para alterar os parâmetros definidos no construtor e algumas rotinas para retornar parte informações de hora (mas não de data). No lugar destas últimas, o melhor é incluir o time.h padrão do C e usar a função (maldita*) localtime para obter dia, mês, ano, hora, minuto e segundo separados a partir do tempo Unix.
* localtime comete o crime de retornar um ponteiro para um estrutura de dados interna à biblioteca, o que cria problema em caso de reentrância ou multi-threading.
É claro que não resisti de fazer a minha própria implementação do cliente SNTP. O programa abaixo é um teste que tenta comunicar com um servidor NTP a cada minuto e envia para a serial o resultado e informações adicionais. No próximo post vou usar uma versão deste código organizado em uma classe para facilitar o uso.
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <time.h>
// Pacote do NTP
typedef struct {
uint8_t ctrl; // LI/VN/Mode
uint8_t stratum;
uint8_t poll;
uint8_t precision;
uint8_t rootDelay[4];
uint8_t rootDispersion[4];
uint8_t refIdent[4];
uint8_t refTimestamp[8];
uint8_t orgTimestamp[8];
uint8_t recTimestamp[8];
uint8_t txmTimestamp[8];
} PKT_NTP;
PKT_NTP pktTx; // pacote enviado ao servidor
PKT_NTP pktRx; // pacore recebido do servidor
// Controle dos tempos da tentativa
const uint32_t MIN_TENTATIVA = 30000;
const uint32_t MAX_TENTATIVA = 180000;
uint32_t intervTentativa = MIN_TENTATIVA;
uint32_t proxTentativa = 0;
// Controle dos tempos de atualização
uint32_t ultAtualizacao = 0;
const uint32_t intervAtualizacao = 60000; // 1 minuto em milisegundos
// Timestamp local
uint32_t timestamp = 0;
// Configurações do WiFi
const char* ssid = "minharede"; // Nome da rede WiFi
const char* password = "segredo"; // Senha da rede WiFi
// Servidor NTP
const char* servidorNTP = "a.ntp.br";
const int NTP_PORT = 123;
const int LOCAL_PORT = 1234;
// Ajuste para o fuso horário (UTC-3)
const uint32_t epochUnix = 2208988800UL;
const int fusoHorario = -10800; // em segundos
// Acesso ao UDP
WiFiUDP udp;
// Iniciação
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
sntpInit();
}
void loop() {
if (WiFi.status() == WL_CONNECTED) {
sntpUpdate();
delay (300);
}
}
// Iniciação do acesso ao SNTP
void sntpInit () {
memset (&pktTx, 0, sizeof(pktTx));
pktTx.ctrl = (4 << 3) | 3; // Versão 4, Modo 3 (client)
if (!udp.begin (LOCAL_PORT)) {
Serial.println ("Erro ao iniciar UDP");
}
}
// Trata atualização periódica do timestamp local
void sntpUpdate() {
if (millis() > proxTentativa) {
uint32_t tempoDesdeAtualizacao = (millis() - ultAtualizacao) / 1000UL;
Serial.print (timestamp+ tempoDesdeAtualizacao);
// Envia a solicitação
putUInt32 (pktTx.txmTimestamp, timestamp+tempoDesdeAtualizacao);
udp.beginPacket(servidorNTP, NTP_PORT);
udp.write((uint8_t *)&pktTx, sizeof(pktTx));
udp.endPacket();
// Espera a resposta
int timeout = 0;
int cb = 0;
do {
delay ( 10 );
cb = udp.parsePacket();
if (timeout > 100) {
Serial.println (" - Sem resposta");
proxTentativa = millis() + intervTentativa;
if (intervTentativa < MAX_TENTATIVA) {
intervTentativa += intervTentativa;
}
return; // timeout de um segundo
}
timeout++;
} while (cb == 0);
intervTentativa = MIN_TENTATIVA;
// Le a resposta
udp.read((uint8_t *) &pktRx, sizeof(pktRx));
// Consistência básica
if (((pktRx.ctrl & 0x3F) != ( (4 << 3) | 4)) ||
((pktRx.ctrl & 0xC0) == (3 << 6)) ||
(pktRx.stratum == 0) ||
(memcmp(pktRx.orgTimestamp, pktTx.txmTimestamp, 4) != 0)) {
Serial.print (" - Resposta invalida");
proxTentativa = millis() + intervTentativa;
if (intervTentativa < MAX_TENTATIVA) {
intervTentativa += intervTentativa;
}
return;
}
// Pega o resultado
ultAtualizacao = millis();
proxTentativa = ultAtualizacao + intervAtualizacao;
timestamp = getUInt32 (pktRx.txmTimestamp);
time_t hora = sntpTime();
Serial.print (" - ");
Serial.print (timestamp);
Serial.print (" - ");
struct tm *ptm = gmtime(&hora);
Serial.print (ptm->tm_mday);
Serial.print ("/");
Serial.print (ptm->tm_mon + 1);
Serial.print ("/");
Serial.print (ptm->tm_year + 1900);
Serial.print (" ");
Serial.print (ptm->tm_hour);
Serial.print (":");
Serial.print (ptm->tm_min);
Serial.print (":");
Serial.println (ptm->tm_sec);
}
}
// Informa a hora local no formato Unix
time_t sntpTime() {
uint32_t tempoDesdeAtualizacao = millis() - ultAtualizacao;
uint32_t tempoUTC = timestamp + tempoDesdeAtualizacao/1000;
return (time_t) (tempoUTC - epochUnix + fusoHorario);
}
// Rotinas para mover uint32_t de/para os pacotes
void putUInt32 (uint8_t *p, uint32_t val) {
p[0] = (uint8_t) ((val >> 24) & 0xFF);
p[1] = (uint8_t) ((val >> 16) & 0xFF);
p[2] = (uint8_t) ((val >> 8) & 0xFF);
p[3] = (uint8_t) (val & 0xFF);
}
uint32_t getUInt32 (uint8_t *p) {
return (((uint32_t) p[0]) << 24) |
(((uint32_t) p[1]) << 16) |
(((uint32_t) p[2]) << 8) |
((uint32_t) p[3]);
}

2 comentários:
muito bom, eu estou ganhando uma surra desse NTP, fiz umas aplicações a uns 3 anos atras, mas agora parou de sincronizar, precisava de um upgrade, mas a maioria dos exemplos que eu achei, inclusive usando essa biblioteca por algum motivo não sincronizam, testei varias urls tbm, mal posso esperar a versão com classes.
Já esta no github: https://github.com/dquadros/ContRegressiva
Postar um comentário