From 1822014e48d19c750d897c4837df2822b38aacc4 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Sat, 12 Sep 2026 03:13:36 +0200 Subject: [PATCH] feat: Licht nach einem OTA wie bei power_on_behaviour=last wiederherstellen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec 8.2 verlangt, dass ein durch ein Update ausgelöster Neustart nicht wie ein Stromausfall wirkt: Das Flag ota_pending wurde bisher nur geschrieben und beim nächsten Boot wieder gelöscht, nl_boot_state() wendet aber weiterhin das normale power_on_behaviour an. Eine eingeschaltete Lampe mit pob=off kam nach einem Update aus zurück, mit pob=on umgekehrt. nl_boot_state() bekommt dafür ein Argument after_ota; ist ein gespeicherter Zustand vorhanden, wird er unverändert übernommen. Ohne gespeicherten Zustand gelten weiterhin die normalen Regeln. Hosttests: vier neue Fälle in test_boot_state.c (pob=off/on über ein OTA, Sanitisierung bleibt aktiv, kein gespeicherter Zustand). --- src/app_main.c | 15 ++++++----- src/light/nl_boot_state.c | 9 ++++++- src/light/nl_boot_state.h | 8 ++++-- test/test_boot_state.c | 53 +++++++++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/src/app_main.c b/src/app_main.c index e17b9e8..8b1bee2 100644 --- a/src/app_main.c +++ b/src/app_main.c @@ -149,7 +149,7 @@ static void app_task(void *arg) uint32_t base = nl_flash_region_base(); nl_engine_cfg_t cfg; nl_light_state_t last, initial, pending; - bool have_last, counter_cleared = false; + bool have_last, after_ota, counter_cleared = false; bool light_alive = false, net_alive = false, http_alive = false; uint8_t boots; uint32_t boot_ms, light_seen, net_seen, http_seen; @@ -165,10 +165,12 @@ static void app_task(void *arg) nl_state_log_init(base + NL_FL_STATE_0, base + NL_FL_STATE_1); have_last = nl_state_log_load(&last); store_unlock(); - if (s_settings.ota_pending) { - /* We came back from an update attempt (accepted or rejected). Just clear the flag so it - * doesn't stick in flash forever; nl_boot_state() below restores the light from `last`/ - * power_on_behaviour as on any other boot, no special-casing needed here. */ + after_ota = s_settings.ota_pending != 0; + if (after_ota) { + /* We came back from an update attempt (accepted or rejected). Clear the flag so it does + * not stick in flash forever; nl_boot_state() below uses `after_ota` to restore the light + * effectively as power_on_behaviour = last (spec 8.2: a reboot caused by an update is not + * a power cut). */ s_settings.ota_pending = 0; printf("boot after ota attempt\r\n"); store_lock(); @@ -183,7 +185,8 @@ static void app_task(void *arg) cfg.ct_max = board->ct_max; cfg.output = led_out; cfg.ctx = NULL; - initial = nl_boot_state((nl_pob_t)s_settings.power_on_behaviour, have_last ? &last : NULL, board); + initial = nl_boot_state((nl_pob_t)s_settings.power_on_behaviour, have_last ? &last : NULL, board, + after_ota); nl_app_lock(); nl_engine_init(&cfg); nl_ctrl_init(board, s_settings.fade_in_ms, s_settings.fade_out_ms); diff --git a/src/light/nl_boot_state.c b/src/light/nl_boot_state.c index e9792d5..1141cf9 100644 --- a/src/light/nl_boot_state.c +++ b/src/light/nl_boot_state.c @@ -20,7 +20,8 @@ nl_light_state_t nl_boot_default_state(const nl_board_t *b) return s; } -nl_light_state_t nl_boot_state(nl_pob_t pob, const nl_light_state_t *last, const nl_board_t *b) +nl_light_state_t nl_boot_state(nl_pob_t pob, const nl_light_state_t *last, const nl_board_t *b, + bool after_ota) { nl_light_state_t s = last ? *last : nl_boot_default_state(b); @@ -30,6 +31,12 @@ nl_light_state_t nl_boot_state(nl_pob_t pob, const nl_light_state_t *last, const s.mode = NL_MODE_WHITE; s.ct = clamp_ct(s.ct, b); + /* Spec 8.2: after an OTA the light is restored effectively as power_on_behaviour = last - + * a reboot triggered by an update must not look like a power cut to the user. Without a + * stored state there is nothing to restore, so the normal rules apply. */ + if (after_ota && last) + return s; + if (pob == NL_POB_ON) s.on = true; else if (pob == NL_POB_OFF) diff --git a/src/light/nl_boot_state.h b/src/light/nl_boot_state.h index b7686fa..6e89019 100644 --- a/src/light/nl_boot_state.h +++ b/src/light/nl_boot_state.h @@ -9,8 +9,12 @@ /* Warm white, 100 %, on. */ nl_light_state_t nl_boot_default_state(const nl_board_t *b); -/* State to apply after power-up. last == NULL when nothing is stored. Stored values are sanitised. */ -nl_light_state_t nl_boot_state(nl_pob_t pob, const nl_light_state_t *last, const nl_board_t *b); +/* State to apply after power-up. last == NULL when nothing is stored. Stored values are sanitised. + * after_ota: this boot follows a firmware update (settings flag ota_pending). Spec 8.2 - such a + * reboot is not a power cut, so the light comes back effectively as power_on_behaviour = last, + * whatever pob says. */ +nl_light_state_t nl_boot_state(nl_pob_t pob, const nl_light_state_t *last, const nl_board_t *b, + bool after_ota); bool nl_light_state_equal(const nl_light_state_t *a, const nl_light_state_t *b); diff --git a/test/test_boot_state.c b/test/test_boot_state.c index df7ba2b..c46db57 100644 --- a/test/test_boot_state.c +++ b/test/test_boot_state.c @@ -23,14 +23,14 @@ static void test_default_state(void) static void test_last_restores_everything(void) { nl_light_state_t last = red_off(); - nl_light_state_t s = nl_boot_state(NL_POB_LAST, &last, nl_board_find("evk")); + nl_light_state_t s = nl_boot_state(NL_POB_LAST, &last, nl_board_find("evk"), false); TEST_ASSERT_TRUE(nl_light_state_equal(&last, &s)); } static void test_last_without_saved_state_uses_default(void) { const nl_board_t *b = nl_board_find("evk"); - nl_light_state_t s = nl_boot_state(NL_POB_LAST, NULL, b); + nl_light_state_t s = nl_boot_state(NL_POB_LAST, NULL, b, false); nl_light_state_t d = nl_boot_default_state(b); TEST_ASSERT_TRUE(nl_light_state_equal(&d, &s)); } @@ -38,7 +38,7 @@ static void test_last_without_saved_state_uses_default(void) static void test_on_forces_on_with_last_values(void) { nl_light_state_t last = red_off(); - nl_light_state_t s = nl_boot_state(NL_POB_ON, &last, nl_board_find("evk")); + nl_light_state_t s = nl_boot_state(NL_POB_ON, &last, nl_board_find("evk"), false); TEST_ASSERT_TRUE(s.on); TEST_ASSERT_EQUAL_UINT8(40, s.brightness); TEST_ASSERT_EQUAL(NL_MODE_COLOR, s.mode); @@ -48,7 +48,7 @@ static void test_off_forces_off(void) { nl_light_state_t last = red_off(); last.on = true; - nl_light_state_t s = nl_boot_state(NL_POB_OFF, &last, nl_board_find("evk")); + nl_light_state_t s = nl_boot_state(NL_POB_OFF, &last, nl_board_find("evk"), false); TEST_ASSERT_FALSE(s.on); TEST_ASSERT_EQUAL_UINT8(40, s.brightness); } @@ -56,12 +56,51 @@ static void test_off_forces_off(void) static void test_sanitizes_stored_values(void) { nl_light_state_t last = {true, 0, NL_MODE_COLOR, 1, 2, 3, 9000}; - nl_light_state_t s = nl_boot_state(NL_POB_LAST, &last, nl_board_find("e27_cw")); + nl_light_state_t s = nl_boot_state(NL_POB_LAST, &last, nl_board_find("e27_cw"), false); TEST_ASSERT_EQUAL_UINT8(100, s.brightness); /* invalid 0 -> 100 */ TEST_ASSERT_EQUAL(NL_MODE_WHITE, s.mode); /* no RGB on this board */ TEST_ASSERT_EQUAL_UINT16(5000, s.ct); /* clamped */ } +/* Spec 8.2: a reboot caused by an OTA is not a power cut - the light comes back effectively as + * power_on_behaviour = last, whatever pob says. */ +static void test_after_ota_ignores_pob_off(void) +{ + nl_light_state_t last = red_off(); + nl_light_state_t s; + last.on = true; + s = nl_boot_state(NL_POB_OFF, &last, nl_board_find("evk"), true); + TEST_ASSERT_TRUE(s.on); + TEST_ASSERT_TRUE(nl_light_state_equal(&last, &s)); +} + +static void test_after_ota_ignores_pob_on(void) +{ + nl_light_state_t last = red_off(); /* stored state: off */ + nl_light_state_t s = nl_boot_state(NL_POB_ON, &last, nl_board_find("evk"), true); + TEST_ASSERT_FALSE(s.on); + TEST_ASSERT_TRUE(nl_light_state_equal(&last, &s)); +} + +static void test_after_ota_still_sanitizes(void) +{ + nl_light_state_t last = {true, 200, NL_MODE_COLOR, 1, 2, 3, 9000}; + nl_light_state_t s = nl_boot_state(NL_POB_OFF, &last, nl_board_find("e27_cw"), true); + TEST_ASSERT_TRUE(s.on); + TEST_ASSERT_EQUAL_UINT8(100, s.brightness); + TEST_ASSERT_EQUAL(NL_MODE_WHITE, s.mode); + TEST_ASSERT_EQUAL_UINT16(5000, s.ct); +} + +/* Nothing stored (first boot after a migration): there is no "last" to restore, so the normal + * power_on_behaviour rules apply even with the flag set. */ +static void test_after_ota_without_saved_state_uses_pob(void) +{ + const nl_board_t *b = nl_board_find("evk"); + nl_light_state_t s = nl_boot_state(NL_POB_OFF, NULL, b, true); + TEST_ASSERT_FALSE(s.on); +} + int main(void) { UNITY_BEGIN(); @@ -71,5 +110,9 @@ int main(void) RUN_TEST(test_on_forces_on_with_last_values); RUN_TEST(test_off_forces_off); RUN_TEST(test_sanitizes_stored_values); + RUN_TEST(test_after_ota_ignores_pob_off); + RUN_TEST(test_after_ota_ignores_pob_on); + RUN_TEST(test_after_ota_still_sanitizes); + RUN_TEST(test_after_ota_without_saved_state_uses_pob); return UNITY_END(); }