#include "app_keys.h" #include "wifi_settings_page.h" #include "ipv4.h" #include "ui/fonts.h" #include "ui/widgets.h" namespace roro { using State = WifiController::State; void WifiSettingsPage::enter() { view_ = View::Main; forgetDialog_.reset(); refreshMain(); } 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))); } std::string WifiSettingsPage::statusText() const { switch (wifi_.state()) { case State::Off: return "Off"; case State::Scanning: return "Searching..."; case State::Waiting: return "No saved network in range"; case State::Connecting: return "Joining " + wifi_.ssid() + "..."; case State::Connected: return wifi_.ssid() + " (" + std::to_string(wifi_.rssi()) + " dBm)"; case State::Monitoring: return "Monitoring"; } return ""; } bool WifiSettingsPage::onKey(const KeyEvent& e) { if (forgetDialog_) { forgetDialog_->onKey(e); if (forgetDialog_->result() == 1) { saved_.forget(netSsid_); wifi_.ipSettingChanged(netSsid_); // it may be the one in use enter(); } if (forgetDialog_ && forgetDialog_->result() != DialogModel::kPending) forgetDialog_.reset(); return true; } switch (view_) { 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; for (auto& n : wifi_.listScan()) if (!n.ssid.empty()) found.push_back(n); if (wifi_.listScanDone()) scan_.setCount(static_cast(found.size())); switch (e.key) { case Key::Up: scan_.up(); break; case Key::Down: scan_.down(); break; case Key::Back: view_ = View::Main; break; case Key::Select: if (scan_.selected() < 0) break; newSsid_ = found[scan_.selected()].ssid; newHidden_ = false; if (found[scan_.selected()].open) { std::string error = saved_.add(newSsid_, "", false); if (!error.empty()) warn(error); wifi_.savedNetworksChanged(); enter(); } else { editor_ = LineEditor(63); view_ = View::Password; } break; default: break; } return true; } case View::Ssid: case View::Password: switch (e.key) { case Key::Char: 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: enter(); break; case Key::Select: if (view_ == View::Ssid) { newSsid_ = editor_.text(); editor_ = LineEditor(63); view_ = View::Password; } else { std::string error = saved_.add(newSsid_, editor_.text(), newHidden_); if (!error.empty()) { warn(error); } else { warn("Saved " + newSsid_, NotificationLevel::Info); wifi_.savedNetworksChanged(); enter(); } } break; default: break; } 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) { const auto& area = theme::kContent; switch (view_) { case View::Main: widgets::list( c, main_, area, [this](int i) -> std::string { 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: { const auto& n = saved_.at(i - kFixedRows); return " " + n.ssid + (n.hidden ? " (hidden)" : ""); } } }, [this](int i) -> std::string { 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_) 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}); break; } case View::Scan: { if (!wifi_.listScanDone()) { widgets::textLines(c, {"Scanning..."}, 0, area); break; } std::vector found; for (auto& n : wifi_.listScan()) if (!n.ssid.empty()) found.push_back(n); if (scan_.count() != static_cast(found.size())) scan_.setCount(static_cast(found.size())); if (found.empty()) { widgets::textLines(c, {"No networks found."}, 0, area); break; } widgets::list( c, scan_, area, [&](int i) { return found[i].ssid + (found[i].open ? " (open)" : ""); }, [&](int i) { return std::to_string(found[i].rssi) + " dBm"; }); break; } case View::Ssid: case View::Password: { c.setFont(&fonts::body); c.setTextColor(theme::kMuted); std::string title = view_ == View::Ssid ? "Hidden network name" : "Password for " + newSsid_; c.drawString(title.c_str(), 4, area.y + 4); widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0}); if (view_ == View::Password) c.drawString("Leave it empty for an open network.", 4, area.y + 44); break; } } } void WifiSettingsPage::help(std::vector& out) const { if (forgetDialog_) return keys::add(out, keys::kDialog); switch (view_) { case View::Main: keys::add(out, keys::kWifi); break; case View::Servers: keys::add(out, keys::kWifiServers); break; case View::Network: keys::add(out, keys::kWifiNetwork); break; case View::Details: keys::add(out, keys::kWifiStatus); break; case View::Scan: keys::add(out, keys::kWifiScan); break; case View::Ssid: keys::add(out, keys::kWifiName); break; case View::Password: case View::Edit: keys::add(out, keys::kText); break; } } const char* WifiSettingsPage::helpTitle() const { switch (view_) { case View::Servers: return "DNS and NTP"; case View::Network: return "A saved network"; case View::Details: return "Wi-Fi status"; case View::Scan: return "Add a network"; case View::Ssid: return "Hidden network name"; case View::Password: return "Wi-Fi password"; case View::Edit: return "Wi-Fi: a field"; default: return "Wi-Fi"; } } // 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()}; } 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