Updates: a newer release installs by itself, after a question
CI / build (pull_request) Successful in 2m8s
Site / build (pull_request) Successful in 11s

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:
2026-10-11 17:49:09 +02:00
co-authored by Claude Opus 5.5
parent 8ba0102910
commit c3fd9909a1
13 changed files with 211 additions and 15 deletions
+69 -3
View File
@@ -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();
}
}