12#if FL_PLATFORM_HAS_LARGE_MEMORY
20struct OtaStateHolder {
21 AutoResearchOtaState
state;
24struct RpOtaUpdateState {
36 bool attempted =
false;
37 bool succeeded =
false;
38 char last_error[96] = {};
41RpOtaUpdateState& getRpOtaUpdateState() {
57#if defined(FL_IS_ESP32)
58#include "soc/soc_caps.h"
61#if defined(FL_IS_ESP32) && defined(SOC_WIFI_SUPPORTED) && SOC_WIFI_SUPPORTED
73#include <esp_http_server.h>
75#include <mbedtls/sha256.h>
78struct EspOtaRuntimeState {
79 fl::unique_ptr<fl::net::OTA>
ota;
80 esp_netif_t* netif_ap =
nullptr;
81 bool event_loop_initialized =
false;
82 bool wifi_initialized =
false;
85struct OtaArtifactState {
87 httpd_handle_t
server =
nullptr;
88 size_t expected_size = 0;
89 size_t received_size = 0;
90 size_t served_requests = 0;
91 char expected_sha256[65] = {};
94OtaArtifactState& getOtaArtifactState() {
98EspOtaRuntimeState& getEspOtaRuntimeState() {
102static bool copySha256(
char* destination,
size_t capacity,
const char* source) {
103 if (source ==
nullptr || capacity != 65) {
107 while (source[index] !=
'\0') {
108 const char value = source[index];
109 const bool is_digit = value >=
'0' && value <=
'9';
110 const bool is_lower = value >=
'a' && value <=
'f';
111 if ((!is_digit && !is_lower) || index + 1 >= capacity) {
114 destination[index] = value;
120 destination[index] =
'\0';
124static bool sha256File(
const char* path,
char* output,
size_t output_capacity) {
125 if (output_capacity != 65) {
128 File file = LittleFS.open(path, FILE_READ);
129 if (!file || file.isDirectory()) {
132 mbedtls_sha256_context context;
133 mbedtls_sha256_init(&context);
134 mbedtls_sha256_starts(&context, 0);
136 while (file.available()) {
137 const size_t read = file.read(bytes,
sizeof(bytes));
140 mbedtls_sha256_free(&context);
143 mbedtls_sha256_update(&context, bytes, read);
146 mbedtls_sha256_finish(&context, digest);
147 mbedtls_sha256_free(&context);
149 for (
size_t index = 0; index <
sizeof(digest); ++index) {
150 output[index * 2] =
"0123456789abcdef"[digest[index] >> 4];
151 output[index * 2 + 1] =
"0123456789abcdef"[digest[index] & 0x0f];
157static esp_err_t serveOtaArtifact(httpd_req_t* request) {
158 File file = LittleFS.open(
"/rp2350.bin", FILE_READ);
159 if (!file || file.isDirectory()) {
160 return httpd_resp_send_err(request, HTTPD_404_NOT_FOUND,
"Artifact unavailable");
162 httpd_resp_set_type(request,
"application/octet-stream");
176 httpd_resp_set_hdr(request,
"Content-Length",
length);
178 while (file.available()) {
179 const size_t read = file.read(bytes,
sizeof(bytes));
180 if (read == 0 || httpd_resp_send_chunk(request,
181 reinterpret_cast<const char*
>(bytes),
193 const esp_err_t finished = httpd_resp_send_chunk(request,
nullptr, 0);
194 if (finished != ESP_OK) {
201 ++getOtaArtifactState().served_requests;
209static bool initOtaWifiAP() {
211 EspOtaRuntimeState& runtimeState = getEspOtaRuntimeState();
217 esp_err_t err = esp_netif_init();
218 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
219 FL_WARN(
"[OTA] esp_netif_init failed: " << esp_err_to_name(err));
224 if (!runtimeState.event_loop_initialized) {
225 err = esp_event_loop_create_default();
226 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
227 FL_WARN(
"[OTA] esp_event_loop_create_default failed: " << esp_err_to_name(err));
230 runtimeState.event_loop_initialized =
true;
234 if (!runtimeState.netif_ap) {
235 runtimeState.netif_ap = esp_netif_create_default_wifi_ap();
236 if (!runtimeState.netif_ap) {
237 FL_WARN(
"[OTA] esp_netif_create_default_wifi_ap failed");
243 if (!runtimeState.wifi_initialized) {
244 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
245 err = esp_wifi_init(&cfg);
246 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
247 FL_WARN(
"[OTA] esp_wifi_init failed: " << esp_err_to_name(err));
250 runtimeState.wifi_initialized =
true;
254 wifi_config_t wifi_config = {};
258 wifi_config.ap.max_connection = 4;
259 wifi_config.ap.authmode = WIFI_AUTH_WPA2_PSK;
260 wifi_config.ap.channel = 1;
262 err = esp_wifi_set_mode(WIFI_MODE_AP);
264 FL_WARN(
"[OTA] esp_wifi_set_mode failed: " << esp_err_to_name(err));
268 err = esp_wifi_set_config(WIFI_IF_AP, &wifi_config);
270 FL_WARN(
"[OTA] esp_wifi_set_config failed: " << esp_err_to_name(err));
274 err = esp_wifi_start();
276 FL_WARN(
"[OTA] esp_wifi_start failed: " << esp_err_to_name(err));
292 EspOtaRuntimeState& runtimeState = getEspOtaRuntimeState();
295 if (!initOtaWifiAP()) {
297 response.set(
"error",
"Failed to start WiFi AP for OTA");
305 response.set(
"error",
"Failed to start OTA server");
306 runtimeState.ota.reset();
326 EspOtaRuntimeState& runtimeState = getEspOtaRuntimeState();
329 if (runtimeState.ota.get()) {
330 runtimeState.ota.reset();
332 FL_WARN(
"[OTA] OTA server stopped");
339 FL_WARN(
"[OTA] WiFi AP stopped");
348 OtaArtifactState&
state = getOtaArtifactState();
349 if (expected_size == 0 || !copySha256(
state.expected_sha256,
350 sizeof(
state.expected_sha256), sha256)) {
352 response.set(
"error",
"Invalid artifact metadata");
355 if (!LittleFS.begin(
true)) {
357 response.set(
"error",
"LittleFS mount failed");
360 if (expected_size > LittleFS.totalBytes()) {
362 response.set(
"error",
"Artifact exceeds fixture filesystem");
368 LittleFS.remove(
"/rp2350.bin.part");
386 LittleFS.remove(
"/rp2350.bin");
387 const size_t free_bytes = LittleFS.totalBytes() - LittleFS.usedBytes();
388 if (expected_size > free_bytes) {
392 response.set(
"error",
"Artifact exceeds free space on fixture filesystem");
393 response.set(
"freeBytes",
static_cast<int64_t
>(free_bytes));
394 response.set(
"neededBytes",
static_cast<int64_t
>(expected_size));
397 state.file = LittleFS.open(
"/rp2350.bin.part", FILE_WRITE);
400 response.set(
"error",
"Artifact staging open failed");
403 state.expected_size = expected_size;
404 state.received_size = 0;
406 response.set(
"capacity",
static_cast<int64_t
>(LittleFS.totalBytes()));
412 OtaArtifactState&
state = getOtaArtifactState();
416 response.set(
"error",
"Invalid artifact chunk");
419 const size_t written =
state.file.write(bytes.
data(), bytes.
size());
420 state.received_size += written;
422 response.set(
"received",
static_cast<int64_t
>(
state.received_size));
423 if (written != bytes.
size()) {
424 response.set(
"error",
"Artifact write failed");
431 OtaArtifactState&
state = getOtaArtifactState();
434 response.set(
"error",
"Artifact size mismatch");
438 char actual_sha256[65];
439 if (!sha256File(
"/rp2350.bin.part", actual_sha256,
sizeof(actual_sha256)) ||
441 LittleFS.remove(
"/rp2350.bin.part");
443 response.set(
"error",
"Artifact SHA-256 mismatch");
446 LittleFS.remove(
"/rp2350.bin");
447 if (!LittleFS.rename(
"/rp2350.bin.part",
"/rp2350.bin")) {
449 response.set(
"error",
"Artifact publish failed");
461 state.served_requests = 0;
463 response.set(
"size",
static_cast<int64_t
>(
state.received_size));
464 response.set(
"sha256", actual_sha256);
470 OtaArtifactState&
state = getOtaArtifactState();
471 if (!LittleFS.begin(
true) || !LittleFS.exists(
"/rp2350.bin")) {
473 response.set(
"error",
"No published RP2350 artifact");
477 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
478 config.server_port = 8081;
488 config.ctrl_port = 32769;
489 if (httpd_start(&
state.server, &config) != ESP_OK) {
491 response.set(
"error",
"Artifact HTTP server start failed");
494 httpd_uri_t artifact = {};
495 artifact.uri =
"/rp2350.bin";
496 artifact.method = HTTP_GET;
497 artifact.handler = serveOtaArtifact;
498 if (httpd_register_uri_handler(
state.server, &artifact) != ESP_OK) {
499 httpd_stop(
state.server);
500 state.server =
nullptr;
502 response.set(
"error",
"Artifact HTTP route registration failed");
508 response.set(
"port",
static_cast<int64_t
>(8081));
514 OtaArtifactState&
state = getOtaArtifactState();
515 response.set(
"success", LittleFS.begin(
true) && LittleFS.exists(
"/rp2350.bin"));
516 response.set(
"servedRequests",
static_cast<int64_t
>(
state.served_requests));
517 response.set(
"received",
static_cast<int64_t
>(
state.received_size));
530 response.set(
"error",
"OTA autoresearch only supported on ESP32");
545 response.set(
"error",
"Artifact staging only supported on ESP32-C6");
553 response.set(
"error",
"Artifact staging only supported on ESP32-C6");
560 response.set(
"error",
"Artifact staging only supported on ESP32-C6");
567 response.set(
"error",
"Artifact server only supported on ESP32-C6");
574 response.set(
"error",
"Artifact server only supported on ESP32-C6");
580#if defined(FL_IS_RP2350) && defined(PICO_CYW43_SUPPORTED)
583#include <HTTPUpdate.h>
588 RpOtaUpdateState&
state = getRpOtaUpdateState();
589 if (host ==
nullptr || port == 0) {
591 response.set(
"error",
"Invalid artifact endpoint");
595 while (host[index] !=
'\0' && index + 1 <
sizeof(
state.host)) {
596 state.host[index] = host[index];
599 if (host[index] !=
'\0') {
601 response.set(
"error",
"Artifact host is too long");
604 state.host[index] =
'\0';
606 state.pending =
true;
613 RpOtaUpdateState&
state = getRpOtaUpdateState();
614 if (!
state.pending) {
617 state.pending =
false;
636 const t_httpUpdate_return
result = updater.update(client,
state.host,
state.port,
640 wdt.
begin(watchdog_restore_ms);
643 state.attempted =
true;
648 auto reason = updater.getLastErrorString();
649 const char* reason_text = reason.c_str();
651 while (copied + 1 <
sizeof(
state.last_error) && reason_text[copied] !=
'\0') {
652 state.last_error[copied] = reason_text[copied];
655 state.last_error[copied] =
'\0';
657 if (result != HTTP_UPDATE_OK) {
658 FL_WARN(
"[OTA] RP2350W artifact update failed: " << updater.getLastErrorString());
664 RpOtaUpdateState&
state = getRpOtaUpdateState();
682 response.set(
"error",
"Artifact update only supported on RP2350W");
691 response.set(
"error",
"Artifact update only supported on RP2350W");
#define AUTORESEARCH_OTA_HOSTNAME
fl::json queueOtaArtifactUpdate(const char *host, uint16_t port)
Queue an RP2350W HTTP update so the RPC response can be sent first.
fl::json writeOtaArtifact(fl::vector< fl::u8 > bytes)
fl::json otaArtifactStatus()
AutoResearchOtaState & getOtaState()
Get current OTA autoresearch state.
fl::json stopOta()
Stop OTA server and WiFi AP, release all resources.
#define AUTORESEARCH_OTA_PASSWORD
fl::json finishOtaArtifact()
fl::json startOtaArtifactServer()
#define AUTORESEARCH_OTA_SSID
#define AUTORESEARCH_OTA_AP_IP
#define AUTORESEARCH_OTA_PORT
fl::json rpOtaUpdateStatus()
Outcome of the last pollOtaArtifactUpdate fetch, readable after the host reconnects.
fl::json startOta()
Start WiFi Soft AP and OTA HTTP server.
fl::json beginOtaArtifact(size_t expected_size, const char *sha256)
Stage one RP2350W firmware artifact on the ESP32-C6 fixture.
void pollOtaArtifactUpdate(uint32_t watchdog_restore_ms)
Execute a queued RP2350W update from the sketch loop.
#define AUTORESEARCH_OTA_OTA_PASSWORD
State for OTA autoresearch.
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
static T & instance() FL_NO_EXCEPT
void disable() FL_NO_EXCEPT
void begin(fl::u32 timeout_ms) FL_NO_EXCEPT
Unified cross-platform watchdog interface.
static json object() FL_NO_EXCEPT
fl::size size() const FL_NO_EXCEPT
bool empty() const FL_NO_EXCEPT
fl::UISlider length("Length", 1.0f, 0.0f, 1.0f, 0.01f)
FastLED's Elegant JSON Library: fl::json
Centralized logging categories for FastLED hardware interfaces and subsystems.
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
void * memcpy(void *dest, const void *src, size_t n) FL_NO_EXCEPT
size_t strlen(const char *s) FL_NO_EXCEPT
int snprintf(char *buffer, fl::size size, const char *format) FL_NO_EXCEPT
FASTLED_FORCE_INLINE int strcmp(const char *s1, const char *s2) FL_NO_EXCEPT
expected< T, E > result
Alias for expected (Rust-style naming)
Public include for fl::net::OTA.
Unified cross-platform Watchdog Timer API for FastLED.
fl::net::wifi — platform-neutral WiFi connection API (FastLED#3576 Phase 6).