#include #include #include "wm_include.h" #include "wm_http_fwup.h" #include "config/nl_settings.h" #include "sys/nl_app.h" #include "sys/nl_fwup_hook.h" #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); #define NL_OTA_URL_MAX 256 #define NL_OTA_STACK 2048 /* words */ #define NL_OTA_PRIO 34 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 volatile bool s_skipped; 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) { s_busy = false; s_error = NULL; } bool nl_ota_busy(void) { return s_busy; } bool nl_ota_skipped(void) { return s_skipped; } const char *nl_ota_last_error(void) { return s_error; } void nl_ota_set_error(const char *e) { s_error = e; } /* Called by the SDK firmware-update path once the image has been written and hashed. sha256 covers the whole payload (T_BOOTER header + compressed image), tail is the 512 byte end block that used to carry the QConnex RSA signature. NB: this runs on the SDK's own fwup_scheduler task (tls_fwup_init(), stack FWUP_TASK_STK_SIZE*4, ~2 KB) - NOT on the 8 KB nl_ota task stack. Keep this hook minimal: one short printf, no extra locals (a version buffer used to sit here and was removed). */ bool nl_fwup_verify_tail(const uint8_t sha256[32], const uint8_t *tail, uint32_t tail_len) { nl_trailer_err_t e = nl_trailer_verify(tail, tail_len, NINLIGHT_DEVICE_ID, sha256); if (e != NL_TRAILER_OK) { s_error = nl_trailer_err_name(e); 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); /* Deliberately skipped, not failed: leave last_ota_error alone. Reporting "trailer" * here used to make a perfectly healthy lamp look like it had rejected a malformed * image, which is exactly the wrong thing to read off a lamp in the field. */ s_skipped = true; 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 reach the console. s_url is validated by nl_ota_start() to start with "http://". */ static void log_url(const char *url) { const char *at = strchr(url + 7, '@'); if (at != NULL) printf("ota: downloading http://%s\r\n", at + 1); else printf("ota: downloading %s\r\n", url); } static void ota_task(void *arg) { int rc; (void)arg; log_url(s_url); rc = t_http_fwup(s_url); /* Only reached on failure - the SDK resets the chip itself after a good image. */ if (s_error == NULL && !s_skipped) s_error = "download"; printf("ota: failed (rc=%d, %s)\r\n", rc, s_error); { nl_settings_t *s; nl_app_lock(); s = nl_app_settings(); s->ota_pending = 0; nl_app_unlock(); (void)nl_app_save_settings(); } s_busy = false; tls_os_task_del(NL_OTA_PRIO, NULL); } static int ota_start(const char *url, bool reject_running_version) { size_t n; if (url == NULL || strncmp(url, "http://", 7) != 0) return -1; n = strlen(url); if (n <= 7 || n >= sizeof s_url) /* n == 7 is bare "http://" with no host */ return -1; if (s_busy) { /* Do NOT touch s_error here. last_ota_error describes the outcome of the *running or last* * update; a request we reject because one is already in flight has no outcome of its own. * Writing "busy" into it used to poison the running attempt's slot: ota_task() only falls * back to "download" when s_error is still NULL, so a failed transfer was afterwards * reported as `busy` instead of `download`. Found on the EVK, 2026-09-12. The REST layer * reports the rejection to its own caller with 409. */ return -1; } s_busy = true; s_error = NULL; s_skipped = false; 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. */ nl_app_flush(); { nl_settings_t *s; nl_app_lock(); s = nl_app_settings(); s->ota_pending = 1; nl_app_unlock(); (void)nl_app_save_settings(); } if (tls_os_task_create(NULL, "nl_ota", ota_task, NULL, (u8 *)s_stack, sizeof(s_stack), NL_OTA_PRIO, 0) != TLS_OS_SUCCESS) { /* The task never ran, so clear the ota_pending flag we just persisted above - otherwise * the next boot wrongly logs "boot after ota attempt" for an update that never started. */ nl_settings_t *s; nl_app_lock(); s = nl_app_settings(); s->ota_pending = 0; 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); }