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.
#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:

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