Public Access
"A Debug Build keeps its console: update it from your PC" ran off the edge of the release page. It now reads "Debug Build: update from the PC". Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
285 lines
11 KiB
C++
285 lines
11 KiB
C++
#include "firmware_page.h"
|
|
|
|
#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 {
|
|
|
|
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<std::mutex> g(lock_);
|
|
files_.clear();
|
|
listed_ = false;
|
|
}
|
|
list_.setCount(kFixed);
|
|
storage_.runJob([this]() {
|
|
std::vector<std::string> 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<std::mutex> g(lock_);
|
|
files_ = found;
|
|
listed_ = true;
|
|
});
|
|
}
|
|
|
|
std::vector<std::string> FirmwarePage::files() {
|
|
std::lock_guard<std::mutex> 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 "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 = "Debug Build: update from the PC"; // it keeps its console (Q171)
|
|
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);
|
|
int pick = list_.selected() - kFixed;
|
|
if (confirm_->result() == 1 && pick >= 0 && pick < static_cast<int>(found.size())) update_.installFromSd(found[pick]);
|
|
if (confirm_->result() != DialogModel::kPending) confirm_.reset();
|
|
return true;
|
|
}
|
|
list_.setCount(kFixed + static_cast<int>(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<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").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;
|
|
{
|
|
std::lock_guard<std::mutex> g(lock_);
|
|
listed = listed_;
|
|
}
|
|
list_.setCount(kFixed + static_cast<int>(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<int>(found.size()) ? found[pick] : "";
|
|
widgets::dialog(c, "Install update?", name + " is checked before anything is written. The device restarts after.", *confirm_);
|
|
}
|
|
}
|
|
|
|
} // namespace roro
|