Chapter 27 — Adding a New Transport

This chapter explains how to add a new transport layer alongside the existing CAN driver. The architecture cleanly separates transport-independent protocol logic (in openlcb/) from transport-specific code (in drivers/), so a new transport only needs to convert between its wire format and openlcb_msg_t.

27.1 Architecture Boundary

flowchart TB subgraph Core["openlcb/ — Transport-Independent Core"] SM["Main Dispatcher"] Login["Login State Machine"] Proto["Protocol Handlers
(SNIP, Events, Datagrams, ...)"] Buf["Buffer System"] Node["Node Management"] end subgraph CAN["drivers/canbus/ — CAN Transport"] CANSM["CAN State Machine"] CANRX["CAN RX Pipeline"] CANTX["CAN TX Pipeline"] Alias["Alias Mappings"] CANBuf["CAN Buffer/FIFO"] end subgraph TCP["drivers/tcpip/ — TCP/IP Transport (new)"] TCPSM["TCP State Machine"] TCPRX["TCP RX Handler"] TCPTX["TCP TX Handler"] TCPBuf["TCP Buffer"] end CANRX -->|"openlcb_msg_t"| SM SM -->|"openlcb_msg_t"| CANTX SM <--> Proto SM <--> Login TCPRX -->|"openlcb_msg_t"| SM SM -->|"openlcb_msg_t"| TCPTX style Core fill:#e8f5e9,stroke:#2e7d32 style CAN fill:#e3f2fd,stroke:#1565c0 style TCP fill:#fff3e0,stroke:#e65100

The key boundary: transport drivers produce and consume openlcb_msg_t structures. Everything above that boundary is shared code that works identically regardless of the transport.

27.2 What a Transport Must Provide

Every transport driver must implement three fundamental operations:

OperationDescriptionCAN Example
RX: Wire to openlcb_msg_t Convert incoming wire-format data into a populated openlcb_msg_t and push it to the core's incoming FIFO CAN RX pipeline: multi-frame assembly, alias-to-Node-ID resolution
TX: openlcb_msg_t to wire Convert an outgoing openlcb_msg_t into the transport's wire format and transmit CAN TX pipeline: fragmentation into CAN frames, Node-ID-to-alias resolution
Login sequence Perform any transport-specific node registration before the core login state machine takes over CAN login: CID/RID/AMD alias allocation sequence

27.3 Driver Folder Structure

Create a new folder under drivers/ for your transport:

drivers/
    canbus/                     // Existing CAN transport
        can_config.h
        can_config.c
        can_main_statemachine.h
        can_main_statemachine.c
        can_rx_statemachine.h
        can_tx_statemachine.h
        ...
    tcpip/                      // New TCP/IP transport
        tcpip_config.h          // User-facing config struct
        tcpip_config.c          // Internal wiring
        tcpip_main_statemachine.h
        tcpip_main_statemachine.c
        tcpip_rx_handler.h
        tcpip_rx_handler.c
        tcpip_tx_handler.h
        tcpip_tx_handler.c

27.4 CAN vs. TCP/IP Comparison

The CAN transport has significant complexity due to the constrained bus format. A TCP/IP transport is substantially simpler:

ConcernCAN TransportTCP/IP Transport
Node addressing 12-bit CAN aliases mapped to 48-bit Node IDs via LFSR + alias table Full 48-bit Node IDs carried directly in every message
Message fragmentation Required: 8-byte CAN frame limit requires multi-frame datagram/stream assembly Not required: TCP carries arbitrarily large messages intact
Login sequence Complex: CID7/6/5/4, 200ms wait, RID, AMD (per node) Simple: connect, send Initialization Complete
Alias management Required: alias_mappings module, duplicate detection, AME/AMD/AMR Not required: no aliases
Collision detection ISR-level alias collision detection with re-seed and re-login Not applicable
Thread safety model ISR pushes to FIFO, main loop pops (lock/unlock around FIFO operations) Socket read in main loop or dedicated thread (lock/unlock around FIFO)
Buffer pools Separate CAN frame buffer pool (can_msg_t) plus OpenLCB message buffers Only OpenLCB message buffers needed (no intermediate frame buffers)

27.5 Integration with the Core

The transport driver connects to the core through the same interfaces the CAN driver uses:

sequenceDiagram participant HW as Transport Hardware participant RX as Transport RX Handler participant FIFO as OpenLCB Buffer FIFO participant SM as Main Dispatcher participant TX as Transport TX Handler HW->>RX: Raw data received RX->>RX: Convert to openlcb_msg_t RX->>FIFO: Push incoming message Note over FIFO: lock/unlock SM->>FIFO: Pop incoming message SM->>SM: Route to protocol handler SM->>TX: Outgoing openlcb_msg_t TX->>TX: Convert to wire format TX->>HW: Transmit

Key Integration Points

  1. Buffer allocation: The transport allocates openlcb_msg_t from the shared buffer pool (OpenLcbBufferStore_allocate_buffer()) for incoming messages.
  2. FIFO push: Converted messages are pushed to the OpenLCB incoming FIFO using OpenLcbBufferFifo_push().
  3. TX path: The core calls the transport's send function (via function pointer in the interface struct) when it has a message to transmit.
  4. Login coordination: The transport drives the early login states (CAN-specific alias allocation), then hands off to the core login state machine for the OpenLCB-level login (Initialization Complete, event enumeration).

27.6 Configuration Struct Pattern

Follow the same pattern as can_config.h. Create a user-facing config struct with hardware driver function pointers:

// tcpip_config.h

typedef struct {
    // REQUIRED: Send raw TCP data
    bool (*transmit_tcp_message)(uint8_t *data, uint16_t length);

    // REQUIRED: Check if TCP socket is ready to send
    bool (*is_tx_ready)(void);

    // REQUIRED: Lock/unlock (same as openlcb_config_t)
    void (*lock_shared_resources)(void);
    void (*unlock_shared_resources)(void);

    // OPTIONAL: Diagnostic callbacks
    void (*on_rx)(openlcb_msg_t *msg);
    void (*on_tx)(openlcb_msg_t *msg);

} tcpip_config_t;

extern void TcpipConfig_initialize(const tcpip_config_t *config);

27.7 What Changes in the Application

When using a different transport, the application's main loop changes to call the new transport's state machine instead of the CAN state machine:

// CAN application main loop:
while (1) {
    OpenLcb_run();  // Internally calls CanMainStateMachine_run()
                    // then OpenLcb login/main state machines
}

// TCP/IP application main loop:
while (1) {
    TcpipMainStateMachine_run();    // Transport-specific
    OpenLcbLoginMainStatemachine_run();  // Shared
    OpenLcbMainStatemachine_run();       // Shared
}
Shared Protocol Handlers

The protocol handlers (SNIP, Events, Datagrams, Train Control, etc.) are completely transport-independent. They work identically on CAN, TCP/IP, or any future transport. Only the transport driver layer changes.

27.8 Checklist for a New Transport

ItemDescription
Config structCreate *_config.h with hardware driver function pointers
Wiring moduleCreate *_config.c to build internal interface structs
RX handlerConvert wire format to openlcb_msg_t, push to FIFO
TX handlerConvert openlcb_msg_t to wire format, transmit
Login driverHandle transport-specific login steps before handing off to core login SM
State machineCreate main state machine to orchestrate RX/TX/login
Thread safetyEnsure lock/unlock around all shared resource access (buffers, FIFO, node state)
TestsWrite Google Tests with mock hardware interface
← Prev: Ch 26 — Adding a Protocol Next: Ch 28 — Porting to a Platform →