feat(sacn): zweite Slot-Belegung "rgb" fuer pixelorientierte Werkzeuge

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.
This commit is contained in:
Michael Sauer
2026-09-20 03:35:19 +02:00
parent 2c7ebac6d1
commit fbdf0c40ef
13 changed files with 311 additions and 32 deletions
+10 -1
View File
@@ -77,7 +77,16 @@ Die Lampe empfängt sACN (E1.31) auf UDP-Port 5568 — per Multicast auf
`239.255.<universe_hi>.<universe_lo>` und per Unicast direkt an ihre IP. Nach einem
Update ist der Empfang aktiv: Universe 1, Startslot 1.
Slots ab `start_slot`: **Dimmer, R, G, B, CW, WW** (Boards ohne RGB: **Dimmer, CW, WW**).
Es gibt zwei Slot-Belegungen, umschaltbar über `layout`:
| `layout` | Slots ab `start_slot` | wofür |
|---|---|---|
| `dimmer` (Vorgabe) | Dimmer, R, G, B, CW, WW — ohne RGB: Dimmer, CW, WW | DMX-Pulte, die einen Master-Dimmer erwarten |
| `rgb` | R, G, B (3 Slots auf jedem Board) | pixelorientierte Werkzeuge wie LedFx, die je Pixel drei Kanäle schreiben |
Im `rgb`-Layout ist jede Lampe genau ein Pixel: Lampe 1 auf Offset 0, Lampe 2 auf Offset 3,
und so weiter. Ein Board ohne Farbkanäle nimmt den größten der drei Werte auf beide
Weißkanäle — es folgt damit der Helligkeit des Effekts.
Wieviele es sind, steht als `slots` in `GET system/settings`.
Während Pakete fließen, hat sACN Vorrang; die Werte gehen ohne Fade direkt an die
+3 -1
View File
@@ -50,7 +50,9 @@ const nl_board_t *nl_board_get(void)
}
#endif
uint8_t nl_board_dmx_slots(const nl_board_t *b)
uint8_t nl_board_dmx_slots(const nl_board_t *b, bool rgb_layout)
{
if (rgb_layout)
return 3u; /* R,G,B - unabhaengig vom Board, damit eine Lampe ein Pixel ist */
return (b != NULL && b->has_rgb) ? 6u : 3u;
}
+6 -3
View File
@@ -41,8 +41,11 @@ const nl_board_t *nl_board_find(const char *id);
/* Board selected at build time via NINLIGHT_DEVICE_ID. */
const nl_board_t *nl_board_get(void);
/* Slots, die eine Lampe im DMX-Universe belegt: Dimmer + R,G,B,CW,WW bzw. Dimmer + CW,WW.
* Steht hier, damit API, Uebernahme-Schicht und Doku dieselbe Zahl benutzen. */
uint8_t nl_board_dmx_slots(const nl_board_t *b);
/* Slots, die eine Lampe im DMX-Universe belegt. Steht hier, damit API, Uebernahme-Schicht
* und Doku dieselbe Zahl benutzen.
* rgb_layout = false: Dimmer + R,G,B,CW,WW (6) bzw. Dimmer + CW,WW (3)
* rgb_layout = true: R,G,B (3) auf JEDEM Board - so ist eine Lampe genau ein Pixel
* fuer pixelorientierte Werkzeuge wie LedFx. */
uint8_t nl_board_dmx_slots(const nl_board_t *b, bool rgb_layout);
#endif
+7
View File
@@ -56,6 +56,12 @@ typedef char nl_settings_size_check[(sizeof(nl_settings_t) % 4u == 0u) ? 1 : -1]
* Bei sacn_enabled hiesse das: sACN nach dem Update auf jeder Lampe aus. */
typedef char nl_settings_legacy_check[offsetof(nl_settings_t, sacn_enabled) == 208 ? 1 : -1];
/* Dasselbe fuer das naechste angehaengte Feld: sacn_layout muss bei 216 beginnen, dem
* sizeof(nl_settings_t) der Firmware davor. Wer hier anstoesst, hat ein Feld zu frueh
* eingefuegt - dann kaeme es aus einem aelteren Satz als Fuellbyte zurueck, nicht als
* Vorgabe. */
typedef char nl_settings_layout_offset_check[offsetof(nl_settings_t, sacn_layout) == 216 ? 1 : -1];
static uint32_t s_addr[2];
void nl_settings_defaults(nl_settings_t *s)
@@ -71,6 +77,7 @@ void nl_settings_defaults(nl_settings_t *s)
s->sacn_universe = 1;
s->sacn_start_slot = 1;
s->sacn_hold = 0;
s->sacn_layout = 0;
}
void nl_settings_init(uint32_t addr_a, uint32_t addr_b)
+6 -1
View File
@@ -35,7 +35,12 @@ typedef struct {
uint16_t sacn_universe; /* 1..63999 (Vorgabe 1) */
uint16_t sacn_start_slot; /* 1..512 (Vorgabe 1) */
uint8_t sacn_hold; /* 0 = nach dem Timeout zurueck zum HA-Zustand, 1 = halten */
uint8_t _padding[1]; /* padding for 4-byte alignment of sizeof(nl_settings_t) */
uint8_t _padding[1]; /* haelt sizeof auf einem Vielfachen von 4 */
uint8_t sacn_layout; /* Offset 216 - 0 = Dimmer voran, 1 = nur R,G,B (nl_dmx_layout_t).
* Dieselbe Regel wie oben: neue Felder beginnen bei oder hinter
* dem vorherigen sizeof (216), sonst ueberschreibt das Fuellbyte
* eines aelteren Satzes sie. */
uint8_t _padding2[3]; /* haelt sizeof auf einem Vielfachen von 4 */
} nl_settings_t;
void nl_settings_defaults(nl_settings_t *s);
+34 -10
View File
@@ -16,10 +16,11 @@ static uint32_t s_last_ms;
void nl_dmx_init(const nl_board_t *board)
{
s_board = board;
s_slots = nl_board_dmx_slots(board);
s_cfg.start_slot = 1;
s_cfg.hold = false;
s_cfg.release_fade_ms = 0;
s_cfg.rgb_layout = false;
s_slots = nl_board_dmx_slots(board, false);
s_streaming = false;
s_override = false;
s_prio = 0;
@@ -35,6 +36,9 @@ void nl_dmx_configure(const nl_dmx_cfg_t *cfg)
* sein, den es nicht sehen kann. */
if (s_cfg.start_slot < 1u)
s_cfg.start_slot = 1u;
/* Die Slotzahl haengt am Layout, nicht nur am Board - hier nachziehen, sonst liest
* nl_dmx_feed() beim Umschalten die falsche Anzahl. */
s_slots = nl_board_dmx_slots(s_board, s_cfg.rgb_layout);
}
bool nl_dmx_active(void) { return s_override; }
@@ -106,17 +110,37 @@ void nl_dmx_feed(const nl_e131_packet_t *p, uint32_t now_ms)
s_streaming = true;
s = p->slots + (s_cfg.start_slot - 1u);
dim = s[0];
memset(ch, 0, sizeof ch);
if (s_board->has_rgb) {
ch[NL_CH_R] = scale(s[1], dim);
ch[NL_CH_G] = scale(s[2], dim);
ch[NL_CH_B] = scale(s[3], dim);
ch[NL_CH_CW] = scale(s[4], dim);
ch[NL_CH_WW] = scale(s[5], dim);
if (s_cfg.rgb_layout) {
/* Drei Kanaele, kein Dimmer: so schreiben pixelorientierte Werkzeuge (LedFx und
* Verwandte) je Pixel genau R,G,B. Eine Lampe ist damit ein Pixel. */
if (s_board->has_rgb) {
ch[NL_CH_R] = s[0];
ch[NL_CH_G] = s[1];
ch[NL_CH_B] = s[2];
} else {
/* Ein Board ohne Farbkanaele bekommt trotzdem ein Farbtripel. Genommen wird der
* groesste der drei Werte auf beide Weisskanaele: die Lampe folgt damit sichtbar
* der Helligkeit des Effekts, ohne dass ein Farbmodell in den DMX-Pfad kommt -
* genau das wird hier sonst bewusst vermieden. */
uint8_t v = s[0] > s[1] ? s[0] : s[1];
if (s[2] > v)
v = s[2];
ch[NL_CH_CW] = v;
ch[NL_CH_WW] = v;
}
} else {
ch[NL_CH_CW] = scale(s[1], dim);
ch[NL_CH_WW] = scale(s[2], dim);
dim = s[0];
if (s_board->has_rgb) {
ch[NL_CH_R] = scale(s[1], dim);
ch[NL_CH_G] = scale(s[2], dim);
ch[NL_CH_B] = scale(s[3], dim);
ch[NL_CH_CW] = scale(s[4], dim);
ch[NL_CH_WW] = scale(s[5], dim);
} else {
ch[NL_CH_CW] = scale(s[1], dim);
ch[NL_CH_WW] = scale(s[2], dim);
}
}
nl_engine_override(ch);
s_override = true;
+3
View File
@@ -14,6 +14,9 @@ typedef struct {
uint16_t start_slot; /* 1..512 */
bool hold; /* false = nach dem Timeout zurueck zum Zustand von nl_ctrl */
uint16_t release_fade_ms; /* Dauer der Rueckblende (aus settings.fade_in_ms) */
/* false: Dimmer voran (DMX-Pulte erwarten das). true: nur R,G,B - dann ist eine Lampe
* genau ein Pixel fuer Werkzeuge wie LedFx, die je Pixel drei Kanaele schreiben. */
bool rgb_layout;
} nl_dmx_cfg_t;
/* Die Engine muss initialisiert sein. Nicht threadsicher: Aufrufer serialisieren. */
+42 -13
View File
@@ -305,7 +305,8 @@ static void handle_get_settings(const nl_api_ctx_t *ctx, nl_api_response_t *resp
cJSON_AddStringToObject(sa, "on_timeout", s.sacn_hold ? "hold" : "release");
/* Informativ und bewusst aus dem Board, nicht aus dem Datensatz: so steht beim
* Adressieren direkt da, wieviel Platz diese Lampe im Universe belegt. */
cJSON_AddNumberToObject(sa, "slots", nl_board_dmx_slots(ctx->board));
cJSON_AddStringToObject(sa, "layout", s.sacn_layout ? "rgb" : "dimmer");
cJSON_AddNumberToObject(sa, "slots", nl_board_dmx_slots(ctx->board, s.sacn_layout != 0));
}
reply_json(resp, 200, o);
}
@@ -356,12 +357,29 @@ static void handle_put_settings(const nl_api_ctx_t *ctx, const nl_http_request_t
s.auth_all = cJSON_IsTrue(all) ? 1 : 0;
} else if (strcmp(type, "sacn") == 0) {
const cJSON *en = cJSON_GetObjectItemCaseSensitive(j, "enabled");
const char *ot = NULL;
const char *ot = NULL, *lo = NULL;
long un = 0, st = 0;
int unp, stp, otp;
uint8_t slots = nl_board_dmx_slots(ctx->board);
int unp, stp, otp, lop;
bool rgb;
uint8_t slots;
uint16_t want_start;
if (en && !cJSON_IsBool(en)) {
/* Das Layout wird ZUERST ausgewertet: es bestimmt die Slotzahl und damit die
* Obergrenze fuer start_slot. Wer in derselben Anfrage beides setzt, soll gegen das
* neue Layout geprueft werden, nicht gegen das alte. */
lop = get_string(j, "layout", 1, 16, &lo);
if (lop < 0 || (lop == 1 && strcmp(lo, "dimmer") != 0 && strcmp(lo, "rgb") != 0)) {
err = "invalid layout";
lop = 0; /* die Zweige unten duerfen den Wert dann nicht mehr benutzen */
rgb = s.sacn_layout != 0;
} else {
rgb = (lop == 1) ? (strcmp(lo, "rgb") == 0) : (s.sacn_layout != 0);
}
slots = nl_board_dmx_slots(ctx->board, rgb);
if (err) {
/* invalid layout steht schon fest */
} else if (en && !cJSON_IsBool(en)) {
err = "invalid enabled";
} else if ((unp = get_int(j, "universe", 1, 63999, &un)) < 0) {
err = "invalid universe";
@@ -373,14 +391,25 @@ static void handle_put_settings(const nl_api_ctx_t *ctx, const nl_http_request_t
(otp == 1 && strcmp(ot, "release") != 0 && strcmp(ot, "hold") != 0)) {
err = "invalid on_timeout";
} else {
if (en)
s.sacn_enabled = cJSON_IsTrue(en) ? 1 : 0;
if (unp == 1)
s.sacn_universe = (uint16_t)un;
if (stp == 1)
s.sacn_start_slot = (uint16_t)st;
if (otp == 1)
s.sacn_hold = strcmp(ot, "hold") == 0 ? 1 : 0;
/* Ein Wechsel auf ein breiteres Layout kann einen bisher zulaessigen start_slot
* unzulaessig machen. Das wird abgelehnt statt still verschoben: eine Lampe, die
* nach einer Einstellung woanders im Universe liegt als angegeben, findet niemand
* wieder. */
want_start = (stp == 1) ? (uint16_t)st : s.sacn_start_slot;
if ((uint32_t)want_start + slots - 1u > 512u) {
err = "invalid start_slot";
} else {
if (en)
s.sacn_enabled = cJSON_IsTrue(en) ? 1 : 0;
if (unp == 1)
s.sacn_universe = (uint16_t)un;
if (stp == 1)
s.sacn_start_slot = (uint16_t)st;
if (otp == 1)
s.sacn_hold = strcmp(ot, "hold") == 0 ? 1 : 0;
if (lop == 1)
s.sacn_layout = rgb ? 1 : 0;
}
}
} else {
err = "invalid type";
+1
View File
@@ -40,6 +40,7 @@ void nl_sacn_apply_settings(const nl_settings_t *s)
c.start_slot = s->sacn_start_slot;
c.hold = s->sacn_hold != 0;
c.release_fade_ms = s->fade_in_ms;
c.rgb_layout = s->sacn_layout != 0;
nl_app_lock();
nl_dmx_configure(&c);
nl_app_unlock();
+67
View File
@@ -508,9 +508,76 @@ static void test_commands(void)
TEST_ASSERT_EQUAL_STRING("invalid command", error_text());
}
/* --- sacn: Belegung --- */
static void test_settings_reports_the_layout_and_matching_slots(void)
{
call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL);
TEST_ASSERT_EQUAL_STRING("dimmer", cJSON_GetStringValue(sacn_field("layout")));
TEST_ASSERT_EQUAL_INT(6, (int)cJSON_GetNumberValue(sacn_field("slots")));
fake_settings.sacn_layout = 1;
call(NL_HTTP_GET, "/api/v1/system/settings", TOKEN, NULL);
TEST_ASSERT_EQUAL_STRING("rgb", cJSON_GetStringValue(sacn_field("layout")));
/* Im rgb-Layout belegt auch ein Board mit Farbe nur drei Slots. */
TEST_ASSERT_EQUAL_INT(3, (int)cJSON_GetNumberValue(sacn_field("slots")));
}
static void test_put_sacn_layout(void)
{
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"rgb\"}");
TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_layout);
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"dimmer\"}");
TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_EQUAL_UINT8(0, fake_settings.sacn_layout);
}
static void test_put_sacn_rejects_an_unknown_layout(void)
{
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"pixel\"}");
TEST_ASSERT_EQUAL_INT(400, resp.status);
TEST_ASSERT_EQUAL_STRING("invalid layout", error_text());
TEST_ASSERT_EQUAL_INT(0, fake_save_calls);
}
/* Die Obergrenze fuer start_slot haengt am Layout. Im rgb-Layout (3 Slots) ist 510 erlaubt,
* mit Dimmer (6 Slots) nicht mehr - ein Wechsel darf einen unzulaessigen Zustand also nicht
* still herstellen. */
static void test_put_sacn_start_slot_limit_follows_the_layout(void)
{
fake_settings.sacn_layout = 1;
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"start_slot\":510}");
TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_EQUAL_UINT16(510, fake_settings.sacn_start_slot);
/* Zurueck auf dimmer wuerde 510 + 6 - 1 = 515 bedeuten: abgelehnt. */
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN, "{\"type\":\"sacn\",\"layout\":\"dimmer\"}");
TEST_ASSERT_EQUAL_INT(400, resp.status);
TEST_ASSERT_EQUAL_STRING("invalid start_slot", error_text());
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_layout); /* nichts uebernommen */
}
static void test_put_sacn_layout_and_start_slot_together(void)
{
/* Beides in einer Anfrage: geprueft wird gegen das NEUE Layout. */
fake_settings.sacn_layout = 0;
call(NL_HTTP_PUT, "/api/v1/system/settings", TOKEN,
"{\"type\":\"sacn\",\"layout\":\"rgb\",\"start_slot\":510}");
TEST_ASSERT_EQUAL_INT(200, resp.status);
TEST_ASSERT_EQUAL_UINT8(1, fake_settings.sacn_layout);
TEST_ASSERT_EQUAL_UINT16(510, fake_settings.sacn_start_slot);
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_settings_reports_the_layout_and_matching_slots);
RUN_TEST(test_put_sacn_layout);
RUN_TEST(test_put_sacn_rejects_an_unknown_layout);
RUN_TEST(test_put_sacn_start_slot_limit_follows_the_layout);
RUN_TEST(test_put_sacn_layout_and_start_slot_together);
RUN_TEST(test_unknown_path_and_method);
RUN_TEST(test_get_state);
RUN_TEST(test_white_board_capabilities);
+6 -3
View File
@@ -108,9 +108,12 @@ static void test_known_values_from_old_firmware(void)
static void test_dmx_slots(void)
{
TEST_ASSERT_EQUAL_UINT8(6, nl_board_dmx_slots(nl_board_find("gu10_rgb")));
TEST_ASSERT_EQUAL_UINT8(3, nl_board_dmx_slots(nl_board_find("g95_cw")));
TEST_ASSERT_EQUAL_UINT8(6, nl_board_dmx_slots(nl_board_find("evk")));
TEST_ASSERT_EQUAL_UINT8(6, nl_board_dmx_slots(nl_board_find("gu10_rgb"), false));
TEST_ASSERT_EQUAL_UINT8(3, nl_board_dmx_slots(nl_board_find("g95_cw"), false));
TEST_ASSERT_EQUAL_UINT8(6, nl_board_dmx_slots(nl_board_find("evk"), false));
/* Im rgb-Layout belegt JEDES Board drei Slots - eine Lampe ist ein Pixel. */
TEST_ASSERT_EQUAL_UINT8(3, nl_board_dmx_slots(nl_board_find("gu10_rgb"), true));
TEST_ASSERT_EQUAL_UINT8(3, nl_board_dmx_slots(nl_board_find("g95_cw"), true));
}
int main(void)
+86
View File
@@ -45,6 +45,20 @@ static void start(const char *board_id, uint16_t start_slot, bool hold)
dc.start_slot = start_slot;
dc.hold = hold;
dc.release_fade_ms = 0;
dc.rgb_layout = false;
nl_dmx_configure(&dc);
}
/* Wie start(), aber mit der Belegung ohne Dimmer (drei Kanaele je Lampe). */
static void start_rgb(const char *board_id, uint16_t start_slot)
{
nl_dmx_cfg_t dc;
start(board_id, start_slot, false);
dc.start_slot = start_slot;
dc.hold = false;
dc.release_fade_ms = 0;
dc.rgb_layout = true;
nl_dmx_configure(&dc);
}
@@ -303,9 +317,81 @@ static void test_configure_during_stream_takes_effect(void)
TEST_ASSERT_EQUAL_UINT8(222, last_ch[NL_CH_R]);
}
/* --- Belegung "rgb": drei Kanaele, kein Dimmer --- */
static void test_rgb_layout_maps_three_slots_to_colour(void)
{
uint8_t d[3] = { 10, 20, 30 };
start_rgb("gu10_rgb", 1);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 3, 1000);
nl_engine_tick(10);
TEST_ASSERT_TRUE(nl_dmx_active());
TEST_ASSERT_EQUAL_UINT8(10, last_ch[NL_CH_R]);
TEST_ASSERT_EQUAL_UINT8(20, last_ch[NL_CH_G]);
TEST_ASSERT_EQUAL_UINT8(30, last_ch[NL_CH_B]);
/* Kein Dimmer heisst auch: die Weisskanaele bleiben aus, sie haben keinen Slot. */
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_CW]);
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_WW]);
}
static void test_rgb_layout_passes_full_value_through(void)
{
/* Der Fall, der mit der Dimmer-Belegung schiefging: ein rotes Effektbild eines
* pixelorientierten Werkzeugs (255,0,0) muss rot leuchten, nicht dunkel bleiben. */
uint8_t d[3] = { 255, 0, 0 };
start_rgb("gu10_rgb", 1);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 3, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(255, last_ch[NL_CH_R]);
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_G]);
}
static void test_rgb_layout_on_a_board_without_colour_uses_the_largest_value(void)
{
uint8_t d[3] = { 40, 200, 90 };
start_rgb("g95_cw", 1);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 3, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(200, last_ch[NL_CH_CW]);
TEST_ASSERT_EQUAL_UINT8(200, last_ch[NL_CH_WW]);
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_R]);
}
static void test_rgb_layout_uses_start_slot(void)
{
/* Zweite Lampe im Universe: LedFx-Offset 3, also Slots 4..6. */
uint8_t d[6] = { 9, 9, 9, 11, 22, 33 };
start_rgb("gu10_rgb", 4);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(11, last_ch[NL_CH_R]);
TEST_ASSERT_EQUAL_UINT8(33, last_ch[NL_CH_B]);
}
static void test_rgb_layout_needs_only_three_slots(void)
{
/* Mit Dimmer braeuchte das gu10_rgb sechs Slots und wuerde ein Paket mit dreien
* verwerfen - im rgb-Layout reichen drei. */
uint8_t d[3] = { 1, 2, 3 };
start_rgb("gu10_rgb", 1);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 3, 1000);
nl_engine_tick(10);
TEST_ASSERT_TRUE(nl_dmx_active());
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_rgb_layout_maps_three_slots_to_colour);
RUN_TEST(test_rgb_layout_passes_full_value_through);
RUN_TEST(test_rgb_layout_on_a_board_without_colour_uses_the_largest_value);
RUN_TEST(test_rgb_layout_uses_start_slot);
RUN_TEST(test_rgb_layout_needs_only_three_slots);
RUN_TEST(test_first_packet_takes_over);
RUN_TEST(test_master_dimmer_scales);
RUN_TEST(test_dimmer_zero_is_dark);
+40
View File
@@ -238,6 +238,43 @@ static void test_record_without_sacn_gets_defaults(void)
/* 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);
@@ -260,6 +297,9 @@ int main(void)
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();
}