• Система автоматизации с открытым исходным кодом на базе esp8266/esp32 микроконтроллеров и приложения IoT Manager. Наша группа в Telegram

Делюсь опытом ESP8266 синхронизация времени с NTP сервером. UnixTime и конвертация времени.

Paul_B

Member
Нашел и отладил утилиты синхронизации времени с NTP сервером, конвертации времени из UNIX формата (число секунд с 01.01.1970) в обычный и обратно. Что очень полезно для программирования событий на ESP 8266. Естественно, чтобы использовать их, необходимо, чтобы ESP8266 была подключена к сети с выходом в интернет. До того, как произойдет синхронизация (может не произойти с первого раза), она пытается это делать каждые 10 секунд, после этого - раз в сутки, т.к. есть встроенные часы по таймеру Tcker.h

Также прикладываю утилиты конвертации IP адреса в/из строкового формата.

Код:
#include <WiFiUdp.h>
#include <Ticker.h>

/*******************настройки для NTP-синхронизации времени****************/
#define GMT 3 
bool AskNTPTime=false;
unsigned int udpPort = 2390;      // local port to listen for UDP packets
IPAddress timeServerIP;           // time.nist.gov NTP server address
const char* ntpServerName = "time.nist.gov";
const int NTP_PACKET_SIZE = 48;   // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
WiFiUDP udp;
/*******************настройки времени и даты*******************************/

uint8_t   blink_loop = 0;
uint16_t  frequency, blink_mode = 0;
uint8_t hour, minute, second, day, month, year, weekday;
uint8_t monthDays[12]={31,28,31,30,31,30,31,31,30,31,30,31};
unsigned long secsUnix;
unsigned long Tm_blink;
Ticker timer;

IPAddress ip, gateway, subnet;

void setup(void)
{ 
    Serial.begin(115200);
    Serial.println(); 
    timer.attach(1, timerTime);

   Serial.println("Starting UDP");
   udp.begin(udpPort);
   Serial.print("Local port: ");
   Serial.println(udp.localPort());
 
}
void loop() {

    if ( WiFi.status() == WL_CONNECTED)
        {
          if(AskNTPTime==true)
            {
             if(hour==0 && minute==0 && second<2) AskNTPTime==false;
            }
          else  if(second%10<2) Ask_NTP_Time();           
        }
}

void timerTime()
{
  if(AskNTPTime==true) secsUnix++;  // если время синхронизировано, то отсчитываем время в секундах
 
  second++;                        // ведем реальный отсчет времени   
  if (second > 59)
     {
      second = 0;
      minute++;
      if (minute > 59)
        {
         minute = 0;
         hour++;
         if (hour > 23) 
           {
             hour = 0;
             day++;
             if(month>1) 
               if(day>monthDays[month-1]) 
                 {
                  day=1;
                  month++;
                  if(month>12) { year++; month=1; }
                 }
            } 
         }
      }     
}



String IP_to_String(IPAddress ip)
{
  return(String(ip[0])+"."+String(ip[1])+"."+String(ip[2])+"."+String(ip[3]));
}


IPAddress String_to_IP(String strIP)
{
int Parts[4] = {0,0,0,0};
int Part = 0;
for ( byte i=0; i<strIP.length(); i++ )
{
  char c = strIP.charAt(i);
  if ( c == '.' )
  {
    Part++;
    continue;
  }
  if ( c<48 || c>57 ) continue; // не цифровой символ
   
  Parts[Part] *= 10;
  Parts[Part] += c - '0';
}
IPAddress ip_str( Parts[0], Parts[1], Parts[2], Parts[3] );
return(ip_str);
}


bool Ask_NTP_Time()
{ 
  int cb;
  while (udp.parsePacket() > 0) {delay(1);}
 
  WiFi.hostByName(ntpServerName, timeServerIP);
  sendNTPpacket(timeServerIP); // send an NTP packet to a time server
                               // wait to see if a reply is available
  unsigned long beginWait = millis();
  while (millis() - beginWait < 2000) 
    {
      cb = udp.parsePacket();
      if (cb >= NTP_PACKET_SIZE) 
         { 
          Serial.print("packet received, length=");
          Serial.println(cb);
                                                   // We've received a packet, read the data from it
          udp.read(packetBuffer, NTP_PACKET_SIZE); // read the packet into the buffer
     
          //the timestamp starts at byte 40 of the received packet and is four bytes,
          // or two words, long. First, esxtract the two words:
          unsigned long secsSince1900;
          secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
          secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
          secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
          secsSince1900 |= (unsigned long)packetBuffer[43];
         
          Serial.print("Seconds since Jan 1 1900 = " );
          Serial.println(secsSince1900);
         
          // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
          const unsigned long seventyYears = 2208988800UL;
          // subtract seventy years:
         
          secsUnix = secsSince1900 - seventyYears;
         
          // print Unix time:
          Serial.print("Unix time = ");
          Serial.println(secsUnix);
          Unix_to_GMT(secsUnix);
               
         
          AskNTPTime=true; // признак того, что время синхронизировано
          // выдерживаем паузу
          while (millis() - beginWait < 3000) {delay(10);}
         
          Serial.print("The UTC time is ");       // UTC is the time at Greenwich Meridian (GMT)
          Serial.println(Time_to_String());
               
          return(true);
        }
    }   
  Serial.println("No packet yet");
  while (millis() - beginWait < 1000) {delay(10);}
  return(false); 
  // wait ten seconds before asking for the time again
}


