Commit Graph
81 Commits
Author SHA1 Message Date
Michael Sauer 1c29735eaf feat(wifi): Standalone-Zustand mit begrenztem Startpuls
open_access() fordert zusaetzlich !standalone, damit der Tokenschutz auch
waehrend eines Onboarding-Tests aus dem Standalone-Modus erhalten bleibt.
2026-10-01 23:18:43 +02:00
Michael Sauer d4b683de8f feat(settings): Felder fuer den Standalone-AP-Modus 2026-10-01 23:01:02 +02:00
Michael Sauer 543123a60d feat(api): system/info sagt, ob der sACN-Empfaenger laeuft
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.
2026-09-27 02:48:48 +02:00
Michael Sauer e10147b70f Merge remote-tracking branch 'origin/montana-black-shield' into merge-probe
* origin/montana-black-shield:
  docs(bulb-map): Montana-Black-Shield (f960) ergaenzt
  chore(tools): qconnex-Migrationswerkzeug und HA-Integration ins Repo
  feat(station): Montana-Black-Produkt-ID -> Ziel ledshield_rgb
  feat(board): ledshield_rgb — Montana Black RGB-LED-Shield (analoge RGB-Streifen)
  build: tools/podman.sh — docker.sh-Pendant für podman (gleiches Image, gleicher Mount)
  Überträgt lokale Änderungen (nl_crc8 unsigned-shift, docker.sh cd -P) in neuen Top-Level-Tree und entfernt altes ninlight-w600/
  Initial commit
2026-09-26 17:18:27 +02:00
Robin von Bülow 8294688e37 feat(board): ledshield_rgb — Montana Black RGB-LED-Shield (analoge RGB-Streifen)
Viertes Analog-Profil neben den SM2135EH-Lampen: PWM-Kanaele R=PB13 (ch1),
G=PB16 (ch2), B=PB15 (ch3) bei 800 Hz, PIN-belegt aus der Original-Firmware
(qcl_build_config_lamp.h, HARDWARE_MOBL_LED_SHIELD). Kein CW/WW-Zweig:
ct_min == ct_max ist das Erkennungszeichen fuer 'weiss = RGB-Mischung',
nl_color.c gibt in diesem Fall alle drei Farbkanalgleich.

Neues Bauziel in allen Skripten; test_release zaehlt Dateizahlen jetzt aus
TARGETS statt hart 8/16.
2026-09-25 17:26:46 +02:00
Michael Sauer 9ee1983dff feat: sACN-Belegung rgb als Vorgabe
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.
2026-09-22 23:20:12 +02:00
Michael Sauer fdded43ab9 fix(sacn): Multicast kam nie an - htonl() zerriss die Gruppenadresse
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.
2026-09-21 23:06:01 +02:00
Robin von Bülow 1c960ad5cc Überträgt lokale Änderungen (nl_crc8 unsigned-shift, docker.sh cd -P) in neuen Top-Level-Tree und entfernt altes ninlight-w600/ 2026-09-21 14:35:22 +02:00
Michael Sauer fbdf0c40ef feat(sacn): zweite Slot-Belegung "rgb" fuer pixelorientierte Werkzeuge
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.
2026-09-20 03:35:19 +02:00
Michael Sauer fd61f151d7 feat(sacn): Ergebnis des Multicast-Beitritts sichtbar machen
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.
2026-09-20 02:23:33 +02:00
Michael Sauer 9b80ab0e0d docs(light,e131): zwei Stellen benennen, die sonst wie Versehen aussehen
nl_engine_override_active() hat keinen Aufrufer im src/-Baum - die Firmware fragt
nl_dmx_active(). Die Funktion bleibt, weil die Engine-Tests sonst keinen Blick auf den
Override-Zustand haben; der Header sagt das jetzt, damit niemand nach dem Aufrufer sucht.

Der E1.31-Parser ueberliest Synchronisationsadresse und das Bit "force synchronization".
Fuer eine einzelne Lampe ist sofortige Ausgabe richtig - auf ein Sync-Paket zu warten
hiesse, bei einem Sender mit gesetzter Adresse dunkel zu bleiben. Der Kommentar nennt es
als bewusste Abweichung von E1.31-2018 Abschnitt 6.2.4.1.
2026-09-20 01:16:42 +02:00
Michael Sauer c70b6db4b8 fix(sacn): ein nicht gestarteter Empfangs-Task darf die Lampe nicht unbrauchbar machen
nl_sacn_beat ist ein Pflichtterm in der Wachhund-Bedingung. Schlug tls_os_task_create()
fehl, bewegte sich der Herzschlag nie, der Wachhund wurde nie gefuettert und die Lampe
setzte sich alle 20 s zurueck - ohne Weg hinein. Ein Zusatzdienst muss zu "kein sACN"
degradieren, nicht zu "keine Lampe".

