FastLED 3.9.15
Loading...
Searching...
No Matches
fetch_request.cpp.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "fl/task/executor.h"
5#include "fl/log/log.h"
6#include "fl/stl/int.h"
7
8#ifdef FASTLED_HAS_NETWORKING
9
10#include "fl/stl/cstring.h" // For memcpy
11
12// Platform-specific socket includes
13#ifdef FL_IS_WIN
14 // Minimize Windows headers to avoid conflicts with Arduino types
15 #ifndef WIN32_LEAN_AND_MEAN
16 #define WIN32_LEAN_AND_MEAN
17 #endif
18 #ifndef NOGDI
19 #define NOGDI // Exclude GDI headers (prevents CRGB conflict)
20 #endif
21 #ifndef NOMINMAX
22 #define NOMINMAX // Exclude min/max macros
23 #endif
24
25 // Undef Arduino macros that conflict with Windows SDK headers
26 #ifdef INPUT
27 #undef INPUT
28 #endif
29 #ifdef OUTPUT
30 #undef OUTPUT
31 #endif
32
33 // Prevent Windows from typedef'ing types that conflict with Arduino
34 #define boolean _FL_BOOLEAN_WORKAROUND
35 #define CRGB _FL_CRGB_WORKAROUND
36
37 // IWYU pragma: begin_keep
38 #include <winsock2.h>
39 #include <ws2tcpip.h>
40 // IWYU pragma: end_keep
41
42 // Undefine workarounds
43 #undef boolean
44 #undef CRGB
45
46 #define SOCKET_ERROR_WOULD_BLOCK WSAEWOULDBLOCK
47 #define GET_SOCKET_ERROR() WSAGetLastError()
48 #define CLOSE_SOCKET(s) closesocket(s)
49#else
50 // IWYU pragma: begin_keep
51 #include <sys/socket.h>
52 #include <sys/select.h>
53 #include <sys/time.h>
54 #include <netinet/in.h>
55 #include <netdb.h>
56 #include <unistd.h>
57 #include <fcntl.h>
58 #include <errno.h>
59#include "fl/stl/noexcept.h"
60 // IWYU pragma: end_keep
61 #define SOCKET_ERROR_WOULD_BLOCK EWOULDBLOCK
62 #define GET_SOCKET_ERROR() errno
63 #define CLOSE_SOCKET(s) close(s)
64#endif
65
66namespace fl {
67namespace net {
68namespace http {
69
70FetchRequest::FetchRequest(const fl::string& url, const FetchOptions& opts, fl::task::Promise<Response> prom)
71 : mState(DNS_LOOKUP)
72 , mPromise(prom)
73 , mParsedUrl(url)
74 , mHostname()
75 , mPort(80)
76 , mPath("/")
77 , mSocketFd(-1)
78 , mDnsResult(nullptr)
79 , mRequestBuffer()
80 , mResponseBuffer()
81 , mBytesSent(0)
82 , mStateStartTime(fl::millis())
83{
84 if (!mParsedUrl.isValid() || mParsedUrl.host().empty()) {
85 complete_error("Invalid URL");
86 return;
87 }
88 mHostname = fl::string(mParsedUrl.host().data(), mParsedUrl.host().size());
89 mPort = mParsedUrl.port();
90 fl::string_view p = mParsedUrl.path();
91 mPath = p.empty() ? fl::string("/") : fl::string(p.data(), p.size());
92}
93
94FetchRequest::~FetchRequest() FL_NOEXCEPT {
95 close_socket();
96}
97
98void FetchRequest::update() {
99 switch (mState) {
100 case DNS_LOOKUP:
101 handle_dns_lookup();
102 break;
103 case CONNECTING:
104 handle_connecting();
105 break;
106 case SENDING:
107 handle_sending();
108 break;
109 case RECEIVING:
110 handle_receiving();
111 break;
112 case COMPLETED:
113 case FAILED:
114 // Done - no-op
115 break;
116 }
117}
118
119void FetchRequest::handle_dns_lookup() {
120 // Pump async system before DNS to keep server responsive
121 fl::task::run(1000);
122
123 FL_WARN("[FETCH] Resolving hostname: " << mHostname);
124
125 // DNS lookup (blocking 10-100ms, but acceptable)
126 // Note: localhost is typically instant due to OS caching
127 mDnsResult = gethostbyname(mHostname.c_str());
128
129 // Pump async system after DNS to resume server pumping
130 fl::task::run(1000);
131
132 if (!mDnsResult) {
133 complete_error("DNS lookup failed");
134 return;
135 }
136
137 // Create socket
138 mSocketFd = socket(AF_INET, SOCK_STREAM, 0);
139 if (mSocketFd < 0) {
140 complete_error("Failed to create socket");
141 return;
142 }
143
144 // Set non-blocking mode
145#ifdef FL_IS_WIN
146 u_long mode = 1; // 1 = non-blocking
147 ioctlsocket(mSocketFd, FIONBIO, &mode);
148#else
149 int flags = fcntl(mSocketFd, F_GETFL, 0);
150 fcntl(mSocketFd, F_SETFL, flags | O_NONBLOCK);
151#endif
152
153 // Initiate non-blocking connect
154 sockaddr_in server_addr{};
155 server_addr.sin_family = AF_INET;
156 server_addr.sin_port = htons(static_cast<u16>(mPort));
157 // Use memcpy to extract address pointer — h_addr_list[0] may be misaligned
158 // on some platforms (macOS arm64 UBSan flags this)
159 char *addr_ptr = nullptr;
160 fl::memcpy(&addr_ptr, mDnsResult->h_addr_list, sizeof(addr_ptr));
161 fl::memcpy(&server_addr.sin_addr, addr_ptr, mDnsResult->h_length);
162
163 FL_WARN("[FETCH] Waiting for connection to " << mHostname << ":" << mPort);
164
165 connect(mSocketFd, (sockaddr*)&server_addr, sizeof(server_addr));
166 // connect() returns immediately with EINPROGRESS/WSAEWOULDBLOCK (expected)
167
168 mState = CONNECTING;
169 mStateStartTime = fl::millis();
170}
171
172void FetchRequest::handle_connecting() {
173 fd_set write_fds;
174 FD_ZERO(&write_fds);
175 FD_SET(mSocketFd, &write_fds);
176
177 struct timeval timeout;
178 timeout.tv_sec = 0;
179 timeout.tv_usec = 0; // Non-blocking check
180
181 int result = select(mSocketFd + 1, nullptr, &write_fds, nullptr, &timeout);
182
183 if (result > 0) {
184 // Check for connection errors
185 int sock_err = 0;
186 socklen_t len = sizeof(sock_err);
187 getsockopt(mSocketFd, SOL_SOCKET, SO_ERROR, (char*)&sock_err, &len);
188
189 if (sock_err != 0) {
190 complete_error("Connection failed");
191 return;
192 }
193
194 // Connected! Build HTTP request
195 mRequestBuffer = "GET " + mPath + " HTTP/1.1\r\n";
196 mRequestBuffer += "Host: " + mHostname + "\r\n";
197 mRequestBuffer += "Connection: close\r\n\r\n";
198
199 mBytesSent = 0;
200 mState = SENDING;
201 mStateStartTime = fl::millis();
202 } else if (result < 0) {
203 complete_error("select() failed during connection");
204 } else {
205 // Timeout check (5 seconds)
206 if (fl::millis() - mStateStartTime > 5000) {
207 complete_error("Connection timeout");
208 }
209 // else: Not ready yet, try again on next update()
210 }
211}
212
213void FetchRequest::handle_sending() {
214 ssize_t sent = send(mSocketFd,
215 mRequestBuffer.c_str() + mBytesSent,
216 mRequestBuffer.size() - mBytesSent,
217 0);
218
219 if (sent > 0) {
220 mBytesSent += sent;
221 if (mBytesSent >= mRequestBuffer.size()) {
222 FL_WARN("[FETCH] Waiting for HTTP response...");
223 mState = RECEIVING;
224 mStateStartTime = fl::millis();
225 }
226 } else if (sent < 0) {
227 int err = GET_SOCKET_ERROR();
228 if (err != SOCKET_ERROR_WOULD_BLOCK) {
229 complete_error("Send failed");
230 }
231 // EWOULDBLOCK: Try again on next update()
232 }
233}
234
235void FetchRequest::handle_receiving() {
236 char buffer[4096];
237 ssize_t bytes = recv(mSocketFd, buffer, sizeof(buffer), 0);
238
239 if (bytes > 0) {
240 mResponseBuffer.append(buffer, static_cast<size_t>(bytes));
241 mStateStartTime = fl::millis(); // Reset timeout on data received
242 } else if (bytes == 0) {
243 // Connection closed - parse response and complete
244 Response resp = parse_http_response(mResponseBuffer);
245 complete_success(resp);
246 } else {
247 int err = GET_SOCKET_ERROR();
248 if (err == SOCKET_ERROR_WOULD_BLOCK) {
249 // Check timeout (5 seconds)
250 if (fl::millis() - mStateStartTime > 5000) {
251 complete_error("Response timeout");
252 }
253 // else: Not ready, try again on next update()
254 } else {
255 complete_error("Receive failed");
256 }
257 }
258}
259
260Response FetchRequest::parse_http_response(const fl::string& raw) {
261 const char* data = raw.c_str();
262 const size_t len = raw.size();
263
264 // Find end of headers (double CRLF)
265 size_t header_end = raw.find("\r\n\r\n");
266 if (header_end == fl::string::npos) {
267 return Response(500, "Internal Server Error");
268 }
269
270 // Parse status line: "HTTP/1.1 200 OK\r\n"
271 size_t first_space = raw.find(' ');
272 size_t second_space = raw.find(' ', first_space + 1);
273 if (first_space == fl::string::npos || second_space == fl::string::npos ||
274 first_space >= header_end || second_space >= header_end) {
275 return Response(500, "Internal Server Error");
276 }
277
278 // Parse status code from digits without allocating
279 int status_code = 0;
280 for (size_t i = first_space + 1; i < second_space; ++i) {
281 status_code = status_code * 10 + (data[i] - '0');
282 }
283
284 // Status text ends at first \r\n
285 size_t status_line_end = raw.find("\r\n");
286 size_t st_len = (status_line_end != fl::string::npos ? status_line_end : header_end) - (second_space + 1);
287
288 Response resp(status_code, fl::string(data + second_space + 1, st_len));
289 resp.set_body(fl::string(data + header_end + 4, len - header_end - 4));
290
291 // Parse response headers using indices into raw
292 size_t pos = status_line_end + 2; // Skip past status line CRLF
293 while (pos < header_end) {
294 size_t line_end = raw.find("\r\n", pos);
295 if (line_end == fl::string::npos || line_end > header_end) {
296 line_end = header_end;
297 }
298
299 // Find colon separator
300 size_t colon = fl::string::npos;
301 for (size_t i = pos; i < line_end; ++i) {
302 if (data[i] == ':') {
303 colon = i;
304 break;
305 }
306 }
307
308 if (colon != fl::string::npos) {
309 // Build lowercase header name
310 fl::string name(data + pos, colon - pos);
311 for (size_t i = 0; i < name.size(); ++i) {
312 if (name[i] >= 'A' && name[i] <= 'Z') {
313 name[i] += ('a' - 'A');
314 }
315 }
316
317 // Value: skip colon and optional leading space
318 size_t val_start = colon + 1;
319 if (val_start < line_end && data[val_start] == ' ') {
320 ++val_start;
321 }
322 resp.set_header(name, fl::string(data + val_start, line_end - val_start));
323 }
324
325 pos = line_end + 2;
326 }
327
328 return resp;
329}
330
331void FetchRequest::complete_success(const Response& resp) {
332 close_socket();
333 mState = COMPLETED;
334
335 if (mPromise.valid() && !mPromise.is_completed()) {
336 mPromise.complete_with_value(resp);
337 }
338}
339
340void FetchRequest::complete_error(const char* message) {
341 close_socket();
342 mState = FAILED;
343
344 if (mPromise.valid() && !mPromise.is_completed()) {
345 mPromise.complete_with_error(fl::task::Error(message));
346 }
347}
348
349void FetchRequest::close_socket() {
350 if (mSocketFd >= 0) {
351 CLOSE_SOCKET(mSocketFd);
352 mSocketFd = -1;
353 }
354}
355
356} // namespace http
357} // namespace net
358} // namespace fl
359
360#endif // FASTLED_HAS_NETWORKING
uint8_t pos
Definition Blur.ino:11
fl::size find(const char &value) const FL_NOEXCEPT
const char * c_str() const FL_NOEXCEPT
fl::size size() const FL_NOEXCEPT
Fetch options builder (fluent interface)
Definition fetch.h:195
FetchRequest(const fl::string &url, const FetchOptions &opts, fl::task::Promise< Response > promise)
Construct a new fetch request.
constexpr bool empty() const FL_NOEXCEPT
static constexpr fl::size npos
Definition string.h:195
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.
void run(fl::u32 microseconds, ExecFlags flags)
Run selected task subsystems.
void * memcpy(void *dest, const void *src, size_t n) FL_NOEXCEPT
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_NOEXCEPT
Error type for promises.
Definition promise.h:39