Ein toter Empfangs-Task war von aussen nicht von "es sendet gerade niemand" zu
unterscheiden: beides meldete nur active=false. "multicast" half nicht weiter -
es zeigt den gemerkten Erfolg des LETZTEN Beitritts und bleibt true, auch wenn
der Socket laengst zu ist.
Genau dieser Fall trat am Montana-Black-Shield auf (2026-09-26, direkt nach
Migration und Onboarding) und hat eine halbe Stunde Suche gekostet, an deren
Ende die Ursache immer noch offen war. nl_sacn_running() gab es da schon, es
wurde nur nirgends ausgeliefert.
Zwei Tests: der bestehende prueft running=true mit, ein neuer haelt den toten
Task fest.
Pixelorientierte Werkzeuge wie LedFx schreiben drei Kanaele je Pixel; mit
dem Dimmer voran landet deren erstes Pixel auf Dimmer, R, G. rgb ist damit
die Belegung, die out of the box das Richtige tut - der Dimmer bleibt fuer
DMX-Pulte eine Umschaltung entfernt.
LedFx und Verwandte denken in RGB-Pixeln: Ihr e131-Geraet schreibt je Pixel
genau drei Kanaele. Unsere Belegung beginnt mit einem Master-Dimmer, also landet
deren erstes Pixel auf Dimmer, R, G - ein rotes Effektbild (255,0,0) hiesse bei
uns "Dimmer 255, Rot 0, Gruen 0" und damit Dunkelheit. Von Hand eintragen liess
sich eine Lampe schon, sie zeigte aber Unsinn.
Der Dimmer bleibt fuer DMX-Pulte richtig, deshalb eine zweite Belegung statt
einer anderen: layout = "dimmer" (Vorgabe, unveraendert) oder "rgb" (R,G,B, drei
Slots auf jedem Board). Im rgb-Layout ist eine Lampe genau ein Pixel.
Ein Board ohne Farbkanaele bekommt trotzdem ein Farbtripel. Genommen wird der
groesste der drei Werte auf beide Weisskanaele: die Lampe folgt sichtbar der
Helligkeit des Effekts, ohne dass ein Farbmodell in den DMX-Pfad kommt - genau
das wird dort sonst bewusst vermieden.
Die Obergrenze fuer start_slot haengt jetzt am Layout (512 - slots + 1). Ein
Wechsel, der einen bisher zulaessigen start_slot unzulaessig macht, wird
abgelehnt statt still verschoben: eine Lampe, die nach einer Einstellung
woanders im Universe liegt als angegeben, findet niemand wieder.
Das neue Settings-Feld beginnt bei Offset 216, dem sizeof davor, und ist durch
eine eigene Pruefung zur Uebersetzungszeit und einen Migrationstest abgesichert -
derselbe Fallstrick, der beim Schlussreview fuer sacn_enabled gefunden wurde.
Bei der Abnahme kam Multicast nicht an, waehrend Unicast lief. Der Rueckgabewert
von IP_ADD_MEMBERSHIP wurde bis jetzt verworfen - absichtlich, weil ein
gescheiterter Beitritt den Unicast-Empfang nicht verhindern soll. Die Folge war
aber, dass sich die entscheidende Frage nicht beantworten liess: tritt die Lampe
nicht bei, oder reicht das WLAN die Gruppe nicht weiter?
system/info traegt deshalb jetzt "sacn.multicast", und der Beitritt schreibt beim
Socket-Aufbau eine Zeile auf die Konsole. Am Verhalten aendert sich nichts: ein
Fehlschlag bleibt folgenlos fuer Unicast.
Zwei neue Einstellungen, beide wie fade_in/fade_out ueber
POST /api/v1/device/state les- und schreibbar und sofort wirksam:
- transition_default (Voreinstellung 300 ms) fuellt die Transition fuer
Farb-, Weiss- und Helligkeitswechsel bei eingeschalteter Lampe, wenn der
Aufruf kein eigenes "transition" mitbringt. Beim Ein- und Ausschalten
behalten fade_in/fade_out weiterhin Vorrang.
- wake_on_change (Voreinstellung aus) schaltet die Lampe bei einer Farb-,
Weiss- oder Helligkeitsaenderung ein, wenn sie aus war. Ein
ausdrueckliches "on": false im selben Aufruf gewinnt immer.
Beide Felder haengen am Ende von nl_settings_t, damit bestehende Lampen
ueber die Laengentoleranz von read_rec() ihre echten Voreinstellungen
bekommen statt Nullen. nl_ctrl_set_fades() heisst jetzt
nl_ctrl_set_options().
Dazu ein Treiberfehler, der erst an der migrierten GU10 RGB auffiel: der
WY-Zweig von nl_sm2135eh_set() schrieb nur Header und WY-Daten, nie die
RGB-Datenregister. Der Chip behielt also die zuletzt gesetzte Farbe, und
bei einem direkten Wechsel Farbe -> Weiss (ohne Zwischenschritt ueber
dunkel) schaelte sie sich beim Ausblenden wieder frei. Der Header sendet
jetzt die - in Weiss immer nullwertigen - RGB-Daten mit.
Die Bruno-Collection zeigt Transition jetzt zusammen mit einem Farb- und
einem Weisswechsel im selben Aufruf und hat einen Request fuer die
beiden neuen Einstellungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Antwort gab bisher nur die gespeicherte SSID aus. Wurde eine Lampe nie
per REST onboardet, ist die leer, und die Lampe laeuft in Wirklichkeit auf
den einkompilierten NINLIGHT_WIFI_SSID/-PASSWORD (siehe app_main.c) -
GET system/settings suggerierte damit "kein WLAN konfiguriert", obwohl die
Lampe zuverlaessig verbunden ist, und widersprach scheinbar system/info.
Additiv, ohne die Bedeutung von "ssid" zu aendern: wifi.source ist jetzt
"stored" oder "compiled". Spec (§7.2) und bruno/ninlight.yml dokumentieren
das Feld, ein neuer Hosttest deckt den compiled-Fall ab.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ein zweiter fw_update waehrend eines laufenden Downloads setzte s_error
auf "busy". Da ota_task() nur bei leerem Fehlerfeld auf "download"
zurueckfaellt, wurde ein spaeter gescheiterter Transfer danach als
"busy" gemeldet statt als "download". Die Abweisung hat kein eigenes
Ergebnis - sie wird der API mit 409 gemeldet, last_ota_error bleibt beim
Ergebnis des laufenden bzw. letzten Updates.
Dazu die Doku auf das am EVK gemessene Verhalten korrigiert: stirbt die
Gegenstelle mitten im Download, haengt der Lesevorgang rund sieben
Minuten in einem TCP-Timeout und bricht ohne Range-Resume ab.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Das Feld lieferte ohnehin nur den festen String "unknown" - kein SDK-Aufruf
lieferte je einen echten Reset-Grund. README/hardware-checklist erwähnten
es nicht.
M2: WLAN-Passwort muss leer (offenes Netz) oder 8..64 Zeichen lang sein
(400 "invalid password" bei 1..7 Zeichen). CMake prüft NINLIGHT_WIFI_PASSWORD
ebenso (0 oder 8..63 Zeichen) und der Zeichen-Blacklist-Check bekommt `;`
dazu.
M4: Ein Authorization-Wert, der länger als NL_TOKEN_LEN-1 (64) ist, wird
von nl_http_parse() nicht mehr stillschweigend abgeschnitten, sondern setzt
`auth_too_long` in nl_http_request_t; nl_api_handle() behandelt eine solche
Anfrage als unauthentifiziert (außer bei offenem Zugriff), damit ein
abgeschnittener Wert nie zufällig auf ein kürzeres Token passt.
M5: get_int() prüfte die (long)-Cast-Gleichheit vor dem Bereichscheck -
bei sehr großen oder NaN-Werten ist der Cast von double nach long
undefiniertes Verhalten. Bereichs- und NaN-Check laufen jetzt vor dem Cast.
T1: sdk/Platform/Drivers/internalflash/wm_internal_fls.c: tls_fls_init()
ignorierte den Rückgabewert von tls_os_sem_create(&s_fls2_lock, 1) - jetzt
eine TLS_DBGPRT_ERR-Meldung, falls das fehlschlägt (dokumentiert in
sdk/PATCHES.md).
Tests: test_api.c (Passwortlängen, auth_too_long, 1e300-Brightness),
test_http.c (auth_too_long wird gesetzt). Für M5 zeigt der Host-Test kein
sichtbares RED, weil der (double)->(long)-Cast auf dieser Plattform/diesem
Compiler keinen Crash auslöst (implementation-defined statt UB-Crash) - der
Fix wird trotzdem angewendet, da er auf dem Zielcompiler (arm-none-eabi 4.9)
nicht garantiert sicher ist.
C1: Ein POST an /api/v1/device/state mit tief verschachteltem JSON (z.B.
20 Ebenen "[[[[...") lässt cJSONs rekursiven Parser den 6-KB-Stack des
HTTP-Tasks überlaufen - CJSON_NESTING_LIMIT war mit 1000 viel zu hoch und
FreeRTOS' Stack-Overflow-Checking ist aus. Begrenzt auf 8 Ebenen, sowohl
für die Firmware (third_party/cJSON/cJSON.c) als auch für die
Host-Tests (nl_cjson), damit beide dasselbe Verhalten zeigen.
Test: test_post_validation deckt jetzt einen Body mit 20 verschachtelten
Arrays ab (400 "invalid json").