nl_sacn_running() meldet den Fehlschlag, app_main merkt ihn sich einmal als
sacn_required, und eine Zeile auf der Konsole macht ihn sichtbar.
2026-09-20 01:16:42 +02:00
Michael Sauer 5c17c94dbc fix(console): Licht-Befehl gibt den DMX-Override frei - die Konsole ist der Rettungsweg
nl_app_api.c loest den Override vor nl_ctrl_apply() auf, die serielle Konsole nicht.
Waehrend eines Streams - im Modus "hold" dauerhaft - aenderte ein Licht-Befehl den
Zustand unter der Haube, die Lampe tat sichtbar nichts und die Konsole meldete trotzdem
"ok". Wer zur seriellen Leitung greift, hat meist genau diesen Fall: dunkel geparkt,
kein Controller da.

nl_dmx_release() liegt in der schon gehaltenen Lock-Klammer, nl_app_lock() ist nicht
rekursiv.
2026-09-20 01:16:42 +02:00
Michael Sauer 993b7c1575 fix(settings): neue Felder erst ab dem alten sizeof - sonst war sACN nach dem Update aus
Ein Datensatz aus der Firmware vor diesem Zweig ist 208 Byte lang und traegt hinter
wake_on_change (206) noch ein Fuellbyte. sacn_enabled lag auf genau diesem Byte (207),
und read_rec() kopiert hdr.length Bytes ueber die Vorgaben - jede Lampe im Feld waere
nach der OTA mit sacn_enabled == 0 hochgekommen und haette keinen Socket gebunden,
genau das Gegenteil dessen, was Spezifikation und README versprechen.

_legacy_pad schiebt das erste neue Feld auf 208; _padding schrumpft auf 1 Byte, damit
sizeof ein Vielfaches von 4 bleibt (216). nl_settings_legacy_check pinnt den Offset zur
Uebersetzungszeit fest, denn die bisherige Pruefung sah nur die Teilbarkeit durch 4 und
liess diesen Fall durch. Der Test kuerzte auf offsetof(sacn_enabled) - eine Laenge, die
es auf keiner Lampe je gab - und ging deshalb durch; er kuerzt jetzt auf die Literalzahl
208 und stellt das Fuellbyte nach.
2026-09-20 01:16:42 +02:00
Michael Sauer c547a9f2b2 feat(sacn): UDP-Empfangs-Task und Verdrahtung in App und API 2026-09-20 00:51:33 +02:00
Michael Sauer 545ca30699 fix(sacn): validate universe/start_slot via get_int(), reject fractional input 2026-09-20 00:29:47 +02:00
Michael Sauer e892b441ce feat(sacn): Endpunkt zum Einstellen und Statusfeld in system/info 2026-09-20 00:24:09 +02:00
Michael Sauer 882cb55297 fix(sacn): sequence handling on priority change and start_slot clamp 2026-09-20 00:16:39 +02:00
Michael Sauer fae22f8c64 feat(sacn): Uebernahme-Schicht mit Timeout, Prioritaet und Sequenz 2026-09-20 00:08:49 +02:00
Michael Sauer 6be0473f18 feat(sacn): Override-Zweig in der Licht-Engine 2026-09-20 00:02:16 +02:00
Michael Sauer 33d085cbed feat(sacn): Slotzahl je Board und vier Settings-Felder 2026-09-19 23:56:00 +02:00
Michael Sauer f09a51088a fix(sacn): validate universe range (1..63999), add boundary tests
Universe 0 and values >63999 are reserved per E1.31 spec and must be rejected.
Priority is deliberately not validated (compared relatively in later tasks only).

