Skip to content

CommsChannels

Rob Dobson edited this page Oct 25, 2026 · 2 revisions

Comms Channels

Comms Channels are Raft's transport-agnostic message pipes. Each channel binds an interface (BLE, serial, sockets, WebSocket, …) to a protocol codec (RICSerial, RICFrame, RICJSON, …) and provides framed, queued, flow-controlled message delivery between the device and a peer.

For the larger picture see Communications Stack Overview.

Roles

Class Role
CommsCoreIF Public interface used by SysMods to register channels, register protocols, and send/receive messages.
CommsChannelManager The concrete implementation of CommsCoreIF. Itself a SysMod — created by SysManager early in startup so other SysMods can register channels in their addCommsChannels() override.
CommsChannel One bidirectional channel: name, interface, protocol codec, inbound queue, outbound queue, settings.
ProtocolBase Base class for protocol codecs. Implementations: ProtocolRICSerial, ProtocolRICFrame, ProtocolRICJSON, ProtocolROSSerial.
CommsChannelMsg Decoded message: channelID, protocol code, msg number, type code, payload bytes.
CommsChannelSettings Per-channel block sizes and queue depths.
CommsChannelBridge Forwards messages between two channels.

Channel lifecycle

  1. CommsChannelManager is constructed as a SysMod (typically by RaftCoreApp).
  2. Protocol codecs are added by SysMods in their addCommsChannels(CommsCoreIF&) override. The standard set (RICSerial, RICFrame, RICJSON) is added by ProtocolExchange.
  3. Transport SysMods register channels also from addCommsChannels(). Each call to registerChannel() returns a channelID.
  4. At runtime the manager allocates the actual codec object lazily the first time a channel needs to encode or decode.
  5. Inbound: the transport calls inboundHandleMsg(channelID, bytes, len) whenever it has raw bytes; the manager queues them and the channel's codec deframes them on its next service.
  6. Outbound: anything in the system calls outboundHandleMsg(CommsChannelMsg&); the manager finds the channel by ID, encodes via its codec and calls the channel's outboundHandleMsgCB. See Message queues for when that happens.

CommsCoreIF API

The full interface lives in CommsCoreIF.h. The methods most useful from a SysMod are:

uint32_t registerChannel(const char* protocolName,
                         const char* interfaceName,
                         const char* channelName,
                         CommsChannelOutboundHandleMsgFnType outboundHandleMsgCB,
                         CommsChannelOutboundCanAcceptFnType outboundCanAcceptCB,
                         const CommsChannelSettings* pSettings = nullptr);

void addProtocol(ProtocolCodecFactoryHelper& protocolDef);

bool inboundCanAccept(uint32_t channelID);
void inboundHandleMsg(uint32_t channelID, const uint8_t* pMsg, uint32_t msgLen);
void inboundHandleMsgVec(uint32_t channelID, const SpiramAwareUint8Vector& msg);

bool outboundCanAccept(uint32_t channelID, CommsMsgTypeCode msgType, bool& noConn);
CommsCoreRetCode outboundHandleMsg(CommsChannelMsg& msg);

int32_t getChannelIDByName(const String& channelName, const String& protocolName);

There are also bridge methods (bridgeRegister, bridgeUnregister, bridgeHandleInboundMsg, bridgeHandleOutboundMsg) — see Bridges below.

Special channel IDs

