From fbdf0c40ef6accce6927d27c774f87db4f029500 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Sun, 20 Sep 2026 03:35:19 +0200 Subject: [PATCH] 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. --- README.md | 11 ++++- src/board/nl_board.c | 4 +- src/board/nl_board.h | 9 +++-- src/config/nl_settings.c | 7 ++++ src/config/nl_settings.h | 7 +++- src/light/nl_dmx.c | 44 +++++++++++++++----- src/light/nl_dmx.h | 3 ++ src/net/nl_api.c | 55 +++++++++++++++++++------ src/net/nl_sacn.c | 1 + test/test_api.c | 67 +++++++++++++++++++++++++++++++ test/test_board.c | 9 +++-- test/test_dmx.c | 86 ++++++++++++++++++++++++++++++++++++++++ test/test_settings.c | 40 +++++++++++++++++++ 13 files changed, 311 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 9f8a014..28e57fe 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,16 @@ Die Lampe empfängt sACN (E1.31) auf UDP-Port 5568 — per Multicast auf `239.255..` 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 diff --git a/src/board/nl_board.c b/src/board/nl_board.c index 70d4dee..0a49838 100644 --- a/src/board/nl_board.c +++ b/src/board/nl_board.c @@ -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; } diff --git a/src/board/nl_board.h b/src/board/nl_board.h index 7c02950..7cda347 100644 --- a/src/board/nl_board.h +++ b/src/board/nl_board.h @@ -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 diff --git a/src/config/nl_settings.c b/src/config/nl_settings.c index 3a95fcd..970ed4f 100644 --- a/src/config/nl_settings.c +++ b/src/config/nl_settings.c @@ -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) diff --git a/src/config/nl_settings.h b/src/config/nl_settings.h index bef8e92..476efc1 100644 --- a/src/config/nl_settings.h +++ b/src/config/nl_settings.h @@ -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); diff --git a/src/light/nl_dmx.c b/src/light/nl_dmx.c index b5bae2e..c8d1223 100644 --- a/src/light/nl_dmx.c +++ b/src/light/nl_dmx.c @@ -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; diff --git a/src/light/nl_dmx.h b/src/light/nl_dmx.h index af905c5..ac519c0 100644 --- a/src/light/nl_dmx.h +++ b/src/light/nl_dmx.h @@ -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. */ diff --git a/src/net/nl_api.c b/src/net/nl_api.c index d649c5d..79e3bd4 100644 --- a/src/net/nl_api.c +++ b/src/net/nl_api.c @@ -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"; diff --git a/src/net/nl_sacn.c b/src/net/nl_sacn.c index 431bc69..0a756d0 100644 --- a/src/net/nl_sacn.c +++ b/src/net/nl_sacn.c @@ -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(); diff --git a/test/test_api.c b/test/test_api.c index dc39b67..b721e15 100644 --- a/test/test_api.c +++ b/test/test_api.c @@ -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); diff --git a/test/test_board.c b/test/test_board.c index 45e8d89..bb3655d 100644 --- a/test/test_board.c +++ b/test/test_board.c @@ -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) diff --git a/test/test_dmx.c b/test/test_dmx.c index 7d58375..d695787 100644 --- a/test/test_dmx.c +++ b/test/test_dmx.c @@ -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); diff --git a/test/test_settings.c b/test/test_settings.c index 790c0ff..4e9fc74 100644 --- a/test/test_settings.c +++ b/test/test_settings.c @@ -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(); }