Die Lampe trat seit jeher der falschen Gruppe bei. Aus Universe 1 wurde 239.255.239.255 statt 239.255.0.1, und damit lauschte sie auf einer Adresse, auf die niemand sendet: Multicast kam nie an, Unicast lief, und der Beitritt meldete trotzdem Erfolg - er gelang ja, nur eben fuer die falsche Gruppe. Ursache ist ein nicht geklammertes Makro im SDK: #define htonl(n) (((n & 0xff) << 24) | ((n & 0xff00) << 8) | ...) Stand die Adressrechnung direkt im Aufruf, traf die Maske nach der Textersetzung nur den letzten Term, weil & staerker bindet als |. Nachgerechnet ergibt das fuer Universe 1 exakt die 0xefffefff, die am EVK auf der seriellen Konsole stand. Die Rechnung liegt jetzt in nl_e131_group_addr() - im reinen, host-testbaren Teil, mit Testfaellen fuer beide Byte-Haelften. Der Aufrufer reicht eine Variable an htonl(), keinen Ausdruck mehr. Gefunden am EVK ueber die serielle Konsole, nachdem vier Netz-Konfigurationen und fuenf andere Theorien nichts ergeben hatten.
200 lines
7.1 KiB
C
200 lines
7.1 KiB
C
#include <stdio.h>
|
|
#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;
|
|
|
|
/* 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;
|
|
c.rgb_layout = s->sacn_layout != 0;
|
|
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;
|
|
uint32_t group;
|
|
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.
|
|
*
|
|
* 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);
|
|
/* Die Gruppenadresse wird ZUERST in eine Variable gerechnet und erst dann umgedreht.
|
|
* Das ist kein Schoenheitsfehler: htonl() ist in diesem SDK ein Makro, das sein
|
|
* Argument NICHT klammert (sdk/Include/Net/wm_sockets2.0.3.h):
|
|
*
|
|
* #define htonl(n) (((n & 0xff) << 24) | ((n & 0xff00) << 8) | ...)
|
|
*
|
|
* Ein Ausdruck mit | wird dabei zerrissen, weil & staerker bindet als | - die Maske
|
|
* trifft nur den letzten Term. Frueher stand der ganze Ausdruck direkt im Aufruf, und
|
|
* fuer Universe 1 kam 239.255.239.255 statt 239.255.0.1 heraus. Die Lampe trat also
|
|
* sauber einer Gruppe bei, auf die niemand sendete: Multicast kam nie an, waehrend
|
|
* Unicast lief, und der Beitritt meldete trotzdem Erfolg. Gefunden am 2026-09-21 am
|
|
* EVK ueber die serielle Konsole. */
|
|
group = nl_e131_group_addr(universe);
|
|
mreq.imr_multiaddr.s_addr = htonl(group);
|
|
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; }
|