Test coverage:
- test_universe_zero_rejected: rejects universe 0
- test_universe_too_high_rejected: rejects universe 64000
- test_universe_is_read: continues to accept universe 63999
2026-09-19 23:49:47 +02:00
Michael Sauer cdbbe31831 feat(sacn): E1.31-Pakete pruefen und zerlegen 2026-09-19 23:44:55 +02:00
Michael Sauer 6f0137908c fix: SM2135EH-Weissreste beim Wechsel zurueck auf Farbe loeschen
Der RGB-Zweig von nl_sm2135eh_set() schrieb Stromregister, Kanalbyte und
die drei RGB-Werte, liess SM_ADDR_WY (0xC5) aber unangetastet. Der Chip
behielt die zuletzt gesetzte Weiss-Mischung, und beim direkten Wechsel
Weiss -> Farbe (ohne Zwischenschritt ueber dunkel) schaelte sich der
Weissanteil beim Ausblenden der Farbe wieder frei - an einer migrierten
GU10 RGB gemessen.

Das ist dieselbe Klasse Fehler wie in 00d5a4c fuer die Gegenrichtung
behoben (WY-Zweig schrieb nie die RGB-Register), nur bislang unentdeckt
in dieser Richtung. Der RGB-Zweig schreibt jetzt zusaetzlich WY mit
0/0 - ein zweiter Bus-Transfer pro Aktualisierung, symmetrisch zum
WY-Zweig, der ebenfalls zwei Transfers braucht.
2026-09-14 23:59:38 +02:00
Michael SauerandClaude Opus 5 7a5f26d717 fix(board): Farbtemperaturbereiche der Weiss-Varianten korrigieren
Die vier reinen Weiss-Lampen standen flach auf 2500-6500 K, was fuer keine
von ihnen stimmte. Nach Michaels Produktdaten:

- E27 CW/WW, E14 CW/WW, GU10 CW/WW: 2700-6500 K (vorher 2500-5000)
- G95 Vintage:                      2000-5000 K (vorher 2500-5000)

Die drei erstgenannten haben damit oben 1500 K verschenkt, die G95 unten
500 K. Die RGB-Varianten und das EVK standen schon richtig auf 2700-6500.

test_board.c prueft die Bereiche jetzt pro Board statt ueber die Faustregel
"RGB oder nicht" - die G95 ist die Ausnahme, und eine Faustregel, die eine
Ausnahme hat, ist keine. Die beiden Clamp-Tests in test_boot_state.c lesen
die Obergrenze jetzt vom Board, statt die Zahl zu wiederholen: sie pruefen
das Begrenzen, nicht die Konstante.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:28:33 +02:00
Michael SauerandClaude Opus 5 00d5a4c532 feat: Default-Transition, Weckschalter und farbfreier Ausschaltfade
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>
2026-09-13 02:56:56 +02:00
Michael SauerandClaude Opus 5 f32216d17f fix: SM2135EH beim Ausschalten in den Standby legen
Bei allen Kanaelen auf null schrieb der Treiber nur Nullen ins
Kanal-Register, liess den Chip aber aktiv - er laesst dann genug
Reststrom durch, dass die Lampe schwach weiterglimmt. An einer
migrierten GU10 RGB gemessen. Jetzt wird Bit 5 (Standby) gesetzt.

Die alte QConnex-Firmware hatte diese Logik, aber auskommentiert
(qcl_lamp.c) - das Verhalten ist von dort geerbt. Lampen an direkten
PWM-Pins (G95, CW-Varianten) sind nicht betroffen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 02:32:31 +02:00
Michael SauerandClaude Sonnet 5 2dfb1fd1c2 feat: GET system/settings zeigt Herkunft der WLAN-Zugangsdaten
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>
2026-09-13 01:29:34 +02:00
Michael SauerandClaude Sonnet 5 908fda8240 fix: OTA-Log im Recovery-Skip-Pfad nicht mehr als Fehlschlag ausgeben
Auf dem Überspring-Pfad (Recovery lehnt ein Image ab, weil es bereits die
laufende Version trägt) bleibt s_error absichtlich NULL, landete aber
trotzdem in printf("ota: failed (rc=%d, %s)\r\n", rc, s_error) - ein
Nullzeiger an %s ist undefiniertes Verhalten und wurde bislang nur zufällig
von newlib als "(null)" abgefangen. Zusätzlich widersprach die Meldung
"failed" der direkt folgenden Zeile "recovery: image already up to date,
continuing normal boot".

