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
(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:
| Operation | Description | CAN 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:
| Concern | CAN Transport | TCP/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:
Key Integration Points
- Buffer allocation: The transport allocates
openlcb_msg_tfrom the shared buffer pool (OpenLcbBufferStore_allocate_buffer()) for incoming messages. - FIFO push: Converted messages are pushed to the OpenLCB incoming FIFO using
OpenLcbBufferFifo_push(). - TX path: The core calls the transport's send function (via function pointer in the interface struct) when it has a message to transmit.
- 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
}
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
| Item | Description |
|---|---|
| Config struct | Create *_config.h with hardware driver function pointers |
| Wiring module | Create *_config.c to build internal interface structs |
| RX handler | Convert wire format to openlcb_msg_t, push to FIFO |
| TX handler | Convert openlcb_msg_t to wire format, transmit |
| Login driver | Handle transport-specific login steps before handing off to core login SM |
| State machine | Create main state machine to orchestrate RX/TX/login |
| Thread safety | Ensure lock/unlock around all shared resource access (buffers, FIFO, node state) |
| Tests | Write Google Tests with mock hardware interface |