Chapter 1 — Project Overview

1.1 What Is OpenLCB / LCC?

OpenLCB (Open Layout Control Bus) is a networking standard for model railroad control systems. Its NMRA-adopted profile is called LCC (Layout Command Control). The protocol defines a message-based communication system where every device on the network is a node identified by a globally unique 48-bit Node ID. Nodes exchange messages identified by a Message Type Indicator (MTI) to implement protocols such as events, datagrams, configuration memory access, train control, and broadcast time.

This library provides a portable, pure-C implementation of the OpenLCB protocol stack. It is designed to run on bare-metal microcontrollers (dsPIC, STM32, ESP32, RP2040) as well as desktop platforms (macOS, Linux) for testing and simulation.

1.2 Design Philosophy

Four principles govern every design decision in this library:

Principle Description
Zero dynamic allocation All memory is statically allocated at compile time via fixed-size pools. There are no calls to malloc() or free() anywhere in the library. Pool sizes are controlled by USER_DEFINED_* macros in openlcb_user_config.h.
Non-blocking state machines Every state machine does a fixed, bounded amount of work per call and returns. No function ever spins waiting for a condition. The main loop calls each state machine in round-robin fashion, ensuring all nodes and protocols make progress.
Static buffer pools Message payloads come in four fixed sizes: BASIC (16 bytes), DATAGRAM (72 bytes), SNIP (256 bytes), and STREAM (512 bytes). Each pool has a compile-time depth. Reference counting allows the same buffer to be shared across queues without copying.
Callback-based dependency injection Modules do not call each other directly. Instead, the application creates an interface struct containing function pointers, passes it to the module's *_initialize() function, and the module stores the pointer. This decouples the core protocol logic from platform-specific I/O.

1.3 Architecture Boundary

The library is split into two major layers separated by the openlcb_msg_t boundary type:

