docs/remote_stream_investigation.md
Manual I/O boilerplate - Users must manually:
No unified stream handling - Every example reimplements the same pattern:
if (Serial.available()) {
fl::string jsonRpc = readSerialJson();
// 40+ lines of manual parsing/response handling
}
remote.tick(millis());
auto results = remote.getResults();
for (const auto& r : results) {
fl::Remote::printJson(r.to_json());
}
Inconsistent output - Immediate responses vs scheduled results handled differently
// Remote.ino example (113-183)
void loop() {
remote.tick(millis()); // 1. Process scheduled calls
auto results = remote.getResults(); // 2. Get scheduled results
for (const auto& r : results) {
fl::Remote::printJson(r.to_json()); // 3. Print scheduled results
}
if (Serial.available()) { // 4. Read input
fl::string jsonRpc = readSerialJson();
fl::json doc = fl::json::parse(jsonRpc);
auto response = remote.processRpc(fl::Remote::RpcRequest{...});
fl::json response = fl::json::object(); // 5. Build response
if (response.ok()) {
response.set("status", "ok");
// ... 40 lines of response building
}
fl::Remote::printJson(response); // 6. Print immediate response
}
}
// Setup
remote.setStream(&Serial);
// Loop
remote.update(millis()); // Does everything: tick + pull + push
Pros:
Cons:
// Setup (optional - can also use manual processRpc)
remote.attachStream(&Serial);
// Loop
remote.pull(); // Read available input, process RPCs, queue responses
remote.tick(millis()); // Process scheduled calls
remote.push(); // Write queued responses and scheduled results
Pros:
Cons:
remote.onRequest([](Stream& stream) {
if (stream.available()) {
return stream.readStringUntil('\n');
}
return fl::string();
});
remote.onResponse([](const fl::json& response) {
Serial.println(response.to_string());
});
Pros:
Cons:
// Abstract stream interface (compatible with Arduino Stream)
class RpcStream {
public:
virtual ~RpcStream() = default;
virtual int available() = 0;
virtual int read() = 0;
virtual size_t write(const uint8_t* buffer, size_t size) = 0;
virtual size_t print(const char* str) = 0;
virtual size_t println(const char* str) = 0;
};
// Adapter for Arduino Serial/Stream
template<typename TStream>
class ArduinoStreamAdapter : public RpcStream {
TStream* mStream;
public:
ArduinoStreamAdapter(TStream* stream) : mStream(stream) {}
int available() override { return mStream->available(); }
int read() override { return mStream->read(); }
size_t write(const uint8_t* buffer, size_t size) override {
return mStream->write(buffer, size);
}
size_t print(const char* str) override { return mStream->print(str); }
size_t println(const char* str) override { return mStream->println(str); }
};
class Remote {
public:
// Stream attachment (optional - maintains backward compatibility)
void attachStream(RpcStream* stream);
void detachStream();
bool hasStream() const;
// Stream-based I/O (only work if stream attached)
size_t pull(); // Read available input, process RPCs, queue responses
size_t push(); // Write queued responses and scheduled results
// Convenience method (combines pull + tick + push)
size_t update(u32 currentTimeMs);
private:
RpcStream* mStream = nullptr;
fl::vector<fl::json> mOutgoingQueue; // Queued responses to send
fl::string mInputBuffer; // Partial line buffer for reading
};
size_t Remote::pull() {
if (!mStream) return 0;
size_t processed = 0;
// Read available data into buffer
while (mStream->available()) {
int c = mStream->read();
if (c < 0) break;
if (c == '\n' || c == '\r') {
if (!mInputBuffer.empty()) {
// Process complete line
fl::json doc = fl::json::parse(mInputBuffer);
auto response = processRpc(fl::Remote::RpcRequest{
doc["function"] | fl::string(""),
doc["args"],
static_cast<u32>(doc["timestamp"] | 0)
});
// Build response JSON and queue it
fl::json responseJson = buildResponseJson(response);
mOutgoingQueue.push_back(responseJson);
mInputBuffer.clear();
processed++;
}
} else {
mInputBuffer += static_cast<char>(c);
}
}
return processed;
}
size_t Remote::push() {
if (!mStream) return 0;
size_t written = 0;
// Write queued immediate responses
while (!mOutgoingQueue.empty()) {
const fl::json& response = mOutgoingQueue[0];
printJson(response); // Uses existing printJson helper
mOutgoingQueue.erase(mOutgoingQueue.begin());
written++;
}
// Write scheduled results
auto results = getResults();
for (const auto& r : results) {
printJson(r.to_json());
written++;
}
return written;
}
size_t Remote::update(u32 currentTimeMs) {
size_t processed = pull();
size_t executed = tick(currentTimeMs);
size_t written = push();
return processed + executed + written;
}
void setup() {
Serial.begin(115200);
FastLED.addLeds<WS2812, DATA_PIN>(leds, NUM_LEDS);
// Register functions
remote.bind("setLed", [](int i, int r, int g, int b) {
leds[i] = CRGB(r, g, b);
});
// Attach stream (optional - can still use manual processRpc)
remote.attachStream(&Serial);
}
void loop() {
// Option 1: Simple (single call)
remote.update(millis());
// Option 2: Explicit (more control)
// remote.pull(); // Read input, queue responses
// remote.tick(millis()); // Process scheduled calls
// remote.push(); // Write all output
FastLED.show();
delay(10);
}
Instead of abstract RpcStream, use template to avoid virtual calls:
template<typename TStream>
class Remote {
public:
void attachStream(TStream* stream) { mStream = stream; }
size_t pull() {
if (!mStream) return 0;
// Read from mStream directly (no virtual calls)
while (mStream->available()) {
int c = mStream->read();
// ...
}
}
private:
TStream* mStream = nullptr;
};
// Usage
fl::Remote<decltype(Serial)> remote;
remote.attachStream(&Serial);
Pros:
Cons:
Implement Option B with abstract RpcStream:
RpcStream abstract interfaceArduinoStreamAdapter<T> template for Arduino compatibilityattachStream/detachStream/hasStream to Remotepull/push/update methods to RemoteThis provides:
update())pull/tick/push)pull() auto-parse JSON or just read lines and let user control parsing?push() automatically call printJson() or let user control formatting?update() exist or force users to call pull/tick/push explicitly?