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