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

Нужна помощь Время по NTP (UDP).

Andr

New member
Здравствуйте!
Начал писать некую прогу с синхронизации времени и сразу столкнулся с проблемой. В коде ниже раз в две минуты происходит (должна происходить) синхронизация по NTP. Для этого посылаются UDP пакеты, пока не придет ответ. После перезагрузки часы идут правильно, но через некоторое время, думаю когда появляются проблемы со связью (задержки) часы начинают отставать на 2 минуты. Я далек от понимания всех тонкостей работы протокола и железа, но появилось предположение, что ответ приходит с задержкой и отправляется еще один запрос и после второго запроса приходит первый ответ, а второй ответ становится в очередь и вываливается когда вычитывается через две минуты. Но уже отправлен очередной запрос и реальное время вычитается опять через две минуты. В логе ниже после 19:6:33 отключил интернет для ESP, пакет ушел в никуда, но время вычиталось 19:10:33 (стоящее в очереди?).
Вот вопрос. Как с этим можно бороться?
Пока наблюдал вообще пришел мусор (Time: 9:28:16.0). Как с этим бороться?

Буду благодарен за подсказки по устранению этих неприятностей.

Код:
sending NTP packet...
packet received, length=48
Unix time = 1505847873
Time: 19:4:33.14843750 - 88579321
sending NTP packet...
packet received, length=48
Unix time = 1505847993
Time: 19:6:33.15234375 - 88705918
sending NTP packet...
packet received, length=48
Unix time = 1505848233
Time: 19:10:33.23046875
sending NTP packet...
packet received, length=48
Unix time = 1505848353
Time: 19:12:33.14062500
sending NTP packet...
No packet yet
sending NTP packet...
packet received, length=48
Unix time = 2085989296
Time: 9:28:16.0
sending NTP packet...
packet received, length=48
Unix time = 1505848593
Time: 19:16:33.46484375
Код:
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#define TIMEZONE 3

char ssid[] = "";  //  your network SSID (name)
char pass[] = "";       // your network password

unsigned int  localPort = 2390;      // local port to listen for UDP packets
unsigned long ntp_time = 0;
unsigned long ntp_time_sss = 0;
long  t_correct        = 0;
unsigned long cur_ms   = 0;
unsigned long ms1      = 0;
unsigned long ms2      = 10000000UL;
unsigned long t_cur    = 0;
bool          points   = true;
unsigned int err_count = 0;


IPAddress timeServerIP;
const char* ntpServerName = "0.pool.ntp.org";
const int NTP_PACKET_SIZE = 48;
byte packetBuffer[ NTP_PACKET_SIZE];
WiFiUDP udp;

void setup()
{
   pinMode(2, OUTPUT);
   WiFi.mode(WIFI_STA);
   Serial.begin(115200);
   Serial.println("");
   Serial.println("");
   Serial.print("Free Memory: ");
   Serial.println(ESP.getFreeHeap());
   if( !ConnectWiFi(AP_SSID,AP_PASS) ){
       Serial.println("Reset ESP8266 ...");
       ESP.reset();
   }
   delay(1000);
 
   udp.begin(localPort);

  lc.shutdown(0,false);// Set the brightness to a medium values
  lc.setIntensity(0,14);// and clear the display
  lc.clearDisplay(0);
 
}

void loop(){
   cur_ms       = millis();
   t_cur        = cur_ms/1000;
// Каждые 120 секунд считываем время в интернете
   if( cur_ms < ms2 || (cur_ms - ms2) >= 120000 )
   {
     // Делаем 10 попыток синхронизации с интернетом
     if( GetNTP() )
     {
       t_correct = ntp_time - millis()/1000;
       ms2       = cur_ms;
       err_count = 0;
     }
     // Если нет соединения с интернетом, перезагружаемся
     if( err_count++ > 10 )
     {
       Serial.println("NTP connect false.");
//       ESP.reset();
     }
   }
 
// Каждые 0.5 секунды выдаем время
   if( (cur_ms - ms1) >= 500 )
   {
     ms1 = cur_ms;
     ntp_time = t_cur + t_correct;
     DisplayTime();
   }
}


bool ConnectWiFi(const char *ssid, const char *pass) {
   for( int i=0; i<3; i++){
      WiFi.begin(ssid,pass);
      delay(1000);
      for( int j=0; j<12; j++ ){
          if (WiFi.status() == WL_CONNECTED) {
              Serial.print("\nWiFi connect true: ");
              Serial.print(WiFi.localIP());
              Serial.print("/");
              Serial.print(WiFi.subnetMask());
              Serial.print("/");
              Serial.println(WiFi.gatewayIP());
              return true;
          }
          delay(1000);
          Serial.print(WiFi.status());
      }
   } 
   Serial.println("\nConnect WiFi failed ...");
   return false;
}

