Dispatcher
The Dispatcher routes framed messages from one byte stream to per-module
handlers, letting several independent protocols share a single USB vendor / CDC
/ socket / UART link. Rather than run a separate StreamParser per protocol
over the same bytes (each re-buffering the whole stream and needing its own
reset-on-overflow bookkeeping), a Dispatcher parses the
Stream Frame APIs stream once and hands each complete frame to the
handler registered for its module id.
Module id
The frame’s module byte (0..255) is the routing key — a full byte, so up to
256 protocols can coexist on one stream. The message/transaction type and
the request/reply direction (flags) travel with the frame and are handed to
the module’s handler untouched; the Dispatcher does not interpret them. espp
built-in protocols use, for example:
Module id |
Protocol |
|---|---|
0 |
OTA |
4 |
crash dump |
5 |
CAN bridge |
A device-side dispatcher registers the modules it serves; frames for an
unregistered module are silently ignored. A protocol’s replies use the same
module as its requests (the reply/direction lives in the frame’s flags, not
the module), so both directions route to the one registered handler — use
frame.is_reply() to tell them apart. In practice a device only receives
requests (it sends the replies), so its handler normally sees requests only.
Application code may assign any unused module id to its own protocol; nothing is
hard-wired to a specific service.
The example multiplexes two toy protocols over one in-memory byte stream (the
same feed() / register_module() pattern applies to a USB vendor / CDC /
socket / UART receive path):
namespace sf = espp::stream_frame;
// Two toy protocols sharing one stream: "control" on module 0 and "telemetry"
// on module 4 (module is a full byte, so up to 256 protocols can coexist).
static constexpr uint8_t kModuleControl = 0;
static constexpr uint8_t kModuleTelemetry = 4;
espp::Dispatcher dispatcher;
dispatcher.register_module(kModuleControl, [&](const sf::Frame &f) {
logger.info("[control] {} type=0x{:02X} ({} payload bytes)", f.is_reply() ? "reply" : "request",
f.type, f.payload.size());
});
dispatcher.register_module(kModuleTelemetry, [&](const sf::Frame &f) {
uint32_t value = f.payload.size() == 4 ? sf::get_u32(f.payload) : 0;
logger.info("[telemetry] type=0x{:02X} value={}", f.type, value);
});
// Build a mixed stream, as a peer would send it.
std::vector<uint8_t> telemetry_payload;
sf::put_u32(telemetry_payload, 42);
std::vector<uint8_t> stream;
auto append = [&](const std::vector<uint8_t> &frame) {
stream.insert(stream.end(), frame.begin(), frame.end());
};
const uint8_t start[] = {'s', 't', 'a', 'r', 't'};
append(sf::build_frame(/*reply=*/false, kModuleControl, 0x01, start));
append(sf::build_frame(/*reply=*/false, kModuleTelemetry, 0x01, telemetry_payload));
// A frame for an unregistered module is silently ignored.
append(sf::build_frame(/*reply=*/false, 9, 0x00));
append(sf::build_frame(/*reply=*/true, kModuleControl, 0x81)); // a reply/event
// Feed it in two arbitrary chunks to show the parser reassembles split frames.
const size_t half = stream.size() / 2;
dispatcher.feed(std::span<const uint8_t>(stream.data(), half));
dispatcher.feed(std::span<const uint8_t>(stream.data() + half, stream.size() - half));
logger.info("done ({} bytes dropped while resyncing)", dispatcher.dropped_bytes());
The codec and dispatcher are also exposed to Python (espp.stream_frame and
espp.Dispatcher); see python/dispatcher.py and python/dispatcher_test.py.
API Reference
Header File
Classes
-
class Dispatcher
Routes framed messages from one byte stream to per-module handlers.
Public Types
-
using handler_fn = std::function<void(const stream_frame::Frame &frame)>
Handler invoked for every frame whose module was registered.
- Param frame:
The decoded frame (module, type, flags/reply, payload).
Public Functions
-
inline void register_module(uint8_t module_id, handler_fn handler)
Register (or replace) the handler for a module id.
- Parameters:
module_id – Module id (0..255) to route to
handler.handler – Callback for frames with this module id. A null handler unregisters the module.
-
inline void unregister_module(uint8_t module_id)
Remove the handler for a module id (frames for it become ignored).
-
inline bool has_module(uint8_t module_id) const
Whether a handler is registered for a module id (reflects applied registrations; changes made during a dispatch apply after it ends).
-
inline void feed(std::span<const uint8_t> data)
Feed raw received bytes: parse and route each complete frame to its module’s handler. Frames whose module has no handler are ignored (so unrelated protocols on the same stream are harmless).
-
inline void dispatch(const stream_frame::Frame &frame)
Route an already-parsed frame (for callers running their own parser).
-
inline void reset()
Discard any partially-buffered frame bytes (transport reconnect or RX overflow) so a frame straddling the gap resynchronizes at once.
-
inline size_t buffered() const
Bytes buffered awaiting frame completion.
-
inline size_t dropped_bytes() const
Total bytes discarded while resynchronizing (diagnostics).
Public Static Functions
-
static inline constexpr uint8_t module_of(const stream_frame::Frame &frame)
The module id a frame will route to (its `module` byte).
-
using handler_fn = std::function<void(const stream_frame::Frame &frame)>