feat: Ring-Log für den Lichtzustand

This commit is contained in:
Michael Sauer
2026-09-11 13:30:26 +02:00
parent 91721ac47d
commit b1b4ec214b
4 changed files with 313 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
#include "config/nl_state_log.h"
#include <string.h>
#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;
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef NL_STATE_LOG_H
#define NL_STATE_LOG_H
#include <stdbool.h>
#include <stdint.h>
#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
+2
View File
@@ -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)
+141
View File
@@ -0,0 +1,141 @@
#include <string.h>
#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();
}