bool GetNTP(void) {
  WiFi.hostByName(ntpServerName, timeServerIP);
  sendNTPpacket(timeServerIP);
  delay(300);
 
  int cb = udp.parsePacket();
  if (!cb) {
    Serial.println("No packet yet");
    return false;
  }
  else {
    Serial.print("packet received, length=");
    Serial.println(cb);
    udp.read(packetBuffer, NTP_PACKET_SIZE);
    unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
    unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
    ntp_time_sss = packetBuffer[44]*390625;
    unsigned long secsSince1900 = highWord << 16 | lowWord;
    const unsigned long seventyYears = 2208988800UL;
    unsigned long epoch = secsSince1900 - seventyYears;
    ntp_time = epoch + TIMEZONE*3600;  
    Serial.print("Unix time = ");
    Serial.println(ntp_time);

{
   uint16_t s = ( ntp_time )%60;
   uint16_t m = ( ntp_time/60 )%60;
   uint16_t h = ( ntp_time/3600 )%24;

   Serial.print("Time: "); Serial.print(h); Serial.print(":"); Serial.print(m); Serial.print(":"); Serial.print(s);Serial.print(".");Serial.print(ntp_time_sss);
   if (points) {Serial.print(" - "); Serial.print(millis());}
   Serial.println();
}  
  }
  return true;
}

unsigned long sendNTPpacket(IPAddress& address)
{
  Serial.println("sending NTP packet...");
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  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
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  udp.beginPacket(address, 123);
  udp.write(packetBuffer, NTP_PACKET_SIZE);
  udp.endPacket();
}
 

=AK=

New member
Никто не гарантирует надежной связи по UDP. Синхронизация с NTP должна рассматриваться как редкое и необязательное явление. Получилось синхронизоваться - ну и чудненько. Не получилось синхронизоваться - ну и фиг с ним, продолжаем считать время сами, может, в следующий раз получится.

Например, заведите свой 32-битный счетчик секунд, у него переполнение будет раз в 136 лет, и инкрементируйте его в основной петле раз в секунду. А когда удастся связаться с NTP - установите установите его на текущее время..

Кстати, вам незачем каждые две минуты ломиться на сервер. Все равно ничего за две минуты не изменится. Кварц достаточно точен, чтобы не уходить существенно даже за несколько часов или дней.
 
  • Like
Реакции: Andr

AndrF

Active member
Кстати, вам незачем каждые две минуты ломиться на сервер. Все равно ничего за две минуты не изменится. Кварц достаточно точен, чтобы не уходить существенно даже за несколько часов или дней.
Некоторые NTP-сервера за подобные извраты вроде банят.

Вроде совершенно нет проблемы поставить DS3231 и синхронизировать время раз в месяц. Но нет же - лепят самоделки, которые чрезмерно часто лезут за синхронизацией и перегружают и без того перегруженные сервера...
 

enjoynering

Well-known member
для начала почитайте стандарт RFC 5905. Все примеры работы с ntp сервером под ардуино - это унылая копипаста одно куска кода , с кучей ошибок. очень неплохо стандарт реализован в исходниках к NodeMCU.

на пример по стандарту интервал между синхронизациями должен быть кратен 2^x, те от 16сек до 36часов. короче и длинне нет смысла.

второе. это же интервал должен передаваться в теле запроса как число tau, где интервал = 2^tau. в ардуиновской копипасте тупо забито непонятное число.

и таких косяков там бесконечность.

такое чувство, что первопроходец на колене написал код, чтоб просто проверить. потом выложил в сеть. хомяки подхватили, растиражировали код и ни у дного не возникло желание проверить что же отправляет на сервер :)

ардуино головного мозга в действии.
 
  • Like
Реакции: Andr

enjoynering

Well-known member
...Вроде совершенно нет проблемы поставить DS3231 и синхронизировать время раз в месяц.
зачем он нуже, если есть встроенный таймер и команда "millis()". вам деньги некуда девать?

считайте время через "millis()" и корректируйте его каждые полчаса через NTP. Все. зачем тут DS3231?
 
  • Like
Реакции: Andr

Andr