ota_task() unterscheidet jetzt explizit zwischen dem Skip-Fall (eigene,
ehrliche "ota: skipped"-Zeile) und dem echten Fehlschlag (bestehende
"ota: failed"-Zeile, weiterhin mit dem "download"-Fallback für ein leeres
s_error). rc bleibt reiner Diagnosewert im Log, da das SDK den Transfer in
beiden Fällen abbricht, sobald der Trailer-Hook das Image ablehnt.

Restliche Leser von s_error/nl_ota_last_error() (nl_recovery.c, nl_api.c)
waren bereits NULL-sicher und mussten nicht angepasst werden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 00:29:12 +02:00
Michael SauerandClaude Opus 5 37a5fe2ca9 fix: uebersprungenes Recovery-Image ist kein Fehler mehr
Lehnte die Recovery ein Image ab, weil es bereits die laufende Version
traegt, meldete die Firmware das als last_ota_error=trailer. Wer eine
Lampe im Feld ausliest, haette dort einen Trailer-Fehler gesehen, wo in
Wahrheit nichts passiert ist - und die Meldung in der Konsole lautete
"update failed". Das bewusste Ueberspringen hat jetzt einen eigenen
Zustand (nl_ota_skipped()), setzt keinen Fehlerwert und wird als
"image already up to date" protokolliert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 11:22:17 +02:00
Michael SauerandClaude Opus 5 5e2dff375e fix: abgewiesener fw_update verdirbt last_ota_error des laufenden Updates nicht mehr
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>
2026-09-12 10:58:09 +02:00
Michael Sauer 49d6b329b0 fix: printf statt wegkompilierter TLS_DBGPRT_ERR-Meldung bei fls_lock-Fehler
Wie beim s_fls2_lock-Fehlerpfad (T1, Plan 2) blieb auch ein Fehlschlag beim
Anlegen des primaeren fls_lock-Semaphors unsichtbar, weil TLS_DBGPRT_ERR in
diesem Build wegkompiliert ist. tls_fls_init() haette TLS_FLS_STATUS_ENOMEM
stumm zurueckgegeben. Ergaenzt außerdem den Zeitobergrenzen-Kommentar in
nl_recovery.c: die Obergrenze schuetzt nur gegen einen echten Stillstand,
nicht gegen einen langsamen, aber laufenden Download - wm_fwup.c bedient den
Watchdog bei jeder empfangenen Nachricht selbst.
2026-09-12 03:27:13 +02:00
Michael Sauer a400cc1649 fix: CRC im Settings-Record dort schreiben, wo der Leser sie erwartet
nl_settings_save() rechnete die CRC über [0, offsetof(rec_t, crc)) und legte sie
dort ab, read_rec() liest sie bei sizeof(rec_hdr_t) + hdr.length. Beide Offsets
stimmen nur überein, solange sizeof(nl_settings_t) ein Vielfaches von 4 ist
(aktuell 204). Ein zusätzliches uint16_t - in Plan 4 (Migration) sehr
wahrscheinlich - macht 206, und jeder gespeicherte Record fiele beim nächsten
Boot still durch die CRC: WLAN, Token und Name wären auf allen Lampen weg.

Geschrieben wird jetzt explizit bei NL_SETTINGS_CRC_OFF(length), also exakt an
der Leseposition; dazu eine Übersetzungszeit-Zusicherung, dass die Nutzdaten
direkt hinter dem Header beginnen (die CRC läuft auf beiden Seiten über einen
zusammenhängenden Block).

Test: test_crc_offset_matches_reader prüft die Symmetrie unabhängig von der
Strukturgröße. Gegenprobe mit einem zusätzlichen uint16_t im Struct: mit der
alten Schreibposition fallen vier Settings-Tests, mit der neuen keiner.
2026-09-12 03:14:31 +02:00
Michael Sauer 8af58d0124 fix: HTTP-Server-Heartbeat auch im Fehlerpfad ticken
Schlägt socket(), bind() oder listen() fehl, schlief die äußere Schleife eine
Sekunde und versuchte es erneut, ohne nl_http_beat zu erhöhen. Da app_task den
Watchdog nur füttert, wenn alle drei Heartbeats vorankommen, resettete der Chip
nach 20 s, bootete, scheiterte wieder - dauerhaft. Der Retry selbst ist die
Fehlerbehandlung, nicht der Reboot.
2026-09-12 03:14:31 +02:00
Michael Sauer cf1e6a351c fix: Recovery-Rettungsweg gegen Endlosschleife, Werksreset und Fremdnetz absichern
C1: nl_recovery_try() verglich nichts mit der laufenden Firmware. Vor einer
laufenden Rettungsstation - laut Spec 9 der Normalfall, solange eine Charge
migriert - lud die Lampe bei jedem Boot dasselbe Image erneut und startete neu.
nl_ota_start_recovery() setzt jetzt ein Flag, das nl_fwup_verify_tail() nur im
Recovery-Pfad auswerten lässt: Trägt der Trailer dieselbe Version wie die
laufende Firmware (nl_trailer_version(), bisher ungenutzt), wird das Image
abgelehnt und der Boot läuft normal weiter. Ein absichtliches Neuflashen
derselben Version über die REST-API bleibt möglich. Kein neuer Wert im
last_ota_error-Vokabular (Spec 8.2); unterschieden wird über die Konsole.

