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 <s>` 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-05 23:52:22 +02:00
co-authored by Claude Opus 5.5
parent bdd027cb50
commit 3e279738b6
3 changed files with 239 additions and 9 deletions
+116 -8
View File
@@ -3,10 +3,121 @@
#include <WiFi.h>
#include <esp_sntp.h>
#include <esp_wifi.h>
#include <lwip/ip_addr.h>
#include <cstring>
#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<uint32_t>(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<int64_t>(time(nullptr)), TimeSource::Ntp);
+28
View File
@@ -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