FastLED 3.10.8
Loading...
Searching...
No Matches
stream_transport.cpp.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "fl/stl/json.h"
5#include "fl/stl/string.h"
6#include "fl/stl/cstring.h"
7#include "fl/stl/stdio.h"
8#include "fl/stl/noexcept.h"
9#include "fl/stl/compiler_control.h" // IWYU pragma: keep - FL_UNUSED
10
11namespace fl {
12namespace net {
13namespace http {
14
15// --- StreamHandle implementation ---
16
22
24 if (mUpdateCallback) {
26 }
27 return *this;
28}
29
31 mPromise.then(fl::move(cb));
32 return *this;
33}
34
36 mPromise.catch_(fl::move(cb));
37 return *this;
38}
39
43
45 return mPromise.valid();
46}
47
48// --- HttpStreamTransport implementation ---
49
50HttpStreamTransport::HttpStreamTransport(const fl::string& host, u16 port, u32 heartbeatIntervalMs)
51 FL_NO_EXCEPT : mConnection(ConnectionConfig{}) // Use default config
52 , mLastHeartbeatSent(0)
53 , mLastHeartbeatReceived(0)
54 , mHeartbeatInterval(heartbeatIntervalMs)
55 , mTimeoutMs(60000) // Default 60s timeout
56 , mWasConnected(false) {
57 // The concrete transport owns the endpoint; connect() is pure virtual here.
58 FL_UNUSED(host);
59 FL_UNUSED(port);
60}
61
63 // Note: Cannot call pure virtual disconnect() here
64 // Subclasses must clean up in their own destructors
65}
66
68 if (id.is_int()) {
69 char buf[32];
70 fl::snprintf(buf, sizeof(buf), "%d", id.as_int().value());
71 return fl::string(buf);
72 }
73 if (id.is_string()) {
74 return id.as_string().value();
75 }
76 // Fallback for other types - use string representation
77 return id.to_string();
78}
79
81 for (;;) {
82 size_t chunkSize = mReader.nextChunkSize();
83 if (chunkSize == 0) {
84 break;
85 }
86
87 u8 stackBuf[512];
88 fl::vector<u8> heapBuf;
89 fl::span<u8> outSpan;
90 if (chunkSize <= sizeof(stackBuf)) {
91 outSpan = fl::span<u8>(stackBuf, sizeof(stackBuf));
92 } else {
93 heapBuf.resize(chunkSize);
94 outSpan = heapBuf;
95 }
96
97 ChunkedReadResult result = mReader.readChunk(outSpan);
98 if (!result.hasData()) {
99 break;
100 }
101
102 fl::string jsonStr(reinterpret_cast<const char*>(result.mData.data()), result.mData.size()); // ok reinterpret cast
103 fl::json json = fl::json::parse(jsonStr.c_str());
104 if (json.is_null()) {
105 continue;
106 }
107
108 // Filter heartbeats
109 if (json["method"].as_string() == "rpc.ping") {
111 continue;
112 }
113
115
116 // Try to dispatch to pending calls/streams by id
117 if (json.contains("id") && !json["id"].is_null()) {
118 fl::string idKey = idToString(json["id"]);
119
120 if (resolveRpc(json, idKey)) {
121 continue; // Consumed by pending call
122 }
123 if (resolveRpcStream(json, idKey)) {
124 continue; // Consumed by pending stream
125 }
126 }
127
128 // Not matched - add to incoming queue for readRequest()
129 mIncomingQueue.push_back(fl::move(json));
130 }
131}
132
134 auto it = mPendingCalls.find(idKey);
135 if (it == mPendingCalls.end()) {
136 return false;
137 }
138 PendingCall* pending = &it->second;
139
140 // Check for error
141 if (msg.contains("error")) {
142 fl::task::Error err(msg["error"]["message"].as_string().value());
143 pending->promise.complete_with_error(err);
144 mPendingCalls.erase(idKey);
145 return true;
146 }
147
148 // Check for ACK (ASYNC mode sends acknowledged first)
149 if (msg.contains("result") && msg["result"].contains("acknowledged")) {
150 if (msg["result"]["acknowledged"].as_bool() == true) {
151 pending->ackReceived = true;
152 return true; // Stay pending, wait for final result
153 }
154 }
155
156 // Final result
157 pending->promise.complete_with_value(msg);
158 mPendingCalls.erase(idKey);
159 return true;
160}
161
163 auto it = mPendingStreams.find(idKey);
164 if (it == mPendingStreams.end()) {
165 return false;
166 }
167 PendingStream* pending = &it->second;
168
169 // Check for error
170 if (msg.contains("error")) {
171 fl::task::Error err(msg["error"]["message"].as_string().value());
172 pending->promise.complete_with_error(err);
173 mPendingStreams.erase(idKey);
174 return true;
175 }
176
177 // Check for ACK
178 if (msg.contains("result") && msg["result"].contains("acknowledged")) {
179 if (msg["result"]["acknowledged"].as_bool() == true) {
180 pending->ackReceived = true;
181 return true;
182 }
183 }
184
185 // Check for stream update
186 if (msg.contains("result") && msg["result"].contains("update")) {
187 if (pending->updateCallback && *pending->updateCallback) {
188 (*pending->updateCallback)(msg["result"]["update"]);
189 }
190 return true;
191 }
192
193 // Check for stream final (stop marker)
194 if (msg.contains("result") && msg["result"].contains("stop")) {
195 if (msg["result"]["stop"].as_bool() == true) {
196 // Resolve with the value field if present, otherwise the full result
197 if (msg["result"].contains("value")) {
198 pending->promise.complete_with_value(msg["result"]["value"]);
199 } else {
200 pending->promise.complete_with_value(msg["result"]);
201 }
202 mPendingStreams.erase(idKey);
203 return true;
204 }
205 }
206
207 // Unrecognized message shape - treat as final
208 pending->promise.complete_with_value(msg);
209 mPendingStreams.erase(idKey);
210 return true;
211}
212
214 int id = mNextCallId++;
215 fl::json request = fl::json::object();
216 request.set("jsonrpc", "2.0");
217 request.set("method", method);
218 request.set("params", params);
219 request.set("id", id);
220 return rpc(request);
221}
222
225
226 if (!isConnected()) {
227 p.complete_with_error(fl::task::Error("Not connected"));
228 return p;
229 }
230
231 fl::string idKey = idToString(fullRequest["id"]);
232 PendingCall pc;
233 pc.promise = p;
234 mPendingCalls.insert(idKey, fl::move(pc));
235
236 writeResponse(fullRequest);
237 return p;
238}
239
241 int id = mNextCallId++;
242 fl::json request = fl::json::object();
243 request.set("jsonrpc", "2.0");
244 request.set("method", method);
245 request.set("params", params);
246 request.set("id", id);
247 return rpcStream(request);
248}
249
251 auto updateCb = fl::make_shared<fl::function<void(const fl::json&)>>();
253
254 if (!isConnected()) {
255 p.complete_with_error(fl::task::Error("Not connected"));
256 return StreamHandle(fl::move(p), fl::move(updateCb));
257 }
258
259 fl::string idKey = idToString(fullRequest["id"]);
261 ps.promise = p;
262 ps.updateCallback = updateCb;
263 mPendingStreams.insert(idKey, fl::move(ps));
264
265 writeResponse(fullRequest);
266 return StreamHandle(p, updateCb);
267}
268
270 if (!isConnected()) {
271 return fl::nullopt;
272 }
273
274 // Process incoming data and drain into queue
277
278 // Pop from incoming queue
279 if (mIncomingQueue.empty()) {
280 return fl::nullopt;
281 }
282
283 fl::json front = fl::move(mIncomingQueue[0]);
284 // Shift remaining elements
285 fl::vector<fl::json> remaining;
286 remaining.reserve(mIncomingQueue.size() - 1);
287 for (size_t i = 1; i < mIncomingQueue.size(); i++) {
288 remaining.push_back(fl::move(mIncomingQueue[i]));
289 }
290 mIncomingQueue = fl::move(remaining);
291
292 return front;
293}
294
296 if (!isConnected()) {
297 return;
298 }
299
300 // Serialize JSON to string
301 fl::string jsonStr = response.to_string();
302
303 // Write chunked data into stack buffer (or heap for large payloads)
304 size_t needed = ChunkedWriter::chunkOverhead(jsonStr.size());
305 u8 stackBuf[512];
306 fl::vector<u8> heapBuf;
307 fl::span<u8> outSpan;
308 if (needed <= sizeof(stackBuf)) {
309 outSpan = fl::span<u8>(stackBuf, sizeof(stackBuf));
310 } else {
311 heapBuf.resize(needed);
312 outSpan = heapBuf;
313 }
314 size_t written = mWriter.writeChunk(
315 fl::span<const u8>(reinterpret_cast<const u8*>(jsonStr.c_str()), jsonStr.size()), // ok reinterpret cast
316 outSpan
317 );
318
319 // Send chunked data
320 if (written > 0) {
321 sendData(fl::span<const u8>(outSpan.data(), written));
322 }
323
324 // Update last sent time
326}
327
329 // Update connection state
330 mConnection.update(currentTimeMs);
331 bool nowConnected = isConnected();
332
333 // Handle state changes
334 if (mWasConnected != nowConnected) {
335 handleConnectionStateChange(currentTimeMs);
336 mWasConnected = nowConnected;
337 }
338
339 if (!isConnected()) {
340 // Attempt reconnection if needed
341 if (mConnection.shouldReconnect()) {
343 }
344 return;
345 }
346
347 // Send heartbeat if needed
348 u32 timeSinceLastSent = currentTimeMs - mLastHeartbeatSent;
349 if (timeSinceLastSent >= mHeartbeatInterval) {
351 }
352
353 // Check for heartbeat timeout
354 checkHeartbeatTimeout(currentTimeMs);
355
356 // Process incoming data and drain messages (resolves promises)
359}
360
364
368
372
376
378 mTimeoutMs = timeoutMs;
379}
380
384
386 // Default implementation uses system time
387 // Subclasses can override for testing
388 return fl::millis();
389}
390
392 // Default implementation does nothing
393 // Subclasses should override to implement reconnection logic
394}
395
397 if (!isConnected()) {
398 return;
399 }
400
401 // Create heartbeat request (rpc.ping notification)
402 fl::json heartbeat = fl::json::object();
403 heartbeat.set("jsonrpc", "2.0");
404 heartbeat.set("method", "rpc.ping");
405 heartbeat.set("id", fl::json()); // null value
406
407 // Write as response (uses same chunked encoding)
408 writeResponse(heartbeat);
409}
410
412 u32 timeSinceLastReceived = currentTimeMs - mLastHeartbeatReceived;
413 if (timeSinceLastReceived >= getTimeout()) {
414 // Heartbeat timeout - connection is dead
415 mConnection.onDisconnected();
416 disconnect();
417 }
418}
419
421 if (!isConnected()) {
422 return false;
423 }
424
425 // Read incoming data from socket
426 u8 buffer[1024];
427 int bytesRead = recvData(buffer);
428
429 if (bytesRead < 0) {
430 // Error reading from socket
431 mConnection.onDisconnected();
432 disconnect();
433 return false;
434 }
435
436 if (bytesRead == 0) {
437 // No data available (non-blocking socket)
438 return false;
439 }
440
441 // Feed data to chunked reader
442 mReader.feed(fl::span<const u8>(buffer, static_cast<size_t>(bytesRead)));
443
444 return true;
445}
446
448 if (isConnected()) {
449 // Just connected — use the caller-provided timestamp to avoid
450 // unsigned-underflow races between getCurrentTimeMs() and the
451 // currentTimeMs snapshot used by checkHeartbeatTimeout().
452 mLastHeartbeatSent = currentTimeMs;
453 mLastHeartbeatReceived = currentTimeMs;
454 if (mOnConnect) {
455 mOnConnect();
456 }
457 } else {
458 // Just disconnected
459 if (mOnDisconnect) {
461 }
462 }
463}
464
465} // namespace http
466} // namespace net
467} // namespace fl
const char * c_str() const FL_NO_EXCEPT
fl::size size() const FL_NO_EXCEPT
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:866
bool is_null() const FL_NO_EXCEPT
Definition json.h:381
bool contains(size_t idx) const FL_NO_EXCEPT
Definition json.h:790
static json parse(const fl::string &txt) FL_NO_EXCEPT
Definition json.h:842
static json object() FL_NO_EXCEPT
Definition json.h:857
static size_t chunkOverhead(size_t dataLen) FL_NO_EXCEPT
virtual ~HttpStreamTransport() FL_NO_EXCEPT
Virtual destructor.
void update(u32 currentTimeMs) FL_NO_EXCEPT
Update connection state (handles reconnection, heartbeat) Call this in main loop.
void handleConnectionStateChange(u32 currentTimeMs) FL_NO_EXCEPT
bool resolveRpc(const fl::json &msg, const fl::string &idKey) FL_NO_EXCEPT
void checkHeartbeatTimeout(u32 currentTimeMs) FL_NO_EXCEPT
fl::optional< fl::json > readRequest() FL_NO_EXCEPT
Read next JSON-RPC request from stream Non-blocking, returns nullopt if no complete request available...
void writeResponse(const fl::json &response) FL_NO_EXCEPT
Write JSON-RPC response to stream.
virtual u32 getCurrentTimeMs() const FL_NO_EXCEPT
Get current time in milliseconds.
virtual void triggerReconnect() FL_NO_EXCEPT
Trigger reconnection (for subclasses to override)
virtual int recvData(fl::span< u8 > buffer) FL_NO_EXCEPT=0
Receive raw data from connection.
virtual int sendData(fl::span< const u8 > data) FL_NO_EXCEPT=0
Send raw data over connection.
HttpStreamTransport(const fl::string &host, u16 port, u32 heartbeatIntervalMs=30000) FL_NO_EXCEPT
Constructor.
fl::flat_map< fl::string, PendingCall, fl::StringFastLess > mPendingCalls
bool resolveRpcStream(const fl::json &msg, const fl::string &idKey) FL_NO_EXCEPT
void setTimeout(u32 timeoutMs) FL_NO_EXCEPT
Set connection timeout.
void setHeartbeatInterval(u32 intervalMs) FL_NO_EXCEPT
Set heartbeat interval.
fl::vector< fl::json > mIncomingQueue
fl::function< void()> StateCallback
Connection state callback.
static fl::string idToString(const fl::json &id) FL_NO_EXCEPT
StreamHandle rpcStream(const fl::string &method, const fl::json &params) FL_NO_EXCEPT
Send a streaming JSON-RPC request, returns StreamHandle for intermediate data.
fl::task::Promise< fl::json > rpc(const fl::string &method, const fl::json &params) FL_NO_EXCEPT
Send a JSON-RPC request, returns promise that resolves with the final response.
fl::flat_map< fl::string, PendingStream, fl::StringFastLess > mPendingStreams
virtual void disconnect() FL_NO_EXCEPT=0
Disconnect from server.
virtual bool isConnected() const FL_NO_EXCEPT=0
Check if connected.
u32 getHeartbeatInterval() const FL_NO_EXCEPT
Get heartbeat interval.
void setOnDisconnect(StateCallback callback) FL_NO_EXCEPT
Set callback for connection lost.
void setOnConnect(StateCallback callback) FL_NO_EXCEPT
Set callback for connection established.
u32 getTimeout() const FL_NO_EXCEPT
Get connection timeout.
fl::shared_ptr< fl::function< void(const fl::json &)> > updateCallback
StreamHandle & then(fl::function< void(const fl::json &)> cb) FL_NO_EXCEPT
Register callback for final result.
StreamHandle & catch_(fl::function< void(const fl::task::Error &)> cb) FL_NO_EXCEPT
Register callback for errors.
fl::task::Promise< fl::json > mPromise
fl::shared_ptr< fl::function< void(const fl::json &)> > mUpdateCallback
StreamHandle() FL_NO_EXCEPT=default
bool valid() const FL_NO_EXCEPT
Check if handle is valid.
StreamHandle & onData(fl::function< void(const fl::json &)> cb) FL_NO_EXCEPT
Register callback for intermediate stream data.
fl::task::Promise< fl::json > & promise() FL_NO_EXCEPT
Access the underlying promise.
Handle for ASYNC_STREAM calls Provides onData() for intermediate updates, plus then()/catch_() for fi...
const T * data() const FL_NO_EXCEPT
Definition span.h:486
bool complete_with_value(const T &value) FL_NO_EXCEPT
Complete the Promise with a result (used by networking library)
Definition promise.h:183
static Promise< T > create() FL_NO_EXCEPT
Create a pending Promise.
Definition promise.h:61
bool complete_with_error(const Error &error) FL_NO_EXCEPT
Complete the Promise with an error (used by networking library)
Definition promise.h:194
Promise class that provides fluent .then() and .catch_() semantics This is a lightweight wrapper arou...
Definition promise.h:58
void reserve(fl::size n) FL_NO_EXCEPT
Definition vector.h:591
void resize(fl::size n) FL_NO_EXCEPT
Definition vector.h:593
void push_back(const T &value) FL_NO_EXCEPT
Definition vector.h:624
#define FL_UNUSED(x)
P ps[MAXP]
Definition Luminova.h:57
FastLED's Elegant JSON Library: fl::json
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
unsigned char u8
Definition stdint.h:131
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
int snprintf(char *buffer, fl::size size, const char *format) FL_NO_EXCEPT
fl::u32 millis() FL_NO_EXCEPT
Universal millisecond timer - returns milliseconds since system startup.
Optional< T > optional
Definition optional.h:16
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
constexpr nullopt_t nullopt
Definition optional.h:13
Base definition for an LED controller.
Definition crgb.hpp:179
Error type for promises.
Definition promise.h:39