// send an NTP request to the time server at the given address
unsigned long sendNTPpacket(IPAddress & address)
{
  Serial.println("sending NTP packet...IP="+IP_to_String(address));
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  udp.beginPacket(address, 123); //NTP requests are to port 123
  udp.write(packetBuffer, NTP_PACKET_SIZE);
  udp.endPacket();
 
}

void Unix_to_GMT(unsigned long epoch)
{
 // корректировка часового пояса и синхронизация 
  epoch = epoch + GMT * 3600;     
 
  second=epoch%60;
  epoch/=60; // now it is minutes
  minute=epoch%60;
  epoch/=60; // now it is hours
  hour=epoch%24;
  epoch/=24; // now it is days
  weekday=(epoch+4)%7; // day week, 0-sunday
  year=70; 
  int days=0;
  while(days + ((year % 4) ? 365 : 366) <= epoch) {
     days += (year % 4) ? 365 : 366;
     year++;
  }
  epoch -= days; // now it is days in this year, starting at 0
 
  days=0;
  month=0;
  byte monthLength=0;
  for (month=0; month<12; month++) {
    if (month==1) { // february
      if (year%4) monthLength=28;
      else monthLength=29;
    }
    else monthLength = monthDays[month];
    if (epoch>=monthLength) epoch-=monthLength;
    else break;   
  }
  month++;       // jan is month 1
  day=epoch+1;  // day of month
} 

String Time_to_String()
{
 String Tm,wd;
 int k,n;
 switch(weekday)
  {
    case 0:
         wd="su";;
         break;
    case 1:
         wd="mo";;
         break;
    case 2:
         wd="tu";;
         break;
    case 4:
         wd="th";;
         break;
    case 5:
         wd="fr";;
         break;   
    case 6:
         wd="sf";;
         break;
  }
 k=day/10;
 n=day%10;
 Tm=String(k)+String(n)+".";
 k=month/10;
 n=month%10;
 Tm=Tm+String(k)+String(n)+".";
 k=1900+year;
 Tm=Tm+String(k)+","+wd+" ";
 k=hour/10;
 n=hour%10;
 Tm=Tm+String(k)+String(n)+":";
 k=minute/10;
 n=minute%10;
 Tm=Tm+String(k)+String(n)+":";
 k=second/10;
 n=second%10;
 Tm=Tm+String(k)+String(n);
 return(Tm);
}

unsigned long GMT_to_Unix(byte y,byte mon,byte d,byte h,byte mt,byte s)
{
// считает количество секунд от текущего времени до времени  d.mon.y h:mt:s 
  unsigned long epoch=0;
  epoch+=s;
  epoch+=mt*60;
  epoch+=h*3600;
  d--;
  epoch+=d*24*3600;
  mon--;
  byte monthLength;
  for (int m=mon; m>=1; m--) {
    if (m==2) { // february
      if (year%4) monthLength=28;
      else monthLength=29;
    }
    else monthLength = monthDays[m-1];
    epoch+=monthLength*24*3600;
  }
  y--;
  int days=0;
 
  while(y>=70) {
     days += (y % 4) ? 365 : 366;
     y--;
  }
  epoch+=days*24*3600;
  epoch = epoch - GMT * 3600;
  return(epoch); 
}
 

CodeNameHawk

Moderator
Команда форума
Что из этого " ESP8266 синхронизация времени с NTP сервером. UnixTime и конвертация времени."
вы не нашли в примере? Разве что GMT_to_Unix. и конвертация в стринг.
Нашел и отладил утилиты синхронизации времени с NTP сервером
А чем не устроили утилиты синхронизации из примера, в что в них изменили?
Насколько отличается время из timerTime() и из сети за сутки?
(Есть подозрения, что если есп сильно занята, может набегать погрешность)
 

