# Standalone NTP Server on ESP8266: Ensuring Accurate Time Without External Dependencies
When internet connections are unreliable and external NTP servers get blocked, a local source of precise time becomes essential. We'll walk through building an autonomous NTP server on the ESP8266 with microsecond accuracy that keeps ticking even without constant internet access.
Why Standard Solutions Fall Short
Modern infrastructures often can't reach global NTP servers due to network restrictions and whitelist policies. When external time sources are cut off, system clocks drift out of sync, which is a big problem for:
- Financial transactions
- Security logging
- Distributed computing systems
- IoT devices with strict timing needs
The fix? Build a local NTP server that maintains accurate time on its own between syncs. The ESP8266 is perfect: low power draw, built-in WiFi, and enough horsepower to handle timestamps.
Time System Architecture
The main challenge is getting past the limits of Arduino's standard time library, which only tracks whole seconds. NTP (RFC 5905) demands microsecond precision. Enter our custom JbTime class, which fixes two core issues:
- No support for fractional seconds in system time
- Drift buildup between syncs
How it works:
- When time arrives from an external server, it captures the processor's microsecond timestamp
- Each request calculates the delta from the sync moment to now
- Correction factors in network delay with this formula:
offset = ((t2 - t1) + (t3 - t4)) / 2
#ifndef jb_time
#define jb_time
#include <stdint.h>
#define JB_TIME_MAX_AGE 600
class JbTime {
private:
uint64_t _last;
uint64_t _sec;
uint32_t _usec;
uint32_t _mark;
public:
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 settime(uint64_t sec, uint32_t usec, uint32_t mark = micros()){
_mark = mark;
_sec = sec;
_sec += usec / 1000000;
_usec = usec % 1000000;
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;
}
}
};
#endif
Handling NTP Requests
The NTP protocol runs on UDP port 123 and sends time as a 64-bit value (32 bits seconds, 32 bits fractions). The trickiest part? Compensating for network delay. Here's the algorithm:
- Client sends request with T1 timestamp (its local time)
- Server receives at T2, sends reply at T3
- Client gets reply at T4
- Offset:
offset = ((T2 - T1) + (T3 - T4)) / 2
The JbNTPClient class handles this with ESP8266 quirks in mind:
- Packets formatted per RFC 5905
- Auto epoch adjustment (NTP starts at 1900, Unix at 1970)
- Filters out bad responses
bool JbNTPClient::requestTime(const char* server, JbTime * mytime) {
// ... initialization
// Calculation T1
uint64_t t1_sec = 0;
uint32_t t1_usec = 0;
systime->gettime(&t1_sec, &t1_usec);
t1_sec += NTP_UNIX_EPOCH_DIFF;
// Sending request
udp.beginPacket(timeServerIP, ntpPort);
udp.write((uint8_t*)&packet, sizeof(NTPPacket));
udp.endPacket();
// Ozhidanie otveta with taymautom 2000 ms
while (udp.parsePacket() == 0) {
if (millis() > timeout) return false;
delay(10);
}
// Raschet T4 and setevoy zaderzhki
double t4 = ntpToDouble(t4_sec, t4_frac);
_networkDelay = (t4 - t1) - (t3 - t2);
double offset = ((t2 - t1) + (t3 - t4)) / 2;
// Installation skorrektirovannogo vremeni
mytime->settime(sec, usec, t4_mark);
return true;
}
Server Side: Serving Time on the Local Network
Once synced with external servers, the device becomes a full-fledged NTP source. It handles client requests via the serve() method:
- Parse incoming UDP packet
- Verify mode (expects mode=3 — client request)
- Build response with current time from JbTime
- Factor in processing time for max accuracy
Key feature: Timestamp captured right at response send (T3) to cut error. Unlike off-the-shelf options, it pulls time straight from a high-res counter, skipping coarse system functions.
Key Features
- Autonomy: Holds 20-30 microsecond precision between syncs, even offline for hours
- Resource Efficiency: ESP8266 sips under 70 mA active, battery-friendly
- Compatibility: RFC 5905 compliant, works with any NTP client out of the box
- Flexibility: Tune max data age (JB_TIME_MAX_AGE) to your needs
- Fault Tolerance: Auto-fails over to backups if primary is down
— Editorial Team
No comments yet.