11#if defined(PICO_CYW43_SUPPORTED)
23#if FL_PLATFORM_HAS_LARGE_MEMORY
43#if defined(FL_IS_ESP32)
44#include "soc/soc_caps.h"
47#if defined(FL_IS_ESP32) && defined(SOC_WIFI_SUPPORTED) && SOC_WIFI_SUPPORTED
64#include <esp_http_client.h>
68 fl::unique_ptr<fl::asio::http::Server> http_server;
69 esp_netif_t* netif_ap =
nullptr;
70 bool event_loop_initialized =
false;
71 bool wifi_initialized =
false;
74EspNetState& espNetState() {
82static bool initWifiAP() {
88 esp_err_t err = esp_netif_init();
89 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
90 FL_WARN(
"[NET] esp_netif_init failed: " << esp_err_to_name(err));
95 if (!espNetState().event_loop_initialized) {
96 err = esp_event_loop_create_default();
97 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
98 FL_WARN(
"[NET] esp_event_loop_create_default failed: " << esp_err_to_name(err));
101 espNetState().event_loop_initialized =
true;
105 if (!espNetState().netif_ap) {
106 espNetState().netif_ap = esp_netif_create_default_wifi_ap();
107 if (!espNetState().netif_ap) {
108 FL_WARN(
"[NET] esp_netif_create_default_wifi_ap failed");
114 if (!espNetState().wifi_initialized) {
115 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
116 err = esp_wifi_init(&cfg);
117 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
118 FL_WARN(
"[NET] esp_wifi_init failed: " << esp_err_to_name(err));
121 espNetState().wifi_initialized =
true;
125 wifi_config_t wifi_config = {};
130 wifi_config.ap.authmode = WIFI_AUTH_WPA2_PSK;
131 wifi_config.ap.channel = 1;
133 err = esp_wifi_set_mode(WIFI_MODE_AP);
135 FL_WARN(
"[NET] esp_wifi_set_mode failed: " << esp_err_to_name(err));
139 err = esp_wifi_set_config(WIFI_IF_AP, &wifi_config);
141 FL_WARN(
"[NET] esp_wifi_set_config failed: " << esp_err_to_name(err));
145 err = esp_wifi_start();
147 FL_WARN(
"[NET] esp_wifi_start failed: " << esp_err_to_name(err));
160static bool startHttpServer() {
161 if (espNetState().http_server.get()) {
180#if defined(FL_IS_ESP_32S3)
181 const char* chip =
"esp32s3";
182#elif defined(FL_IS_ESP_32C6)
183 const char* chip =
"esp32c6";
184#elif defined(FL_IS_ESP_32C3)
185 const char* chip =
"esp32c3";
187 const char* chip =
"esp32";
190 "{\"uptime_ms\":%u,\"free_heap\":%u,\"chip\":\"%s\"}",
191 static_cast<unsigned>(
millis()),
192 static_cast<unsigned>(ESP.getFreeHeap()), chip);
194 resp.
header(
"Content-Type",
"application/json");
209 resp.
header(
"Content-Type",
"application/json");
218 "{\"jsonrpc\":\"2.0\",\"result\":{\"methods\":[\"rpc.discover\",\"ping\",\"debugTest\",\"status\"]},\"id\":1}");
219 resp.
header(
"Content-Type",
"application/json");
224 "{\"jsonrpc\":\"2.0\",\"result\":{\"pong\":true},\"id\":1}");
225 resp.
header(
"Content-Type",
"application/json");
230 "{\"jsonrpc\":\"2.0\",\"result\":{\"ready\":true},\"id\":1}");
231 resp.
header(
"Content-Type",
"application/json");
236 resp.
body(
"{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"Method not found\"},\"id\":1}");
237 resp.
header(
"Content-Type",
"application/json");
245 "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32600,\"message\":\"Invalid Request\"},\"id\":1}");
246 resp.
header(
"Content-Type",
"application/json");
250 "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"Method not found\"},\"id\":1}";
253 "{\"jsonrpc\":\"2.0\",\"result\":{\"methods\":[\"rpc.discover\",\"ping\",\"debugTest\",\"status\"]},\"id\":1}";
255 response =
"{\"jsonrpc\":\"2.0\",\"result\":{\"pong\":true},\"id\":1}";
257 response =
"{\"jsonrpc\":\"2.0\",\"result\":{\"ready\":true},\"id\":1}";
260 resp.
header(
"Content-Type",
"application/json");
264 if (!espNetState().http_server->start(
getNetState().server_port)) {
265 FL_WARN(
"[NET] HTTP server failed to start: " << espNetState().http_server->last_error().c_str());
266 espNetState().http_server.reset();
280static bool initNetifForLoopback() {
281 esp_err_t err = esp_netif_init();
282 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
283 FL_WARN(
"[NET] esp_netif_init failed: " << esp_err_to_name(err));
287 if (!espNetState().event_loop_initialized) {
288 err = esp_event_loop_create_default();
289 if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
290 FL_WARN(
"[NET] esp_event_loop_create_default failed: "
291 << esp_err_to_name(err));
294 espNetState().event_loop_initialized =
true;
299struct HttpResponseCapture {
302 bool truncated =
false;
305struct HttpResponseHash {
310static esp_err_t captureHttpResponse(esp_http_client_event_t* event) {
311 if (event->event_id != HTTP_EVENT_ON_DATA || event->data ==
nullptr ||
312 event->data_len <= 0 || event->user_data ==
nullptr) {
316 static_cast<HttpResponseCapture*
>(
event->user_data);
318 const size_t incoming =
static_cast<size_t>(
event->data_len);
327static esp_err_t hashHttpResponse(esp_http_client_event_t* event) {
328 if (event->event_id != HTTP_EVENT_ON_DATA || event->data ==
nullptr ||
329 event->data_len <= 0 || event->user_data ==
nullptr) {
332 HttpResponseHash*
response =
static_cast<HttpResponseHash*
>(
event->user_data);
333 const char* data =
static_cast<const char*
>(
event->data);
334 for (
int i = 0; i <
event->data_len; ++i) {
338 response->bytes +=
static_cast<size_t>(
event->data_len);
344 for (
size_t i = 0; i <
length; ++i) {
351static fl::json runHttpPayloadEchoTest(
const char* url) {
352 constexpr size_t kPayloadBytes = 4096;
354 for (
size_t i = 0; i < payload.
size(); ++i) {
355 payload[i] =
static_cast<char>(
'A' +
static_cast<char>(i % 23));
360 result.set(
"test",
"POST /echo 4096-byte FNV-1a");
361 result.set(
"bytes",
static_cast<int64_t
>(payload.
size()));
362 result.set(
"expected_hash",
static_cast<int64_t
>(expected_hash));
365 esp_http_client_config_t config = {};
367 config.event_handler = hashHttpResponse;
369 config.timeout_ms = 4000;
370 esp_http_client_handle_t client = esp_http_client_init(&config);
372 result.set(
"passed",
false);
373 result.set(
"error",
"Failed to init HTTP client");
376 esp_http_client_set_method(client, HTTP_METHOD_POST);
377 esp_http_client_set_header(client,
"Content-Type",
"application/octet-stream");
378 esp_http_client_set_post_field(client, payload.
data(),
379 static_cast<int>(payload.
size()));
381 const esp_err_t err = esp_http_client_perform(client);
382 const int status = esp_http_client_get_status_code(client);
383 esp_http_client_cleanup(client);
384 const bool passed = err == ESP_OK &&
status == 200 &&
387 result.set(
"status_code",
static_cast<int64_t
>(status));
388 result.set(
"received_bytes",
static_cast<int64_t
>(
response.bytes));
390 result.set(
"passed", passed);
392 result.set(
"error",
"Echo payload hash mismatch");
398static fl::json runHttpRequestTest(
const char* url,
const char* test_name,
399 const char* request_body,
401 const char* expected_fragment) {
403 result.set(
"test", test_name);
406 esp_http_client_config_t config = {};
408 config.event_handler = captureHttpResponse;
414 config.timeout_ms = 2000;
416 esp_http_client_handle_t client = esp_http_client_init(&config);
418 result.set(
"passed",
false);
419 result.set(
"error",
"Failed to init HTTP client");
423 if (request_body !=
nullptr) {
424 esp_http_client_set_method(client, HTTP_METHOD_POST);
425 esp_http_client_set_header(client,
"Content-Type",
"application/json");
426 esp_http_client_set_post_field(client, request_body,
430 esp_err_t err = esp_http_client_perform(client);
432 result.set(
"passed",
false);
434 ss <<
"HTTP request failed: " << esp_err_to_name(err);
436 esp_http_client_cleanup(client);
440 int status = esp_http_client_get_status_code(client);
441 result.set(
"status_code",
static_cast<int64_t
>(status));
443 const bool status_ok =
status == expected_status;
444 const bool content_ok = expected_fragment ==
nullptr ||
447 if (!status_ok || !content_ok ||
capture.truncated) {
448 result.set(
"passed",
false);
451 ss <<
"Expected status " << expected_status <<
", got " <<
status;
452 }
else if (
capture.truncated) {
453 ss <<
"Response body exceeded capture limit";
455 ss <<
"Unexpected response body";
459 result.set(
"passed",
true);
462 esp_http_client_cleanup(client);
475 response.set(
"error",
"Failed to start WiFi AP");
479 if (!startHttpServer()) {
481 response.set(
"error",
"Failed to start HTTP server");
498 response.set(
"error",
"Failed to start WiFi AP");
517 char url_status[128];
519 char url_rpc_discover[128];
520 char url_rpc_ping[128];
521 char url_rpc_status[128];
522 char url_rpc_unknown[128];
525 snprintf(url_ping,
sizeof(url_ping),
"http://%s:%u/ping", host_ip, port);
526 snprintf(url_status,
sizeof(url_status),
"http://%s:%u/status", host_ip, port);
527 snprintf(url_leds,
sizeof(url_leds),
"http://%s:%u/leds", host_ip, port);
528 snprintf(url_rpc_discover,
sizeof(url_rpc_discover),
"http://%s:%u/rpc/discover", host_ip, port);
529 snprintf(url_rpc_ping,
sizeof(url_rpc_ping),
"http://%s:%u/rpc/ping", host_ip, port);
530 snprintf(url_rpc_status,
sizeof(url_rpc_status),
"http://%s:%u/rpc/status", host_ip, port);
531 snprintf(url_rpc_unknown,
sizeof(url_rpc_unknown),
"http://%s:%u/rpc/unknown", host_ip, port);
532 snprintf(url_rpc,
sizeof(url_rpc),
"http://%s:%u/rpc", host_ip, port);
533 snprintf(url_echo,
sizeof(url_echo),
"http://%s:%u/echo", host_ip, port);
537 fl::json r = runHttpRequestTest(url_ping,
"GET /ping",
nullptr, 200,
540 if (passed.has_value() && passed.value()) {
552 fl::json r = runHttpRequestTest(url_status,
"GET /status",
nullptr, 200,
553 "\"chip\":\"rp2350w\"");
555 if (passed.has_value() && passed.value()) {
567 fl::json r = runHttpRequestTest(url_leds,
"GET /leds",
nullptr, 200,
570 if (passed.has_value() && passed.value()) {
578 const char* rpc_urls[] = {url_rpc_discover, url_rpc_ping, url_rpc_status,
580 const char* rpc_names[] = {
"GET /rpc/discover",
"GET /rpc/ping",
581 "GET /rpc/status",
"GET /rpc/unknown"};
582 const int rpc_statuses[] = {200, 200, 200, 404};
583 const char* rpc_expected[] = {
"\"methods\"",
"\"pong\":true",
584 "\"ready\":true",
"-32601"};
585 for (
size_t i = 0; i < 4; ++i) {
586 fl::json r = runHttpRequestTest(rpc_urls[i], rpc_names[i],
nullptr,
587 rpc_statuses[i], rpc_expected[i]);
589 if (passed.has_value() && passed.value()) {
598 const char* rpc_post_names[] = {
"POST /rpc discover",
"POST /rpc ping",
599 "POST /rpc status",
"POST /rpc unknown"};
600 const char* rpc_post_bodies[] = {
601 "{\"jsonrpc\":\"2.0\",\"method\":\"rpc.discover\",\"id\":1}",
602 "{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}",
603 "{\"jsonrpc\":\"2.0\",\"method\":\"status\",\"id\":1}",
604 "{\"jsonrpc\":\"2.0\",\"method\":\"unknown\",\"id\":1}",
606 const char* rpc_post_expected[] = {
"\"methods\"",
"\"pong\":true",
607 "\"ready\":true",
"-32601"};
608 for (
size_t i = 0; i < 4; ++i) {
609 fl::json r = runHttpRequestTest(url_rpc, rpc_post_names[i],
610 rpc_post_bodies[i], 200,
611 rpc_post_expected[i]);
613 if (passed.has_value() && passed.value()) {
622 fl::json payload_result = runHttpPayloadEchoTest(url_echo);
624 if (payload_passed.has_value() && payload_passed.value()) {
642 response.set(
"error",
"netServerStats is RP-only");
653 if (!initNetifForLoopback()) {
655 response.set(
"error",
"Failed to initialize network stack for loopback");
660 if (!startHttpServer()) {
662 response.set(
"error",
"Failed to start HTTP server for loopback test");
673 char url_status[128];
675 snprintf(url_ping,
sizeof(url_ping),
"http://127.0.0.1:%u/ping",
677 snprintf(url_status,
sizeof(url_status),
"http://127.0.0.1:%u/status",
679 snprintf(url_leds,
sizeof(url_leds),
"http://127.0.0.1:%u/leds",
684 fl::json r = runHttpRequestTest(url_ping,
"GET /ping (loopback)",
685 nullptr, 200,
"pong");
687 if (passed.has_value() && passed.value()) {
697 fl::json r = runHttpRequestTest(url_status,
"GET /status (loopback)",
698 nullptr, 200,
"\"chip\"");
700 if (passed.has_value() && passed.value()) {
710 fl::json r = runHttpRequestTest(url_leds,
"GET /leds (loopback)",
711 nullptr, 200,
"num_leds");
713 if (passed.has_value() && passed.value()) {
732 if (espNetState().http_server.get()) {
733 espNetState().http_server->stop();
734 espNetState().http_server.reset();
736 FL_WARN(
"[NET] HTTP server stopped");
743 FL_WARN(
"[NET] WiFi AP stopped");
752#elif defined(FL_IS_RP2350) && defined(PICO_CYW43_SUPPORTED)
801constexpr uint32_t kRpPeerStallMs = 6000;
804constexpr uint32_t kRpPeerMaxRequestMs = 10000;
807 fl::unique_ptr<WiFiServer>
server;
808 fl::unique_ptr<WiFiClient> client;
809 char request[512] = {};
810 size_t request_length = 0;
811 char body[4097] = {};
812 size_t body_length = 0;
813 size_t expected_body_length = 0;
814 bool headers_complete =
false;
815 bool response_complete =
false;
822 bool last_timeout_was_stall =
false;
823 size_t last_timeout_body_length = 0;
824 size_t last_timeout_expected_body = 0;
825 uint32_t last_timeout_elapsed_ms = 0;
826 bool last_timeout_headers_complete =
false;
827 char last_timeout_request[64] = {};
837RpPeerState& rpPeerState() {
841void resetRpPeerRequest(RpPeerState&
state) {
842 state.request_length = 0;
843 state.body_length = 0;
844 state.expected_body_length = 0;
845 state.headers_complete =
false;
846 state.response_complete =
false;
847 state.request_started_ms = 0;
848 state.last_progress_ms = 0;
849 state.request[0] =
'\0';
850 state.body[0] =
'\0';
853void writeHttpResponse(WiFiClient& client,
const char* body,
854 const char* content_type) {
855 client.print(
"HTTP/1.1 200 OK\r\nContent-Type: ");
856 client.print(content_type);
857 client.print(
"\r\nContent-Length: ");
859 client.print(
"\r\nConnection: close\r\n\r\n");
863void writeHttpNotFoundResponse(WiFiClient& client,
const char* body) {
864 client.print(
"HTTP/1.1 404 Not Found\r\nContent-Type: application/json\r\nContent-Length: ");
866 client.print(
"\r\nConnection: close\r\n\r\n");
870void writeHttpErrorResponse(WiFiClient& client,
const char* status,
872 const char* content_type =
"text/plain") {
873 client.print(
"HTTP/1.1 ");
874 client.print(status);
875 client.print(
"\r\nContent-Type: ");
876 client.print(content_type);
877 client.print(
"\r\nContent-Length: ");
879 client.print(
"\r\nConnection: close\r\n\r\n");
901constexpr uint32_t kRpHttpClientStallMs = 6000;
907constexpr uint32_t kRpHttpClientMaxMs = kRpPeerMaxRequestMs;
910 const char* method,
const char* path,
911 const char* request_body,
const char* test_name,
913 const char* expected_fragment) {
915 result.set(
"test", test_name);
922 uint32_t last_progress_ms = exchange_started_ms;
923 if (!client.connect(host_ip, port)) {
924 result.set(
"passed",
false);
925 result.set(
"error",
"TCP connect failed");
929 client.print(method);
932 client.print(
" HTTP/1.1\r\nHost: ");
933 client.print(host_ip);
934 if (request_body !=
nullptr) {
935 client.print(
"\r\nContent-Type: application/json\r\nContent-Length: ");
938 client.print(
"\r\nConnection: close\r\n\r\n");
939 if (request_body !=
nullptr) {
940 client.print(request_body);
949 const uint32_t hard_deadline_ms = exchange_started_ms + kRpHttpClientMaxMs;
950 const auto expired = [&]() ->
bool {
952 return static_cast<int32_t>(now - last_progress_ms) >= kRpHttpClientStallMs ||
953 static_cast<int32_t>(now - hard_deadline_ms) >= 0;
955 while (!client.available() && client.connected() && !expired()) {
971 size_t last_line_bytes_read = 0;
972 auto read_line = [&](
char* out,
size_t out_size) ->
bool {
975 if (!client.available()) {
976 if (!client.connected()) {
983 const int ch = client.read();
987 last_progress_ms =
millis();
992 if (ch !=
'\r' && used + 1 < out_size) {
993 out[used++] =
static_cast<char>(ch);
1005 char status_line[64];
1006 const bool status_line_complete = read_line(status_line,
sizeof(status_line));
1007 const bool status_bytes_seen = last_line_bytes_read > 0;
1011 const bool peer_open_after_status = client.connected();
1019 long content_length = -1;
1021 while (read_line(header,
sizeof(header))) {
1022 if (header[0] ==
'\0') {
1025 if (content_length < 0 &&
1026 fl::strncmp(header,
"Content-Length:", 15) == 0) {
1027 const char* value = header + 15;
1028 while (*value ==
' ') {
1039 size_t response_length = 0;
1041 while (!expired()) {
1042 if (content_length >= 0 && body_read >= content_length) {
1045 if (client.available()) {
1046 const int ch = client.read();
1050 last_progress_ms =
millis();
1054 if (response_length + 1 <
sizeof(response)) {
1055 response[response_length++] =
static_cast<char>(ch);
1059 if (!client.connected()) {
1069 const bool peer_open_after_body = client.connected();
1074 char expected_status_code[4] = {};
1075 fl::snprintf(expected_status_code,
sizeof(expected_status_code),
"%d",
1077 const bool has_http_prefix =
fl::strncmp(status_line,
"HTTP/1.1 ", 9) == 0 ||
1079 const bool passed = has_http_prefix &&
1080 fl::strncmp(status_line + 9, expected_status_code, 3) == 0;
1081 const bool content_ok = expected_fragment ==
nullptr ||
1082 fl::strstr(response, expected_fragment) !=
nullptr;
1093 const bool body_complete = content_length < 0 || body_read >= content_length;
1095 result.set(
"status_line", status_line);
1096 result.set(
"content_length",
static_cast<int32_t>(content_length));
1098 const bool failed = !(passed && content_ok && body_complete);
1099 result.set(
"passed", !failed);
1105 result.set(
"peer_open_after_status", peer_open_after_status);
1106 result.set(
"peer_open_after_body", peer_open_after_body);
1113 if (!status_line_complete && !status_bytes_seen) {
1114 result.set(
"error",
"No HTTP response: request sent, nothing read");
1115 }
else if (!status_line_complete) {
1116 result.set(
"error",
"Truncated HTTP status line");
1118 result.set(
"error",
"Unexpected HTTP status");
1120 }
else if (!body_complete) {
1121 result.set(
"error",
"Truncated response body");
1122 }
else if (!content_ok) {
1123 result.set(
"error",
"Unexpected response body");
1130 for (
size_t i = 0; i <
length; ++i) {
1138 constexpr size_t kPayloadBytes = 4096;
1140 for (
size_t i = 0; i < payload.
size(); ++i) {
1141 payload[i] =
static_cast<char>(
'A' +
static_cast<char>(i % 23));
1146 result.set(
"test",
"POST /echo 4096-byte FNV-1a");
1147 result.set(
"bytes",
static_cast<int64_t
>(payload.
size()));
1148 result.set(
"expected_hash",
static_cast<int64_t
>(expected_hash));
1155 uint32_t echo_progress_ms = echo_started_ms;
1156 if (!client.connect(host_ip, port)) {
1157 result.set(
"passed",
false);
1158 result.set(
"error",
"TCP connect failed");
1161 client.print(
"POST /echo HTTP/1.1\r\nHost: ");
1162 client.print(host_ip);
1163 client.print(
"\r\nContent-Type: application/octet-stream\r\nContent-Length: ");
1164 client.print(payload.
size());
1165 client.print(
"\r\nConnection: close\r\n\r\n");
1166 client.write(
reinterpret_cast<const uint8_t*
>(payload.
data()), payload.
size());
1171 const uint32_t echo_hard_deadline_ms =
1172 echo_started_ms + kRpHttpClientMaxMs;
1173 const auto echo_expired = [&]() ->
bool {
1175 return static_cast<int32_t>(now - echo_progress_ms) >= kRpHttpClientStallMs ||
1176 static_cast<int32_t>(now - echo_hard_deadline_ms) >= 0;
1178 char status_line[64] = {};
1179 size_t status_length = 0;
1180 bool status_done =
false;
1181 bool header_done =
false;
1183 uint32_t received_hash = 2166136261u;
1184 size_t received_bytes = 0;
1185 while (!echo_expired() && (client.connected() || client.available())) {
1186 while (client.available()) {
1190 if (echo_expired())
break;
1191 const int value = client.read();
1195 echo_progress_ms =
millis();
1196 const char ch =
static_cast<char>(value);
1200 }
else if (ch !=
'\r' && status_length + 1 <
sizeof(status_line)) {
1201 status_line[status_length++] = ch;
1206 header_match = ch ==
"\r\n\r\n"[header_match]
1207 ?
static_cast<uint8_t>(header_match + 1) : 0;
1208 if (header_match == 4) {
1213 received_hash ^=
static_cast<uint8_t>(ch);
1214 received_hash *= 16777619u;
1221 status_line[status_length] =
'\0';
1222 const bool status_ok =
fl::strncmp(status_line,
"HTTP/1.1 200", 12) == 0 ||
1223 fl::strncmp(status_line,
"HTTP/1.0 200", 12) == 0;
1224 const bool passed = status_ok && received_bytes == payload.
size() &&
1225 received_hash == expected_hash;
1226 result.set(
"received_bytes",
static_cast<int64_t
>(received_bytes));
1227 result.set(
"received_hash",
static_cast<int64_t
>(received_hash));
1228 result.set(
"passed", passed);
1230 result.set(
"error",
"Echo payload hash mismatch");
1241 response.set(
"error",
"WiFi station is not connected");
1245 RpPeerState&
state = rpPeerState();
1246 if (!
state.server) {
1250 state.server->begin();
1251 if (*
state.server) {
1252 state.server->setNoDelay(
true);
1258 if (!*
state.server) {
1259 state.server.reset();
1261 response.set(
"error",
"TCP server failed to listen");
1281 if (host_ip ==
nullptr || *host_ip ==
'\0' ||
1284 response.set(
"error",
"WiFi station is not connected");
1289 const char* paths[] = {
"/ping",
"/status",
"/leds",
"/rpc/discover",
1290 "/rpc/ping",
"/rpc/status",
"/rpc/unknown"};
1291 const char* names[] = {
"GET /ping",
"GET /status",
"GET /leds",
1292 "GET /rpc/discover",
"GET /rpc/ping",
"GET /rpc/status",
1293 "GET /rpc/unknown"};
1294 const int expected_statuses[] = {200, 200, 200, 200, 200, 200, 404};
1295 const char* expected[] = {
"pong",
"\"chip\"",
"num_leds",
"\"methods\"",
1296 "\"pong\":true",
"\"ready\":true",
"-32601"};
1298 for (
size_t i = 0; i < 7; ++i) {
1299 fl::json result = runRpHttpRequestTest(host_ip, port,
"GET", paths[i],
1301 expected_statuses[i], expected[i]);
1303 if (result_passed.has_value() && result_passed.value()) {
1310 const char* rpc_post_names[] = {
"POST /rpc discover",
"POST /rpc ping",
1311 "POST /rpc status",
"POST /rpc unknown"};
1312 const char* rpc_post_bodies[] = {
1313 "{\"jsonrpc\":\"2.0\",\"method\":\"rpc.discover\",\"id\":1}",
1314 "{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}",
1315 "{\"jsonrpc\":\"2.0\",\"method\":\"status\",\"id\":1}",
1316 "{\"jsonrpc\":\"2.0\",\"method\":\"unknown\",\"id\":1}",
1318 const char* rpc_post_expected[] = {
"\"methods\"",
"\"pong\":true",
1319 "\"ready\":true",
"-32601"};
1320 for (
size_t i = 0; i < 4; ++i) {
1321 fl::json result = runRpHttpRequestTest(host_ip, port,
"POST",
"/rpc",
1322 rpc_post_bodies[i], rpc_post_names[i],
1323 200, rpc_post_expected[i]);
1325 if (result_passed.has_value() && result_passed.value()) {
1332 fl::json payload_result = runRpHttpPayloadEchoTest(host_ip, port);
1334 if (payload_passed.has_value() && payload_passed.value()) {
1339 response.set(
"success", passed == 12);
1340 response.set(
"tests_passed",
static_cast<int64_t
>(passed));
1341 response.set(
"tests_failed",
static_cast<int64_t
>(12 - passed));
1347 RpPeerState&
state = rpPeerState();
1350 response.set(
"timeoutCount",
static_cast<int64_t
>(
state.timeout_count));
1351 response.set(
"lastTimeoutWasStall",
state.last_timeout_was_stall);
1352 response.set(
"lastTimeoutBodyLength",
1353 static_cast<int64_t
>(
state.last_timeout_body_length));
1354 response.set(
"lastTimeoutExpectedBody",
1355 static_cast<int64_t
>(
state.last_timeout_expected_body));
1356 response.set(
"lastTimeoutElapsedMs",
1357 static_cast<int64_t
>(
state.last_timeout_elapsed_ms));
1358 response.set(
"lastTimeoutHeadersComplete",
state.last_timeout_headers_complete);
1359 response.set(
"lastTimeoutRequest",
state.last_timeout_request);
1366 response.set(
"error",
"Loopback is not applicable to the CYW43 peer path");
1372 RpPeerState&
state = rpPeerState();
1374 state.client->stop();
1375 state.client.reset();
1377 resetRpPeerRequest(
state);
1379 state.server->stop();
1380 state.server.reset();
1390 RpPeerState&
state = rpPeerState();
1391 if (!
state.server) {
1394 if (!
state.client &&
state.server->hasClient()) {
1396 resetRpPeerRequest(
state);
1398 state.last_progress_ms =
state.request_started_ms;
1400 if (!
state.client) {
1403 if (
state.response_complete && !
state.client->connected()) {
1404 state.client.reset();
1405 resetRpPeerRequest(
state);
1416 const bool stalled =
1417 static_cast<int32_t>(now_ms -
state.last_progress_ms) >= kRpPeerStallMs;
1418 const bool over_budget =
1419 static_cast<int32_t>(now_ms -
state.request_started_ms) >= kRpPeerMaxRequestMs;
1420 if (stalled || over_budget) {
1421 if (!
state.response_complete) {
1422 ++
state.timeout_count;
1423 state.last_timeout_was_stall = stalled;
1424 state.last_timeout_body_length =
state.body_length;
1425 state.last_timeout_expected_body =
state.expected_body_length;
1426 state.last_timeout_elapsed_ms = now_ms -
state.request_started_ms;
1427 state.last_timeout_headers_complete =
state.headers_complete;
1428 fl::size copy_len =
state.request_length;
1429 if (copy_len >=
sizeof(
state.last_timeout_request)) {
1430 copy_len =
sizeof(
state.last_timeout_request) - 1;
1433 state.last_timeout_request[copy_len] =
'\0';
1434 writeHttpErrorResponse(*
state.client,
"408 Request Timeout",
"");
1436 state.client->stop();
1437 state.client.reset();
1438 resetRpPeerRequest(
state);
1441 if (
state.response_complete) {
1445 while (
state.client->available() && !
state.headers_complete &&
1446 state.request_length + 1 <
sizeof(
state.request)) {
1447 const int ch =
state.client->read();
1451 state.request[
state.request_length++] =
static_cast<char>(ch);
1454 if (
state.request_length >= 4 &&
1456 state.headers_complete =
true;
1459 if (!
state.headers_complete) {
1460 if (
state.request_length + 1 ==
sizeof(
state.request)) {
1461 writeHttpErrorResponse(*
state.client,
1462 "431 Request Header Fields Too Large",
"");
1463 state.response_complete =
true;
1469 const bool is_post_rpc =
fl::strncmp(
state.request,
"POST /rpc ", 10) == 0;
1470 const bool is_post_echo =
fl::strncmp(
state.request,
"POST /echo ", 11) == 0;
1471 if (is_post &&
state.expected_body_length == 0) {
1472 const char* content_length =
fl::strstr(
state.request,
"\r\nContent-Length:");
1473 if (content_length ==
nullptr) {
1474 writeHttpErrorResponse(*
state.client,
"411 Length Required",
"");
1475 state.response_complete =
true;
1478 content_length =
fl::strstr(content_length,
":");
1480 while (*content_length ==
' ') {
1483 while (*content_length >=
'0' && *content_length <=
'9') {
1484 if (
state.expected_body_length > (
sizeof(
state.body) - 1) / 10) {
1485 writeHttpErrorResponse(*
state.client,
"413 Payload Too Large",
"");
1486 state.response_complete =
true;
1489 state.expected_body_length =
state.expected_body_length * 10 +
1490 static_cast<size_t>(*content_length -
'0');
1491 if (
state.expected_body_length >=
sizeof(
state.body)) {
1492 writeHttpErrorResponse(*
state.client,
"413 Payload Too Large",
"");
1493 state.response_complete =
true;
1498 if (
state.expected_body_length == 0) {
1499 writeHttpErrorResponse(*
state.client,
"400 Bad Request",
"");
1500 state.response_complete =
true;
1504 while (is_post &&
state.client->available() &&
1505 state.body_length <
state.expected_body_length) {
1506 const int ch =
state.client->read();
1510 state.body[
state.body_length++] =
static_cast<char>(ch);
1513 if (is_post &&
state.body_length <
state.expected_body_length) {
1522 writeHttpResponse(*
state.client,
"pong",
"text/plain");
1524 writeHttpResponse(*
state.client,
1525 "{\"chip\":\"rp2350w\",\"network\":\"cyw43\"}",
1526 "application/json");
1528 writeHttpResponse(*
state.client,
"{\"num_leds\":10,\"brightness\":64}",
1529 "application/json");
1531 writeHttpResponse(*
state.client,
1532 "{\"jsonrpc\":\"2.0\",\"result\":{\"methods\":[\"rpc.discover\",\"ping\",\"debugTest\",\"status\"]},\"id\":1}",
1533 "application/json");
1535 writeHttpResponse(*
state.client,
1536 "{\"jsonrpc\":\"2.0\",\"result\":{\"pong\":true},\"id\":1}",
1537 "application/json");
1539 writeHttpResponse(*
state.client,
1540 "{\"jsonrpc\":\"2.0\",\"result\":{\"ready\":true},\"id\":1}",
1541 "application/json");
1543 writeHttpNotFoundResponse(*
state.client,
1544 "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"Method not found\"},\"id\":1}");
1545 }
else if (is_post_echo) {
1546 writeHttpResponse(*
state.client,
state.body,
"application/octet-stream");
1547 }
else if (is_post_rpc) {
1550 writeHttpErrorResponse(
1551 *
state.client,
"400 Bad Request",
1552 "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32600,\"message\":\"Invalid Request\"},\"id\":1}",
1553 "application/json");
1555 writeHttpResponse(*
state.client,
1556 "{\"jsonrpc\":\"2.0\",\"result\":{\"methods\":[\"rpc.discover\",\"ping\",\"debugTest\",\"status\"]},\"id\":1}",
1557 "application/json");
1559 writeHttpResponse(*
state.client,
1560 "{\"jsonrpc\":\"2.0\",\"result\":{\"pong\":true},\"id\":1}",
1561 "application/json");
1563 writeHttpResponse(*
state.client,
1564 "{\"jsonrpc\":\"2.0\",\"result\":{\"ready\":true},\"id\":1}",
1565 "application/json");
1567 writeHttpResponse(*
state.client,
1568 "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32601,\"message\":\"Method not found\"},\"id\":1}",
1569 "application/json");
1572 writeHttpErrorResponse(*
state.client,
"404 Not Found",
"");
1574 state.response_complete =
true;
1582 response.set(
"error",
"netServerStats is RP-only");
1593 response.set(
"error",
"Net autoresearch only supported on ESP32");
1600 response.set(
"error",
"Net autoresearch only supported on ESP32");
1609 response.set(
"error",
"Net autoresearch only supported on ESP32");
1616 response.set(
"error",
"Net loopback autoresearch only supported on ESP32");
#define AUTORESEARCH_NET_SSID
#define AUTORESEARCH_NET_PASSWORD
#define AUTORESEARCH_NET_MAX_CONNECTIONS
void pollNetServer()
Service pending peer HTTP requests from the sketch loop.
fl::json runNetLoopback()
Run self-contained loopback test: start HTTP server, client GETs localhost.
#define AUTORESEARCH_NET_AP_IP
fl::json startNetClient()
Start WiFi Soft AP only (for net-client mode).
AutoResearchNetState & getNetState()
Get current network autoresearch state.
fl::json runNetClientTest(const char *host_ip, uint16_t port)
Run HTTP client tests against a host server.
#define AUTORESEARCH_NET_SERVER_PORT
fl::json startNetServer()
Start WiFi Soft AP and HTTP server with test endpoints.
fl::json stopNet()
Stop WiFi AP and HTTP server/client, release all resources.
fl::json netServerStats()
Why the RP peer server last abandoned a request (FastLED#3899 diagnostics).
State for network autoresearch.
size_t capture(fl::shared_ptr< fl::RxChannel > rx_channel, fl::span< uint8_t > rx_buffer, size_t expected_data_bytes, int tx_pin, const fl::ChipsetTimingConfig &timing, const char *driver_name, fl::RunResult *diagnostics=nullptr)
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
static T & instance() FL_NO_EXCEPT
pointer data() FL_NO_EXCEPT
fl::size size() const FL_NO_EXCEPT
A fixed-size array implementation similar to std::array.
const string & body() const FL_NO_EXCEPT
Get request body (for POST/PUT requests)
bool has_body() const FL_NO_EXCEPT
Check if request has a body.
HTTP request object (immutable, passed by const reference)
Response & header(const string &name, const string &value) FL_NO_EXCEPT
Add HTTP header.
Response & body(const string &content) FL_NO_EXCEPT
Set response body.
static Response bad_request(const string &message) FL_NO_EXCEPT
Factory method for 400 Bad Request response.
static Response ok(const string &body="") FL_NO_EXCEPT
Factory method for 200 OK response.
static Response not_found() FL_NO_EXCEPT
Factory method for 404 Not Found response.
HTTP response builder (fluent interface)
const char * c_str() const FL_NO_EXCEPT
fl::optional< bool > as_bool() const FL_NO_EXCEPT
void push_back(const json &value) FL_NO_EXCEPT
static json object() FL_NO_EXCEPT
static json array() FL_NO_EXCEPT
string str() 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::u32 fnv1a(fl::u32 hash, const fl::i16 *samples, fl::u32 count)
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
u16 hash(i32 x, i32 y) 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
bool isConnected() FL_NO_EXCEPT
True when the station interface has an IP.
void stop() FL_NO_EXCEPT
Stop WiFi entirely (STA and AP).
Status status() FL_NO_EXCEPT
Current connection state.
fl::string ipAddress() FL_NO_EXCEPT
Dotted-quad IP of the station interface ("" until connected).
FASTLED_FORCE_INLINE long atol(const char *str) FL_NO_EXCEPT
int snprintf(char *buffer, fl::size size, const char *format) FL_NO_EXCEPT
void delay(u32 ms, bool run_async=true) FL_NO_EXCEPT
Public delay wrapper that keeps bare Arduino delay() preferred after using fl::delay; while still all...
fl::u32 millis() FL_NO_EXCEPT
Universal millisecond timer - returns milliseconds since system startup.
FASTLED_FORCE_INLINE void * memcpy(void *dest, const void *src, size_t n) FL_NO_EXCEPT
FASTLED_FORCE_INLINE const char * strstr(const char *haystack, const char *needle) FL_NO_EXCEPT
expected< T, E > result
Alias for expected (Rust-style naming)
int available() FL_NO_EXCEPT
FASTLED_FORCE_INLINE int strncmp(const char *s1, const char *s2, size_t n) FL_NO_EXCEPT
FASTLED_FORCE_INLINE size_t strlen(const char *s) FL_NO_EXCEPT
fl::net::wifi — platform-neutral WiFi connection API (FastLED#3576 Phase 6).