New member
Вроде совершенно нет проблемы поставить DS3231 и синхронизировать время раз в месяц. Но нет же - лепят самоделки, которые чрезмерно часто лезут за синхронизацией и перегружают и без того перегруженные сервера...
Да конечно нет проблем! Но зачем? если есть millis()? Двухминутная синхронизация только для отладки и для понимания того, что все работает (не работает), а не ради цели перегрузить сервер. Мне и раз в месяц достаточно синхронизировать. Но вопрос остался КАК правильно это сделать, что бы быть уверенным что синхронизировались с ожидаемой погрешностью.
У меня сейчас происходит так (как я понимаю):
1)Запрос1
2)нет ответа (таймаут 0,300 сек)
3)Запрос2
4)пришел ответ на Запрос1
5)синхронизировались
6)пришел ответ на Запрос2 (сохранился в буфере?)
7)Через 2 минуты посылаем Запрос3
8)читаем буфер, который содержит ответ на Запрос2
9)синхронизировались
10)пришел ответ на Запрос3
Повторяем с п. 7)
Т.е. получается, что из буфера вычитываются данные которые пришли 2 минуты назад.
Как избежать такого? Может вычитывать буфер перед очередным запросом? Или ждать ответа не 300 мсек, а больше? Как рациональнее или правильнее? Хотя бы на словах алгоритм.
 

=AK=

New member
Но вопрос остался КАК правильно это сделать, что бы быть уверенным что синхронизировались с ожидаемой погрешностью.
...
Как избежать такого? Может вычитывать буфер перед очередным запросом? Или ждать ответа не 300 мсек, а больше? Как рациональнее или правильнее? Хотя бы на словах алгоритм.
У вас в коде запутанная и непрозрачная логика работы, куча каких-то ненужных переменных, перепасовок и т.п. Все это лишнее. Сделайте как можно проще, чтобы легко отслеживался алгоритм.

Ваш алгоритм полностью посвящен NTP. А само время вы где считаете? Повторяю, что обращения к NTP должны рассматриваться как вспомогательные, вторичные. На первом месте - самостоятельный счет времени.

millis() быстро переполняется, поэтому непосредственно не годится для счета времени. Он хорош интервалы отсчитывать. Вот с его помощью и отсчитывайте секунды. А сами секунды инкрементируйте в 32-битном счетчике ntp_time, значение которого устанавивается на правильное время, если приxодит ответ. И не надо никаких буферов. Зачем эта путаница?

У вас сейчас "блокирующий" алгоритм: послали запрос - подождали 0.3 сек - проверили ответ. Но 0.3 сек слишком мало, надо 1..2 сек. А на такое время "замирать" нельзя, надо время считать.

В основном цикле:
1. Если прошла 1 секунда, инкрементируем счетчик ntp_time.
2. Если прошло Х секунд с момента последнего обращения к NTP, то сначала вычитываем и выбрасываем что там пришло по UDP, а потом посылаем запрос на сервер.
3. Если в течении секунды после отправки (или даже двух) пришел ответ, преобразуем его и записываем в ntp_time. Если не пришел - перестаем проверять ответы.

Можете коррекцию ввести. Засеките время, когда был послан запрос к NTP, и момент, когда получен ответ. Половину этого интервала добавьте к времени, полученному от NTP, чтобы скомпенсировать время пересылки ответа от сервера к вам.

Вот когда это заработает, тогда, при желании, организуете не одиночный запрос к серверу, а несколько запросов в цикле с солидными паузами между запросами. Если ответ получен - записываете в ntp_time и выходите из цикла.
 
Последнее редактирование:
  • Like
Реакции: Andr

enjoynering

Well-known member
... millis() быстро переполняется, поэтому непосредственно не годится для счета времени.
не так уж и быстро - 4,294,967,295 милисекунд или 4,294,967 секунд или 71582 минуты или 1193 часов или 49.71 дней!!!

учитывая глючность arduino framework-ка, не каждая плата столько проживет без перезагрузки. :)

А сами секунды инкрементируйте в 32-битном счетчике ntp_time
а делать не надо. надо просто завести глобальную переменную типа uint16_t и в нее складывать сколько раз переполнился счетчик. тогда uptime, в секундах, будет вычислятся вот так:

Код:
return _overflowCounter * 4294967UL + millis()/1000;

ну и по правилам хорошего тона, после получения времени, соединение надо закрывать:

Код:
udp.flush();    //empty UDP library rxBuffer
udp.stop();     //stop client & close connection/socket

иначе у вас web socket остается открытым. это не очень актуально для esp, но очень критично для WIZnet W5100. тк у него их всго 4!


