Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,28 @@ jobs:
fail-fast: false
matrix:
config:
- name: NodeMCU (ESP8266)
# TestNTPClient
- name: TestNTPClient (ESP8266)
fqbn: esp8266:esp8266:nodemcuv2
sketch: examples/NodeMCU/NodeMCU.ino
sketch: examples/TestNTPClient/TestNTPClient.ino
platforms: |
- name: esp8266:esp8266
source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json
- name: TestNodeMCU (ESP8266)
- name: TestNTPClient (ESP32)
fqbn: esp32:esp32:esp32
sketch: examples/TestNTPClient/TestNTPClient.ino
platforms: |
- name: esp32:esp32
source-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
- name: TestNTPClient (Uno+ESP-01)
fqbn: arduino:avr:uno
sketch: examples/TestNTPClient/TestNTPClient.ino
platforms: |
- name: arduino:avr
# examples
- name: NodeMCU (ESP8266)
fqbn: esp8266:esp8266:nodemcuv2
sketch: examples/TestNodeMCU/TestNodeMCU.ino
sketch: examples/NodeMCU/NodeMCU.ino
platforms: |
- name: esp8266:esp8266
source-url: https://arduino.esp8266.com/stable/package_esp8266com_index.json
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
/*
EasyNTPClient test suite — NodeMCU (ESP8266)
EasyNTPClient test suite — ESP8266, ESP32, Uno+ESP-01
Open Serial Monitor at 115200 baud after flashing.
Fill in WIFI_SSID and WIFI_PASSWORD before flashing.
*/

#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#elif defined(ESP32)
#include <WiFi.h>
#include <WiFiUdp.h>
#include "esp_task_wdt.h"
#else
#include <SoftwareSerial.h>
#include <WiFiEsp.h>
#include <WiFiEspUdp.h>
#endif
#include <EasyNTPClient.h>

#if defined(ESP8266) || defined(ESP32)
using NTPUdp = WiFiUDP;
#else
static SoftwareSerial esp_serial(3, 2); // D3→ESP-01 TX, D2→ESP-01 RX
using NTPUdp = WiFiEspUDP;
#endif

const char* WIFI_SSID = "<SSID>";
const char* WIFI_PASSWORD = "<PASSWORD>";

Expand All @@ -21,57 +38,57 @@ static const unsigned long MAX_UNIX_2030 = 1893456000UL;
int g_passed = 0;
int g_failed = 0;

void check(bool condition, const char* label) {
if (condition) { Serial.print("[PASS] "); g_passed++; }
else { Serial.print("[FAIL] "); g_failed++; }
void check(bool condition, const __FlashStringHelper* label) {
if (condition) { Serial.print(F("[PASS] ")); g_passed++; }
else { Serial.print(F("[FAIL] ")); g_failed++; }
Serial.println(label);
}

// ── WiFi ─────────────────────────────────────────────────────────────────────

void wifi_connect() {
Serial.print("Connecting to WiFi");
Serial.print(F("Connecting to WiFi"));
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println(" connected.");
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print(F(".")); }
Serial.println(F(" connected."));
}

// ── NTP constants ────────────────────────────────────────────────────────────

void test_constants() {
Serial.println("\n-- NTP constants --");
check(NTP_PACKET_SIZE == 48, "NTP_PACKET_SIZE == 48");
check(NTP_TX_TIMESTAMP_OFFSET == 40, "NTP_TX_TIMESTAMP_OFFSET == 40");
check(NTP_SERVER_PORT == 123, "NTP_SERVER_PORT == 123");
check(NTP_REQUEST_PORT == 1123, "NTP_REQUEST_PORT == 1123");
check(NTP_HEADER_LI == 0b11000000, "NTP_HEADER_LI == 0xC0");
check(NTP_HEADER_VN == 0b00100000, "NTP_HEADER_VN == 0x20");
check(NTP_HEADER_MODE == 0b00000011, "NTP_HEADER_MODE == 0x03");
Serial.println(F("\n-- NTP constants --"));
check(NTP_PACKET_SIZE == 48, F("NTP_PACKET_SIZE == 48"));
check(NTP_TX_TIMESTAMP_OFFSET == 40, F("NTP_TX_TIMESTAMP_OFFSET == 40"));
check(NTP_SERVER_PORT == 123, F("NTP_SERVER_PORT == 123"));
check(NTP_REQUEST_PORT == 1123, F("NTP_REQUEST_PORT == 1123"));
check(NTP_HEADER_LI == 0b11000000, F("NTP_HEADER_LI == 0xC0"));
check(NTP_HEADER_VN == 0b00100000, F("NTP_HEADER_VN == 0x20"));
check(NTP_HEADER_MODE == 0b00000011, F("NTP_HEADER_MODE == 0x03"));
// Combined byte 0 must equal the first byte of the old magic constant 0xEC0600E3.
check((NTP_HEADER_LI | NTP_HEADER_VN | NTP_HEADER_MODE) == 0xE3,
"header byte 0 matches original magic constant 0xE3");
F("header byte 0 matches original magic constant 0xE3"));
}