pvvx

Активный участник сообщества
Насколько отличается время из timerTime() и из сети за сутки?
На много, если используется режимы WiFi sleep (MODEM, LIGTH) или Deep-Sleep. По умолчанию, если вы включили только Station, то активен режим понижения потребления WiFi = MODEM. При всех sleep таймеры и прерывания у ESP8266 не работают, а работает только кривой RC-генератор в RTC.
Зависит от калибровки RC-генератора производимой один раз при старте SDK (+ в SDK есть специальные функции калибровки) и погоды (температуры и напряжения питания).
 

enjoynering

Well-known member
Т.е. в ардуино в режиме station only прерывания аля atachInterrupt() работать не будут? И таймер повышенный на mills() тоже?
 

pvvx

Активный участник сообщества
Т.е. в ардуино в режиме station only прерывания аля atachInterrupt() работать не будут? И таймер повышенный на mills() тоже?
Это смотря какой включен режим пониженного потребления у WiFi. Их там три: NONE, MODEM, LIGTH.
Если NONE - то всё работает. Если LIGTH, то будет дергаться...
Зачем вообще на модуле иметь время? Обычно с модуля пересылают что, а сервер или браузер имеет время приема.
При старте запросили NTP и запомнили. Если надо показать время старта - переслали это дикое число в броузер - он и распечатает через javascript с учетом всего на свете и вычислит сколько прошло, рассчитает поправку, перешлет модулю и т.д...
HTML:
<script type="text/javascript">
var x = ~sntp_time~*1000;
if(x){
var d = new Date(x);
document.getElementById('sntptime').innerHTML= d.toLocaleDateString()+" "+d.toLocaleTimeString();
}
</script>
Запрос у удаленного NTP всё равно не точен (тем более по примеру в данной теме :) ), без специальных алго фильтров и сотней опросов, и при единичном запросе его колбасит обычно на +-2 сек.

Вот древняя тема, если требуется синхронизация даже нескольких ESP8266 с точностью 20 мкс без фильтров и +-2 мкс с фильтрами.

Зачем вообще зависучей игрушке время? :)
Напишите в поиске форума "синхронизация" - только по этому слову выпадут десятки тем и скетчей как получить и обработать время по NTP. Каждый, вновь заходящий, без поиска пишет новую тему и новый глюко-скетч, когда в SDK всё строено и вызывается парой команд, да в протоколе WiFi уже есть синхронизация, точность которой и не снилась NTP...
 
Последнее редактирование:

shuraf

Member
Есть же возможность sdk-ашные возможности синхронизации ntp перенести в ардуино-скетч?
 
Последнее редактирование:

pvvx

Активный участник сообщества
Есть пример синхронизации из WiFi без NTP?
А на чем построены графики расхождения кварцев за отсчеты в 0.1 сек от температуры роутера и ESP в мкс ?
Всё в web-свалке, вам надо какую-то опцию включить при сборке проекта (забыл уже, т.к. у меня проектов много, а это было давно) и описать страничку HTML приема отсчетов c отображением в график на javascript и закинуть на диск web ESP. Тогда в любом броузере будет создавать аналогичный график :) Т.е. в данном случае такой синхронизации хватает для вывода звука на разнесенные колонки и триангуляции на источник звука в пару метров (с учетом скорости ветра) :)

Для отображения часов обычно ставят GPS и используют стандартный её выход строба секундных отсчетов. Но на Arduino невозможно даже с программным ФАПЧ получить точность внешних событий (изменений на GPIO, хоть по прерыванию) лучше 50 us из-за биений с запретами прерываний в системе.
Если поставите внешний MCU, то с бытовых GPS модулей и программным ФАПЧ сможете синхронизовать только вставку кадров (переключение на рекламу без срыва синхро) в транслируемое аналоговое телевидение и то с биениями... :)
 
Последнее редактирование:

gerkimuyda

