fix(sacn): sequence handling on priority change and start_slot clamp

This commit is contained in:
Michael Sauer
2026-09-20 00:16:39 +02:00
parent fae22f8c64
commit 882cb55297
2 changed files with 60 additions and 5 deletions
+18 -2
View File
@@ -27,7 +27,15 @@ void nl_dmx_init(const nl_board_t *board)
s_last_ms = 0; s_last_ms = 0;
} }
void nl_dmx_configure(const nl_dmx_cfg_t *cfg) { s_cfg = *cfg; } void nl_dmx_configure(const nl_dmx_cfg_t *cfg)
{
s_cfg = *cfg;
/* Sicherheitsnetz: start_slot=0 wuerde unten vor den Puffer lesen. Die REST-Schicht
* validiert 1..512, aber dieses Modul soll dafuer nicht auf einen Aufrufer angewiesen
* sein, den es nicht sehen kann. */
if (s_cfg.start_slot < 1u)
s_cfg.start_slot = 1u;
}
bool nl_dmx_active(void) { return s_override; } bool nl_dmx_active(void) { return s_override; }
@@ -73,11 +81,19 @@ void nl_dmx_feed(const nl_e131_packet_t *p, uint32_t now_ms)
if (s_streaming) { if (s_streaming) {
if (p->priority < s_prio) if (p->priority < s_prio)
return; return;
{ if (p->priority == s_prio) {
/* E1.31-2018 Section 6.7.2: verwerfen, wenn die Differenz <= 0 und > -20 ist.
* Das Fenster ist bei -20 exklusiv - eine Sequenznummer genau 20 hinter der
* zuletzt angenommenen gilt bereits wieder als neu und wird angenommen. Nicht
* "korrigieren": ein Reviewer hat genau das schon einmal versucht. */
int8_t d = (int8_t)(p->sequence - s_seq); int8_t d = (int8_t)(p->sequence - s_seq);
if (d <= 0 && d > -NL_DMX_SEQ_WINDOW) if (d <= 0 && d > -NL_DMX_SEQ_WINDOW)
return; /* veraltet oder Wiederholung */ return; /* veraltet oder Wiederholung */
} }
/* p->priority > s_prio: eine neue, hoeherpriore Quelle loest die laufende ab. Sie
* fuehrt ihre eigene Sequenzzaehlung - ein Abgleich gegen s_seq der abgeloesten
* Quelle waere Zufall und koennte die neue Quelle bis zu NL_DMX_SEQ_WINDOW Pakete
* lang verwerfen. Also: wie das erste Paket eines frischen Streams behandeln. */
} }
s_prio = p->priority; s_prio = p->priority;
s_seq = p->sequence; s_seq = p->sequence;
+42 -3
View File
@@ -5,13 +5,11 @@
#include "light/nl_engine.h" #include "light/nl_engine.h"
static uint8_t last_ch[NL_CH_COUNT]; static uint8_t last_ch[NL_CH_COUNT];
static int out_calls;
static void sink(const uint8_t ch[NL_CH_COUNT], void *ctx) static void sink(const uint8_t ch[NL_CH_COUNT], void *ctx)
{ {
(void)ctx; (void)ctx;
memcpy(last_ch, ch, sizeof last_ch); memcpy(last_ch, ch, sizeof last_ch);
out_calls++;
} }
static uint8_t slot_data[16]; static uint8_t slot_data[16];
@@ -48,7 +46,6 @@ static void start(const char *board_id, uint16_t start_slot, bool hold)
dc.hold = hold; dc.hold = hold;
dc.release_fade_ms = 0; dc.release_fade_ms = 0;
nl_dmx_configure(&dc); nl_dmx_configure(&dc);
out_calls = 0;
} }
void setUp(void) void setUp(void)
@@ -149,6 +146,46 @@ static void test_stale_sequence_is_dropped(void)
TEST_ASSERT_EQUAL_UINT8(99, last_ch[NL_CH_R]); 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) static void test_lower_priority_is_ignored_while_streaming(void)
{ {
uint8_t high[6] = { 255, 10, 0, 0, 0, 0 }; uint8_t high[6] = { 255, 10, 0, 0, 0, 0 };
@@ -276,6 +313,8 @@ int main(void)
RUN_TEST(test_start_slot_offset); RUN_TEST(test_start_slot_offset);
RUN_TEST(test_packet_too_short_for_our_slots_is_dropped); RUN_TEST(test_packet_too_short_for_our_slots_is_dropped);
RUN_TEST(test_stale_sequence_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_lower_priority_is_ignored_while_streaming);
RUN_TEST(test_timeout_release_returns_to_engine); RUN_TEST(test_timeout_release_returns_to_engine);
RUN_TEST(test_timeout_hold_keeps_last_values); RUN_TEST(test_timeout_hold_keeps_last_values);