Мы уже привыкли жить в глобальном информационном мире, где, с одной стороны, довольно важно знать точное время, а с другой стороны — легко его получить, достаточно настроить на компьютере NTP, да вот хотя бы просто выполнить команду типа ntpdate pool.ntp.org.
Но есть нюанс: со всеми этими замедлениями, блокировками и «белыми списками» больше нет никакой гарантии, что как раз в нужный момент они не заблокируют нам и NTP протокол, ведь известные мировые NTP-сервера вряд ли будут в белых списках, а использовать какие-нибудь другие — ну, вспоминается история пропадания некоторых доменных имен из НСДИ, и ввод платы «за доступ к часам точного времени», что бы под этим не подразумевалось.
В общем, спасение утопающих…
К тому же, как говорят, «любой, кто увидел Ардуино — рано или поздно делает часы или метеостанцию».
Простые часы мы делать не будем, а сделаем вполне IT-шный NTP-сервер.
За основу берем чип ESP8266, точнее его вариант ESP12-F. Сейчас они считаются устаревшими, все давно сбежали на ESP32, но на самом деле чип хороший, а его производительности тут более чем достаточно.
Если кто до сих пор не знал — он прекрасно программируется стандартными средствами Arduino, прошивка пишется на вполне обычном C++ (каким он был году в 99, без современных наворотов).
Общая идея следующая: настраиваем подключение к WiFi сети, создаем внутри NTP-сервер и раздаем точное время по запросам клиентов из локальной сети.
И сразу же вопрос — где его брать, это точное время?
На самом деле исходим из того, что какой-то доступ куда-то будет — просто непостоянный, примерно как в достославные времена диалапа — сначала нужно подключиться к интернету, потом поработать, потом отключиться.
А это значит, что можно использовать например всё тот же NTP, установить время, потом в течении какого-то периода пользоваться, потом периодически актуализировать его при возможности.
В стандартных пакетах для ESP уже есть «системное время» — его можно установить, затем оно «идёт» вместе с тиками процессора. Тот же самый принцип что и в любой ОС.
И тоже как в любой ОС — точность хода этого времени зависит от того, насколько «ОС» правильно оценивает частоту тактирования процессора, насколько стабильна эта частота, и т.д. В принципе, если просто иногда поправлять его — этого уже вполне достаточно.
В нормальных условиях для этого существует функция привязки к внешним NTP-серверам:
#include <time.h>configTime(0, 0, "pool.ntp.org");
Раз в 3600 секунд «ОС» будет автоматически делать запрос и устанавливать время.
Когда нужно получить теущее время — запрашиваем его:
time_t now = time(nullptr);
По факту — это обычный unixtime, время с секундах с 01.01.1970.
То есть, время тут обрабатыается с точностью до секунд. Обычно этого вполне достаточно. но ведь мы хотели сервер ТОЧНОГО времени, чтобы раздавать его по NTP, а там используются сотые и тысячные доли секунды.
Поэтому прежде всего нужно сделать свой счетчик времени, с микросекундами.
Принцип простой: при установке времени сохраняется указанное точное время и текущий счетчик микросекунд процессора, при запросе к нему добавляется количество микросекунд, прошедших между установкой и запросом, и сохраняется новое время.
Таким образом, чтобы время «шло» — его надо периодически опрашивать, точность — до микросекунд, а погрешность — в пределах времени выполнения функций запроса, около 20-30 микросекунд. Неидеально, но сойдет.
#ifndef jb_time#define jb_time#include <stdint.h>#define JB_TIME_MAX_AGE 600class JbTime {private: uint64_t _last ; // last change time uint64_t _sec ; // seconds uint32_t _usec ; // microseconds uint32_t _mark ; // mark uS for time updatepublic: bool ok; bool fresh; JbTime(){ _sec = 0; _usec = 0; _mark = 0; _last = 0; ok = false; fresh = false; } inline bool old() { if(_sec == 0) return true; if((_sec - _last) > JB_TIME_MAX_AGE) return true; return false; } inline void copy(JbTime * src) { uint64_t sec; uint32_t usec; src->gettime(&sec, &usec); _mark = micros(); _sec = sec; _usec = usec; ok = src->ok; _last = sec; } inline void settime(uint64_t sec, uint32_t usec, uint32_t mark = micros() ){ _mark = mark; _sec = sec; _sec += usec / 1000000; _usec = usec % 1000000; _usec = usec; ok = true; _last = sec; } inline void gettime(uint64_t *o_sec, uint32_t *o_usec){ if(ok){ uint32_t now = micros(); uint32_t delta = now - _mark; _mark = now; uint64_t total = (uint64_t)_usec + delta; _sec += total / 1000000; _usec = total % 1000000; *o_sec = _sec; *o_usec = _usec; }else{ *o_sec = 0; *o_usec = 0; } } inline uint64_t synced(){ return _last; }};#endif
И поскольку это класс — можно создавать несколько разных счетчиков времени, под разные цели.
Так как штатное время работало с точностью до секунд — то и запросы к NTP серверам из стандартной библиотеки выдавали результат с точностью до секунд, а нам нужно точнее.
Протокол NTP описан в RFC 5905: бинарный протокол, основанный на обмене пакетами между клиентом и сервером, где сервер передает свои данные о текущем времени.
Используется UDP, 123 порт, время считается в секундах с 1900 года.
Ключевой момент в формуле расчета сетевой задержки, так как сервер не отвечает на запрос мгновенно, и время нельзя просто так взять и получить.
Создать свой NTP-сервер тоже не очень сложно, достаточно на запрос времени отправить UDP-пакет с подготовленными данными. Время берем свое системное.
Напишем свой класс для работы с NTP, для запросов и ответов сразу:
#ifndef jb_ntp#define jb_ntp#include <ESP8266WiFi.h>#include <WiFiUdp.h>#include <time.h>#include <JbTime.h>#define ntpPort 123#define NTP_UNIX_EPOCH_DIFF 2208988800UL#define MINIMAL_UNIXTIME 1767225600UL// Структура NTP пакета (48 байт)struct NTPPacket { uint8_t li_vn_mode; // leap indicator, version, mode uint8_t stratum; // стратум сервера uint8_t poll; // интервал опроса uint8_t precision; // точность часов uint32_t rootDelay; // задержка до корневого источника uint32_t rootDispersion; // дисперсия uint32_t refId; // id источника uint32_t refTm_s; // время последней синхронизации (секунды) uint32_t refTm_f; // время последней синхронизации (дробная часть) uint32_t origTm_s; // время отправки запроса клиентом (T1) - секунды uint32_t origTm_f; // время отправки запроса клиентом (T1) - дробная часть uint32_t rxTm_s; // время получения запроса сервером (T2) - секунды uint32_t rxTm_f; // время получения запроса сервером (T2) - дробная часть uint32_t txTm_s; // время отправки ответа сервером (T3) - секунды uint32_t txTm_f; // время отправки ответа сервером (T3) - дробная часть} __attribute__((packed));#define JB_NTPCLIENT_NOINIT 1#define JB_NTPCLIENT_NODNS 2#define JB_NTPCLIENT_NOSEND 3#define JB_NTPCLIENT_NOREPLY 4#define JB_NTPCLIENT_BADPACKET 5#define JB_NTPCLIENT_ZEROTIME 6class JbNTPClient {private: WiFiUDP udp; int _error; bool _success; double _networkDelay; JbTime * systime; // преобразование 32-битного NTP времени в double (секунды + дробная часть) inline double ntpToDouble(uint32_t sec, uint32_t frac) { return sec + frac / 4294967296.0; // 2^32 } inline double ntpToDouble(uint64_t sec, uint32_t frac) { uint32_t x_sec = (uint32_t)(sec & 0xFFFFFFFF); return x_sec + frac / 4294967296.0; // 2^32 } // создание NTP-запроса void createRequest(NTPPacket &packet, uint64_t sec, uint32_t frac) { memset(&packet, 0, sizeof(NTPPacket)); packet.li_vn_mode = 0x23; // LI=0, VN=4, Mode=3 (client) uint32_t x_sec = (uint32_t)(sec & 0xFFFFFFFF); packet.txTm_s = htonl(x_sec); packet.txTm_f = htonl(frac); }public: JbNTPClient(JbTime * src) { _success = false; _error = JB_NTPCLIENT_NOINIT; _networkDelay = 0; systime = src; } bool begin() { return udp.begin(ntpPort); } WiFiUDP port() { return udp; } bool success() { return _success; } bool error() { return _error; } double netDelay() { return _networkDelay; } // основная функция запроса к NTP-серверу // заполняет mytime bool requestTime(const char* server, JbTime * mytime) { _success = false; _error = JB_NTPCLIENT_NOINIT; _networkDelay = 0; mytime->ok = false; IPAddress timeServerIP; if (!WiFi.hostByName(server, timeServerIP)) { _error = JB_NTPCLIENT_NODNS; return false; } NTPPacket packet; // T1: время отправки пакета uint64_t t1_sec = 0; uint32_t t1_usec = 0; systime->gettime(&t1_sec, &t1_usec); t1_sec += NTP_UNIX_EPOCH_DIFF; uint32_t t1_frac = (uint32_t)((t1_usec * (uint64_t)0x100000000ULL) / 1000000ULL); createRequest(packet, t1_sec, t1_frac); // отправляем запрос udp.beginPacket(timeServerIP, ntpPort); udp.write((uint8_t*)&packet, sizeof(NTPPacket)); udp.endPacket(); // ожидаем ответ с таймаутом unsigned long timeout = millis() + 2000; // 2 секунды таймаут while (udp.parsePacket() == 0) { if (millis() > timeout) { _error = JB_NTPCLIENT_NOREPLY; return false; } delay(10); } // T4: время получения ответа uint64_t t4_sec; uint32_t t4_usec; systime->gettime(&t4_sec, &t4_usec); t4_sec += NTP_UNIX_EPOCH_DIFF; uint32_t t4_frac = (uint32_t)((t4_usec * (uint64_t)0x100000000ULL) / 1000000ULL); uint32_t t4_mark = micros(); // читаем ответ int len = udp.read((uint8_t*)&packet, sizeof(NTPPacket)); if (len < sizeof(NTPPacket)) { _error = JB_NTPCLIENT_BADPACKET; return false; } uint8_t mode = packet.li_vn_mode & 0x07; if (mode != 4) { _error = JB_NTPCLIENT_BADPACKET; return false; } // извлекаем метки времени из пакета uint32_t t2_sec = ntohl(packet.rxTm_s); uint32_t t2_frac = ntohl(packet.rxTm_f); uint32_t t3_sec = ntohl(packet.txTm_s); uint32_t t3_frac = ntohl(packet.txTm_f); // проверяем, что сервер вернул валидные временные метки if (t2_sec == 0 || t3_sec == 0) { _error = JB_NTPCLIENT_ZEROTIME; return false; } // преобразуем все времена в double double t1 = ntpToDouble(t1_sec, t1_frac); double t2 = ntpToDouble(t2_sec, t2_frac); double t3 = ntpToDouble(t3_sec, t3_frac); double t4 = ntpToDouble(t4_sec, t4_frac); // задержка сети (round-trip delay) _networkDelay = (t4 - t1) - (t3 - t2); // смещение времени (offset) по формуле RFC 5905 double offset = ((t2 - t1) + (t3 - t4)) / 2; // точное текущее время в момент получения ответа (t4) double newtime = t4 + offset; // проверяем эру if(newtime < (MINIMAL_UNIXTIME + NTP_UNIX_EPOCH_DIFF)){ newtime += (1ULL<<32); } // конвертируем в Unix-время (секунды с 1970) uint64_t sec = (uint32_t)newtime - NTP_UNIX_EPOCH_DIFF; uint32_t usec = (newtime - (uint32_t)newtime) * 1000000; _success = true; _error = 0; mytime->settime(sec, usec, t4_mark); return true; } // обработка запросов клиентов если они есть void serve(){ int packetSize = udp.parsePacket(); if (packetSize) { NTPPacket packet; int len = udp.read((uint8_t*)&packet, sizeof(NTPPacket)); if (len < sizeof(NTPPacket)) { return ; } // проверка режима uint8_t mode = packet.li_vn_mode & 0x07; if (mode == 3) { // это запрос клиента if(systime->ok){ uint64_t sec; uint32_t usec; systime->gettime(&sec, &usec); sec += NTP_UNIX_EPOCH_DIFF; uint32_t frac = (uint32_t)((usec * (uint64_t)0x100000000ULL) / 1000000ULL); packet.li_vn_mode = 0b00100100; packet.stratum = 1; packet.poll = 0; packet.precision = 0xEC; packet.rootDelay = 0; packet.rootDispersion = 0; packet.refId = 0; packet.refTm_s = htonl((uint32_t)sec); packet.refTm_f = htonl(frac); packet.origTm_s = packet.txTm_s; packet.origTm_f = packet.txTm_f; packet.rxTm_s = htonl((uint32_t)sec); packet.rxTm_f = htonl(frac); packet.txTm_s = htonl((uint32_t)sec); packet.txTm_f = htonl(frac); udp.beginPacket(udp.remoteIP(), udp.remotePort()); udp.write((uint8_t*)&packet, sizeof(NTPPacket)); udp.endPacket(); } } } }};#endif
В принципе, часть задачи реализована: у нас есть счетчики точного времени, мы можем получать время по NTP с других серверов, и раздавать его сами.
Но как уже говорилось — не факт, что сервера будут доступны.
Добавим немного космоса
Есть еще один источник точного времени, о котором забывают: это GPS.
Сам принцип работы GPS основан на том, что известно точное время и время задержки поступления сигналов с разных спутников, что позволяет вычислять расстояния до них с высокой точностью и выстраивать пространственную картину, в которой место приемника точно известно.
Место сейчас не очень важно, приемник стационарный — а вот время да.
Для этого подключаем модуль GPS Neo-6M

