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.
This commit is contained in:
Michael Sauer
2026-09-12 03:14:16 +02:00
parent 1822014e48
commit cf1e6a351c
7 changed files with 119 additions and 19 deletions
+6
View File
@@ -28,4 +28,10 @@ void nl_app_request_factory_reset(void);
/* Stores new station credentials (from a successful onboarding test) and persists them. */
int nl_app_set_wifi_credentials(const char *ssid, const char *pass);
/* Clears the power-cycle counter ahead of the regular 5 s deadline (spec 5.5), idempotent.
* Used by the boot recovery path once it knows this boot is a recovery boot: a successful
* recovery never returns to the app task, so without this the counter would keep growing
* over recovery cycles and trigger a factory reset after NL_FACTORY_RESET_BOOTS rounds. */
void nl_app_clear_reset_counter(void);
#endif
+36 -4
View File
@@ -10,6 +10,8 @@
#include "sys/nl_ota.h"
#include "util/nl_fwup_trailer.h"
#include "nl_version.h"
/* t_http_fwup() (sdk/Src/App/OTA/wm_http_fwup.c) has no declaration in any SDK header. */
extern int t_http_fwup(char *url);
@@ -19,8 +21,10 @@ extern int t_http_fwup(char *url);
static char s_url[NL_OTA_URL_MAX];
static volatile bool s_busy;
static volatile bool s_reject_running_version; /* recovery path only, see nl_ota_start_recovery() */
static const char * volatile s_error;
static u32 s_stack[NL_OTA_STACK];
static char s_tail_version[NL_TRAILER_VERSION_LEN]; /* used by the hook, kept off its 2 KB stack */
void nl_ota_init(void)
{
@@ -42,10 +46,28 @@ bool nl_fwup_verify_tail(const uint8_t sha256[32], const uint8_t *tail, uint32_t
{
nl_trailer_err_t e = nl_trailer_verify(tail, tail_len, NINLIGHT_DEVICE_ID, sha256);
if (e != NL_TRAILER_OK)
if (e != NL_TRAILER_OK) {
s_error = nl_trailer_err_name(e);
printf("ota: trailer %s\r\n", e == NL_TRAILER_OK ? "ok" : s_error);
return e == NL_TRAILER_OK;
printf("ota: trailer %s\r\n", s_error);
return false;
}
/* C1: recovery only - the rescue station serves the same image to every lamp that boots in
* front of it, so an image that already matches the running version must be refused here;
* otherwise the lamp flashes and reboots in an endless loop. No new last_ota_error value is
* invented for this (the vocabulary is fixed in spec 8.2); the log line below is what tells
* the two cases apart. */
if (s_reject_running_version) {
nl_trailer_version(tail, s_tail_version);
if (strcmp(s_tail_version, NINLIGHT_FW_VERSION) == 0) {
printf("ota: recovery image already carries the running version %s, not flashing\r\n",
s_tail_version);
s_error = "trailer";
return false;
}
printf("ota: recovery image version %s (running %s)\r\n", s_tail_version, NINLIGHT_FW_VERSION);
}
printf("ota: trailer ok\r\n");
return true;
}
/* Logs the URL without any "user:pass@" userinfo, so credentials embedded in the URL never
@@ -83,7 +105,7 @@ static void ota_task(void *arg)
tls_os_task_del(NL_OTA_PRIO, NULL);
}
int nl_ota_start(const char *url)
static int ota_start(const char *url, bool reject_running_version)
{
size_t n;
@@ -98,6 +120,7 @@ int nl_ota_start(const char *url)
}
s_busy = true;
s_error = NULL;
s_reject_running_version = reject_running_version;
memcpy(s_url, url, n + 1);
/* Persist the open light state and mark the pending update before the flash gets busy. */
@@ -122,8 +145,17 @@ int nl_ota_start(const char *url)
nl_app_unlock();
(void)nl_app_save_settings();
s_busy = false;
/* Minor (final review): no value in the fixed vocabulary of spec 8.2 describes "could not
* create the download task" (out of RAM/TCBs), and "download" would claim a transfer that
* never happened. `busy` stays the closest fit - the chip really is too busy to start one -
* and this log line is what distinguishes it from "an update is already running". */
printf("ota: cannot create download task (out of resources)\r\n");
s_error = "busy";
return -1;
}
return 0;
}
int nl_ota_start(const char *url) { return ota_start(url, false); }
int nl_ota_start_recovery(const char *url) { return ota_start(url, true); }
+5
View File
@@ -6,6 +6,11 @@
void nl_ota_init(void);
/* Starts a download task for an http:// URL. 0 = started, -1 = busy or invalid. */
int nl_ota_start(const char *url);
/* Same, but for the boot recovery path (spec 8.3): an image whose trailer carries the running
* firmware version is rejected instead of flashed. The rescue station stays up while a whole
* batch of lamps migrates, so without this check every boot would re-flash the same image
* forever. A deliberate re-flash of the same version stays possible over the REST API. */
int nl_ota_start_recovery(const char *url);
bool nl_ota_busy(void);
/* NULL, or one of: download, crc, trailer, device_mismatch, hash, busy */
const char *nl_ota_last_error(void);