10#include "platforms/esp/is_esp.h"
21#include "platforms/esp/esp_version.h"
22#if defined(FL_IS_ESP32) && !defined(FASTLED_HAS_NETWORKING) && ESP_IDF_VERSION_4_OR_HIGHER
25#include <esp_http_server.h>
29#ifdef FASTLED_HAS_NETWORKING
31#include "platforms/time_platform.h"
35 #include "platforms/win/socket_win.h"
36 #define SOCKET_ERROR_WOULD_BLOCK WSAEWOULDBLOCK
37 #define SOCKET_ERROR_IN_PROGRESS WSAEINPROGRESS
39 #include "platforms/posix/socket_posix.h"
41 #define SOCKET_ERROR_WOULD_BLOCK EWOULDBLOCK
42 #define SOCKET_ERROR_IN_PROGRESS EINPROGRESS
65 return mServer && mServer->is_running();
69 return (mServer && mServer->is_running()) ? mServer->mClientSockets.size() : 0;
79constexpr u32 CONNECTION_TIMEOUT_MS = 30000;
82bool iequals(
const string& a,
const string& b)
FL_NO_EXCEPT {
83 if (a.size() != b.size())
return false;
84 for (
size_t i = 0; i < a.size(); ++i) {
93 while (start < s.size() &&
fl::isspace(s[start])) ++start;
94 size_t end = s.size();
96 return s.substr(start,
end - start);
100vector<string> split(
const string& s,
char delimiter)
FL_NO_EXCEPT {
101 vector<string> tokens;
103 size_t end = s.find(delimiter);
105 tokens.push_back(s.substr(start,
end - start));
107 end = s.find(delimiter, start);
109 tokens.push_back(s.substr(start));
116 if (query.empty() || query[0] !=
'?')
return params;
118 vector<string> pairs = split(query.substr(1),
'&');
119 for (
const auto& pair : pairs) {
120 size_t eq_pos = pair.find(
'=');
122 string key = pair.substr(0, eq_pos);
123 string value = pair.substr(eq_pos + 1);
133 case 200:
return "OK";
134 case 400:
return "Bad Request";
135 case 404:
return "Not Found";
136 case 500:
return "Internal Server Error";
137 default:
return "Unknown";
145 return ioctlsocket(fd, FIONBIO, &mode) == 0;
147 int flags = fcntl(fd, F_GETFL, 0);
148 if (flags == -1)
return false;
149 return fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
160 for (
auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
161 if (iequals(it->first, name)) {
169 auto it = mQuery.find(param);
170 if (it != mQuery.end()) {
181 mHeaders[
"Content-Type"] =
"text/plain";
190 mHeaders[name] =
value;
200 mBody = data.to_string();
201 mHeaders[
"Content-Type"] =
"application/json";
213 resp.status(404).body(
"Not Found\n");
241 result += it->first +
": " + it->second +
"\r\n";
278 mLastError =
"Server already running";
282 if (!setup_listen_socket(port)) {
310 if (client.fd != -1) {
330 mRoutes.push_back({method, path, handler});
334 route(
"GET", path, handler);
338 route(
"POST", path, handler);
342 route(
"PUT", path, handler);
346 route(
"DELETE", path, handler);
359 mListenSocket = socket(AF_INET, SOCK_STREAM, 0);
360 if (mListenSocket < 0) {
361 mLastError =
"Failed to create socket";
367 if (setsockopt(mListenSocket, SOL_SOCKET, SO_REUSEADDR,
368 (
const char*)(&opt),
sizeof(opt)) < 0) {
369 close(mListenSocket);
371 mLastError =
"Failed to set SO_REUSEADDR";
376 if (!set_nonblocking(mListenSocket)) {
377 close(mListenSocket);
379 mLastError =
"Failed to set non-blocking mode";
385 addr.sin_family = AF_INET;
386 addr.sin_addr.s_addr = INADDR_ANY;
387 addr.sin_port = htons(
static_cast<u16
>(port));
389 if (bind(mListenSocket, (sockaddr*)(&addr),
sizeof(addr)) < 0) {
390 close(mListenSocket);
392 mLastError =
"Failed to bind to port";
397 if (
listen(mListenSocket, 5) < 0) {
398 close(mListenSocket);
400 mLastError =
"Failed to listen on socket";
408 sockaddr_in client_addr{};
409 socklen_t addr_len =
sizeof(client_addr);
414 (sockaddr*)(&client_addr),
419 if (WSAGetLastError() == SOCKET_ERROR_WOULD_BLOCK)
break;
421 if (errno == EWOULDBLOCK || errno == EAGAIN)
break;
428 if (!set_nonblocking(client_fd)) {
435 client.
fd = client_fd;
436 client.connect_time = fl::platforms::millis();
442 size_t requests_processed = 0;
446 size_t index = i - 1;
461 resp = (*handler)(*req);
473 requests_processed++;
476 return requests_processed;
484 int bytes = recv(client.fd, buffer,
sizeof(buffer), 0);
486 if (WSAGetLastError() != SOCKET_ERROR_WOULD_BLOCK) {
492 ssize_t bytes = recv(client.fd, buffer,
sizeof(buffer), MSG_DONTWAIT);
494 if (errno != EWOULDBLOCK && errno != EAGAIN) {
501 client.buffer.append(buffer,
static_cast<size_t>(bytes));
504 size_t header_end = client.buffer.find(
"\r\n\r\n");
513 string header_section = client.buffer.substr(0, header_end);
514 vector<string> lines = split(header_section,
'\n');
521 string request_line = trim(lines[0]);
522 vector<string> parts = split(request_line,
' ');
523 if (parts.size() < 3) {
527 req.mMethod = parts[0];
530 string full_path = parts[1];
531 size_t query_pos = full_path.find(
'?');
533 req.mPath = full_path.substr(0, query_pos);
534 req.mQuery = parse_query_string(full_path.substr(query_pos));
536 req.mPath = full_path;
539 req.mHttpVersion = parts[2];
542 for (
size_t i = 1; i < lines.size(); ++i) {
543 string line = trim(lines[i]);
544 if (line.empty())
continue;
546 size_t colon_pos = line.find(
':');
548 string name = trim(line.substr(0, colon_pos));
549 string value = trim(line.substr(colon_pos + 1));
550 req.mHeaders[name] =
value;
556 if (content_length) {
557 int body_len =
atoi(content_length->c_str());
559 size_t body_start = header_end + 4;
560 size_t total_needed = body_start +
static_cast<size_t>(body_len);
562 if (client.buffer.size() < total_needed) {
566 req.mBody = client.buffer.substr(body_start,
static_cast<size_t>(body_len));
575 const char* ptr = data.c_str();
576 size_t remaining = data.size();
578 while (remaining > 0) {
580 int sent = send(client_fd, ptr,
static_cast<int>(remaining), 0);
582 ssize_t sent = send(client_fd, ptr, remaining, 0);
590 remaining -=
static_cast<size_t>(sent);
597 for (
const auto& entry : mRoutes) {
598 if (entry.method == method && entry.path == path) {
599 return entry.handler;
606 if (index >= mClientSockets.size())
return;
608 close(mClientSockets[index].fd);
609 mClientSockets.erase(mClientSockets.begin() +
static_cast<ptrdiff_t>(index));
613 u32 now = fl::platforms::millis();
616 size_t index = i - 1;
619 if (now - client.connect_time > CONNECTION_TIMEOUT_MS) {
629#elif defined(FL_IS_ESP32) && ESP_IDF_VERSION_4_OR_HIGHER
652struct EspPendingRequest {
654 fl::size route_index;
660 SemaphoreHandle_t
done;
668void espRequestCleanup(EspPendingRequest* item) {
670 if (item->response) {
671 delete item->response;
677 vSemaphoreDelete(item->done);
683QueueHandle_t s_esp_request_queue =
nullptr;
693 if (!s_esp_request_queue) {
696 EspPendingRequest* item =
nullptr;
697 while (xQueueReceive(s_esp_request_queue, &item, 0) == pdTRUE) {
702 if (!item->state.compare_exchange_strong(expected, 1)) {
705 espRequestCleanup(item);
709 fl_req.mMethod = item->method;
710 fl_req.mPath = item->uri;
711 fl_req.mHttpVersion =
"HTTP/1.1";
712 if (item->body && item->body_len > 0) {
713 fl_req.mBody = string(item->body, item->body_len);
715 Response* resp =
new Response();
716 if (item->server && item->route_index < item->server->mRoutes.size()) {
717 *resp = item->server->mRoutes[item->route_index].handler(fl_req);
721 item->response = resp;
722 xSemaphoreGive(item->done);
727 return mServer && mServer->is_running();
730 size_t active_task_count() const
FL_NO_EXCEPT override {
731 return (mServer && mServer->is_running()) ? 1 : 0;
743httpd_method_t to_httpd_method(
const string& method) {
744 if (method ==
"GET")
return HTTP_GET;
745 if (method ==
"POST")
return HTTP_POST;
746 if (method ==
"PUT")
return HTTP_PUT;
747 if (method ==
"DELETE")
return HTTP_DELETE;
752struct EspRouteContext {
758esp_err_t esp_route_handler(httpd_req_t* req)
FL_NO_EXCEPT;
765int Server::handle_esp_request(
void* raw_req)
FL_NO_EXCEPT {
769 auto* req =
static_cast<httpd_req_t*
>(raw_req);
770 auto* ctx =
static_cast<EspRouteContext*
>(req->user_ctx);
771 if (!ctx || !ctx->server || !s_esp_request_queue) {
772 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"No context");
776 EspPendingRequest* item =
new EspPendingRequest();
777 item->server = ctx->server;
778 item->route_index = ctx->route_index;
779 item->body =
nullptr;
781 item->response =
nullptr;
782 item->method[0] =
'\0';
784 item->state.store(0);
785 switch (req->method) {
786 case HTTP_POST:
fl::strncpy(item->method,
"POST",
sizeof(item->method) - 1);
break;
787 case HTTP_PUT:
fl::strncpy(item->method,
"PUT",
sizeof(item->method) - 1);
break;
788 case HTTP_DELETE:
fl::strncpy(item->method,
"DELETE",
sizeof(item->method) - 1);
break;
789 default:
fl::strncpy(item->method,
"GET",
sizeof(item->method) - 1);
break;
791 fl::strncpy(item->uri, req->uri,
sizeof(item->uri) - 1);
793 for (
char* c = item->uri; *c; ++c) {
794 if (*c ==
'?') { *c =
'\0';
break; }
797 if (req->content_len > 0 && req->content_len <= 8192) {
798 int total_len =
static_cast<int>(req->content_len);
799 char* buf =
static_cast<char*
>(
fl::malloc(
static_cast<fl::size
>(total_len) + 1));
802 while (received < total_len) {
803 int ret = httpd_req_recv(req, buf + received, total_len - received);
809 buf[received] =
'\0';
811 item->body_len =
static_cast<fl::size
>(received);
815 item->done = xSemaphoreCreateBinary();
817 espRequestCleanup(item);
818 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"No memory");
822 if (xQueueSend(s_esp_request_queue, &item, pdMS_TO_TICKS(100)) != pdTRUE) {
823 espRequestCleanup(item);
824 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Server busy");
828 esp_err_t
result = ESP_OK;
829 if (xSemaphoreTake(item->done, pdMS_TO_TICKS(5000)) != pdTRUE) {
831 if (item->state.compare_exchange_strong(expected, 3)) {
834 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"Main loop busy");
839 (void)xSemaphoreTake(item->done, portMAX_DELAY);
842 if (item->response) {
845 fl::snprintf(status_str,
sizeof(status_str),
"%d", resp->mStatusCode);
846 httpd_resp_set_status(req, status_str);
847 for (
auto it = resp->mHeaders.begin(); it != resp->mHeaders.end(); ++it) {
848 httpd_resp_set_hdr(req, it->first.c_str(), it->second.c_str());
850 httpd_resp_send(req, resp->mBody.c_str(), resp->mBody.size());
852 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
"No response");
855 espRequestCleanup(item);
862esp_err_t esp_route_handler(httpd_req_t* req)
FL_NO_EXCEPT {
863 return static_cast<esp_err_t
>(Server::handle_esp_request(
static_cast<void*
>(req)));
871 for (
auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
872 if (it->first == name) {
880 auto it = mQuery.find(param);
881 if (it != mQuery.end()) {
890 mHeaders[
"Content-Type"] =
"text/plain";
899 mHeaders[name] =
value;
909 mBody = data.to_string();
910 mHeaders[
"Content-Type"] =
"application/json";
922 resp.status(404).body(
"Not Found\n");
951static httpd_handle_t s_esp_httpd =
nullptr;
952using EspRouteContexts = fl::vector<fl::unique_ptr<EspRouteContext>>;
955static EspRouteContexts& espRouteContexts()
FL_NO_EXCEPT {
974 mLastError =
"Server already running";
978 if (!s_esp_request_queue) {
979 s_esp_request_queue = xQueueCreate(4,
sizeof(EspPendingRequest*));
986 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
987 config.server_port =
static_cast<u16
>(port);
989 mLastError =
"Too many HTTP routes";
990 FL_WARN(
"[HTTP] Too many routes for ESP-IDF HTTP server");
992 mAsyncRunner.reset();
995 if (mRoutes.size() > config.max_uri_handlers) {
996 config.max_uri_handlers =
static_cast<u16
>(mRoutes.size());
1005 config.stack_size = 8192;
1007 esp_err_t err = httpd_start(&s_esp_httpd, &config);
1008 if (err != ESP_OK) {
1009 mLastError =
"httpd_start failed";
1010 FL_WARN(
"[HTTP] httpd_start failed: " << esp_err_to_name(err));
1012 mAsyncRunner.reset();
1017 for (
size_t i = 0; i < mRoutes.size(); ++i) {
1020 httpd_uri_t uri_handler = {};
1021 uri_handler.uri = mRoutes[i].path.c_str();
1022 uri_handler.method = to_httpd_method(mRoutes[i].method);
1023 uri_handler.handler = esp_route_handler;
1024 uri_handler.user_ctx = ctx.get();
1026 esp_err_t reg_err = httpd_register_uri_handler(s_esp_httpd, &uri_handler);
1027 if (reg_err != ESP_OK) {
1028 mLastError =
"Failed to register route " + mRoutes[i].path;
1029 FL_WARN(
"[HTTP] Failed to register route " << mRoutes[i].path.c_str() <<
": " << esp_err_to_name(reg_err));
1030 httpd_stop(s_esp_httpd);
1031 s_esp_httpd =
nullptr;
1032 espRouteContexts().clear();
1034 mAsyncRunner.reset();
1037 espRouteContexts().push_back(
fl::move(ctx));
1044 FL_WARN(
"[HTTP] Server started on port " << port);
1054 httpd_stop(s_esp_httpd);
1055 s_esp_httpd =
nullptr;
1064 espRouteContexts().clear();
1070 FL_WARN(
"[HTTP] Server stopped");
1074 mRoutes.push_back({method, path, handler});
1077 if (mRunning && s_esp_httpd) {
1078 size_t idx = mRoutes.size() - 1;
1081 httpd_uri_t uri_handler = {};
1082 uri_handler.uri = mRoutes[idx].path.c_str();
1083 uri_handler.method = to_httpd_method(method);
1084 uri_handler.handler = esp_route_handler;
1085 uri_handler.user_ctx = ctx.get();
1087 httpd_register_uri_handler(s_esp_httpd, &uri_handler);
1088 espRouteContexts().push_back(
fl::move(ctx));
1093 route(
"GET", path, handler);
1097 route(
"POST", path, handler);
1101 route(
"PUT", path, handler);
1105 route(
"DELETE", path, handler);
fl::UICheckbox listen("Listen", true)
static void removeListener(Listener *listener) FL_NO_EXCEPT
static void addListener(Listener *listener, int priority=0) FL_NO_EXCEPT
static T & instance() FL_NO_EXCEPT
optional< string > query(const string ¶m) const FL_NO_EXCEPT
Get query parameter value by name.
optional< string > header(const string &name) const FL_NO_EXCEPT
Get header value by name (case-insensitive)
HTTP request object (immutable, passed by const reference)
Response & header(const string &name, const string &value) FL_NO_EXCEPT
Add HTTP header.
Response & json(const class json &data) FL_NO_EXCEPT
Set JSON response body with automatic Content-Type header.
string to_string() const FL_NO_EXCEPT
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.
Response & status(int code) FL_NO_EXCEPT
Set HTTP status code.
static Response not_found() FL_NO_EXCEPT
Factory method for 404 Not Found response.
map< string, string > mHeaders
static Response internal_error(const string &message) FL_NO_EXCEPT
Factory method for 500 Internal Server Error response.
HTTP response builder (fluent interface)
void accept_connections() FL_NO_EXCEPT
void stop() FL_NO_EXCEPT
Stop server and close all connections.
bool setup_listen_socket(int port) FL_NO_EXCEPT
vector< ClientConnection > mClientSockets
void post(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for POST routes.
Server() FL_NO_EXCEPT
Constructor.
void get(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for GET routes.
bool start(int port=8080) FL_NO_EXCEPT
Start server on specified port.
vector< RouteEntry > mRoutes
void put(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for PUT routes.
void del(const string &path, RouteHandler handler) FL_NO_EXCEPT
Convenience method for DELETE routes.
void cleanup_stale_connections() FL_NO_EXCEPT
size_t process_requests() FL_NO_EXCEPT
bool send_response(int client_fd, const Response &response) FL_NO_EXCEPT
void onExit() FL_NO_EXCEPT override
size_t update() FL_NO_EXCEPT
Update server (process pending requests non-blocking)
void route(const string &method, const string &path, RouteHandler handler) FL_NO_EXCEPT
Register route handler using fl::function.
fl::unique_ptr< ServerAsyncRunner > mAsyncRunner
optional< RouteHandler > find_handler(const string &method, const string &path) const FL_NO_EXCEPT
void close_client(size_t index) FL_NO_EXCEPT
~Server() FL_NO_EXCEPT
Destructor (stops server if running)
optional< Request > read_request(ClientConnection &client) FL_NO_EXCEPT
static constexpr fl::size npos
void register_runner(Runner *r) FL_NO_EXCEPT
Register a runner.
void unregister_runner(Runner *r) FL_NO_EXCEPT
Unregister a runner.
static Executor & instance() FL_NO_EXCEPT
Task executor — runs registered task runners and manages the run loop.
Centralized logging categories for FastLED hardware interfaces and subsystems.
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NO_EXCEPT
function< Response(const Request &)> RouteHandler
Route handler function signature Takes const reference to Request, returns Response by value.
u8 u8 fract8 blur_amount FL_NO_EXCEPT
constexpr int type_rank< T >::value
int snprintf(char *buffer, fl::size size, const char *format) FL_NO_EXCEPT
FASTLED_FORCE_INLINE void free(void *ptr) FL_NO_EXCEPT
MapRedBlackTree< Key, T, Compare, fl::allocator_slab< char > > map
bool isspace(char c) FL_NO_EXCEPT
Check if character is whitespace (space, tab, newline, carriage return)
FASTLED_FORCE_INLINE int atoi(const char *str) FL_NO_EXCEPT
FASTLED_FORCE_INLINE void * malloc(size_t size) FL_NO_EXCEPT
char tolower(char c) FL_NO_EXCEPT
Convert character to lowercase.
expected< T, E > result
Alias for expected (Rust-style naming)
FASTLED_FORCE_INLINE char * strncpy(char *dest, const char *src, size_t n) FL_NO_EXCEPT
constexpr nullopt_t nullopt
string to_string(T value) FL_NO_EXCEPT
Base definition for an LED controller.
static constexpr T max() FL_NO_EXCEPT