diff --git a/src/config/nl_state_log.c b/src/config/nl_state_log.c new file mode 100644 index 0000000..9b24fff --- /dev/null +++ b/src/config/nl_state_log.c @@ -0,0 +1,152 @@ +#include "config/nl_state_log.h" + +#include + +#include "light/nl_boot_state.h" +#include "util/nl_crc.h" + +#define ENTRY_MAGIC 0xA5u + +static uint32_t s_sector[2]; +static bool s_have; +static nl_light_state_t s_latest; +static uint32_t s_seq; +static uint32_t s_active; +static uint32_t s_next; /* next free slot in the active sector */ + +static uint32_t slot_addr(uint32_t sec, uint32_t slot) +{ + return s_sector[sec] + slot * NL_STATE_ENTRY_SIZE; +} + +static void encode(const nl_light_state_t *s, uint32_t seq, uint8_t e[NL_STATE_ENTRY_SIZE]) +{ + memset(e, 0, NL_STATE_ENTRY_SIZE); + e[0] = ENTRY_MAGIC; + e[1] = (uint8_t)((s->on ? 1u : 0u) | (s->mode == NL_MODE_WHITE ? 2u : 0u)); + e[2] = s->brightness; + e[3] = s->r; + e[4] = s->g; + e[5] = s->b; + e[6] = (uint8_t)(s->ct & 0xFFu); + e[7] = (uint8_t)(s->ct >> 8); + e[8] = (uint8_t)seq; + e[9] = (uint8_t)(seq >> 8); + e[10] = (uint8_t)(seq >> 16); + e[11] = (uint8_t)(seq >> 24); + e[15] = nl_crc8(e, NL_STATE_ENTRY_SIZE - 1); +} + +static bool decode(const uint8_t e[NL_STATE_ENTRY_SIZE], nl_light_state_t *s, uint32_t *seq) +{ + if (e[0] != ENTRY_MAGIC || nl_crc8(e, NL_STATE_ENTRY_SIZE - 1) != e[15]) + return false; + s->on = (e[1] & 1u) != 0; + s->mode = (e[1] & 2u) ? NL_MODE_WHITE : NL_MODE_COLOR; + s->brightness = e[2]; + s->r = e[3]; + s->g = e[4]; + s->b = e[5]; + s->ct = (uint16_t)(e[6] | (e[7] << 8)); + *seq = (uint32_t)e[8] | ((uint32_t)e[9] << 8) | ((uint32_t)e[10] << 16) | ((uint32_t)e[11] << 24); + return true; +} + +static bool is_free(const uint8_t e[NL_STATE_ENTRY_SIZE]) +{ + for (uint32_t i = 0; i < NL_STATE_ENTRY_SIZE; i++) + if (e[i] != 0xFF) + return false; + return true; +} + +static uint32_t next_free_slot(uint32_t sec) +{ + uint8_t e[NL_STATE_ENTRY_SIZE]; + uint32_t next = 0; + for (uint32_t slot = 0; slot < NL_STATE_LOG_SLOTS; slot++) { + if (nl_flash_read(slot_addr(sec, slot), e, sizeof e) != 0 || !is_free(e)) + next = slot + 1; + } + return next; +} + +void nl_state_log_init(uint32_t sector0, uint32_t sector1) +{ + uint8_t e[NL_STATE_ENTRY_SIZE]; + nl_light_state_t st; + uint32_t seq; + + s_sector[0] = sector0; + s_sector[1] = sector1; + s_have = false; + s_seq = 0; + s_active = 0; + for (uint32_t sec = 0; sec < 2; sec++) { + for (uint32_t slot = 0; slot < NL_STATE_LOG_SLOTS; slot++) { + if (nl_flash_read(slot_addr(sec, slot), e, sizeof e) != 0) + continue; + if (decode(e, &st, &seq) && (!s_have || seq > s_seq)) { + s_have = true; + s_seq = seq; + s_latest = st; + s_active = sec; + } + } + } + s_next = next_free_slot(s_active); +} + +bool nl_state_log_load(nl_light_state_t *out) +{ + if (!s_have) + return false; + *out = s_latest; + return true; +} + +int nl_state_log_append(const nl_light_state_t *s) +{ + uint8_t e[NL_STATE_ENTRY_SIZE]; + uint32_t sec = s_active; + int r; + + if (s_have && nl_light_state_equal(&s_latest, s)) + return 0; + + if (!s_have) { + sec = 0; + if (s_active != 0 || s_next != 0) { + if (nl_flash_erase_sector(s_sector[0]) != 0) + return -1; + } + s_next = 0; + } else if (s_next >= NL_STATE_LOG_SLOTS) { + sec = 1u - s_active; + if (nl_flash_erase_sector(s_sector[sec]) != 0) + return -1; + s_next = 0; + } + + encode(s, s_seq + 1, e); + r = nl_flash_write(slot_addr(sec, s_next), e, sizeof e); + s_active = sec; + s_next++; /* also skip a half written slot */ + if (r != 0) + return -1; + s_seq++; + s_latest = *s; + s_have = true; + return 0; +} + +int nl_state_log_erase(void) +{ + int r0 = nl_flash_erase_sector(s_sector[0]); + int r1 = nl_flash_erase_sector(s_sector[1]); + s_have = false; + s_seq = 0; + s_active = 0; + s_next = 0; + return (r0 == 0 && r1 == 0) ? 0 : -1; +} diff --git a/src/config/nl_state_log.h b/src/config/nl_state_log.h new file mode 100644 index 0000000..ac2b75d --- /dev/null +++ b/src/config/nl_state_log.h @@ -0,0 +1,18 @@ +#ifndef NL_STATE_LOG_H +#define NL_STATE_LOG_H + +#include +#include + +#include "config/nl_flash.h" +#include "light/nl_light_types.h" + +#define NL_STATE_ENTRY_SIZE 16u +#define NL_STATE_LOG_SLOTS (NL_FLASH_SECTOR_SIZE / NL_STATE_ENTRY_SIZE) + +void nl_state_log_init(uint32_t sector0, uint32_t sector1); +bool nl_state_log_load(nl_light_state_t *out); +int nl_state_log_append(const nl_light_state_t *s); +int nl_state_log_erase(void); + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a580d77..a377612 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -31,3 +31,5 @@ nl_test(test_boot_state test_boot_state.c ${NL_SRC}/light/nl_boot_state.c ${NL_S nl_test(test_control test_control.c ${NL_SRC}/light/nl_control.c ${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/light/nl_engine.c ${NL_SRC}/light/nl_color.c ${NL_SRC}/board/nl_board.c) nl_test(test_settings test_settings.c fake_flash.c ${NL_SRC}/config/nl_settings.c ${NL_SRC}/util/nl_crc.c) +nl_test(test_state_log test_state_log.c fake_flash.c ${NL_SRC}/config/nl_state_log.c + ${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/util/nl_crc.c) diff --git a/test/test_state_log.c b/test/test_state_log.c new file mode 100644 index 0000000..534e6f2 --- /dev/null +++ b/test/test_state_log.c @@ -0,0 +1,141 @@ +#include +#include "unity.h" +#include "fake_flash.h" +#include "config/nl_flash_layout.h" +#include "config/nl_state_log.h" +#include "light/nl_boot_state.h" + +static nl_light_state_t st(uint8_t bri) +{ + nl_light_state_t s = {true, bri, NL_MODE_COLOR, 10, 20, 30, 3000}; + return s; +} + +static void reboot(void) +{ + nl_state_log_init(NL_FL_STATE_0, NL_FL_STATE_1); +} + +void setUp(void) +{ + fake_flash_reset(); + reboot(); +} + +void tearDown(void) +{ + TEST_ASSERT_EQUAL_UINT(0, fake_flash_dirty_writes); +} + +static void test_empty(void) +{ + nl_light_state_t s; + TEST_ASSERT_FALSE(nl_state_log_load(&s)); +} + +static void test_append_and_reload(void) +{ + nl_light_state_t in = st(42), out; + in.mode = NL_MODE_WHITE; + in.on = false; + TEST_ASSERT_EQUAL_INT(0, nl_state_log_append(&in)); + reboot(); + TEST_ASSERT_TRUE(nl_state_log_load(&out)); + TEST_ASSERT_TRUE(nl_light_state_equal(&in, &out)); +} + +static void test_equal_state_is_not_written_again(void) +{ + nl_light_state_t s = st(42); + nl_state_log_append(&s); + nl_state_log_append(&s); + for (int i = 0; i < 16; i++) + TEST_ASSERT_EQUAL_HEX8(0xFF, fake_flash_mem[NL_FL_STATE_0 + 16 + i]); +} + +static void test_wraps_into_second_sector(void) +{ + nl_light_state_t s, out; + for (int i = 0; i < 300; i++) { + s = st((uint8_t)(1 + i % 100)); + s.r = (uint8_t)i; /* make every entry distinct */ + TEST_ASSERT_EQUAL_INT(0, nl_state_log_append(&s)); + } + TEST_ASSERT_EQUAL_UINT(1, fake_flash_erases[NL_FL_STATE_1 / NL_FLASH_SECTOR_SIZE]); + reboot(); + TEST_ASSERT_TRUE(nl_state_log_load(&out)); + TEST_ASSERT_TRUE(nl_light_state_equal(&s, &out)); + /* keep going after the reboot */ + s = st(77); + TEST_ASSERT_EQUAL_INT(0, nl_state_log_append(&s)); + reboot(); + TEST_ASSERT_TRUE(nl_state_log_load(&out)); + TEST_ASSERT_EQUAL_UINT8(77, out.brightness); +} + +static void test_wraps_back_to_first_sector(void) +{ + nl_light_state_t s, out; + for (int i = 0; i < 600; i++) { + s = st(50); + s.r = (uint8_t)i; + s.g = (uint8_t)(i >> 8); + TEST_ASSERT_EQUAL_INT(0, nl_state_log_append(&s)); + } + TEST_ASSERT_EQUAL_UINT(1, fake_flash_erases[NL_FL_STATE_0 / NL_FLASH_SECTOR_SIZE]); + reboot(); + TEST_ASSERT_TRUE(nl_state_log_load(&out)); + TEST_ASSERT_TRUE(nl_light_state_equal(&s, &out)); +} + +static void test_power_loss_during_append(void) +{ + nl_light_state_t a = st(10), b = st(20), c = st(30), out; + nl_state_log_append(&a); + fake_flash_fail_after(5); + TEST_ASSERT_NOT_EQUAL(0, nl_state_log_append(&b)); + fake_flash_fail_after(-1); + reboot(); + TEST_ASSERT_TRUE(nl_state_log_load(&out)); + TEST_ASSERT_EQUAL_UINT8(10, out.brightness); + TEST_ASSERT_EQUAL_INT(0, nl_state_log_append(&c)); + reboot(); + TEST_ASSERT_TRUE(nl_state_log_load(&out)); + TEST_ASSERT_EQUAL_UINT8(30, out.brightness); +} + +static void test_garbage_in_empty_log_is_erased_first(void) +{ + nl_light_state_t s = st(60), out; + fake_flash_mem[NL_FL_STATE_0 + 3] = 0x12; /* leftovers from the old firmware */ + reboot(); + TEST_ASSERT_FALSE(nl_state_log_load(&out)); + TEST_ASSERT_EQUAL_INT(0, nl_state_log_append(&s)); + reboot(); + TEST_ASSERT_TRUE(nl_state_log_load(&out)); + TEST_ASSERT_EQUAL_UINT8(60, out.brightness); +} + +static void test_erase(void) +{ + nl_light_state_t s = st(60), out; + nl_state_log_append(&s); + TEST_ASSERT_EQUAL_INT(0, nl_state_log_erase()); + TEST_ASSERT_FALSE(nl_state_log_load(&out)); + reboot(); + TEST_ASSERT_FALSE(nl_state_log_load(&out)); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_empty); + RUN_TEST(test_append_and_reload); + RUN_TEST(test_equal_state_is_not_written_again); + RUN_TEST(test_wraps_into_second_sector); + RUN_TEST(test_wraps_back_to_first_sector); + RUN_TEST(test_power_loss_during_append); + RUN_TEST(test_garbage_in_empty_log_is_erased_first); + RUN_TEST(test_erase); + return UNITY_END(); +}