...
2. Если прошло Х секунд с момента последнего обращения к NTP, то сначала вычитываем и выбрасываем что там пришло по UDP, а потом посылаем запрос на сервер.
вместо того чтоб разобраться в проблеме вы ему советуете КОСТЫЛЬ. Типичный пример Ардуино головного мозга.
 
Последнее редактирование:
  • Like
Реакции: Andr

AndrF

Active member
зачем он нуже, если есть встроенный таймер и команда "millis()". вам деньги некуда девать?
считайте время через "millis()" и корректируйте его каждые полчаса через NTP. Все. зачем тут DS3231?
На фиг? DS3231 стоит менее 50 рублей - явно не большая потеря для семейного бюджета. Но он позволяет не лазить каждые полчаса на и без того перегруженные NTP-сервера. Батареечка, подключенная к нему, сохранит время при отключении сети, а его выход с частотой 1 Гц заводится на вход прерывания ESP-шки...
 

enjoynering

Well-known member
Но он позволяет не лазить каждые полчаса на и без того перегруженные NTP-сервера.
какая забота о серверах. може тогда и торенты качать не будем и во вконтактике сидеть? зачем сервера нагружать?

Если перегрузить NTP сервер частотой своих запросов, он вас просто отправит в временый бан. Предупредив об этом с помощью Kiss of death соббщений. Которые расписаны в стандарте. Но ардущики стандарты не читают. Стандарты придуманы для лохов.


Батареечка, подключенная к нему, сохранит время при отключении сети
а смысл его хранить если esp, головное устройство для кого все это городилось, обесточенно? как напругу дадут, то esp по NTP получит самое точное время само, без всяких RTC. так что DS3231 совершенно не нужен. я вам только что 50 рублей сэкономил. не благодарите.
 
Последнее редактирование:
  • Like
Реакции: Andr

AndrF

Active member
какая забота о серверах. може тогда и торенты качать не будем и во вконтактике сидеть? зачем сервера нагружать?
Торенты не трогать - это святое!

А нагружаются не только NTP-сервера, но и собственная сетка, в том числе электрическая, так как WiFi молотит почти постоянно и греет атмосферу. А ведь в доме может быть несколько таких часиков...

Но ардущики стандарты не читают. Стандарты придуманы для лохов.
Казалось бы - при чем тут ардуино?

а смысл его хранить если esp, головное устройство для кого все это городилось, обесточенно? как напругу дадут, то esp по NTP получит самое точное время само, без всяких RTC. так что DS3231 совершенно не нужен. я вам только что 50 рублей сэкономил. не благодарите.
Не буду. И не сэкономили вы ни фига - платки у меня давно собраны. В разработке мой вариант, кстати, проще - ну а вы еще повозитесь с календарем. Ну и будильниками...

Просто у нас разные подходы - я предпочитаю без нужды не лазить в сеть, тем более за подобной мелочью. Если говорить о просто WiFi часах, то у меня WiFi модуль в них может быть выключен на месяц и лишь разок включится для синхронизации на несколько секунд. А ведь основное потребление ESP-шки именно на нем.
 

enjoynering

Well-known member
WiFi молотит почти постоянно и греет атмосферу.
носите фольгу из шапочки, греть будет меньше

повозитесь с календарем. Ну и будильниками...
вот с этого и надо было начинать. если нужен календарь с будильником то таки даDS3231 уже не кажется избыточным.

кстати свою библиотеку NTP я написал, пакеты отправляются и принимаются без глюков. гонял 2 дня.

как раз застрял на календре, не хочу тащить в свой проект знаменитую Time либу. слишком она избыточна для меня. скоро положу на гитхаб. будет бомба.
 

pvvx

Активный участник сообщества
не так уж и быстро - 4,294,967,295 милисекунд или 4,294,967 секунд или 71582 минуты или 1193 часов или 49.71 дней!!!

