Routing detailed design

Each routing message uses an 8-byte header followed by its payload:

  • 4 bytes: message ID (big endian)

  • 4 bytes: payload length in bytes (big endian)

  • N bytes: payload

RxAdapter or PduTransportRxAdapter extracts PDU(s) from a payload according to its RxAdapterTable and implements the IReader interface. The RxAdapterTable determines which PDUs a payload contains with the message ID and the payload length. For each PDU, it defines a length and an offset in bytes, i.e., the position of the PDU in the payload.

// The sizes below are expressed as a function of:
//  - N: the number of known PDUs (>= P)
//  - P: the number of known messages
struct RxAdapterTable
{
    // the first internal PDU ID of this channel. All other PDU IDs are derived from it.
    ::etl::be_uint32_t firstId = {};
    // The message IDs that can be received.
    // The index is called messageIndex. It is shared with messageLengths and pduLengthsOffsets.
    // size: P
    ::etl::span<::etl::be_uint32_t const> messageIds;
    // The lengths of the messages that can be received. If the length of a message
    // does not match the expected one, no PDU is extracted. If empty, it is ignored.
    // size: P
    ::etl::span<::etl::be_uint32_t const> messageLengths;
    // A pointer into the PDULengths and PDUOffsets tables.
    // size: P + 1
    ::etl::span<::etl::be_uint32_t const> pduLengthsOffsets;
    // The length of each PDU in each frame.
    // The index is called channelSpecificPduId. It is shared with pduOffsets and
    // used to derive the internal PDU ID by adding it to firstId.
    // size: N
    ::etl::span<::etl::be_uint32_t const> pduLengths;
    // The offset in each frame of each PDU.
    // size: N
    ::etl::span<::etl::be_uint32_t const> pduOffsets;
};

Router reads PDUs from channels using IReader interfaces and writes them to channels using IWriter interfaces. Its PduRoutingTable specifies to which channel(s) a PDU should be forwarded as well as the outgoing message ID.

/**
 * The input of the routing is the output of the adapters, i.e. Internal PDU IDs.
 *
 * The routing table consists of two parts:
 *
 * 1) Routing to destinations.
 *    The internal PDU ID is used as index into destinationOffsets. The range
 *    destinations[destinationOffsets[pduId], destinationOffsets[pduId + 1][ are the
 *    destination channel indices to which the PDU is routed.
 *
 * 2) Translation from internal PDU ID to destination channel specific addressing.
 *    The index of a routing in the destinations table is also used in the outputMessageIds table.
 *    This contains the message ID to output on the channel for this message.
 *
 * The sizes of the spans are expressed according to:
 *  - N: the number of known PDUs
 *  - M: the number of channels
 */
struct PduRoutingTable
{
    // size: [N,(M*N)]
    ::etl::span<uint8_t const> destinations;

    // size: N+1
    ::etl::span<::etl::be_uint32_t const> destinationOffsets;

    // size: [N, (M*N)]
    ::etl::span<::etl::be_uint32_t const> outputMessageIds;
};

LegacyTxAdapter or PduTransportTxAdapter creates outgoing messages out of PDUs according to its TxAdapterTable and implements the IWriter interface. For each message, the TxAdapterTable determines the payload length and the PDU offset within it.

// The sizes below are expresed as a function of:
//  - N: the number of known PDUs
struct TxAdapterTable
{
    // The message IDs which can be sent by this adapter
    // size: N
    ::etl::span<::etl::be_uint32_t const> messageIds;
    // The length of each message to be sent, achieved using padding with 0xFF.
    // size: N
    ::etl::span<::etl::be_uint32_t const> messageLengths;
    // The offset of the PDU to be sent in the message.
    // size: N
    ::etl::span<::etl::be_uint32_t const> pduOffsets;
};

Routing example

