Public Access
M1 step 2: Wi-Fi Service, Saved Networks, NTP, Settings page
- lib/wifi (host-tested): SavedNetworks (up to 8, validated, hidden flag, persisted) and WifiController (joins the strongest Saved Network, tries hidden ones in turn, backoff 10/30/60 s, connect timeout, Monitoring override, radio off without Saved Networks) - WifiService carries out the controller's actions, sets the EU country code, and syncs the Clock over SNTP without touching the TZ - Wi-Fi On/Off setting; Settings > Wi-Fi page: status, add from a scan or a hidden network, forget - Status Bar: W + signal bars, W? while searching, MON while monitoring - Serial: wifi add / wifi status (replaces the step 1 heap probe) Verified on the device: joins the test network ~5 s after boot, clock set over NTP, ~129 KB free heap while connected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
@@ -30,6 +30,9 @@ bool SettingsApp::onKey(const KeyEvent& e) {
|
||||
case Page::Choice: return onChoiceKey(e);
|
||||
case Page::Storage: return onStorageKey(e);
|
||||
case Page::About: return onAboutKey(e);
|
||||
case Page::Wifi:
|
||||
if (!wifiPage_.onKey(e)) page_ = Page::Menu;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -61,7 +64,16 @@ bool SettingsApp::onMenuKey(const KeyEvent& e) {
|
||||
break;
|
||||
case Kind::Toggle: menu_.toggle(i); break;
|
||||
case Kind::Slider: break;
|
||||
case Kind::Page: page_ = menu_.row(i) == Row::Storage ? Page::Storage : Page::About; break;
|
||||
case Kind::Page:
|
||||
switch (menu_.row(i)) {
|
||||
case Row::Storage: page_ = Page::Storage; break;
|
||||
case Row::Wifi:
|
||||
page_ = Page::Wifi;
|
||||
wifiPage_.enter();
|
||||
break;
|
||||
default: page_ = Page::About; break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -123,7 +135,8 @@ bool SettingsApp::onAboutKey(const KeyEvent& e) {
|
||||
|
||||
void SettingsApp::update(uint32_t nowMs) {
|
||||
// Live values on Storage and About.
|
||||
if ((page_ == Page::Storage || page_ == Page::About) && nowMs - lastRefreshMs_ >= 1000) {
|
||||
bool live = page_ == Page::Storage || page_ == Page::About || (page_ == Page::Wifi && wifiPage_.live());
|
||||
if (live && nowMs - lastRefreshMs_ >= 500) {
|
||||
lastRefreshMs_ = nowMs;
|
||||
requestRedraw();
|
||||
}
|
||||
@@ -204,6 +217,7 @@ void SettingsApp::draw(Canvas& c) {
|
||||
*eraseDialog_);
|
||||
break;
|
||||
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
|
||||
case Page::Wifi: wifiPage_.draw(c); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-3
@@ -13,6 +13,7 @@
|
||||
#include "services/battery_service.h"
|
||||
#include "services/clock_service.h"
|
||||
#include "services/storage_service.h"
|
||||
#include "apps/wifi_settings_page.h"
|
||||
#include "settings_menu.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
@@ -25,20 +26,25 @@ struct SettingsAppDeps {
|
||||
BatteryService& battery;
|
||||
StorageService& storage;
|
||||
ClockService& clock;
|
||||
WifiService& wifi;
|
||||
SavedNetworks& savedNetworks;
|
||||
};
|
||||
|
||||
// Settings: every user-facing setting, plus the Storage and About pages.
|
||||
class SettingsApp : public App {
|
||||
public:
|
||||
explicit SettingsApp(const SettingsAppDeps& deps) : d_(deps), menu_(deps.settings) {}
|
||||
explicit SettingsApp(const SettingsAppDeps& deps)
|
||||
: d_(deps), menu_(deps.settings), wifiPage_(deps.settings, deps.savedNetworks, deps.wifi, deps.bus) {}
|
||||
void onEnter() override;
|
||||
bool onKey(const KeyEvent& e) override;
|
||||
void update(uint32_t nowMs) override;
|
||||
bool textEntryActive() const override { return page_ == Page::Text; }
|
||||
bool textEntryActive() const override {
|
||||
return page_ == Page::Text || (page_ == Page::Wifi && wifiPage_.textEntryActive());
|
||||
}
|
||||
void draw(Canvas& c) override;
|
||||
|
||||
private:
|
||||
enum class Page { Menu, Text, Choice, Storage, About };
|
||||
enum class Page { Menu, Text, Choice, Storage, About, Wifi };
|
||||
|
||||
bool onMenuKey(const KeyEvent& e);
|
||||
bool onTextKey(const KeyEvent& e);
|
||||
@@ -51,6 +57,7 @@ class SettingsApp : public App {
|
||||
|
||||
SettingsAppDeps d_;
|
||||
SettingsMenu menu_;
|
||||
WifiSettingsPage wifiPage_;
|
||||
Page page_ = Page::Menu;
|
||||
ListModel list_{theme::kContent.h / theme::kLineHeight};
|
||||
ListModel choices_{theme::kContent.h / theme::kLineHeight};
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "wifi_settings_page.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()); }
|
||||
|
||||
void WifiSettingsPage::warn(const std::string& text, NotificationLevel level) {
|
||||
bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(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(saved_.at(main_.selected() - kFixedRows).ssid);
|
||||
refreshMain();
|
||||
}
|
||||
if (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::Scan: {
|
||||
const auto& found = wifi_.listScan();
|
||||
if (wifi_.listScanDone()) scan_.setCount(static_cast<int>(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;
|
||||
}
|
||||
|
||||
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 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 kAddScanned:
|
||||
case kAddHidden: return ">";
|
||||
default: return "";
|
||||
}
|
||||
});
|
||||
if (forgetDialog_ && main_.selected() >= kFixedRows)
|
||||
widgets::dialog(c, "Forget network?", saved_.at(main_.selected() - kFixedRows).ssid, *forgetDialog_);
|
||||
break;
|
||||
|
||||
case View::Scan: {
|
||||
if (!wifi_.listScanDone()) {
|
||||
widgets::textLines(c, {"Scanning..."}, 0, area);
|
||||
break;
|
||||
}
|
||||
const auto& found = wifi_.listScan();
|
||||
if (scan_.count() != static_cast<int>(found.size())) scan_.setCount(static_cast<int>(found.size()));
|
||||
if (found.empty()) {
|
||||
widgets::textLines(c, {"No networks found.", "", "`: back"}, 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});
|
||||
c.drawString(view_ == View::Ssid ? "Enter: next `: cancel" : "Enter: save (empty = open) `: cancel", 4,
|
||||
area.y + 44);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "dialog_model.h"
|
||||
#include "event_bus.h"
|
||||
#include "key_event.h"
|
||||
#include "line_editor.h"
|
||||
#include "list_model.h"
|
||||
#include "saved_networks.h"
|
||||
#include "services/wifi_service.h"
|
||||
#include "settings.h"
|
||||
#include "ui/canvas.h"
|
||||
#include "ui/theme.h"
|
||||
|
||||
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.
|
||||
class WifiSettingsPage {
|
||||
public:
|
||||
WifiSettingsPage(Settings& settings, SavedNetworks& saved, WifiService& wifi, EventBus& bus)
|
||||
: settings_(settings), saved_(saved), wifi_(wifi), bus_(bus) {}
|
||||
|
||||
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
|
||||
void draw(Canvas& c);
|
||||
|
||||
private:
|
||||
enum class View { Main, Scan, Ssid, Password };
|
||||
enum Fixed { kToggle, kStatus, kAddScanned, kAddHidden, kFixedRows };
|
||||
|
||||
void refreshMain();
|
||||
void warn(const std::string& text, NotificationLevel level = NotificationLevel::Warning);
|
||||
std::string statusText() const;
|
||||
|
||||
Settings& settings_;
|
||||
SavedNetworks& saved_;
|
||||
WifiService& wifi_;
|
||||
EventBus& bus_;
|
||||
View view_ = View::Main;
|
||||
ListModel main_{theme::kContent.h / theme::kLineHeight};
|
||||
ListModel scan_{theme::kContent.h / theme::kLineHeight};
|
||||
LineEditor editor_{63};
|
||||
std::string newSsid_;
|
||||
bool newHidden_ = false;
|
||||
std::unique_ptr<DialogModel> forgetDialog_;
|
||||
};
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user