C1/I2: boot_ms wurde erst nach nl_recovery_try() gesetzt und der
Power-Cycle-Zähler erst 5 s danach gelöscht - bei erfolgreicher Recovery also
nie. Nach NL_FACTORY_RESET_BOOTS Runden folgte ein Werksreset. boot_ms startet
jetzt am Anfang von app_task (damit gilt wieder das in Spec 3.4/5.5 zugesagte
5-s-Fenster statt faktisch ~8 s), und nl_app_clear_reset_counter() löscht den
Zähler zusätzlich, sobald die Rettungsstation gefunden ist.

I3: tls_sys_auto_mode_run() stößt direkt nach UserMain() einen SDK-eigenen
Verbindungsversuch an; das dabei entstehende NETIF_IP_NET_UP war in
recovery_join_event() nicht vom eigenen Join zu unterscheiden, die Lampe hätte
im falschen Netz geladen. nl_wifi_disable_sdk_auto_connect() schaltet das Flag
ab und kappt eine bestehende Verbindung jetzt am Anfang von nl_recovery_try();
zusätzlich prüft nl_wifi_join_blocking() über tls_wifi_get_current_bss(), dass
die Verbindung wirklich zur erwarteten SSID gehört.
2026-09-12 03:14:16 +02:00
Michael Sauer 1822014e48 feat: Licht nach einem OTA wie bei power_on_behaviour=last wiederherstellen
Spec 8.2 verlangt, dass ein durch ein Update ausgelöster Neustart nicht wie ein
Stromausfall wirkt: Das Flag ota_pending wurde bisher nur geschrieben und beim
nächsten Boot wieder gelöscht, nl_boot_state() wendet aber weiterhin das normale
power_on_behaviour an. Eine eingeschaltete Lampe mit pob=off kam nach einem Update
aus zurück, mit pob=on umgekehrt.

nl_boot_state() bekommt dafür ein Argument after_ota; ist ein gespeicherter
Zustand vorhanden, wird er unverändert übernommen. Ohne gespeicherten Zustand
gelten weiterhin die normalen Regeln.

Hosttests: vier neue Fälle in test_boot_state.c (pob=off/on über ein OTA,
Sanitisierung bleibt aktiv, kein gespeicherter Zustand).
2026-09-12 03:13:36 +02:00
Michael Sauer 247d610fe8 fix: Recovery-Rettungsweg härten (Watchdog-Cap, Konsole, Softap-Bit, Scan-Retry) 2026-09-12 02:38:02 +02:00
Michael Sauer 8fa4af7eb5 feat: Recovery-Download beim Booten 2026-09-12 02:24:58 +02:00
Michael Sauer 37c05d2f90 fix: Fix-Runde 1 für Task 4 - Reset nur bei akzeptiertem OTA-Image, hängende Downloads/Sessions begrenzen 2026-09-12 01:56:58 +02:00
Michael Sauer aa8151c3d6 feat: OTA über HTTP mit Trailer-Prüfung 2026-09-12 01:36:57 +02:00
Michael Sauer e3be3e84c1 feat: OTA-Trailer bauen und prüfen 2026-09-12 01:16:04 +02:00
Michael Sauer c96e0442c0 fix: fehlende Felder in gekürzten Settings-Records auf echte Defaults setzen
read_rec() füllte den Puffer bisher mit memset(0) statt mit
nl_settings_defaults() vor, bevor die tatsächlich vorhandenen hdr.length
Bytes darübergeschrieben werden. Ein gekürzter Record, dessen Länge vor
fade_in_ms/fade_out_ms endet, aber token noch enthält, lief damit mit
fade_in_ms=0/fade_out_ms=0 statt der spezifizierten 500 ms - genau der Fall,
für den die Längentoleranz gedacht war. Die davor eingebaute "leerer Record"
Sonderbehandlung in nl_settings_load() (fade_in_ms==0 && fade_out_ms==0 &&
token[0]=='\0' -> volle Defaults) ist dadurch überflüssig geworden und
entfernt.

