src/fl/remote/ARCHITECTURE.md
The fl/remote module provides an ad-hoc JSON-RPC-inspired server for FastLED with a clean separation between transport (I/O) and application logic (RPC dispatch).
Note: This is NOT strict JSON-RPC 2.0, but a simplified ad-hoc protocol inspired by JSON-RPC.
┌───────────────────────────────────┐
│ fl::Remote (RPC Server) │ <- Application Layer
│ - Method registration │
│ - Request dispatch │
│ - Scheduling │
└─────────────┬─────────────────────┘
│
┌────────┴────────┐
│ Factory Functions│ <- Composition Layer
│ (or Callbacks) │
└────────┬────────┘
│
┌───▼────────────────────────┐
│ Transport Layer │
│ ┌─────────┐ ┌──────────┐ │
│ │ Serial │ │ HTTP │ │
│ └─────────┘ └──────────┘ │
└────────────────────────────┘
fl/remote/transport/)Purpose: I/O operations for different transport mechanisms (Serial, HTTP, etc.)
transport/serial/)Files:
serial.h - Public API for serial I/Oserial.cpp.hpp - Implementation of serialization functionsFunctions:
readSerialLine<T>(serial) - Read line from any serial-like objectwriteSerialLine<T>(serial, str) - Write line to any serial-like objectformatJsonResponse(json, prefix) - Serialize JSON to compact string with prefixOptimizations:
fl::string_view to avoid allocationstransport/http/)Purpose: HTTP/1.1 streaming with chunked transfer encoding for bidirectional JSON-RPC
Files:
stream_transport.h/.cpp.hpp - Base class for HTTP streamingstream_server.h/.cpp.hpp - Server-side implementationstream_client.h/.cpp.hpp - Client-side implementationchunked_encoding.h/.cpp.hpp - HTTP chunked encoding parserhttp_parser.h/.cpp.hpp - HTTP request/response parserconnection.h/.cpp.hpp - Connection lifecycle managementnative_server.h/.cpp.hpp - Native platform server (POSIX sockets)native_client.h/.cpp.hpp - Native platform client (POSIX sockets)PROTOCOL.md - Full protocol specificationClasses:
HttpStreamTransport (abstract base):
RequestSource and ResponseSink interfacesHttpStreamServer:
HttpStreamTransport/rpc endpointNativeHttpServer for socket I/OHttpStreamClient:
HttpStreamTransport/rpc endpointNativeHttpClient for socket I/ORPC Modes Supported:
sendFinal()Protocol Features:
Key Property: Network-based transport with automatic reconnection and heartbeat
transport/serial.h)Purpose: Combine transport I/O with JSON parsing for fl::Remote
Functions:
createSerialRequestSource(prefix) - Composes:
string_view (zero-copy)string_view (zero-copy)fl::optional<fl::json>createSerialResponseSink(prefix) - Composes:
Key Property: Factory functions return callbacks for fl::Remote constructor
fl/remote/remote.h)Purpose: High-level RPC server
Functionality:
bind())The transport layer uses fl::string_view to avoid unnecessary string allocations:
// OLD: 3 allocations
fl::string input = line.value(); // Copy 1
input = input.substr(prefixLen); // Copy 2
input = input.trim(); // Copy 3
// NEW: 1 allocation
fl::string_view view = line.value(); // Zero-copy reference
view.remove_prefix(prefixLen); // Pointer arithmetic
// ... trim using remove_prefix/remove_suffix
fl::string input(view); // Single copy for JSON parsing
#include "fl/remote/remote.h"
#include "fl/stl/asio/http/stream_server.h"
// Create HTTP server transport
auto transport = fl::make_shared<fl::HttpStreamServer>(8080);
// Create Remote with callbacks
fl::Remote remote(
[&transport]() { return transport->readRequest(); },
[&transport](const fl::json& r) { transport->writeResponse(r); }
);
// Register SYNC method
remote.bind("add", [](int a, int b) { return a + b; });
// Register ASYNC method
remote.bindAsync("longTask", [](fl::ResponseSend& response, int duration) {
// ACK sent automatically, result sent later
setTimeout([&response, duration]() {
response.send(duration * 2);
}, duration);
}, fl::RpcMode::ASYNC);
// Update loop
void loop() {
transport->update(millis()); // Handle HTTP I/O
remote.update(millis()); // Process RPC requests
}
See docs/RPC_HTTP_STREAMING.md for complete migration guide.
// Custom JSONL streaming protocol
inline fl::function<fl::optional<fl::json>()>
createJsonlRequestSource() {
return []() {
// Reuse transport layer for I/O
fl::SerialReader serial;
auto line = readSerialLine(serial);
if (!line.has_value()) return fl::nullopt;
// Parse JSON directly
return fl::json::parse(line.value());
};
}
fl/remote/
├── remote.h # High-level RPC server
├── remote.cpp.hpp
├── rpc/
│ ├── rpc.h # RPC function binding and dispatch
│ ├── rpc.cpp.hpp
│ ├── rpc_mode.h # RPC modes (SYNC, ASYNC, ASYNC_STREAM)
│ ├── rpc_registry.h # RPC method registry
│ ├── response_send.h # Response API for ASYNC methods
│ ├── response_aware_traits.h # Type traits for ResponseSend detection
│ ├── response_aware_binding.h # Invoker for response-aware methods
│ ├── server.h # RequestSource/ResponseSink types
│ └── _build.hpp
└── transport/
├── serial.h # Serial I/O + factory functions
├── serial.cpp.hpp # formatJsonResponse implementation
├── http/ # HTTP streaming transport
│ ├── README.md # Architecture overview
│ ├── PROTOCOL.md # Protocol specification
│ ├── stream_transport.h # Base class for HTTP streaming
│ ├── stream_transport.cpp.hpp
│ ├── stream_server.h # Server-side implementation
│ ├── stream_server.cpp.hpp
│ ├── stream_client.h # Client-side implementation
│ ├── stream_client.cpp.hpp
│ ├── chunked_encoding.h # Chunked transfer encoding
│ ├── chunked_encoding.cpp.hpp
│ ├── http_parser.h # HTTP request/response parser
│ ├── http_parser.cpp.hpp
│ ├── connection.h # Connection lifecycle
│ ├── connection.cpp.hpp
│ ├── native_server.h # Native server (POSIX)
│ ├── native_server.cpp.hpp
│ ├── native_client.h # Native client (POSIX)
│ ├── native_client.cpp.hpp
│ └── _build.hpp
└── _build.hpp
The serial transport uses a simplified JSON-RPC-inspired format (NOT strict JSON-RPC 2.0):
Request:
{
"method": "functionName",
"params": [arg1, arg2, ...],
"id": 123,
"timestamp": 1000 // Optional, for scheduled execution
}
Response:
{
"jsonrpc": "2.0",
"result": value,
"id": 123
}
Error Response:
{
"jsonrpc": "2.0",
"error": {"code": -32601, "message": "Method not found"},
"id": 123
}
The HTTP transport uses full JSON-RPC 2.0 compliance with three RPC modes:
Client → Server: POST /rpc
{"jsonrpc":"2.0","method":"add","params":[2,3],"id":1}
Server → Client:
{"jsonrpc":"2.0","result":5,"id":1}
Client → Server: POST /rpc
{"jsonrpc":"2.0","method":"longTask","params":[1000],"id":2}
Server → Client (immediate):
{"ack":true}
Server → Client (later):
{"jsonrpc":"2.0","result":2000,"id":2}
Client → Server: POST /rpc
{"jsonrpc":"2.0","method":"streamData","params":[5],"id":3}
Server → Client (immediate):
{"ack":true}
Server → Client (streaming updates):
{"update":0}
{"update":1}
{"update":2}
{"update":3}
{"update":4}
Server → Client (final):
{"value":5,"stop":true}
Protocol Details: See transport/http/PROTOCOL.md for complete specification.
Migration Guide: See docs/RPC_HTTP_STREAMING.md for migration from Serial to HTTP.
FastLED's RPC system supports three modes for different use cases:
Use case: Simple request/response with immediate result.
Registration:
remote.bind("add", [](int a, int b) {
return a + b; // Result sent immediately
});
Flow: Request → Process → Response
Use case: Long-running tasks where immediate response is not possible.
Registration:
remote.bindAsync("longTask", [](fl::ResponseSend& response, int duration) {
// ACK sent automatically
// Process in background
setTimeout([&response, duration]() {
response.send(duration * 2); // Send result when ready
}, duration);
}, fl::RpcMode::ASYNC);
Flow: Request → ACK → [processing] → Response
Use case: Progressive tasks with incremental updates (progress bars, data streaming).
Registration:
remote.bindAsync("streamData", [](fl::ResponseSend& response, int count) {
// ACK sent automatically
// Send progressive updates
for (int i = 0; i < count; i++) {
setTimeout([&response, i]() {
response.sendUpdate(i); // Send update
}, i * 100);
}
// Send final result
setTimeout([&response, count]() {
response.sendFinal(count); // Send final with "stop: true"
}, count * 100);
}, fl::RpcMode::ASYNC_STREAM);
Flow: Request → ACK → [updates] → Final Response
The ResponseSend class provides three methods for ASYNC/ASYNC_STREAM modes:
class ResponseSend {
public:
// ASYNC: Send final result
void send(const fl::json& result);
// ASYNC_STREAM: Send update (no "stop" marker)
void sendUpdate(const fl::json& update);
// ASYNC_STREAM: Send final result (with "stop: true")
void sendFinal(const fl::json& result);
};
Note: The ACK is sent automatically by the framework when a response-aware method is invoked. You only need to call send(), sendUpdate(), or sendFinal() for the actual results.
See examples/Validation/MIGRATION_JsonRpcTransport.md for migration guide from old API.
Key Changes:
normalizeJsonRpcRequest())filterSchemaResponse())FlSerialIn → SerialReader, FlSerialOut → SerialWriterSee docs/RPC_HTTP_STREAMING.md for complete migration guide.
Key Changes:
Remote::createSerial() with callback-based constructorHttpStreamServer or HttpStreamClient transport objecttransport->update(millis()) to loop┌──────┐ ┌────────┐
│Client│ │Server │
└──┬───┘ └───┬────┘
│ {"method":"add", │
│ "params":[2,3],"id":1}│
├──────────────────────>│
│ │ (process)
│ {"result":5,"id":1} │
│<───────────────────────┤
│ │
┌──────┐ ┌────────┐
│Client│ │Server │
└──┬───┘ └───┬────┘
│ {"method":"longTask", │
│ "params":[1000],"id":2}│
├──────────────────────>│
│ {"ack":true} │
│<───────────────────────┤
│ │
│ │ (background processing)
│ │
│ {"result":2000,"id":2} │
│<───────────────────────┤
│ │
┌──────┐ ┌────────┐
│Client│ │Server │
└──┬───┘ └───┬────┘
│ {"method":"streamData",│
│ "params":[3],"id":3} │
├──────────────────────>│
│ {"ack":true} │
│<───────────────────────┤
│ │
│ {"update":0} │
│<───────────────────────┤
│ {"update":1} │
│<───────────────────────┤
│ {"update":2} │
│<───────────────────────┤
│ {"value":3,"stop":true}│
│<───────────────────────┤
│ │
┌─────────────────────────────────────────────────────────────────────┐
│ fl::Remote │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ RPC Registry │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ SYNC │ │ ASYNC │ │ASYNC_STREAM │ │ │
│ │ │ Methods │ │ Methods │ │ Methods │ │ │
│ │ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ (dispatch) │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Response Handling │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ Direct │ │ ACK + │ │ ACK + Updates│ │ │
│ │ │ Response │ │ Result │ │ + Final │ │ │
│ │ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
│ (RequestSource / ResponseSink)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ Transport Layer │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Serial Transport │ │ HTTP Streaming │ │
│ │ │ │ │ │
│ │ - readSerialLine │ │ - HttpStreamServer │ │
│ │ - writeSerialLine│ │ - HttpStreamClient │ │
│ │ - Zero-copy │ │ - Chunked encoding │ │
│ │ optimization │ │ - Heartbeat/keepalive │ │
│ │ │ │ - Auto-reconnection │ │
│ └──────────────────┘ │ - Multi-client support │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