FastLED 3.10.9
Loading...
Searching...
No Matches
rpc.cpp.hpp
Go to the documentation of this file.
1#include "fl/remote/rpc/rpc.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 rpc.discover
10#include "fl/stl/optional.h"
11#include "fl/stl/shared_ptr.h"
12#include "fl/stl/string.h"
13#include "fl/stl/strstream.h"
14#include "fl/stl/tuple.h"
16#include "fl/stl/vector.h"
17#include "fl/stl/noexcept.h"
18
19namespace fl {
20
21// =============================================================================
22// Rpc::setResponseSink() - Set response sink for async ACKs
23// =============================================================================
24
28
30#if FL_PLATFORM_HAS_LARGE_MEMORY
31 mResponseStreamAvailable = fl::move(available);
32#else
33 (void)available;
34#endif
35}
36
38#if FL_PLATFORM_HAS_LARGE_MEMORY
39 mResponseStreamSink = fl::move(sink);
40#else
41 (void)sink;
42#endif
43}
44
45// =============================================================================
46// Rpc::bindAsync() - Bind async method with ResponseSend parameter
47// =============================================================================
48
49void Rpc::bindAsync(const char* name,
50 fl::function<void(ResponseSend&, const json&)> fn,
52 fl::string key(name);
53
54 detail::RpcEntry entry;
55 entry.mTypeTag = detail::TypeTag<void(const json&)>::id();
56 entry.mMode = mode;
57 entry.mIsResponseAware = true;
58 entry.mResponseAwareFn = fl::move(fn);
59
60#if FL_PLATFORM_HAS_LARGE_MEMORY
61 // Create schema generator for void(json) signature (params not decomposed).
62 // Gated on Low-memory per FastLED #3081 / #3079 (rpc.discover unreachable).
64#endif
65 entry.mDescription = "";
66 entry.mTags = {};
67
68 // Create a placeholder invoker (actual invocation handled in handle())
69 struct PlaceholderInvoker : public detail::ErasedInvoker {
71 // Should not be called - handle() will call mResponseAwareFn directly
73 }
74 };
76
77 mRegistry[key] = fl::move(entry);
78}
79
80// =============================================================================
81// Rpc::bindStreaming() - Bind streaming method with JsonStreamWriter parameter
82// =============================================================================
83
85#if FL_PLATFORM_HAS_LARGE_MEMORY
86 fl::string key(name);
87
88 detail::RpcEntry entry;
89 entry.mTypeTag = detail::TypeTag<void(const json&)>::id();
90 entry.mMode = RpcMode::SYNC;
91 entry.mIsStreaming = true;
92 entry.mStreamingFn = fl::move(fn);
93
95 entry.mDescription = "";
96 entry.mTags = {};
97
98 struct PlaceholderInvoker : public detail::ErasedInvoker {
101 }
102 };
104
105 mRegistry[key] = fl::move(entry);
106#else
107 (void)name;
108 (void)fn;
109#endif
110}
111
112// =============================================================================
113// Rpc::handle() - Process JSON-RPC requests
114// =============================================================================
115
116// Compact error-message constants. Long error strings inflate the
117// per-call-site `fl::string` ctor + concat code on Cortex-M0+ /
118// no-FPU targets where every `.text` byte counts. Large-memory targets
119// keep the descriptive form; Low-memory targets use the short form.
120#if FL_PLATFORM_HAS_LARGE_MEMORY
121# define FL_RPC_ERR_NO_METHOD "Invalid Request: missing 'method'"
122# define FL_RPC_ERR_METHOD_NOT_STR "Invalid Request: 'method' must be a string"
123# define FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX "Method not found: "
124# define FL_RPC_ERR_PARAMS_NOT_ARRAY "Invalid params: must be an array"
125# define FL_RPC_ERR_INVALID_PARAMS_PREFIX "Invalid params: "
126#else
127# define FL_RPC_ERR_NO_METHOD "method"
128# define FL_RPC_ERR_METHOD_NOT_STR "method"
129# define FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX "404: "
130# define FL_RPC_ERR_PARAMS_NOT_ARRAY "params"
131# define FL_RPC_ERR_INVALID_PARAMS_PREFIX "params: "
132#endif
133
135 // Extract method name
136 if (!request.contains("method")) {
137 FL_ERROR("RPC: Invalid Request - missing 'method' field");
138 return detail::makeJsonRpcError(-32600, FL_RPC_ERR_NO_METHOD, request["id"]);
139 }
140
141 auto methodOpt = request["method"].as_string();
142 if (!methodOpt.has_value()) {
143 FL_ERROR("RPC: Invalid Request - 'method' must be a string");
144 return detail::makeJsonRpcError(-32600, FL_RPC_ERR_METHOD_NOT_STR, request["id"]);
145 }
146 fl::string methodName = methodOpt.value();
147
148#if FL_PLATFORM_HAS_LARGE_MEMORY
149 // Handle built-in rpc.discover method. Gated on Low-memory targets because
150 // it transitively anchors `TypedSchemaGenerator<Sig>::params()` for every
151 // registered method (each 800+ B per signature) plus the schema-building
152 // JSON tree. The LPC8xx / AVR-class JSON-RPC bring-up surface treats the
153 // RPC catalog as a known constant -- callers don't introspect at runtime.
154 // See FastLED #3081.
155 if (methodName == "rpc.discover") {
156 // Stream when possible: the schema tree for a large registry can
157 // exceed the free heap on ESP32-S3 (FastLED #4899). Scheduled calls
158 // (non-zero timestamp) keep the inline result so it is recorded.
159 // Stream sinks run the callback synchronously, so `self` outlives it.
160 const bool scheduled = request.contains("timestamp") &&
161 request["timestamp"].as_int().value_or(0) != 0;
162 if (!scheduled && mResponseStreamSink &&
163 (!mResponseStreamAvailable || mResponseStreamAvailable())) {
164 fl::json requestId = request.contains("id") ? request["id"] : json(nullptr);
165 const bool includeId = request.contains("id");
166 const Rpc* self = this;
167 mResponseStreamSink([self, requestId, includeId](fl::JsonStreamWriter& writer) {
169 writer, requestId, includeId,
170 [self](fl::JsonStreamWriter& resultWriter) {
171 self->writeSchema(resultWriter);
172 });
173 });
174 fl::json skip = fl::json::object();
175 skip.set("noEnqueue", true);
176 return skip;
177 }
178 json response = json::object();
179 response.set("jsonrpc", "2.0");
180 response.set("result", schema());
181 if (request.contains("id")) {
182 response.set("id", request["id"]);
183 }
184 return response;
185 }
186#endif
187
188 // Look up the method
189 auto it = mRegistry.find(methodName);
190 if (it == mRegistry.end()) {
191 FL_WARN("RPC: Method not found: " << methodName.c_str());
192 return detail::makeJsonRpcError(-32601, fl::string(FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX) + methodName, request["id"]);
193 }
194
195 // Extract params (default to empty array)
196 json params = request.contains("params") ? request["params"] : json::parse("[]");
197 if (!params.is_array()) {
198 FL_ERROR("RPC: Invalid params - must be an array for method: " << methodName.c_str());
199 return detail::makeJsonRpcError(-32602, FL_RPC_ERR_PARAMS_NOT_ARRAY, request["id"]);
200 }
201
202 // Check if this is an async function
203 const detail::RpcEntry& entry = it->second;
204#if FL_PLATFORM_HAS_LARGE_MEMORY
205 if (entry.mIsStreaming) {
206 if (!mResponseStreamSink) {
207 return detail::makeJsonRpcError(-32603, "Streaming response sink not configured", request["id"]);
208 }
209
210 fl::StreamingRpcHandler streamFn = entry.mStreamingFn;
211 fl::json paramsCopy = params;
212 fl::json requestId = request.contains("id") ? request["id"] : json(nullptr);
213 const bool includeId = request.contains("id");
214 mResponseStreamSink([streamFn, paramsCopy, requestId, includeId](fl::JsonStreamWriter& writer) {
216 writer,
217 requestId,
218 includeId,
219 [streamFn, paramsCopy, requestId](fl::JsonStreamWriter& resultWriter) {
220 if (streamFn) {
221 streamFn(resultWriter, paramsCopy, requestId);
222 } else {
223 resultWriter.valueNull();
224 }
225 });
226 });
227
228 fl::json skip = fl::json::object();
229 skip.set("noEnqueue", true);
230 return skip;
231 }
232
233 bool isAsync = (entry.mMode == RpcMode::ASYNC || entry.mMode == RpcMode::ASYNC_STREAM);
234
235 // Check if this is a response-aware function (uses ResponseSend&)
236 bool isResponseAware = entry.mIsResponseAware;
237
238 // For async functions, send ACK immediately
239 if (isAsync && mResponseSink && request.contains("id")) {
240 json ack = json::object();
241 ack.set("jsonrpc", "2.0");
242 ack.set("id", request["id"]);
243
244 json ackResult = json::object();
245 ackResult.set("acknowledged", true);
246 ack.set("result", ackResult);
247
248 mResponseSink(ack);
249 FL_DBG("RPC: Sent ACK for async method: " << methodName.c_str());
250 }
251#endif
252
254
255#if FL_PLATFORM_HAS_LARGE_MEMORY
256 // Handle response-aware methods (with ResponseSend& parameter)
257 if (isResponseAware) {
258 // Create ResponseSend instance
259 fl::json requestId = request.contains("id") ? request["id"] : json(nullptr);
260 ResponseSend responseSend(requestId, mResponseSink);
261
262 // Invoke user function with ResponseSend& and raw JSON params
263 entry.mResponseAwareFn(responseSend, params);
264
265 // Return success with null result (actual responses sent via ResponseSend)
266 resultTuple = fl::make_tuple(TypeConversionResult::success(), json(nullptr));
267 } else {
268 // Regular invocation
269 resultTuple = entry.mInvoker->invoke(params);
270 }
271#else
272 // Low-memory targets only register regular (non-response-aware) sync RPCs;
273 // the bindAsync path is gated out (see #3224 Tier 1B). Drop the
274 // isResponseAware branch entirely to slim Rpc::handle.
275 resultTuple = entry.mInvoker->invoke(params);
276#endif
277
278 TypeConversionResult convResult = fl::get<0>(resultTuple);
279 json returnVal = fl::get<1>(resultTuple);
280
281 // Check for conversion errors
282 if (!convResult.ok()) {
283 FL_ERROR("RPC: Invalid params for method '" << methodName.c_str() << "': " << convResult.errorMessage().c_str());
284 return detail::makeJsonRpcError(-32602, fl::string(FL_RPC_ERR_INVALID_PARAMS_PREFIX) + convResult.errorMessage(), request["id"]);
285 }
286
287 // Build success response
288 json response = json::object();
289 response.set("jsonrpc", "2.0");
290 response.set("result", returnVal);
291
292 // Include id if present (for request/response correlation)
293 if (request.contains("id")) {
294 response.set("id", request["id"]);
295 }
296
297#if FL_PLATFORM_HAS_LARGE_MEMORY
298 // Include warnings if any. Low-memory targets emit warnings only as
299 // explicit error returns (e.g. our gated `float -> int not supported`
300 // path in #3224 Tier 1A) -- the warnings-array variant emplace + nested
301 // json::array() builder + push_back path was a measurable contributor
302 // to the LowMemory .text mass on its own.
303 if (convResult.hasWarning()) {
304 json warnings = json::array();
305 for (fl::size i = 0; i < convResult.warnings().size(); ++i) {
306 warnings.push_back(json(convResult.warnings()[i]));
307 }
308 response.set("warnings", warnings);
309 }
310
311 // For async functions, mark response to signal "ACK already sent" so
312 // downstream queue machinery skips pushing a duplicate response.
313 // Renamed from `__async` in #3228 -- field is a public envelope-control
314 // marker, not a reserved-namespace identifier.
315 if (isAsync) {
316 response.set("ackOnly", true);
317 }
318#endif
319
320 return response;
321}
322
323// =============================================================================
324// Rpc::handle_maybe() - Process notifications (no id returns nullopt)
325// =============================================================================
326
328 // If no id, this is a notification - process but don't return response
329 if (!request.contains("id")) {
330 // Still need to execute the method
331 if (request.contains("method")) {
332 auto methodOpt = request["method"].as_string();
333 if (methodOpt.has_value()) {
334 fl::string methodName = methodOpt.value();
335 auto it = mRegistry.find(methodName);
336 if (it != mRegistry.end()) {
337 json params = request.contains("params") ? request["params"] : json::parse("[]");
338 if (params.is_array()) {
339 it->second.mInvoker->invoke(params);
340 }
341 }
342 }
343 }
344 return fl::nullopt;
345 }
346
347 return handle(request);
348}
349
350// =============================================================================
351// Rpc::tags() - Returns list of unique tags
352// =============================================================================
353
356 for (auto it = mRegistry.begin(); it != mRegistry.end(); ++it) {
357 for (fl::size i = 0; i < it->second.mTags.size(); ++i) {
358 bool found = false;
359 for (fl::size j = 0; j < result.size(); ++j) {
360 if (result[j] == it->second.mTags[i]) {
361 found = true;
362 break;
363 }
364 }
365 if (!found) {
366 result.push_back(it->second.mTags[i]);
367 }
368 }
369 }
370 return result;
371}
372
373// =============================================================================
374// Rpc::methods() - Returns flat method array
375// =============================================================================
376
378 json arr = json::array();
379 for (auto it = mRegistry.begin(); it != mRegistry.end(); ++it) {
380 // Format: ["methodName", "returnType", [["param1", "type1"], ["param2", "type2"]], "mode"]
381 json methodTuple = json::array();
382 methodTuple.push_back(it->first.c_str()); // Method name
383 methodTuple.push_back(it->second.mSchemaGenerator->resultTypeName()); // Return type
384 methodTuple.push_back(it->second.mSchemaGenerator->params()); // Params array
385
386 // Add mode (sync or async)
387 const char* modeStr = (it->second.mMode == RpcMode::ASYNC) ? "async" : "sync";
388 methodTuple.push_back(modeStr);
389
390 arr.push_back(methodTuple);
391 }
392 return arr;
393}
394
395// =============================================================================
396// Rpc::schema() - Returns flat schema
397// =============================================================================
398
400 json doc = json::object();
401 doc.set("schema", methods());
402 return doc;
403}
404
405#if FL_PLATFORM_HAS_LARGE_MEMORY
406void Rpc::writeSchema(fl::JsonStreamWriter& writer) const FL_NO_EXCEPT {
407 writer.beginObject();
408 writer.key("schema");
409 writer.beginArray();
410 for (auto it = mRegistry.begin(); it != mRegistry.end(); ++it) {
411 writer.beginArray();
412 writer.value(it->first.c_str());
413 writer.value(it->second.mSchemaGenerator->resultTypeName());
414 writer.value(it->second.mSchemaGenerator->params());
415 writer.value((it->second.mMode == RpcMode::ASYNC) ? "async" : "sync");
416 writer.endArray();
417 }
418 writer.endArray();
419 writer.endObject();
420}
421#endif
422
423} // namespace fl
int requestId
Helper class for sending responses in async/streaming RPC methods.
void bindAsync(const char *name, fl::function< void(ResponseSend &, const json &)> fn, fl::RpcMode mode=fl::RpcMode::ASYNC) FL_NO_EXCEPT
Bind async method with ResponseSend& parameter (for ASYNC/ASYNC_STREAM modes) Signature: void(Respons...
Definition rpc.cpp.hpp:49
fl::unordered_map< fl::string, detail::RpcEntry > mRegistry
Definition rpc.h:417
void bindStreaming(const char *name, fl::StreamingRpcHandler fn) FL_NO_EXCEPT
Bind a method that streams its JSON-RPC result directly to the transport.
Definition rpc.cpp.hpp:84
fl::optional< json > handle_maybe(const json &request) FL_NO_EXCEPT
For notifications (no id), returns nullopt.
Definition rpc.cpp.hpp:327
void setResponseStreamSink(fl::ResponseStreamSink sink) FL_NO_EXCEPT
Set response stream sink for methods registered with bindStreaming().
Definition rpc.cpp.hpp:37
Rpc() FL_NO_EXCEPT=default
fl::function< void(const fl::json &)> mResponseSink
Definition rpc.h:418
json schema() const FL_NO_EXCEPT
Returns flat schema document.
Definition rpc.cpp.hpp:399
json handle(const json &request) FL_NO_EXCEPT
Process a JSON-RPC request.
Definition rpc.cpp.hpp:134
void setResponseSink(fl::function< void(const fl::json &)> sink) FL_NO_EXCEPT
Set response sink for sending ACK responses (used by async functions)
Definition rpc.cpp.hpp:25
fl::vector< fl::string > tags() const FL_NO_EXCEPT
Returns list of unique tags used across all methods.
Definition rpc.cpp.hpp:354
void setResponseStreamAvailable(fl::function< bool()> available) FL_NO_EXCEPT
Reports whether the stream sink currently reaches a transport.
Definition rpc.cpp.hpp:29
json methods() const FL_NO_EXCEPT
Returns flat method array: [["name", "returnType", [["param1", "type1"], ...]], .....
Definition rpc.cpp.hpp:377
bool hasWarning() const FL_NO_EXCEPT
const fl::vector< fl::string > & warnings() const FL_NO_EXCEPT
const fl::string & errorMessage() const FL_NO_EXCEPT
static TypeConversionResult success() FL_NO_EXCEPT
bool ok() const FL_NO_EXCEPT
const char * c_str() const FL_NO_EXCEPT
void set(const fl::string &key, const json &value) FL_NO_EXCEPT
Definition json.h:873
bool is_array() const FL_NO_EXCEPT
Definition json.h:389
void push_back(const json &value) FL_NO_EXCEPT
Definition json.h:925
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:849
static json object() FL_NO_EXCEPT
Definition json.h:864
static json array() FL_NO_EXCEPT
Definition json.h:860
FastLED's Elegant JSON Library: fl::json
#define FL_WARN(...)
Definition log.h:437
#define FL_DBG
Definition log.h:553
#define FL_ERROR(...)
Definition log.h:373
Centralized logging categories for FastLED hardware interfaces and subsystems.
tuple< typename fl::decay< Ts >::type... > make_tuple(Ts &&... args) FL_NO_EXCEPT
Definition tuple.h:106
auto invoke(F &&f, T1 &&t1, Args &&... args) FL_NO_EXCEPT -> enable_if_t< is_member_function_pointer< typename remove_reference< F >::type >::value &&!detail::use_pointer_syntax< T1 >::value, decltype((fl::forward< T1 >(t1).*f)(fl::forward< Args >(args)...))>
Definition functional.h:45
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:423
json makeJsonRpcError(int code, const fl::string &message, const json &id) FL_NO_EXCEPT
fl::shared_ptr< ErasedInvoker > mInvoker
fl::function< void(ResponseSend &, const json &)> mResponseAwareFn
fl::shared_ptr< ErasedSchemaGenerator > mSchemaGenerator
fl::string mDescription
fl::vector< fl::string > mTags
const void * mTypeTag
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition s16x16x4.h:30
u8 u8 fract8 blur_amount FL_NO_EXCEPT
Definition blur.h:186
Optional< T > optional
Definition optional.h:16
fl::function< void(fl::JsonStreamWriter &, const fl::json &, const fl::json &)> StreamingRpcHandler
expected< T, E > result
Alias for expected (Rust-style naming)
Definition result.h:31
void streamJsonRpcResultEnvelope(fl::JsonStreamWriter &writer, const fl::json &requestId, bool includeId, const fl::JsonStreamCallback &writeResult) FL_NO_EXCEPT
constexpr nullopt_t nullopt
Definition optional.h:13
pair_element< I, T1, T2 >::type & get(pair< T1, T2 > &p) FL_NO_EXCEPT
Definition pair.h:119
fl::function< void(fl::JsonStreamCallback)> ResponseStreamSink
int available() FL_NO_EXCEPT
RpcMode
Definition rpc_mode.h:9
Base definition for an LED controller.
#define FL_RPC_ERR_NO_METHOD
Definition rpc.cpp.hpp:127
#define FL_RPC_ERR_INVALID_PARAMS_PREFIX
Definition rpc.cpp.hpp:131
#define FL_RPC_ERR_METHOD_NOT_STR
Definition rpc.cpp.hpp:128
#define FL_RPC_ERR_PARAMS_NOT_ARRAY
Definition rpc.cpp.hpp:130
#define FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX
Definition rpc.cpp.hpp:129