Cross-platform transport implementations for fl::Remote JSON-RPC communication.
Overview
The transport layer provides generic I/O for serial communication. It is protocol-agnostic and can work with any JSON-based protocol, not just JSON-RPC.
Factory functions compose the transport layer with the JSON-RPC protocol layer (from fl/remote/rpc/protocol.h) to create RequestSource and ResponseSink callbacks for fl::Remote.
Design Goals:
- Separation of Concerns: Transport (I/O) vs Protocol (JSON-RPC logic)
- Testability: Mock serial objects for unit testing
- Portability: Works across all FastLED platforms
- Flexibility: Easy to add new transports (HTTP, WebSocket) or protocols
Architecture
┌─────────────────────────────────────────┐
│ fl::Remote (RPC Server) │
│ - Method registration │
│ - JSON-RPC dispatch │
│ - Scheduling │
└──────────────┬─────────────┬────────────┘
│ │
RequestSource ResponseSink
│ │
┌─────┴─────────────┴─────┐
│ Factory Functions │ <- Compose layers
│ (serial.h) │
└─────┬──────────────┬─────┘
│ │
┌──────────┴─────┐ ┌─────┴──────────┐
│ Protocol Layer │ │ Protocol Layer │
│ (rpc/protocol) │ │ (rpc/protocol) │
│ - Normalize │ │ - Filter │
│ requests │ │ schemas │
└────────┬───────┘ └───────┬────────┘
│ │
┌────────┴───────┐ ┌───────┴────────┐
│ Transport Layer│ │ Transport Layer│
│ (serial.h) │ │ (serial.h) │
│ - Read line │ │ - Write line │
│ - Parse JSON │ │ - Serialize │
└────────────────┘ └────────────────┘
Serial Transport
Basic Usage
return "pong";
});
}
fl::unique_ptr< fl::Remote > remote
JSON-RPC server with scheduling support.
fl::function< fl::optional< fl::json >()> createSerialRequestSource(const char *prefix="")
Create a JSON-RPC RequestSource that reads from fl:: serial input.
fl::function< void(const fl::json &)> createSerialResponseSink(const char *prefix="REMOTE: ")
Create a JSON-RPC ResponseSink that writes to fl:: serial output.
Factory Functions
createSerialRequestSource(prefix = "")
Creates a RequestSource callback that:
- Reads lines from
fl::available() / fl::read()
- Strips optional prefix from input
- Parses JSON-RPC requests
- Returns
optional<Json> (nullopt if no data)
createSerialResponseSink(prefix = "REMOTE: ")
Creates a ResponseSink callback that:
- Formats JSON-RPC responses
- Prepends optional prefix
- Writes to
fl::println()
createSerialTransport(responsePrefix, requestPrefix)
Creates both callbacks in one call:
fl::pair< fl::function< fl::optional< fl::json >()>, fl::function< void(const fl::json &)> > createSerialTransport(const char *responsePrefix="REMOTE: ", const char *requestPrefix="")
Create RequestSource and ResponseSink pair for serial I/O.
Custom Serial Adapters
For non-fl:: serial sources (e.g., Arduino Serial on specific platforms):
struct ArduinoSerialIn {
int available()
const {
return Serial.available(); }
int read() {
return Serial.read(); }
};
struct ArduinoSerialOut {
};
auto source = []{
ArduinoSerialIn serial;
return fl::parseJsonRpcRequest(line.value());
};
ArduinoSerialOut serial;
auto formatted = fl::formatJsonRpcResponse(response, "REMOTE: ");
};
Serial println(server.last_error().c_str())
fl::optional< fl::string > readSerialLine(SerialIn &serial, char delimiter='\n', fl::optional< u32 > timeoutMs=fl::nullopt)
Read a line from a serial-like input source (blocking with optional timeout)
void writeSerialLine(SerialOut &serial, const fl::string &str)
Write a string with newline to a serial-like output.
constexpr nullopt_t nullopt
Pure Functions (Unit Testable)
formatJsonResponse(response, prefix)
Generic JSON serialization to single-line string with optional prefix:
response.set("success", true);
static json object() FL_NOEXCEPT
fl::string formatJsonResponse(const fl::json &response, const char *prefix)
Serialize JSON response to a string.
Note: This is a generic JSON function, not JSON-RPC specific. Works with any JSON object.
JSON-RPC Protocol Functions
The JSON-RPC specific functions have moved to fl/remote/rpc/protocol.h:
normalizeJsonRpcRequest(json) (in rpc/protocol.h)
Transforms old JSON-RPC format to standard 2.0:
- Old:
{"function": "test", "args": [...]}
- New:
{"method": "test", "params": [...]}
#include "fl/remote/rpc/protocol.h"
fl::json normalized = fl::normalizeJsonRpcRequest(oldFormat);
T value() const FL_NOEXCEPT
static json parse(const fl::string &txt) FL_NOEXCEPT
filterSchemaResponse(response) (in rpc/protocol.h)
Filters schema responses to prevent stack overflow on constrained platforms:
#include "fl/remote/rpc/protocol.h"
fl::json response = getRpcDiscoverResponse();
fl::json filtered = fl::filterSchemaResponse(response);
Use case: ESP32-C6, ESP8266, and other platforms with limited stack can overflow when serializing large JSON-RPC schema responses.
Template Functions (Generic I/O)
readSerialLine<SerialIn>(serial, delimiter)
Reads line from any serial-like object with available() and read():
MockSerialIn mock({"hello", "world"});
writeSerialLine<SerialOut>(serial, str)
Writes line to any serial-like object with println():
Testing
#include <gtest/gtest.h>
TEST(Transport, ParseRequest) {
fl::string input = R
"({"method": "status", "params": []})";
auto request = fl::parseJsonRpcRequest(input);
ASSERT_TRUE(request.has_value());
EXPECT_EQ(request->get("method").as_string().value(), "status");
}
TEST(Transport, FormatResponse) {
response.set("result", "ok");
auto formatted = fl::transport::formatJsonRpcResponse(response, "RPC: ");
EXPECT_TRUE(formatted.find("RPC: ") == 0);
}
TEST(Transport, MockSerial) {
struct MockSerial {
int available()
const {
return buffer.
size() -
pos; }
int read() {
return pos < buffer.
size() ? buffer[
pos++] : -1; }
};
MockSerial mock;
EXPECT_EQ(line.value(), "test");
EXPECT_EQ(mock.output, "response\n");
}
fl::size size() const FL_NOEXCEPT
static constexpr fl::size npos
Adding New Transports
To add a new transport (e.g., WebSocket):
- Create header/implementation in
src/fl/net/ (see src/fl/net/http/ for reference)
- Implement factory functions:
}
Base definition for an LED controller.
- Reuse parsing functions:
parseJsonRpcRequest, formatJsonRpcResponse
- Add to
_build.cpp.hpp
Platform Support
The serial transport uses fl::available(), fl::read(), and fl::println() which work on:
- ✅ AVR (Arduino Uno, Mega, etc.)
- ✅ ESP32 (all variants)
- ✅ ESP8266
- ✅ STM32 (all families)
- ✅ Teensy (3.x, 4.x)
- ✅ SAMD21/SAMD51
- ✅ RP2040
- ✅ Host (native builds)
- ✅ WASM (browser)
Files
- serial.h - Public API (factory functions, templates)
- serial.cpp.hpp - Implementation (pure parsing functions)
- **_build.hpp** - Build integration
- README.md - This file
See Also