test_load_accepts_shorter_record kürzt jetzt bis vor fade_in_ms und prüft
explizit auf die echten Defaults (500/500/0) statt nur auf Felder vor dem
gekürzten Bereich. test_load_rejects_longer_record verzichtet auf die tote
CRC-Neuberechnung, da read_rec() zu lange Records schon anhand der Länge
verwirft, bevor Payload oder CRC gelesen werden.
2026-09-12 01:11:18 +02:00
Michael Sauer 8805aa764d fix: Härtungen aus dem Plan-2-Review
- wm_internal_fls.c: TLS_DBGPRT_ERR ist in diesem Build wegkompiliert, daher
  printf() für die fehlgeschlagene s_fls2_lock-Anlage; PATCHES.md korrigiert.
- nl_wifi_w600.c: der SoftAP-Backstop (Zerstören eines unerwarteten SDK-Softap)
  lief bisher innerhalb von lock()/unlock() - tls_wifi_softap_destroy() und
  clear_softap_param() greifen aber auf SDK-Zustand zu, nicht auf s_sm/die
  Credential-Puffer, die s_lock eigentlich schützt. Jetzt wird im Lock nur noch
  ein lokales Flag gesetzt, die SDK-Aufrufe laufen danach.
- clear_softap_param() liest TLS_PARAM_ID_WPROTOCOL jetzt aus SRAM (FALSE)
  statt Flash (TRUE) - das Schreiben bleibt weiterhin geflasht.
- app_main.c füttert den Watchdog erst, wenn Licht-, Netz- und HTTP-Task
  jeweils seit dem letzten Feed einen Fortschritt gemeldet haben. Dafür
  bekommen nl_wifi_w600.c (nl_net_beat) und nl_http_server.c (nl_http_beat)
  je einen volatile Heartbeat-Zähler; der HTTP-Listen-Socket bekommt ein
  SO_RCVTIMEO von 2 s auf accept(), damit er auch ohne Anfragen tickt.
2026-09-12 00:59:40 +02:00
Michael Sauer e5145626b0 fix: reset_reason aus system/info entfernen
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.
2026-09-12 00:57:08 +02:00
Michael Sauer bf8f76ecc3 build: Firmware-Version bei jedem Build neu erzeugen
NINLIGHT_FW_VERSION kam bisher aus einer configure-Zeit-Variable, die nur bei
erneutem CMake-Lauf aktualisiert wurde - nach einem Commit ohne Re-Konfiguration
zeigte Banner/system/info also eine veraltete Git-Version. cmake/write_version.cmake
schreibt build/generated/nl_version.h bei jedem Build frisch (add_custom_target ALL),
app_main.c/nl_app_api.c/nl_mdns.c inkludieren das Makro jetzt von dort statt aus
target_compile_definitions.
2026-09-12 00:56:38 +02:00
Michael Sauer 6e9fb09455 test: Settings-Format längentolerant machen, ota_pending ergänzen
read_rec() akzeptiert jetzt Records mit hdr.length <= sizeof(nl_settings_t):
CRC wird nur über die tatsächlich geschriebenen Bytes geprüft, fehlende Felder
bleiben auf 0 (Default). Ein zu langer (unbekannter) Record wird weiter
abgelehnt. Ein durch massive Kürzung "leer" gelesener Record (fade_in_ms,
fade_out_ms und token allesamt 0) fällt komplett auf die Defaults zurück.
Neues Feld ota_pending (nach fade_out_ms) für den kommenden OTA-Ablauf.
2026-09-12 00:55:20 +02:00
Michael Sauer 6754cdfb66 fix: RSSI mit Vorzeichen melden 2026-09-12 00:32:37 +02:00
Michael Sauer 2e90b8fd06 fix: kleinere Netzwerk-Härtungen
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.
2026-09-11 23:52:03 +02:00
Michael Sauer d098623121 fix: SDK-SoftAP-Parameter löschen
C2: tls_wifi_softap_create() setzt das SOFTAP-Bit (IEEE80211_MODE_AP = 4)
in TLS_PARAM_ID_WPROTOCOL und schreibt es in den Flash; tls_wifi_softap_destroy()
löscht es nie wieder. Solange das Bit gesetzt bleibt, erstellt
sys_net_status_changed() (sdk/Platform/Sys/tls_sys.c:519-525) bei jedem
NETIF_IP_NET_UP über tls_auto_reconnect_softap() erneut den Onboarding-AP
mit den SDK-eigenen (öffentlichen Default-)Parametern - unabhängig vom
Auto-Connect-Flag. Nach jedem Onboarding/Fallback käme der AP
`ninlight-<mac4>` mit dem öffentlichen Default-Passwort bei jedem
Reconnect/Reboot zurück, offen fürs LAN (SDK-NAPT).