flowchart TB subgraph APP["Application Layer"] A1["openlcb_application.h"] A2["openlcb_application_train.h"] A3["openlcb_application_broadcast_time.h"] end subgraph CORE["openlcb/ — Transport-Independent Core"] direction TB B1["openlcb_main_statemachine"] B2["openlcb_login_statemachine"] B3["Protocol Handlers
(message_network, event_transport,
datagram, snip, config)"] B4["Buffer System
(buffer_store, buffer_fifo, buffer_list)"] B5["Node Management
(openlcb_node)"] B6["Utilities
(utilities, float16, gridconnect)"] end subgraph CAN["drivers/canbus/ — CAN Transport"] direction TB C1["can_main_statemachine"] C2["can_login_statemachine"] C3["can_rx_statemachine / can_tx_statemachine"] C4["alias_mappings / listener_alias_table"] C5["CAN Buffer System
(can_buffer_store, can_buffer_fifo)"] end subgraph FUTURE["drivers/tcpip/ — Future TCP/IP Transport"] D1["(not yet implemented)"] end APP --> CORE CORE <-->|"openlcb_msg_t"| CAN CORE <-->|"openlcb_msg_t"| FUTURE style APP fill:#e8f5e9,stroke:#388e3c style CORE fill:#e3f2fd,stroke:#1565c0 style CAN fill:#fff3e0,stroke:#e65100 style FUTURE fill:#f3e5f5,stroke:#7b1fa2

The openlcb/ directory contains everything that is transport-independent: types, defines, buffer management, node management, the main state machine, the login state machine, and all protocol handlers. The drivers/canbus/ directory contains the CAN-specific transport layer. A future drivers/tcpip/ directory would implement the TCP/IP transport, communicating with the core through the same openlcb_msg_t boundary.

1.4 The openlcb_msg_t Boundary Type

The openlcb_msg_t structure is the universal message currency of the library. It carries everything needed to process a message at the protocol level:

typedef struct {
    openlcb_msg_state_t state;      // allocated, inprocess, invalid, loopback
    uint16_t mti;                   // Message Type Indicator
    uint16_t source_alias;          // 12-bit CAN alias of sender
    uint16_t dest_alias;            // 12-bit CAN alias of recipient (0 = global)
    node_id_t source_id;            // 48-bit Node ID of sender
    node_id_t dest_id;              // 48-bit Node ID of recipient (0 = global)
    payload_type_enum payload_type; // BASIC, DATAGRAM, SNIP, or STREAM
    uint16_t payload_count;         // Number of valid bytes in payload
    openlcb_payload_t *payload;     // Pointer to payload buffer
    openlcb_msg_timer_t timer;      // Timer/retry union
    uint8_t reference_count;        // Number of active references
} openlcb_msg_t;
Note: Alias Fields

The source_alias and dest_alias fields are CAN-specific concepts (12-bit alias for the 48-bit Node ID). They ride along in the core openlcb_msg_t structure for convenience, avoiding the need for a separate CAN-specific message wrapper. On a non-CAN transport these fields would simply be zero.

1.5 Directory Structure

src/
+-- openlcb/                        # Transport-independent core
|   +-- openlcb_types.h             # All type definitions
|   +-- openlcb_defines.h           # All protocol constants (MTI, errors, etc.)
|   +-- openlcb_buffer_store.h/.c   # Segregated payload buffer pool
|   +-- openlcb_buffer_fifo.h/.c    # Circular FIFO for message pointers
|   +-- openlcb_buffer_list.h/.c    # Random-access list for multi-frame assembly
|   +-- openlcb_node.h/.c           # Node pool and enumeration
|   +-- openlcb_main_statemachine.h/.c
|   +-- openlcb_login_statemachine.h/.c
|   +-- openlcb_message_network.h/.c
|   +-- openlcb_event_transport.h/.c
|   +-- openlcb_datagram.h/.c
|   +-- openlcb_snip.h/.c
|   +-- openlcb_config.h/.c         # Configuration memory protocol
|   +-- openlcb_application.h/.c    # Application-level callbacks
|   +-- openlcb_application_train.h/.c
|   +-- openlcb_application_broadcast_time.h/.c
|   +-- openlcb_utilities.h/.c      # Payload insert/extract helpers
|   +-- openlcb_float16.h/.c        # IEEE 754 half-precision speed encoding
|   +-- openlcb_gridconnect.h/.c    # GridConnect ASCII format conversion
|   +-- *_Test.cxx                  # Google Test files
|
+-- drivers/
|   +-- canbus/                     # CAN transport layer
|       +-- can_types.h             # CAN frame types and constants
|       +-- can_buffer_store.h/.c   # CAN frame buffer pool
|       +-- can_buffer_fifo.h/.c    # CAN frame FIFO
|       +-- can_main_statemachine.h/.c
|       +-- can_login_statemachine.h/.c
|       +-- can_rx_statemachine.h/.c
|       +-- can_tx_statemachine.h/.c
|       +-- alias_mappings.h/.c     # Global alias-to-NodeID table
|       +-- listener_alias_table.h/.c
|       +-- *_Test.cxx
|
+-- applications/                   # Platform-specific demo projects
|   +-- arduino/                    # ESP32, RP2040
|   +-- dspic/                      # Microchip dsPIC
|   +-- platformio/                 # PlatformIO builds (ESP32, macOS)
|   +-- stm32_cubeide/              # STM32 CubeIDE
|   +-- ti_thiea/                   # TI MSPM0
|   +-- xcode/                      # macOS Xcode
|
+-- utilities/                      # Shared utility code
+-- test/                           # Test infrastructure
+-- build/                          # Build output

1.6 Target Platforms

The library has been tested or has demo projects for the following platforms:

Platform Toolchain Notes
Microchip dsPIC MPLAB X / XC16 16-bit, bare-metal, hardware CAN
STM32F407 STM32CubeIDE ARM Cortex-M4, hardware CAN
ESP32 Arduino / PlatformIO WiFi or hardware CAN via MCP2515
Raspberry Pi Pico (RP2040) Arduino CAN via MCP2515 SPI adapter
TI MSPM0 TI THEIA IDE ARM Cortex-M0+
macOS / Linux Xcode / PlatformIO / CMake Desktop simulation via GridConnect over TCP or serial

Because the library is pure C with no OS dependencies in the core, porting to a new platform requires only providing the platform-specific callbacks (CAN driver send/receive, timer tick, lock/unlock) and a suitable openlcb_user_config.h.

Next: Ch 2 — Building and Running →