From c547a9f2b22f9a55a7843c47430b18ead48b21fc Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Sun, 20 Sep 2026 00:51:33 +0200 Subject: [PATCH] feat(sacn): UDP-Empfangs-Task und Verdrahtung in App und API --- CMakeLists.txt | 1 + src/app_main.c | 30 ++++++-- src/net/nl_sacn.c | 158 +++++++++++++++++++++++++++++++++++++++++++ src/net/nl_sacn.h | 17 +++++ src/sys/nl_app_api.c | 17 ++++- 5 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 src/net/nl_sacn.c create mode 100644 src/net/nl_sacn.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b2831d8..47c59aa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,7 @@ set(NL_FW_SOURCES src/net/nl_recovery.c src/net/nl_mdns.c src/net/nl_http_server.c + src/net/nl_sacn.c src/sys/nl_app_api.c third_party/cJSON/cJSON.c) diff --git a/src/app_main.c b/src/app_main.c index 7b2f96f..1c935e4 100644 --- a/src/app_main.c +++ b/src/app_main.c @@ -13,10 +13,12 @@ #include "drivers/nl_led.h" #include "light/nl_boot_state.h" #include "light/nl_control.h" +#include "light/nl_dmx.h" #include "light/nl_engine.h" #include "net/nl_http_server.h" #include "net/nl_mdns.h" #include "net/nl_recovery.h" +#include "net/nl_sacn.h" #include "net/nl_wifi.h" #include "sys/nl_app.h" #include "sys/nl_app_api.h" @@ -147,6 +149,7 @@ static void light_task(void *arg) now = nl_app_now_ms(); nl_app_lock(); nl_engine_tick(now - last); + nl_dmx_tick(now); nl_app_unlock(); last = now; s_light_beat++; @@ -160,9 +163,9 @@ static void app_task(void *arg) nl_engine_cfg_t cfg; nl_light_state_t last, initial, pending; bool have_last, after_ota; - bool light_alive = false, net_alive = false, http_alive = false; + bool light_alive = false, net_alive = false, http_alive = false, sacn_alive = false; uint8_t boots; - uint32_t boot_ms, light_seen, net_seen, http_seen; + uint32_t boot_ms, light_seen, net_seen, http_seen, sacn_seen; (void)arg; /* I2: the 5 s of spec 3.4 step 7 / 5.5 count from power-up, not from "after the recovery @@ -205,6 +208,7 @@ static void app_task(void *arg) nl_ctrl_init(board, s_settings.fade_in_ms, s_settings.fade_out_ms, s_settings.transition_ms, s_settings.wake_on_change != 0); nl_ctrl_boot(&initial); + nl_dmx_init(board); nl_app_unlock(); tls_os_task_create(NULL, "nl_light", light_task, NULL, (u8 *)s_light_stack, sizeof(s_light_stack), NL_LIGHT_PRIO, 0); @@ -231,9 +235,19 @@ static void app_task(void *arg) nl_http_server_start(nl_app_api_ctx()); + { + static nl_settings_t sacn_boot; /* copy: the network task may change s_settings later */ + nl_app_lock(); + sacn_boot = s_settings; + nl_app_unlock(); + nl_sacn_apply_settings(&sacn_boot); + } + nl_sacn_start(); + light_seen = s_light_beat; net_seen = nl_net_beat; http_seen = nl_http_beat; + sacn_seen = nl_sacn_beat; for (;;) { uint32_t now; bool due; @@ -262,8 +276,8 @@ static void app_task(void *arg) } } - /* Feed the watchdog only once the light, network and HTTP tasks have each made - * progress since the last feed - a single stuck task (e.g. nl_app_lock() held + /* Feed the watchdog only once the light, network, HTTP and sACN tasks have each + * made progress since the last feed - a single stuck task (e.g. nl_app_lock() held * forever) must still starve the watchdog, not just the light task. */ if (s_light_beat != light_seen) { light_seen = s_light_beat; @@ -277,8 +291,12 @@ static void app_task(void *arg) http_seen = nl_http_beat; http_alive = true; } - if (light_alive && net_alive && http_alive) { - light_alive = net_alive = http_alive = false; + if (nl_sacn_beat != sacn_seen) { + sacn_seen = nl_sacn_beat; + sacn_alive = true; + } + if (light_alive && net_alive && http_alive && sacn_alive) { + light_alive = net_alive = http_alive = sacn_alive = false; tls_watchdog_clr(); } } diff --git a/src/net/nl_sacn.c b/src/net/nl_sacn.c new file mode 100644 index 0000000..ebb48e9 --- /dev/null +++ b/src/net/nl_sacn.c @@ -0,0 +1,158 @@ +#include + +#include "wm_include.h" +#include "wm_sockets.h" + +#include "light/nl_dmx.h" +#include "net/nl_e131.h" +#include "net/nl_sacn.h" +#include "net/nl_wifi.h" +#include "sys/nl_app.h" + +#define NL_SACN_PRIO 41 /* zwischen Licht (30) und HTTP (42) */ +#define NL_SACN_RECV_TIMEOUT_S 2 + +static u32 s_stack[512]; /* 2 KB: Parser ohne Heap, Puffer liegt statisch daneben */ +static uint8_t s_buf[NL_E131_MAX_PACKET]; + +/* Vom HTTP-Task geschrieben, vom Empfangs-Task gelesen. Jedes Feld passt in einen + * atomaren Zugriff; s_dirty wird zuletzt gesetzt und zuerst geloescht, damit der Task + * keine halb uebernommene Konfiguration sieht. */ +static volatile uint8_t s_enabled = 1; +static volatile uint16_t s_universe = 1; +static volatile bool s_dirty = true; + +volatile uint32_t nl_sacn_beat; + +void nl_sacn_apply_settings(const nl_settings_t *s) +{ + nl_dmx_cfg_t c; + + c.start_slot = s->sacn_start_slot; + c.hold = s->sacn_hold != 0; + c.release_fade_ms = s->fade_in_ms; + nl_app_lock(); + nl_dmx_configure(&c); + nl_app_unlock(); + + s_enabled = s->sacn_enabled; + s_universe = s->sacn_universe; + s_dirty = true; +} + +/* Legt den Socket an und tritt der Universe-Gruppe bei. -1 bei Fehlschlag. */ +static int open_socket(uint16_t universe, char ip_seen[16]) +{ + struct sockaddr_in addr; + struct ip_mreq mreq; + struct timeval tv; + nl_wifi_status_t w; + int on = 1; + int fd = socket(AF_INET, SOCK_DGRAM, 0); + + if (fd < 0) + return -1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on); + memset(&addr, 0, sizeof addr); + addr.sin_family = AF_INET; + addr.sin_port = htons(NL_E131_PORT); + addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (bind(fd, (struct sockaddr *)&addr, sizeof addr) != 0) { + closesocket(fd); + return -1; + } + /* Ohne Verkehr wuerde recvfrom() sonst ewig blockieren; dieser Timeout haelt den + * Herzschlag am Leben - derselbe Grund, aus dem der HTTP-Server accept() begrenzt. */ + tv.tv_sec = NL_SACN_RECV_TIMEOUT_S; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + + nl_wifi_status(&w); + strncpy(ip_seen, w.sta_ip, 15); + ip_seen[15] = '\0'; + + /* 239.255.. gemaess E1.31 Anhang A. Ein fehlgeschlagener Beitritt ist kein + * Abbruchgrund: Unicast an die Lampen-IP kommt auf demselben Socket weiterhin an. */ + memset(&mreq, 0, sizeof mreq); + mreq.imr_multiaddr.s_addr = + htonl(0xefff0000u | ((uint32_t)(universe >> 8) << 8) | (universe & 0xffu)); + mreq.imr_interface.s_addr = ip_seen[0] ? inet_addr(ip_seen) : htonl(INADDR_ANY); + setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof mreq); + return fd; +} + +static void sacn_task(void *arg) +{ + int fd = -1; + char ip_seen[16] = ""; + uint16_t bound_universe = 0; + + (void)arg; + for (;;) { + nl_wifi_status_t w; + nl_e131_packet_t p; + uint32_t started_ms; + int n; + + nl_sacn_beat++; + + if (!s_enabled) { + if (fd >= 0) { + closesocket(fd); + fd = -1; + } + s_dirty = false; + tls_os_time_delay(HZ / 2); + continue; + } + + /* Neu aufbauen, wenn die Konfiguration sich geaendert hat oder die Station eine + * andere IP bekommen hat - nach einem WLAN-Wechsel gilt die alte Mitgliedschaft + * nicht mehr, und ohne Neubeitritt kaeme nie wieder ein Multicast-Paket an. */ + nl_wifi_status(&w); + if (fd >= 0 && (s_dirty || bound_universe != s_universe || + strncmp(ip_seen, w.sta_ip, sizeof ip_seen) != 0)) { + closesocket(fd); + fd = -1; + } + if (fd < 0) { + s_dirty = false; + bound_universe = s_universe; + fd = open_socket(bound_universe, ip_seen); + if (fd < 0) { + tls_os_time_delay(HZ); /* z.B. noch kein netif: spaeter erneut versuchen */ + continue; + } + } + + started_ms = nl_app_now_ms(); + n = recvfrom(fd, s_buf, sizeof s_buf, 0, NULL, NULL); + if (n < 0) { + /* Die Zeitueberschreitung ist der Normalfall ohne Verkehr und laeuft die vollen + * zwei Sekunden. Kommt der Fehler deutlich frueher, ist der Socket hart kaputt + * (das lwIP dieser Version reicht kein errno heraus, die Laufzeit ist das + * einzige Unterscheidungsmerkmal): neu aufbauen statt heiss zu drehen. */ + if (nl_app_now_ms() - started_ms < NL_SACN_RECV_TIMEOUT_S * 1000u / 2u) { + closesocket(fd); + fd = -1; + tls_os_time_delay(HZ); + } + continue; + } + if (!nl_e131_parse(s_buf, (size_t)n, &p)) + continue; + if (p.universe != bound_universe) + continue; + + /* nl_dmx teilt sich seinen Zustand mit der Licht-Engine und ist nicht threadsicher. */ + nl_app_lock(); + nl_dmx_feed(&p, nl_app_now_ms()); + nl_app_unlock(); + } +} + +void nl_sacn_start(void) +{ + tls_os_task_create(NULL, "nl_sacn", sacn_task, NULL, (u8 *)s_stack, sizeof(s_stack), + NL_SACN_PRIO, 0); +} diff --git a/src/net/nl_sacn.h b/src/net/nl_sacn.h new file mode 100644 index 0000000..0ac3f33 --- /dev/null +++ b/src/net/nl_sacn.h @@ -0,0 +1,17 @@ +#ifndef NL_SACN_H +#define NL_SACN_H + +#include + +#include "config/nl_settings.h" + +/* Wie nl_net_beat und nl_http_beat: app_main fuettert den Watchdog erst, wenn sich jeder + * Herzschlag bewegt hat - ein haengender Empfangs-Task muss den Chip zuruecksetzen. */ +extern volatile uint32_t nl_sacn_beat; + +/* Uebernimmt enabled/universe/start_slot/hold und fade_in_ms. Darf jederzeit aufgerufen + * werden; der Task baut seinen Socket beim naechsten Durchlauf neu auf. */ +void nl_sacn_apply_settings(const nl_settings_t *s); +void nl_sacn_start(void); + +#endif diff --git a/src/sys/nl_app_api.c b/src/sys/nl_app_api.c index 1fdc7f9..c9859f2 100644 --- a/src/sys/nl_app_api.c +++ b/src/sys/nl_app_api.c @@ -5,9 +5,11 @@ #include "cJSON.h" #include "light/nl_control.h" +#include "light/nl_dmx.h" #include "light/nl_engine.h" #include "net/nl_mdns.h" #include "net/nl_names.h" +#include "net/nl_sacn.h" #include "net/nl_wifi.h" #include "sys/nl_app.h" #include "sys/nl_app_api.h" @@ -29,6 +31,9 @@ static nl_ctrl_err_t apply(const nl_light_change_t *c) { nl_ctrl_err_t e; nl_app_lock(); + /* Ein Befehl aus HA oder per curl holt die Kontrolle zurueck - auch im Modus "hold", + * in dem der Override sonst stehen bliebe. */ + nl_dmx_release(); e = nl_ctrl_apply(c, nl_app_now_ms()); nl_app_unlock(); return e; @@ -46,8 +51,10 @@ static void get_settings(nl_settings_t *out) static int save_settings(const nl_settings_t *s) { nl_settings_t *cur; + nl_settings_t applied; /* copy taken under the lock: nl_sacn_apply_settings() takes it itself */ char name[NL_NAME_LEN]; bool name_changed; + int r; nl_app_lock(); cur = nl_app_settings(); @@ -60,10 +67,14 @@ static int save_settings(const nl_settings_t *s) } nl_ctrl_set_options(cur->fade_in_ms, cur->fade_out_ms, cur->transition_ms, cur->wake_on_change != 0); memcpy(name, cur->name, sizeof name); + applied = *cur; nl_app_unlock(); if (name_changed) nl_mdns_set_name(name); - return nl_app_save_settings(); + r = nl_app_save_settings(); + if (r == 0) + nl_sacn_apply_settings(&applied); /* neues Universe/Startslot/hold sofort wirksam */ + return r; } static int wifi_configure(const char *ssid, const char *pass) { return nl_wifi_request_onboard(ssid, pass); } @@ -79,6 +90,10 @@ static void get_info(nl_sys_info_t *out) nl_names_mac_str(mac, out->mac); nl_app_lock(); memcpy(out->name, nl_app_settings()->name, sizeof out->name); + /* Gleicher Sperrabschnitt wie der Name: nl_dmx ist nicht threadsicher, und + * nl_app_lock() ist nicht rekursiv - also hier mit erledigen statt ein zweites Mal + * zu sperren. */ + out->sacn_active = nl_dmx_active(); nl_app_unlock(); nl_wifi_status(&w); out->wifi_mode = w.mode;