OTA: the firmware rolls itself back; the bootloader doesn't

A deliberately crashing update looped forever on the device: the
prebuilt bootloader ignores ESP_OTA_IMG_PENDING_VERIFY despite the
app-side rollback config. UpdateService::bootGuard() now runs first in
setup(): it counts starts on Probation in NVS and, on the second
unconfirmed start, marks the image invalid and reboots into the
previous one. Confirming (or the Wi-Fi rollback) resets the counter.
ADR 0003 records the limit: a crash in the first milliseconds still
needs USB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-03 21:48:39 +02:00
co-authored by Claude Opus 5.5
parent de5931210f
commit 45542dcbff
6 changed files with 34 additions and 1 deletions
+3 -1
View File
@@ -91,12 +91,14 @@ static StatusInfo currentStatus() {
}
void setup() {
nvs.begin();
UpdateService::bootGuard(nvs); // first: before anything that could crash on new firmware
auto cfg = M5.config();
M5Cardputer.begin(cfg, true);
M5Cardputer.Display.setRotation(1);
Serial.begin(115200);
nvs.begin();
settings.load();
battery = new BatteryService(bus);
+15
View File
@@ -91,6 +91,19 @@ void UpdateService::start() {
if (!task_) xTaskCreate(taskEntry, "update", 8192, this, 1, &task_);
}
void UpdateService::bootGuard(KeyValueStore& store) {
esp_ota_img_states_t state;
bool probation = esp_ota_get_state_partition(esp_ota_get_running_partition(), &state) == ESP_OK &&
state == ESP_OTA_IMG_PENDING_VERIFY;
int32_t attempts = 0;
store.getInt("ota_attempts", attempts);
if (Probation::rollBackAtBoot(probation, attempts)) {
store.putInt("ota_attempts", 0);
esp_ota_mark_app_invalid_rollback_and_reboot(); // does not return when there's a previous image
}
store.putInt("ota_attempts", probation ? attempts + 1 : 0);
}
void UpdateService::tick(uint32_t nowMs) {
if (!probation_) return;
bool wifiConfigured = settings_.getBool(Setting::WifiEnabled) && saved_.count() > 0;
@@ -101,10 +114,12 @@ void UpdateService::tick(uint32_t nowMs) {
esp_ota_mark_app_valid_cancel_rollback();
probation_ = false;
store_.putString("ota_pending", "");
store_.putInt("ota_attempts", 0);
notify(std::string("Updated to ") + versionString(), NotificationLevel::Info);
break;
case Probation::Verdict::RollBack:
// ota_pending still names this version: the previous firmware will report the failure.
store_.putInt("ota_attempts", 0);
delay(200);
esp_ota_mark_app_invalid_rollback_and_reboot();
break;
+3
View File
@@ -20,6 +20,9 @@ class UpdateService : public Service {
enum class Phase { Idle, Receiving, Installed, Failed };
static constexpr uint16_t kPort = 3232;
// Call first in setup(): rolls back new firmware that already died once on Probation.
static void bootGuard(KeyValueStore& store);
UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage,
EventBus& bus, const Settings& settings);
const char* name() const override { return "update"; }