quarta-feira, agosto 14, 2019

Obtendo a Data e Hora Atual de um Servidor NTP

De vez em quando precisamos da data e hora atual em um projeto com microcontrolador. Uma opção é incluir um relógio não volátil (como eu fiz aqui), mas quando temos conexão à internet podemos obter estas informações de um servidor NTP.


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.
O servidor irá responder (ou não, o protocolo UDP não garante a entrega, só a integridade) com um pacote com todos os campos preenchidos. Em particular "Transmit Timerstamp" é o timestamp do servidor quando ele montou o pacote, que (por simplificação) adotamos como nosso novo timestamp (o NTP prevê algorítimos para descontar os delays e obter uma aproximação melhor).

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
A RF4330 detalha estas consistências e mais outras.

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.

Um Exemplo

É 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.
  1. #include <ESP8266WiFi.h>  
  2. #include <WiFiUdp.h>  
  3.   
  4. #include <time.h>  
  5.   
  6. // Pacote do NTP  
  7. typedef struct {  
  8.   uint8_t ctrl;       // LI/VN/Mode  
  9.   uint8_t stratum;  
  10.   uint8_t poll;  
  11.   uint8_t precision;  
  12.   uint8_t rootDelay[4];  
  13.   uint8_t rootDispersion[4];  
  14.   uint8_t refIdent[4];  
  15.   uint8_t refTimestamp[8];  
  16.   uint8_t orgTimestamp[8];  
  17.   uint8_t recTimestamp[8];  
  18.   uint8_t txmTimestamp[8];  
  19. } PKT_NTP;  
  20.   
  21. PKT_NTP pktTx;  // pacote enviado ao servidor  
  22. PKT_NTP pktRx;  // pacore recebido do servidor  
  23.   
  24. // Controle dos tempos da tentativa  
  25. const uint32_t MIN_TENTATIVA = 30000;  
  26. const uint32_t MAX_TENTATIVA = 180000;  
  27. uint32_t intervTentativa = MIN_TENTATIVA;  
  28. uint32_t proxTentativa = 0;  
  29.   
  30. // Controle dos tempos de atualização  
  31. uint32_t ultAtualizacao = 0;  
  32. const uint32_t intervAtualizacao = 60000; // 1 minuto em milisegundos  
  33.   
  34. // Timestamp local  
  35. uint32_t timestamp = 0;   
  36.   
  37. // Configurações do WiFi  
  38. const char* ssid     = "minharede"// Nome da rede WiFi  
  39. const char* password = "segredo";   // Senha da rede WiFi  
  40.   
  41. // Servidor NTP  
  42. const char* servidorNTP = "a.ntp.br";   
  43. const int NTP_PORT = 123;  
  44. const int LOCAL_PORT = 1234;  
  45.   
  46.  // Ajuste para o fuso horário (UTC-3)  
  47. const uint32_t epochUnix = 2208988800UL;  
  48. const int fusoHorario = -10800;           // em segundos  
  49.   
  50. // Acesso ao UDP  
  51. WiFiUDP udp;  
  52.   
  53. // Iniciação  
  54. void setup() {  
  55.   Serial.begin(115200);  
  56.   WiFi.begin(ssid, password);  
  57.   sntpInit();  
  58. }  
  59.   
  60. void loop() {  
  61.   if (WiFi.status() == WL_CONNECTED) {  
  62.     sntpUpdate();  
  63.     delay (300);  
  64.   }  
  65. }  
  66.   
  67. // Iniciação do acesso ao SNTP  
  68. void sntpInit () {  
  69.   memset (&pktTx, 0, sizeof(pktTx));  
  70.   pktTx.ctrl = (4 << 3) | 3;  // Versão 4, Modo 3 (client)  
  71.   if (!udp.begin (LOCAL_PORT)) {  
  72.     Serial.println ("Erro ao iniciar UDP");  
  73.   }  
  74. }  
  75.   
  76. // Trata atualização periódica do timestamp local  
  77. void sntpUpdate() {  
  78.   if (millis() > proxTentativa) {  
  79.     uint32_t tempoDesdeAtualizacao = (millis() - ultAtualizacao) / 1000UL;  
  80.     Serial.print (timestamp+ tempoDesdeAtualizacao);  
  81.     // Envia a solicitação  
  82.     putUInt32 (pktTx.txmTimestamp, timestamp+tempoDesdeAtualizacao);  
  83.     udp.beginPacket(servidorNTP, NTP_PORT);  
  84.     udp.write((uint8_t *)&pktTx, sizeof(pktTx));  
  85.     udp.endPacket();  
  86.     // Espera a resposta  
  87.     int timeout = 0;  
  88.     int cb = 0;  
  89.     do {  
  90.       delay ( 10 );  
  91.       cb = udp.parsePacket();  
  92.       if (timeout > 100) {  
  93.         Serial.println (" - Sem resposta");  
  94.         proxTentativa = millis() + intervTentativa;  
  95.         if (intervTentativa < MAX_TENTATIVA) {  
  96.           intervTentativa += intervTentativa;  
  97.         }  
  98.         return// timeout de um segundo  
  99.       }  
  100.       timeout++;  
  101.     } while (cb == 0);  
  102.     intervTentativa = MIN_TENTATIVA;  
  103.     // Le a resposta  
  104.     udp.read((uint8_t *) &pktRx, sizeof(pktRx));  
  105.     // Consistência básica  
  106.     if (((pktRx.ctrl & 0x3F) != ( (4 << 3) | 4)) ||  
  107.         ((pktRx.ctrl & 0xC0) == (3 << 6)) ||  
  108.         (pktRx.stratum == 0) ||  
  109.         (memcmp(pktRx.orgTimestamp, pktTx.txmTimestamp, 4) != 0)) {  
  110.       Serial.print (" - Resposta invalida");  
  111.       proxTentativa = millis() + intervTentativa;  
  112.       if (intervTentativa < MAX_TENTATIVA) {  
  113.         intervTentativa += intervTentativa;  
  114.       }  
  115.       return;  
  116.     }  
  117.     // Pega o resultado  
  118.     ultAtualizacao = millis();  
  119.     proxTentativa = ultAtualizacao + intervAtualizacao;  
  120.     timestamp = getUInt32 (pktRx.txmTimestamp);  
  121.     time_t hora = sntpTime();  
  122.     Serial.print (" - ");  
  123.     Serial.print (timestamp);  
  124.     Serial.print (" - ");  
  125.     struct tm *ptm = gmtime(&hora);  
  126.     Serial.print (ptm->tm_mday);  
  127.     Serial.print ("/");  
  128.     Serial.print (ptm->tm_mon + 1);  
  129.     Serial.print ("/");  
  130.     Serial.print (ptm->tm_year + 1900);  
  131.     Serial.print (" ");  
  132.     Serial.print (ptm->tm_hour);  
  133.     Serial.print (":");  
  134.     Serial.print (ptm->tm_min);  
  135.     Serial.print (":");  
  136.     Serial.println (ptm->tm_sec);  
  137.   }  
  138. }  
  139.   
  140. // Informa a hora local no formato Unix  
  141. time_t sntpTime() {  
  142.     uint32_t tempoDesdeAtualizacao = millis() - ultAtualizacao;  
  143.     uint32_t tempoUTC = timestamp + tempoDesdeAtualizacao/1000;  
  144.     return (time_t) (tempoUTC - epochUnix + fusoHorario);  
  145. }  
  146.   
  147. // Rotinas para mover uint32_t de/para os pacotes  
  148. void putUInt32 (uint8_t *p, uint32_t val) {  
  149.   p[0] = (uint8_t) ((val >> 24) & 0xFF);  
  150.   p[1] = (uint8_t) ((val >> 16) & 0xFF);  
  151.   p[2] = (uint8_t) ((val >> 8) & 0xFF);  
  152.   p[3] = (uint8_t) (val & 0xFF);  
  153. }  
  154. uint32_t getUInt32 (uint8_t *p) {  
  155.   return (((uint32_t) p[0]) << 24) |  
  156.          (((uint32_t) p[1]) << 16) |  
  157.          (((uint32_t) p[2]) << 8) |  
  158.          ((uint32_t) p[3]);  
  159. }  

2 comentários:

agroloco disse...

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.

Daniel Quadros disse...

Já esta no github: https://github.com/dquadros/ContRegressiva