#include #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; /* Faellt nur aus, wenn der Task gar nicht erst entsteht. app_main fragt das ab: ohne Task * bewegt sich nl_sacn_beat nie, und eine Wachhund-Bedingung, die trotzdem darauf besteht, * wuerde die Lampe im 20-s-Takt endlos zuruecksetzen - ein Zusatzdienst darf sie nicht * unbrauchbar machen. */ static volatile bool s_running = true; /* Ergebnis des letzten Multicast-Beitritts; ueber system/info sichtbar. */ static volatile bool s_joined; 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. * * Das Ergebnis wird aber festgehalten und ueber system/info sichtbar gemacht. Bei der * Abnahme am 2026-09-20 kam Multicast nicht an, waehrend Unicast lief - und weil der * Rueckgabewert damals verworfen wurde, liess sich nicht unterscheiden, ob die Lampe * nicht beitritt oder das WLAN die Gruppe nicht weiterreicht. Genau diese Frage * beantwortet das Feld. */ 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); s_joined = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof mreq) == 0; printf("sacn: universe %u, multicast join %s\r\n", (unsigned)universe, s_joined ? "ok" : "FEHLGESCHLAGEN"); 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) { if (tls_os_task_create(NULL, "nl_sacn", sacn_task, NULL, (u8 *)s_stack, sizeof(s_stack), NL_SACN_PRIO, 0) != TLS_OS_SUCCESS) { s_running = false; printf("sacn: task create failed - receiver off\r\n"); } } bool nl_sacn_running(void) { return s_running; } bool nl_sacn_joined(void) { return s_joined; }