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:
2026-10-06 00:04:53 +02:00
co-authored by Claude Opus 5.5
parent 3e279738b6
commit acf7697bdc
5 changed files with 365 additions and 42 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ The Service that owns the Wi-Fi radio. It's always in exactly one mode: *Off*, *
_Avoid_: network manager _Avoid_: network manager
**Saved Network**: **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 _Avoid_: profile, known network
**IRC Service**: **IRC Service**:
+9 -1
View File
@@ -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. **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 ## 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. 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) | | `sound on` / `sound off` | Toggles the Sound setting (beep + LED) |
| `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s | | `short` / `normal` | Screen timeouts 5 s / 10 s, or 30 s / 60 s |
| `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) | | `wifi add <ssid><TAB><password>` | Adds a Saved Network (so credentials stay out of the repo) |
| `wifi ip <ssid> dhcp` / `wifi ip <ssid> <address>/<prefix> [gateway]` | A Saved Network's IP setting: Automatic, or Fixed. Debug Builds: add `try <seconds>` to go back to the previous setting unless `wifi ip keep` follows |
| `wifi dns <a> [b]` / `wifi dns always on\|off` / `wifi ntp <a> [b]` | DNS servers (used on Fixed networks, or always), and NTP servers |
| `log <text>` | Appends a line to a test IRC Log (`/irc/dev/#test/<date>.log`) | | `log <text>` | Appends a line to a test IRC Log (`/irc/dev/#test/<date>.log`) |
| `sd card` | What the SD card says it is: type, size, and its identity register (maker, name, revision, serial, date) | | `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 | | `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 <host> <port> <sha256>` | Pins a certificate by hand (the Gemini App asks when one changes) | | `gemini trust <host> <port> <sha256>` | Pins a certificate by hand (the Gemini App asks when one changes) |
| `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) | | `irc say <buffer> <text>` | Types into a Buffer, commands included (`irc say 0 /join #test`) |
| `irc dump` | Prints IRC status, memory, and the last lines of each Buffer | | `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 | | `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 | | `tasks` | FreeRTOS tasks: state, priority, lowest free stack, CPU share |
| `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) | | `reboot` / `boot other` | Restart, or restart into the other app slot (a manual Rollback) |
+14 -1
View File
@@ -1,6 +1,6 @@
# S1 — System basics # 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. **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. - 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). - 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 ### Work breakdown
1. **IPv4 logic** (host-tested): parsing and formatting addresses, prefix and mask, the checks of Q111. 1. **IPv4 logic** (host-tested): parsing and formatting addresses, prefix and mask, the checks of Q111.
+309 -33
View File
@@ -1,5 +1,6 @@
#include "wifi_settings_page.h" #include "wifi_settings_page.h"
#include "ipv4.h"
#include "ui/fonts.h" #include "ui/fonts.h"
#include "ui/widgets.h" #include "ui/widgets.h"
@@ -15,6 +16,62 @@ void WifiSettingsPage::enter() {
void WifiSettingsPage::refreshMain() { main_.setCount(kFixedRows + saved_.count()); } 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) { void WifiSettingsPage::warn(const std::string& text, NotificationLevel level) {
bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(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_) { if (forgetDialog_) {
forgetDialog_->onKey(e); forgetDialog_->onKey(e);
if (forgetDialog_->result() == 1) { if (forgetDialog_->result() == 1) {
saved_.forget(saved_.at(main_.selected() - kFixedRows).ssid); saved_.forget(netSsid_);
refreshMain(); 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; return true;
} }
switch (view_) { switch (view_) {
case View::Main: case View::Main: return mainKey(e);
switch (e.key) { case View::Servers: return serversKey(e);
case Key::Up: main_.up(); return true; case View::Network: return networkKey(e);
case Key::Down: main_.down(); return true; case View::Edit: return editKey(e);
case Key::Back: return false; case View::Details:
case Key::Left: if (e.key == Key::Back || e.key == Key::Select) view_ = View::Main;
case Key::Right: return true;
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::Scan: { case View::Scan: {
std::vector<ScanEntry> found; std::vector<ScanEntry> found;
@@ -131,6 +168,159 @@ bool WifiSettingsPage::onKey(const KeyEvent& e) {
return true; 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) { void WifiSettingsPage::draw(Canvas& c) {
const auto& area = theme::kContent; const auto& area = theme::kContent;
switch (view_) { switch (view_) {
@@ -141,6 +331,7 @@ void WifiSettingsPage::draw(Canvas& c) {
switch (i) { switch (i) {
case kToggle: return "Wi-Fi"; case kToggle: return "Wi-Fi";
case kStatus: return "Status"; case kStatus: return "Status";
case kServers: return "DNS and NTP";
case kAddScanned: return "Add a network"; case kAddScanned: return "Add a network";
case kAddHidden: return "Add a hidden network"; case kAddHidden: return "Add a hidden network";
default: { default: {
@@ -153,14 +344,78 @@ void WifiSettingsPage::draw(Canvas& c) {
switch (i) { switch (i) {
case kToggle: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off"; case kToggle: return settings_.getBool(Setting::WifiEnabled) ? "On" : "Off";
case kStatus: return statusText(); case kStatus: return statusText();
case kServers:
case kAddScanned: case kAddScanned:
case kAddHidden: return ">"; 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 ""; default: return "";
} }
}); });
if (forgetDialog_ && main_.selected() >= kFixedRows) if (forgetDialog_) widgets::dialog(c, "Forget network?", netSsid_, *forgetDialog_);
widgets::dialog(c, "Forget network?", saved_.at(main_.selected() - kFixedRows).ssid, *forgetDialog_);
break; 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: { case View::Scan: {
if (!wifi_.listScanDone()) { 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 } // namespace roro
+32 -6
View File
@@ -16,8 +16,9 @@
namespace roro { namespace roro {
// Settings → Wi-Fi: the On/Off switch, current connection, Saved Networks, and adding networks // Settings → Wi-Fi: the On/Off switch, the connection and its details, DNS and NTP servers, and
// from a scan or by name (hidden). Owned by the Settings App. // 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 { class WifiSettingsPage {
public: public:
WifiSettingsPage(Settings& settings, SavedNetworks& saved, WifiService& wifi, EventBus& bus) WifiSettingsPage(Settings& settings, SavedNetworks& saved, WifiService& wifi, EventBus& bus)
@@ -25,17 +26,31 @@ class WifiSettingsPage {
void enter(); void enter();
bool onKey(const KeyEvent& e); // false: leave the page bool onKey(const KeyEvent& e); // false: leave the page
bool textEntryActive() const { return view_ == View::Ssid || view_ == View::Password; } bool textEntryActive() const { return view_ == View::Ssid || view_ == View::Password || view_ == View::Edit; }
bool live() const { return view_ == View::Main || view_ == View::Scan; } // redraw periodically // 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); void draw(Canvas& c);
private: private:
enum class View { Main, Scan, Ssid, Password }; enum class View { Main, Scan, Ssid, Password, Details, Servers, Network, Edit };
enum Fixed { kToggle, kStatus, kAddScanned, kAddHidden, kFixedRows }; 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 refreshMain();
void warn(const std::string& text, NotificationLevel level = NotificationLevel::Warning); void warn(const std::string& text, NotificationLevel level = NotificationLevel::Warning);
std::string statusText() const; 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_; Settings& settings_;
SavedNetworks& saved_; SavedNetworks& saved_;
@@ -44,10 +59,21 @@ class WifiSettingsPage {
View view_ = View::Main; View view_ = View::Main;
ListModel main_{theme::kContent.h / theme::kLineHeight}; ListModel main_{theme::kContent.h / theme::kLineHeight};
ListModel scan_{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}; LineEditor editor_{63};
std::string newSsid_; std::string newSsid_;
bool newHidden_ = false; bool newHidden_ = false;
std::unique_ptr<DialogModel> forgetDialog_; std::unique_ptr<DialogModel> 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 } // namespace roro