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