// ── basic sync ───────────────────────────────────────────────────────────────

void test_basic_sync() {
Serial.println("\n-- basic sync --");
WiFiUDP udp;
Serial.println(F("\n-- basic sync --"));
NTPUdp udp;
EasyNTPClient client(udp, "pool.ntp.org");

unsigned long t = client.getUnixTime();
check(t > MIN_UNIX_2024, "time is after 2024-01-01");
check(t < MAX_UNIX_2030, "time is before 2030-01-01");
check(t > MIN_UNIX_2024, F("time is after 2024-01-01"));
check(t < MAX_UNIX_2030, F("time is before 2030-01-01"));
}

// ── socket reuse ─────────────────────────────────────────────────────────────

void test_client_reuse() {
Serial.println("\n-- socket reuse across client instances --");
Serial.println(F("\n-- socket reuse across client instances --"));

// Both clients share the same WiFiUDP object. Without Fix B+C the second
// Both clients share the same UDP object. Without Fix B+C the second
// begin() is skipped (static flag) and the second sync fails.
WiFiUDP udp;
NTPUdp udp;
unsigned long t1 = 0, t2 = 0;

{
Expand All @@ -84,132 +101,161 @@ void test_client_reuse() {
t2 = c2.getUnixTime(); // Fix C re-runs begin(); Fix B already closed it cleanly
}

check(t1 > MIN_UNIX_2024, "first client syncs successfully");
check(t2 > MIN_UNIX_2024, "second client syncs on same UDP object");
check(t2 >= t1 && (t2 - t1) < 5, "timestamps consistent between clients");
check(t1 > MIN_UNIX_2024, F("first client syncs successfully"));
check(t2 > MIN_UNIX_2024, F("second client syncs on same UDP object"));
check(t2 >= t1 && (t2 - t1) < 5, F("timestamps consistent between clients"));
}

// ── offset decoupling ────────────────────────────────────────────────────────

void test_offset_immediate() {
Serial.println("\n-- offset change takes effect without resync --");
WiFiUDP udp;
Serial.println(F("\n-- offset change takes effect without resync --"));
NTPUdp udp;
EasyNTPClient client(udp, "pool.ntp.org");

// Sync once with offset = 0; capture base time.
client.setTimeOffset(0);
unsigned long base = client.getUnixTime();
check(base > MIN_UNIX_2024, "initial sync with offset=0 succeeds");
check(base > MIN_UNIX_2024, F("initial sync with offset=0 succeeds"));

// Change offset without waiting for a resync (mUpdateInterval not elapsed).
client.setTimeOffset(3600);
unsigned long adjusted = client.getUnixTime();
long delta = (long)adjusted - (long)base;
check(delta >= 3598 && delta <= 3602, "+3600 s offset reflected on next call");
check(delta >= 3598 && delta <= 3602, F("+3600 s offset reflected on next call"));

// Reverse to zero.
client.setTimeOffset(0);
unsigned long restored = client.getUnixTime();
delta = (long)restored - (long)base;
check(delta >= 0 && delta <= 2, "offset=0 reflected immediately after reversal");
check(delta >= 0 && delta <= 2, F("offset=0 reflected immediately after reversal"));
}

// ── wasUpdated flag ──────────────────────────────────────────────────────────

void test_was_updated() {
Serial.println("\n-- wasUpdated() flag --");
WiFiUDP udp;
Serial.println(F("\n-- wasUpdated() flag --"));
NTPUdp udp;
EasyNTPClient client(udp, "pool.ntp.org");

check(!client.wasUpdated(), "wasUpdated() is false before first sync");
check(!client.wasUpdated(), F("wasUpdated() is false before first sync"));

client.getUnixTime();
check(client.wasUpdated(), "wasUpdated() is true after successful sync");
check(client.wasUpdated(), F("wasUpdated() is true after successful sync"));
}

// ── NTP server get/set ───────────────────────────────────────────────────────

void test_set_ntp_server() {
Serial.println("\n-- setNTPServer() / getNTPServer() --");
WiFiUDP udp;
Serial.println(F("\n-- setNTPServer() / getNTPServer() --"));
NTPUdp udp;
EasyNTPClient client(udp, "pool.ntp.org");

check(strcmp(client.getNTPServer(), "pool.ntp.org") == 0,
"getNTPServer() returns initial pool");
F("getNTPServer() returns initial pool"));

client.setNTPServer("time.cloudflare.com");
check(strcmp(client.getNTPServer(), "time.cloudflare.com") == 0,
"getNTPServer() reflects setNTPServer()");
F("getNTPServer() reflects setNTPServer()"));

