FastLED 3.10.8
Loading...
Searching...
No Matches
AutoResearchOta.cpp
Go to the documentation of this file.
1// examples/AutoResearch/AutoResearchOta.cpp
2//
3// OTA autoresearch implementation for ESP32.
4// Uses fl::net::OTA for the OTA HTTP server with Basic Auth.
5// Uses ESP-IDF native APIs for WiFi Soft AP.
6// Guarded with FL_IS_ESP32 - no-op stubs on other platforms.
7
8// Gate out under low-memory mode -- no OTA path on parts that don't have
9// WiFi / a network stack. Matches the conditional structure in
10// AutoResearch.ino itself.
12#if FL_PLATFORM_HAS_LARGE_MEMORY
13
14#include "AutoResearchOta.h"
15#include "fl/stl/json.h"
16#include "fl/stl/singleton.h"
17#include "fl/log/log.h"
18#include "fl/stl/vector.h"
19
20struct OtaStateHolder {
21 AutoResearchOtaState state;
22};
23
24struct RpOtaUpdateState {
25 bool pending = false;
26 char host[16] = {};
27 uint16_t port = 0;
28 // Why the last attempt ended as it did. `pollOtaArtifactUpdate` runs from
29 // loop() long after the host has closed the RPC link -- the peer OTA flow
30 // closes it deliberately so the board can reboot into the new image -- so
31 // the FL_WARN it used to emit went to a serial port with nobody on the
32 // other end. A failed fetch was then indistinguishable from one that
33 // never ran: both left the C6 reporting `servedRequests: 0` and no reason
34 // anywhere. Keep the outcome here so the host can ask once it reconnects.
35 // See FastLED#3956.
36 bool attempted = false;
37 bool succeeded = false;
38 char last_error[96] = {};
39};
40
41RpOtaUpdateState& getRpOtaUpdateState() {
43}
44
47}
48
49// ============================================================================
50// ESP32 Implementation
51// ============================================================================
52
53// ESP32-H2 (and other WiFi-less ESP32 family members) must take the
54// stub branch — esp_wifi.h does not even preprocess there
55// (CONFIG_ESP_WIFI_* are absent from its sdkconfig). Gate on actual
56// WiFi silicon, not on the family macro. (FastLED#3576 Phase 4)
57#if defined(FL_IS_ESP32)
58#include "soc/soc_caps.h" // SOC_WIFI_SUPPORTED
59#endif
60
61#if defined(FL_IS_ESP32) && defined(SOC_WIFI_SUPPORTED) && SOC_WIFI_SUPPORTED
62
63#include "fl/net/ota.h"
64#include "fl/stl/cstring.h"
65#include "fl/stl/unique_ptr.h"
66#include <Arduino.h>
67
68// ESP-IDF headers for WiFi AP
69// IWYU pragma: begin_keep
70#include <esp_wifi.h>
71#include <esp_netif.h>
72#include <esp_event.h>
73#include <esp_http_server.h>
74#include <LittleFS.h>
75#include <mbedtls/sha256.h>
76// IWYU pragma: end_keep
77
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;
83};
84
85struct OtaArtifactState {
86 File file;
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] = {};
92};
93
94OtaArtifactState& getOtaArtifactState() {
96}
97
98EspOtaRuntimeState& getEspOtaRuntimeState() {
100}
101
102static bool copySha256(char* destination, size_t capacity, const char* source) {
103 if (source == nullptr || capacity != 65) {
104 return false;
105 }
106 size_t index = 0;
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) {
112 return false;
113 }
114 destination[index] = value;
115 ++index;
116 }
117 if (index != 64) {
118 return false;
119 }
120 destination[index] = '\0';
121 return true;
122}
123
124static bool sha256File(const char* path, char* output, size_t output_capacity) {
125 if (output_capacity != 65) {
126 return false;
127 }
128 File file = LittleFS.open(path, FILE_READ);
129 if (!file || file.isDirectory()) {
130 return false;
131 }
132 mbedtls_sha256_context context;
133 mbedtls_sha256_init(&context);
134 mbedtls_sha256_starts(&context, 0);
135 uint8_t bytes[512];
136 while (file.available()) {
137 const size_t read = file.read(bytes, sizeof(bytes));
138 if (read == 0) {
139 file.close();
140 mbedtls_sha256_free(&context);
141 return false;
142 }
143 mbedtls_sha256_update(&context, bytes, read);
144 }
145 uint8_t digest[32];
146 mbedtls_sha256_finish(&context, digest);
147 mbedtls_sha256_free(&context);
148 file.close();
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];
152 }
153 output[64] = '\0';
154 return true;
155}
156
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");
161 }
162 httpd_resp_set_type(request, "application/octet-stream");
163 // Content-Length is REQUIRED by the client, even though the body streams
164 // out chunked. In the pinned arduino-pico core (rp2040-5.7.0),
165 // HTTPClient::_size is initialised to -1 and only ever assigned from a
166 // Content-Length header (HTTPClient.cpp:1111), and HTTPUpdate takes the
167 // `len > 0` branch or fails with HTTP_UE_SERVER_NOT_REPORT_SIZE
168 // (HTTPUpdate.cpp:232, :295). Dropping this header makes every
169 // applyOtaArtifact fail with "Server did not report size".
170 //
171 // So the #3956 review question "confirm this Arduino-Pico OTA client
172 // accepts chunked responses" resolves to: it does not — it decodes the
173 // chunked body but takes the image size from Content-Length. Keep both.
174 char length[16];
175 snprintf(length, sizeof(length), "%u", static_cast<unsigned>(file.size()));
176 httpd_resp_set_hdr(request, "Content-Length", length);
177 uint8_t bytes[512];
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),
182 read) != ESP_OK) {
183 file.close();
184 // Deliberately NO terminating zero chunk here: in chunked framing
185 // a zero-length chunk means "body ended normally", so sending one
186 // after a failed read would present a half-written firmware image
187 // as a complete one. Returning ESP_FAIL drops the connection
188 // mid-stream, which is how the client detects the truncation.
189 return ESP_FAIL;
190 }
191 }
192 file.close();
193 const esp_err_t finished = httpd_resp_send_chunk(request, nullptr, 0);
194 if (finished != ESP_OK) {
195 return finished;
196 }
197 // Count the request only once the whole artifact is on the wire. Bumping
198 // this up front made a 404 or a partial send look like a successful
199 // transfer, and the host asserts `servedRequests >= 1` as proof the
200 // RP2350W actually downloaded the image.
201 ++getOtaArtifactState().served_requests;
202 return ESP_OK;
203}
204
205// ============================================================================
206// WiFi Soft AP Setup (same pattern as AutoResearchNet.cpp::initWifiAP)
207// ============================================================================
208
209static bool initOtaWifiAP() {
210 AutoResearchOtaState& otaState = getOtaState();
211 EspOtaRuntimeState& runtimeState = getEspOtaRuntimeState();
212 if (otaState.wifi_ap_active) {
213 return true; // Already active
214 }
215
216 // Initialize TCP/IP stack (safe to call multiple times)
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));
220 return false;
221 }
222
223 // Create default event loop (safe to call multiple times)
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));
228 return false;
229 }
230 runtimeState.event_loop_initialized = true;
231 }
232
233 // Create default WiFi AP netif
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");
238 return false;
239 }
240 }
241
242 // Initialize WiFi with default config
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));
248 return false;
249 }
250 runtimeState.wifi_initialized = true;
251 }
252
253 // Configure AP
254 wifi_config_t wifi_config = {};
256 wifi_config.ap.ssid_len = strlen(AUTORESEARCH_OTA_SSID);
258 wifi_config.ap.max_connection = 4;
259 wifi_config.ap.authmode = WIFI_AUTH_WPA2_PSK;
260 wifi_config.ap.channel = 1;
261
262 err = esp_wifi_set_mode(WIFI_MODE_AP);
263 if (err != ESP_OK) {
264 FL_WARN("[OTA] esp_wifi_set_mode failed: " << esp_err_to_name(err));
265 return false;
266 }
267
268 err = esp_wifi_set_config(WIFI_IF_AP, &wifi_config);
269 if (err != ESP_OK) {
270 FL_WARN("[OTA] esp_wifi_set_config failed: " << esp_err_to_name(err));
271 return false;
272 }
273
274 err = esp_wifi_start();
275 if (err != ESP_OK) {
276 FL_WARN("[OTA] esp_wifi_start failed: " << esp_err_to_name(err));
277 return false;
278 }
279
280 otaState.wifi_ap_active = true;
281 FL_WARN("[OTA] WiFi AP started: SSID=" << AUTORESEARCH_OTA_SSID << " IP=" << AUTORESEARCH_OTA_AP_IP);
282 return true;
283}
284
285// ============================================================================
286// Public API
287// ============================================================================
288
291 AutoResearchOtaState& otaState = getOtaState();
292 EspOtaRuntimeState& runtimeState = getEspOtaRuntimeState();
293
294 // Start WiFi AP
295 if (!initOtaWifiAP()) {
296 response.set("success", false);
297 response.set("error", "Failed to start WiFi AP for OTA");
298 return response;
299 }
300
301 // Create and start OTA server
302 runtimeState.ota = fl::make_unique<fl::net::OTA>();
303 if (!runtimeState.ota->begin(AUTORESEARCH_OTA_HOSTNAME, AUTORESEARCH_OTA_OTA_PASSWORD)) {
304 response.set("success", false);
305 response.set("error", "Failed to start OTA server");
306 runtimeState.ota.reset();
307 return response;
308 }
309
310 otaState.ota_active = true;
311 FL_WARN("[OTA] OTA server started: hostname=" << AUTORESEARCH_OTA_HOSTNAME);
312
313 response.set("success", true);
314 response.set("ssid", AUTORESEARCH_OTA_SSID);
315 response.set("password", AUTORESEARCH_OTA_PASSWORD);
317 response.set("port", static_cast<int64_t>(AUTORESEARCH_OTA_PORT));
318 response.set("ota_password", AUTORESEARCH_OTA_OTA_PASSWORD);
319 response.set("hostname", AUTORESEARCH_OTA_HOSTNAME);
320 return response;
321}
322
325 AutoResearchOtaState& otaState = getOtaState();
326 EspOtaRuntimeState& runtimeState = getEspOtaRuntimeState();
327
328 // Stop OTA server
329 if (runtimeState.ota.get()) {
330 runtimeState.ota.reset();
331 otaState.ota_active = false;
332 FL_WARN("[OTA] OTA server stopped");
333 }
334
335 // Stop WiFi
336 if (otaState.wifi_ap_active) {
337 esp_wifi_stop();
338 otaState.wifi_ap_active = false;
339 FL_WARN("[OTA] WiFi AP stopped");
340 }
341
342 response.set("success", true);
343 return response;
344}
345
346fl::json beginOtaArtifact(size_t expected_size, const char* sha256) {
348 OtaArtifactState& state = getOtaArtifactState();
349 if (expected_size == 0 || !copySha256(state.expected_sha256,
350 sizeof(state.expected_sha256), sha256)) {
351 response.set("success", false);
352 response.set("error", "Invalid artifact metadata");
353 return response;
354 }
355 if (!LittleFS.begin(true)) {
356 response.set("success", false);
357 response.set("error", "LittleFS mount failed");
358 return response;
359 }
360 if (expected_size > LittleFS.totalBytes()) {
361 response.set("success", false);
362 response.set("error", "Artifact exceeds fixture filesystem");
363 return response;
364 }
365 if (state.file) {
366 state.file.close();
367 }
368 LittleFS.remove("/rp2350.bin.part");
369 // Drop the previous artifact before staging the next one, not after.
370 //
371 // `finishOtaArtifact` replaces `/rp2350.bin` only once `.part` is
372 // complete and its SHA-256 checks out, which is the right order when
373 // there is room for both. There is not: the fixture's spiffs partition
374 // is 0x130000 (1,245,184 B) and the RP image is 1,030,348 B, so a second
375 // transfer needs 2,060,696 B of a 1,245,184 B filesystem. The first run
376 // on a blank filesystem transfers and verifies fine; every run after it
377 // dies partway with `Artifact write failed`, at byte 200,448 on this
378 // bench -- which is the 214,836 B of headroom left over, less LittleFS
379 // block overhead. See FastLED#3956.
380 //
381 // The cost is that a transfer which fails midway now leaves no artifact
382 // rather than the previous one. That trade is right here: the artifact
383 // is a test fixture the harness re-stages on every run, and the
384 // alternative is that the second and every subsequent run cannot stage
385 // at all.
386 LittleFS.remove("/rp2350.bin");
387 const size_t free_bytes = LittleFS.totalBytes() - LittleFS.usedBytes();
388 if (expected_size > free_bytes) {
389 // Fail here with the numbers rather than partway through the
390 // transfer with a bare "write failed" 20 minutes in.
391 response.set("success", false);
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));
395 return response;
396 }
397 state.file = LittleFS.open("/rp2350.bin.part", FILE_WRITE);
398 if (!state.file) {
399 response.set("success", false);
400 response.set("error", "Artifact staging open failed");
401 return response;
402 }
403 state.expected_size = expected_size;
404 state.received_size = 0;
405 response.set("success", true);
406 response.set("capacity", static_cast<int64_t>(LittleFS.totalBytes()));
407 return response;
408}
409
412 OtaArtifactState& state = getOtaArtifactState();
413 if (!state.file || bytes.empty() ||
414 bytes.size() > state.expected_size - state.received_size) {
415 response.set("success", false);
416 response.set("error", "Invalid artifact chunk");
417 return response;
418 }
419 const size_t written = state.file.write(bytes.data(), bytes.size());
420 state.received_size += written;
421 response.set("success", written == bytes.size());
422 response.set("received", static_cast<int64_t>(state.received_size));
423 if (written != bytes.size()) {
424 response.set("error", "Artifact write failed");
425 }
426 return response;
427}
428
431 OtaArtifactState& state = getOtaArtifactState();
432 if (!state.file || state.received_size != state.expected_size) {
433 response.set("success", false);
434 response.set("error", "Artifact size mismatch");
435 return response;
436 }
437 state.file.close();
438 char actual_sha256[65];
439 if (!sha256File("/rp2350.bin.part", actual_sha256, sizeof(actual_sha256)) ||
440 fl::strcmp(actual_sha256, state.expected_sha256) != 0) {
441 LittleFS.remove("/rp2350.bin.part");
442 response.set("success", false);
443 response.set("error", "Artifact SHA-256 mismatch");
444 return response;
445 }
446 LittleFS.remove("/rp2350.bin");
447 if (!LittleFS.rename("/rp2350.bin.part", "/rp2350.bin")) {
448 response.set("success", false);
449 response.set("error", "Artifact publish failed");
450 return response;
451 }
452 // A fresh artifact starts with a fresh serve count. `served_requests`
453 // lives in singleton state that outlives any one transfer, and
454 // `otaArtifactStatus` is what the host uses to decide whether the RP
455 // actually fetched: `servedRequests < 1` is the failure condition. Left
456 // carrying a previous artifact's count, that check passes on history --
457 // the run is scored on a download that happened before this image
458 // existed. In practice the C6 is reflashed between runs, which hides it;
459 // any path that stages twice without a reflash does not. See
460 // FastLED#3956.
461 state.served_requests = 0;
462 response.set("success", true);
463 response.set("size", static_cast<int64_t>(state.received_size));
464 response.set("sha256", actual_sha256);
465 return response;
466}
467
470 OtaArtifactState& state = getOtaArtifactState();
471 if (!LittleFS.begin(true) || !LittleFS.exists("/rp2350.bin")) {
472 response.set("success", false);
473 response.set("error", "No published RP2350 artifact");
474 return response;
475 }
476 if (!state.server) {
477 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
478 config.server_port = 8081;
479 // Distinct from the control socket the peer's main HTTP server is
480 // already holding. `HTTPD_DEFAULT_CONFIG()` hardcodes
481 // `.ctrl_port = ESP_HTTPD_DEF_CTRL_PORT` (32768), and
482 // `fl::asio::http::Server::start()` takes that default too, so the
483 // second `httpd_start` on the device binds a UDP port the first one
484 // owns and fails. `server_port` differing is not enough -- the
485 // conflict is on the control socket, not the listening socket.
486 // Reached only in the peer OTA flow, where `startNetServer` runs
487 // immediately before this. See FastLED#3956.
488 config.ctrl_port = 32769;
489 if (httpd_start(&state.server, &config) != ESP_OK) {
490 response.set("success", false);
491 response.set("error", "Artifact HTTP server start failed");
492 return response;
493 }
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;
501 response.set("success", false);
502 response.set("error", "Artifact HTTP route registration failed");
503 return response;
504 }
505 }
506 response.set("success", true);
508 response.set("port", static_cast<int64_t>(8081));
509 return response;
510}
511
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));
518 return response;
519}
520
521#else // !FL_IS_ESP32 || !SOC_WIFI_SUPPORTED
522
523// ============================================================================
524// Stub Implementation for Non-ESP32 Platforms
525// ============================================================================
526
529 response.set("success", false);
530 response.set("error", "OTA autoresearch only supported on ESP32");
531 return response;
532}
533
536 response.set("success", true);
537 return response;
538}
539
540fl::json beginOtaArtifact(size_t expected_size, const char* sha256) {
541 (void)expected_size;
542 (void)sha256;
544 response.set("success", false);
545 response.set("error", "Artifact staging only supported on ESP32-C6");
546 return response;
547}
548
550 (void)bytes;
552 response.set("success", false);
553 response.set("error", "Artifact staging only supported on ESP32-C6");
554 return response;
555}
556
559 response.set("success", false);
560 response.set("error", "Artifact staging only supported on ESP32-C6");
561 return response;
562}
563
566 response.set("success", false);
567 response.set("error", "Artifact server only supported on ESP32-C6");
568 return response;
569}
570
573 response.set("success", false);
574 response.set("error", "Artifact server only supported on ESP32-C6");
575 return response;
576}
577
578#endif // FL_IS_ESP32
579
580#if defined(FL_IS_RP2350) && defined(PICO_CYW43_SUPPORTED)
581#include "FastLED.h" // FastLED.watchdog()
582#include "fl/wdt/watchdog.h"
583#include <HTTPUpdate.h>
584#include <WiFi.h>
585
586fl::json queueOtaArtifactUpdate(const char* host, uint16_t port) {
588 RpOtaUpdateState& state = getRpOtaUpdateState();
589 if (host == nullptr || port == 0) {
590 response.set("success", false);
591 response.set("error", "Invalid artifact endpoint");
592 return response;
593 }
594 size_t index = 0;
595 while (host[index] != '\0' && index + 1 < sizeof(state.host)) {
596 state.host[index] = host[index];
597 ++index;
598 }
599 if (host[index] != '\0') {
600 response.set("success", false);
601 response.set("error", "Artifact host is too long");
602 return response;
603 }
604 state.host[index] = '\0';
605 state.port = port;
606 state.pending = true;
607 response.set("success", true);
608 response.set("accepted", true);
609 return response;
610}
611
612void pollOtaArtifactUpdate(uint32_t watchdog_restore_ms) {
613 RpOtaUpdateState& state = getRpOtaUpdateState();
614 if (!state.pending) {
615 return;
616 }
617 state.pending = false;
618
619 // updater.update() is a synchronous, multi-second firmware download run
620 // straight from loop(). The sketch feeds the watchdog only at the top and
621 // bottom of the loop scope, so a download longer than that window resets
622 // the board mid-flash-write.
623 //
624 // Re-arming with a longer timeout does NOT work here: Watchdog::begin()
625 // clamps to FL_WATCHDOG_MAX_TIMEOUT_MS, which is 16777 ms on RP2350
626 // (watchdog_rp.impl.hpp:47, :111) because the hardware counter is 24-bit
627 // microseconds. Asking for more silently yields ~16.8 s, and a real image
628 // download outruns that too. Halt the counter for the transfer and re-arm
629 // afterwards, so the loop is protected again the moment it returns.
630 fl::Watchdog& wdt = FastLED.watchdog();
631 wdt.feed();
632 wdt.disable();
633
634 WiFiClient client;
635 HTTPUpdate updater;
636 const t_httpUpdate_return result = updater.update(client, state.host, state.port,
637 "/rp2350.bin");
638
639 // Re-arm before reporting, so a slow log write is already covered.
640 wdt.begin(watchdog_restore_ms);
641 wdt.feed();
642
643 state.attempted = true;
644 state.succeeded = (result == HTTP_UPDATE_OK);
645 // `auto`, not `fl::string`: HTTPUpdate hands back an `arduino::String`
646 // and there is no conversion. Naming it keeps the temporary alive for
647 // the c_str() read below.
648 auto reason = updater.getLastErrorString();
649 const char* reason_text = reason.c_str();
650 size_t copied = 0;
651 while (copied + 1 < sizeof(state.last_error) && reason_text[copied] != '\0') {
652 state.last_error[copied] = reason_text[copied];
653 ++copied;
654 }
655 state.last_error[copied] = '\0';
656
657 if (result != HTTP_UPDATE_OK) {
658 FL_WARN("[OTA] RP2350W artifact update failed: " << updater.getLastErrorString());
659 }
660}
661
664 RpOtaUpdateState& state = getRpOtaUpdateState();
665 response.set("success", true);
666 response.set("attempted", state.attempted);
667 response.set("succeeded", state.succeeded);
668 response.set("pending", state.pending);
669 response.set("lastError", state.last_error);
670 response.set("host", state.host);
671 response.set("port", static_cast<int64_t>(state.port));
672 return response;
673}
674
675#else
676
677fl::json queueOtaArtifactUpdate(const char* host, uint16_t port) {
678 (void)host;
679 (void)port;
681 response.set("success", false);
682 response.set("error", "Artifact update only supported on RP2350W");
683 return response;
684}
685
686void pollOtaArtifactUpdate(uint32_t /*watchdog_restore_ms*/) {}
687
690 response.set("success", false);
691 response.set("error", "Artifact update only supported on RP2350W");
692 return response;
693}
694
695#endif
696
697#endif // FL_PLATFORM_HAS_LARGE_MEMORY
#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::HttpServer server
TestState state
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
fl::net::OTA ota
Definition OTA.ino:117
static T & instance() FL_NO_EXCEPT
Definition singleton.h:41
void disable() FL_NO_EXCEPT
void begin(fl::u32 timeout_ms) FL_NO_EXCEPT
void feed() FL_NO_EXCEPT
Unified cross-platform watchdog interface.
Definition watchdog.h:123
static json object() FL_NO_EXCEPT
Definition json.h:857
fl::size size() const FL_NO_EXCEPT
bool empty() const FL_NO_EXCEPT
T * data() FL_NO_EXCEPT
Definition vector.h:619
fl::UISlider length("Length", 1.0f, 0.0f, 1.0f, 0.01f)
FastLED's Elegant JSON Library: fl::json
#define FL_WARN(...)
Definition log.h:437
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
Definition unique_ptr.h:263
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
Definition cstring.h:60
int read() FL_NO_EXCEPT
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
Public include for fl::net::OTA.
unsigned char uint8_t
Definition stdint.h:208
fl::u32 uint32_t
Definition stdint.h:218
fl::u16 uint16_t
Definition stdint.h:213
Unified cross-platform Watchdog Timer API for FastLED.
fl::net::wifi — platform-neutral WiFi connection API (FastLED#3576 Phase 6).