Комментарий в Ардуино-стиле: обратите внимание, он «красненький»!
Дело в том, что есть еще другие, как правило синего цвета, но отличаются они не цветом, а наличием вывода PPS: здесь 5 контактов, один из них PPS, а там 4, и PPS нет.
Но он нам нужен.
Это довольно самодостаточный модуль, который сам пытается искать сигналы спутников, сам рассчитывает геопозицию и т.д., и выдает уже готовые результаты в виде обычного TTL serial. То есть, достоточно просто читать что он пишет — и среди строк разного типа есть такая, которая содержит текущее время с точностью до секунды, и дату.
Для этого неплохо подходит сообщение GPRMC, выглядит примерно так:
$GPRMC,050603.00,A,2236.91423,N,11403.34555,E,0.13,303.34,020126,D*7F
В данном случае —
050603 это время, 05:06:03 UTC
020126 — дата, 2026.01.02
A — данные валидны
2236.91423,N,11403.34555,E — координаты
0.13 — скорость в узлах (мили в час),
303.34 — азимут направления движения (как известно — всегда есть погрешность по скорости, а неподвижный объект “крутится” в случайных направлениях)
Для того, чтобы узнать время — нужно прочитать это сообщение, разобрать его, и вытащить дату и время.
Но время тут в секундах, а нужно ТОЧНЕЕ.
Именно для этого и нужен вывод PPS:
PPS (Pulse Per Second): это не просто «моргалка светодиодом раз в секунду», тут логика работы такая, что когда начинается очередная секунда — на этом выводе появляется импульс. Точность — единицы микросекунд. И уже после этого — в serial идут строки с новым временем, какая именно секунда началась.
Если отследить этот момент, и потом получить строку со временем — мы будем точно знать, сколько уже микросекунд прошло, и можем установить системное время максимально точно.
Для этого используем прерывание, отмечаем момент импульса — затем получаем значение секунды которая началась, а в момент запроса на время вычислим разницу и тем самым получим почти точное время.
Но есть еще один нюанс:
В наше время могут заблокировать не только NTP, но и заглушить GPS. Точнее, занимаются спуфингом, забивая эфир ложными сигналами — при этом приемник либо просто не может поймать локацию (и соответственно время), либо ловит обманку, показывая что летит где-то на высоте, с большой скоростью, и в сотне километров отсюда.
Естественно, время при этом тоже неверное.
Поэтому пришлось добавить контроль местонахождения: если высота, координаты и скорость сильно отличаются от заданных — значит, GPS врёт.
Получилось вот так:
#include <ESP8266WiFi.h>#include <time.h>#include <JbTime.h>//#define DEBUG#ifdef DEBUGvoid MqttPublish(const char * str);#endif/* add to main.inovoid GPSSetup();bool GPSGetTime(JbTime * time);void GPSLoop();*/#define REAL_ALT XXX // RMC 7#define REAL_LAT XXXX // RMC 5#define REAL_LON XXXX // RMC 3#define PIN_INT 13#define BUFLEN 200volatile bool ppsFlag = false;volatile uint32_t ppsMicros = 0;bool gpsOk = false;bool altOk = false;uint64_t sec = 0;#define X_GGA 0x00#define X_GLL 0x01#define X_GSA 0x02#define X_GSV 0x03#define X_RMC 0x04#define X_VTG 0x05void calcChecksum(uint8_t *data, uint8_t len, uint8_t &ck_a, uint8_t &ck_b) { ck_a = 0; ck_b = 0; for (uint8_t i = 0; i < len; i++) { ck_a = ck_a + data[i]; ck_b = ck_b + ck_a; }}void sendUBX(uint8_t *msg, uint8_t len) { for (uint8_t i = 0; i < len; i++) { Serial.write(msg[i]); }}void disableX(uint8_t x){ uint8_t buffer[] = { 0xB5, 0x62, // Header 0x06, 0x01, // CFG-MSG 0x03, 0x00, // Length 0xF0, 0x03, 0x00, // Disable GSV (rate = 0) 0xFD, 0x15 // Checksum }; buffer[7] = x; calcChecksum(&buffer[2], sizeof(buffer) - 4, buffer[9], buffer[10]); sendUBX(buffer, sizeof(buffer));}//-------------------------------------void ICACHE_RAM_ATTR onPPS() { ppsMicros = micros(); // фиксируем момент импульса ppsFlag = true; // отметка о событии}//-------------------------------------void GPSSetup(){ gpsOk = false; altOk = false; ppsFlag = false; ppsMicros = micros(); disableX(X_GSV); disableX(X_GSA); disableX(X_VTG); disableX(X_GLL); pinMode(PIN_INT, INPUT); attachInterrupt(digitalPinToInterrupt(PIN_INT), onPPS, RISING);}//-------------------------------------bool GPSGetTime(JbTime * time){ if(gpsOk && altOk){ uint32_t nowMicros = micros(); uint32_t usec = nowMicros - ppsMicros; if(ppsFlag){ // RMC не успел придти time->settime(sec + 1,usec); }else{ time->settime(sec, usec); } return true; } return false;}// -----------------------------------------------void parse_rmc(char * str){ char *token; char *rest = str; char *fields[20]; int count = 0; // разбор на токены while ((token = strsep(&rest, ",")) != NULL) { fields[count++] = token; } gpsOk = false; // предварительно - OK if(strcmp(fields[2],"A")==0){ double lon = 0; sscanf(fields[3], "%lf", &lon ); double lat = 0; sscanf(fields[5], "%lf", &lat ); float speed; sscanf(fields[7], "%f", &speed ); if(speed > 0.2) return; if(abs(lon - REAL_LON) > 0.03) return; if(abs(lat - REAL_LAT) > 0.03) return; // ---------------------- int hh, mm, ss; int day, mon, year; // time sscanf(fields[1], "%2d%2d%2d", &hh, &mm, &ss); // date sscanf(fields[9], "%2d%2d%2d", &day, &mon, &year); if (year < 80) year += 2000; else year += 1900; struct tm t = {0}; t.tm_year = year - 1900; t.tm_mon = mon - 1; t.tm_mday = day; t.tm_hour = hh; t.tm_min = mm; t.tm_sec = ss; if(ppsFlag){ // PPS был, отмечаем секунду sec = mktime(&t); ppsFlag = false; } // ---------------------- gpsOk = true; }}void parse_gga(char * str){ char *token; char *rest = str; char *fields[20]; int count = 0; // разбор на токены while ((token = strsep(&rest, ",")) != NULL) { fields[count++] = token; } altOk = false; float alt = 0; sscanf(fields[9], "%f", &alt ); if(abs(alt - REAL_ALT) < 50) altOk = true;}// -----------------------------------------------void GPSLoop(){ static char buffer[BUFLEN]; static int p = 0; while (Serial.available()) { char c = Serial.read(); if (c == '\n') { // конец строки if(memcmp(buffer,"$GPRMC",6)==0){ parse_rmc(buffer); } else{ //GGA - 9 высота if(memcmp(buffer,"$GPGGA",6)==0){ parse_gga(buffer); } } buffer[0] = 0; // очищаем буфер p = 0; return; // выходим сразу после одной строки } else if (c != '\r' && p < (BUFLEN - 1) ) { // игнорируем CR buffer[p] = c; p++; buffer[p] = 0; } }}
Сохраним время
Но всё это работает, пока модуль работает. Стоит отключить питание — и время пропадает, а вот сможем ли мы получить его после перезапуска — в нынешних условиях большой вопрос. Кроме того, точность хода системного времени при отсутствии синхронизации тоже под вопросом, поэтому следующий шаг — добавить RTC, “часы реального времени”. Примерно как в компьютерах — встроенный чип с батарейкой “биоса” на плате (никакому “BIOS” батарейка не нужна, это именно для RTC).
По сути это что-то вроде электронных часов, только без дисплея: в основе кварцевый резонатор на частоту 32768 Гц, из которой с помощью несложных двоичных делителей получается 1Гц, которые затем считаются, складываясь в минуты, часы и дни. Никаких микропроцессоров, простая бинарная логика — вот её-то и питает батарейка. В процессе работы можно установить системное время в RTC, и после выключения внешнего питания часы продолжают идти. При очередной загрузке из RTC можно получить текущее время и установить как системное.
По подобной схеме устроены примерно все электронные цифровые часы: если на них написано “Quartz” — это как раз про тот кварцевый резонатор на 32768 Гц. Кто с ними сталкивался — знает и ключевую проблему: время “плывёт”, часы постепенно отстают или спешат, как повезет. Причина не в качестве самой электроники часов, а в точности резонансной частоты этого резонатора, которая зависит от его физических размеров, точности изготовления на заводе, и даже от температуры — ведь при изменении температуры физические тела сжимаются или расширяются, в том числе кристаллы кварца.
Для этого придумали системы термостабилизации (изолированная коробка со стабильной температурой) и термокомпенсации (добавление или удаление “тиков” по таблицам зависимости от текущей температуры).
Поэтому, при выборе варианта RTC вместо, например, DS1301 (простые часы с обычным “часовым” резонатором) лучше выбирать DS3231, где кристалл встроен в микросхему, и имеется готовая система термокомпенсации. У такого модуля стабильность хода намного выше, а значит, что и время он будет держать точнее, даже без синхронизации.

