From b8f80dad48f255246ec3ba4d0f9814b62671acd5 Mon Sep 17 00:00:00 2001 From: Jared Dohrman Date: Tue, 9 Jun 2026 20:49:07 +1000 Subject: [PATCH] feat: add CLI-configurable NTP servers --- eastmesh-docs/custom-cli.md | 11 +++++ examples/simple_repeater/MyMesh.cpp | 24 ++++++++++ src/helpers/NetworkPrefs.cpp | 24 +++++++++- src/helpers/NetworkPrefs.h | 3 ++ src/helpers/NetworkService.cpp | 71 ++++++++++++++++++++++++++++- src/helpers/NetworkService.h | 4 ++ 6 files changed, 135 insertions(+), 2 deletions(-) diff --git a/eastmesh-docs/custom-cli.md b/eastmesh-docs/custom-cli.md index 7f6ed067..b1df7217 100644 --- a/eastmesh-docs/custom-cli.md +++ b/eastmesh-docs/custom-cli.md @@ -109,6 +109,17 @@ Legacy dotted aliases are also accepted: - `get wifi.powersaving`: shows the current Wi-Fi power save mode. - `set wifi.powersaving none|min|max`: sets Wi-Fi power saving mode. +### NTP Settings For Observers + +- `get ntp.server1`: shows the primary NTP server. +- `get ntp.server2`: shows the secondary NTP server. +- `get ntp.server3`: shows the tertiary NTP server. +- `set ntp.server1 `: sets the primary NTP server and restarts time sync. +- `set ntp.server2 `: sets the secondary NTP server and restarts time sync. +- `set ntp.server3 `: sets the tertiary NTP server and restarts time sync. + +Default servers are `au.pool.ntp.org`, `time.google.com`, and `time.cloudflare.com`. + ### ESP-NOW Bridge Settings For Observer ESP-NOW Builds These commands are available on `*_repeater_observer_espnow` firmware targets. diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 941d6e8c..f7c21091 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -2335,6 +2335,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply sprintf(reply, "> %s", network.getWifiSSID()[0] ? network.getWifiSSID() : "-"); } else if (strcmp(command, "get wifi.powersaving") == 0) { sprintf(reply, "> %s", network.getWifiPowerSave()); + } else if (strcmp(command, "get ntp.server1") == 0) { + sprintf(reply, "> %s", network.getNtpServer(1)); + } else if (strcmp(command, "get ntp.server2") == 0) { + sprintf(reply, "> %s", network.getNtpServer(2)); + } else if (strcmp(command, "get ntp.server3") == 0) { + sprintf(reply, "> %s", network.getNtpServer(3)); #endif #if defined(ESP_PLATFORM) && WITH_WEB_PANEL } else if (memcmp(command, "set web ", 8) == 0) { @@ -2378,6 +2384,24 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - use none|min|max"); } + } else if (memcmp(command, "set ntp.server1 ", 16) == 0) { + if (network.setNtpServer(1, &command[16])) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - bad ntp.server1"); + } + } else if (memcmp(command, "set ntp.server2 ", 16) == 0) { + if (network.setNtpServer(2, &command[16])) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - bad ntp.server2"); + } + } else if (memcmp(command, "set ntp.server3 ", 16) == 0) { + if (network.setNtpServer(3, &command[16])) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Err - bad ntp.server3"); + } #endif #ifdef WITH_MQTT_UPLINK } else if (memcmp(command, "mqtt.owner ", 11) == 0) { diff --git a/src/helpers/NetworkPrefs.cpp b/src/helpers/NetworkPrefs.cpp index 7d76c670..02f0f295 100644 --- a/src/helpers/NetworkPrefs.cpp +++ b/src/helpers/NetworkPrefs.cpp @@ -9,6 +9,10 @@ namespace { +constexpr const char* kDefaultNtpServer1 = "au.pool.ntp.org"; +constexpr const char* kDefaultNtpServer2 = "time.google.com"; +constexpr const char* kDefaultNtpServer3 = "time.cloudflare.com"; + struct LegacyWebPrefsV1 { uint32_t magic; uint8_t web_enabled; @@ -86,6 +90,18 @@ bool saveNvsNetworkPrefs(const NetworkPrefs&) { } #endif +void applyNtpDefaults(NetworkPrefs& prefs) { + if (prefs.ntp_server1[0] == 0) { + StrHelper::strncpy(prefs.ntp_server1, kDefaultNtpServer1, sizeof(prefs.ntp_server1)); + } + if (prefs.ntp_server2[0] == 0) { + StrHelper::strncpy(prefs.ntp_server2, kDefaultNtpServer2, sizeof(prefs.ntp_server2)); + } + if (prefs.ntp_server3[0] == 0) { + StrHelper::strncpy(prefs.ntp_server3, kDefaultNtpServer3, sizeof(prefs.ntp_server3)); + } +} + } // namespace void NetworkPrefsStore::setDefaults(NetworkPrefs& prefs) { @@ -93,6 +109,7 @@ void NetworkPrefsStore::setDefaults(NetworkPrefs& prefs) { prefs.magic = kMagic; prefs.wifi_powersave = 0; prefs.wifi_channel = 0; + applyNtpDefaults(prefs); } bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs, @@ -101,12 +118,15 @@ bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs, const char* legacy_wifi_pwd) { setDefaults(prefs); if (fs == nullptr) { - loadNvsNetworkPrefs(prefs); + if (loadNvsNetworkPrefs(prefs)) { + applyNtpDefaults(prefs); + } return false; } if (!fs->exists(kFilename)) { if (loadNvsNetworkPrefs(prefs)) { + applyNtpDefaults(prefs); save(fs, prefs); return true; } @@ -140,6 +160,7 @@ bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs, if (!ok || persisted.magic != kMagic) { fs->remove(kFilename); if (loadNvsNetworkPrefs(prefs)) { + applyNtpDefaults(prefs); save(fs, prefs); return true; } @@ -148,6 +169,7 @@ bool NetworkPrefsStore::load(FILESYSTEM* fs, NetworkPrefs& prefs, } prefs = persisted; + applyNtpDefaults(prefs); if (prefs.wifi_powersave > 2) { prefs.wifi_powersave = 0; } diff --git a/src/helpers/NetworkPrefs.h b/src/helpers/NetworkPrefs.h index 9eb9096b..9aaeefac 100644 --- a/src/helpers/NetworkPrefs.h +++ b/src/helpers/NetworkPrefs.h @@ -10,6 +10,9 @@ struct NetworkPrefs { uint8_t reserved[2]; char wifi_ssid[33]; char wifi_pwd[65]; + char ntp_server1[64]; + char ntp_server2[64]; + char ntp_server3[64]; }; class NetworkPrefsStore { diff --git a/src/helpers/NetworkService.cpp b/src/helpers/NetworkService.cpp index ae72c030..bc43d309 100644 --- a/src/helpers/NetworkService.cpp +++ b/src/helpers/NetworkService.cpp @@ -16,6 +16,7 @@ constexpr unsigned long kWifiRetryMillis = 15000; constexpr unsigned long kWifiConnectTimeoutMillis = 45000; constexpr unsigned long kWifiChannelHintTimeoutMillis = 7000; constexpr time_t kMinSaneEpoch = 1735689600; // 2025-01-01T00:00:00Z +constexpr size_t kNtpServerMaxLen = 64; bool isValidWifiChannel(uint8_t channel) { return channel >= 1 && channel <= 14; @@ -119,6 +120,69 @@ bool NetworkService::setWifiPassword(const char* pwd) { return ok; } +bool NetworkService::isValidNtpServer(const char* server) { + if (server == nullptr || server[0] == 0) { + return false; + } + size_t len = 0; + while (server[len] != 0) { + const char c = server[len]; + if (c <= ' ' || c == ',' || c == '\x7F') { + return false; + } + len++; + if (len >= kNtpServerMaxLen) { + return false; + } + } + return true; +} + +bool NetworkService::setNtpServer(uint8_t index, const char* server) { + if (!isValidNtpServer(server)) { + return false; + } + + char* target = nullptr; + switch (index) { + case 1: + target = _prefs.ntp_server1; + break; + case 2: + target = _prefs.ntp_server2; + break; + case 3: + target = _prefs.ntp_server3; + break; + default: + return false; + } + + if (strcmp(target, server) == 0) { + return true; + } + + StrHelper::strncpy(target, server, sizeof(_prefs.ntp_server1)); + const bool ok = savePrefs(); +#if defined(ESP_PLATFORM) + restartTimeSync(); +#endif + return ok; +} + +const char* NetworkService::getNtpServer(uint8_t index) const { + switch (index) { + case 1: + return _prefs.ntp_server1; + case 2: + return _prefs.ntp_server2; + case 3: + return _prefs.ntp_server3; + default: + return ""; + } +} + bool NetworkService::setWifiPowerSave(const char* mode) { if (mode == nullptr) { return false; @@ -242,6 +306,11 @@ void NetworkService::reconnectWifi() { _last_wifi_attempt = 0; } +void NetworkService::restartTimeSync() { + _sntp_started = false; + _have_time_sync = false; +} + bool NetworkService::isWifiConnected() const { #if defined(ESP_PLATFORM) return _wifi_started && WiFi.status() == WL_CONNECTED; @@ -372,7 +441,7 @@ void NetworkService::updateTimeSync() { } if (!_sntp_started) { - configTzTime("UTC0", "au.pool.ntp.org", "time.google.com", "time.cloudflare.com"); + configTzTime("UTC0", _prefs.ntp_server1, _prefs.ntp_server2, _prefs.ntp_server3); _sntp_started = true; } diff --git a/src/helpers/NetworkService.h b/src/helpers/NetworkService.h index 470e8b76..ef911f62 100644 --- a/src/helpers/NetworkService.h +++ b/src/helpers/NetworkService.h @@ -26,6 +26,8 @@ public: const char* getWifiSSID() const { return _prefs.wifi_ssid; } bool setWifiPowerSave(const char* mode); const char* getWifiPowerSave() const; + bool setNtpServer(uint8_t index, const char* server); + const char* getNtpServer(uint8_t index) const; void formatWifiStatusReply(char* reply, size_t reply_size) const; void reconnectWifi(); @@ -38,7 +40,9 @@ private: static const char* getPowerSaveLabel(uint8_t mode); void ensureWifi(bool network_required); void updateTimeSync(); + void restartTimeSync(); #endif + static bool isValidNtpServer(const char* server); bool savePrefs(); FILESYSTEM* _fs;