LedFx und Verwandte denken in RGB-Pixeln: Ihr e131-Geraet schreibt je Pixel genau drei Kanaele. Unsere Belegung beginnt mit einem Master-Dimmer, also landet deren erstes Pixel auf Dimmer, R, G - ein rotes Effektbild (255,0,0) hiesse bei uns "Dimmer 255, Rot 0, Gruen 0" und damit Dunkelheit. Von Hand eintragen liess sich eine Lampe schon, sie zeigte aber Unsinn. Der Dimmer bleibt fuer DMX-Pulte richtig, deshalb eine zweite Belegung statt einer anderen: layout = "dimmer" (Vorgabe, unveraendert) oder "rgb" (R,G,B, drei Slots auf jedem Board). Im rgb-Layout ist eine Lampe genau ein Pixel. Ein Board ohne Farbkanaele bekommt trotzdem ein Farbtripel. Genommen wird der groesste der drei Werte auf beide Weisskanaele: die Lampe folgt sichtbar der Helligkeit des Effekts, ohne dass ein Farbmodell in den DMX-Pfad kommt - genau das wird dort sonst bewusst vermieden. Die Obergrenze fuer start_slot haengt jetzt am Layout (512 - slots + 1). Ein Wechsel, der einen bisher zulaessigen start_slot unzulaessig macht, wird abgelehnt statt still verschoben: eine Lampe, die nach einer Einstellung woanders im Universe liegt als angegeben, findet niemand wieder. Das neue Settings-Feld beginnt bei Offset 216, dem sizeof davor, und ist durch eine eigene Pruefung zur Uebersetzungszeit und einen Migrationstest abgesichert - derselbe Fallstrick, der beim Schlussreview fuer sacn_enabled gefunden wurde.
306 lines
11 KiB
C
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_dimmer(void)
|
|
{
|
|
nl_settings_t s;
|
|
TEST_ASSERT_FALSE(nl_settings_load(&s));
|
|
TEST_ASSERT_EQUAL_UINT8(0, s.sacn_layout);
|
|
}
|
|
|
|
static void test_sacn_layout_roundtrip(void)
|
|
{
|
|
nl_settings_t in = sample("layout"), out;
|
|
in.sacn_layout = 1;
|
|
TEST_ASSERT_EQUAL_INT(0, nl_settings_save(&in));
|
|
TEST_ASSERT_TRUE(nl_settings_load(&out));
|
|
TEST_ASSERT_EQUAL_UINT8(1, 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 = 1;
|
|
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(0, 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_dimmer);
|
|
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();
|
|
}
|