feat(sacn): Uebernahme-Schicht mit Timeout, Prioritaet und Sequenz

This commit is contained in:
Michael Sauer
2026-09-20 00:08:49 +02:00
parent 6be0473f18
commit fae22f8c64
5 changed files with 437 additions and 0 deletions
+1
View File
@@ -74,6 +74,7 @@ set(NL_FW_SOURCES
src/board/nl_board.c
src/light/nl_color.c
src/light/nl_engine.c
src/light/nl_dmx.c
src/light/nl_control.c
src/light/nl_boot_state.c
src/config/nl_settings.c
+115
View File
@@ -0,0 +1,115 @@
#include "light/nl_dmx.h"
#include <string.h>
#include "light/nl_engine.h"
static const nl_board_t *s_board;
static uint8_t s_slots; /* 3 oder 6 */
static nl_dmx_cfg_t s_cfg;
static bool s_streaming; /* es fliessen Pakete */
static bool s_override; /* die Engine haelt unsere Kanaele */
static uint8_t s_prio;
static uint8_t s_seq;
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_streaming = false;
s_override = false;
s_prio = 0;
s_seq = 0;
s_last_ms = 0;
}
void nl_dmx_configure(const nl_dmx_cfg_t *cfg) { s_cfg = *cfg; }
bool nl_dmx_active(void) { return s_override; }
void nl_dmx_release(void)
{
if (s_override)
nl_engine_override_clear(s_cfg.release_fade_ms);
s_override = false;
s_streaming = false;
s_prio = 0;
}
/* Der Strom ist zu Ende - entweder durch das terminated-Bit oder durch Timeout. Im
* "release"-Modus geben wir den Override frei und blenden zurueck zum Zustand von
* nl_ctrl; im "hold"-Modus bleiben die zuletzt gesetzten Kanalwerte einfach stehen. */
static void stream_ended(void)
{
s_streaming = false;
s_prio = 0;
if (!s_cfg.hold && s_override) {
nl_engine_override_clear(s_cfg.release_fade_ms);
s_override = false;
}
}
static uint8_t scale(uint8_t v, uint8_t dim)
{
return (uint8_t)(((uint16_t)v * (uint16_t)dim + 127u) / 255u);
}
void nl_dmx_feed(const nl_e131_packet_t *p, uint32_t now_ms)
{
const uint8_t *s;
uint8_t ch[NL_CH_COUNT];
uint8_t dim;
if (s_board == NULL)
return;
/* Reicht das Paket bis zu unserem letzten Slot? Ein halb belegter Satz ergaebe auf den
* restlichen Kanaelen willkuerliche Werte - also gar nicht erst anwenden. */
if ((uint32_t)s_cfg.start_slot + s_slots - 1u > (uint32_t)p->slot_count)
return;
if (s_streaming) {
if (p->priority < s_prio)
return;
{
int8_t d = (int8_t)(p->sequence - s_seq);
if (d <= 0 && d > -NL_DMX_SEQ_WINDOW)
return; /* veraltet oder Wiederholung */
}
}
s_prio = p->priority;
s_seq = p->sequence;
s_last_ms = now_ms;
if (p->terminated) { /* die Quelle sagt selbst, dass sie fertig ist */
stream_ended();
return;
}
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);
} else {
ch[NL_CH_CW] = scale(s[1], dim);
ch[NL_CH_WW] = scale(s[2], dim);
}
nl_engine_override(ch);
s_override = true;
}
void nl_dmx_tick(uint32_t now_ms)
{
if (!s_streaming)
return;
if (now_ms - s_last_ms >= NL_DMX_TIMEOUT_MS)
stream_ended();
}
+32
View File
@@ -0,0 +1,32 @@
#ifndef NL_DMX_H
#define NL_DMX_H
#include <stdbool.h>
#include <stdint.h>
#include "board/nl_board.h"
#include "net/nl_e131.h"
#define NL_DMX_TIMEOUT_MS 2500u /* E1.31, Abschnitt 6.7.1 */
#define NL_DMX_SEQ_WINDOW 20 /* E1.31, Abschnitt 6.7.2 */
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) */
} nl_dmx_cfg_t;
/* Die Engine muss initialisiert sein. Nicht threadsicher: Aufrufer serialisieren. */
void nl_dmx_init(const nl_board_t *board);
void nl_dmx_configure(const nl_dmx_cfg_t *cfg);
/* Ein vom Parser angenommenes Paket. Prueft Sequenz, Prioritaet und Slotbereich und setzt
* bei Annahme den Override. */
void nl_dmx_feed(const nl_e131_packet_t *p, uint32_t now_ms);
/* Aus dem Licht-Task (alle ~10 ms): prueft den Timeout. */
void nl_dmx_tick(uint32_t now_ms);
/* Ein HA-/REST-Befehl holt die Kontrolle zurueck - in beiden Timeout-Modi. */
void nl_dmx_release(void);
/* true, solange die Lampe ferngesteuert ist, einschliesslich der Haltephase. */
bool nl_dmx_active(void);
#endif
+2
View File
@@ -45,6 +45,8 @@ nl_test(test_console_cmd test_console_cmd.c ${NL_SRC}/sys/nl_console_cmd.c)
nl_test(test_names test_names.c ${NL_SRC}/net/nl_names.c)
nl_test(test_http test_http.c ${NL_SRC}/net/nl_http.c)
nl_test(test_e131 test_e131.c ${NL_SRC}/net/nl_e131.c)
nl_test(test_dmx test_dmx.c ${NL_SRC}/light/nl_dmx.c ${NL_SRC}/light/nl_engine.c
${NL_SRC}/light/nl_color.c ${NL_SRC}/board/nl_board.c ${NL_SRC}/net/nl_e131.c)
nl_test(test_api test_api.c fake_api_ctx.c ${NL_SRC}/net/nl_api.c ${NL_SRC}/board/nl_board.c
${NL_SRC}/light/nl_boot_state.c ${NL_SRC}/config/nl_settings.c ${NL_SRC}/util/nl_crc.c fake_flash.c)
target_link_libraries(test_api PRIVATE nl_cjson)
+287
View File
@@ -0,0 +1,287 @@
#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 int out_calls;
static void sink(const uint8_t ch[NL_CH_COUNT], void *ctx)
{
(void)ctx;
memcpy(last_ch, ch, sizeof last_ch);
out_calls++;
}
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;
nl_dmx_configure(&dc);
out_calls = 0;
}
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_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]);
}
int main(void)
{
UNITY_BEGIN();
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_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();
}