учитывая глючность arduino framework-ка, не каждая плата столько проживет без перезагрузки. :)
Вы немного ошиблись... :)
Полный аппаратный счетчик ESP8266 совсем немного больше - 64 бита в us всего :)
Код:
// #define MAC_TIMER64BIT_COUNT_ADDR 0x3ff21048
//===============================================================================
// get_mac_time() = get_tsf_ap() -  TSF AP
//-------------------------------------------------------------------------------
uint64 ICACHE_FLASH_ATTR get_mac_time(void)
{
    union {
        volatile uint32 dw[2];
        uint64 dd;
    }ux;
    ets_intr_lock();
    volatile uint32 * ptr = (volatile uint32 *)MAC_TIMER64BIT_COUNT_ADDR;
    ux.dw[0] = ptr[0];
    ux.dw[1] = ptr[1];
    if(ux.dw[1] != ptr[1]) {
        ux.dw[0] = ptr[0];
        ux.dw[1] = ptr[1];
    }
    ets_intr_unlock();
    return ux.dd;
}
NTP сервер бывает и местный - в роутере. Запросы раз в минуту по WiFi погоды для сети не делают, переговоров у самого модуля с AP будет больше во много раз за данный период.
У ESP8266 WiFi сети мешают не дополнительные запросы пары пакетов в сотни байт хоть в каждую секунду, а поддержка его со стороны AP с понижением уровня всей сети (ESP8266 стар, имеет всего HT20 и нет сертификации у Альянса WiFi - т.е. ведет себя по своему, мешая другим, а не согласно спекам Альянса WiFi и имеет шумные боковые лепестки - забивает соседние каналы. Последнее сильно относиться к платам с али, т.к. нет никаких фильтров у антенны и общее разгильдяйство - лиш-бы продать).
Но самую большую проблему в WiFi создают задержки в реализации Arduino IDE для работы части WiFi - сдвигают и сбивают окно передачи beacon у AP и спящие устройства потребляют больше. На всё это производитель ESP8266, если докеряться, рекомендует применять один модуль УЫЗ8266 с одной AP (без других участников в WiFi диапазоне и области работы ESP8266). По этим и другим причинам данный чип отнесен в категорию DIY и подобласть - домашнее изучение работы с WiFi, а не для поделок... И споры что и как делает ради обучения начинающий с ESP8266 неуместны.
Хочет долбить хоть каждые 10 миллисекунды NTP - пусть долбит, главное чтобы правильно был построен алгоритм... В SDK встроен SNTP клиент и лучше обращаться к нему, а не дублировать обрезки от Arduino-писателей.

PS: cм: 3.13. SNTP APIs: "2c-esp8266_non_os_sdk_api_reference_en.pdf"
+ Arduino/sntp.h at master · esp8266/Arduino · GitHub
...

 
Последнее редактирование:
  • Like
Реакции: Andr

pvvx

Активный участник сообщества
точно. и календарь есть по адресу - Arduino/tools/sdk/lwip/src/core/sntp.c
здорово. спасибо. это очень упрощает жизнь.
Там ещё какой-то программный таймер прикручен, но не сказано как он поведет себя в режиме LIGHT sleep WiFi. Не доделан и прием адреса местного NTP по DHCP...
Но минимальный уровень, повыше и получше чем у куска раскиданного везде кода "примера" NTP из Arduino, работает.
 

Andr

New member
ну и по правилам хорошего тона, после получения времени, соединение надо закрывать:
  • udp.flush(); //empty UDP library rxBuffer
  • udp.stop(); //stop client & close connection/socket
Действительно проблема с UDP.
Изменил код - перенес udp.begin(localPort); из сетапа в цикл и добавил ваши две строки - похоже косяк определен ;) :
Код:
bool GetNTP(void) {
  udp.begin(localPort);
  WiFi.hostByName(ntpServerName, timeServerIP);
  sendNTPpacket(timeServerIP);
  delay(300);
  int cb = udp.parsePacket();
  if (!cb) {
    Serial.println("No packet yet");
    return false;
  }
  else {
    Serial.print("packet received, length=");
    Serial.println(cb);
    udp.read(packetBuffer, NTP_PACKET_SIZE);
    udp.flush();
    udp.stop();
Большое спасибо!
 

enjoynering

Well-known member
отлично и КОСТЫЛЬ не пригодился

....
2. Если прошло Х секунд с момента последнего обращения к NTP, то сначала вычитываем и выбрасываем что там пришло по UDP, а потом посылаем запрос на сервер.
щас бы нагородили огород.
 

=AK=

New member
не так уж и быстро - 4,294,967,295 милисекунд или 4,294,967 секунд или 71582 минуты или 1193 часов или 49.71 дней!!!
Ну а по мне это быстро. Связи с NTP нет пару месяцев - и время сбилось.

а делать не надо. надо просто завести глобальную переменную типа uint16_t и в нее складывать сколько раз переполнился счетчик.
Чем это лучше, чем инкрементировать глобальную uint32_t каждую секунду?

ну и по правилам хорошего тона, после получения времени, соединение надо закрывать:
То же вопрос. Чем это лучше, чем открыть соединение один раз и вычищать приемник перед отправкой запроса?
 
Сверху Снизу