Public Access
S1 #7 step 4: fixed IPv4, DNS and NTP in Settings
Enter on a Saved Network opens its page instead of asking to forget it: "IP address" switches between Automatic and Fixed, with an address, a prefix and an optional gateway. Fixed starts from what the network is giving the device; the draft is checked and applied on leaving the page, so a half-typed address is never used. "DNS and NTP" holds the two DNS servers, "Always use my DNS" and the two NTP servers. Enter on Status shows the connection's details and where each value came from. Address fields take digits and dots only; refusals show as Toasts. Checked on the device through the screens: Fixed 10.39.39.13 applied and reverted to Automatic, a prefix of 99 refused. Measurements in docs/milestones/S1.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
+309
-33
@@ -1,5 +1,6 @@
|
||||
#include "wifi_settings_page.h"
|
||||
|
||||
#include "ipv4.h"
|
||||
#include "ui/fonts.h"
|
||||
#include "ui/widgets.h"
|
||||
|
||||
@@ -15,6 +16,62 @@ void WifiSettingsPage::enter() {
|
||||
|
||||
void WifiSettingsPage::refreshMain() { main_.setCount(kFixedRows + saved_.count()); }
|
||||
|
||||
// With Automatic, a network's page shows only the IP row and Forget.
|
||||
WifiSettingsPage::NetworkRow WifiSettingsPage::networkRow(int i) const {
|
||||
if (draftFixed_) return static_cast<NetworkRow>(i);
|
||||
return i == 0 ? kIpMode : kForget;
|
||||
}
|
||||
|
||||
void WifiSettingsPage::openNetwork(const SavedNetwork& n) {
|
||||
netSsid_ = n.ssid;
|
||||
draftFixed_ = n.fixed;
|
||||
draftAddress_ = n.fixed ? net::formatIpv4(n.ip.address) : "";
|
||||
draftPrefix_ = n.fixed ? std::to_string(n.ip.prefix) : "";
|
||||
draftGateway_ = n.fixed && n.ip.gateway ? net::formatIpv4(n.ip.gateway) : "";
|
||||
network_.setCount(draftFixed_ ? 5 : 2);
|
||||
network_.select(0);
|
||||
view_ = View::Network;
|
||||
}
|
||||
|
||||
// Leaving a network's page applies its IP setting (Q114), if it passes the checks (Q111).
|
||||
bool WifiSettingsPage::leaveNetwork() {
|
||||
const SavedNetwork* n = saved_.find(netSsid_);
|
||||
if (!n) return true; // forgotten meanwhile
|
||||
std::string why;
|
||||
if (draftFixed_) {
|
||||
net::FixedIp fixed;
|
||||
why = net::parseFixed(draftAddress_ + "/" + draftPrefix_ + (draftGateway_.empty() ? "" : " " + draftGateway_), fixed);
|
||||
if (draftAddress_.empty()) why = "A Fixed setting needs an address";
|
||||
if (!why.empty()) {
|
||||
warn(why);
|
||||
return false;
|
||||
}
|
||||
bool same = n->fixed && n->ip.address == fixed.address && n->ip.prefix == fixed.prefix && n->ip.gateway == fixed.gateway;
|
||||
if (same) return true;
|
||||
why = saved_.setIp(netSsid_, &fixed);
|
||||
if (why.empty()) warn(netSsid_ + ": fixed, " + net::formatFixed(fixed), NotificationLevel::Info);
|
||||
} else {
|
||||
if (!n->fixed) return true;
|
||||
why = saved_.setIp(netSsid_, nullptr);
|
||||
if (why.empty()) warn(netSsid_ + ": automatic (DHCP)", NotificationLevel::Info);
|
||||
}
|
||||
if (!why.empty()) {
|
||||
warn(why);
|
||||
return false;
|
||||
}
|
||||
wifi_.ipSettingChanged(netSsid_);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WifiSettingsPage::edit(Field field, const std::string& title, const std::string& value, View from) {
|
||||
field_ = field;
|
||||
editTitle_ = title;
|
||||
editFrom_ = from;
|
||||
editor_ = LineEditor(field == Field::Ntp1 || field == Field::Ntp2 ? 63 : 15);
|
||||
for (char ch : value) editor_.insert(static_cast<uint8_t>(ch));
|
||||
view_ = View::Edit;
|
||||
}
|
||||
|
||||
void WifiSettingsPage::warn(const std::string& text, NotificationLevel level) {
|
||||
bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(level)));
|
||||
}
|
||||
@@ -35,42 +92,22 @@ bool WifiSettingsPage::onKey(const KeyEvent& e) {
|
||||
if (forgetDialog_) {
|
||||
forgetDialog_->onKey(e);
|
||||
if (forgetDialog_->result() == 1) {
|
||||
saved_.forget(saved_.at(main_.selected() - kFixedRows).ssid);
|
||||
refreshMain();
|
||||
saved_.forget(netSsid_);
|
||||
wifi_.ipSettingChanged(netSsid_); // it may be the one in use
|
||||
enter();
|
||||
}
|
||||
if (forgetDialog_->result() != DialogModel::kPending) forgetDialog_.reset();
|
||||
if (forgetDialog_ && forgetDialog_->result() != DialogModel::kPending) forgetDialog_.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (view_) {
|
||||
case View::Main:
|
||||
switch (e.key) {
|
||||
case Key::Up: main_.up(); return true;
|
||||
case Key::Down: main_.down(); return true;
|
||||
case Key::Back: return false;
|
||||
case Key::Left:
|
||||
case Key::Right:
|
||||
if (main_.selected() != kToggle) return true;
|
||||
[[fallthrough]];
|
||||
case Key::Select:
|
||||
switch (main_.selected()) {
|
||||
case kToggle: settings_.setBool(Setting::WifiEnabled, !settings_.getBool(Setting::WifiEnabled)); break;
|
||||
case kStatus: break;
|
||||
case kAddScanned:
|
||||
wifi_.startListScan();
|
||||
scan_.setCount(0);
|
||||
view_ = View::Scan;
|
||||
break;
|
||||
case kAddHidden:
|
||||
newHidden_ = true;
|
||||
editor_ = LineEditor(32);
|
||||
view_ = View::Ssid;
|
||||
break;
|
||||
default: forgetDialog_.reset(new DialogModel({"Cancel", "Forget"})); break;
|
||||
}
|
||||
return true;
|
||||
default: return true;
|
||||
}
|
||||
case View::Main: return mainKey(e);
|
||||
case View::Servers: return serversKey(e);
|
||||
case View::Network: return networkKey(e);
|
||||
case View::Edit: return editKey(e);
|
||||
case View::Details:
|
||||
if (e.key == Key::Back || e.key == Key::Select) view_ = View::Main;
|
||||
return true;
|
||||
|
||||
case View::Scan: {
|
||||
std::vector<ScanEntry> found;
|
||||
@@ -131,6 +168,159 @@ bool WifiSettingsPage::onKey(const KeyEvent& e) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WifiSettingsPage::mainKey(const KeyEvent& e) {
|
||||
switch (e.key) {
|
||||
case Key::Up: main_.up(); return true;
|
||||
case Key::Down: main_.down(); return true;
|
||||
case Key::Back: return false;
|
||||
case Key::Left:
|
||||
case Key::Right:
|
||||
if (main_.selected() != kToggle) return true;
|
||||
[[fallthrough]];
|
||||
case Key::Select:
|
||||
switch (main_.selected()) {
|
||||
case kToggle: settings_.setBool(Setting::WifiEnabled, !settings_.getBool(Setting::WifiEnabled)); break;
|
||||
case kStatus: view_ = View::Details; break;
|
||||
case kServers:
|
||||
servers_.setCount(kServerRows);
|
||||
view_ = View::Servers;
|
||||
break;
|
||||
case kAddScanned:
|
||||
wifi_.startListScan();
|
||||
scan_.setCount(0);
|
||||
view_ = View::Scan;
|
||||
break;
|
||||
case kAddHidden:
|
||||
newHidden_ = true;
|
||||
editor_ = LineEditor(32);
|
||||
view_ = View::Ssid;
|
||||
break;
|
||||
default: openNetwork(saved_.at(main_.selected() - kFixedRows)); break;
|
||||
}
|
||||
return true;
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
// DNS and NTP servers (Q108 to Q110).
|
||||
bool WifiSettingsPage::serversKey(const KeyEvent& e) {
|
||||
switch (e.key) {
|
||||
case Key::Up: servers_.up(); break;
|
||||
case Key::Down: servers_.down(); break;
|
||||
case Key::Back: view_ = View::Main; break;
|
||||
case Key::Left:
|
||||
case Key::Right:
|
||||
if (servers_.selected() != kDnsAlways) break;
|
||||
[[fallthrough]];
|
||||
case Key::Select:
|
||||
switch (servers_.selected()) {
|
||||
case kDns1: edit(Field::Dns1, "First DNS server", settings_.getString(Setting::Dns1), View::Servers); break;
|
||||
case kDns2: edit(Field::Dns2, "Second DNS server (or empty)", settings_.getString(Setting::Dns2), View::Servers); break;
|
||||
case kDnsAlways:
|
||||
settings_.setBool(Setting::DnsAlways, !settings_.getBool(Setting::DnsAlways));
|
||||
wifi_.serversChanged();
|
||||
break;
|
||||
case kNtp1: edit(Field::Ntp1, "First NTP server", settings_.getString(Setting::Ntp1), View::Servers); break;
|
||||
case kNtp2: edit(Field::Ntp2, "Second NTP server (or empty)", settings_.getString(Setting::Ntp2), View::Servers); break;
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WifiSettingsPage::networkKey(const KeyEvent& e) {
|
||||
switch (e.key) {
|
||||
case Key::Up: network_.up(); break;
|
||||
case Key::Down: network_.down(); break;
|
||||
case Key::Back:
|
||||
if (leaveNetwork()) enter();
|
||||
break;
|
||||
case Key::Left:
|
||||
case Key::Right:
|
||||
if (networkRow(network_.selected()) != kIpMode) break;
|
||||
[[fallthrough]];
|
||||
case Key::Select:
|
||||
switch (networkRow(network_.selected())) {
|
||||
case kIpMode: {
|
||||
draftFixed_ = !draftFixed_;
|
||||
// Fixed for the first time: start from what the network gave us, if we're on it.
|
||||
WifiService::Connection now = wifi_.connection();
|
||||
if (draftFixed_ && draftAddress_.empty() && now.connected && wifi_.ssid() == netSsid_) {
|
||||
draftAddress_ = now.address;
|
||||
draftPrefix_ = std::to_string(now.prefix);
|
||||
draftGateway_ = now.gateway;
|
||||
}
|
||||
if (draftFixed_ && draftPrefix_.empty()) draftPrefix_ = "24";
|
||||
network_.setCount(draftFixed_ ? 5 : 2);
|
||||
network_.select(0);
|
||||
break;
|
||||
}
|
||||
case kAddress: edit(Field::Address, "Address", draftAddress_, View::Network); break;
|
||||
case kPrefix: edit(Field::Prefix, "Prefix (1 to 30; 24 = 255.255.255.0)", draftPrefix_, View::Network); break;
|
||||
case kGateway: edit(Field::Gateway, "Gateway (or empty for none)", draftGateway_, View::Network); break;
|
||||
case kForget: forgetDialog_.reset(new DialogModel({"Cancel", "Forget"})); break;
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// One field. Addresses take digits and dots only (Q112); what's typed is checked on Enter (Q111).
|
||||
bool WifiSettingsPage::editKey(const KeyEvent& e) {
|
||||
bool name = field_ == Field::Ntp1 || field_ == Field::Ntp2;
|
||||
switch (e.key) {
|
||||
case Key::Char: {
|
||||
bool digit = e.ch >= '0' && e.ch <= '9';
|
||||
bool ok = name ? e.ch > ' ' && e.ch < 0x7F : field_ == Field::Prefix ? digit : digit || e.ch == '.';
|
||||
if (ok) editor_.insert(e.ch);
|
||||
break;
|
||||
}
|
||||
case Key::Delete: editor_.backspace(); break;
|
||||
case Key::Left: editor_.left(); break;
|
||||
case Key::Right: editor_.right(); break;
|
||||
case Key::Back: view_ = editFrom_; break;
|
||||
case Key::Select: {
|
||||
std::string text = editor_.text(), why;
|
||||
uint32_t address;
|
||||
switch (field_) {
|
||||
case Field::Address:
|
||||
if (!net::parseIpv4(text, address)) why = "Four numbers from 0 to 255, like 10.39.39.13";
|
||||
else draftAddress_ = text;
|
||||
break;
|
||||
case Field::Prefix: {
|
||||
int p = text.empty() ? 0 : atoi(text.c_str());
|
||||
if (p < 1 || p > 30) why = "The prefix must be 1 to 30";
|
||||
else draftPrefix_ = std::to_string(p);
|
||||
break;
|
||||
}
|
||||
case Field::Gateway:
|
||||
if (!text.empty() && !net::parseIpv4(text, address)) why = "Four numbers from 0 to 255, or empty";
|
||||
else draftGateway_ = text;
|
||||
break;
|
||||
case Field::Dns1:
|
||||
case Field::Dns2:
|
||||
if (!settings_.setString(field_ == Field::Dns1 ? Setting::Dns1 : Setting::Dns2, text))
|
||||
why = field_ == Field::Dns1 ? "An IPv4 address, like 9.9.9.9" : "An IPv4 address, or empty";
|
||||
else wifi_.serversChanged();
|
||||
break;
|
||||
case Field::Ntp1:
|
||||
case Field::Ntp2:
|
||||
if (!settings_.setString(field_ == Field::Ntp1 ? Setting::Ntp1 : Setting::Ntp2, text))
|
||||
why = field_ == Field::Ntp1 ? "A host name or an IPv4 address" : "A host name, an IPv4 address, or empty";
|
||||
else wifi_.serversChanged();
|
||||
break;
|
||||
}
|
||||
if (why.empty()) view_ = editFrom_;
|
||||
else warn(why);
|
||||
break;
|
||||
}
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void WifiSettingsPage::draw(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
switch (view_) {
|
||||
@@ -141,6 +331,7 @@ void WifiSettingsPage::draw(Canvas& c) {
|
||||
switch (i) {
|
||||
case kToggle: return "Wi-Fi";
|
||||
case kStatus: return "Status";
|
||||
case kServers: return "DNS and NTP";
|
||||
case kAddScanned: return "Add a network";
|
||||
case kAddHidden: return "Add a hidden network";
|
||||
default: {
|
||||
@@ -153,14 +344,78 @@ void WifiSettingsPage::draw(Canvas& c) {
|
||||
switch (i) {
|
||||
case kToggle: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
|
||||
case kStatus: return statusText();
|
||||
case kServers:
|
||||
case kAddScanned:
|
||||
case kAddHidden: return ">";
|
||||
default: return saved_.at(i - kFixedRows).fixed ? "fixed" : "";
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case View::Details: drawDetails(c); break;
|
||||
|
||||
case View::Servers:
|
||||
widgets::list(
|
||||
c, servers_, area,
|
||||
[](int i) -> std::string {
|
||||
switch (i) {
|
||||
case kDns1: return "DNS 1";
|
||||
case kDns2: return "DNS 2";
|
||||
case kDnsAlways: return "Always use my DNS";
|
||||
case kNtp1: return "NTP 1";
|
||||
default: return "NTP 2";
|
||||
}
|
||||
},
|
||||
[this](int i) -> std::string {
|
||||
switch (i) {
|
||||
case kDns1: return settings_.getString(Setting::Dns1);
|
||||
case kDns2: return settings_.getString(Setting::Dns2).empty() ? "none" : settings_.getString(Setting::Dns2);
|
||||
case kDnsAlways: return settings_.getBool(Setting::DnsAlways) ? "On" : "Off";
|
||||
case kNtp1: return settings_.getString(Setting::Ntp1);
|
||||
default: return settings_.getString(Setting::Ntp2).empty() ? "none" : settings_.getString(Setting::Ntp2);
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case View::Network: {
|
||||
c.setFont(&fonts::small);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString(netSsid_.c_str(), 4, area.y + 2);
|
||||
widgets::list(
|
||||
c, network_, {area.x, area.y + 12, area.w, area.h - 12},
|
||||
[this](int i) -> std::string {
|
||||
switch (networkRow(i)) {
|
||||
case kIpMode: return "IP address";
|
||||
case kAddress: return " Address";
|
||||
case kPrefix: return " Prefix";
|
||||
case kGateway: return " Gateway";
|
||||
default: return "Forget this network";
|
||||
}
|
||||
},
|
||||
[this](int i) -> std::string {
|
||||
switch (networkRow(i)) {
|
||||
case kIpMode: return draftFixed_ ? "Fixed" : "Automatic";
|
||||
case kAddress: return draftAddress_.empty() ? "not set" : draftAddress_;
|
||||
case kPrefix: {
|
||||
int p = atoi(draftPrefix_.c_str());
|
||||
return draftPrefix_ + " (" + net::formatIpv4(net::maskOf(p)) + ")";
|
||||
}
|
||||
case kGateway: return draftGateway_.empty() ? "none" : draftGateway_;
|
||||
default: return "";
|
||||
}
|
||||
});
|
||||
if (forgetDialog_ && main_.selected() >= kFixedRows)
|
||||
widgets::dialog(c, "Forget network?", saved_.at(main_.selected() - kFixedRows).ssid, *forgetDialog_);
|
||||
if (forgetDialog_) widgets::dialog(c, "Forget network?", netSsid_, *forgetDialog_);
|
||||
break;
|
||||
}
|
||||
|
||||
case View::Edit: {
|
||||
c.setFont(&fonts::body);
|
||||
c.setTextColor(theme::kMuted);
|
||||
c.drawString(editTitle_.c_str(), 4, area.y + 4);
|
||||
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
||||
c.drawString("Enter: save `: cancel", 4, area.y + 44);
|
||||
break;
|
||||
}
|
||||
|
||||
case View::Scan: {
|
||||
if (!wifi_.listScanDone()) {
|
||||
@@ -195,4 +450,25 @@ void WifiSettingsPage::draw(Canvas& c) {
|
||||
}
|
||||
}
|
||||
|
||||
// What the device is using and where each part came from (Q113).
|
||||
void WifiSettingsPage::drawDetails(Canvas& c) {
|
||||
const auto& area = theme::kContent;
|
||||
WifiService::Connection n = wifi_.connection();
|
||||
std::vector<std::string> lines;
|
||||
if (!n.connected) {
|
||||
lines = {statusText(), "", "`: back"};
|
||||
} else {
|
||||
lines.push_back(wifi_.ssid() + ", " + std::to_string(wifi_.rssi()) + " dBm");
|
||||
lines.push_back("Address " + n.address + "/" + std::to_string(n.prefix) + (n.fixed ? " (fixed)" : " (DHCP)"));
|
||||
lines.push_back("Mask " + n.mask);
|
||||
lines.push_back("Gateway " + (n.gateway.empty() ? std::string("none") : n.gateway));
|
||||
std::string from = n.dnsFromSettings ? " (Settings)" : " (DHCP)";
|
||||
lines.push_back("DNS " + (n.dns[0].empty() ? std::string("none") : n.dns[0] + from));
|
||||
if (!n.dns[1].empty()) lines.push_back("DNS " + n.dns[1] + from);
|
||||
for (int i = 0; i < n.ntpCount; i++)
|
||||
lines.push_back("NTP " + n.ntp[i].server + (n.ntp[i].fromDhcp ? " (DHCP)" : "") + (n.ntp[i].answered ? ", answered" : ""));
|
||||
}
|
||||
widgets::textLines(c, lines, 0, {area.x + 2, area.y + 2, area.w - 2, area.h - 2});
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
|
||||
Reference in New Issue
Block a user