feat(sacn): UDP-Empfangs-Task und Verdrahtung in App und API

This commit is contained in:
Michael Sauer
2026-09-20 00:51:33 +02:00
parent 545ca30699
commit c547a9f2b2
5 changed files with 216 additions and 7 deletions
+1
View File
@@ -97,6 +97,7 @@ set(NL_FW_SOURCES
src/net/nl_recovery.c src/net/nl_recovery.c
src/net/nl_mdns.c src/net/nl_mdns.c
src/net/nl_http_server.c src/net/nl_http_server.c
src/net/nl_sacn.c
src/sys/nl_app_api.c src/sys/nl_app_api.c
third_party/cJSON/cJSON.c) third_party/cJSON/cJSON.c)
+24 -6
View File
@@ -13,10 +13,12 @@
#include "drivers/nl_led.h" #include "drivers/nl_led.h"
#include "light/nl_boot_state.h" #include "light/nl_boot_state.h"
#include "light/nl_control.h" #include "light/nl_control.h"
#include "light/nl_dmx.h"
#include "light/nl_engine.h" #include "light/nl_engine.h"
#include "net/nl_http_server.h" #include "net/nl_http_server.h"
#include "net/nl_mdns.h" #include "net/nl_mdns.h"
#include "net/nl_recovery.h" #include "net/nl_recovery.h"
#include "net/nl_sacn.h"
#include "net/nl_wifi.h" #include "net/nl_wifi.h"
#include "sys/nl_app.h" #include "sys/nl_app.h"
#include "sys/nl_app_api.h" #include "sys/nl_app_api.h"
@@ -147,6 +149,7 @@ static void light_task(void *arg)
now = nl_app_now_ms(); now = nl_app_now_ms();
nl_app_lock(); nl_app_lock();
nl_engine_tick(now - last); nl_engine_tick(now - last);
nl_dmx_tick(now);
nl_app_unlock(); nl_app_unlock();
last = now; last = now;
s_light_beat++; s_light_beat++;
@@ -160,9 +163,9 @@ static void app_task(void *arg)
nl_engine_cfg_t cfg; nl_engine_cfg_t cfg;
nl_light_state_t last, initial, pending; nl_light_state_t last, initial, pending;
bool have_last, after_ota; 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; 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; (void)arg;
/* I2: the 5 s of spec 3.4 step 7 / 5.5 count from power-up, not from "after the recovery /* 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, nl_ctrl_init(board, s_settings.fade_in_ms, s_settings.fade_out_ms, s_settings.transition_ms,
s_settings.wake_on_change != 0); s_settings.wake_on_change != 0);
nl_ctrl_boot(&initial); nl_ctrl_boot(&initial);
nl_dmx_init(board);
nl_app_unlock(); 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); 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()); 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; light_seen = s_light_beat;
net_seen = nl_net_beat; net_seen = nl_net_beat;
http_seen = nl_http_beat; http_seen = nl_http_beat;
sacn_seen = nl_sacn_beat;
for (;;) { for (;;) {
uint32_t now; uint32_t now;
bool due; 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 /* Feed the watchdog only once the light, network, HTTP and sACN tasks have each
* progress since the last feed - a single stuck task (e.g. nl_app_lock() held * 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. */ * forever) must still starve the watchdog, not just the light task. */
if (s_light_beat != light_seen) { if (s_light_beat != light_seen) {
light_seen = s_light_beat; light_seen = s_light_beat;
@@ -277,8 +291,12 @@ static void app_task(void *arg)
http_seen = nl_http_beat; http_seen = nl_http_beat;
http_alive = true; http_alive = true;
} }
if (light_alive && net_alive && http_alive) { if (nl_sacn_beat != sacn_seen) {
light_alive = net_alive = http_alive = false; 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(); tls_watchdog_clr();
} }
} }
+158
View File
@@ -0,0 +1,158 @@
#include <string.h>
#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.<hi>.<lo> 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);
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef NL_SACN_H
#define NL_SACN_H
#include <stdint.h>
#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
+16 -1
View File
@@ -5,9 +5,11 @@
#include "cJSON.h" #include "cJSON.h"
#include "light/nl_control.h" #include "light/nl_control.h"
#include "light/nl_dmx.h"
#include "light/nl_engine.h" #include "light/nl_engine.h"
#include "net/nl_mdns.h" #include "net/nl_mdns.h"
#include "net/nl_names.h" #include "net/nl_names.h"
#include "net/nl_sacn.h"
#include "net/nl_wifi.h" #include "net/nl_wifi.h"
#include "sys/nl_app.h" #include "sys/nl_app.h"
#include "sys/nl_app_api.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_ctrl_err_t e;
nl_app_lock(); 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()); e = nl_ctrl_apply(c, nl_app_now_ms());
nl_app_unlock(); nl_app_unlock();
return e; return e;
@@ -46,8 +51,10 @@ static void get_settings(nl_settings_t *out)
static int save_settings(const nl_settings_t *s) static int save_settings(const nl_settings_t *s)
{ {
nl_settings_t *cur; 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]; char name[NL_NAME_LEN];
bool name_changed; bool name_changed;
int r;
nl_app_lock(); nl_app_lock();
cur = nl_app_settings(); 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); 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); memcpy(name, cur->name, sizeof name);
applied = *cur;
nl_app_unlock(); nl_app_unlock();
if (name_changed) if (name_changed)
nl_mdns_set_name(name); 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); } 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_names_mac_str(mac, out->mac);
nl_app_lock(); nl_app_lock();
memcpy(out->name, nl_app_settings()->name, sizeof out->name); 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_app_unlock();
nl_wifi_status(&w); nl_wifi_status(&w);
out->wifi_mode = w.mode; out->wifi_mode = w.mode;