#include #include //#include #include #include // For IP and sync time #include #include #include // ESP32S3 pins #define CS_PIN (10) #define BUSY_PIN (3) #define RES_PIN (46) #define DC_PIN (9) // Wi-Fi credentials (replace with your own) const char* ssid = "Livebox-7E76"; const char* password = "oLkR9ngFR6hGSruH4y"; // NTP settings const char* ntpServer = "fr.pool.ntp.org"; const int timeZone = 1; // Adjust for your timezone (e.g., CET = 1, CEST = 2) WiFiUDP Udp; unsigned long lastSyncTime = 0; // Last successful NTP sync (Unix timestamp) // Buffer for NTP packets static uint8_t ntpPacketBuffer[48]; // 1.54'' EPD Module GxEPD2_BW display(GxEPD2_154_D67(CS_PIN, DC_PIN, RES_PIN, BUSY_PIN)); void setup() { Serial.begin(115200); display.init(115200, true, 50, false); // Connect to Wi-Fi WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nWiFi connected. IP: " + WiFi.localIP().toString()); // Sync time with NTP syncNTPTime(); lastSyncTime = now(); // Record sync time // Initial full screen update updateFullScreen(); } // Global variables static uint8_t lastMinute = 0; void loop() { uint8_t currentMinute = minute(); uint8_t currentSecond = second(); if (currentMinute != lastMinute && currentSecond == 0) { lastMinute = currentMinute; updateFullScreen(); } delay(100); // Reduce CPU usage } // Sync time with NTP server void syncNTPTime() { Udp.begin(2390); // Local port for NTP setSyncProvider(getNtpTime); setSyncInterval(3600); // Sync every hour (optional) } uint8_t stratum = 0; float rootDelay = 0; // Fetch NTP time (callback for TimeLib) time_t getNtpTime() { // Send NTP request sendNTPpacket(ntpServer); // Wait for response (1.5s timeout) uint32_t beginWait = millis(); while (millis() - beginWait < 1500) { int size = Udp.parsePacket(); if (size >= 48) { Udp.read(ntpPacketBuffer, 48); // Read packet into buffer // --- Debug output --- Serial.println("--- NTP Packet Details (Fixed Decoding) ---"); // --- Extract and decode all fields --- // LI // 0 = no warning // 1 = last minute has 61 seconds // 2 = last minute has 59 seconds // 3 = unknown (clock unsynchronized) uint8_t li = (ntpPacketBuffer[0] >> 6) & 0x03; // Leap Indicator (2 bits) Serial.print("Leap Indicator: "); Serial.println(li); //NTP version number, typically 4. uint8_t vn = (ntpPacketBuffer[0] >> 3) & 0x07; // Version Number (3 bits) Serial.print("Version: "); Serial.println(vn); //Association mode: //0 = reserved //1 = symmetric active //2 = symmetric passive //3 = client //4 = server //5 = broadcast //6 = control //7 = private uint8_t mode = ntpPacketBuffer[0] & 0x07; // Mode (3 bits) Serial.print("Mode: "); Serial.println(mode); // Stratum (byte 1) // Indicates the distance from the reference clock. // 0 = invalid // 1 = primary server // 2–15 = secondary // 16 = unsynchronized stratum = ntpPacketBuffer[1]; Serial.print("Stratum: "); Serial.println(stratum); // Poll Interval (byte 2) // Maximum interval between successive messages, in log₂(seconds). Typical range is 6 to 10. // Example RAW=0 means that the poll interval is 1 second int8_t pollIntervalRaw = ntpPacketBuffer[2]; Serial.print("Poll Interval RAW: "); Serial.println(pollIntervalRaw); // Precision (byte 3) // Signed log₂(seconds) of system clock precision (e.g., –18 ≈ 1 microsecond). int8_t precisionRaw = ~(ntpPacketBuffer[3]); Serial.print("Precision RAW: "); Serial.println(precisionRaw); // Root Delay (bytes 4-7, 16.16 fixed-point) // Total round-trip delay to the reference clock, in NTP short format. // The NTP Short Format is a uint32_t with the first 16 bits being the seconds //(i.e seconds = format >> 16) and the other 16 bits being the fraction // of a second (i.e fraction = format & 0xFFFF). After converting I'd like the output as a double. uint32_t rootDelayRaw = (ntpPacketBuffer[4] << 24) | (ntpPacketBuffer[5] << 16) | (ntpPacketBuffer[6] << 8) | ntpPacketBuffer[7]; rootDelay = (rootDelayRaw >> 16) + ((rootDelayRaw & 0xFFFF) / 65536.0); Serial.print("Root Delay: "); Serial.println(rootDelay, 6); // Root Dispersion (bytes 8-11, 16.16 fixed-point) // Total dispersion to the reference clock, in NTP short format. uint32_t rootDispersionRaw = (ntpPacketBuffer[8] << 24) | (ntpPacketBuffer[9] << 16) | (ntpPacketBuffer[10] << 8) | ntpPacketBuffer[11]; float rootDispersion = (rootDispersionRaw >> 16) + ((rootDispersionRaw & 0xFFFF) / 65536.0); Serial.print("Root Dispersion: "); Serial.println(rootDispersion, 6); // Reference Identifier (bytes 12-16) // Identifies the specific server or reference clock; interpretation depends on Stratum. // For packet stratum 0 (unspecified or invalid), this // is a four-character ASCII [RFC1345] string, called the "kiss code", // used for debugging and monitoring purposes. For stratum 1 (reference // clock), this is a four-octet, left-justified, zero-padded ASCII // string assigned to the reference clock. The authoritative list of // Reference Identifiers is maintained by IANA; however, any string // beginning with the ASCII character "X" is reserved for unregistered // experimentation and development. The identifiers in Figure 12 have // been used as ASCII identifiers: // +------+----------------------------------------------------------+ // | ID | Clock Source | // +------+----------------------------------------------------------+ // | GOES | Geosynchronous Orbit Environment Satellite | // | GPS | Global Position System | // | GAL | Galileo Positioning System | // | PPS | Generic pulse-per-second | // | IRIG | Inter-Range Instrumentation Group | // | WWVB | LF Radio WWVB Ft. Collins, CO 60 kHz | // | DCF | LF Radio DCF77 Mainflingen, DE 77.5 kHz | // | HBG | LF Radio HBG Prangins, HB 75 kHz | // | MSF | LF Radio MSF Anthorn, UK 60 kHz | // | JJY | LF Radio JJY Fukushima, JP 40 kHz, Saga, JP 60 kHz | // | LORC | MF Radio LORAN C station, 100 kHz | // | TDF | MF Radio Allouis, FR 162 kHz | // | CHU | HF Radio CHU Ottawa, Ontario | // | WWV | HF Radio WWV Ft. Collins, CO | // | WWVH | HF Radio WWVH Kauai, HI | // | NIST | NIST telephone modem | // | ACTS | NIST telephone modem | // | USNO | USNO telephone modem | // | PTB | European telephone modem | // +------+----------------------------------------------------------+ Serial.print("Reference ID: "); if (stratum <= 1 ) { Serial.printf("%c.%c.%c.%c\n", ntpPacketBuffer[12], ntpPacketBuffer[13], ntpPacketBuffer[14], ntpPacketBuffer[15]); } else if (stratum >= 2 && stratum <= 15) { // Stratum 2-15: IPv4 address of the reference server Serial.printf("%d.%d.%d.%d\n", ntpPacketBuffer[12], ntpPacketBuffer[13], ntpPacketBuffer[14], ntpPacketBuffer[15]); } else if (stratum == 16) { // Stratum 16: Unsynchronized (reserved) Serial.println("Unsynchronized"); } // Reference Timestamp (bytes 16-23) // Time when the system clock was last set or corrected, in NTP timestamp format. uint32_t referenceTimestampRaw = (ntpPacketBuffer[16] << 24) | (ntpPacketBuffer[17] << 16) | (ntpPacketBuffer[18] << 8) | ntpPacketBuffer[19]; //by reading the first four bytes we ignore the decimal seconds part of the timestamp time_t referenceTimestamp = referenceTimestampRaw - 2208988800UL + (timeZone * 3600); Serial.print("Reference Timestamp: "); Serial.println(referenceTimestamp); // Origin Timestamp (bytes 24-31) // Time at the client when the request departed, in NTP timestamp format. uint32_t originTimestampRaw = (ntpPacketBuffer[24] << 24) | (ntpPacketBuffer[25] << 16) | (ntpPacketBuffer[26] << 8) | ntpPacketBuffer[27]; //by reading the first four bytes we ignore the decimal seconds part of the timestamp time_t originTimestamp = originTimestampRaw - 2208988800UL + (timeZone * 3600); Serial.print("Origin Timestamp: "); Serial.println(originTimestamp); // Receive Timestamp (bytes 32-39) // The local time, in timestamp format, when the latest NTP message arrived. uint32_t receiveTimestampRaw = (ntpPacketBuffer[32] << 24) | (ntpPacketBuffer[33] << 16) | (ntpPacketBuffer[34] << 8) | ntpPacketBuffer[35]; //by reading the first four bytes we ignore the decimal seconds part of the timestamp time_t receiveTimestamp = receiveTimestampRaw - 2208988800UL + (timeZone * 3600); Serial.print("Receive Timestamp: "); Serial.println(receiveTimestamp); // Transmit timestamp (bytes 40-47) //Time at the server when the response left, in NTP timestamp format. uint32_t transmitTimestampRaw = (ntpPacketBuffer[40] << 24) | (ntpPacketBuffer[41] << 16) | (ntpPacketBuffer[42] << 8) | ntpPacketBuffer[43]; //by reading the first four bytes we ignore the decimal seconds part of the timestamp // Convert to Unix time (NTP time is seconds since 1900) time_t transmitTimestamp = transmitTimestampRaw - 2208988800UL + (timeZone * 3600); lastSyncTime = transmitTimestamp; setTime(transmitTimestamp); Serial.print("Transmit Timestamp: "); Serial.println(transmitTimestamp); return transmitTimestamp; } } return 0; // Failed to sync } // Send NTP request void sendNTPpacket(const char* address) { memset(ntpPacketBuffer, 0, 48); // NTP request packet (see RFC 5905) ntpPacketBuffer[0] = 0x1B; // LI=0, VN=3, Mode=3 (client) Udp.beginPacket(address, 123); // NTP port Udp.write(ntpPacketBuffer, 48); Udp.endPacket(); } // Update the full screen (IP, clock, last sync) void updateFullScreen() { display.setRotation(1); display.setFullWindow(); display.firstPage(); do { display.fillScreen(GxEPD_WHITE); // --- Top: IP Address --- display.setFont(&FreeMono9pt7b); display.setTextSize(1); display.setTextColor(GxEPD_BLACK); char ipStr[16]; sprintf(ipStr, "%d.%d.%d.%d", WiFi.localIP()[0], WiFi.localIP()[1], WiFi.localIP()[2], WiFi.localIP()[3]); int16_t ipX = 5; display.setCursor(ipX, 15); display.print(ipStr); // Internal temperature float internalTemp = temperatureRead(); sprintf(ipStr, "Internal: %.1f C", internalTemp); display.setCursor(ipX, 30); display.print(ipStr); // --- Center: Clock (HH:MM) --- //display.setFont(&FreeSerifBold18pt7b); display.setFont(&DSEG7_Classic_Bold_58); display.setTextSize(1); char timeStr[6]; sprintf(timeStr, "%02d:%02d", hour(), minute()); int16_t tbx, tby; uint16_t tbw, tbh; display.getTextBounds(timeStr, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((display.width() - tbw) / 2) - tbx; uint16_t y = ((display.height() - tbh) / 2) - tby; display.setCursor(x, y); display.print(timeStr); // --- Bottom: Last NTP Sync --- display.setFont(&FreeMono9pt7b); display.setTextSize(1); char syncStr[20]; sprintf(syncStr, "Last sync: %02d:%02d", hour(lastSyncTime), minute(lastSyncTime)); //int16_t syncX = (display.width() - strlen(syncStr) * 6) / 2; int16_t syncX = 5; display.setCursor(syncX, display.height() - 5); display.print(syncStr); sprintf(syncStr, "Stratum: %02d", stratum); //int16_t syncX = (display.width() - strlen(syncStr) * 6) / 2; display.setCursor(syncX, display.height() - 20); display.print(syncStr); sprintf(syncStr, "Root Del: %.5f", rootDelay); //int16_t syncX = (display.width() - strlen(syncStr) * 6) / 2; display.setCursor(syncX, display.height() - 35); display.print(syncStr); if (stratum <= 1 ) { // Stratum 1: 4 char code of the reference clock sprintf(syncStr, "R: %c%c%c%c", ntpPacketBuffer[12], ntpPacketBuffer[13], ntpPacketBuffer[14], ntpPacketBuffer[15]); } else if (stratum >= 2 && stratum <= 15) { // Stratum 2-15: IPv4 address of the reference server int count = (ntpPacketBuffer[12] >= 100) + (ntpPacketBuffer[13] >= 100) + (ntpPacketBuffer[14] >= 100) + (ntpPacketBuffer[15] >= 100); if(count == 4) { //At last 4 values are above 100 and below 255 sprintf(syncStr, "R:%d.%d.%d.%d", ntpPacketBuffer[12], ntpPacketBuffer[13], ntpPacketBuffer[14], ntpPacketBuffer[15]); } else if (count == 3) { sprintf(syncStr, "Re:%d.%d.%d.%d", ntpPacketBuffer[12], ntpPacketBuffer[13], ntpPacketBuffer[14], ntpPacketBuffer[15]); }else if (count == 2) { sprintf(syncStr, "Ref:%d.%d.%d.%d", ntpPacketBuffer[12], ntpPacketBuffer[13], ntpPacketBuffer[14], ntpPacketBuffer[15]); } else if (count <= 1) { sprintf(syncStr, "Ref :%d.%d.%d.%d", ntpPacketBuffer[12], ntpPacketBuffer[13], ntpPacketBuffer[14], ntpPacketBuffer[15]); } } else if (stratum == 16) { // Stratum 16: Unsynchronized (reserved) sprintf(syncStr, "Unsynchronized"); } //int16_t syncX = (display.width() - strlen(syncStr) * 6) / 2; display.setCursor(syncX, display.height() - 50); display.print(syncStr); } while (display.nextPage()); }