FastLED 3.10.8
Loading...
Searching...
No Matches
remote.cpp.hpp
Go to the documentation of this file.
1#include "fl/remote/remote.h"
2#include "fl/stl/int.h"
3#include "fl/stl/json.h"
4#include "fl/log/log.h"
5#include "fl/system/sketch_macros.h" // FL_PLATFORM_HAS_LARGE_MEMORY -- gates scheduled-RPC + result-tracking
6#include "fl/remote/rpc/rpc.h"
8#include "fl/remote/types.h"
10#include "fl/stl/chrono.h"
11#include "fl/stl/cstddef.h"
12#include "fl/stl/function.h"
13#include "fl/stl/move.h"
14#include "fl/stl/optional.h"
15#include "fl/stl/string.h"
16#include "fl/stl/strstream.h"
17#include "fl/stl/vector.h"
18#include "fl/stl/noexcept.h"
19
20namespace fl {
21
23 bool removed = mRpc.unbind(name.c_str());
24 if (removed) {
25 FL_DBG("Unregistered RPC function: " << name);
26 }
27 return removed;
28}
29
30bool Remote::has(const fl::string& name) const FL_NO_EXCEPT {
31 return mRpc.has(name.c_str());
32}
33
34// Async Response Support
35// All three sendAsync* methods touch mAsyncRequests, which Low-memory drops
36// (see #3224 Tier 1B RAM-side companion). On Low-memory these methods
37// become no-ops with a one-line FL_WARN; they remain in the API surface so
38// callers don't break, but they're link-DCE-friendly if no caller exists.
39
40#if FL_PLATFORM_HAS_LARGE_MEMORY
41void Remote::sendAsyncResponse(const char* method, const fl::json& result) FL_NO_EXCEPT {
42 fl::string methodName(method);
43 auto it = mAsyncRequests.find(methodName);
44 if (it == mAsyncRequests.end()) {
45 FL_WARN("No pending async request for method: " << method);
46 return;
47 }
48
49 int requestId = it->second.requestId;
50 mAsyncRequests.erase(it);
51
52 // Build JSON-RPC response
53 fl::json response = fl::json::object();
54 response.set("jsonrpc", "2.0");
55 response.set("id", requestId);
56 response.set("result", result);
57
58 // Send via response sink
59 if (mResponseSink) {
60 mResponseSink(response);
61 FL_DBG("Sent async response for " << method << " (id=" << requestId << ")");
62 }
63}
64
65void Remote::sendStreamUpdate(const char* method, const fl::json& update) FL_NO_EXCEPT {
66 fl::string methodName(method);
67 auto it = mAsyncRequests.find(methodName);
68 if (it == mAsyncRequests.end()) {
69 FL_WARN("No pending async request for method: " << method);
70 return;
71 }
72
73 int requestId = it->second.requestId;
74 // Don't erase - stream is still active
75
76 // Build JSON-RPC response with "update" marker
77 fl::json response = fl::json::object();
78 response.set("jsonrpc", "2.0");
79 response.set("id", requestId);
80
81 fl::json resultObj = fl::json::object();
82 resultObj.set("update", update);
83 response.set("result", resultObj);
84
85 // Send via response sink
86 if (mResponseSink) {
87 mResponseSink(response);
88 FL_DBG("Sent stream update for " << method << " (id=" << requestId << ")");
89 }
90}
91
92void Remote::sendStreamFinal(const char* method, const fl::json& result) FL_NO_EXCEPT {
93 fl::string methodName(method);
94 auto it = mAsyncRequests.find(methodName);
95 if (it == mAsyncRequests.end()) {
96 FL_WARN("No pending async request for method: " << method);
97 return;
98 }
99
100 int requestId = it->second.requestId;
101 mAsyncRequests.erase(it); // Stream complete - remove request
102
103 // Build JSON-RPC response with "stop" marker
104 fl::json response = fl::json::object();
105 response.set("jsonrpc", "2.0");
106 response.set("id", requestId);
107
108 fl::json resultObj = fl::json::object();
109 resultObj.set("value", result);
110 resultObj.set("stop", true);
111 response.set("result", resultObj);
112
113 // Send via response sink
114 if (mResponseSink) {
115 mResponseSink(response);
116 FL_DBG("Sent stream final for " << method << " (id=" << requestId << ")");
117 }
118}
119#else // !FL_PLATFORM_HAS_LARGE_MEMORY
120// Low-memory: async-tracking storage is dropped (#3224 Tier 1B RAM-side).
121// Keep the public API for source-compat; they no-op on this tier.
122void Remote::sendAsyncResponse(const char* /*method*/, const fl::json& /*result*/) {
123 FL_WARN_LIT("sendAsyncResponse: async dispatch not supported on Low-memory targets");
124}
125void Remote::sendStreamUpdate(const char* /*method*/, const fl::json& /*update*/) {
126 FL_WARN_LIT("sendStreamUpdate: async streams not supported on Low-memory targets");
127}
128void Remote::sendStreamFinal(const char* /*method*/, const fl::json& /*result*/) {
129 FL_WARN_LIT("sendStreamFinal: async streams not supported on Low-memory targets");
130}
131#endif
132
133// Error Reporting
134
136 fl::json params = fl::json::object();
137 params.set("message", message);
138 reportError(params);
139}
140
142 if (!mResponseSink) {
143 return;
144 }
145 fl::json notification = fl::json::object();
146 notification.set("jsonrpc", "2.0");
147 notification.set("method", "__error");
148 notification.set("params", data);
149 mResponseSink(notification);
150}
151
152// RPC Processing
153
155#if FL_PLATFORM_HAS_LARGE_MEMORY
156 // Extract optional timestamp field (0 = immediate, >0 = scheduled).
157 // Low-memory builds drop the scheduling path entirely (see #3224 Tier 1B):
158 // the LPC8xx integer-only RPC contract is invoked immediate-only.
159 u32 timestamp = 0;
160 if (request.contains("timestamp") && request["timestamp"].is_int()) {
161 timestamp = static_cast<u32>(request["timestamp"].as_int().value());
162 }
163#endif
164
165 u32 receivedAt = fl::millis();
166
167#if FL_PLATFORM_HAS_LARGE_MEMORY
168 // Execute or schedule
169 if (timestamp != 0) {
170 // Scheduled execution - result will be pushed to ResponseSink after execution
171 scheduleFunction(timestamp, receivedAt, request);
172 FL_DBG("RPC: Scheduled function - result will be pushed after execution");
173
174 // Return acknowledgment with null result and "scheduled" marker
175 fl::json response = fl::json::object();
176 if (request.contains("id")) {
177 response.set("id", request["id"]);
178 }
179 response.set("result", fl::json(nullptr));
180 response.set("scheduled", true); // Marker to not queue this response
181 return response;
182 }
183#endif
184
185 // Immediate execution path (always reached on Low-memory).
186
187#if FL_PLATFORM_HAS_LARGE_MEMORY
188 // Store request ID BEFORE invoking function (needed for async functions).
189 // This allows sendAsyncResponse() to find the request ID while function is
190 // running. Low-memory drops the async-tracking machinery (no production
191 // LowMemory sketch uses bindAsync / sendAsyncResponse today) -- see
192 // #3224 Tier 1B.
193 if (request.contains("id") && request.contains("method")) {
194 fl::string methodName = request["method"].as_string().value_or("");
195 int requestId = request["id"].as_int().value_or(0);
196 mAsyncRequests[methodName] = {requestId, receivedAt};
197 FL_DBG("Stored request ID for " << methodName.c_str() << " (id=" << requestId << ")");
198 }
199#endif
200
201 // Immediate execution - pass directly to Rpc
202 fl::json response = mRpc.handle(request);
203
204#if FL_PLATFORM_HAS_LARGE_MEMORY
205 // For async functions, response already sent via sendAsyncResponse().
206 // Envelope marker names renamed in #3228 -- accept legacy `__async` for
207 // one release of back-compat in case external Server forks set it.
208 bool ackAlready = (response.contains("ackOnly") && response["ackOnly"].as_bool().value_or(false))
209 || (response.contains("__async") && response["__async"].as_bool().value_or(false));
210 if (ackAlready) {
211 // Don't return response (ACK already sent by Rpc).
212 // Return a skip-envelope to prevent Server from queueing it.
213 fl::json nullResponse = fl::json::object();
214 nullResponse.set("noEnqueue", true);
215 return nullResponse;
216 }
217
218 // For sync functions, remove request ID (not needed)
219 if (request.contains("method")) {
220 fl::string methodName = request["method"].as_string().value_or("");
221 mAsyncRequests.erase(methodName);
222 }
223#endif
224
225#if FL_PLATFORM_HAS_LARGE_MEMORY
226 // Record result for the post-call results vector. LowMemory targets drop
227 // mResults entirely (see #3224 Tier 1B) -- RPC callers there receive
228 // results directly via the response sink, not through Remote::mResults.
229 if (response.contains("result") && request.contains("method")) {
230 fl::string funcName = request["method"].as_string().value_or("");
231 recordResult(funcName, response["result"], 0, receivedAt, receivedAt, false);
232 }
233#else
234 (void)receivedAt;
235#endif
236
237 return response;
238}
239
240#if FL_PLATFORM_HAS_LARGE_MEMORY
241void Remote::scheduleFunction(u32 timestamp, u32 receivedAt, const fl::json& jsonRpcRequest) FL_NO_EXCEPT {
242 // Make explicit copy for capture (avoid reference issues)
243 fl::json requestCopy = jsonRpcRequest;
244 fl::string funcName = requestCopy["method"].as_string().value_or("unknown");
245
246 // Wrap RPC execution in lambda and schedule it
247 mScheduler.schedule(timestamp, [this, requestCopy, timestamp, receivedAt, funcName]() {
248 u32 executedAt = fl::millis();
249
250 // Execute JSON-RPC request
251 fl::json response = mRpc.handle(requestCopy);
252
253 // Record result with timing metadata
254 if (response.contains("result") && requestCopy.contains("method")) {
255 recordResult(funcName, response["result"], timestamp, receivedAt, executedAt, true);
256 }
257 });
258
259 FL_DBG("Scheduled RPC: " << funcName << " at " << timestamp);
260}
261
262void Remote::recordResult(const fl::string& funcName, const fl::json& result, u32 scheduledAt, u32 receivedAt, u32 executedAt, bool wasScheduled) FL_NO_EXCEPT {
263 mResults.push_back({funcName, result, scheduledAt, receivedAt, executedAt, wasScheduled});
264}
265#endif
266
267// Update Loop
268
269size_t Remote::tick(u32 currentTimeMs) FL_NO_EXCEPT {
270#if FL_PLATFORM_HAS_LARGE_MEMORY
271 // Clear previous results
272 mResults.clear();
273
274 // Delegate to generic scheduler - tasks handle their own execution and result recording
275 return mScheduler.tick(currentTimeMs);
276#else
277 (void)currentTimeMs;
278 return 0;
279#endif
280}
281
282// Utility Methods
283
285#if FL_PLATFORM_HAS_LARGE_MEMORY
286 return mScheduler.pendingCount();
287#else
288 return 0;
289#endif
290}
291
293#if FL_PLATFORM_HAS_LARGE_MEMORY
294 if ((flags & ClearFlags::Results) != ClearFlags::None) {
295 mResults.clear();
296 FL_DBG("Cleared RPC results");
297 }
298 if ((flags & ClearFlags::Scheduled) != ClearFlags::None) {
299 mScheduler.clear();
300 FL_DBG("Cleared scheduled RPC calls");
301 }
302#endif
303 if ((flags & ClearFlags::Functions) != ClearFlags::None) {
304 mRpc.clear();
305 FL_DBG("Cleared registered RPC functions");
306 }
307}
308
309// Constructor
310
314
316 : Server(fl::move(source), fl::move(sink))
317{
318#if FL_PLATFORM_HAS_LARGE_MEMORY
319 setResponseStreamSink(fl::move(streamSink));
320#else
321 (void)streamSink;
322#endif
323
324 // Set request handler to processRpc
325 setRequestHandler([this](const fl::json& request) {
326 return processRpc(request);
327 });
328
329 // Set response sink on Rpc for async ACKs
330 mRpc.setResponseSink([this](const fl::json& response) {
331 // Send response directly via Server's response sink
332 if (mResponseSink) {
333 mResponseSink(response);
334 }
335 });
336
337#if FL_PLATFORM_HAS_LARGE_MEMORY
338 mRpc.setResponseStreamSink([this](fl::JsonStreamCallback writeJson) {
339 if (mResponseStreamSink) {
340 mResponseStreamSink(fl::move(writeJson));
341 }
342 });
343#endif
344}
345
346// Server Coordination
347
348size_t Remote::update(u32 currentTimeMs) FL_NO_EXCEPT {
349 size_t processed = Server::pull(); // Pull requests from Server
350 size_t executed = tick(currentTimeMs); // Process scheduled tasks
351
352#if FL_PLATFORM_HAS_LARGE_MEMORY
353 // Push scheduled results as JSON-RPC responses. Gated on Low-memory
354 // because mResults is dropped there (see #3224 Tier 1B).
355 for (const auto& r : mResults) {
356 fl::json response = fl::json::object();
357 response.set("result", r.result);
358 // Note: We don't have the original request ID for scheduled calls
359 // This could be improved by storing the ID with RpcResult
360 mOutgoingQueue.push_back(response);
361 }
362#endif
363
364 size_t sent = Server::push(); // Push responses from Server
365 return processed + executed + sent;
366}
367
368// Schema Methods
369
372
373 // Get flat JSON schema from underlying RPC
374 // Format: [["methodName", "returnType", [["param1", "type1"], ...]], ...]
375 fl::json jsonMethods = mRpc.methods();
376
377 if (!jsonMethods.is_array()) {
378 return result;
379 }
380
381 // Convert each flat method array to MethodInfo struct
382 for (fl::size i = 0; i < jsonMethods.size(); i++) {
383 fl::json method = jsonMethods[i];
384
385 if (!method.is_array() || method.size() < 3) {
386 continue; // Invalid method format
387 }
388
389 MethodInfo info;
390
391 // method[0] = method name
392 info.name = method[0].as_string().value_or("");
393
394 // method[1] = return type
395 info.returnType = method[1].as_string().value_or("void");
396
397 // method[2] = params array: [["param1", "type1"], ["param2", "type2"], ...]
398 if (method[2].is_array()) {
399 fl::json params = method[2];
400 for (fl::size j = 0; j < params.size(); j++) {
401 fl::json param = params[j];
402 if (param.is_array() && param.size() >= 2) {
403 ParamInfo paramInfo;
404 paramInfo.name = param[0].as_string().value_or("");
405 paramInfo.type = param[1].as_string().value_or("unknown");
406 info.params.push_back(fl::move(paramInfo));
407 }
408 }
409 }
410
411 // Flat schema doesn't include description/tags
412 info.description = "";
413 info.tags.clear();
414
415 result.push_back(fl::move(info));
416 }
417
418 return result;
419}
420
421} // namespace fl
int requestId
FastLED chrono implementation - duration types for time measurements.
Remote(RequestSource source, ResponseSink sink) FL_NO_EXCEPT
Construct with I/O callbacks.
void clear(ClearFlags flags) FL_NO_EXCEPT
Clear state (bitwise OR of ClearFlags)
fl::ParamInfo ParamInfo
Definition remote.h:52
fl::json processRpc(const fl::json &request) FL_NO_EXCEPT
Process JSON-RPC request (with optional "timestamp" field for scheduling) Returns JSON-RPC response: ...
fl::MethodInfo MethodInfo
Definition remote.h:51
size_t pendingCount() const FL_NO_EXCEPT
Get number of pending scheduled calls.
bool has(const fl::string &name) const FL_NO_EXCEPT
Check if method is registered.
fl::RemoteClearFlags ClearFlags
Definition remote.h:50
bool unbind(const fl::string &name) FL_NO_EXCEPT
Unregister method by name.
fl::vector< MethodInfo > methods() const FL_NO_EXCEPT
Get method information for all registered methods.
void sendAsyncResponse(const char *method, const fl::json &result) FL_NO_EXCEPT
Send async response for a previously-called async method The request ID is automatically retrieved fr...
void sendStreamUpdate(const char *method, const fl::json &update) FL_NO_EXCEPT
Send stream update for a streaming async method (ASYNC_STREAM mode) The request ID is automatically r...
void sendStreamFinal(const char *method, const fl::json &result) FL_NO_EXCEPT
Send final stream response for a streaming async method (ASYNC_STREAM mode) The request ID is automat...
void reportError(const fl::string &message) FL_NO_EXCEPT
Send an error notification to the remote peer.
fl::Rpc mRpc
Definition remote.h:225
size_t tick(u32 currentTimeMs) FL_NO_EXCEPT
Process scheduled calls (call regularly)
fl::vector< fl::json > mOutgoingQueue
Definition server.h:104
fl::function< fl::optional< fl::json >()> RequestSource
Definition server.h:31
void setRequestHandler(RequestHandler handler) FL_NO_EXCEPT
Set request handler.
size_t update() FL_NO_EXCEPT
Main update: pull + push.
Server() FL_NO_EXCEPT
Default constructor.
size_t pull() FL_NO_EXCEPT
Pull requests from source, process, queue responses.
void setResponseStreamSink(ResponseStreamSink sink) FL_NO_EXCEPT
Set streaming response sink callback.
size_t push() FL_NO_EXCEPT
Push queued responses to sink.
fl::function< void(const fl::json &)> ResponseSink
Definition server.h:32
ResponseSink mResponseSink
Definition server.h:99
const char * c_str() const FL_NO_EXCEPT
basic_string & erase(fl::size pos=0, fl::size count=npos) FL_NO_EXCEPT
size_t size() const FL_NO_EXCEPT
Definition json.h:798
fl::optional< fl::string > as_string() const FL_NO_EXCEPT
Definition json.h:443
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:866
bool is_array() const FL_NO_EXCEPT
Definition json.h:389
bool contains(size_t idx) const FL_NO_EXCEPT
Definition json.h:790
static json object() FL_NO_EXCEPT
Definition json.h:857
void clear() FL_NO_EXCEPT
Definition vector.h:634
FastLED's Elegant JSON Library: fl::json
#define FL_WARN(...)
Definition log.h:437
#define FL_WARN_LIT(LITERAL)
Definition log.h:455
#define FL_DBG
Definition log.h:553
Centralized logging categories for FastLED hardware interfaces and subsystems.
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
constexpr int type_rank< T >::value
fl::u32 millis() FL_NO_EXCEPT
Universal millisecond timer - returns milliseconds since system startup.
fl::function< void(fl::JsonStreamWriter &)> JsonStreamCallback
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
fl::function< void(fl::JsonStreamCallback)> ResponseStreamSink
Base definition for an LED controller.
Definition crgb.hpp:179
fl::vector< ParamInfo > params
Definition rpc.h:125
fl::vector< fl::string > tags
Definition rpc.h:128
fl::string name
Definition rpc.h:124
fl::string type
Definition rpc.h:117
fl::string description
Definition rpc.h:127
fl::string name
Definition rpc.h:116
fl::string returnType
Definition rpc.h:126