Public Access
- 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
225 lines
7.9 KiB
C++
225 lines
7.9 KiB
C++
#include "settings_app.h"
|
|
|
|
#include <Arduino.h>
|
|
|
|
#include "platform/identity.h"
|
|
#include "ui/fonts.h"
|
|
#include "ui/widgets.h"
|
|
#include "version.h"
|
|
|
|
namespace roro {
|
|
|
|
using Kind = SettingsMenu::Kind;
|
|
using Row = SettingsMenu::Row;
|
|
|
|
void SettingsApp::onEnter() {
|
|
page_ = Page::Menu;
|
|
eraseDialog_.reset();
|
|
list_.setCount(menu_.count());
|
|
}
|
|
|
|
void SettingsApp::warn(const std::string& text) {
|
|
d_.bus.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(NotificationLevel::Warning)));
|
|
}
|
|
|
|
bool SettingsApp::onKey(const KeyEvent& e) {
|
|
requestRedraw();
|
|
switch (page_) {
|
|
case Page::Menu: return onMenuKey(e);
|
|
case Page::Text: return onTextKey(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;
|
|
}
|
|
|
|
bool SettingsApp::onMenuKey(const KeyEvent& e) {
|
|
int i = list_.selected();
|
|
switch (e.key) {
|
|
case Key::Up: list_.up(); return true;
|
|
case Key::Down: list_.down(); return true;
|
|
case Key::Left:
|
|
case Key::Right:
|
|
if (menu_.kind(i) == Kind::Slider) menu_.adjust(i, e.key == Key::Right ? 1 : -1);
|
|
if (menu_.kind(i) == Kind::Toggle) menu_.toggle(i);
|
|
return true;
|
|
case Key::Select: break;
|
|
default: return false; // Back leaves Settings
|
|
}
|
|
editingRow_ = i;
|
|
switch (menu_.kind(i)) {
|
|
case Kind::Text:
|
|
editor_ = LineEditor(menu_.maxBytes(i));
|
|
editor_.setText(menu_.value(i));
|
|
page_ = Page::Text;
|
|
break;
|
|
case Kind::Choice:
|
|
choices_.setCount(static_cast<int>(menu_.choices(i).size()));
|
|
choices_.select(menu_.currentChoice(i) < 0 ? 0 : menu_.currentChoice(i));
|
|
page_ = Page::Choice;
|
|
break;
|
|
case Kind::Toggle: menu_.toggle(i); break;
|
|
case Kind::Slider: 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;
|
|
}
|
|
|
|
bool SettingsApp::onTextKey(const KeyEvent& e) {
|
|
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::Select: {
|
|
std::string error = menu_.setText(editingRow_, editor_.text());
|
|
if (error.empty()) page_ = Page::Menu;
|
|
else warn(error);
|
|
break;
|
|
}
|
|
case Key::Back: page_ = Page::Menu; break; // cancel
|
|
default: break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SettingsApp::onChoiceKey(const KeyEvent& e) {
|
|
switch (e.key) {
|
|
case Key::Up: choices_.up(); break;
|
|
case Key::Down: choices_.down(); break;
|
|
case Key::Select: {
|
|
std::string error = menu_.choose(editingRow_, choices_.selected());
|
|
if (error.empty()) page_ = Page::Menu;
|
|
else warn(error);
|
|
break;
|
|
}
|
|
case Key::Back: page_ = Page::Menu; break;
|
|
default: break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SettingsApp::onStorageKey(const KeyEvent& e) {
|
|
if (eraseDialog_) {
|
|
eraseDialog_->onKey(e);
|
|
if (eraseDialog_->result() == 1 && !d_.storage.requestFormat()) warn("A format is already running");
|
|
if (eraseDialog_->result() != DialogModel::kPending) eraseDialog_.reset();
|
|
return true;
|
|
}
|
|
if (e.key == Key::Select) {
|
|
if (d_.storage.state().present) eraseDialog_.reset(new DialogModel({"Cancel", "Erase"}));
|
|
else warn("No SD card");
|
|
}
|
|
if (e.key == Key::Back) page_ = Page::Menu;
|
|
return true;
|
|
}
|
|
|
|
bool SettingsApp::onAboutKey(const KeyEvent& e) {
|
|
if (e.key == Key::Back) page_ = Page::Menu;
|
|
if (e.key == Key::Char && e.ch == 'w') d_.apps.open("demo"); // hidden widget demo
|
|
return true;
|
|
}
|
|
|
|
void SettingsApp::update(uint32_t nowMs) {
|
|
// Live values on Storage and About.
|
|
bool live = page_ == Page::Storage || page_ == Page::About || (page_ == Page::Wifi && wifiPage_.live());
|
|
if (live && nowMs - lastRefreshMs_ >= 500) {
|
|
lastRefreshMs_ = nowMs;
|
|
requestRedraw();
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> SettingsApp::storageLines() const {
|
|
auto s = d_.storage.state();
|
|
std::vector<std::string> lines;
|
|
char buf[64];
|
|
if (d_.storage.formatting()) {
|
|
lines.push_back("Erasing the SD card...");
|
|
} else if (!s.present) {
|
|
lines.push_back("No SD card inserted.");
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "SD card: %u MB, %d%% used", static_cast<unsigned>(s.totalBytes >> 20), s.usedPercent);
|
|
lines.push_back(buf);
|
|
lines.push_back(s.logsAllowed ? "Logs: recording" : "Logs: paused (over 90%)");
|
|
lines.push_back(s.capturesAllowed ? "Captures: allowed" : "Captures: stopped (card full)");
|
|
}
|
|
lines.push_back("");
|
|
lines.push_back("Enter: erase SD card");
|
|
return lines;
|
|
}
|
|
|
|
std::vector<std::string> SettingsApp::aboutLines() const {
|
|
std::vector<std::string> lines;
|
|
char buf[64];
|
|
snprintf(buf, sizeof(buf), "%s %s", kProductName, versionString());
|
|
lines.push_back(buf);
|
|
lines.push_back("Node " + identity::nodeId());
|
|
const auto& b = d_.battery.estimator();
|
|
snprintf(buf, sizeof(buf), "Battery %d%% (%d mV)", b.percent(), b.millivolts());
|
|
lines.push_back(buf);
|
|
snprintf(buf, sizeof(buf), "Memory %u KB free (min %u KB)", ESP.getFreeHeap() / 1024, ESP.getMinFreeHeap() / 1024);
|
|
lines.push_back(buf);
|
|
uint32_t up = millis() / 1000;
|
|
snprintf(buf, sizeof(buf), "Uptime %luh %02lum %02lus", (unsigned long)(up / 3600), (unsigned long)(up / 60 % 60),
|
|
(unsigned long)(up % 60));
|
|
lines.push_back(buf);
|
|
lines.push_back("Clock " + d_.clock.displayTime());
|
|
lines.push_back("License GPL-3.0");
|
|
return lines;
|
|
}
|
|
|
|
void SettingsApp::draw(Canvas& c) {
|
|
const auto& area = theme::kContent;
|
|
switch (page_) {
|
|
case Page::Menu:
|
|
widgets::list(
|
|
c, list_, area, [this](int i) { return menu_.label(i); },
|
|
[this](int i) {
|
|
auto kind = menu_.kind(i);
|
|
if (kind == Kind::Page) return std::string(">");
|
|
if (kind == Kind::Slider) return "< " + menu_.value(i) + " >";
|
|
return menu_.value(i);
|
|
});
|
|
break;
|
|
case Page::Text:
|
|
c.setFont(&fonts::body);
|
|
c.setTextColor(theme::kMuted);
|
|
c.drawString(menu_.label(editingRow_).c_str(), 4, area.y + 4);
|
|
widgets::lineEditor(c, editor_, {4, area.y + 22, area.w - 8, 0});
|
|
c.drawString(("Enter: save `: cancel " + std::to_string(editor_.text().size()) + "/" +
|
|
std::to_string(editor_.maxBytes()) + " bytes")
|
|
.c_str(),
|
|
4, area.y + 44);
|
|
break;
|
|
case Page::Choice: {
|
|
auto options = menu_.choices(editingRow_);
|
|
int current = menu_.currentChoice(editingRow_);
|
|
widgets::list(c, choices_, area, [&](int i) { return (i == current ? "* " : " ") + options[i]; });
|
|
break;
|
|
}
|
|
case Page::Storage:
|
|
widgets::textLines(c, storageLines(), 0, area);
|
|
if (eraseDialog_)
|
|
widgets::dialog(c, "Erase SD card?", "Everything on the card is deleted. It becomes one FAT32 partition.",
|
|
*eraseDialog_);
|
|
break;
|
|
case Page::About: widgets::textLines(c, aboutLines(), 0, area); break;
|
|
case Page::Wifi: wifiPage_.draw(c); break;
|
|
}
|
|
}
|
|
|
|
} // namespace roro
|