More XIAO Fun

With apologies to GNU('s not Unix) and GNU GNATS
Now that the Wi-Fi Switch for Domoticz using a XIAO ESP32C3 project is in a working state, a break was in order even if I should get on with Part 5. It was an opportunity to do something about my unfortunate contribution to a digital tragedy of the commons. All the home automation devices based on Tasmota call on outside NTP servers to get the time. Our sprinkling of voice assistants and video streaming devices do the same thing and there are usually an additional six, or more, larger devices (desktops, portable computers, NAS, smart phones, tablets) left on that probably check up on the time on a regular basis. A better Internet citizen would set up a local NTP server to service all those devices.
Table of Contents
- Why Build a Time Server and Why GPS?
- The ATGM366H-5N-31 GPS Module
- An NTP Server
- A Second Insight
- Other Libraries
- Overview of the Firmware
- Testing GNATS
- Adding a DS3231 Real Time Clock
- Source Code and Additional Thoughts
Why Build a Time Server and Why GPS?
Why buid a time server? The obvious solution is to enable an NTP server in the system router or firewall device if possible. The router from my Internet service provider (ISP) does not support this. Besides, GNATS which uses the global positioning system (GPS) will not rely on an Internet time source which is good given that storms have caused power loss for hours and even days in the past. Some would ask why care about providing accurate time to devices that rely on power when there is no power? Some power outages have been hard on our previous ISP that could not get its system on line for days after power was restored. Besides, many of our devices are battery powered and we do have an emergency generator. Its not powerful enough to run the water pump, but it can power the fridge, some lights and quite a few devices. We all have priorities. There are plenty of other reasons to run a local time server.
Why use GPS as a time source? My past experience with GPS indoors has been very unsuccessful. As it happened, I ran across that old USB GPS receiver that came with a long-lost copy of Microsoft Streets & Trips purchased years ago. A Linux daemon (gpsd — a GPS service daemon) worked with the receiver and it did coax some data from it, including the time. The next day, it would not work. Perhaps atmospheric conditions were worse, perhaps I had done something wrong when playing with the receiver, but it did not really matter because it is not a good idea to base projects on a single device that is no longer readily available. Furthermore USB GPS receivers are not recommended as time sources because of transmission delays across a USB connection.
Universal-Solder sent an e-mail about a sale which included the serial ATGM336H GPS Module for Arduino Raspberry Pi STM32 ESP32. Because Arduino libraries were easy to find and the device was affordable, I splurged on a couple receivers. That thumb nail sized receiver looked like the perfect mate to the diminutive XIAO ESP32C3 to come up with a tiny time server. So there's another reason for using GPS: it is cheap. A bare time server can be had for slightly less than $10 to $15.
There is another possibility: atomic clock receivers. I did purchase a kit, again from Universal-Solder a couple of years ago. Unfortunately, I never got around to building it. Looking at it now that the first version of GNATS is finished, it looks to be a simple kit to build, so there may very well be a follow-up post on using this time reference.
The ATGM366H-5N-31 GPS Module
Here is some information about the GPS module that I gleaned from some vendor sale blurbs.
Supported satellite nav systems | GNSS and BDS | ![]() ![]() |
---|---|---|
Interface | Two wire serial and PPS (precise pulse second) | |
Baud | 9600 (default) | |
Output protocol | NMEA 0183 | |
Compatibilty | ublox NEO-6M | |
Time to first fix | ~32 seconds | |
Postionning accuracy | 2.5 metres | |
Update frequency | 1 Hz (default) or 10 hz | |
Supply voltage VCC | 2.7 - 3.6 Volts | |
Average power consumption | < 25 mA @ 3.3V volts | |
Dimensions | 13mm x 16mm | |
Antenna | included | |
Libaries | TinyGPS++, NeoGPS (not tested), and probably others |
The baud and update frequency values imply that settings could be changed on the module. After all, it has a serial Rx connection. However the only document I could find in English, a ATGM33H-5N User Manual has no explicit information. There are references to other documents but no links. Chinese speakers may find more information from the Hangzhou ZhongKe Microelectronics web site. Consequently, I used the default values.
Those wanting the NMEA 0183 standard can purchase it here for a few thousand dollars. Of course, I relied on information gleaned from the web such as What Exactly Is GPS NMEA Data?. The first test of the hardware was done with a XIAO ESP32C3. Only three connections are required between the XIAO and the GPS receiver.
Then I modified the DeviceExample.ino
example sketch included in the TinyGPSPlus library to use the hardware UART. This is the code with the modifications.
And here is matching PIO configuration file, platformio.ini
As can be seen, I tried this with two different XIAO ESP32 modules, but all my initial development was done with the XIAO ESP32C3. I placed the GPS receiver as close to the window as the USB cable to the desktop would allow. The antenna was about 5 cm from the glass pane and about 1.2 metres from the southernmost corner of the house and perhaps 1.5 metres above the ground. After letting the program run for about an hour, the coordinates had settled to constant values.

I entered the reported longitude and latitude into the map at epsg.io and was very pleased with the result. The coordinates corresponded to the correct corner of the house based on satellite imaging. It appears that the claimed 2.5 metre accuracy is not off the mark. The program also displays the UTC time. Indeed these were available much before the first positional fix was obtained, which is of importance given the project's objective. Note the 1 second resolution. There will be more information about that later.
An NTP Server
Now that I had a source of accurate time signals, I needed to find a way to turn the XIAO into an NTP server. It should not have been surprised that a great number of persons have done this type of thing before. Many take this subject very seriously and most of these would not approve of having a server providing time packets over Wi-Fi. I don't have an ESP32 with an Ethernet connector. Even if technically the little XIAO ESP32C3 will be a stratum 1 NTP server, it does not have to be accurate to the microsecond. A home automation device can be turned on or off a half second late or a full two seconds early and it won't matter. So at this stage anyway, I can live with the jitters associated with a Wi-Fi connection. Time keeping on the Internet is a complex subject with diagrams such as the one below and erudite discussions about precision, frequency, jitters and so on.

To be honest I don't understand most of it. Nevertheless, I will try to do this first version as correctly as my limited knowledge allows. Of course, the best approach in circumstances such as these it to find an example that seems comprehensible and then try it out and hope for an aha! moment. I chose the ElektorLabs 180662 mini NTP with ESP32 project by Mathias Claussen because it had a write up and a modular approach which made it possible to start with the basics. I especially liked that it had a separate NTP server library with a very simple interface that could be used right away.
The server uses the asynchronous UDP library to handle incoming NTP requests and dispatch the responses with minimal interaction with the rest of the firmware. As can be seen, two callback functions are used to get the current time, so that all our main program has to do is to implement these functions. In my first version, that's exactly what I did in a very straightforward way: getUTCTime()
returns the 32-bit timestamp from ESP32 built-in real-time clock (RTC). For this to work, the RTC has to be set to the correct UTC (or GMT, ZULU etc) time, but let's worry about that later. I have already justified the decision not to worry about subseconds.
That was good enough to establish that the NTP library worked which was to be expected, of course. On looking more closely at ntp_server.cpp
, I spotted a few mistakes and thought that the code could be tightened a bit. Take this statement with a grain of salt, C++ is definitely not my field of expertise. It should be emphasized that GNATS is not burdened with the reuse of ... the timekeeping corefunctions [...] synchronize to different [time] sources with different priorities. For the time being, the basic server has only one time source, the GPS, which is used to correct the ESP32 real-time clock (RTC) peripheral at regular intervals. The RTC should be able to keep the time accurately enough in between these updates so that it can be used as the time source in ntp_server
. The Espressif documentation Get Current Time in ESP-IDF Programming - Guide System Time shows how to get the UTC time with microsecond accuracy. So why not use that function directly without going through callback functions? My logic is that if multiple time sources were to be made available later, they should be used to synchronize the RTC at regular intervals in whatever priority will be deemed best, in a way that will be of no importance to the NTP server. Consequently the header file becomes even simpler. That was the first aha! moment: the RTC will be the time keeper, which will be "disciplined" by the GPS. That way if for whatever reason the GPS can no longer supply a valid time signal, the RTC can continue to provide a reasonably accurate time to the NTP server for a long while.
Of course, the implementation has to be modified. Hopefully comparison with the original Elektor code and the generous comments make clear what was done.
In the ntp_packet_t
struct, the type of the precision
code should be a signed 8-bit integer, as most times it will be a negative value. The function DeterminePrecision()
in the Elektor code, was obviously meant to calculate that precision, but it was never used and its unusual divisor, which I could not fathom, always yielded a 0 value. With the obvious divisor to calculate the average time to call the gettimeofday()
function the precision is -16
which was more in line with values used elsewhere. A quick test showed that using the minimum time instead of the average gives the same precision.
One of the advantages of using gettimeofday()
is that the time value has a microsecond resolution while the original code used subsecond meant millisecond. Such precision was not that important for my intended use. Nevertheless, it seems like a good idea to use the increased resolution at this level, just in case I eventually implement a better time synchronization algorithm. The NTP time stamps measure fractions of a second with even greater resolution than the microsecond. I could not find a universal best way of converting microseconds to 32-bit fractions of a second, so I chose the following: ntp_fraction = (uint32_t) ( ((uint64_t) x_us << 32) / 1000000 )
where x_us
is the number of microseconds. Detail in this gist.
There is no doubt room for improvement. In particular, the arbitrary assignments of values to rootDelay
and rootDispersion
should be investigated further. Next time I am looking for a rabbit hole in which to get lost, this might be a good topic to take on.
There is another worry. The incoming requests for NTP time packets will arrive asynchronously so that gettimeofday()
will be called asynchronously. The main firmware task will update the RTC at regular intervals, so there is a potential for clashes. Hopefully, the underlying RTOS has some sort of lock mechanism to avoid contention over the resource. I am not sure. The function gettimeofday()
is implemented as a wrapper around the IDF function _gettimeofday_r()
. I believe that the trailing _r
signals a reentrant routine which avoids one pitfall for sure. On the other hand, that does not answer my question about possible intervention between gettimeofday
and settimeofday
that is used to update the RTC time.
A Second Insight
Notwithstanding my misgivings, the modified NTP server did work and it was possible to update the RTC with time values obtained from the GPS. As often the case, the more complicated part of the code is dealing with non-normal operations. Notably, what should be done until the GPS can start acquiring time data? I kept on putting off that problem because ultimately, my goal is to add an external real-time clock, a DS3231 probably, as a backup. However I had nagging doubts about that because past experience has shown me that these RTC are not entirely dependable. I previously discussed problems with the home automation system hosted on a Raspberry Pi equipped with a malfunctioning RTC, The Domoticz Time Synchronization Problem. Then it dawned on me that what had been learned about the systemd-timesynced
handling of the system clock when there is no RTC was actually applicable here.
I added an time_t mclock = 0
variable that will mimic the systemd-timesynced clock
file.
/var/lib/systemd/timesync/clock
The modification time ("mtime") of this file is updated on each successful NTP synchronization or after each SaveIntervalSec= time interval, as specified in timesyncd.conf(5). At the minimum, it will be set to the systemd build date. It is used to ensure that the system clock remains roughly monotonic across reboots, in case no local RTC is available. (source)
Could not have said that any better myself. Of course there's a little problem, because a variable such as mclock
will not survive reboots. Consequently, whenever a new value is assigned to mclock
it is saved to non-volatile storage (NVS) which in the case of the ESP32 is flash memory. This is done with the Preferences
library which is the ESP32 replacement of the Arduino EEPROM library
In the course of developing this bit of code, I changed my mind about the name of the Preferences namespace
. There is no way of removing a namespace, but the Espressif documentation provides a sketch to erase and reformat the NVS memory.
Other Libraries
It is a bit pretentious to call smalldebug.h
a library. It is just defines two macros: DBG(...)
and DBG(...)
. These are used throughout the code instead of Serial.println(...)
and Serial.printf(...)
. The advantage of using these macros is that all the print statements will be stripped from the compiled firmware when the ENABLE_DBG
macro is set to 0.
The other library used is ESP8266 and ESP32 OLED driver for SSD1306 displays by ThingPulse, Fabrice Weinberg. This library is certainly not the only available library for SDD1306 OLED displays, but I like it for a number of reasons. It has powerful graphic capabilities. The analogue clock is a good example and would be an alternate method of showing the local time. The built-in fonts support the full Latin1 character set. This means that it would be possible to write the full date in French with months such as février and août with diacritics displayed correctly. That said, the display is not a required element of this project and it can be omitted without problems. Just set the HAS_OLED build-flag to 0 in that case.
As shown in the previous section, the ESP32 real time clock will be set with the last known valid date and time. This can be the last value of mclock
saved to NVS or it can be the date and time in effect when the compilation of the firmware was started on the computer hosting PlatformIO. That time is available as the built-in PlatformIO variable $UNIX_TIME
which is assigned to the COMPILE_TIME
macro added to environment build_flags
in platformio.ini
.
I could not find the equivalent in the Arduino IDE. I mashed together two libraries found on GitHub, buildTime by Alex Gyver and arduino_compiledate by Mikael Sundin, to make something almost as easy to use in the Arduino IDE. It is the mdBuildTime "library". The word is in quote because it contains a single function: time_t unixbuildtime()
. Here is how to use it, making sure to set the correct time zone beforehand.
Here is the layout of the time server with the optional OLED screen.
Be carefull, 0.96" OLED screen based on the SDD1306 controller are ubiquitous, but they do not all have the shown pinout. Indeed some connect over a SPI bus instead of I²C.
Overview of the Firmware
Assuming that the GPS receiver has locked onto a satellite, it sends messages over the serial link to the ESP32 at the start of every second. That data is passed on to the TinyGPSPlus library which decodes the messages. The library updates an internal time stamp, and other data such as longitude and latitude as the messages come in. Of course none of this starts until the receiver has acquired a signal which may never happen. On the other hand, the ESP RTC is continuously updated from the moment the microprocessor is powered up, but with an initial time value of 0. So there are two clocks. One knows what the actual time is with great accuracy when it functions but there is no guarantee that it will function. The other measures accurately enough the elapsed time since the last reboot of the ESP but it has no way to know when it actually started ticking. So we need to combine these two clocks by updating the RTC time as soon as the GPS provides a valid time stamp and then correcting the RTC on a regular basis with the latest GPS time. Some call these corrections discipline.
Here is a simplified flowchart of the firmware. Basically, it's just a big timing loop (like all Arduino sketches) where actions are taken at regular intervals. These intervals are defined in the platformio.ini
configuration file.
- SYNC_POLL_TIME
This specifies the time interval between attempts to update the ESP real time clock from the latest obtained GPS time stamp for the first time. Since that initial updating of the RTC with a correct time value is important, the macro is set to 10,000 milliseconds (= 10 seconds).
- GPS_POLL_TIME
This specifies the time interval between attempts to update the ESP real time clock from latest obtained GPS time stamp after the first successful update. Because it can be assumed that the ESP32 RTC is relatively accurate, the macro is set to 3,600,000 milliseconds (= 1 hour).
- SAVE_CLOCK_TIME:
This specifies the time interval between at which the ESP32 real time clock is saved to non-volatile memory. It does not matter if the RTC has been updated or not from a GPS time signal. As explained above, the idea is to keep the time moving ahead even if the ESP is rebooted. This macro is set to 7,200,000 milliseconds (= 2 hours) to avoid overtaxing the on-board flash memory.
- GPS_WARNING_TIME
This specifies the time between displays of the "GPS NOT FOUND" message on the OLED screen. It is set to 300,000 milliseconds (= 5 minutes). Because time value shown on the OLED is updated at the start of every minute, the not found message will typically be displayed for approximately one minute.
Testing GNATS
Although not strictly necessary to test GNATS, a static IP address was assigned to the XIAO ESP32C3: 192.168.1.23
. This was done with the home network DHCP server which is running on the ISP provided router. Then I created a simple Python ntpc.py
by combining the work of others. There is also a more sophisticated version ntpc2.py
to shows the content of the UDP packet received from the time server. It helped while improving ntp_server
; more on that later.
Make sure that the script works by running it, without any argument, from a computer with Python 3 and a working connection to the Internet. It's best to ensure that the script was in a directory included in the path. In my case that was .local/bin
in my home directory. After copying the script to that directory, I madeit executable and then ran it without a command line parameter.
That showed that the client does work. Testing GNATS with the script was easy.
Don't worry about the actual reported times, I am writing this after the fact.
The real test was to open an SSH session on a Raspberry Pi and have it use GNATS as a time server. That's a bit tricky because the Pi, which does not have a battery-backed real-time clock, automatically gets its time from well-known Internet NTP servers. To truly test GNATS as a replacement time server for the Pi, it is necessary to prevent the Pi from getting the time from a time server. The easy way of achieving this paradoxical situation is to make sure that the Pi cannot reach the Internet. This can be done by setting up a wired local network which is not linked to the Internet or to a real-time clock, but that requires an extra Wi-Fi router. Not everyone has will have a spare router on hand, so here is a procedure to perform tests in a less rigorous manner.
Connect the Raspberry Pi, GNATS and the desktop computer to the local network. The desktop can also be connected to the XIAO ESP32C3 via a USB cable to see the GNATS debug messages. Do not risk making a mess of an SD card containing a working system. I strongly suggest that this experiment be done with a new SD card on which Raspberry Pi OS is installed and updated. I ran my tests with the lastest 64 bit lite version. Open an SSH session using the correct host name of the Pi which will probably not be tarte
. Actually, the Pi had a fixed IP address, 192.168.1.22
, that was assigned by the DHCP server based on the Pi's MAC address. The desktop has a dynamic address assigned by the DHCP server which was at the time of the test 192.168.1.158
. GNATS has a self assigned static IP address: 192.168.1.23
. Finally, the router; which is also the gateway for all the devices on the LAN, is at 192.168.1.1
which is also a fixed address.
Not surprisingly, the time and date are correct, the Pi managed to get the time from a default time server on the Internet (debian.pool.ntp.org
). Now let's break this by overriding this default with an address to a non-existing device on the local network.
Let's make that the Pi is not requesting the address of an NTP server from the DHCP server.
Let's now clear the system journals as much as possible to be able to see, if necessary, what is going on after rebooting.
The output will be different, but that does not matter. Now let's reboot after erasing the time stamps that the system leaves in the file system to set the initial time of the Pi even before attempting to get the time from an NTP server.
The (wrong) date and time correspond to the systemd
compilation date which is used to "touch" clock
which is the systemd-timesync
timestamp.
Now let's tell systemd-timesync
to get its time from GNATS, check one last time that the date is wrong and then reboot.
After a suitable delay, open a new SSH session and check that the time is correct and that GNATS is working as a local time server.
Here is a curated list of journal entries that shows what happens during the boot process with respect to setting the system time.
So GNATS works, but truth be told at first the Raspberry Pi would not udate the system time even if it was getting UDP time packets from GNATS. It turns out that my improvements to ntp_server
were not so good; I was not setting the reference time stamp correctly. That's fixed now.
It would have been possible to test GNATS directly with the desktop computer without involving the Pi. I just did not want to risk creating any problems on my main computer just as it was best to install a new SD card on the Pi to avoid problems with the working system that was on that machine which is normally the rig used to test changes to the home automation system. Avoiding putting these complex systems in jeopardy is worth the extra work.
The real test of GNATS is actual use. In the next blog, GNATS was used to update the time on a Pi along side with real NTP servers from the Web. Unfortunately, the results were not very good. According to chrony
, GNATS was classed as a falseticker because its time was too divergent from the majority of other sources. This result make me hesitate between calling the project GNAT's Not Accurate Time Server and GNAT's Nearly Accurate Time Server.
A proper timeserver should take advantage of the PPS (precise pulse per second) signal of the ATGM366. This pulse should trigger an interrupt that starts a true microsecond timer at the beginning of each second. Also a proper time server should not be running over Wi-Fi which suffers from much greater variability in the time to transmit packets than an Ethernet connection would.
Adding a DS3231 Real Time Clock
Minimal support for a battery-backed real time clock can be added with relative ease. As I had DS3231 based clock modules, which were used with the Raspberry Pi, it was an obvious choice. Since it is an I²C device, connecting it to the GNATS was straightforward.
Because I use the easily found CR2032 as the backup library instead of a rechargeable LIR2032, the schematic shows that the current limiting resistor of the battery charge circuit has been removed. There is plenty of documentation about this on the web: ZS-042 DS3231 RTC module, [DS3231: CR2032 vs LIR2032] Warning! Is your module killing the battery?. Perhaps the biggest problem is choosing from among the numerous Arduino libraries for the DS3231 real time clock that are compatible with the ESP32.
- Rtc by Michael Miller (Makuna): An RTC library with deep device support. As the description implies, this is a big library which supports many RTC DS1302, DS1307, DS3231, and the DS3231.
- RTClib by Adafruit: A fork of Jeelab's fantastic RTC library. Works with DS1307, DS3231, PCF8523, PCF8563 on multiple architectures. Like most Adafruit libraries for hardware devices it depends on BusIO from Adafruit.
- DS3231 by Andrew Wickert, Eric Ayars, Jean-Claude Wippler, Northern Widget LLC: An Arduino library for the DS3231 real-time clock (RTC).
- DS3232RTC by Jack Christensen: an Arduino library that supports the Maxim Integrated DS3231 and DS3232 Real-Time Clocks. This library is intended to be used with PJRC's Arduino Time library.
- Arduino, ESP8266, STM32, ESP32 and others uRTCLib by Naguissa: tiny library [with] basic RTC functionality on Arduino, ESP8266, STM32, ESP32 and other microcontrollers; the DS1307, DS3231 and DS3232 RTCs are supported.
- ds3231 by Petre Rodan (rodan): DS3231 library for the Arduino.
Let's be clear, the battery backup RTC is not being used as an alternate time source should the GPS receiver not be able to connect to enough satellites to obtain time signals. Instead the RTC is a supplementary mechanism to improve the initial setting of the ESP real time clock until the GPS receiver acquires time signals. The flowchart on the right shows how this is done. In the end, the ESP RTC will be set to whichever is the latest: the time saved in NVS, the time read from the battery-backed DS3231, the firmware compile time.
Whenever the ESP RTC is updated from the GPS time (in function savemclock()
), that time stamp is also saved to NVS memory and used to reset the DS3231.These are the only changes made to GNATS when adding the RTC. The ElektorLabs NTP project makes better use of the hardware RTC. Time can be updated from 1) an RTC, 2) an NTP server (or servers I assume) and 3) a GPS receiver. When a time stamp is obtained from a source with a higher priority than ever encountered, it becomes the current master source and it's time stamp is used to update the time sources with a lower priority. If 15 minutes elapses without obtaining a time stamp from the current master, it is demoted and replaced with the source lower down the priority chain. That is an interesting mechanism since it means that the hardware real time clock can be used to update the ESP RTC when the GPS receiver is no longer able to get time values.
Source Code and Additional Thoughts
GNATS is available on GitHub. While the repository is publicly available, it should not be viewed as anything more than a pedagogical tool. It should not be used as the primary source of accurate time. However, I believe that it is still a valuable addition to my home automation system as a backup time source when access to better clocks is lost. In that role, I have no doubt that the single core ESP32-C3 found on the XIAO ESP32C3 will be up to the task. The follow-on post, Using a Local Network Time Server provides some insight into how GNATS can be added to a local network.
As mentioned above, better use of the DS3231 hardware clock could be made as a backup for the GPS and it is certainly something that I would add if GNATS were to be added to my home automation system. However that may not be the case. I will soon be adding a hardware firewall running either pfSense or OPNsense. It seems that both these can use a serial GPS receiver as a time source, so that it would no longer be necessary to create a full NTP server.
One of the very nice characteristics of the XIAO line of products from SeeedStudio is that they all provide UART, I²C and SPI serial interfaces on the same pins. While developing GNATS on a XIAO ESP32C3, I received a XIAO ESP32S3 with its two core ESP32-S3 that should be even more powerful than the original two core ESP32. I have used it instead of the XIAO ESP32C3 with success. All that had to be done was to modify the board definition in the platformio.ini
configuration file. That is already available in the file supplied in the Github repository. I should point out that I had to move to the latest version of the Espressif 32 platform (version 6.3.1) released on May 26th because support for the XIAO ESP32S3 has been included only recently.