New member
стандартные arduino примеры об этом не знают. :) Вы бы взяли и рассказали как надо в картинках.
Как гласит народная мудрость: - Сказавши "а", надо сказать и "б" ... :):):)
:D Так там ведь все просто: Arduino/sntp.h и Arduino/sntp.c
Делаем [inline]#include "lwip/sntp.h"[/inline], а потом:
Код:
sntp_stop();
sntp_setservername(0,"ntp.time.in.ua");
sntp_setservername(1,"pool.ntp.org");
sntp_set_timezone(2);
sntp_init();
Вуаля. И остается только получать время [inline]uint32 Time = sntp_get_current_timestamp()[/inline] o_O
ps1: системе надо секунду-две для синхронизации в фоне, прежде чем функция начнет отдавать значение отличное от нуля.
ps2: [inline]Serial.print( sntp_get_real_time(sntp_get_current_timestamp()) );[/inline]
ps3: Синхронизация раз в час.
ps4: на всякий случай, как всегда, предупреждаю, что это с последней версией библиотеки. Что творилось в прошлых - не знаю.
https://github.com/esp8266/Arduino/releases/download/2.4.0-rc2/package_esp8266com_index.json
 
Последнее редактирование:
  • Like
Реакции: kab

kab

New member
@gerkimuyda
Не сочтите за наглость, но, может у Вас получится переложить на язык Arduino мысли @pvvx по поводу синхронизации времени без интернета, а only используя время из WiFi?
 

gerkimuyda

New member
kab, у меня вместо приведенных выше серверов прописан мой роутер, на который, естественно, возложена миссия ntp-сервера и клиента, заодно и в его dns-ах статикой прописаны его ип на всякие time.windows.net и т.д. o_O
Еще в раздаваемых dhcp-опциях указан ntp-сервер, но использовать ntp из dhcp-настроек в esp8266 у меня сходу не получилось. Да и я особо и не разбирался. Там еще опции #define перед компиляцией менять надо и специальную функцию вызывать... В общем, для "хомячков" не пригодный путь :D Да и альтернатива ему - использовать ntp-сервером IP, полученный как "шлюз по умолчанию" (т.е. сам роутер - это в большинстве случаев так и обстоит на самом деле). :rolleyes:
Зачем усложнять, если проще все локальные устройства синхронизировать по одному времени (с один локальным ntp), а для timestamp в БД использовать локальную метку времени, которая легко отслеживает timezone и переход на летнее/зимнее время.
А вы пробовали полученное от одного клиента будущее значение времени из одной таймзоны, сохранить универсально на сервере (в другой таймзоне), чтобы другому клиенту отдать в его (третей) таймзоне? С учетом того, что к моменту наступления этого будущего времени сервер и первый клиент может уже сменить летнее/зимнее время, а для второго клиента этот переход еще не наступил? o_O
Вы любите теоретические задачки - Вам понравится. :) (А я сейчас отдыхаю и не хочу напрягаться. Подобная ситуация у меня была в связке php+javascript)
 

pvvx

Активный участник сообщества
Не сочтите за наглость, но, может у Вас получится переложить на язык Arduino мысли @pvvx по поводу синхронизации времени без интернета, а only используя время из WiFi?
Там ничего сложного нет, кроме борьбы с самой концепцией Arduino IDE, её распределению системных процессов и организации делать всё полингом, а не по событиям. И надо патчить либу SDK, где уже описано...
Как оптимальная модификация, является установка на роутер системы синхронизации с внешним временем его TSF. Тогда все устройства в сети будут знать точное время каждый beacon (каждую 0.1 секунду) и не тратить на это время и ресурсы. Протокол WiFi позволяет это, иметь в счетчике TSF хоть текущее Московское время с шагом в мкс.
Так-же, в некоторых роутерах есть свой NTP, который определяется по DHCP. Делается это для того, что с дальним сервером синхронизация скачет, а в местной интрасети задержки минимальны и точность местного NTP лучше.
Еще в раздаваемых dhcp-опциях указан ntp-сервер, но использовать ntp из dhcp-настроек в esp8266 у меня сходу не получилось. Да и я особо и не разбирался. Там еще опции #define перед компиляцией менять надо и специальную функцию вызывать... В общем, для "хомячков" не пригодный путь :D
Это есть в web-свалке, когда-то было реализовано "по просьбе трудящихся" :)
Какие-то наброски проявлялись в Arduino, но я не особо слежу за ними и не знаю - доделали ли это там :) Ведь для этого надо вставить до десятка строк, т.е. очень и оочень сложно, при наличии десятков примеров в сети :) :)
 
Последнее редактирование:

gerkimuyda

New member
вы бы объяснили хомячкам, что у вас OpenWrt стоит для этих целей.
Нет. Обычный Mikrotik, правда на нем можно раскатать OpenWrt, но я этого не делал. А NTP-сервер есть и в мыльницах типа D-Lint, TP-Link и т.д. Просто внимательнее посмотрите настройки или документацию. Да если у вас хороший инет - просто пингами определите самый ближайший ntp-сервер к вам и по нему синхронизуйтесь... Может у провайдера он молча установлен?
 
Сверху Снизу