Constant Value Meaning
CommsCoreIF::CHANNEL_ID_UNDEFINED 0xffff Unset/invalid
CommsCoreIF::CHANNEL_ID_REST_API 0xfffe Used by HTTP requests (they don't have a real channel)
MSG_CHANNEL_ID_ALL 10000 Broadcast — all channels

Registering a channel — example

This is a minimal transport SysMod that exposes one channel:

class MyTransport : public RaftSysMod
{
public:
    MyTransport(const char* pModuleName, RaftJsonIF& sysConfig)
        : RaftSysMod(pModuleName, sysConfig) {}

protected:
    virtual void addCommsChannels(CommsCoreIF& commsCore) override final
    {
        // Choose a protocol that matches the wire format of this transport.
        // RICSerial = HDLC-framed binary; RICFrame = length-prefixed; RICJSON = pure JSON.
        static const CommsChannelSettings settings(
            /*inboundBlockLen   */ 1200,
            /*inboundBlockMax   */ 5000,
            /*inboundQueueLen   */ 0,    // 0 = default
            /*inboundQueueBytes */ 0,
            /*outboundBlockLen  */ 5000,
            /*outboundQueueLen  */ 0);

        _channelID = commsCore.registerChannel(
            /*protocolName  */ "RICSerial",
            /*interfaceName */ "MyIF",
            /*channelName   */ "MyIF",
            /*outboundCB    */ std::bind(&MyTransport::sendBytes, this, std::placeholders::_1),
            /*canAcceptCB   */ std::bind(&MyTransport::canSend, this,
                                         std::placeholders::_1, std::placeholders::_2, std::placeholders::_3),
            &settings);

        _pCommsCore = &commsCore;
    }

    // Called by the transport when bytes arrive
    void onRxBytes(const uint8_t* p, uint32_t len)
    {
        if (_pCommsCore)
            _pCommsCore->inboundHandleMsg(_channelID, p, len);
    }

    // Called by CommsChannelManager when it has bytes to send out
    bool sendBytes(CommsChannelMsg& msg)
    {
        // Push msg.getBuf()/msg.getBufLen() out of your transport
        return true;
    }

    bool canSend(uint32_t channelID, CommsMsgTypeCode msgType, bool& noConn)
    {
        noConn = !_isConnected;
        return _isConnected && _txReady;
    }

private:
    uint32_t _channelID = CommsCoreIF::CHANNEL_ID_UNDEFINED;
    CommsCoreIF* _pCommsCore = nullptr;
    bool _isConnected = false;
    bool _txReady = true;
};

Real transports in the framework: BLEManager (registers a RICSerial channel over a GATT characteristic), SerialConsole (a channel sharing the console port with log text - see SerialConsole), CommandSerial (one RICSerial channel per UART), CommandSocket (TCP server, one channel per connection), and the WebServer's WebSockets (one channel per connection slot).

CommsChannelSettings

Defaults are defined in CommsChannelSettings.h:

Field Default Meaning
inboundBlockLen 1200 Expected per-message inbound block size in bytes. It is what inboundMsgBlockMax() returns, and RICREST file uploads on the channel use blocks of at most 2/3 of it, so 800 bytes by default
inboundBlockLenMax 5000 Maximum inbound block size
inboundQueueCountMax 20 Max number of queued inbound messages (one arriving when the queue is full is dropped and a warning logged)
inboundQueueBytesMax 20000 Not currently enforced
outboundBlockLen 5000 Per-message outbound block size
outboundQueueMaxLen 20 Max queued outbound messages

A value of 0 means the default. Pass a custom CommsChannelSettings to registerChannel() to tune per-channel queues and block sizes. For example, BLE uses its maxPktLen (500 by default) for the block sizes, so its uploads use blocks of up to 333 bytes, and a WebServer WebSocket on a board with PSRAM uses an inboundBlockLen of 24000 (blocks of up to 16000 bytes).

Message queues

CommsChannelManager::loop() runs on the main task (the task running SysManager::loop()). On each pass, for each channel:

  • Outbound: it reads the type of the next queued message without copying it (outboundQueuePeekType(), which uses ThreadSafeQueue::peekWith()) and asks the channel's outboundCanAcceptCB whether that type can be sent. If it can, the message is moved out of the queue, encoded by the codec and passed to outboundHandleMsgCB. If the callback reports no connection (noConn), the message is taken and discarded so the queue doesn't block. At most one outbound message per channel is sent per pass.
  • Inbound: one queued block of received bytes is passed to the codec, if the codec is ready for more (readyForRxData()).

Commands, responses and reports go through the outbound queue (outboundQueueMaxLen). Publish messages are not queued: outboundHandleMsg() sends a publish at once if the channel can accept it and drops it otherwise, and skips it if any other message is waiting in the channel's queue (unless IMPLEMENT_PUBLISH_EVEN_IF_OUTBOUND_QUEUE_NOT_EMPTY is defined). Command responses therefore aren't held up behind publishes.

Codecs are only created on the main task, and some transports' send paths (e.g. WebSockets) are main-task only, so code on another task that sends on a channel should hand the message to the main task first. Queue elements are held in PSRAM where available, and messages are moved through the queues rather than copied - see ThreadSafeQueue.

CommsChannelMsg

The decoded message container. Important fields/methods:

uint32_t          getChannelID();
CommsMsgProtocol  getProtocol();      // RICSerial, RICREST, BridgeRICREST, RawCmdFrame, ...
uint32_t          getMsgNumber();
CommsMsgTypeCode  getMsgTypeCode();   // COMMAND, RESPONSE, PUBLISH, REPORT
const uint8_t*    getBuf();
uint32_t          getBufLen();

void setAsResponse(const CommsChannelMsg& original);   // copy channelID + msgNum
void setFromBuffer(uint32_t channelID, CommsMsgProtocol protocol,
                   uint32_t msgNum, CommsMsgTypeCode type,
                   const uint8_t* p, uint32_t len);

CommsMsgProtocol values: MSG_PROTOCOL_ROSSERIAL, MSG_PROTOCOL_RICREST, MSG_PROTOCOL_BRIDGE_RICREST, MSG_PROTOCOL_RAWCMDFRAME, MSG_PROTOCOL_NONE.

Protocol codecs

A protocol codec deframes incoming bytes and frames outgoing messages. Codecs are added to the manager by passing a ProtocolCodecFactoryHelper to addProtocol():

ProtocolCodecFactoryHelper helper = {
    ProtocolRICSerial::getProtocolNameStatic(),    // "RICSerial"
    ProtocolRICSerial::createInstance,             // factory fn
    configGetConfig(),                             // config source
    "RICSerial",                                   // config prefix
    inboundFrameRxCB,                              // called with each decoded CommsChannelMsg
    canAcceptInboundCB                             // flow-control predicate
};
commsCore.addProtocol(helper);

ProtocolExchange registers the standard codec set (RICSerial, RICFrame, RICJSON) on behalf of the framework. Most application code does not need to add additional codecs.

Codec Wire framing Typical use
ProtocolRICSerial HDLC (MiniHDLC), with byte-stuffed FLAGs and CRC UART, BLE GATT, WebSocket (the default pcol)
ProtocolRICFrame Two-byte header (msgNum, protocol/type) + payload TCP/sockets, where stream framing is provided by the transport
ProtocolRICJSON Plain JSON, no overhead JSON-only links
ProtocolROSSerial ROS serial format Legacy ROS bridge

See RICREST Protocol for the inner-message format that all of these typically carry.

Bridges

A bridge makes two channels exchange messages transparently. Typical use: an embedded peripheral on a serial port is reachable from a BLE-connected host without the host having to know how to talk to that peripheral.

uint32_t bridgeID = commsCore.bridgeRegister(
    "myBridge",
    establishmentChannelID,    // the side that opened the bridge (e.g. BLE)
    otherChannelID,            // the side being bridged to (e.g. a serial peripheral)
    /*idleCloseSecs*/ 30);
// ...
commsCore.bridgeUnregister(bridgeID, /*forceClose*/ false);

While the bridge is open, RICREST messages can be wrapped in MSG_PROTOCOL_BRIDGE_RICREST envelopes (see CommsBridgeMsg); ProtocolExchange recognises these and forwards them via bridgeHandleInboundMsg() / bridgeHandleOutboundMsg().

Diagnostics

CommsChannelManager exposes getInfoJSON() which returns a JSON list describing every registered channel — its ID, name, interface, protocol, and queue stats. This is useful from a SysMod's getDebugJSON() override.

Clone this wiki locally