И снова — та же проблема что с GPS (дискретность — 1 секунда), и подобное же решение: если у модуля есть выход SQ — его можно настроить на выдачу сигнала в 1 Гц, и по фронту импульса ловить микросекунды через прерывание, так как импульс жестко привязан к счетчику секунд.
Останется настроить еще установку времени в RTC, так, чтобы секунды устанавливались максимально близко к реальным, с учетом задержек на процесс записи времени в чип.
#include <RTClib.h>#include <JbTime.h>/* add to main.inovoid RTCSetup();bool RTCSetTime(JbTime * time);bool RTCGetTime(JbTime * time);float RTCGetTemperature();void RTCLoop(JbTime * time);*/// information#define P_SDA 4#define P_SCL 5#define PIN_SQ 12#define RTC_MIN_YEAR 2026RTC_DS3231 rtc;volatile uint32_t sqMicros = 0;bool rtcOk = false;#define RTC_WRITE_DELAY_MAX 30000#define RTC_WRITE_DELAY_MIN 28000// -----------------------------------------------float RTCGetTemperature(){ if(!rtcOk) return -99; return rtc.getTemperature();}// -----------------------------------------------bool RTCGetTime(JbTime * time){ if(!rtcOk) return false; if( rtc.lostPower() ) return false; DateTime now = rtc.now(); if(now.year() < RTC_MIN_YEAR) return false; unsigned long sec = now.unixtime(); uint32_t nowMicros = micros(); uint32_t usec = nowMicros - sqMicros; time->settime(sec,usec); return true;}// -----------------------------------------------void ICACHE_RAM_ATTR onSQ() { sqMicros = micros(); // фиксируем момент импульса}// -----------------------------------------------void RTCSetup() { sqMicros = 0; Wire.begin(); rtcOk = rtc.begin(); if(rtcOk){ rtc.writeSqwPinMode(DS3231_SquareWave1Hz); pinMode(PIN_SQ, INPUT); attachInterrupt(digitalPinToInterrupt(PIN_SQ), onSQ, FALLING); delay(3000); }}// -----------------------------------------------bool RTCSetTime(JbTime * time){ if(!rtcOk) return false; uint64_t sec ; uint32_t usec ; time->gettime(&sec, &usec); // left to next second uint32_t x = 1000000 - usec; if (x < RTC_WRITE_DELAY_MIN) return false; while(x > RTC_WRITE_DELAY_MAX){ delay(1); x -= 1000; } if(x > RTC_WRITE_DELAY_MIN && x < RTC_WRITE_DELAY_MAX){ sec ++; rtc.adjust(DateTime(sec)); return true; } return false;}// -----------------------------------------------void RTCLoop(JbTime * time){ if(!rtcOk) return ; if(time->fresh){ if(RTCSetTime(time)){ time->fresh = false; } } else if(time->old()){ if(RTCGetTime(time)){ // nothing } }}
Ну и подключить это все в основном коде:
#include <JbTime.h>void WifiSetup();void WifiLoop();String WifiIP();// ---------------------------void MqttSetup();void MqttLoop();void MqttPublish(const char * str);void MqttPublish(const char * topic, const char * str);void MqttPublishBin(const char * topic, byte * data, int len);// ---------------------------void RTCSetup();bool RTCSetTime(JbTime * time);bool RTCGetTime(JbTime * time);float RTCGetTemperature();void RTCLoop(JbTime * time);// ---------------------------void GPSSetup();bool GPSGetTime(JbTime * time);void GPSLoop();// ---------------------------#define IND 2JbTime systime;JbTime rtctime;JbTime gpstime;JbTime ntptime1;JbTime ntptime2;#include <JbNTP.h>JbNTPClient ntp(&systime);#define NTP_PERIOD 3700000unsigned long ntp_timer;void NTPSetup(){ ntp.begin(); ntp_timer = 0;}void NTPLoop(){ ntp.serve();}void sync_time() { // try to get GPS time GPSGetTime(&gpstime); if(gpstime.ok && !gpstime.old()){ systime.copy(&gpstime); systime.fresh = true; } // try to get NTP time bool var1 = ntp.requestTime("xxxxxxxxx",&ntptime1); bool var2 = ntp.requestTime("yyyyyyyyy",&ntptime2); if((var1 || var2) && !systime.fresh){ uint64_t ntp1_sec = 0; uint32_t ntp1_usec = 0; ntptime1.gettime(&ntp1_sec, &ntp1_usec); uint64_t ntp2_sec = 0; uint32_t ntp2_usec = 0; ntptime2.gettime(&ntp2_sec, &ntp2_usec); if(ntp1_sec == ntp2_sec){ uint32_t mid = (ntp1_usec + ntp2_usec ) >> 1; systime.settime(ntp1_sec, mid); systime.fresh = true; }else{ if(var1 && !var2){ systime.settime(ntp1_sec, ntp1_usec); systime.fresh = true; } if(!var1 && var2){ systime.settime(ntp2_sec, ntp2_usec); systime.fresh = true; } } } if(systime.fresh){ if(RTCSetTime(&systime)){ systime.fresh = false; } }}// =====================================#include <ArduinoJson.h>unsigned long publish_timer = 0;#define PUBLISH_PERIOD 60000#define PUBLISH_LENGTH 300void Publish(){ DynamicJsonDocument doc(PUBLISH_LENGTH); char buf[30]; float t = RTCGetTemperature(); doc["t"] = t; uint64_t sec = 0; uint32_t usec = 0; // --------------- if(RTCGetTime(&rtctime)){ rtctime.gettime(&sec, &usec); sprintf(buf,"%llu.%06u",sec,usec); doc["rtc"] = buf; }else{ doc["rtc"] = "none"; } // --------------- GPSGetTime(&gpstime); if(gpstime.ok && !gpstime.old()){ gpstime.gettime(&sec, &usec); sprintf(buf,"%llu.%06u",sec,usec); doc["gps"] = buf; }else{ doc["gps"] = "none"; } // --------------- if(ntptime1.ok){ ntptime1.gettime(&sec, &usec); sprintf(buf,"%llu.%06u",sec,usec); doc["ntp1"] = buf; }else{ doc["ntp1"] = "none"; } if(ntptime2.ok){ ntptime2.gettime(&sec, &usec); sprintf(buf,"%llu.%06u",sec,usec); doc["ntp2"] = buf; }else{ doc["ntp2"] = "none"; } // --------------- if(systime.ok){ systime.gettime(&sec, &usec); sprintf(buf,"%llu.%06u",sec,usec); doc["sys"] = buf; }else{ doc["sys"] = "none"; } // --------------- doc["ip"] = WifiIP(); // --------------- String message; serializeJson(doc, message); MqttPublish(message.c_str());}void msg_callback(char* topic, byte* payload, unsigned int length) { if(!strncmp((char *)payload, "reset", length)){ ESP.reset(); } if(!strncmp((char *)payload, "sync", length)){ sync_time(); }}//==========================================void setup() { Serial.begin(9600); pinMode(IND,OUTPUT); WifiSetup(); MqttSetup(); RTCSetup(); NTPSetup(); GPSSetup();}void loop() { WifiLoop(); MqttLoop(); NTPLoop(); GPSLoop(); RTCLoop(&systime); if( (millis() - publish_timer) > PUBLISH_PERIOD ){ publish_timer = millis(); Publish(); digitalWrite(IND,LOW); delay(50); digitalWrite(IND,HIGH); } if( (millis() - ntp_timer) > NTP_PERIOD || ntp_timer == 0 ){ ntp_timer = millis(); sync_time(); } delay(100);}
Теперь при старте девайс берет время из RTC, пытается синхронизироваться по нескольким NTP-серверам, по GPS, при успехе обновляет RTC, и сам по нему сверяется время от времени. Ну и сам работает NTP-сервером.
Можно было бы еще приделать к нему экранчик и выводить текущее время — но по идее он должен висеть где-то на стене в маленькой коробке и не мозолить глаза, так что именно как часы его использовать не будем. В конце концов ничего теперь не мешает сделать отдельные часы, синхронизирующиеся уже с ним.
ссылка на оригинал статьи https://habr.com/ru/articles/1023414/