It’s been done before, lots of times. Sort of. But maybe not just this way. The collection of parts and pieces provides for a broad curriculum of learning possibilities. Here are some of the issues that have come up so far.
SNTP vs NTP. NTP has a long history and it is intended for always on computers where the clock needs tweaking over time to keep it ‘close enough’ without any hiccups (the principle of least surprise for any software using the clock). The simple version doesn’t have the sophisticated algorithms to correct errors gently. It is designed for computers that are turned on and off and it just bangs the clock to the correct time and doesn’t worry about what anybody using the clock might think.
The first issue is about the nature of keeping time. There is general agreement on the length of a second based on the TAI (International Atomic Time). In the 70’s gravitational time dilation became a concern so that getting clocks all over the world in sync meant making adjustments to a chosen altitude on a chosen geoid. Then there’s the the issue raised by tidal and other forces that influence orbital and rotational periods. Navigators need time to sub-second resolution that accommodates these astronomic variations so there’s a difference between whether adjustments are made to the atomic seconds in sub-second sizes for navigation or in leap-second sizes for most everybody else.
This is the first step in timekeeping: counting seconds and trying to fit the count to the earth’s current rotational period. Next up is when to start counting. They call this the epoch. NTC uses 1900, POSIX uses 1970, MicroPython on the ESP8266 uses 2000, GPS uses 1980. The reason behind this is about how high a computer can count easily. Converting between these counts is complicated because some use leap seconds and others do not. After you’ve got the seconds since some known time and date and the leap seconds settled, you can then figure out the current date and time. Of course, at that point, you have to start looking at time zones and daylight savings times rules.
At least, with a GPS based NTP server, the GPS receiver does the conversion between GPS time and UTC and the difference between UTC and NTP is fixed (2,208,988,800). NTP is a bit more concerned about leap seconds than UTC because NTP likes smooth transitions but my server isn’t going to worry about that. Both SNTP and NTP use the same UDP packet containing several timestamps for measuring transmission lag and other critical parameters. As with most computer clocks, the timestamp counts seconds.It has a 32 bit part for seconds and another 32 bit part for the fraction of a second. That means it can count for 136 years with a precision of 232 picoseconds [NTP Era and Numbering]. The rollover in 2036 is farther out than the expected lifespan of the hardware so I don’t worry about that. The fractional seconds is another problem as the ESP8266 doesn’t clock time in binary fractions.
The idea is to use the high precision pulse per second (PPS) line from the GPS receiver to mark the start of each second and then calculate the NTP fraction of a second from this using the time functions in MicroPython. The PPS triggers an interrupt, the interrupt service routine reads the ESP8266 clock and stores it in a global variable. When NTP needs a timestamp, it reads the ESP8266 clock, finds the difference to the Top of Second value in milli or micro seconds, converts that value to modulo 2^32 and posts the result into the NTP timestamp. That conversion may be obvious to many but it took me a while to figure out what people were talking about.
To convert a decimal fraction of a second like milliseconds to the NTP fraction is an equal ratios problem. How many milliseconds is to 1000 milliseconds in a full second has to be the same as how many NTP fractional parts there are in 2^32 fractional parts in a full second. That becomes a simple A/B = C/D algebra problem where B is given as 1000 for milliseconds or 1,000,000 for microseconds and D is 2^32 or 4294967296. If you are converting from NTP timestamp to decimal fractional seconds, then you know C and can calculate A. If you are going from decimal to NTP (like in our NTP server) then you know A and solve for C.
After all this, we get into computer word sizes and binary value representations. MicroPython has some facilities for this but it is a bit of shoehorning to make it fit, I’ll have to use Python arrays to get unsigned 32 bit integers and the struct class to handle bytearray packing for the NTP packet. Nifty compsci stuff that is.