From 3e279738b6eeb82aa6dce8fba3e2fdcf2390d66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Mon, 5 Oct 2026 23:52:22 +0200 Subject: [PATCH] S1 #7 step 3: the Wi-Fi Service applies IP, DNS and NTP settings Joining a Saved Network uses its Fixed address, mask and gateway, or DHCP. DNS comes from Settings on Fixed networks and when "Always use my DNS" is on; NTP servers come from Settings, after any that DHCP offered. Both are re-checked every 30 s, since a DHCP renewal puts DHCP's DNS back and clears the NTP slots it didn't fill. `wifi status` shows what's in use, where it came from, and which NTP servers answered; `wifi ip`, `wifi dns`, `wifi ntp`. Debug Builds: `wifi ip ... try ` reverts unless kept. On knbg-guests (10.39.39.0/24, gateway .1): Fixed .12 and .13 both reach the internet through 9.9.9.9; a wrong gateway on trial cut the device off and came back by itself; back to DHCP; both NTP servers answer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- src/main.cpp | 96 +++++++++++++++++++++++++- src/services/wifi_service.cpp | 124 +++++++++++++++++++++++++++++++--- src/services/wifi_service.h | 28 ++++++++ 3 files changed, 239 insertions(+), 9 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 36beec4..5ee043b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -18,6 +18,7 @@ #include "apps/setup_app.h" #include "event_bus.h" #include "file_receiver.h" +#include "ipv4.h" #include "key_mapper.h" #include "platform/console.h" #include "platform/crash_report.h" @@ -359,6 +360,27 @@ static void uploadStep() { } } +#ifdef RORO_DEBUG +// `wifi ip ... try `: a trial IP setting that reverts unless `wifi ip keep` arrives, so a +// wrong address tried over Wi-Fi doesn't cut the device off for good. +static struct { + bool active = false; + uint32_t untilMs = 0; + std::string ssid; + bool wasFixed = false; + net::FixedIp was; +} ipTrial; + +static void ipTrialStep() { + if (!ipTrial.active || static_cast(millis() - ipTrial.untilMs) < 0) return; + ipTrial.active = false; + savedNetworks->setIp(ipTrial.ssid, ipTrial.wasFixed ? &ipTrial.was : nullptr); + console.printf("wifi ip: trial over, %s is back to %s\n", ipTrial.ssid.c_str(), + ipTrial.wasFixed ? net::formatFixed(ipTrial.was).c_str() : "Automatic (DHCP)"); + wifi->ipSettingChanged(ipTrial.ssid); +} +#endif + static const char* const kHelp = "info firmware, uptime, memory, Wi-Fi, app slots\n" "tasks FreeRTOS tasks: state, priority, free stack, CPU\n" @@ -374,12 +396,15 @@ static const char* const kHelp = "coredump erase forget the core dump in flash\n" "key press a key: up down left right select back home del tab space, or one character\n" "wifi status | wifi add \n" + "wifi ip dhcp | wifi ip
/ [gateway] a Saved Network's IP setting\n" + "wifi dns [b] | wifi dns always on|off | wifi ntp [b] DNS and NTP servers\n" "gemini get fetch a Gemini page and report header, size, certificate, heap\n" "irc start | irc stop | irc dump | irc say \n" "ls [folder] | rm | install (Update from SD)\n" "sd card | sd list | cat | log | burst | sound on|off | short | normal\n" #ifdef RORO_DEBUG "crash abort|wdt crash on purpose (to test crash reports and Safe Mode)\n" + "wifi ip ... try | wifi ip keep a trial IP setting: back to the previous one unless kept\n" "lora inject [rssi] [snr] a packet into the LoRa Scanner as if received (nothing is sent)\n" "coredump get (Debug Console only) send the raw core dump: use scripts/rdbg.py coredump\n" "reset (Debug Console only) restart at once, even if the main loop is stuck\n" @@ -588,11 +613,77 @@ static void runCommand(String line) { } }); } - if (line == "wifi status") + if (line == "wifi status") { console.printf("wifi: state %d ssid '%s' rssi %d ip %s clock %s heap %u min %u | fw %s%s\n", (int)wifi->state(), wifi->ssid().c_str(), wifi->rssi(), wifi->ip().c_str(), clockService->displayTime().c_str(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), versionString(), update->onProbation() ? " (on probation)" : ""); + WifiService::Connection c = wifi->connection(); // S1, Q115: what's in use and where it came from + if (c.connected) { + console.printf("wifi: address %s/%d (%s), gateway %s\n", c.address.c_str(), c.prefix, c.fixed ? "fixed" : "DHCP", + c.gateway.empty() ? "none" : c.gateway.c_str()); + console.printf("wifi: dns %s %s (%s)\n", c.dns[0].empty() ? "none" : c.dns[0].c_str(), c.dns[1].c_str(), + c.dnsFromSettings ? "Settings" : "DHCP"); + console.print("wifi: ntp"); + for (int i = 0; i < c.ntpCount; i++) console.printf(" %s (%s%s)", c.ntp[i].server.c_str(), c.ntp[i].fromDhcp ? "DHCP" : "Settings", c.ntp[i].answered ? ", answered" : ""); + console.println(c.ntpCount ? "" : " none"); + } + } + if (line.startsWith("wifi ip ")) { // wifi ip dhcp |
/ [gateway] (the SSID may hold spaces) + std::string rest = line.substring(8).c_str(); +#ifdef RORO_DEBUG + if (rest == "keep") { // the trial setting stays + console.println(ipTrial.active ? "wifi ip: kept" : "wifi ip: no trial running"); + ipTrial.active = false; + return; + } + uint32_t trialS = 0; + size_t tryAt = rest.rfind(" try "); + if (tryAt != std::string::npos) { + trialS = strtoul(rest.c_str() + tryAt + 5, nullptr, 10); + rest = rest.substr(0, tryAt); + } +#endif + std::string ssid, why; + net::FixedIp fixed; + bool dhcp = rest.size() > 5 && rest.compare(rest.size() - 5, 5, " dhcp") == 0; + if (dhcp) ssid = rest.substr(0, rest.size() - 5); + else { + size_t slash = rest.rfind('/'), space = slash == std::string::npos ? slash : rest.rfind(' ', slash); + if (space == std::string::npos) why = "usage: wifi ip dhcp |
/ [gateway]"; + else { + ssid = rest.substr(0, space); + why = net::parseFixed(rest.substr(space + 1), fixed); + } + } + const SavedNetwork* before = why.empty() ? savedNetworks->find(ssid) : nullptr; +#ifdef RORO_DEBUG + if (before && trialS) ipTrial = {true, millis() + trialS * 1000, ssid, before->fixed, before->ip}; +#endif + if (why.empty()) why = savedNetworks->setIp(ssid, dhcp ? nullptr : &fixed); + if (!why.empty()) return (void)console.printf("wifi ip: %s\n", why.c_str()); + console.printf("wifi ip: %s is now %s\n", ssid.c_str(), dhcp ? "Automatic (DHCP)" : net::formatFixed(fixed).c_str()); + wifi->ipSettingChanged(ssid); + } + if ((line.startsWith("wifi dns ") && !line.startsWith("wifi dns always")) || line.startsWith("wifi ntp ")) { // [b] + bool dns = line.startsWith("wifi dns "); + std::string rest = line.substring(9).c_str(); + size_t space = rest.find(' '); + std::string first = rest.substr(0, space), second = space == std::string::npos ? "" : rest.substr(space + 1); + Setting a = dns ? Setting::Dns1 : Setting::Ntp1, b = dns ? Setting::Dns2 : Setting::Ntp2; + std::string oldFirst = settings.getString(a); + // Both or neither: a refused second entry leaves the first as it was. + bool ok = settings.setString(a, first) && (settings.setString(b, second) || (settings.setString(a, oldFirst), false)); + if (!ok) console.printf("wifi %s: %s\n", dns ? "dns" : "ntp", dns ? "an IPv4 address, then a second one if you like" : "a host name or an IPv4 address, then a second one if you like"); + else { + console.printf("wifi %s: %s %s\n", dns ? "dns" : "ntp", first.c_str(), second.c_str()); + wifi->serversChanged(); + } + } + if (line == "wifi dns always on" || line == "wifi dns always off") { + settings.setBool(Setting::DnsAlways, line.endsWith("on")); + wifi->serversChanged(); + } if (line == "sound off") settings.setBool(Setting::Sound, false); if (line == "sound on") settings.setBool(Setting::Sound, true); if (line == "short") { @@ -671,6 +762,9 @@ void loop() { serialCommands(); remoteCommands(); +#ifdef RORO_DEBUG + ipTrialStep(); +#endif noteStableOnce(now); uploadStep(); printListingWhenReady(); diff --git a/src/services/wifi_service.cpp b/src/services/wifi_service.cpp index 536c1f0..f7971a9 100644 --- a/src/services/wifi_service.cpp +++ b/src/services/wifi_service.cpp @@ -3,10 +3,121 @@ #include #include #include +#include + +#include + +#include "ipv4.h" namespace roro { +namespace { +IPAddress toIp(uint32_t a) { return IPAddress(a >> 24, a >> 16 & 255, a >> 8 & 255, a & 255); } +IPAddress toIp(const std::string& text) { + uint32_t a = 0; + net::parseIpv4(text, a); + return toIp(a); +} +const IPAddress kNoAddress(static_cast(0)); +// A slot lwIP filled from DHCP has an address and no name; ours are set by name. +bool sntpSlotFromDhcp(int i) { + const ip_addr_t* a = esp_sntp_getserver(i); + return !esp_sntp_getservername(i) && a && !ip_addr_isany(a); +} +constexpr int kNtpSlots = 3; // CONFIG_LWIP_SNTP_MAX_SERVERS +constexpr uint32_t kServersEveryMs = 30000; +} // namespace + +// Joins a Saved Network with its IP setting (S1, Q105): Fixed, or DHCP. +void WifiService::join(const std::string& ssid, const std::string& password) { + const SavedNetwork* n = saved_.find(ssid); + fixed_ = n && n->fixed; + if (fixed_) { + // Nothing comes from DHCP here: forget the NTP servers a previous network offered. + for (int i = 0; i < kNtpSlots; i++) + if (sntpSlotFromDhcp(i)) esp_sntp_setserver(i, nullptr); + WiFi.config(toIp(n->ip.address), toIp(n->ip.gateway), toIp(net::maskOf(n->ip.prefix)), + toIp(settings_.getString(Setting::Dns1)), toIp(settings_.getString(Setting::Dns2))); + } else { + WiFi.config(kNoAddress, kNoAddress, kNoAddress); // DHCP + } + WiFi.begin(ssid.c_str(), password.empty() ? nullptr : password.c_str()); +} + +void WifiService::ipSettingChanged(const std::string& ssid) { + if (controller_.state() != WifiController::State::Connected || controller_.ssid() != ssid) return; + WiFi.disconnect(); + apply(controller_.disconnected(millis())); + controller_.retryNow(millis()); +} + +// DNS and NTP as decided in Q108 and Q110. Run when connected, when a setting changes, and now +// and then: a DHCP renewal puts DHCP's DNS back and clears the NTP slots it didn't fill. +void WifiService::applyServers(Why why) { + if (controller_.state() != WifiController::State::Connected) return; + serversCheckedMs_ = millis(); + bool changed = why != Why::Check; + + bool wasFromSettings = dnsFromSettings_; + dnsFromSettings_ = fixed_ || settings_.getBool(Setting::DnsAlways); + if (dnsFromSettings_) { + IPAddress dns1 = toIp(settings_.getString(Setting::Dns1)), dns2 = toIp(settings_.getString(Setting::Dns2)); + if (WiFi.dnsIP(0) != dns1 || WiFi.dnsIP(1) != dns2) WiFi.setDNS(dns1, dns2); + } else if (why == Why::SettingsChanged && wasFromSettings) { + // "Always use my DNS" was switched off: only a new lease brings DHCP's servers back. + ipSettingChanged(controller_.ssid()); + return; + } + + // NTP: the servers DHCP offered stay first; ours follow. + int fromDhcp = 0; + while (!fixed_ && fromDhcp < kNtpSlots && sntpSlotFromDhcp(fromDhcp)) fromDhcp++; + std::string wanted[2] = {settings_.getString(Setting::Ntp1), settings_.getString(Setting::Ntp2)}; + bool touched = false; + for (int i = fromDhcp, mine = 0; i < kNtpSlots; i++, mine++) { + const char* current = esp_sntp_getservername(i); + if (mine < 2 && !wanted[mine].empty()) { + if (current && wanted[mine] == current) continue; + ntpNames_[mine] = wanted[mine]; + esp_sntp_setservername(i, ntpNames_[mine].c_str()); + touched = true; + } else if (current || sntpSlotFromDhcp(i)) { + esp_sntp_setserver(i, nullptr); + touched = true; + } + } + if (!esp_sntp_enabled()) { + esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); + esp_sntp_init(); + } else if (touched || changed) { + esp_sntp_restart(); + } + if (touched || changed) ntpWaiting_ = true; +} + +WifiService::Connection WifiService::connection() const { + Connection c; + c.connected = controller_.state() == WifiController::State::Connected; + if (!c.connected) return c; + c.fixed = fixed_; + c.dnsFromSettings = dnsFromSettings_; + c.address = WiFi.localIP().toString().c_str(); + c.mask = WiFi.subnetMask().toString().c_str(); + c.prefix = WiFi.subnetCIDR(); + IPAddress gateway = WiFi.gatewayIP(); + if (gateway != kNoAddress) c.gateway = gateway.toString().c_str(); + for (int i = 0; i < 2; i++) + if (WiFi.dnsIP(i) != kNoAddress) c.dns[i] = WiFi.dnsIP(i).toString().c_str(); + for (int i = 0; i < kNtpSlots; i++) { + const char* name = esp_sntp_getservername(i); + bool answered = esp_sntp_getreachability(i) != 0; + if (name) c.ntp[c.ntpCount++] = {name, false, answered}; + else if (sntpSlotFromDhcp(i)) c.ntp[c.ntpCount++] = {ipaddr_ntoa(esp_sntp_getserver(i)), true, answered}; + } + return c; +} + std::string WifiService::ip() const { return controller_.state() == WifiController::State::Connected ? WiFi.localIP().toString().c_str() : ""; } @@ -20,6 +131,7 @@ void WifiService::startScan(int channel) { WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(false); // the controller decides when to reconnect esp_wifi_set_country_code("EU", true); + esp_sntp_servermode_dhcp(true); // before DHCP runs: take the NTP servers it offers (Q110) radioInitialised_ = true; } if (scanRunning_) return; @@ -87,13 +199,7 @@ void WifiService::finishScan(uint32_t nowMs) { void WifiService::startNtp() { // Not configTime(): it would overwrite the POSIX TZ the Clock applies. - if (!esp_sntp_enabled()) { - esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); - esp_sntp_setservername(0, "pool.ntp.org"); - esp_sntp_init(); - } else { - esp_sntp_restart(); - } + applyServers(Why::Joined); ntpWaiting_ = true; } @@ -102,7 +208,7 @@ void WifiService::apply(const WifiController::Step& step) { switch (step.action) { case Action::None: break; case Action::StartScan: startScan(); break; - case Action::Connect: WiFi.begin(step.ssid.c_str(), step.password.empty() ? nullptr : step.password.c_str()); break; + case Action::Connect: join(step.ssid, step.password); break; case Action::Disconnect: WiFi.disconnect(); break; case Action::RadioOff: WiFi.disconnect(true); @@ -143,6 +249,8 @@ void WifiService::tick(uint32_t nowMs) { apply(controller_.update(nowMs, settings_.getBool(Setting::WifiEnabled))); + if (controller_.state() == State::Connected && nowMs - serversCheckedMs_ >= kServersEveryMs) applyServers(Why::Check); + if (ntpWaiting_ && sntp_get_sync_status() == SNTP_SYNC_STATUS_COMPLETED) { ntpWaiting_ = false; clock_.set(static_cast(time(nullptr)), TimeSource::Ntp); diff --git a/src/services/wifi_service.h b/src/services/wifi_service.h index a5c2910..53be347 100644 --- a/src/services/wifi_service.h +++ b/src/services/wifi_service.h @@ -36,6 +36,27 @@ class WifiService : public Service { // Done with list scans: switch the radio back off if Wi-Fi itself is off. void endListScans(); + // What the device is using and where each part came from (S1, Q113). + struct Connection { + bool connected = false; + bool fixed = false; // the address: Fixed, or from DHCP + bool dnsFromSettings = false; // DNS: from Settings, or from DHCP + std::string address, mask, gateway; + int prefix = 0; + std::string dns[2]; + struct Ntp { + std::string server; + bool fromDhcp = false; + bool answered = false; // it has replied since the last (re)start + } ntp[3]; + int ntpCount = 0; + }; + Connection connection() const; + // A Saved Network's IP setting changed: if it's the one in use, join it again with it (Q114). + void ipSettingChanged(const std::string& ssid); + // The DNS or NTP settings changed: use them now. + void serversChanged() { applyServers(Why::SettingsChanged); } + // A Saved Network was added: try it now rather than after the retry delay. void savedNetworksChanged() { controller_.retryNow(millis()); } @@ -47,6 +68,9 @@ class WifiService : public Service { void startScan(int channel = 0); void finishScan(uint32_t nowMs); void startNtp(); + void join(const std::string& ssid, const std::string& password); + enum class Why { Joined, SettingsChanged, Check }; + void applyServers(Why why); // DNS and NTP, as Settings and the network say Settings& settings_; SavedNetworks& saved_; @@ -58,6 +82,10 @@ class WifiService : public Service { uint32_t listScanSeq_ = 0; bool ntpWaiting_ = false; bool radioInitialised_ = false; + bool fixed_ = false; // the network in use has a Fixed address + bool dnsFromSettings_ = false; + std::string ntpNames_[2]; // lwIP keeps the pointers, so the names live here + uint32_t serversCheckedMs_ = 0; }; } // namespace roro