The diagram below shows a concrete routing path through the tables. A message arriving on PDU_TRANSPORT0 with ID 18 is routed to CAN0 (outgoing ID 1), which in turn routes to PDU_TRANSPORT1 (outgoing ID 4096), which loops back to PDU_TRANSPORT0 (outgoing ID 256). This illustrates how the routing module chains multi-hop routes across CAN and PDU transport channels using only ID-based table lookups.

The diagram is generated with:

cd tools
python -m blob.routing visualize --channel PDU_TRANSPORT0 --id 18 \
    libs/bsw/routing/test/routing.jsonl
digraph Routing {
  graph[fontname="Arial" fontsize="10" ranksep=1.0 nodesep=1.0 label="routing graph for input of Id: 18 on Channel: PDU_TRANSPORT0"]
    "Input" -> "PDU_TRANSPORT0" [label = "Id: 18"]
    "PDU_TRANSPORT0" -> "CAN0" [label = "Id: 1"]
    "CAN0" -> "PDU_TRANSPORT1" [label = "Id: 4096"]
    "PDU_TRANSPORT1" -> "PDU_TRANSPORT0" [label = "Id: 256"]
}

System context

The building-block diagram in the module overview shows the internal components. From an architectural point of view it is equally important to see the module in its system context — the external actors it depends on and the boundaries it must not cross:

left to right direction

[Configuration blob\n(in flash)] as blob
[CAN / FlexRay\ndrivers] as bus
[Network stack\n(UDP sockets)] as net
[Periodic scheduler\n(async context)] as sched
[Error handler\n(integrator callback)] as err

package "routing" {
  [Integration] as integ
}

blob --> integ : parsed tables (read-only)
bus <--> integ : PDUs via SPSC MemoryQueue\n(driver context)
net <--> integ : datagrams\n(network context)
sched --> integ : route()/send/timeout\n(periodic context)
integ --> err : status codes

This context view makes the two critical boundaries explicit: the flash boundary (the blob is read-only and never copied) and the context boundary between the bus driver context and the routing/network context, which is crossed only through single-producer/single-consumer queues.

Error handling and reliability

The module is fail-operational at the PDU granularity: a malformed or undeliverable PDU is dropped and reported, but never corrupts the pipeline or blocks other PDUs. Errors are surfaced through the integrator-supplied ErrorHandler callback rather than through exceptions or return codes on the hot path. The reported status codes are:

    enum class StatusCode : uint8_t
    {
        OK,
        // There is not enough data to parse the header.
        INVALID_MESSAGE_HEADER_SIZE,
        // The message ID isn't found in the RX/TX adapter table.
        UNKNOWN_MESSAGE_ID,
        // The parsed length of the message is larger than the length of data available.
        INVALID_PARSED_LENGTH,
        // The parsed length of the incoming message doesn't match the configured one (for messages
        // which have a configured messaged length).
        INVALID_MESSAGE_LENGTH,
        // The size of every PDU expected for the parsed ID is larger than the length of data
        // available. Only if at least a PDU is expected for this ID.
        INVALID_PAYLOAD_LENGTH,
        // There is not enough free space in the queue to write the outgoing PDU.
        MEM_ALLOCATION_FAILURE,
        // A PDU transport channel received data from an invalid source.
        INVALID_REMOTE_IP_ADDRESS,
    };

Key reliability properties:

  • Bounded, non-blocking degradation. When an RX allocation fails (queue full), the incoming datagram/frame is discarded and counted; the error is reported only on the first failure of a run to avoid log flooding. Statistics counters (failedMemAllocPdus, invalidIpAddressPdus, timeoutSends, flushes, …) are exposed for health monitoring.

  • Source filtering. UDP receivers optionally validate the source IP against a configured allow-list before accepting a datagram.

  • No silent blocking on TX. The legacy CAN TX path performs no retry; a failed bus write is dropped rather than stalling the router.

  • Configuration integrity is a precondition supplied by the blob module when the integrator passes a span returned by blob::load(). The routing loaders then perform their own table-structure checks before adding channels and routing entries.