From 4ef41347abb1b71df66fb81650aa9ebfd752cb1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Tue, 6 Oct 2026 16:06:49 +0200 Subject: [PATCH] Updates from Gitea, step 4: the Firmware page shows the project's releases Latest release (checked on Enter, or c), Older releases (the last ten, newest first), a release page with the tag's message, and an install dialog; going back to an older release asks differently. A failed check someone asked for shows a Toast. A Debug Build shows the latest release and says it can't install it: it would take its console away. Tried on the device: a check, the list, the release page, the dialog (Cancel is the default; Back cancels), and the install from the screen with its progress screen, then the restart into the release build and its confirmation. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- src/apps/firmware_page.cpp | 185 +++++++++++++++++++++++++++++++- src/apps/firmware_page.h | 27 ++++- src/services/update_service.cpp | 5 +- 3 files changed, 212 insertions(+), 5 deletions(-) diff --git a/src/apps/firmware_page.cpp b/src/apps/firmware_page.cpp index ead3b64..b72ef2c 100644 --- a/src/apps/firmware_page.cpp +++ b/src/apps/firmware_page.cpp @@ -2,7 +2,14 @@ #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 { @@ -17,6 +24,9 @@ bool endsWith(const std::string& s, const std::string& suffix) { void FirmwarePage::enter() { confirm_.reset(); + ask_ = Ask::None; + view_ = View::Main; + message_.clear(); { std::lock_guard g(lock_); files_.clear(); @@ -40,7 +50,76 @@ std::vector FirmwarePage::files() { 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 "failed: Enter retries"; + } + if (!g.latest(r)) return "Enter: check"; + 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 (release::isDebugBuild(running)) why = "A Debug Build keeps its console: update it from your PC"; + 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); @@ -54,8 +133,22 @@ bool FirmwarePage::onKey(const KeyEvent& e) { 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() >= kFixed) confirm_.reset(new DialogModel({"Cancel", "Install"})); + 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; } @@ -63,6 +156,89 @@ bool FirmwarePage::onKey(const KeyEvent& e) { } 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); + c.drawString(ok ? "Enter: install c: check again" : (why + " c: check again").c_str(), 4, area.y + area.h - 9); +} + +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(); }); + c.setFont(&fonts::small); + c.setTextColor(theme::kMuted); + c.drawString("Enter: details c: look again", 4, area.y + area.h - 9); +} + +void FirmwarePage::drawMain(Canvas& c) { auto found = files(); bool listed; { @@ -78,6 +254,8 @@ void FirmwarePage::draw(Canvas& c) { 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); } @@ -87,6 +265,11 @@ void FirmwarePage::draw(Canvas& c) { 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 >"; } diff --git a/src/apps/firmware_page.h b/src/apps/firmware_page.h index e59b05d..25ce79e 100644 --- a/src/apps/firmware_page.h +++ b/src/apps/firmware_page.h @@ -8,6 +8,7 @@ #include "dialog_model.h" #include "key_event.h" #include "list_model.h" +#include "release_info.h" #include "services/storage_service.h" #include "services/update_service.h" #include "services/wifi_service.h" @@ -16,8 +17,9 @@ namespace roro { -// Settings → Firmware: the running version and its Probation, where to push Firmware Updates, and -// the Update Files on the SD card (in /updates) to install from. +// Settings → Firmware: the running version and its Probation, where to push Firmware Updates, the +// project's releases on Gitea (the latest, and the ten before it), and the Update Files on the SD +// card (in /updates) to install from. class FirmwarePage { public: FirmwarePage(UpdateService& update, WifiService& wifi, StorageService& storage) @@ -28,15 +30,34 @@ class FirmwarePage { void draw(Canvas& c); private: - enum Row { kVersion, kStatus, kAddress, kSdHeader, kFixed }; + enum Row { kVersion, kStatus, kAddress, kLatest, kOlder, kSdHeader, kFixed }; + enum class View { Main, Release, Older }; + enum class Ask { None, SdFile, Release }; std::vector files(); + std::string latestText(bool& warn) const; + void openRelease(const release::Release& r); + bool installable(std::string& why) const; // the shown release: can it be installed from here? + void drawMain(Canvas& c); + void drawRelease(Canvas& c); + void drawOlder(Canvas& c); + void say(const std::string& text); UpdateService& update_; WifiService& wifi_; StorageService& storage_; ListModel list_{theme::kContent.h / theme::kLineHeight}; + ListModel older_{(theme::kContent.h - 11) / theme::kLineHeight}; // above the hint line std::unique_ptr confirm_; + Ask ask_ = Ask::None; + + View view_ = View::Main; + release::Release shown_; // the release page + std::vector olderReleases_; + uint32_t olderSeq_ = 0; + int shownTop_ = 0; + std::string message_; + uint32_t messageMs_ = 0; // Filled on the storage task. std::mutex lock_; diff --git a/src/services/update_service.cpp b/src/services/update_service.cpp index 3ee0a63..69cfde4 100644 --- a/src/services/update_service.cpp +++ b/src/services/update_service.cpp @@ -217,7 +217,7 @@ std::string UpdateService::failedVersion() const { std::string UpdateService::requestInstall(const std::string& tag, bool force) { if (phase_ != Phase::Idle || giteaBusy()) return "Busy with something else"; - if (!force && release::isDebugBuild(versionString())) return "A Debug Build keeps its console: update it from your PC"; + if (!force && release::isDebugBuild(runningVersion())) return "A Debug Build keeps its console: update it from your PC"; if (wifi_.state() != WifiController::State::Connected) return "No Wi-Fi"; release::Release r; if (!gitea_.find(tag, r)) return "Look for releases first"; @@ -305,6 +305,9 @@ void UpdateService::serve() { #endif case Request::None: break; } + // A check or a list someone asked for that failed says so; the daily one stays quiet. + if ((r == Request::Check || r == Request::List) && gitea_.status() == GiteaReleases::Status::Failed) + notify("Releases: " + gitea_.error(), NotificationLevel::Warning); // IRC comes back, unless the device is about to restart into an update. if (held_ && phase_ != Phase::Installed && holdMemory) holdMemory(false); held_ = false;