Public Access
Updates: a newer release installs by itself, after a question
With Settings > System > Install updates on By itself (issue #52), a release found by the daily check is offered once no key was pressed for 2 minutes and nothing would be cut by a restart. The question goes over whatever App is open, with Cancel and Accept, and installs after 30 s without an answer. Each update that held, and each one undone, is a dated line in /system/updates.log on the card. Tried on the device: the question, Cancel, and an install of v0.23.0 left unanswered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
+69
-3
@@ -20,6 +20,7 @@
|
||||
#include "services/web_share.h"
|
||||
#include "apps/gemini_app.h"
|
||||
#include "apps/gnss_app.h"
|
||||
#include "auto_update.h"
|
||||
#include "apps/irc_app.h"
|
||||
#include "apps/launcher_app.h"
|
||||
#include "apps/lora_scanner_app.h"
|
||||
@@ -81,6 +82,12 @@ static Screen screen;
|
||||
static BatteryService* battery;
|
||||
static StorageService* storageService;
|
||||
static FileOps* fileOps;
|
||||
static WebShare* webShare;
|
||||
// Installing a release by itself (issue #52): see autoUpdateStep().
|
||||
static release::AutoUpdate autoUpdate;
|
||||
static std::unique_ptr<DialogModel> autoDialog;
|
||||
static uint32_t lastKeyMs = 0;
|
||||
static bool autoRedraw = false;
|
||||
static std::vector<std::string> filesInUse(const std::string& path, bool folder);
|
||||
static PowerService* power;
|
||||
static ClockService* clockService;
|
||||
@@ -238,7 +245,7 @@ void setup() {
|
||||
apps->registerApp({"gnss", "GNSS", false, new GnssApp(*gnssService, settings, *storageService), icons::gnss});
|
||||
apps->registerApp({"gemini", "Gemini", false, new GeminiApp(*geminiService), icons::gemini});
|
||||
apps->registerApp({"lora", "LoRa Scanner", false, new LoraScannerApp(*radioService, *loraCapture, settings, *clockService), icons::lora});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus, *new WebShare(*storageService, *fileOps, *wifi)), icons::storage});
|
||||
apps->registerApp({"storage", "Storage", false, new StorageApp(*fileOps, *storageService, *clockService, *update, *power, bus, *(webShare = new WebShare(*storageService, *fileOps, *wifi))), icons::storage});
|
||||
apps->registerApp({"notes", "Notes", false, new NotesApp(*fileOps, *storageService, *clockService, *power), icons::notes});
|
||||
sshService = new SshService(settings);
|
||||
sshApp = new SshApp(*sshService, settings, *storageService);
|
||||
@@ -569,6 +576,9 @@ static void updateCommand(const String& args) {
|
||||
console.printf("update: running %s, failed here before: %s, daily check %s, heap %u\n", update->runningVersion().c_str(),
|
||||
update->failedVersion().empty() ? "none" : update->failedVersion().c_str(),
|
||||
settings.getBool(Setting::CheckUpdates) ? "on" : "off", (unsigned)ESP.getFreeHeap());
|
||||
console.printf("update: installing by itself %s%s%s\n", settings.getBool(Setting::InstallUpdates) ? "on" : "off",
|
||||
autoUpdate.waiting() ? ", waiting for a quiet moment to offer " : autoUpdate.asking() ? ", asking about " : "",
|
||||
autoUpdate.waiting() || autoUpdate.asking() ? autoUpdate.tag().c_str() : "");
|
||||
console.printf("update: gitea %s %s\n", g.status() == GiteaReleases::Status::Done ? "done" : g.status() == GiteaReleases::Status::Failed ? "failed" : g.status() == GiteaReleases::Status::Busy ? "busy" : "never asked", g.error().c_str());
|
||||
printReleases(false);
|
||||
} else if (args.startsWith("install ")) {
|
||||
@@ -849,8 +859,57 @@ static void screenshotKey() {
|
||||
}
|
||||
|
||||
// Every key goes through here, from the keyboard or from a console's `key`.
|
||||
// Installing a release by itself (issue #52, Settings > System > Install updates): once the daily
|
||||
// check has found one, wait until nothing would be cut by a restart and no key was pressed for a
|
||||
// while, then ask on the screen, over whatever is open. Left unanswered for 30 s, it is installed.
|
||||
|
||||
// Would a restart now cut something short?
|
||||
static bool busyForRestart() {
|
||||
return gnssService->tracking() || loraCapture->capturing() || upload.active() || fileOps->busy() ||
|
||||
(scpService && scpService->busy()) || (sshService && sshService->active()) || (webShare && webShare->running()) ||
|
||||
(noiseTest && noiseTest->running()) || update->phase() != UpdateService::Phase::Idle || update->onProbation() ||
|
||||
update->giteaBusy();
|
||||
}
|
||||
|
||||
static void autoUpdateStep(uint32_t now) {
|
||||
static int shownSeconds = -1;
|
||||
if (!settings.getBool(Setting::InstallUpdates) && (autoUpdate.waiting() || autoUpdate.asking())) autoUpdate.withdraw();
|
||||
std::string tag;
|
||||
if (update->takeOffer(tag)) autoUpdate.offer(tag);
|
||||
if (autoDialog && autoDialog->result() != DialogModel::kPending) autoUpdate.answer(autoDialog->result() == 1);
|
||||
switch (autoUpdate.step(now, busyForRestart(), now - lastKeyMs)) {
|
||||
case release::AutoUpdate::Action::None: break;
|
||||
case release::AutoUpdate::Action::Ask:
|
||||
autoDialog.reset(new DialogModel({"Cancel", "Accept"}));
|
||||
power->onNotification(now, now + release::AutoUpdate::kAskMs); // a screen that is off lights up for it
|
||||
autoRedraw = true;
|
||||
break;
|
||||
case release::AutoUpdate::Action::Dismiss:
|
||||
autoDialog.reset();
|
||||
autoRedraw = true;
|
||||
break;
|
||||
case release::AutoUpdate::Action::Install: {
|
||||
autoDialog.reset();
|
||||
autoRedraw = true;
|
||||
std::string why = update->requestInstall(autoUpdate.tag(), true);
|
||||
if (!why.empty())
|
||||
bus.publish(Event::withText(EventType::Notification, ("Update not installed: " + why).c_str(), static_cast<int32_t>(NotificationLevel::Warning)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
int seconds = autoUpdate.asking() ? autoUpdate.secondsLeft(now) : -1;
|
||||
if (seconds != shownSeconds) {
|
||||
shownSeconds = seconds;
|
||||
autoRedraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void handleKey(const KeyEvent& e) {
|
||||
if (e.key == Key::Screenshot) return screenshotKey();
|
||||
if (autoDialog) { // the question takes every key until it is answered
|
||||
autoDialog->onKey(e);
|
||||
return;
|
||||
}
|
||||
apps->handleKey(e);
|
||||
}
|
||||
|
||||
@@ -1534,6 +1593,7 @@ static void loopPass() {
|
||||
printListingWhenReady();
|
||||
M5Cardputer.update();
|
||||
if (M5Cardputer.Keyboard.isChange()) {
|
||||
lastKeyMs = now;
|
||||
bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen
|
||||
keyMapper.setTextEntry(apps->foreground().textEntryActive());
|
||||
auto events = keyMapper.update(readKeys()); // always, so held keys are tracked
|
||||
@@ -1549,6 +1609,7 @@ static void loopPass() {
|
||||
bus.dispatch();
|
||||
notifier->update(now);
|
||||
apps->update(now);
|
||||
autoUpdateStep(now);
|
||||
|
||||
if (now - lastLog > 10000) {
|
||||
lastLog = now;
|
||||
@@ -1594,10 +1655,15 @@ static void loopPass() {
|
||||
screen.invalidate();
|
||||
redrawAfterUpdate = true;
|
||||
}
|
||||
if (apps->takeRedraw() || toastChanged || status != lastStatus || redrawAfterUpdate) {
|
||||
if (apps->takeRedraw() || toastChanged || status != lastStatus || redrawAfterUpdate || autoRedraw) {
|
||||
lastStatus = status;
|
||||
redrawAfterUpdate = false;
|
||||
screen.render(*apps, status, toast);
|
||||
autoRedraw = false;
|
||||
if (autoDialog) {
|
||||
Screen::Question question{"Update to " + autoUpdate.tag() + "?",
|
||||
"It installs in " + std::to_string(autoUpdate.secondsLeft(now)) + " s unless you cancel, and the device restarts.", autoDialog.get()};
|
||||
screen.render(*apps, status, toast, &question);
|
||||
} else screen.render(*apps, status, toast);
|
||||
update->firstFrameDrawn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,26 @@ void UpdateService::notify(const std::string& text, NotificationLevel level) {
|
||||
bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(level)));
|
||||
}
|
||||
|
||||
// A line in the record of updates on the card (issue #52): the firmware can change without
|
||||
// anyone looking, so what was installed and when is written down.
|
||||
void UpdateService::record(const std::string& what) {
|
||||
time_t now = time(nullptr);
|
||||
char when[24] = "undated";
|
||||
if (now >= kClockSetAfter) {
|
||||
struct tm t;
|
||||
gmtime_r(&now, &t);
|
||||
strftime(when, sizeof when, "%Y-%m-%d %H:%M UTC", &t);
|
||||
}
|
||||
storage_.appendLine(kRecordPath, std::string(when) + " " + what);
|
||||
}
|
||||
|
||||
bool UpdateService::takeOffer(std::string& tag) {
|
||||
if (!offer_) return false;
|
||||
offer_ = false;
|
||||
tag = offered_;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string UpdateService::incomingVersion() const { return incoming_; }
|
||||
|
||||
void UpdateService::start() {
|
||||
@@ -108,6 +128,8 @@ void UpdateService::start() {
|
||||
store_.getString("ota_from", from);
|
||||
if (!pending.empty() && pending != versionString()) {
|
||||
notify("Update to " + pending + " failed, back on " + versionString(), NotificationLevel::Warning);
|
||||
record(pending + " failed and was undone: back on " + versionString());
|
||||
store_.putString("ota_auto", "");
|
||||
store_.putString("ota_failed", pending); // not announced again until there's a newer one (Q168)
|
||||
store_.putString("ota_pending", "");
|
||||
}
|
||||
@@ -142,6 +164,14 @@ void UpdateService::tick(uint32_t nowMs) {
|
||||
store_.putString("ota_pending", "");
|
||||
store_.putInt("ota_attempts", 0);
|
||||
notify(std::string("Updated to ") + versionString(), NotificationLevel::Info);
|
||||
{
|
||||
std::string from, automatic;
|
||||
store_.getString("ota_from", from);
|
||||
store_.getString("ota_auto", automatic);
|
||||
record(std::string(versionString()) + " installed" + (automatic == versionString() ? " by itself" : "") +
|
||||
(from.empty() ? "" : ", from " + from));
|
||||
store_.putString("ota_auto", "");
|
||||
}
|
||||
break;
|
||||
case Probation::Verdict::RollBack:
|
||||
// ota_pending still names this version: the previous firmware will report the failure.
|
||||
@@ -215,12 +245,13 @@ std::string UpdateService::failedVersion() const {
|
||||
return failed;
|
||||
}
|
||||
|
||||
std::string UpdateService::requestInstall(const std::string& tag) {
|
||||
std::string UpdateService::requestInstall(const std::string& tag, bool automatic) {
|
||||
if (phase_ != Phase::Idle || giteaBusy()) return "Busy with something else";
|
||||
if (wifi_.state() != WifiController::State::Connected) return "No Wi-Fi";
|
||||
release::Release r;
|
||||
if (!gitea_.find(tag, r)) return "Look for releases first";
|
||||
if (!release::trustedAssetUrl(r.otaUrl)) return "That download isn't on the project's server";
|
||||
store_.putString("ota_auto", automatic ? tag : "");
|
||||
installTag_ = tag;
|
||||
request_ = Request::Install;
|
||||
return "";
|
||||
@@ -281,7 +312,14 @@ void UpdateService::serve() {
|
||||
time_t now = time(nullptr);
|
||||
if (now >= kClockSetAfter) store_.putInt("rel_day", static_cast<int32_t>(now / 86400));
|
||||
release::Release latest;
|
||||
if (r == Request::BackgroundCheck && gitea_.latest(latest) &&
|
||||
// Installing by itself (issue #52): offered at each daily check while it is the newest,
|
||||
// so a "not now" lasts a day. The main loop asks on the screen when the moment is quiet.
|
||||
if (r == Request::BackgroundCheck && gitea_.latest(latest) && settings_.getBool(Setting::InstallUpdates) &&
|
||||
release::shouldInstall(latest, runningVersion(), failedVersion())) {
|
||||
announced_ = latest.tag;
|
||||
offered_ = latest.tag;
|
||||
offer_ = true;
|
||||
} else if (r == Request::BackgroundCheck && gitea_.latest(latest) &&
|
||||
release::shouldAnnounce(latest, runningVersion(), failedVersion(), announced_)) {
|
||||
announced_ = latest.tag;
|
||||
notify(latest.tag + " is out: Settings > System > Firmware", NotificationLevel::Info); // 48 bytes at most
|
||||
|
||||
@@ -51,7 +51,12 @@ class UpdateService : public Service {
|
||||
void requestList() { request_ = Request::List; }
|
||||
// Downloads `tag` (a release known from the last check or list) into the inactive slot and
|
||||
// installs it. "" when it started, or why not.
|
||||
std::string requestInstall(const std::string& tag);
|
||||
// `automatic`: nobody asked for it (issue #52), and the record of updates says so.
|
||||
std::string requestInstall(const std::string& tag, bool automatic = false);
|
||||
// A release the daily check found that may be installed without being asked for (the setting
|
||||
// is on): true once for each find, with its tag. The main loop picks the moment.
|
||||
bool takeOffer(std::string& tag);
|
||||
static constexpr const char* kRecordPath = "/system/updates.log"; // what was installed, and when
|
||||
bool giteaBusy() const { return request_ != Request::None || serving_; }
|
||||
// Asked to make room for a TLS connection (R1, Q172): IRC steps aside while the Update Service
|
||||
// talks to Gitea. Set by the main loop; `true` to step aside, `false` to come back.
|
||||
@@ -85,6 +90,7 @@ class UpdateService : public Service {
|
||||
void listen();
|
||||
void install(class UpdateSource& source, const char* via);
|
||||
void notify(const std::string& text, NotificationLevel level);
|
||||
void record(const std::string& what);
|
||||
|
||||
KeyValueStore& store_;
|
||||
WifiService& wifi_;
|
||||
@@ -103,7 +109,8 @@ class UpdateService : public Service {
|
||||
volatile Request request_ = Request::None;
|
||||
volatile bool serving_ = false;
|
||||
bool held_ = false; // IRC was asked to step aside, on this task
|
||||
std::string installTag_, fakeVersion_, announced_;
|
||||
std::string installTag_, fakeVersion_, announced_, offered_;
|
||||
volatile bool offer_ = false;
|
||||
bool dailyCheck_ = false;
|
||||
uint32_t nextCheckMs_ = 90000; // not before the device has settled
|
||||
std::string probeHost_, probePath_;
|
||||
|
||||
+3
-2
@@ -12,12 +12,12 @@ bool Screen::begin() {
|
||||
return canvas_.createSprite(theme::kWidth, theme::kHeight) != nullptr;
|
||||
}
|
||||
|
||||
void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toast) {
|
||||
void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toast, const Question* question) {
|
||||
const auto& area = theme::kContent;
|
||||
App& app = apps.foreground();
|
||||
// An App that keeps what it drew: only the Status Bar is cleared, unless the App has changed
|
||||
// or a Toast or the help panel, drawn over it, has just gone.
|
||||
bool overlay = apps.help().isOpen() || toast, retains = app.retainsContent();
|
||||
bool overlay = apps.help().isOpen() || toast || question, retains = app.retainsContent();
|
||||
if (overlayWas_ && !overlay) lost_ = true;
|
||||
if (retains && &app == lastApp_ && !lost_) {
|
||||
canvas_.fillRect(0, 0, theme::kWidth, area.y, theme::kBackground);
|
||||
@@ -31,6 +31,7 @@ void Screen::render(AppManager& apps, const StatusInfo& status, const Toast* toa
|
||||
canvas_.setClipRect(area.x, area.y, area.w, area.h);
|
||||
app.draw(canvas_);
|
||||
if (apps.help().isOpen()) widgets::help(canvas_, apps.help()); // over the App, under the Status Bar
|
||||
if (question) widgets::dialog(canvas_, question->title, question->message, *question->dialog);
|
||||
canvas_.clearClipRect();
|
||||
widgets::statusBar(canvas_, status);
|
||||
if (toast) widgets::toast(canvas_, *toast);
|
||||
|
||||
+6
-1
@@ -10,7 +10,12 @@ namespace roro {
|
||||
class Screen {
|
||||
public:
|
||||
bool begin(); // allocates the frame buffer (~32 KB at 8-bit colour)
|
||||
void render(AppManager& apps, const StatusInfo& status, const Toast* toast);
|
||||
// A question the device itself asks, over whatever App is open (issue #52).
|
||||
struct Question {
|
||||
std::string title, message;
|
||||
const DialogModel* dialog;
|
||||
};
|
||||
void render(AppManager& apps, const StatusInfo& status, const Toast* toast, const Question* question = nullptr);
|
||||
// Takes over the screen while a Firmware Update is received or about to restart.
|
||||
void renderUpdate(const std::string& title, const std::string& detail, int percent);
|
||||
Canvas& canvas() { return canvas_; }
|
||||
|
||||
Reference in New Issue
Block a user