Zwei neue Einstellungen, beide wie fade_in/fade_out ueber POST /api/v1/device/state les- und schreibbar und sofort wirksam: - transition_default (Voreinstellung 300 ms) fuellt die Transition fuer Farb-, Weiss- und Helligkeitswechsel bei eingeschalteter Lampe, wenn der Aufruf kein eigenes "transition" mitbringt. Beim Ein- und Ausschalten behalten fade_in/fade_out weiterhin Vorrang. - wake_on_change (Voreinstellung aus) schaltet die Lampe bei einer Farb-, Weiss- oder Helligkeitsaenderung ein, wenn sie aus war. Ein ausdrueckliches "on": false im selben Aufruf gewinnt immer. Beide Felder haengen am Ende von nl_settings_t, damit bestehende Lampen ueber die Laengentoleranz von read_rec() ihre echten Voreinstellungen bekommen statt Nullen. nl_ctrl_set_fades() heisst jetzt nl_ctrl_set_options(). Dazu ein Treiberfehler, der erst an der migrierten GU10 RGB auffiel: der WY-Zweig von nl_sm2135eh_set() schrieb nur Header und WY-Daten, nie die RGB-Datenregister. Der Chip behielt also die zuletzt gesetzte Farbe, und bei einem direkten Wechsel Farbe -> Weiss (ohne Zwischenschritt ueber dunkel) schaelte sie sich beim Ausblenden wieder frei. Der Header sendet jetzt die - in Weiss immer nullwertigen - RGB-Daten mit. Die Bruno-Collection zeigt Transition jetzt zusammen mit einem Farb- und einem Weisswechsel im selben Aufruf und hat einen Request fuer die beiden neuen Einstellungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
201 lines
6.9 KiB
C
201 lines
6.9 KiB
C
#include <stddef.h>
|
|
#include <string.h>
|
|
#include "unity.h"
|
|
#include "fake_flash.h"
|
|
#include "config/nl_flash_layout.h"
|
|
#include "config/nl_settings.h"
|
|
#include "light/nl_light_types.h"
|
|
#include "util/nl_crc.h"
|
|
|
|
void setUp(void)
|
|
{
|
|
fake_flash_reset();
|
|
nl_settings_init(NL_FL_SETTINGS_A, NL_FL_SETTINGS_B);
|
|
}
|
|
|
|
void tearDown(void)
|
|
{
|
|
TEST_ASSERT_EQUAL_UINT(0, fake_flash_dirty_writes);
|
|
}
|
|
|
|
static nl_settings_t sample(const char *name)
|
|
{
|
|
nl_settings_t s;
|
|
nl_settings_defaults(&s);
|
|
strcpy(s.wifi_ssid, "ninHome");
|
|
strcpy(s.wifi_pass, "secret");
|
|
strcpy(s.name, name);
|
|
s.auth_all = 1;
|
|
s.power_on_behaviour = NL_POB_ON;
|
|
s.fade_in_ms = 1200;
|
|
return s;
|
|
}
|
|
|
|
static void test_defaults(void)
|
|
{
|
|
nl_settings_t s;
|
|
TEST_ASSERT_FALSE(nl_settings_load(&s));
|
|
TEST_ASSERT_EQUAL_STRING("", s.wifi_ssid);
|
|
TEST_ASSERT_EQUAL_STRING("aabbccddeeff0011223344", s.token);
|
|
TEST_ASSERT_EQUAL_STRING("", s.name);
|
|
TEST_ASSERT_EQUAL_UINT8(0, s.auth_all);
|
|
TEST_ASSERT_EQUAL_UINT8(NL_POB_LAST, s.power_on_behaviour);
|
|
TEST_ASSERT_EQUAL_UINT16(500, s.fade_in_ms);
|
|
TEST_ASSERT_EQUAL_UINT16(500, s.fade_out_ms);
|
|
TEST_ASSERT_EQUAL_UINT16(300, s.transition_ms);
|
|
TEST_ASSERT_EQUAL_UINT8(0, s.wake_on_change);
|
|
}
|
|
|
|
static void test_save_and_load(void)
|
|
{
|
|
nl_settings_t in = sample("Wohnzimmer"), out;
|
|
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_MEMORY(&in, &out, sizeof in);
|
|
}
|
|
|
|
static void test_alternates_slots(void)
|
|
{
|
|
nl_settings_t s, out;
|
|
s = sample("a"); nl_settings_save(&s);
|
|
s = sample("b"); nl_settings_save(&s);
|
|
s = sample("c"); nl_settings_save(&s);
|
|
TEST_ASSERT_EQUAL_UINT(2, fake_flash_erases[0]);
|
|
TEST_ASSERT_EQUAL_UINT(1, fake_flash_erases[1]);
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_STRING("c", out.name);
|
|
}
|
|
|
|
static void test_power_loss_keeps_previous(void)
|
|
{
|
|
nl_settings_t s = sample("old"), out;
|
|
nl_settings_save(&s);
|
|
s = sample("new");
|
|
fake_flash_fail_after(50);
|
|
TEST_ASSERT_NOT_EQUAL(0, nl_settings_save(&s));
|
|
fake_flash_fail_after(-1);
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_STRING("old", out.name);
|
|
}
|
|
|
|
static void test_corrupt_newest_falls_back(void)
|
|
{
|
|
nl_settings_t s, out;
|
|
s = sample("first"); nl_settings_save(&s); /* slot A */
|
|
s = sample("second"); nl_settings_save(&s); /* slot B */
|
|
fake_flash_mem[NL_FL_SETTINGS_B + 40] ^= 0x01;
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_STRING("first", out.name);
|
|
}
|
|
|
|
static void test_load_accepts_shorter_record(void)
|
|
{
|
|
nl_settings_t in, out;
|
|
uint32_t crc;
|
|
/* simulate a record written by a firmware that predates fade_in_ms/fade_out_ms/
|
|
* ota_pending: length ends right before fade_in_ms. Those fields must come back as
|
|
* their real defaults (500 ms / 0), not as zero just because the record didn't carry
|
|
* them - that's the whole point of the length tolerance. */
|
|
in = sample("alt");
|
|
nl_settings_save(&in);
|
|
{
|
|
uint16_t shorter = (uint16_t)offsetof(nl_settings_t, fade_in_ms);
|
|
memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2); /* header.length */
|
|
crc = nl_crc32(0, &fake_flash_mem[NL_FL_SETTINGS_A], 12 + shorter);
|
|
memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 12 + shorter], &crc, 4);
|
|
}
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_STRING("alt", out.name);
|
|
TEST_ASSERT_EQUAL_STRING("ninHome", out.wifi_ssid);
|
|
TEST_ASSERT_EQUAL_UINT16(500, out.fade_in_ms);
|
|
TEST_ASSERT_EQUAL_UINT16(500, out.fade_out_ms);
|
|
TEST_ASSERT_EQUAL_UINT8(0, out.ota_pending);
|
|
TEST_ASSERT_EQUAL_UINT16(300, out.transition_ms);
|
|
TEST_ASSERT_EQUAL_UINT8(0, out.wake_on_change);
|
|
}
|
|
|
|
static void test_transition_and_wake_roundtrip(void)
|
|
{
|
|
nl_settings_t in = sample("neu"), out;
|
|
in.transition_ms = 1500;
|
|
in.wake_on_change = 1;
|
|
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_UINT16(1500, out.transition_ms);
|
|
TEST_ASSERT_EQUAL_UINT8(1, out.wake_on_change);
|
|
}
|
|
|
|
static void test_load_rejects_longer_record(void)
|
|
{
|
|
nl_settings_t in = sample("x"), out;
|
|
uint16_t longer = (uint16_t)(sizeof(nl_settings_t) + 1);
|
|
nl_settings_save(&in);
|
|
/* read_rec() rejects hdr.length > sizeof(nl_settings_t) outright, before it even reads
|
|
* the payload or the CRC - so the stored CRC is irrelevant here and deliberately left
|
|
* as whatever nl_settings_save() wrote for the original (shorter) length. */
|
|
memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &longer, 2); /* header.length */
|
|
TEST_ASSERT_FALSE(nl_settings_load(&out)); /* unknown newer layout -> defaults */
|
|
}
|
|
|
|
static void test_ota_pending_default_and_roundtrip(void)
|
|
{
|
|
nl_settings_t in, out;
|
|
TEST_ASSERT_FALSE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_UINT8(0, out.ota_pending);
|
|
|
|
in = sample("ota");
|
|
in.ota_pending = 1;
|
|
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_UINT8(1, out.ota_pending);
|
|
}
|
|
|
|
/* I6: the writer must place the CRC exactly where the reader looks for it - directly behind
|
|
* `length` payload bytes. Both sides only agreed by accident while sizeof(nl_settings_t) is a
|
|
* multiple of 4; one extra uint16_t in the struct used to move the written CRC two bytes past
|
|
* the read offset, which would have failed every stored record's CRC after an update. This test
|
|
* nails the invariant independently of the struct size. */
|
|
static void test_crc_offset_matches_reader(void)
|
|
{
|
|
nl_settings_t in = sample("crc"), out;
|
|
uint16_t length;
|
|
uint32_t crc, stored;
|
|
const uint8_t *rec = &fake_flash_mem[NL_FL_SETTINGS_A];
|
|
|
|
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
|
|
memcpy(&length, rec + 6, 2); /* header.length */
|
|
TEST_ASSERT_EQUAL_UINT16(sizeof(nl_settings_t), length);
|
|
|
|
crc = nl_crc32(0, rec, 12u + length); /* header + exactly `length` payload bytes */
|
|
memcpy(&stored, rec + 12u + length, 4); /* where read_rec() takes the CRC from */
|
|
TEST_ASSERT_EQUAL_HEX32(crc, stored);
|
|
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out)); /* and the record really does read back */
|
|
TEST_ASSERT_EQUAL_MEMORY(&in, &out, sizeof in);
|
|
}
|
|
|
|
static void test_erase(void)
|
|
{
|
|
nl_settings_t s = sample("x"), out;
|
|
nl_settings_save(&s);
|
|
TEST_ASSERT_EQUAL_INT(0, nl_settings_erase());
|
|
TEST_ASSERT_FALSE(nl_settings_load(&out));
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_defaults);
|
|
RUN_TEST(test_transition_and_wake_roundtrip);
|
|
RUN_TEST(test_save_and_load);
|
|
RUN_TEST(test_alternates_slots);
|
|
RUN_TEST(test_power_loss_keeps_previous);
|
|
RUN_TEST(test_corrupt_newest_falls_back);
|
|
RUN_TEST(test_load_accepts_shorter_record);
|
|
RUN_TEST(test_load_rejects_longer_record);
|
|
RUN_TEST(test_ota_pending_default_and_roundtrip);
|
|
RUN_TEST(test_crc_offset_matches_reader);
|
|
RUN_TEST(test_erase);
|
|
return UNITY_END();
|
|
}
|