Files
ninlight-w600/src/app_main.c
T
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

303 lines
9.4 KiB
C

#include <stdio.h>
#include <string.h>
#include "wm_include.h"
#include "wm_watchdog.h"
#include "board/nl_board.h"
#include "config/nl_flash.h"
#include "config/nl_flash_layout.h"
#include "config/nl_reset_counter.h"
#include "config/nl_settings.h"
#include "config/nl_state_log.h"
#include "drivers/nl_led.h"
#include "light/nl_boot_state.h"
#include "light/nl_control.h"
#include "light/nl_engine.h"
#include "net/nl_http_server.h"
#include "net/nl_mdns.h"
#include "net/nl_recovery.h"
#include "net/nl_wifi.h"
#include "sys/nl_app.h"
#include "sys/nl_app_api.h"
#include "sys/nl_console.h"
#include "sys/nl_ota.h"
#include "nl_version.h"
#define NL_LIGHT_PRIO 30
#define NL_APP_PRIO 40
static tls_os_mutex_t *s_lock;
static tls_os_mutex_t *s_store_lock; /* serialises all nl_settings_* / nl_state_log_* / nl_resetcnt_* calls */
static nl_settings_t s_settings;
static volatile uint32_t s_light_beat;
static volatile bool s_factory_reset_requested;
static volatile bool s_counter_cleared;
static u32 s_app_stack[1024];
static u32 s_light_stack[512];
void nl_app_lock(void) { tls_os_mutex_acquire(s_lock, 0); }
void nl_app_unlock(void) { tls_os_mutex_release(s_lock); }
static void store_lock(void) { tls_os_mutex_acquire(s_store_lock, 0); }
static void store_unlock(void) { tls_os_mutex_release(s_store_lock); }
uint32_t nl_app_now_ms(void)
{
return (uint32_t)((uint64_t)tls_os_get_time() * 1000u / HZ);
}
nl_settings_t *nl_app_settings(void) { return &s_settings; }
int nl_app_save_settings(void)
{
static nl_settings_t copy; /* guarded by the store lock */
int r;
store_lock();
nl_app_lock();
copy = s_settings;
nl_app_unlock();
r = nl_settings_save(&copy);
store_unlock();
return r;
}
void nl_app_flush(void)
{
nl_light_state_t st;
store_lock();
nl_app_lock();
nl_ctrl_get(&st);
nl_app_unlock();
if (nl_state_log_append(&st) == 0) { /* no write when unchanged */
nl_app_lock();
nl_ctrl_persisted(&st);
nl_app_unlock();
}
store_unlock();
}
void nl_app_reboot(void)
{
printf("reboot\r\n");
nl_app_flush();
tls_sys_reset();
}
void nl_app_request_factory_reset(void) { s_factory_reset_requested = true; }
void nl_app_clear_reset_counter(void)
{
if (s_counter_cleared)
return;
store_lock();
s_counter_cleared = nl_resetcnt_clear() == 0;
store_unlock();
}
int nl_app_set_wifi_credentials(const char *ssid, const char *pass)
{
nl_app_lock();
strncpy(s_settings.wifi_ssid, ssid, NL_SSID_LEN - 1);
s_settings.wifi_ssid[NL_SSID_LEN - 1] = '\0';
strncpy(s_settings.wifi_pass, pass, NL_PASS_LEN - 1);
s_settings.wifi_pass[NL_PASS_LEN - 1] = '\0';
nl_app_unlock();
return nl_app_save_settings();
}
void nl_app_factory_reset(void)
{
printf("factory reset\r\n");
nl_app_lock();
nl_engine_blink(3);
nl_app_unlock();
tls_os_time_delay(2 * HZ); /* 3 blinks = 1.8 s, the light task keeps running */
store_lock();
nl_settings_erase();
nl_state_log_erase();
nl_resetcnt_clear();
store_unlock();
/* C2: also wipe the SDK's own Wi-Fi params (SSID/key/WPROTOCOL, incl. any leftover
* softap bit) - verified in sdk/Platform/Common/Params/wm_param.c that
* tls_param_to_default() (tls_param_load_factory_default() + param_to_flash()) only
* touches struct tls_sys_param (Wi-Fi/network config); it never touches the MAC
* address or RF calibration data, which live in a separate efuse-backed FT_PARAM_ST
* area (sdk/Platform/Drivers/efuse/wm_efuse.c) that this call never reads or writes. */
tls_param_to_default();
tls_sys_reset();
}
static void led_out(const uint8_t ch[NL_CH_COUNT], void *ctx)
{
(void)ctx;
nl_led_set(ch);
}
static void light_task(void *arg)
{
uint32_t last = nl_app_now_ms();
(void)arg;
for (;;) {
uint32_t now;
tls_os_time_delay(HZ / 100 ? HZ / 100 : 1); /* ~10 ms */
now = nl_app_now_ms();
nl_app_lock();
nl_engine_tick(now - last);
nl_app_unlock();
last = now;
s_light_beat++;
}
}
static void app_task(void *arg)
{
const nl_board_t *board = nl_board_get();
uint32_t base = nl_flash_region_base();
nl_engine_cfg_t cfg;
nl_light_state_t last, initial, pending;
bool have_last, after_ota;
bool light_alive = false, net_alive = false, http_alive = false;
uint8_t boots;
uint32_t boot_ms, light_seen, net_seen, http_seen;
(void)arg;
/* I2: the 5 s of spec 3.4 step 7 / 5.5 count from power-up, not from "after the recovery
* scan" - otherwise the documented "< 5 s per power cycle" window silently becomes ~8 s. */
boot_ms = nl_app_now_ms();
nl_flash_init();
nl_led_init(board); /* hardware init first (spec 3.4): LEDs dark before any flash/state scan */
store_lock();
nl_resetcnt_init(base + NL_FL_RESETCNT);
boots = nl_resetcnt_boot();
nl_settings_init(base + NL_FL_SETTINGS_A, base + NL_FL_SETTINGS_B);
nl_settings_load(&s_settings);
nl_state_log_init(base + NL_FL_STATE_0, base + NL_FL_STATE_1);
have_last = nl_state_log_load(&last);
store_unlock();
after_ota = s_settings.ota_pending != 0;
if (after_ota) {
/* We came back from an update attempt (accepted or rejected). Clear the flag so it does
* not stick in flash forever; nl_boot_state() below uses `after_ota` to restore the light
* effectively as power_on_behaviour = last (spec 8.2: a reboot caused by an update is not
* a power cut). */
s_settings.ota_pending = 0;
printf("boot after ota attempt\r\n");
store_lock();
(void)nl_settings_save(&s_settings);
store_unlock();
}
printf("boot #%u, settings: pob=%u fade=%u/%u, last state: %s\r\n", boots,
s_settings.power_on_behaviour, s_settings.fade_in_ms, s_settings.fade_out_ms,
have_last ? "yes" : "no");
cfg.ct_min = board->ct_min;
cfg.ct_max = board->ct_max;
cfg.output = led_out;
cfg.ctx = NULL;
initial = nl_boot_state((nl_pob_t)s_settings.power_on_behaviour, have_last ? &last : NULL, board,
after_ota);
nl_app_lock();
nl_engine_init(&cfg);
nl_ctrl_init(board, s_settings.fade_in_ms, s_settings.fade_out_ms);
nl_ctrl_boot(&initial);
nl_app_unlock();
tls_os_task_create(NULL, "nl_light", light_task, NULL, (u8 *)s_light_stack, sizeof(s_light_stack), NL_LIGHT_PRIO, 0);
if (boots >= NL_FACTORY_RESET_BOOTS)
nl_app_factory_reset(); /* does not return */
tls_watchdog_init(20u * 1000u * 1000u); /* 20 s */
nl_ota_init();
nl_console_start(); /* keep the serial fallback alive during nl_recovery_try() below */
nl_recovery_try(); /* may not return: a successful recovery resets the chip */
{
static nl_settings_t boot; /* copy: the network task may change s_settings later */
nl_app_lock();
boot = s_settings;
nl_app_unlock();
nl_mdns_set_name(boot.name);
if (boot.wifi_ssid[0])
nl_wifi_start(boot.wifi_ssid, boot.wifi_pass, NINLIGHT_AP_PASSWORD);
else
nl_wifi_start(NINLIGHT_WIFI_SSID, NINLIGHT_WIFI_PASSWORD, NINLIGHT_AP_PASSWORD);
}
nl_http_server_start(nl_app_api_ctx());
light_seen = s_light_beat;
net_seen = nl_net_beat;
http_seen = nl_http_beat;
for (;;) {
uint32_t now;
bool due;
tls_os_time_delay(HZ / 10); /* 100 ms */
now = nl_app_now_ms();
if (!s_counter_cleared && now - boot_ms >= NL_RESETCNT_CLEAR_MS)
nl_app_clear_reset_counter();
if (s_factory_reset_requested)
nl_app_factory_reset(); /* does not return */
nl_app_lock();
due = nl_ctrl_persist_due(now, &pending);
nl_app_unlock();
if (due) {
int r;
store_lock();
r = nl_state_log_append(&pending);
store_unlock();
if (r == 0) {
nl_app_lock();
nl_ctrl_persisted(&pending);
nl_app_unlock();
}
}
/* Feed the watchdog only once the light, network and HTTP tasks have each made
* progress since the last feed - a single stuck task (e.g. nl_app_lock() held
* forever) must still starve the watchdog, not just the light task. */
if (s_light_beat != light_seen) {
light_seen = s_light_beat;
light_alive = true;
}
if (nl_net_beat != net_seen) {
net_seen = nl_net_beat;
net_alive = true;
}
if (nl_http_beat != http_seen) {
http_seen = nl_http_beat;
http_alive = true;
}
if (light_alive && net_alive && http_alive) {
light_alive = net_alive = http_alive = false;
tls_watchdog_clr();
}
}
}
void UserMain(void)
{
printf("\r\nninlight %s for %s (built %s)\r\n", NINLIGHT_FW_VERSION, NINLIGHT_DEVICE_ID, NINLIGHT_BUILD_DATE);
if (nl_board_get() == NULL) {
printf("FATAL: unknown board %s\r\n", NINLIGHT_DEVICE_ID);
return;
}
if (tls_os_mutex_create(0, &s_lock) != TLS_OS_SUCCESS) {
printf("FATAL: cannot create app lock\r\n");
return;
}
if (tls_os_mutex_create(0, &s_store_lock) != TLS_OS_SUCCESS) {
printf("FATAL: cannot create store lock\r\n");
return;
}
tls_os_task_create(NULL, "nl_app", app_task, NULL, (u8 *)s_app_stack, sizeof(s_app_stack), NL_APP_PRIO, 0);
}