Public Access
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:
+184
-1
@@ -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 >";
|
||||
}
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user