FastLED 3.9.15
Loading...
Searching...
No Matches
server.cpp.hpp
Go to the documentation of this file.
1
3
4#pragma once
5
7#include "fl/stl/stdio.h" // fl::snprintf — avoids _svfprintf_r (#2773 item 1.1)
8#include "fl/task/executor.h"
9#include "platforms/esp/is_esp.h" // ok platform headers - for FL_IS_ESP32 // IWYU pragma: keep
10
11// Common includes needed by both POSIX/Windows and ESP32 implementations
12#include "fl/stl/cctype.h"
13#include "fl/stl/cstring.h"
14#include "fl/stl/malloc.h"
15#include "fl/log/log.h"
16
17// ESP32 HTTP server header (must be before namespace declarations)
18// Requires IDF 4.0+ for HTTPD_500_INTERNAL_SERVER_ERROR, httpd_resp_send_err, etc.
19#include "platforms/esp/esp_version.h" // ok platform headers // IWYU pragma: keep
20#if defined(FL_IS_ESP32) && !defined(FASTLED_HAS_NETWORKING) && ESP_IDF_VERSION_4_OR_HIGHER
21// IWYU pragma: begin_keep
22#include <esp_http_server.h>
23// IWYU pragma: end_keep
24#endif
25
26#ifdef FASTLED_HAS_NETWORKING
27
28#include "platforms/time_platform.h"
29
30// Platform-specific socket includes
31#ifdef FL_IS_WIN
32 #include "platforms/win/socket_win.h" // ok platform headers // IWYU pragma: keep
33 #define SOCKET_ERROR_WOULD_BLOCK WSAEWOULDBLOCK
34 #define SOCKET_ERROR_IN_PROGRESS WSAEINPROGRESS
35#else
36 #include "platforms/posix/socket_posix.h" // ok platform headers (includes all system socket headers) // IWYU pragma: keep
37#include "fl/stl/noexcept.h"
38 #define SOCKET_ERROR_WOULD_BLOCK EWOULDBLOCK
39 #define SOCKET_ERROR_IN_PROGRESS EINPROGRESS
40#endif
41
42namespace fl {
43namespace asio {
44namespace http {
45
46// ========== Async System Integration ==========
47
51class Server::ServerAsyncRunner : public task::Runner {
52public:
53 explicit ServerAsyncRunner(Server* server) : mServer(server) {}
54
55 void update() override {
56 if (mServer) {
57 mServer->update(); // Call server's update() and discard return value
58 }
59 }
60
61 bool has_active_tasks() const override {
62 return mServer && mServer->is_running();
63 }
64
65 size_t active_task_count() const override {
66 return (mServer && mServer->is_running()) ? mServer->mClientSockets.size() : 0;
67 }
68
69private:
70 Server* mServer;
71};
72
73namespace {
74
75// Connection timeout (30 seconds)
76constexpr u32 CONNECTION_TIMEOUT_MS = 30000;
77
78// Helper: Case-insensitive string comparison
79bool iequals(const string& a, const string& b) {
80 if (a.size() != b.size()) return false;
81 for (size_t i = 0; i < a.size(); ++i) {
82 if (fl::tolower(a[i]) != fl::tolower(b[i])) return false;
83 }
84 return true;
85}
86
87// Helper: Trim whitespace from string
88string trim(const string& s) {
89 size_t start = 0;
90 while (start < s.size() && fl::isspace(s[start])) ++start;
91 size_t end = s.size();
92 while (end > start && fl::isspace(s[end - 1])) --end;
93 return s.substr(start, end - start);
94}
95
96// Helper: Split string by delimiter
97vector<string> split(const string& s, char delimiter) {
98 vector<string> tokens;
99 size_t start = 0;
100 size_t end = s.find(delimiter);
101 while (end != string::npos) {
102 tokens.push_back(s.substr(start, end - start));
103 start = end + 1;
104 end = s.find(delimiter, start);
105 }
106 tokens.push_back(s.substr(start));
107 return tokens;
108}
109
110// Helper: Parse query string (e.g., "?id=123&name=test")
111map<string, string> parse_query_string(const string& query) {
112 map<string, string> params;
113 if (query.empty() || query[0] != '?') return params;
114
115 vector<string> pairs = split(query.substr(1), '&');
116 for (const auto& pair : pairs) {
117 size_t eq_pos = pair.find('=');
118 if (eq_pos != string::npos) {
119 string key = pair.substr(0, eq_pos);
120 string value = pair.substr(eq_pos + 1);
121 params[key] = value;
122 }
123 }
124 return params;
125}
126
127// Helper: Get HTTP status text
128const char* status_text(int code) {
129 switch (code) {
130 case 200: return "OK";
131 case 400: return "Bad Request";
132 case 404: return "Not Found";
133 case 500: return "Internal Server Error";
134 default: return "Unknown";
135 }
136}
137
138// Helper: Set socket to non-blocking mode
139bool set_nonblocking(int fd) {
140#ifdef FL_IS_WIN
141 u_long mode = 1;
142 return ioctlsocket(fd, FIONBIO, &mode) == 0;
143#else
144 int flags = fcntl(fd, F_GETFL, 0);
145 if (flags == -1) return false;
146 return fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
147#endif
148}
149
150} // anonymous namespace
151
152//==============================================================================
153// Request implementation
154//==============================================================================
155
156optional<string> Request::header(const string& name) const {
157 for (auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
158 if (iequals(it->first, name)) {
159 return it->second;
160 }
161 }
162 return nullopt;
163}
164
165optional<string> Request::query(const string& param) const {
166 auto it = mQuery.find(param);
167 if (it != mQuery.end()) {
168 return it->second;
169 }
170 return nullopt;
171}
172
173//==============================================================================
174// Response implementation
175//==============================================================================
176
178 mHeaders["Content-Type"] = "text/plain";
179}
180
181Response& Response::status(int code) {
182 mStatusCode = code;
183 return *this;
184}
185
186Response& Response::header(const string& name, const string& value) {
187 mHeaders[name] = value;
188 return *this;
189}
190
191Response& Response::body(const string& content) {
192 mBody = content;
193 return *this;
194}
195
196Response& Response::json(const class json& data) {
197 mBody = data.to_string();
198 mHeaders["Content-Type"] = "application/json";
199 return *this;
200}
201
202Response Response::ok(const string& body) {
203 Response resp;
204 resp.status(200).body(body);
205 return resp;
206}
207
209 Response resp;
210 resp.status(404).body("Not Found\n");
211 return resp;
212}
213
214Response Response::bad_request(const string& message) {
215 Response resp;
216 resp.status(400).body(message + "\n");
217 return resp;
218}
219
220Response Response::internal_error(const string& message) {
221 Response resp;
222 resp.status(500).body(message + "\n");
223 return resp;
224}
225
226string Response::to_string() const {
227 string result;
228
229 // Status line
230 result += "HTTP/1.0 ";
232 result += " ";
233 result += status_text(mStatusCode);
234 result += "\r\n";
235
236 // Headers
237 for (auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
238 result += it->first + ": " + it->second + "\r\n";
239 }
240
241 // Content-Length (auto-calculated)
242 result += "Content-Length: " + fl::to_string(mBody.size()) + "\r\n";
243
244 // End of headers
245 result += "\r\n";
246
247 // Body
248 result += mBody;
249
250 return result;
251}
252
253//==============================================================================
254// HttpServer implementation
255//==============================================================================
256
258 // Register as engine event listener for automatic cleanup on exit
260}
261
263 // Unregister from engine events
265 stop();
266}
267
268void Server::onExit() {
269 // Automatically stop server on engine shutdown
270 stop();
271}
272
273bool Server::start(int port) {
274 if (mRunning) {
275 mLastError = "Server already running";
276 return false;
277 }
278
280 return false;
281 }
282
283 mPort = port;
284 mRunning = true;
285 mLastError.clear();
286
287 // Register with async system for automatic updates
288 if (!mAsyncRunner) {
291 }
292
293 return true;
294}
295
296void Server::stop() {
297 if (!mRunning) return;
298
299 // Unregister from async system
300 if (mAsyncRunner) {
302 mAsyncRunner.reset();
303 }
304
305 // Close all client connections
306 for (auto& client : mClientSockets) {
307 if (client.fd != -1) {
308 close(client.fd);
309 }
310 }
311 mClientSockets.clear();
312
313 // Close listen socket
314 if (mListenSocket != -1) {
315 close(mListenSocket);
316 mListenSocket = -1;
317 }
318
319 // Free route handlers to prevent leaks when server lives in a shared library
320 // (LSAN runs before shared library static destructors)
321 mRoutes.clear();
322
323 mRunning = false;
324}
325
326void Server::route(const string& method, const string& path, RouteHandler handler) {
327 mRoutes.push_back({method, path, handler});
328}
329
330void Server::get(const string& path, RouteHandler handler) {
331 route("GET", path, handler);
332}
333
334void Server::post(const string& path, RouteHandler handler) {
335 route("POST", path, handler);
336}
337
338void Server::put(const string& path, RouteHandler handler) {
339 route("PUT", path, handler);
340}
341
342void Server::del(const string& path, RouteHandler handler) {
343 route("DELETE", path, handler);
344}
345
346size_t Server::update() {
347 if (!mRunning) return 0;
348
351 return process_requests();
352}
353
354bool Server::setup_listen_socket(int port) {
355 // Create socket (initialization handled by socket wrapper)
356 mListenSocket = socket(AF_INET, SOCK_STREAM, 0);
357 if (mListenSocket < 0) {
358 mLastError = "Failed to create socket";
359 return false;
360 }
361
362 // Set socket options
363 int opt = 1;
364 if (setsockopt(mListenSocket, SOL_SOCKET, SO_REUSEADDR,
365 (const char*)(&opt), sizeof(opt)) < 0) {
366 close(mListenSocket);
367 mListenSocket = -1;
368 mLastError = "Failed to set SO_REUSEADDR";
369 return false;
370 }
371
372 // Set non-blocking
373 if (!set_nonblocking(mListenSocket)) {
374 close(mListenSocket);
375 mListenSocket = -1;
376 mLastError = "Failed to set non-blocking mode";
377 return false;
378 }
379
380 // Bind socket
381 sockaddr_in addr{};
382 addr.sin_family = AF_INET;
383 addr.sin_addr.s_addr = INADDR_ANY;
384 addr.sin_port = htons(static_cast<u16>(port));
385
386 if (bind(mListenSocket, (sockaddr*)(&addr), sizeof(addr)) < 0) {
387 close(mListenSocket);
388 mListenSocket = -1;
389 mLastError = "Failed to bind to port";
390 return false;
391 }
392
393 // Listen
394 if (listen(mListenSocket, 5) < 0) {
395 close(mListenSocket);
396 mListenSocket = -1;
397 mLastError = "Failed to listen on socket";
398 return false;
399 }
400
401 return true;
402}
403
405 sockaddr_in client_addr{};
406 socklen_t addr_len = sizeof(client_addr);
407
408 // Accept all pending connections (non-blocking)
409 while (true) {
410 int client_fd = accept(mListenSocket,
411 (sockaddr*)(&client_addr),
412 &addr_len);
413
414 if (client_fd < 0) {
415#ifdef FL_IS_WIN
416 if (WSAGetLastError() == SOCKET_ERROR_WOULD_BLOCK) break;
417#else
418 if (errno == EWOULDBLOCK || errno == EAGAIN) break;
419#endif
420 // Other error - log and continue
421 break;
422 }
423
424 // Set client socket to non-blocking
425 if (!set_nonblocking(client_fd)) {
426 close(client_fd);
427 continue;
428 }
429
430 // Add to client list
431 ClientConnection client;
432 client.fd = client_fd;
433 client.connect_time = fl::platforms::millis();
434 mClientSockets.push_back(client);
435 }
436}
437
439 size_t requests_processed = 0;
440
441 // Process all clients (iterate backwards to allow removal)
442 for (size_t i = mClientSockets.size(); i > 0; --i) {
443 size_t index = i - 1;
444 ClientConnection& client = mClientSockets[index];
445
446 // Try to read request
447 optional<Request> req = read_request(client);
448 if (!req) {
449 continue; // No complete request yet
450 }
451
452 // Find handler
453 optional<RouteHandler> handler = find_handler(req->method(), req->path());
454
455 Response resp;
456 if (handler) {
457 // Call handler
458 resp = (*handler)(*req);
459 } else {
460 // No handler found - 404
461 resp = Response::not_found();
462 }
463
464 // Send response
465 send_response(client.fd, resp);
466
467 // Close connection (HTTP/1.0 - no keep-alive)
468 close_client(index);
469
470 requests_processed++;
471 }
472
473 return requests_processed;
474}
475
476optional<Request> Server::read_request(ClientConnection& client) {
477 // Read data from socket
478 char buffer[4096];
479
480#ifdef FL_IS_WIN
481 int bytes = recv(client.fd, buffer, sizeof(buffer), 0);
482 if (bytes <= 0) {
483 if (WSAGetLastError() != SOCKET_ERROR_WOULD_BLOCK) {
484 return nullopt; // Connection closed or error
485 }
486 return nullopt; // No data yet
487 }
488#else
489 ssize_t bytes = recv(client.fd, buffer, sizeof(buffer), MSG_DONTWAIT);
490 if (bytes <= 0) {
491 if (errno != EWOULDBLOCK && errno != EAGAIN) {
492 return nullopt; // Connection closed or error
493 }
494 return nullopt; // No data yet
495 }
496#endif
497
498 client.buffer.append(buffer, static_cast<size_t>(bytes));
499
500 // Check if we have complete HTTP request (ends with \r\n\r\n)
501 size_t header_end = client.buffer.find("\r\n\r\n");
502 if (header_end == string::npos) {
503 return nullopt; // Headers not complete yet
504 }
505
506 // Parse request
507 Request req;
508
509 // Split into lines
510 string header_section = client.buffer.substr(0, header_end);
511 vector<string> lines = split(header_section, '\n');
512
513 if (lines.empty()) {
514 return nullopt;
515 }
516
517 // Parse request line: "GET /path HTTP/1.1\r"
518 string request_line = trim(lines[0]);
519 vector<string> parts = split(request_line, ' ');
520 if (parts.size() < 3) {
521 return nullopt;
522 }
523
524 req.mMethod = parts[0];
525
526 // Split path and query string
527 string full_path = parts[1];
528 size_t query_pos = full_path.find('?');
529 if (query_pos != string::npos) {
530 req.mPath = full_path.substr(0, query_pos);
531 req.mQuery = parse_query_string(full_path.substr(query_pos));
532 } else {
533 req.mPath = full_path;
534 }
535
536 req.mHttpVersion = parts[2];
537
538 // Parse headers
539 for (size_t i = 1; i < lines.size(); ++i) {
540 string line = trim(lines[i]);
541 if (line.empty()) continue;
542
543 size_t colon_pos = line.find(':');
544 if (colon_pos != string::npos) {
545 string name = trim(line.substr(0, colon_pos));
546 string value = trim(line.substr(colon_pos + 1));
547 req.mHeaders[name] = value;
548 }
549 }
550
551 // Read body if Content-Length present
552 optional<string> content_length = req.header("Content-Length");
553 if (content_length) {
554 int body_len = atoi(content_length->c_str());
555 if (body_len > 0) {
556 size_t body_start = header_end + 4;
557 size_t total_needed = body_start + static_cast<size_t>(body_len);
558
559 if (client.buffer.size() < total_needed) {
560 return nullopt; // Body not complete yet
561 }
562
563 req.mBody = client.buffer.substr(body_start, static_cast<size_t>(body_len));
564 }
565 }
566
567 return req;
568}
569
570bool Server::send_response(int client_fd, const Response& response) {
571 string data = response.to_string();
572 const char* ptr = data.c_str();
573 size_t remaining = data.size();
574
575 while (remaining > 0) {
576#ifdef FL_IS_WIN
577 int sent = send(client_fd, ptr, static_cast<int>(remaining), 0);
578#else
579 ssize_t sent = send(client_fd, ptr, remaining, 0);
580#endif
581
582 if (sent <= 0) {
583 return false;
584 }
585
586 ptr += sent;
587 remaining -= static_cast<size_t>(sent);
588 }
589
590 return true;
591}
592
593optional<RouteHandler> Server::find_handler(const string& method, const string& path) const {
594 for (const auto& entry : mRoutes) {
595 if (entry.method == method && entry.path == path) {
596 return entry.handler;
597 }
598 }
599 return nullopt;
600}
601
602void Server::close_client(size_t index) {
603 if (index >= mClientSockets.size()) return;
604
605 close(mClientSockets[index].fd);
606 mClientSockets.erase(mClientSockets.begin() + static_cast<ptrdiff_t>(index));
607}
608
610 u32 now = fl::platforms::millis();
611
612 for (size_t i = mClientSockets.size(); i > 0; --i) {
613 size_t index = i - 1;
614 const ClientConnection& client = mClientSockets[index];
615
616 if (now - client.connect_time > CONNECTION_TIMEOUT_MS) {
617 close_client(index);
618 }
619 }
620}
621
622} // namespace http
623} // namespace asio
624} // namespace fl
625
626#elif defined(FL_IS_ESP32) && ESP_IDF_VERSION_4_OR_HIGHER
627
628// ============================================================================
629// ESP32 Implementation using esp_http_server.h (IDF 4.0+)
630// ============================================================================
631
632namespace fl {
633namespace asio {
634namespace http {
635
636// Minimal definition for unique_ptr<ServerAsyncRunner> destruction
637// (ESP32 uses esp_http_server tasks, not runner)
639
640// ========== ESP32 Helpers ==========
641
642namespace {
643
644// Map method string to httpd_method_t
645httpd_method_t to_httpd_method(const string& method) {
646 if (method == "GET") return HTTP_GET;
647 if (method == "POST") return HTTP_POST;
648 if (method == "PUT") return HTTP_PUT;
649 if (method == "DELETE") return HTTP_DELETE;
650 return HTTP_GET;
651}
652
653// Context passed to ESP-IDF URI handler callback
654struct EspRouteContext {
655 Server* server;
656 size_t route_index;
657};
658
659// Forward declaration - implemented as Server static method for private access
660esp_err_t esp_route_handler(httpd_req_t* req);
661
662} // anonymous namespace
663
664// Static method on Server - has access to private members of Request/Response.
665// Signature uses void* to avoid ESP-IDF types in header; cast to httpd_req_t* here.
666// This is called from the ESP-IDF HTTP server task.
667int Server::handle_esp_request(void* raw_req) {
668 auto* req = static_cast<httpd_req_t*>(raw_req);
669 auto* ctx = static_cast<EspRouteContext*>(req->user_ctx);
670 if (!ctx || !ctx->server) {
671 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "No context");
672 return ESP_FAIL;
673 }
674
675 // Build fl::asio::http::Request from httpd_req_t
676 Request fl_req;
677 fl_req.mPath = req->uri;
678 switch (req->method) {
679 case HTTP_GET: fl_req.mMethod = "GET"; break;
680 case HTTP_POST: fl_req.mMethod = "POST"; break;
681 case HTTP_PUT: fl_req.mMethod = "PUT"; break;
682 case HTTP_DELETE: fl_req.mMethod = "DELETE"; break;
683 default: fl_req.mMethod = "GET"; break;
684 }
685 fl_req.mHttpVersion = "HTTP/1.1";
686
687 // Strip query string from path
688 size_t q = fl_req.mPath.find('?');
689 if (q != string::npos) {
690 fl_req.mPath = fl_req.mPath.substr(0, q);
691 }
692
693 // Read POST body if present
694 if (req->content_len > 0 && req->content_len <= 8192) {
695 int total_len = req->content_len;
696 char* buf = static_cast<char*>(fl::malloc(total_len + 1));
697 if (buf) {
698 int received = 0;
699 while (received < total_len) {
700 int ret = httpd_req_recv(req, buf + received, total_len - received);
701 if (ret <= 0) {
702 break;
703 }
704 received += ret;
705 }
706 buf[received] = '\0';
707 fl_req.mBody = string(buf, received);
708 fl::free(buf);
709 }
710 }
711
712 // Call the RouteHandler
713 if (ctx->route_index >= ctx->server->mRoutes.size()) {
714 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid route");
715 return ESP_FAIL;
716 }
717
718 Response resp = ctx->server->mRoutes[ctx->route_index].handler(fl_req);
719
720 // Send response using Response::to_string() which serializes to HTTP format
721 // Use esp_http_server APIs to send the response parts
722 char status_str[32];
723 fl::snprintf(status_str, sizeof(status_str), "%d", resp.mStatusCode);
724 httpd_resp_set_status(req, status_str);
725
726 for (auto it = resp.mHeaders.begin(); it != resp.mHeaders.end(); ++it) {
727 httpd_resp_set_hdr(req, it->first.c_str(), it->second.c_str());
728 }
729
730 httpd_resp_send(req, resp.mBody.c_str(), resp.mBody.size());
731 return ESP_OK;
732}
733
734namespace {
735
736// ESP-IDF URI handler callback - delegates to Server::handle_esp_request
737esp_err_t esp_route_handler(httpd_req_t* req) {
738 return static_cast<esp_err_t>(Server::handle_esp_request(static_cast<void*>(req)));
739}
740
741} // anonymous namespace
742
743// ========== Request implementation ==========
744
745optional<string> Request::header(const string& name) const {
746 for (auto it = mHeaders.begin(); it != mHeaders.end(); ++it) {
747 if (it->first == name) {
748 return it->second;
749 }
750 }
751 return nullopt;
752}
753
754optional<string> Request::query(const string& param) const {
755 auto it = mQuery.find(param);
756 if (it != mQuery.end()) {
757 return it->second;
758 }
759 return nullopt;
760}
761
762// ========== Response implementation ==========
763
765 mHeaders["Content-Type"] = "text/plain";
766}
767
768Response& Response::status(int code) {
769 mStatusCode = code;
770 return *this;
771}
772
773Response& Response::header(const string& name, const string& value) {
774 mHeaders[name] = value;
775 return *this;
776}
777
778Response& Response::body(const string& content) {
779 mBody = content;
780 return *this;
781}
782
783Response& Response::json(const class json& data) {
784 mBody = data.to_string();
785 mHeaders["Content-Type"] = "application/json";
786 return *this;
787}
788
789Response Response::ok(const string& body) {
790 Response resp;
791 resp.status(200).body(body);
792 return resp;
793}
794
796 Response resp;
797 resp.status(404).body("Not Found\n");
798 return resp;
799}
800
801Response Response::bad_request(const string& message) {
802 Response resp;
803 resp.status(400).body(message + "\n");
804 return resp;
805}
806
807Response Response::internal_error(const string& message) {
808 Response resp;
809 resp.status(500).body(message + "\n");
810 return resp;
811}
812
813string Response::to_string() const {
814 // Not used on ESP32 (responses sent via httpd_resp_send)
815 return mBody;
816}
817
818// ========== Server implementation (ESP32) ==========
819
820// ESP32 stores httpd_handle_t and route contexts via mListenSocket as an opaque int
821// and mClientSockets is unused. We use a static map for the httpd handle.
822
823// We store the httpd_handle_t in a file-scoped variable since the Server class
824// members (mListenSocket, mClientSockets) are typed for POSIX sockets.
825static httpd_handle_t s_esp_httpd = nullptr;
826static fl::vector<fl::unique_ptr<EspRouteContext>> s_esp_route_contexts;
827
830}
831
834 stop();
835}
836
837void Server::onExit() {
838 stop();
839}
840
841bool Server::start(int port) {
842 if (mRunning) {
843 mLastError = "Server already running";
844 return false;
845 }
846
847 httpd_config_t config = HTTPD_DEFAULT_CONFIG();
848 config.server_port = static_cast<u16>(port);
849
850 esp_err_t err = httpd_start(&s_esp_httpd, &config);
851 if (err != ESP_OK) {
852 mLastError = "httpd_start failed";
853 FL_WARN("[HTTP] httpd_start failed: " << esp_err_to_name(err));
854 return false;
855 }
856
857 // Register all routes that were added before start()
858 for (size_t i = 0; i < mRoutes.size(); ++i) {
859 auto ctx = fl::make_unique<EspRouteContext>(EspRouteContext{this, i});
860
861 httpd_uri_t uri_handler = {};
862 uri_handler.uri = mRoutes[i].path.c_str();
863 uri_handler.method = to_httpd_method(mRoutes[i].method);
864 uri_handler.handler = esp_route_handler;
865 uri_handler.user_ctx = ctx.get();
866
867 esp_err_t reg_err = httpd_register_uri_handler(s_esp_httpd, &uri_handler);
868 if (reg_err != ESP_OK) {
869 FL_WARN("[HTTP] Failed to register route " << mRoutes[i].path.c_str());
870 }
871 s_esp_route_contexts.push_back(fl::move(ctx));
872 }
873
874 mPort = port;
875 mRunning = true;
876 mLastError.clear();
877
878 FL_WARN("[HTTP] Server started on port " << port);
879 return true;
880}
881
882void Server::stop() {
883 if (!mRunning) return;
884
885 if (s_esp_httpd) {
886 httpd_stop(s_esp_httpd);
887 s_esp_httpd = nullptr;
888 }
889
890 // Clean up route contexts (unique_ptr auto-deletes on clear)
891 s_esp_route_contexts.clear();
892
893 // Free route handlers
894 mRoutes.clear();
895
896 mRunning = false;
897 FL_WARN("[HTTP] Server stopped");
898}
899
900void Server::route(const string& method, const string& path, RouteHandler handler) {
901 mRoutes.push_back({method, path, handler});
902
903 // If server is already running, register the route immediately
904 if (mRunning && s_esp_httpd) {
905 size_t idx = mRoutes.size() - 1;
906 auto ctx = fl::make_unique<EspRouteContext>(EspRouteContext{this, idx});
907
908 httpd_uri_t uri_handler = {};
909 uri_handler.uri = mRoutes[idx].path.c_str();
910 uri_handler.method = to_httpd_method(method);
911 uri_handler.handler = esp_route_handler;
912 uri_handler.user_ctx = ctx.get();
913
914 httpd_register_uri_handler(s_esp_httpd, &uri_handler);
915 s_esp_route_contexts.push_back(fl::move(ctx));
916 }
917}
918
919void Server::get(const string& path, RouteHandler handler) {
920 route("GET", path, handler);
921}
922
923void Server::post(const string& path, RouteHandler handler) {
924 route("POST", path, handler);
925}
926
927void Server::put(const string& path, RouteHandler handler) {
928 route("PUT", path, handler);
929}
930
931void Server::del(const string& path, RouteHandler handler) {
932 route("DELETE", path, handler);
933}
934
935size_t Server::update() {
936 // ESP-IDF HTTP server runs in its own task, no polling needed
937 return 0;
938}
939
940} // namespace http
941} // namespace asio
942} // namespace fl
943
944#else // !FASTLED_HAS_NETWORKING && !(FL_IS_ESP32 && IDF4+)
945
946// Stub implementation for platforms without networking (includes ESP32 IDF 3.x)
947namespace fl {
948namespace asio {
949namespace http {
950
951// Minimal definition for unique_ptr<ServerAsyncRunner> destruction
953
954optional<string> Request::header(const string&) const { return nullopt; }
955optional<string> Request::query(const string&) const { return nullopt; }
956
957Response::Response() = default;
958Response& Response::status(int) { return *this; }
959Response& Response::header(const string&, const string&) { return *this; }
960Response& Response::body(const string&) { return *this; }
961Response& Response::json(const class json&) { return *this; }
962
963Response Response::ok(const string&) { return Response(); }
965Response Response::bad_request(const string&) { return Response(); }
966Response Response::internal_error(const string&) { return Response(); }
967
968string Response::to_string() const { return ""; }
969
971Server::~Server() FL_NOEXCEPT = default;
972void Server::onExit() {}
973bool Server::start(int) { return false; }
975void Server::route(const string&, const string&, RouteHandler) {}
976void Server::get(const string&, RouteHandler) {}
977void Server::post(const string&, RouteHandler) {}
978void Server::put(const string&, RouteHandler) {}
979void Server::del(const string&, RouteHandler) {}
980size_t Server::update() { return 0; }
981
982} // namespace http
983} // namespace asio
984} // namespace fl
985
986#endif // FASTLED_HAS_NETWORKING
fl::HttpServer server
static void removeListener(Listener *listener) FL_NOEXCEPT
static void addListener(Listener *listener, int priority=0) FL_NOEXCEPT
optional< string > header(const string &name) const
Get header value by name (case-insensitive)
map< string, string > mHeaders
Definition server.h:90
map< string, string > mQuery
Definition server.h:91
optional< string > query(const string &param) const
Get query parameter value by name.
static Response not_found()
Factory method for 404 Not Found response.
Response & json(const class json &data)
Set JSON response body with automatic Content-Type header.
static Response internal_error(const string &message)
Factory method for 500 Internal Server Error response.
Response & status(int code)
Set HTTP status code.
Response & body(const string &content)
Set response body.
static Response ok(const string &body="")
Factory method for 200 OK response.
Response & header(const string &name, const string &value)
Add HTTP header.
map< string, string > mHeaders
Definition server.h:144
static Response bad_request(const string &message)
Factory method for 400 Bad Request response.
HTTP response builder (fluent interface)
Definition server.h:95
void del(const string &path, RouteHandler handler)
Convenience method for DELETE routes.
optional< Request > read_request(ClientConnection &client)
void get(const string &path, RouteHandler handler)
Convenience method for GET routes.
vector< ClientConnection > mClientSockets
Definition server.h:252
void put(const string &path, RouteHandler handler)
Convenience method for PUT routes.
~Server() FL_NOEXCEPT
Destructor (stops server if running)
vector< RouteEntry > mRoutes
Definition server.h:251
void onExit() override
int port() const
Get server port.
Definition server.h:223
void stop()
Stop server and close all connections.
bool send_response(int client_fd, const Response &response)
void post(const string &path, RouteHandler handler)
Convenience method for POST routes.
bool start(int port=8080)
Start server on specified port.
Server() FL_NOEXCEPT
Constructor.
size_t update()
Update server (process pending requests non-blocking)
fl::unique_ptr< ServerAsyncRunner > mAsyncRunner
Definition server.h:255
optional< RouteHandler > find_handler(const string &method, const string &path) const
void route(const string &method, const string &path, RouteHandler handler)
Register route handler using fl::function.
bool setup_listen_socket(int port)
void close_client(size_t index)
static constexpr fl::size npos
Definition string.h:195
void register_runner(Runner *r)
Register a runner.
static Executor & instance()
void unregister_runner(Runner *r)
Unregister a runner.
void clear() FL_NOEXCEPT
Definition vector.h:634
void push_back(const T &value) FL_NOEXCEPT
Definition vector.h:624
Task executor — runs registered task runners and manages the run loop.
#define FL_WARN(X)
Definition log.h:276
Centralized logging categories for FastLED hardware interfaces and subsystems.
function< Response(const Request &)> RouteHandler
Route handler function signature Takes const reference to Request, returns Response by value.
Definition server.h:151
constexpr remove_reference< T >::type && move(T &&t) FL_NOEXCEPT
Definition s16x16x4.h:28
string to_string(T value) FL_NOEXCEPT
Definition string.h:450
char tolower(char c) FL_NOEXCEPT
Convert character to lowercase.
Definition cctype.h:32
constexpr int type_rank< T >::value
MapRedBlackTree< Key, T, Compare, fl::allocator_slab< char > > map
Definition map.h:283
fl::enable_if<!fl::is_array< T >::value, unique_ptr< T > >::type make_unique(Args &&... args) FL_NOEXCEPT
Definition unique_ptr.h:261
Optional< T > optional
Definition optional.h:16
int snprintf(char *buffer, fl::size size, const char *format, const Args &... args) FL_NOEXCEPT
Snprintf-like formatting function that writes to a buffer.
Definition stdio.h:666
void * malloc(size_t size)
Definition malloc.cpp.hpp:9
void free(void *ptr)
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
constexpr nullopt_t nullopt
Definition optional.h:13
int atoi(const char *str)
fl::ptrdiff ptrdiff_t
Definition s16x16x4.h:226
bool isspace(char c) FL_NOEXCEPT
Check if character is whitespace (space, tab, newline, carriage return)
Definition cctype.h:18
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT