src/fl/remote/transport/README.md
Cross-platform transport implementations for fl::Remote JSON-RPC communication.
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:
┌─────────────────────────────────────────┐
│ 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 │
└────────────────┘ └────────────────┘
#include "fl/remote/transport/serial.h"
#include "fl/remote/remote.h"
// Create transport callbacks
auto source = fl::createSerialRequestSource();
auto sink = fl::createSerialResponseSink("REMOTE: ");
// Create Remote with serial transport
fl::Remote remote(source, sink);
// Register RPC methods
remote.bind("ping", []() {
return "pong";
});
// Main loop
void loop() {
remote.update(millis()); // Pull + process + push
}
createSerialRequestSource(prefix = "")Creates a RequestSource callback that:
fl::available() / fl::read()optional<Json> (nullopt if no data)auto source = fl::createSerialRequestSource();
createSerialResponseSink(prefix = "REMOTE: ")Creates a ResponseSink callback that:
fl::println()auto sink = fl::createSerialResponseSink("RESULT: ");
createSerialTransport(responsePrefix, requestPrefix)Creates both callbacks in one call:
auto [source, sink] = fl::createSerialTransport("REMOTE: ");
fl::Remote remote(source, sink);
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 {
void println(const char* str) { Serial.println(str); }
};
// Use template functions directly
auto source = []{
ArduinoSerialIn serial;
auto line = fl::readSerialLine(serial);
if (!line.has_value()) return fl::nullopt;
return fl::parseJsonRpcRequest(line.value());
};
auto sink = [](const fl::json& response) {
ArduinoSerialOut serial;
auto formatted = fl::formatJsonRpcResponse(response, "REMOTE: ");
fl::writeSerialLine(serial, formatted);
};
fl::Remote remote(source, sink);
formatJsonResponse(response, prefix)Generic JSON serialization to single-line string with optional prefix:
fl::json response = fl::json::object();
response.set("success", true);
auto formatted = fl::formatJsonResponse(response, "REMOTE: ");
// formatted = "REMOTE: {\"success\":true}"
Note: This is a generic JSON function, not JSON-RPC specific. Works with any JSON object.
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:
{"function": "test", "args": [...]}{"method": "test", "params": [...]}#include "fl/remote/rpc/protocol.h"
fl::json oldFormat = fl::json::parse(R"({"function": "ping", "args": []})").value();
fl::json normalized = fl::normalizeJsonRpcRequest(oldFormat);
// normalized = {"method": "ping", "params": []}
filterSchemaResponse(response) (in rpc/protocol.h)Filters schema responses to prevent stack overflow on constrained platforms:
#include "fl/remote/rpc/protocol.h"
// Detects large schema responses (rpc.discover) and replaces with minimal message
fl::json response = getRpcDiscoverResponse();
fl::json filtered = fl::filterSchemaResponse(response);
// If response contains "schema", "openrpc", or "methods" keys,
// returns minimal response: {"jsonrpc": "2.0", "id": ..., "result": {"message": "...", "methodCount": N}}
// Otherwise returns original response unchanged
Use case: ESP32-C6, ESP8266, and other platforms with limited stack can overflow when serializing large JSON-RPC schema responses.
readSerialLine<SerialIn>(serial, delimiter)Reads line from any serial-like object with available() and read():
MockSerialIn mock({"hello", "world"});
auto line = fl::readSerialLine(mock);
// line = "hello"
writeSerialLine<SerialOut>(serial, str)Writes line to any serial-like object with println():
MockSerialOut mock;
fl::writeSerialLine(mock, "test");
// mock output = "test\n"
#include "fl/remote/transport/serial.h"
#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) {
fl::json response = fl::json::object();
response.set("result", "ok");
auto formatted = fl::transport::formatJsonRpcResponse(response, "RPC: ");
EXPECT_TRUE(formatted.find("RPC: ") == 0);
EXPECT_TRUE(formatted.find("\"result\":\"ok\"") != fl::string::npos);
}
TEST(Transport, MockSerial) {
struct MockSerial {
fl::string buffer = "test\n";
size_t pos = 0;
int available() const { return buffer.size() - pos; }
int read() { return pos < buffer.size() ? buffer[pos++] : -1; }
void println(const char* s) { output += s; output += '\n'; }
fl::string output;
};
MockSerial mock;
auto line = fl::readSerialLine(mock);
EXPECT_EQ(line.value(), "test");
fl::writeSerialLine(mock, "response");
EXPECT_EQ(mock.output, "response\n");
}
To add a new transport (e.g., WebSocket):
src/fl/net/ (see src/fl/net/http/ for reference)namespace fl {
fl::function<fl::optional<fl::json>()> createWsRequestSource(const char* url);
fl::function<void(const fl::json&)> createWsResponseSink(const char* url);
}
parseJsonRpcRequest, formatJsonRpcResponse_build.cpp.hppThe serial transport uses fl::available(), fl::read(), and fl::println() which work on:
fl/remote/remote.h - JSON-RPC serverfl/remote/rpc/server.h - RequestSource and ResponseSink typesexamples/Validation/ValidationRemote.cpp - Usage example