#include "firmware_page.h" #include #include #include "cleanup_plan.h" #include "text_wrap.h" #include "ui/fonts.h" #include "ui/widgets.h" #include "update_check.h" #include "version.h" namespace roro { namespace { constexpr const char* kUpdatesFolder = "/updates"; bool endsWith(const std::string& s, const std::string& suffix) { return s.size() >= suffix.size() && s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0; } } // namespace void FirmwarePage::enter() { confirm_.reset(); ask_ = Ask::None; view_ = View::Main; message_.clear(); { std::lock_guard g(lock_); files_.clear(); listed_ = false; } list_.setCount(kFixed); storage_.runJob([this]() { std::vector found; File dir = SD.open(kUpdatesFolder); if (dir && dir.isDirectory()) for (File f = dir.openNextFile(); f; f = dir.openNextFile()) if (!f.isDirectory() && endsWith(f.name(), ".ota")) found.push_back(std::string(kUpdatesFolder) + "/" + f.name()); std::lock_guard g(lock_); files_ = found; listed_ = true; }); } std::vector FirmwarePage::files() { std::lock_guard g(lock_); return files_; } void FirmwarePage::say(const std::string& text) { message_ = text; messageMs_ = millis(); } // The row for the latest release: what's known, in a few words. std::string FirmwarePage::latestText(bool& warn) const { warn = false; GiteaReleases& g = update_.gitea(); if (update_.giteaBusy() || g.status() == GiteaReleases::Status::Busy) return "checking..."; release::Release r; if (g.status() == GiteaReleases::Status::Failed && !g.latest(r)) { warn = true; return "the check failed"; } if (!g.latest(r)) return "not checked yet"; return r.tag + (release::isNewer(r, update_.runningVersion()) ? " (new)" : " (current)"); } void FirmwarePage::openRelease(const release::Release& r) { shown_ = r; shownTop_ = 0; view_ = View::Release; } // Can the release on the page be installed from here, and if not, why (in words for the screen)? bool FirmwarePage::installable(std::string& why) const { std::string running = update_.runningVersion(); if (!shown_.usable()) why = "Nothing to install in it"; else if (!release::versionNewer(shown_.tag, running) && !versionOlder(shown_.tag, running)) why = "That's the version running"; else if (wifi_.state() != WifiController::State::Connected) why = "Not connected to Wi-Fi"; else return true; return false; } bool FirmwarePage::onKey(const KeyEvent& e) { if (view_ != View::Main) { if (confirm_) { confirm_->onKey(e); if (confirm_->result() == 1 && ask_ == Ask::Release) { std::string why = update_.requestInstall(shown_.tag); if (!why.empty()) say(why); } if (confirm_->result() != DialogModel::kPending) confirm_.reset(); return true; } if (view_ == View::Release) { if (e.key == Key::Back) view_ = olderReleases_.empty() || older_.count() == 0 ? View::Main : View::Older; else if (e.key == Key::Up) shownTop_ = std::max(0, shownTop_ - 1); else if (e.key == Key::Down) shownTop_++; // clamped when drawn else if (e.key == Key::Char && (e.ch == 'c' || e.ch == 'C') && !update_.giteaBusy()) update_.requestCheck(); else if (e.key == Key::Select) { std::string why; if (!installable(why)) return say(why), true; ask_ = Ask::Release; confirm_.reset(new DialogModel({"Cancel", "Install"})); } return true; } // The older releases. if (e.key == Key::Back) view_ = View::Main; else if (e.key == Key::Up) older_.up(); else if (e.key == Key::Down) older_.down(); else if (e.key == Key::Char && (e.ch == 'c' || e.ch == 'C') && !update_.giteaBusy()) update_.requestList(); else if (e.key == Key::Select && older_.selected() >= 0 && older_.selected() < static_cast(olderReleases_.size())) openRelease(olderReleases_[older_.selected()]); return true; } auto found = files(); if (confirm_) { confirm_->onKey(e); int pick = list_.selected() - kFixed; if (confirm_->result() == 1 && pick >= 0 && pick < static_cast(found.size())) update_.installFromSd(found[pick]); if (confirm_->result() != DialogModel::kPending) confirm_.reset(); return true; } list_.setCount(kFixed + static_cast(found.size())); switch (e.key) { case Key::Up: list_.up(); break; case Key::Down: list_.down(); break; case Key::Back: return false; case Key::Char: if ((e.ch == 'c' || e.ch == 'C') && !update_.giteaBusy()) update_.requestCheck(); break; case Key::Select: if (list_.selected() == kLatest) { release::Release r; if (update_.giteaBusy()) break; if (update_.gitea().latest(r)) openRelease(r); else update_.requestCheck(); } else if (list_.selected() == kOlder) { view_ = View::Older; if (olderReleases_.empty() && !update_.giteaBusy()) update_.requestList(); } else if (list_.selected() >= kFixed) { ask_ = Ask::SdFile; confirm_.reset(new DialogModel({"Cancel", "Install"})); } break; default: break; } return true; } void FirmwarePage::draw(Canvas& c) { if (!message_.empty() && millis() - messageMs_ > 4000) message_.clear(); // What the last answer left: the list of older releases. if (update_.gitea().seq() != olderSeq_ && !update_.giteaBusy()) { olderSeq_ = update_.gitea().seq(); olderReleases_ = update_.gitea().list(); older_.setCount(static_cast(olderReleases_.size())); } switch (view_) { case View::Main: drawMain(c); break; case View::Release: drawRelease(c); break; case View::Older: drawOlder(c); break; } if (confirm_ && view_ != View::Main) { std::string running = update_.runningVersion(); bool older = versionOlder(shown_.tag, running); widgets::dialog(c, older ? "Go back?" : "Install update?", older ? shown_.tag + " is older than " + running + ". Install it anyway? The device restarts." : shown_.tag + " replaces " + running + ". The device restarts once it's written.", *confirm_); } if (!message_.empty() && !confirm_) { c.setFont(&fonts::small); c.setTextColor(theme::kWarning); c.setTextDatum(top_left); c.drawString(message_.c_str(), 4, theme::kContent.y + theme::kContent.h - 9); } } void FirmwarePage::drawRelease(Canvas& c) { const auto& area = theme::kContent; std::string running = update_.runningVersion(); std::vector lines; lines.push_back(shown_.tag + (shown_.tag == running ? " (running)" : "")); lines.push_back("Published " + shown_.date() + ", " + formatBytes(shown_.otaSize)); if (versionOlder(shown_.tag, running)) lines.push_back("Older than what's running"); auto measure = widgets::bodyMeasure(c); lines.push_back(""); for (auto& l : wrapText(shown_.notes, area.w - 12, measure)) lines.push_back(l); int rows = (area.h - 12) / theme::kLineHeight; shownTop_ = std::clamp(shownTop_, 0, std::max(0, static_cast(lines.size()) - rows)); widgets::textLines(c, lines, shownTop_, {area.x + 2, area.y + 2, area.w - 2, area.h - 12}); std::string why; bool ok = installable(why); c.setFont(&fonts::small); c.setTextDatum(top_left); c.setTextColor(theme::kMuted); if (!ok) c.drawString(why.c_str(), 4, area.y + area.h - 9); // why it can't be installed from here } void FirmwarePage::drawOlder(Canvas& c) { const auto& area = theme::kContent; GiteaReleases& g = update_.gitea(); c.setTextDatum(top_left); if (update_.giteaBusy() || g.status() == GiteaReleases::Status::Busy) { c.setFont(&fonts::body); c.setTextColor(theme::kMuted); c.drawString("Looking at the server...", 4, area.y + 4); return; } if (olderReleases_.empty()) { c.setFont(&fonts::body); c.setTextColor(g.status() == GiteaReleases::Status::Failed ? theme::kWarning : theme::kMuted); c.drawString(g.status() == GiteaReleases::Status::Failed ? g.error().c_str() : "No releases known.", 4, area.y + 4); c.setFont(&fonts::small); c.setTextColor(theme::kMuted); c.drawString("c: look again", 4, area.y + area.h - 9); return; } std::string running = update_.runningVersion(); widgets::list( c, older_, {area.x, area.y, area.w, area.h - 11}, [&](int i) { const auto& r = olderReleases_[i]; return r.tag + (r.tag == running ? " (running)" : release::versionNewer(r.tag, running) ? " (new)" : ""); }, [&](int i) { return olderReleases_[i].date(); }); } void FirmwarePage::help(std::vector& out) const { if (confirm_) return help::dialog(out); switch (view_) { case View::Main: help::list(out, "check, open, or install"); out.push_back({"c", "look for a newer release"}); break; case View::Release: out.push_back({"; .", "scroll"}); out.push_back({"Enter", "install it"}); out.push_back({"c", "check again"}); break; case View::Older: help::list(out, "its details"); out.push_back({"c", "read the list again"}); break; } } const char* FirmwarePage::helpTitle() const { return view_ == View::Release ? "A release" : view_ == View::Older ? "Older releases" : "Firmware"; } void FirmwarePage::drawMain(Canvas& c) { auto found = files(); bool listed; { std::lock_guard g(lock_); listed = listed_; } list_.setCount(kFixed + static_cast(found.size())); std::string ip = wifi_.ip(); widgets::list( c, list_, theme::kContent, [&](int i) -> std::string { switch (i) { case kVersion: return "Version"; case kStatus: return "Status"; case kAddress: return "Push to"; case kLatest: return "Latest release"; case kOlder: return "Older releases"; case kSdHeader: return listed ? (found.empty() ? "No .ota files in /updates" : "On the SD card:") : "Looking on the SD card..."; default: return " " + found[i - kFixed].substr(std::string(kUpdatesFolder).size() + 1); } }, [&](int i) -> std::string { switch (i) { case kVersion: return versionString(); case kStatus: return update_.onProbation() ? "on probation" : "confirmed"; case kAddress: return ip.empty() ? "Wi-Fi not connected" : ip + ":" + std::to_string(UpdateService::kPort); case kLatest: { bool warn; return latestText(warn); } case kOlder: return ">"; case kSdHeader: return ""; default: return "install >"; } }); if (confirm_) { int pick = list_.selected() - kFixed; std::string name = pick >= 0 && pick < static_cast(found.size()) ? found[pick] : ""; widgets::dialog(c, "Install update?", name + " is checked before anything is written. The device restarts after.", *confirm_); } } } // namespace roro