unsigned long t = client.getUnixTime();
check(t > MIN_UNIX_2024, "syncs from server set via setNTPServer()");
check(t > MIN_UNIX_2024, F("syncs from server set via setNTPServer()"));
}

// ── update interval get/set ───────────────────────────────────────────────────

void test_set_update_interval() {
Serial.println("\n-- setUpdateInterval() / getUpdateInterval() / 4-arg constructor --");
WiFiUDP udp;
Serial.println(F("\n-- setUpdateInterval() / getUpdateInterval() / 4-arg constructor --"));
NTPUdp udp;

EasyNTPClient client(udp, "pool.ntp.org");
client.setUpdateInterval(30);
check(client.getUpdateInterval() == 30, "getUpdateInterval() reflects setUpdateInterval(30 s)");
check(client.getUpdateInterval() == 30,
F("getUpdateInterval() reflects setUpdateInterval(30 s)"));

EasyNTPClient client2(udp, "pool.ntp.org", 0, 120);
check(client2.getUpdateInterval() == 120, "4-arg constructor sets update interval to 120 s");
check(client2.getUpdateInterval() == 120,
F("4-arg constructor sets update interval to 120 s"));

unsigned long t = client2.getUnixTime();
check(t > MIN_UNIX_2024, "syncs with interval set via 4-arg constructor");
check(t > MIN_UNIX_2024, F("syncs with interval set via 4-arg constructor"));
}

// ── stale time preservation ──────────────────────────────────────────────────

void test_stale_time() {
Serial.println("\n-- stale time preserved when resync fails (wait ~70 s) --");
WiFiUDP udp;
#if defined(ESP8266) || defined(ESP32)
Serial.println(F("\n-- stale time preserved when resync fails (wait ~70 s) --"));
NTPUdp udp;
EasyNTPClient client(udp, "pool.ntp.org");

// Populate mServerTime with a good sync.
unsigned long good = client.getUnixTime();
check(good > MIN_UNIX_2024, "initial sync succeeds before WiFi drop");
check(good > MIN_UNIX_2024, F("initial sync succeeds before WiFi drop"));

// Drop WiFi and wait for mUpdateInterval (60 s default) to expire so the
// next getUnixTime() call attempts a resync, fails, and should return the
// preserved mServerTime + millis() drift.
WiFi.disconnect();
Serial.println(" WiFi disconnected. Waiting 65 s...");
Serial.println(F(" WiFi disconnected. Waiting 65 s..."));
#if defined(ESP32)
// vTaskDelay() does not reset the TWDT; feed it once per second.
for (int i = 0; i < 65; i++) { delay(1000); esp_task_wdt_reset(); }
#else
delay(65000);
#endif

unsigned long stale = client.getUnixTime();
long drift = (long)stale - (long)good;
// Allow 63-72 s: 65 s delay plus up to 7 s for sync timeout polling.
check(drift >= 63 && drift <= 72, "stale time preserved and advancing during no-WiFi");
check(drift >= 63 && drift <= 72,
F("stale time preserved and advancing during no-WiFi"));

// Reconnect before the next test group.
wifi_connect();
#else
Serial.println(F("\n-- stale time: skipped"
" (WiFiEsp does not support WiFi.disconnect() on AVR) --"));
#endif
}

// ── entry points ─────────────────────────────────────────────────────────────

#define RUN(n, fn) do { \
Serial.println(F("\n>>> [" #n "/8] " #fn)); \
int _p = g_passed, _f = g_failed; \
fn(); \
Serial.print(F("<<< [" #n "/8] ")); \
Serial.print(g_passed - _p); Serial.print(F(" passed, ")); \
Serial.print(g_failed - _f); Serial.println(F(" failed")); \
} while (0)

void setup() {
Serial.begin(115200);
delay(100);
Serial.println("\n=== EasyNTPClient test suite ===");
Serial.println(F("\n=== EasyNTPClient test suite ==="));

#if !defined(ESP8266) && !defined(ESP32)
esp_serial.begin(9600);
WiFi.init(&esp_serial);
#endif

wifi_connect();

test_constants();
test_basic_sync();
test_client_reuse();
test_offset_immediate();
test_was_updated();
test_set_ntp_server();
test_set_update_interval();
test_stale_time();

Serial.println("\n=== Results ===");
Serial.print(g_passed); Serial.println(" passed");
Serial.print(g_failed); Serial.println(" failed");
RUN(1, test_constants);
RUN(2, test_basic_sync);
RUN(3, test_client_reuse);
RUN(4, test_offset_immediate);
RUN(5, test_was_updated);
RUN(6, test_set_ntp_server);
RUN(7, test_set_update_interval);
RUN(8, test_stale_time);

Serial.println(F("\n=== Results ==="));
Serial.print(g_passed); Serial.println(F(" passed"));
Serial.print(g_failed); Serial.println(F(" failed"));
}

#undef RUN

void loop() {}
Loading