diff --git a/CONTEXT.md b/CONTEXT.md index a6c083b..dfd6ef1 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -69,7 +69,7 @@ The Service that owns the Wi-Fi radio. It's always in exactly one mode: *Off*, * _Avoid_: network manager **Saved Network**: -A Wi-Fi network the device may join on its own (name, password). When several are in range, the strongest wins. +A Wi-Fi network the device may join on its own: its name, its password, and how it gets its address, *Automatic* (DHCP) or *Fixed* (an address, a prefix and an optional gateway typed in Settings). When several are in range, the strongest wins. _Avoid_: profile, known network **IRC Service**: diff --git a/README.md b/README.md index fc9ee3f..2f40415 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ To install from the SD card instead, copy the `.ota` file from `.pio/build/cardp **The private key** lives in `~/.config/roro9stack/ota-key.pem` and must never be committed. If it's lost, generate a new pair and flash once over USB. +## Networks without DHCP + +Each Saved Network gets its address automatically (DHCP) or has a Fixed one (docs/milestones/S1.md): in Settings > Wi-Fi, Enter on a network opens its page, where "IP address" switches between Automatic and Fixed, with an address, a prefix length (24 is 255.255.255.0) and an optional gateway. Switching to Fixed starts from what the network is giving the device at that moment. The setting is checked and applied when you leave the page. IPv4 only. + +"DNS and NTP" on the same screen holds two DNS servers (9.9.9.9 and 1.1.1.1 by default), used on Fixed networks, or on every network with "Always use my DNS"; and two NTP servers (pool.ntp.org and time.cloudflare.com), used after any the network's DHCP offers. Enter on "Status" shows what's in use and where each value came from. + ## Gemini The Gemini App browses Geminispace (docs/milestones/G1.md): Tab and Shift+Tab pick a link, Enter follows it, Back returns (to where the page was scrolled), Space pages down, `g` types an address. Certificates are trusted on first use; a changed one stops the page and asks. @@ -75,6 +81,8 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve | `sound on` / `sound off` | Toggles the Sound setting (beep + LED) | | `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s | | `wifi add ` | Adds a Saved Network (so credentials stay out of the repo) | +| `wifi ip dhcp` / `wifi ip
/ [gateway]` | A Saved Network's IP setting: Automatic, or Fixed. Debug Builds: add `try ` to go back to the previous setting unless `wifi ip keep` follows | +| `wifi dns [b]` / `wifi dns always on\|off` / `wifi ntp [b]` | DNS servers (used on Fixed networks, or always), and NTP servers | | `log ` | Appends a line to a test IRC Log (`/irc/dev/#test/.log`) | | `sd card` | What the SD card says it is: type, size, and its identity register (maker, name, revision, serial, date) | | `sd list` | Lists the files of each Storage Clean-up category | @@ -85,7 +93,7 @@ The LoRa Scanner (docs/milestones/M3.md) listens with the Cap's radio and **neve | `gemini trust ` | Pins a certificate by hand (the Gemini App asks when one changes) | | `irc say ` | Types into a Buffer, commands included (`irc say 0 /join #test`) | | `irc dump` | Prints IRC status, memory, and the last lines of each Buffer | -| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap | +| `wifi status` | Prints Wi-Fi state, network, signal, clock and free heap, then the address, gateway, DNS and NTP servers in use and where each came from | | `info` | Firmware, uptime, last start reason, memory, Wi-Fi, the SD card and its write faults since boot, and both app slots with their versions and OTA states | | `tasks` | FreeRTOS tasks: state, priority, lowest free stack, CPU share | | `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) | diff --git a/docs/milestones/S1.md b/docs/milestones/S1.md index a77cac3..41c6575 100644 --- a/docs/milestones/S1.md +++ b/docs/milestones/S1.md @@ -1,6 +1,6 @@ # S1 — System basics -**Status:** in progress (branch `s1`). The SD driver fix shipped in v0.6.1 (issue #21, ADR 0007). Fixed IPv4 settings (issue #7) are being built. The System Monitor (#11) hasn't started. +**Status:** in progress (branch `s1`). The SD driver fix shipped in v0.6.1 (issue #21, ADR 0007). Fixed IPv4 settings (issue #7) are done and checked on the device. The System Monitor (#11) hasn't started. **Goal:** the device works on any network, the card can be trusted, and you can see what the system is doing. A side milestone, like G1. @@ -39,6 +39,19 @@ The SDK already allows 3 NTP servers and 3 DNS servers and can take NTP servers - Connection details show what's in use and where it came from. - Tested on `knbg-guests` with 10.39.39.12 (the device's DHCP lease) and 10.39.39.13 (free: the device is alone on that network). +### Measured (2026-10-05 and 06, on `knbg-guests`) + +The network is 10.39.39.0/24, gateway 10.39.39.1; DHCP gives 10.39.39.1 as DNS and offers no NTP server. + +- **Fixed 10.39.39.12/24** (the device's own lease) and **Fixed 10.39.39.13/24**, gateway 10.39.39.1: the device joins with that address, DNS is 9.9.9.9 and 1.1.1.1 from Settings, and a Gemini page loads (name resolution, routing, TLS). On .13, .12 no longer answers. +- **A wrong gateway** (10.39.39.254) on a 60 s trial: the device stops answering from another subnet, and comes back by itself with the previous setting. +- **Back to Automatic:** 10.39.39.12 by DHCP again, DNS 10.39.39.1 from DHCP. +- **"Always use my DNS"** on an Automatic network: DNS becomes 9.9.9.9 and 1.1.1.1; switched off, the device joins again and has DHCP's DNS back. +- **NTP:** `pool.ntp.org` answers; set to `time.cloudflare.com` alone, that one answers within 25 s. +- **Refusals**, on the console and in Settings: the network's own address, a gateway outside the subnet, a prefix of 31 or 99, 10.39.39.300, an unknown network, a DNS name where an address is needed, a host name with an underscore. +- **In Settings:** the network's page pre-fills Fixed with the address, prefix and gateway in use; leaving the page applies it; connection details show each value and where it came from. +- **Not tested:** NTP servers offered by DHCP (this network offers none), and a Fixed network with no gateway. + ### Work breakdown 1. **IPv4 logic** (host-tested): parsing and formatting addresses, prefix and mask, the checks of Q111. diff --git a/src/apps/wifi_settings_page.cpp b/src/apps/wifi_settings_page.cpp index 7e57ced..39e39a9 100644 --- a/src/apps/wifi_settings_page.cpp +++ b/src/apps/wifi_settings_page.cpp @@ -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(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(ch)); + view_ = View::Edit; +} + void WifiSettingsPage::warn(const std::string& text, NotificationLevel level) { bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast(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 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 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 diff --git a/src/apps/wifi_settings_page.h b/src/apps/wifi_settings_page.h index 6984fa1..d69f4cf 100644 --- a/src/apps/wifi_settings_page.h +++ b/src/apps/wifi_settings_page.h @@ -16,8 +16,9 @@ namespace roro { -// Settings → Wi-Fi: the On/Off switch, current connection, Saved Networks, and adding networks -// from a scan or by name (hidden). Owned by the Settings App. +// Settings → Wi-Fi: the On/Off switch, the connection and its details, DNS and NTP servers, and +// the Saved Networks: adding one from a scan or by name (hidden), and each one's own page with its +// IP setting, Automatic or Fixed (docs/milestones/S1.md, Q113). Owned by the Settings App. class WifiSettingsPage { public: WifiSettingsPage(Settings& settings, SavedNetworks& saved, WifiService& wifi, EventBus& bus) @@ -25,17 +26,31 @@ class WifiSettingsPage { void enter(); bool onKey(const KeyEvent& e); // false: leave the page - bool textEntryActive() const { return view_ == View::Ssid || view_ == View::Password; } - bool live() const { return view_ == View::Main || view_ == View::Scan; } // redraw periodically + bool textEntryActive() const { return view_ == View::Ssid || view_ == View::Password || view_ == View::Edit; } + // Redraw periodically: the status, a scan, the connection details. + bool live() const { return view_ == View::Main || view_ == View::Scan || view_ == View::Details; } void draw(Canvas& c); private: - enum class View { Main, Scan, Ssid, Password }; - enum Fixed { kToggle, kStatus, kAddScanned, kAddHidden, kFixedRows }; + enum class View { Main, Scan, Ssid, Password, Details, Servers, Network, Edit }; + enum Fixed { kToggle, kStatus, kServers, kAddScanned, kAddHidden, kFixedRows }; + enum ServerRow { kDns1, kDns2, kDnsAlways, kNtp1, kNtp2, kServerRows }; + // A Saved Network's page: with Automatic, only kIpMode and kForget are shown. + enum NetworkRow { kIpMode, kAddress, kPrefix, kGateway, kForget }; + enum class Field { Address, Prefix, Gateway, Dns1, Dns2, Ntp1, Ntp2 }; void refreshMain(); void warn(const std::string& text, NotificationLevel level = NotificationLevel::Warning); std::string statusText() const; + bool mainKey(const KeyEvent& e); + bool serversKey(const KeyEvent& e); + bool networkKey(const KeyEvent& e); + bool editKey(const KeyEvent& e); + void openNetwork(const SavedNetwork& n); + bool leaveNetwork(); // checks and applies the draft; false (with a Toast) when it's refused + NetworkRow networkRow(int i) const; + void edit(Field field, const std::string& title, const std::string& value, View from); + void drawDetails(Canvas& c); Settings& settings_; SavedNetworks& saved_; @@ -44,10 +59,21 @@ class WifiSettingsPage { View view_ = View::Main; ListModel main_{theme::kContent.h / theme::kLineHeight}; ListModel scan_{theme::kContent.h / theme::kLineHeight}; + ListModel servers_{theme::kContent.h / theme::kLineHeight}; + ListModel network_{theme::kContent.h / theme::kLineHeight - 1}; LineEditor editor_{63}; std::string newSsid_; bool newHidden_ = false; std::unique_ptr forgetDialog_; + // The Saved Network whose page is open, and its IP setting as being typed (a draft: applied on + // leaving the page, so a half-typed address is never used). + std::string netSsid_; + bool draftFixed_ = false; + std::string draftAddress_, draftPrefix_, draftGateway_; + // The field being edited, and where Enter and Back return to. + Field field_ = Field::Address; + std::string editTitle_; + View editFrom_ = View::Main; }; } // namespace roro