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