#include #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(); }