Verifiziert in sdk/Platform/Sys/tls_sys.c, sdk/Include/Platform/wm_params.h
und sdk/Platform/Common/Params/wm_param.c:
- TLS_PARAM_ID_WPROTOCOL (10) ist ein u8, TLS_PARAM_IEEE80211_SOFTAP == 4 ==
  IEEE80211_MODE_AP (sdk/Include/WiFi/wm_wifi.h).
- tls_param_get()/tls_param_set(id, argv, to_flash) - genau die Signaturen
  aus dem Finding.
- tls_param_to_default() (tls_param_load_factory_default() + param_to_flash())
  arbeitet ausschließlich auf `struct tls_sys_param` (Wi-Fi/Netzwerk-Konfig);
  es liest/schreibt nirgends die MAC-Adresse oder RF-Kalibrierdaten - die
  liegen in einer komplett getrennten, efuse-gestützten FT_PARAM_ST-Struktur
  in sdk/Platform/Drivers/efuse/wm_efuse.c. tls_param_to_default() ist damit
  für den Werksreset sicher.

Fix (nl_wifi_w600.c):
1. clear_softap_param(): liest TLS_PARAM_ID_WPROTOCOL, löscht bei gesetztem
   IEEE80211_MODE_AP-Bit und persistiert das (to_flash=TRUE). Aufgerufen in
   nl_wifi_start() (vor allem anderen) und nach jedem tls_wifi_softap_destroy().
2. nl_wifi_start(): zerstört einen bereits vom SDK selbst erzeugten SoftAP
   (tls_wifi_softap_get_state() != WM_WIFI_DISCONNECTED), bevor unsere eigene
   State Machine startet.
3. Backstop im Netzwerk-Task: NETIF_WIFI_SOFTAP_SUCCESS, während die State
   Machine sagt, der AP solle unten sein (s_sm.ap_up == false) -> AP wieder
   zerstören und Param erneut löschen (geloggt).
4. Werksreset (nl_app_factory_reset, src/app_main.c): setzt zusätzlich
   tls_param_to_default() (siehe Verifikation oben) und dokumentiert das
   inline.

Nebenbei im selben Treiber erledigt, weil sie denselben Codepfad betreffen:
- M2: connect_station() loggt den Rückgabewert von tls_wifi_connect(), wenn
  er nicht WM_SUCCESS ist.
- M3: start_ap() merkt sich einen fehlgeschlagenen tls_wifi_softap_create()
  (`s_ap_create_pending`) und wiederholt ihn alle 5 s, solange die State
  Machine den AP oben haben will.

Nicht host-testbar (bindet SDK-Header ein); über den Firmware-Build
verifiziert (baut sauber für evk, keine neuen Warnungen).
2026-09-11 23:51:38 +02:00
Michael Sauer 5f45b9e996 fix: Gesamt-Timeout für HTTP-Anfragen
I2: SO_RCVTIMEO=5s war ein Timeout pro recv(), nicht für die gesamte
Anfrage. Ein Client, der 1 Byte alle 4,9 s schickt, konnte den einzigen
HTTP-Server-Task damit stundenlang belegen (Spec §7.2 will 5 s pro Header).

serve() rechnet jetzt eine absolute Deadline (nl_app_now_ms() + 5000 ms)
und setzt SO_RCVTIMEO vor jedem recv() auf die verbleibende Zeit (mindestens
1 ms); ist die Deadline überschritten, wird die Verbindung sofort verworfen.

Nicht host-testbar (nl_http_server.c bindet SDK-Sockets ein); über den
Firmware-Build verifiziert.
2026-09-11 23:51:11 +02:00