FastLED 3.10.8
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 mResponseStreamSink = fl::move(sink);
32#else
33 (void)sink;
34#endif
35}
36
37// =============================================================================
38// Rpc::bindAsync() - Bind async method with ResponseSend parameter
39// =============================================================================
40
41void Rpc::bindAsync(const char* name,
42 fl::function<void(ResponseSend&, const json&)> fn,
44 fl::string key(name);
45
46 detail::RpcEntry entry;
47 entry.mTypeTag = detail::TypeTag<void(const json&)>::id();
48 entry.mMode = mode;
49 entry.mIsResponseAware = true;
50 entry.mResponseAwareFn = fl::move(fn);
51
52#if FL_PLATFORM_HAS_LARGE_MEMORY
53 // Create schema generator for void(json) signature (params not decomposed).
54 // Gated on Low-memory per FastLED #3081 / #3079 (rpc.discover unreachable).
56#endif
57 entry.mDescription = "";
58 entry.mTags = {};
59
60 // Create a placeholder invoker (actual invocation handled in handle())
61 struct PlaceholderInvoker : public detail::ErasedInvoker {
63 // Should not be called - handle() will call mResponseAwareFn directly
65 }
66 };
68
69 mRegistry[key] = fl::move(entry);
70}
71
72// =============================================================================
73// Rpc::bindStreaming() - Bind streaming method with JsonStreamWriter parameter
74// =============================================================================
75
77#if FL_PLATFORM_HAS_LARGE_MEMORY
78 fl::string key(name);
79
80 detail::RpcEntry entry;
81 entry.mTypeTag = detail::TypeTag<void(const json&)>::id();
82 entry.mMode = RpcMode::SYNC;
83 entry.mIsStreaming = true;
84 entry.mStreamingFn = fl::move(fn);
85
87 entry.mDescription = "";
88 entry.mTags = {};
89
90 struct PlaceholderInvoker : public detail::ErasedInvoker {
93 }
94 };
96
97 mRegistry[key] = fl::move(entry);
98#else
99 (void)name;
100 (void)fn;
101#endif
102}
103
104// =============================================================================
105// Rpc::handle() - Process JSON-RPC requests
106// =============================================================================
107
108// Compact error-message constants. Long error strings inflate the
109// per-call-site `fl::string` ctor + concat code on Cortex-M0+ /
110// no-FPU targets where every `.text` byte counts. Large-memory targets
111// keep the descriptive form; Low-memory targets use the short form.
112#if FL_PLATFORM_HAS_LARGE_MEMORY
113# define FL_RPC_ERR_NO_METHOD "Invalid Request: missing 'method'"
114# define FL_RPC_ERR_METHOD_NOT_STR "Invalid Request: 'method' must be a string"
115# define FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX "Method not found: "
116# define FL_RPC_ERR_PARAMS_NOT_ARRAY "Invalid params: must be an array"
117# define FL_RPC_ERR_INVALID_PARAMS_PREFIX "Invalid params: "
118#else
119# define FL_RPC_ERR_NO_METHOD "method"
120# define FL_RPC_ERR_METHOD_NOT_STR "method"
121# define FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX "404: "
122# define FL_RPC_ERR_PARAMS_NOT_ARRAY "params"
123# define FL_RPC_ERR_INVALID_PARAMS_PREFIX "params: "
124#endif
125
127 // Extract method name
128 if (!request.contains("method")) {
129 FL_ERROR("RPC: Invalid Request - missing 'method' field");
130 return detail::makeJsonRpcError(-32600, FL_RPC_ERR_NO_METHOD, request["id"]);
131 }
132
133 auto methodOpt = request["method"].as_string();
134 if (!methodOpt.has_value()) {
135 FL_ERROR("RPC: Invalid Request - 'method' must be a string");
136 return detail::makeJsonRpcError(-32600, FL_RPC_ERR_METHOD_NOT_STR, request["id"]);
137 }
138 fl::string methodName = methodOpt.value();
139
140#if FL_PLATFORM_HAS_LARGE_MEMORY
141 // Handle built-in rpc.discover method. Gated on Low-memory targets because
142 // it transitively anchors `TypedSchemaGenerator<Sig>::params()` for every
143 // registered method (each 800+ B per signature) plus the schema-building
144 // JSON tree. The LPC8xx / AVR-class JSON-RPC bring-up surface treats the
145 // RPC catalog as a known constant -- callers don't introspect at runtime.
146 // See FastLED #3081.
147 if (methodName == "rpc.discover") {
148 json response = json::object();
149 response.set("jsonrpc", "2.0");
150 response.set("result", schema());
151 if (request.contains("id")) {
152 response.set("id", request["id"]);
153 }
154 return response;
155 }
156#endif
157
158 // Look up the method
159 auto it = mRegistry.find(methodName);
160 if (it == mRegistry.end()) {
161 FL_WARN("RPC: Method not found: " << methodName.c_str());
162 return detail::makeJsonRpcError(-32601, fl::string(FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX) + methodName, request["id"]);
163 }
164
165 // Extract params (default to empty array)
166 json params = request.contains("params") ? request["params"] : json::parse("[]");
167 if (!params.is_array()) {
168 FL_ERROR("RPC: Invalid params - must be an array for method: " << methodName.c_str());
169 return detail::makeJsonRpcError(-32602, FL_RPC_ERR_PARAMS_NOT_ARRAY, request["id"]);
170 }
171
172 // Check if this is an async function
173 const detail::RpcEntry& entry = it->second;
174#if FL_PLATFORM_HAS_LARGE_MEMORY
175 if (entry.mIsStreaming) {
176 if (!mResponseStreamSink) {
177 return detail::makeJsonRpcError(-32603, "Streaming response sink not configured", request["id"]);
178 }
179
180 fl::StreamingRpcHandler streamFn = entry.mStreamingFn;
181 fl::json paramsCopy = params;
182 fl::json requestId = request.contains("id") ? request["id"] : json(nullptr);
183 const bool includeId = request.contains("id");
184 mResponseStreamSink([streamFn, paramsCopy, requestId, includeId](fl::JsonStreamWriter& writer) {
186 writer,
187 requestId,
188 includeId,
189 [streamFn, paramsCopy, requestId](fl::JsonStreamWriter& resultWriter) {
190 if (streamFn) {
191 streamFn(resultWriter, paramsCopy, requestId);
192 } else {
193 resultWriter.valueNull();
194 }
195 });
196 });
197
198 fl::json skip = fl::json::object();
199 skip.set("noEnqueue", true);
200 return skip;
201 }
202
203 bool isAsync = (entry.mMode == RpcMode::ASYNC || entry.mMode == RpcMode::ASYNC_STREAM);
204
205 // Check if this is a response-aware function (uses ResponseSend&)
206 bool isResponseAware = entry.mIsResponseAware;
207
208 // For async functions, send ACK immediately
209 if (isAsync && mResponseSink && request.contains("id")) {
210 json ack = json::object();
211 ack.set("jsonrpc", "2.0");
212 ack.set("id", request["id"]);
213
214 json ackResult = json::object();
215 ackResult.set("acknowledged", true);
216 ack.set("result", ackResult);
217
218 mResponseSink(ack);
219 FL_DBG("RPC: Sent ACK for async method: " << methodName.c_str());
220 }
221#endif
222
224
225#if FL_PLATFORM_HAS_LARGE_MEMORY
226 // Handle response-aware methods (with ResponseSend& parameter)
227 if (isResponseAware) {
228 // Create ResponseSend instance
229 fl::json requestId = request.contains("id") ? request["id"] : json(nullptr);
230 ResponseSend responseSend(requestId, mResponseSink);
231
232 // Invoke user function with ResponseSend& and raw JSON params
233 entry.mResponseAwareFn(responseSend, params);
234
235 // Return success with null result (actual responses sent via ResponseSend)
236 resultTuple = fl::make_tuple(TypeConversionResult::success(), json(nullptr));
237 } else {
238 // Regular invocation
239 resultTuple = entry.mInvoker->invoke(params);
240 }
241#else
242 // Low-memory targets only register regular (non-response-aware) sync RPCs;
243 // the bindAsync path is gated out (see #3224 Tier 1B). Drop the
244 // isResponseAware branch entirely to slim Rpc::handle.
245 resultTuple = entry.mInvoker->invoke(params);
246#endif
247
248 TypeConversionResult convResult = fl::get<0>(resultTuple);
249 json returnVal = fl::get<1>(resultTuple);
250
251 // Check for conversion errors
252 if (!convResult.ok()) {
253 FL_ERROR("RPC: Invalid params for method '" << methodName.c_str() << "': " << convResult.errorMessage().c_str());
254 return detail::makeJsonRpcError(-32602, fl::string(FL_RPC_ERR_INVALID_PARAMS_PREFIX) + convResult.errorMessage(), request["id"]);
255 }
256
257 // Build success response
258 json response = json::object();
259 response.set("jsonrpc", "2.0");
260 response.set("result", returnVal);
261
262 // Include id if present (for request/response correlation)
263 if (request.contains("id")) {
264 response.set("id", request["id"]);
265 }
266
267#if FL_PLATFORM_HAS_LARGE_MEMORY
268 // Include warnings if any. Low-memory targets emit warnings only as
269 // explicit error returns (e.g. our gated `float -> int not supported`
270 // path in #3224 Tier 1A) -- the warnings-array variant emplace + nested
271 // json::array() builder + push_back path was a measurable contributor
272 // to the LowMemory .text mass on its own.
273 if (convResult.hasWarning()) {
274 json warnings = json::array();
275 for (fl::size i = 0; i < convResult.warnings().size(); ++i) {
276 warnings.push_back(json(convResult.warnings()[i]));
277 }
278 response.set("warnings", warnings);
279 }
280
281 // For async functions, mark response to signal "ACK already sent" so
282 // downstream queue machinery skips pushing a duplicate response.
283 // Renamed from `__async` in #3228 -- field is a public envelope-control
284 // marker, not a reserved-namespace identifier.
285 if (isAsync) {
286 response.set("ackOnly", true);
287 }
288#endif
289
290 return response;
291}
292
293// =============================================================================
294// Rpc::handle_maybe() - Process notifications (no id returns nullopt)
295// =============================================================================
296
298 // If no id, this is a notification - process but don't return response
299 if (!request.contains("id")) {
300 // Still need to execute the method
301 if (request.contains("method")) {
302 auto methodOpt = request["method"].as_string();
303 if (methodOpt.has_value()) {
304 fl::string methodName = methodOpt.value();
305 auto it = mRegistry.find(methodName);
306 if (it != mRegistry.end()) {
307 json params = request.contains("params") ? request["params"] : json::parse("[]");
308 if (params.is_array()) {
309 it->second.mInvoker->invoke(params);
310 }
311 }
312 }
313 }
314 return fl::nullopt;
315 }
316
317 return handle(request);
318}
319
320// =============================================================================
321// Rpc::tags() - Returns list of unique tags
322// =============================================================================
323
326 for (auto it = mRegistry.begin(); it != mRegistry.end(); ++it) {
327 for (fl::size i = 0; i < it->second.mTags.size(); ++i) {
328 bool found = false;
329 for (fl::size j = 0; j < result.size(); ++j) {
330 if (result[j] == it->second.mTags[i]) {
331 found = true;
332 break;
333 }
334 }
335 if (!found) {
336 result.push_back(it->second.mTags[i]);
337 }
338 }
339 }
340 return result;
341}
342
343// =============================================================================
344// Rpc::methods() - Returns flat method array
345// =============================================================================
346
348 json arr = json::array();
349 for (auto it = mRegistry.begin(); it != mRegistry.end(); ++it) {
350 // Format: ["methodName", "returnType", [["param1", "type1"], ["param2", "type2"]], "mode"]
351 json methodTuple = json::array();
352 methodTuple.push_back(it->first.c_str()); // Method name
353 methodTuple.push_back(it->second.mSchemaGenerator->resultTypeName()); // Return type
354 methodTuple.push_back(it->second.mSchemaGenerator->params()); // Params array
355
356 // Add mode (sync or async)
357 const char* modeStr = (it->second.mMode == RpcMode::ASYNC) ? "async" : "sync";
358 methodTuple.push_back(modeStr);
359
360 arr.push_back(methodTuple);
361 }
362 return arr;
363}
364
365// =============================================================================
366// Rpc::schema() - Returns flat schema
367// =============================================================================
368
370 json doc = json::object();
371 doc.set("schema", methods());
372 return doc;
373}
374
375} // 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:41
fl::unordered_map< fl::string, detail::RpcEntry > mRegistry
Definition rpc.h:406
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:76
fl::optional< json > handle_maybe(const json &request) FL_NO_EXCEPT
For notifications (no id), returns nullopt.
Definition rpc.cpp.hpp:297
void setResponseStreamSink(fl::ResponseStreamSink sink) FL_NO_EXCEPT
Set response stream sink for methods registered with bindStreaming().
Definition rpc.cpp.hpp:29
fl::function< void(const fl::json &)> mResponseSink
Definition rpc.h:407
json schema() const FL_NO_EXCEPT
Returns flat schema document.
Definition rpc.cpp.hpp:369
json handle(const json &request) FL_NO_EXCEPT
Process a JSON-RPC request.
Definition rpc.cpp.hpp:126
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:324
json methods() const FL_NO_EXCEPT
Returns flat method array: [["name", "returnType", [["param1", "type1"], ...]], .....
Definition rpc.cpp.hpp:347
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:866
bool is_array() const FL_NO_EXCEPT
Definition json.h:389
void push_back(const json &value) FL_NO_EXCEPT
Definition json.h:918
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 json array() FL_NO_EXCEPT
Definition json.h:853
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.
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:30
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
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
RpcMode
Definition rpc_mode.h:9
Base definition for an LED controller.
Definition crgb.hpp:179
#define FL_RPC_ERR_NO_METHOD
Definition rpc.cpp.hpp:119
#define FL_RPC_ERR_INVALID_PARAMS_PREFIX
Definition rpc.cpp.hpp:123
#define FL_RPC_ERR_METHOD_NOT_STR
Definition rpc.cpp.hpp:120
#define FL_RPC_ERR_PARAMS_NOT_ARRAY
Definition rpc.cpp.hpp:122
#define FL_RPC_ERR_METHOD_NOT_FOUND_PREFIX
Definition rpc.cpp.hpp:121