Keep ESP32 OTA reachable whatever the firmware does

- Rollback: override verifyRollbackLater() so a new OTA image stays
  pending until it ran 60 s with WiFi; any earlier restart rolls back
  (the Arduino core marked every image valid at boot before)
- Task watchdog (30 s) on loop(), paused during OTA uploads
- Crash counter in RTC memory: OTA-only window after a crash, 10 min
  safe mode (WiFi + OTA only) after 3 crashes in a row
- Restart when WiFi stays down for 10 min
- Home Assistant sensor 'FlipDot Display Firmware' with version/state
- logic/boot_policy with tests (60 tests total)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-13 01:05:06 +02:00
1 parent 683a47c055
commit ddbdf7e65f
14 files changed
+517 -42

No files matched your search

+86
View File
@@ -11,6 +11,7 @@
#include <vector>
#include "config.h"
#include "logic/boot_policy.h"
#include "logic/display_controller.h"
#include "logic/power_timer.h"
#include "logic/schedule.h"
@@ -171,6 +172,81 @@ void test_timer_across_millis_overflow()
TEST_ASSERT_TRUE(t.expired(0x00000100u));
}
// ---------------------------------------------------------------------------
// Boot policy (keeping OTA alive)
// ---------------------------------------------------------------------------
void test_power_on_starts_normally_and_clears_crashes()
{
BootDecision d = decideBoot(ResetKind::PowerOn, 2);
TEST_ASSERT_TRUE(d.mode == BootMode::Normal);
TEST_ASSERT_EQUAL(0, d.crash_count);
}
void test_software_reset_starts_normally_and_keeps_count()
{
// e.g. the restart after an OTA update
BootDecision d = decideBoot(ResetKind::Software, 1);
TEST_ASSERT_TRUE(d.mode == BootMode::Normal);
TEST_ASSERT_EQUAL(1, d.crash_count);
}
void test_other_reset_starts_normally_and_keeps_count()
{
BootDecision d = decideBoot(ResetKind::Other, 2);
TEST_ASSERT_TRUE(d.mode == BootMode::Normal);
TEST_ASSERT_EQUAL(2, d.crash_count);
}
void test_first_crashes_give_a_panic_window()
{
BootDecision d = decideBoot(ResetKind::Crash, 0);
TEST_ASSERT_TRUE(d.mode == BootMode::PanicWindow);
TEST_ASSERT_EQUAL(1, d.crash_count);
d = decideBoot(ResetKind::Crash, d.crash_count);
TEST_ASSERT_TRUE(d.mode == BootMode::PanicWindow);
TEST_ASSERT_EQUAL(2, d.crash_count);
}
void test_repeated_crashes_give_safe_mode()
{
uint8_t count = 0;
BootDecision d = {BootMode::Normal, 0};
for (int i = 0; i < SAFE_MODE_CRASHES; i++)
{
d = decideBoot(ResetKind::Crash, count);
count = d.crash_count;
}
TEST_ASSERT_TRUE(d.mode == BootMode::SafeMode);
}
void test_after_safe_mode_one_more_crash_gives_safe_mode_again()
{
uint8_t count = crashCountAfterSafeMode();
TEST_ASSERT_TRUE(decideBoot(ResetKind::Software, count).mode == BootMode::Normal);
TEST_ASSERT_TRUE(decideBoot(ResetKind::Crash, count).mode == BootMode::SafeMode);
}
void test_crash_count_does_not_overflow()
{
BootDecision d = decideBoot(ResetKind::Crash, 255);
TEST_ASSERT_EQUAL(255, d.crash_count);
TEST_ASSERT_TRUE(d.mode == BootMode::SafeMode);
}
void test_new_firmware_needs_time_and_wifi_to_be_accepted()
{
TEST_ASSERT_FALSE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME - 1, true));
TEST_ASSERT_FALSE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME, false));
TEST_ASSERT_TRUE(shouldMarkFirmwareValid(FIRMWARE_VALID_TIME, true));
}
void test_crash_count_cleared_after_running_long_enough()
{
TEST_ASSERT_FALSE(shouldClearCrashCount(CRASH_FREE_TIME - 1));
TEST_ASSERT_TRUE(shouldClearCrashCount(CRASH_FREE_TIME));
}
// ---------------------------------------------------------------------------
// Status LED
// ---------------------------------------------------------------------------
@@ -348,6 +424,16 @@ int main()
RUN_TEST(test_timer_cancel);
RUN_TEST(test_timer_across_millis_overflow);
RUN_TEST(test_power_on_starts_normally_and_clears_crashes);
RUN_TEST(test_software_reset_starts_normally_and_keeps_count);
RUN_TEST(test_other_reset_starts_normally_and_keeps_count);
RUN_TEST(test_first_crashes_give_a_panic_window);
RUN_TEST(test_repeated_crashes_give_safe_mode);
RUN_TEST(test_after_safe_mode_one_more_crash_gives_safe_mode_again);
RUN_TEST(test_crash_count_does_not_overflow);
RUN_TEST(test_new_firmware_needs_time_and_wifi_to_be_accepted);
RUN_TEST(test_crash_count_cleared_after_running_long_enough);
RUN_TEST(test_led_off_while_display_off);
RUN_TEST(test_led_blinks_slow_with_wifi_and_fast_without);