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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-06 16:06:49 +02:00
co-authored by Claude Sonnet 5.5
parent 510ce42a99
commit 4ef41347ab
3 changed files with 212 additions and 5 deletions
+184 -1
View File
@@ -2,7 +2,14 @@
#include <SD.h>
#include <algorithm>
#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<std::mutex> g(lock_);
files_.clear();
@@ -40,7 +50,76 @@ std::vector<std::string> 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<int>(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<int>(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<std::string> 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<int>(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 >";
}
+24 -3
View File
@@ -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<std::string> 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<DialogModel> confirm_;
Ask ask_ = Ask::None;
View view_ = View::Main;
release::Release shown_; // the release page
std::vector<release::Release> olderReleases_;
uint32_t olderSeq_ = 0;
int shownTop_ = 0;
std::string message_;
uint32_t messageMs_ = 0;
// Filled on the storage task.
std::mutex lock_;
+4 -1
View File
@@ -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;