Skip to content

Instantly share code, notes, and snippets.

@mscalora
Created September 15, 2025 02:13
Show Gist options
  • Select an option

  • Save mscalora/8517e1823c71c8ab80bf3ddf199aa91b to your computer and use it in GitHub Desktop.

Select an option

Save mscalora/8517e1823c71c8ab80bf3ddf199aa91b to your computer and use it in GitHub Desktop.
A very small NTP client
#include <vector>
#include <functional>
const char* poolNTPServerName = "pool.ntp.org";
const unsigned long seventyYears = 2208988800UL;
class MicroNTP {
public:
static const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
protected:
byte packetBuffer[MicroNTP::NTP_PACKET_SIZE]; // Buffer to hold incoming and outgoing packets
WiFiUDP* udp;
public:
unsigned int localPort = 2390; // The local port to listen for UDP packets
unsigned long lastSendMS = 0;
unsigned long lastSyncMS = 0;
unsigned long lastSyncGMTEpoch = 0;
unsigned long resyncIntervalMS = 1000 * 60 * 1;
long timezoneOffset = 0;
std::vector<std::function<void (int epochSec)> > firstSync;
std::vector<std::function<void (int epochSec)> > everySync;
MicroNTP(WiFiUDP& clientUdp) {
udp = &clientUdp;
}
/**
* @brief notify callback of first (or next) sync, only called once
*/
void notifyFirstSync(std::function<void (time_t)> listener) {
firstSync.push_back(listener);
}
/**
* @brief notify callback of every time sync
*/
void notifyEverySync(std::function<void (time_t)> listener) {
everySync.push_back(listener);
}
time_t getGMTEpoch() {
return lastSyncGMTEpoch + (millis()-lastSyncMS)/1000;
}
time_t getLocalEpoch() {
return lastSyncGMTEpoch + timezoneOffset + (millis()-lastSyncMS)/1000;
}
void setTimezoneOffset(long offset) {
timezoneOffset = offset;
}
// Function to send an NTP request packet
void sendNTPpacket(const char* server = poolNTPServerName) {
Serial.println(F("[MicroNTP] Sending NTP request"));
// Set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form an NTP request
packetBuffer[0] = 0b11100011; // LI, VN, and Mode
// Send the packet
IPAddress ntpServerIp;
if (!WiFi.hostByName(server, ntpServerIp)) {
Serial.println("DNS lookup failed");
return;
}
udp->beginPacket(ntpServerIp, 123); // NTP requests are sent to port 123
udp->write(packetBuffer, NTP_PACKET_SIZE);
udp->endPacket();
lastSendMS = millis();
}
bool handleNTPPacket() {
unsigned long now = millis();
int packetSize = udp->parsePacket();
if (packetSize) {
Serial.printf("Received UDP packet of size %d\n", packetSize);
// Read the packet into the buffer
udp->read(packetBuffer, NTP_PACKET_SIZE);
// The timestamp is in the last 8 bytes of the packet
// Bytes 40-43 hold the seconds part
unsigned long highWord = (packetBuffer[40] << 24) | (packetBuffer[41] << 16) | (packetBuffer[42] << 8) | packetBuffer[43];
// Combine the bytes to get the NTP timestamp
unsigned long ntpTimestamp = highWord;
// Convert NTP time (since Jan 1, 1900) to Unix time (since Jan 1, 1970)
unsigned long unixTimestamp = ntpTimestamp - seventyYears;
lastSyncMS = now;
Serial.printf("Last Sync: lastSyncMS=%lu %lu\n", lastSyncMS, now);
lastSyncGMTEpoch = unixTimestamp;
Serial.printf("Current Unix epoch time: %lu received at device ms %lu\n", unixTimestamp, now);
time_t localEpoch = unixTimestamp;
for (const auto& callback : firstSync) {
callback(localEpoch);
}
firstSync.clear();
for (const auto& callback : everySync) {
callback(localEpoch);
}
return true;
} else {
if (now - lastSendMS > resyncIntervalMS) {
if (lastSyncMS < lastSendMS) {
Serial.println(F("[MicroNTP] Missing response for previous NTP request"));
}
sendNTPpacket();
}
return false;
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment