Files
ninlight-w600/test/test_settings.c
T
Michael Sauer 9ee1983dff feat: sACN-Belegung rgb als Vorgabe
Pixelorientierte Werkzeuge wie LedFx schreiben drei Kanaele je Pixel; mit
dem Dimmer voran landet deren erstes Pixel auf Dimmer, R, G. rgb ist damit
die Belegung, die out of the box das Richtige tut - der Dimmer bleibt fuer
DMX-Pulte eine Umschaltung entfernt.
2026-09-22 23:20:12 +02:00

306 lines
11 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));
}
static void test_sacn_defaults(void)
{
nl_settings_t s;
TEST_ASSERT_FALSE(nl_settings_load(&s));
TEST_ASSERT_EQUAL_UINT8(1, s.sacn_enabled);
TEST_ASSERT_EQUAL_UINT16(1, s.sacn_universe);
TEST_ASSERT_EQUAL_UINT16(1, s.sacn_start_slot);
TEST_ASSERT_EQUAL_UINT8(0, s.sacn_hold);
}
static void test_sacn_roundtrip(void)
{
nl_settings_t in = sample("dmx"), out;
in.sacn_enabled = 0;
in.sacn_universe = 4711;
in.sacn_start_slot = 300;
in.sacn_hold = 1;
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
TEST_ASSERT_TRUE(nl_settings_load(&out));
TEST_ASSERT_EQUAL_UINT8(0, out.sacn_enabled);
TEST_ASSERT_EQUAL_UINT16(4711, out.sacn_universe);
TEST_ASSERT_EQUAL_UINT16(300, out.sacn_start_slot);
TEST_ASSERT_EQUAL_UINT8(1, out.sacn_hold);
}
/* Ein Datensatz aus einer Firmware VOR sACN: die vier Felder muessen als echte Vorgaben
* zurueckkommen, nicht als Null - sonst waere sACN auf jeder aktualisierten Lampe aus.
* Die Laenge ist bewusst die Literalzahl 208: das war sizeof(nl_settings_t) vor diesem
* Zweig und damit die Laenge, die auf jeder ausgelieferten Lampe im Flash steht. Ein
* offsetof() auf ein heutiges Feld wuerde mit der Struktur mitwandern und genau den Fall
* verfehlen, um den es hier geht. Das letzte Byte (207) war im 208-Byte-Satz reines
* Fuellbyte und daher 0 - es wird hier nachgestellt, denn genau dieses Byte legt sich
* beim Lesen ueber das erste neue Feld. */
static void test_record_without_sacn_gets_defaults(void)
{
nl_settings_t in = sample("alt"), out;
uint32_t crc;
nl_settings_save(&in);
{
uint16_t shorter = 208u; /* sizeof(nl_settings_t) vor sACN */
fake_flash_mem[NL_FL_SETTINGS_A + 12 + 207] = 0; /* Fuellbyte des alten Satzes */
memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2);
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_UINT8(1, out.sacn_enabled);
TEST_ASSERT_EQUAL_UINT16(1, out.sacn_universe);
TEST_ASSERT_EQUAL_UINT16(1, out.sacn_start_slot);
TEST_ASSERT_EQUAL_UINT8(0, out.sacn_hold);
}
/* Die Satzlaenge muss durch 4 teilbar bleiben, sonst laufen Schreiber und Leser beim CRC
* auseinander (siehe NL_SETTINGS_CRC_OFF in nl_settings.c). */
static void test_sacn_layout_defaults_to_rgb(void)
{
nl_settings_t s;
TEST_ASSERT_FALSE(nl_settings_load(&s));
TEST_ASSERT_EQUAL_UINT8(1, s.sacn_layout);
}
static void test_sacn_layout_roundtrip(void)
{
nl_settings_t in = sample("layout"), out;
in.sacn_layout = 0; /* weg von der Vorgabe, sonst prueft der Test nur diese */
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
TEST_ASSERT_TRUE(nl_settings_load(&out));
TEST_ASSERT_EQUAL_UINT8(0, out.sacn_layout);
}
/* Derselbe Fallstrick wie bei sacn_enabled, eine Feldgeneration weiter: ein Satz, der von
* einer Firmware MIT sACN, aber OHNE layout geschrieben wurde, ist 216 Byte lang. Laege
* sacn_layout darunter, kaeme es als Fuellbyte dieses Satzes zurueck statt als Vorgabe. */
static void test_record_without_layout_gets_the_default(void)
{
nl_settings_t in = sample("vor-layout"), out;
uint32_t crc;
in.sacn_layout = 0; /* der abgeschnittene Satz darf diese 0 nicht zurueckliefern */
nl_settings_save(&in);
{
uint16_t shorter = 216u; /* sizeof(nl_settings_t) vor dem Layout-Feld */
memcpy(&fake_flash_mem[NL_FL_SETTINGS_A + 6], &shorter, 2);
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("vor-layout", out.name);
TEST_ASSERT_EQUAL_UINT8(1, out.sacn_enabled);
TEST_ASSERT_EQUAL_UINT8(1, out.sacn_layout);
}
static void test_record_size_stays_aligned(void)
{
TEST_ASSERT_EQUAL_UINT(0, sizeof(nl_settings_t) % 4u);
}
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);
RUN_TEST(test_sacn_defaults);
RUN_TEST(test_sacn_roundtrip);
RUN_TEST(test_record_without_sacn_gets_defaults);
RUN_TEST(test_sacn_layout_defaults_to_rgb);
RUN_TEST(test_sacn_layout_roundtrip);
RUN_TEST(test_record_without_layout_gets_the_default);
RUN_TEST(test_record_size_stays_aligned);
return UNITY_END();
}