Back to Fastled

fl/remote/transport - JSON-RPC Transport Layer

src/fl/remote/transport/README.md

3.10.39.0 KB
Original Source

fl/remote/transport - JSON-RPC Transport Layer

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

cpp
#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
}

Factory Functions

createSerialRequestSource(prefix = "")

Creates a RequestSource callback that:

  1. Reads lines from fl::available() / fl::read()
  2. Strips optional prefix from input
  3. Parses JSON-RPC requests
  4. Returns optional<Json> (nullopt if no data)
cpp
auto source = fl::createSerialRequestSource();

createSerialResponseSink(prefix = "REMOTE: ")

Creates a ResponseSink callback that:

  1. Formats JSON-RPC responses
  2. Prepends optional prefix
  3. Writes to fl::println()
cpp
auto sink = fl::createSerialResponseSink("RESULT: ");

createSerialTransport(responsePrefix, requestPrefix)

Creates both callbacks in one call:

cpp
auto [source, sink] = fl::createSerialTransport("REMOTE: ");
fl::Remote remote(source, sink);

Custom Serial Adapters

For non-fl:: serial sources (e.g., Arduino Serial on specific platforms):

cpp
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);

Pure Functions (Unit Testable)

formatJsonResponse(response, prefix)

Generic JSON serialization to single-line string with optional prefix:

cpp
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.

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": [...]}
cpp
#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:

cpp
#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.

Template Functions (Generic I/O)

readSerialLine<SerialIn>(serial, delimiter)

Reads line from any serial-like object with available() and read():

cpp
MockSerialIn mock({"hello", "world"});
auto line = fl::readSerialLine(mock);
// line = "hello"

writeSerialLine<SerialOut>(serial, str)

Writes line to any serial-like object with println():

cpp
MockSerialOut mock;
fl::writeSerialLine(mock, "test");
// mock output = "test\n"

Testing

cpp
#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");
}

Adding New Transports

To add a new transport (e.g., WebSocket):

  1. Create header/implementation in src/fl/net/ (see src/fl/net/http/ for reference)
  2. Implement factory functions:
    cpp
    namespace fl {
        fl::function<fl::optional<fl::json>()> createWsRequestSource(const char* url);
        fl::function<void(const fl::json&)> createWsResponseSink(const char* url);
    }
    
  3. Reuse parsing functions: parseJsonRpcRequest, formatJsonRpcResponse
  4. 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

  • fl/remote/remote.h - JSON-RPC server
  • fl/remote/rpc/server.h - RequestSource and ResponseSink types
  • examples/Validation/ValidationRemote.cpp - Usage example