diff --git a/firmware/lib/event_log/event_log.cpp b/firmware/lib/event_log/event_log.cpp index 3766935..95fd630 100644 --- a/firmware/lib/event_log/event_log.cpp +++ b/firmware/lib/event_log/event_log.cpp @@ -27,6 +27,11 @@ g_head = 0; g_cnt = 0; } + extern "C" void event_log_test_simulate_reboot() { + // Simulate device reboot: clear in-RAM state, keep persistent slots. + g_head = 0; + g_cnt = 0; + } #endif static const size_t SLOTS = 32; @@ -69,6 +74,7 @@ void event_log_init() { Serial.println("[evlog] NVS begin failed"); return; } +#endif // Scan all 32 slots; locate the one with the largest seq. // Empty log: every slot tag == 0 (not a valid EventLogTag, which starts at 1). uint32_t max_seq = 0; @@ -91,9 +97,6 @@ void event_log_init() { g_head = 0; g_cnt = 0; } -#else - // nothing -#endif } void event_log_write(EventLogTag tag, uint16_t data0, uint16_t data1) { diff --git a/firmware/lib/event_log/event_log.h b/firmware/lib/event_log/event_log.h index d4ff60f..0782743 100644 --- a/firmware/lib/event_log/event_log.h +++ b/firmware/lib/event_log/event_log.h @@ -33,6 +33,7 @@ struct EventLogEntry { static_assert(sizeof(EventLogEntry) == 32, "EventLogEntry must be 32 bytes"); // NVS-backed 32-slot ring buffer. Safe to call before NTP sync. +// Call exactly once from application setup, before any task writes events. void event_log_init(); void event_log_write(EventLogTag tag, uint16_t data0 = 0, uint16_t data1 = 0); size_t event_log_read_recent(EventLogEntry* out, size_t max_entries); diff --git a/firmware/test/test_event_log/test_event_log.cpp b/firmware/test/test_event_log/test_event_log.cpp index aceb47f..5742b2e 100644 --- a/firmware/test/test_event_log/test_event_log.cpp +++ b/firmware/test/test_event_log/test_event_log.cpp @@ -74,6 +74,58 @@ void test_path_hash_distinguishes_real_api_paths() { TEST_ASSERT_NOT_EQUAL(h2, h3); } +extern "C" void event_log_test_simulate_reboot(); + +void test_boot_recovery_after_partial_fill() { + // Phase 1: write 5 entries before "reboot" + event_log_init(); + for (uint16_t i = 0; i < 5; i++) event_log_write(EVT_HTTP_OK, i, 0); + + // Phase 2: simulate reboot (clear RAM state, keep slots), re-init, verify + event_log_test_simulate_reboot(); + event_log_init(); + + // All 5 original entries should still be readable, newest first + EventLogEntry buf[8]; + size_t n = event_log_read_recent(buf, 8); + TEST_ASSERT_EQUAL(5, n); + TEST_ASSERT_EQUAL(4, buf[0].data0); // newest + TEST_ASSERT_EQUAL(0, buf[4].data0); // oldest + + // Phase 3: write one more — seq must continue (not restart at 0), + // so the new entry is the newest and slot index 5 holds it + event_log_write(EVT_HTTP_OK, 99, 0); + n = event_log_read_recent(buf, 8); + TEST_ASSERT_EQUAL(6, n); + TEST_ASSERT_EQUAL(99, buf[0].data0); + TEST_ASSERT_EQUAL(4, buf[1].data0); +} + +void test_boot_recovery_after_wrap() { + // Phase 1: write 40 entries (wraps the 32-slot ring once; oldest 8 dropped) + event_log_init(); + for (uint16_t i = 0; i < 40; i++) event_log_write(EVT_HTTP_OK, i, 0); + + // Phase 2: simulate reboot, re-init + event_log_test_simulate_reboot(); + event_log_init(); + + // Still 32 entries visible, newest=39, oldest=8 + EventLogEntry buf[32]; + size_t n = event_log_read_recent(buf, 32); + TEST_ASSERT_EQUAL(32, n); + TEST_ASSERT_EQUAL(39, buf[0].data0); + TEST_ASSERT_EQUAL(8, buf[31].data0); + + // Phase 3: one more write — newest becomes 100, head advances past + // wherever the max-seq slot was, oldest drops to data0=9 + event_log_write(EVT_HTTP_OK, 100, 0); + n = event_log_read_recent(buf, 32); + TEST_ASSERT_EQUAL(32, n); + TEST_ASSERT_EQUAL(100, buf[0].data0); + TEST_ASSERT_EQUAL(9, buf[31].data0); +} + int main() { UNITY_BEGIN(); RUN_TEST(test_entry_is_32_bytes); @@ -83,5 +135,7 @@ int main() { RUN_TEST(test_empty_log_read_returns_zero); RUN_TEST(test_read_recent_truncates_to_max_entries); RUN_TEST(test_path_hash_distinguishes_real_api_paths); + RUN_TEST(test_boot_recovery_after_partial_fill); + RUN_TEST(test_boot_recovery_after_wrap); return UNITY_END(); }