sntp.cpp
1 #include "sntp.h" 2 #include "../services/rtc.h" 3 4 #include <atomic> 5 #include <Arduino.h> 6 #include <time.h> 7 #include "esp_sntp.h" 8 9 static std::atomic<bool> synced = false; 10 static std::atomic<uint32_t> synced_epoch = 0; 11 12 static void on_time_sync(struct timeval *tv) { 13 (void)tv; 14 time_t now_utc; 15 time(&now_utc); 16 synced_epoch.store((uint32_t)now_utc, std::memory_order_release); 17 synced.store(true, std::memory_order_release); 18 Serial.printf("[ntp] synced, epoch=%lu\n", (unsigned long)now_utc); 19 } 20 21 bool networking::sntp::sync() { 22 synced.store(false, std::memory_order_relaxed); 23 synced_epoch.store(0, std::memory_order_relaxed); 24 25 setenv("TZ", config::sntp::TIME_ZONE, 1); 26 tzset(); 27 28 sntp_set_time_sync_notification_cb(on_time_sync); 29 configTzTime(config::sntp::TIME_ZONE, config::sntp::SERVER_1, config::sntp::SERVER_2); 30 31 uint32_t start = millis(); 32 while (!synced.load(std::memory_order_acquire) && (millis() - start) < config::sntp::SYNC_TIMEOUT_MS) { 33 vTaskDelay(pdMS_TO_TICKS(100)); 34 } 35 36 uint32_t synced_epoch_value = synced_epoch.load(std::memory_order_acquire); 37 if (synced.load(std::memory_order_acquire) && synced_epoch_value > 0) { 38 services::rtc::setEpoch(synced_epoch_value); 39 Serial.printf("[ntp] local time: %s\n", networking::sntp::accessLocalTimeString()); 40 } else { 41 Serial.println(F("[ntp] sync timeout — using RTC time")); 42 } 43 44 return synced; 45 } 46 47 bool networking::sntp::isSynced() { 48 return synced.load(std::memory_order_acquire); 49 } 50 51 const char *networking::sntp::accessLocalTimeString() { 52 static char buf[32]; 53 struct tm timeinfo; 54 if (!getLocalTime(&timeinfo, 0)) { 55 snprintf(buf, sizeof(buf), "(no time)"); 56 return buf; 57 } 58 strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %Z", &timeinfo); 59 return buf; 60 } 61 62 uint32_t networking::sntp::accessUTCEpoch() { 63 time_t now_utc; 64 time(&now_utc); 65 return (uint32_t)now_utc; 66 } 67 68 // ───────────────────────────────────────────────────────────────────────────── 69 // Tests 70 // ───────────────────────────────────────────────────────────────────────────── 71 #ifdef PIO_UNIT_TESTING 72 73 74 #include "sntp.h" 75 #include <networking/wifi.h> 76 #include "services/rtc.h" 77 #include <testing/utils.h> 78 79 80 namespace networking::sntp { void test(void); } 81 82 static WifiNvsSnapshot saved; 83 static void save_nvs(void) { wifi_nvs_save(&saved); } 84 static void restore_nvs(void) { wifi_nvs_restore(&saved); } 85 86 static void test_sntp_config_defaults(void) { 87 GIVEN("the NTP configuration"); 88 THEN("all required fields are set"); 89 90 TEST_ASSERT_NOT_NULL_MESSAGE(config::sntp::SERVER_1, 91 "device: NTP server must not be NULL"); 92 TEST_ASSERT_NOT_EMPTY_MESSAGE(config::sntp::SERVER_1, 93 "device: NTP server must not be empty"); 94 TEST_ASSERT_NOT_NULL_MESSAGE(config::sntp::TIME_ZONE, 95 "device: timezone must not be NULL"); 96 TEST_ASSERT_NOT_EMPTY_MESSAGE(config::sntp::TIME_ZONE, 97 "device: timezone must not be empty"); 98 TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(0, config::sntp::SYNC_TIMEOUT_MS, 99 "device: sync timeout must be > 0"); 100 101 } 102 103 static void test_sntp_not_synced_before_connect(void) { 104 GIVEN("WiFi has not connected yet"); 105 THEN("NTP reports not synced"); 106 107 TEST_ASSERT_FALSE_MESSAGE(networking::sntp::isSynced(), 108 "device: NTP should not be synced before wifi_connect"); 109 110 } 111 112 static void test_sntp_syncs_and_updates_rtc(void) { 113 GIVEN("WiFi is connected"); 114 WHEN("NTP sync completes"); 115 THEN("the RTC is updated to match"); 116 117 save_nvs(); 118 119 networking::wifi::sta::initialize(); 120 if (!networking::wifi::sta::connect()) { 121 restore_nvs(); 122 TEST_IGNORE_MESSAGE("skipped — no WiFi connection"); 123 return; 124 } 125 126 services::rtc::initialize(); 127 uint32_t rtc_before = services::rtc::accessEpoch(); 128 129 bool synced = networking::sntp::sync(); 130 if (!synced) { 131 restore_nvs(); 132 TEST_IGNORE_MESSAGE("skipped — NTP sync timed out"); 133 return; 134 } 135 136 TEST_MESSAGE(networking::sntp::accessLocalTimeString()); 137 138 uint32_t rtc_after = services::rtc::accessEpoch(); 139 uint32_t ntp_epoch = networking::sntp::accessUTCEpoch(); 140 141 TEST_ASSERT_UINT32_WITHIN_MESSAGE(2, ntp_epoch, rtc_after, 142 "device: RTC epoch diverges from NTP by more than 2 seconds"); 143 144 uint32_t drift_before = (rtc_before > ntp_epoch) ? rtc_before - ntp_epoch : ntp_epoch - rtc_before; 145 uint32_t drift_after = (rtc_after > ntp_epoch) ? rtc_after - ntp_epoch : ntp_epoch - rtc_after; 146 TEST_ASSERT_LESS_OR_EQUAL_UINT32_MESSAGE(drift_before, drift_after, 147 "device: RTC did not get closer to NTP after sync — ds3231_set_epoch may not have been called"); 148 149 TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE(1735689600, ntp_epoch, 150 "device: NTP epoch is before 2025 — sync may have failed"); 151 152 restore_nvs(); 153 } 154 155 void networking::sntp::test(void) { 156 MODULE("NTP"); 157 RUN_TEST(test_sntp_config_defaults); 158 RUN_TEST(test_sntp_not_synced_before_connect); 159 RUN_TEST(test_sntp_syncs_and_updates_rtc); 160 } 161 162 #endif