#include "settings_app.h" #include #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(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); } 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(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: page_ = menu_.row(i) == Row::Storage ? Page::Storage : Page::About; 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. if ((page_ == Page::Storage || page_ == Page::About) && nowMs - lastRefreshMs_ >= 1000) { lastRefreshMs_ = nowMs; requestRedraw(); } } std::vector SettingsApp::storageLines() const { auto s = d_.storage.state(); std::vector 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(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 SettingsApp::aboutLines() const { std::vector 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; } } } // namespace roro