Files
Michael Sauer fbdf0c40ef 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.
2026-09-20 03:35:19 +02:00

413 lines
14 KiB
C

#include <string.h>
#include "unity.h"
#include "board/nl_board.h"
#include "light/nl_dmx.h"
#include "light/nl_engine.h"
static uint8_t last_ch[NL_CH_COUNT];
static void sink(const uint8_t ch[NL_CH_COUNT], void *ctx)
{
(void)ctx;
memcpy(last_ch, ch, sizeof last_ch);
}
static uint8_t slot_data[16];
/* Baut ein Paket aus slot_data und speist es ein - als Funktion, damit kein Aufrufer
* die Adresse eines Rueckgabewerts nehmen muss. */
static void feed(uint8_t prio, uint8_t seq, bool terminated, uint16_t count, uint32_t now_ms)
{
nl_e131_packet_t p;
p.universe = 1;
p.priority = prio;
p.sequence = seq;
p.terminated = terminated;
p.slot_count = count;
p.slots = slot_data;
nl_dmx_feed(&p, now_ms);
}
static void start(const char *board_id, uint16_t start_slot, bool hold)
{
const nl_board_t *b = nl_board_find(board_id);
nl_engine_cfg_t cfg;
nl_dmx_cfg_t dc;
nl_light_state_t off = { false, 100, NL_MODE_WHITE, 0, 0, 0, 3000 };
cfg.ct_min = b->ct_min;
cfg.ct_max = b->ct_max;
cfg.output = sink;
cfg.ctx = NULL;
nl_engine_init(&cfg);
nl_engine_set(&off, 0);
nl_dmx_init(b);
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);
}
void setUp(void)
{
memset(slot_data, 0, sizeof slot_data);
memset(last_ch, 0, sizeof last_ch);
}
void tearDown(void) {}
static void test_first_packet_takes_over(void)
{
/* Dimmer voll, R=10 G=20 B=30 CW=40 WW=50 */
uint8_t d[6] = { 255, 10, 20, 30, 40, 50 };
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 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]);
TEST_ASSERT_EQUAL_UINT8(40, last_ch[NL_CH_CW]);
TEST_ASSERT_EQUAL_UINT8(50, last_ch[NL_CH_WW]);
}
static void test_master_dimmer_scales(void)
{
uint8_t d[6] = { 128, 200, 200, 200, 200, 200 };
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 1000);
nl_engine_tick(10);
TEST_ASSERT_UINT8_WITHIN(1, 100, last_ch[NL_CH_R]);
}
static void test_dimmer_zero_is_dark(void)
{
uint8_t d[6] = { 0, 255, 255, 255, 255, 255 };
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 1000);
nl_engine_tick(10);
TEST_ASSERT_TRUE(nl_dmx_active()); /* dunkel, aber sehr wohl ferngesteuert */
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_R]);
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_WW]);
}
static void test_board_without_rgb_uses_three_slots(void)
{
uint8_t d[3] = { 255, 77, 88 };
start("g95_cw", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 3, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(77, last_ch[NL_CH_CW]);
TEST_ASSERT_EQUAL_UINT8(88, last_ch[NL_CH_WW]);
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_R]);
}
static void test_start_slot_offset(void)
{
uint8_t d[9] = { 9, 9, 9, 255, 11, 22, 33, 44, 55 };
start("gu10_rgb", 4, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 9, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(11, last_ch[NL_CH_R]);
TEST_ASSERT_EQUAL_UINT8(55, last_ch[NL_CH_WW]);
}
static void test_packet_too_short_for_our_slots_is_dropped(void)
{
uint8_t d[4] = { 255, 1, 2, 3 };
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 4, 1000); /* braucht 6, bekommt 4 */
nl_engine_tick(10);
TEST_ASSERT_FALSE(nl_dmx_active());
}
static void test_stale_sequence_is_dropped(void)
{
uint8_t d[6] = { 255, 10, 0, 0, 0, 0 };
uint8_t older[6] = { 255, 99, 0, 0, 0, 0 };
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 50, false, 6, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(10, last_ch[NL_CH_R]);
memcpy(slot_data, older, sizeof older);
feed(100, 40, false, 6, 1010); /* 10 zurueck: im Fenster, verwerfen */
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(10, last_ch[NL_CH_R]);
feed(100, 20, false, 6, 1020); /* 30 zurueck: ausserhalb, annehmen */
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(99, last_ch[NL_CH_R]);
}
static void test_a_packet_exactly_twenty_behind_is_accepted(void)
{
/* E1.31-2018 6.7.2 verwirft "<= 0 und > -20" - bei d == -20 ist das Fenster bereits
* verlassen, das Paket gilt als neu. Ein Reviewer hat das schon einmal fuer einen
* Off-by-one gehalten; dieser Test legt fest, auf welcher Seite die Grenze liegt. */
uint8_t d[6] = { 255, 10, 0, 0, 0, 0 };
uint8_t twenty_behind[6] = { 255, 77, 0, 0, 0, 0 };
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 20, false, 6, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(10, last_ch[NL_CH_R]);
memcpy(slot_data, twenty_behind, sizeof twenty_behind);
feed(100, 0, false, 6, 1010); /* d = 0 - 20 = -20: ausserhalb des Fensters, annehmen */
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(77, last_ch[NL_CH_R]);
}
static void test_higher_priority_source_takes_over_despite_stale_looking_sequence(void)
{
/* Quelle A laeuft mit hoher Sequenznummer. Quelle B mit hoeherer Prioritaet startet mit
* einer Sequenz, die - gegen A's Zaehler geprueft - wie ein alter Wiederholer aussaehe.
* B fuehrt aber ihre eigene Zaehlung und muss trotzdem sofort uebernehmen. */
uint8_t a[6] = { 255, 10, 0, 0, 0, 0 };
uint8_t b[6] = { 255, 222, 0, 0, 0, 0 };
start("gu10_rgb", 1, false);
memcpy(slot_data, a, sizeof a);
feed(100, 50, false, 6, 1000); /* Quelle A, Prioritaet 100, Sequenz 50 */
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(10, last_ch[NL_CH_R]);
memcpy(slot_data, b, sizeof b);
/* Quelle B, Prioritaet 150, Sequenz 45: gegen A's Zaehler (50) waere d = -5, also
* innerhalb des Verwerfungsfensters - trotzdem muss B sofort uebernehmen. */
feed(150, 45, false, 6, 1010);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(222, last_ch[NL_CH_R]);
}
static void test_lower_priority_is_ignored_while_streaming(void)
{
uint8_t high[6] = { 255, 10, 0, 0, 0, 0 };
uint8_t low[6] = { 255, 200, 0, 0, 0, 0 };
start("gu10_rgb", 1, false);
memcpy(slot_data, high, sizeof high);
feed(150, 1, false, 6, 1000);
nl_engine_tick(10);
memcpy(slot_data, low, sizeof low);
feed(100, 2, false, 6, 1010);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(10, last_ch[NL_CH_R]);
/* Nach dem Timeout ist wieder jede Prioritaet willkommen. */
nl_dmx_tick(1000 + NL_DMX_TIMEOUT_MS + 1);
feed(100, 3, false, 6, 5000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(200, last_ch[NL_CH_R]);
}
static void test_timeout_release_returns_to_engine(void)
{
uint8_t d[6] = { 255, 255, 255, 255, 255, 255 };
start("gu10_rgb", 1, false); /* die Lampe steht auf "aus" */
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(255, last_ch[NL_CH_R]);
nl_dmx_tick(1000 + NL_DMX_TIMEOUT_MS - 1);
TEST_ASSERT_TRUE(nl_dmx_active());
nl_dmx_tick(1000 + NL_DMX_TIMEOUT_MS);
nl_engine_tick(10);
TEST_ASSERT_FALSE(nl_dmx_active());
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_R]); /* wieder aus, wie von nl_ctrl gesetzt */
}
static void test_timeout_hold_keeps_last_values(void)
{
uint8_t d[6] = { 255, 123, 0, 0, 0, 0 };
start("gu10_rgb", 1, true);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 1000);
nl_engine_tick(10);
nl_dmx_tick(1000 + NL_DMX_TIMEOUT_MS + 500);
nl_engine_tick(10);
TEST_ASSERT_TRUE(nl_dmx_active());
TEST_ASSERT_EQUAL_UINT8(123, last_ch[NL_CH_R]);
}
static void test_hold_keeps_darkness(void)
{
uint8_t dark[6] = { 0, 0, 0, 0, 0, 0 };
nl_light_state_t on = { true, 100, NL_MODE_WHITE, 0, 0, 0, 3000 };
start("gu10_rgb", 1, true);
nl_engine_set(&on, 0); /* HA haette die Lampe gern an */
memcpy(slot_data, dark, sizeof dark);
feed(100, 1, false, 6, 1000);
nl_dmx_tick(1000 + NL_DMX_TIMEOUT_MS + 1);
nl_engine_tick(10);
/* Genau der Fall, fuer den "hold" existiert: es bleibt dunkel. */
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_WW]);
}
static void test_terminated_bit_ends_immediately(void)
{
uint8_t d[6] = { 255, 255, 0, 0, 0, 0 };
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 1000);
nl_engine_tick(10);
feed(100, 2, true, 6, 1010);
nl_engine_tick(10);
TEST_ASSERT_FALSE(nl_dmx_active());
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_R]);
}
static void test_release_hands_control_back_in_both_modes(void)
{
uint8_t d[6] = { 255, 255, 0, 0, 0, 0 };
int mode;
for (mode = 0; mode < 2; mode++) {
start("gu10_rgb", 1, mode == 1);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 6, 1000);
nl_engine_tick(10);
TEST_ASSERT_TRUE(nl_dmx_active());
nl_dmx_release();
nl_engine_tick(10);
TEST_ASSERT_FALSE(nl_dmx_active());
TEST_ASSERT_EQUAL_UINT8(0, last_ch[NL_CH_R]);
}
}
static void test_configure_during_stream_takes_effect(void)
{
uint8_t d[9] = { 255, 1, 0, 0, 0, 0, 0, 0, 0 };
nl_dmx_cfg_t dc;
start("gu10_rgb", 1, false);
memcpy(slot_data, d, sizeof d);
feed(100, 1, false, 9, 1000);
nl_engine_tick(10);
TEST_ASSERT_EQUAL_UINT8(1, last_ch[NL_CH_R]);
slot_data[3] = 255; /* Slot 4 = Dimmer, wenn start_slot 4 ist */
slot_data[4] = 222; /* Slot 5 = R, wenn start_slot 4 ist */
dc.start_slot = 4;
dc.hold = false;
dc.release_fade_ms = 0;
nl_dmx_configure(&dc);
feed(100, 2, false, 9, 1010);
nl_engine_tick(10);
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);
RUN_TEST(test_board_without_rgb_uses_three_slots);
RUN_TEST(test_start_slot_offset);
RUN_TEST(test_packet_too_short_for_our_slots_is_dropped);
RUN_TEST(test_stale_sequence_is_dropped);
RUN_TEST(test_a_packet_exactly_twenty_behind_is_accepted);
RUN_TEST(test_higher_priority_source_takes_over_despite_stale_looking_sequence);
RUN_TEST(test_lower_priority_is_ignored_while_streaming);
RUN_TEST(test_timeout_release_returns_to_engine);
RUN_TEST(test_timeout_hold_keeps_last_values);
RUN_TEST(test_hold_keeps_darkness);
RUN_TEST(test_terminated_bit_ends_immediately);
RUN_TEST(test_release_hands_control_back_in_both_modes);
RUN_TEST(test_configure_during_stream_takes_effect);
return UNITY_END();
}