Chapter 26 — Adding a New Protocol

This chapter provides a step-by-step guide for adding a new protocol handler to the OpenLCB C Library. Protocol handlers live in the openlcb/ directory and must remain transport-independent -- they operate on openlcb_msg_t structures and 48-bit Node IDs, never on CAN frames or aliases directly.

26.1 Overview of the Process

flowchart TD A["Step 1: Define MTI constants"] --> B["Step 2: Add compile-time feature guard"] B --> C["Step 3: Create handler source files"] C --> D["Step 4: Register handler with dispatcher"] D --> E["Step 5: Add application callbacks to openlcb_config_t"] E --> F["Step 6: Wire callbacks in openlcb_config.c"] F --> G["Step 7: Write Google Tests"] G --> H["Step 8: Update documentation"] style A fill:#e3f2fd,stroke:#1565c0 style D fill:#fff3e0,stroke:#e65100 style G fill:#e8f5e9,stroke:#2e7d32

26.2 Step 1: Define MTI Constants

Add the MTI values for your protocol's request and reply messages to openlcb_defines.h. Follow the existing grouping pattern with Doxygen @defgroup tags:

/**
 * @defgroup mti_my_protocol My Protocol MTI Codes
 * @brief Description of the protocol.
 * @{
 */

    /** @brief Request message for My Protocol */
#define MTI_MY_PROTOCOL_REQUEST 0x0nnn

    /** @brief Reply message for My Protocol */
#define MTI_MY_PROTOCOL_REPLY 0x0nnn

    /** @} */ // end of mti_my_protocol
MTI Bit Fields

MTI values encode addressing mode, priority, and content flags. See Appendix A for the full MTI bit field layout. If your protocol uses addressed messages, set the destination-address-present bit (0x0008). If it carries an Event ID, set the event-present bit (0x0004).

If your protocol uses Protocol Support Inquiry, also add a PSI bit constant:

    /** @brief My Protocol support bit in Protocol Support Reply */
#define PSI_MY_PROTOCOL 0x000nnn

26.3 Step 2: Add Compile-Time Feature Guard

Define a feature flag macro for the new protocol. The application enables it in openlcb_user_config.h:

// In the application's openlcb_user_config.h:
#define OPENLCB_COMPILE_MY_PROTOCOL

If your protocol depends on another (e.g., it requires datagrams), add a dependency check in openlcb_config.h:

#if defined(OPENLCB_COMPILE_MY_PROTOCOL) && !defined(OPENLCB_COMPILE_DATAGRAMS)
#error "OPENLCB_COMPILE_MY_PROTOCOL requires OPENLCB_COMPILE_DATAGRAMS"
#endif

The existing dependency validation pattern in openlcb_config.h enforces these at compile time:

FeatureDepends On
MEMORY_CONFIGURATIONDATAGRAMS
BROADCAST_TIMEEVENTS
TRAIN_SEARCHEVENTS + TRAIN
FIRMWAREMEMORY_CONFIGURATION

26.4 Step 3: Create Handler Source Files

Create a header and implementation file in the openlcb/ directory:

openlcb/
    protocol_my_handler.h      // Public API
    protocol_my_handler.c      // Implementation

The handler follows the standard callback interface pattern. Define an interface struct that the wiring layer will populate:

// protocol_my_handler.h

#ifndef __PROTOCOL_MY_HANDLER__
#define __PROTOCOL_MY_HANDLER__

#include "openlcb_types.h"

#ifdef OPENLCB_COMPILE_MY_PROTOCOL

typedef struct {
    // Required: function to send a reply message
    void (*send_openlcb_msg)(openlcb_msg_t *openlcb_msg);

    // Optional: application callback for protocol-specific events
    void (*on_my_protocol_request)(openlcb_node_t *node, openlcb_msg_t *msg);
} protocol_my_handler_interface_t;

extern void ProtocolMyHandler_initialize(
    const protocol_my_handler_interface_t *interface);

extern void ProtocolMyHandler_handle_request(
    openlcb_statemachine_info_t *statemachine_info);

extern void ProtocolMyHandler_handle_reply(
    openlcb_statemachine_info_t *statemachine_info);

#endif /* OPENLCB_COMPILE_MY_PROTOCOL */
#endif /* __PROTOCOL_MY_HANDLER__ */

Implementation Pattern

The implementation stores a static pointer to the interface struct and uses it to call back into the wiring layer:

// protocol_my_handler.c

#include "protocol_my_handler.h"

#ifdef OPENLCB_COMPILE_MY_PROTOCOL

static const protocol_my_handler_interface_t *_interface;

void ProtocolMyHandler_initialize(
    const protocol_my_handler_interface_t *interface) {
    _interface = interface;
}

void ProtocolMyHandler_handle_request(
    openlcb_statemachine_info_t *statemachine_info) {

    openlcb_node_t *node = statemachine_info->openlcb_node;
    openlcb_msg_t *incoming = statemachine_info->incoming_msg_info.msg_ptr;
    openlcb_msg_t *outgoing =
        &statemachine_info->outgoing_msg_info.openlcb_msg.openlcb_msg;

    // Build the reply in the outgoing worker buffer
    outgoing->mti = MTI_MY_PROTOCOL_REPLY;
    outgoing->dest_id = incoming->source_id;
    outgoing->source_id = node->id;
    // ... populate payload ...

    statemachine_info->outgoing_msg_info.valid = 1;

    // Notify application if callback is provided
    if (_interface->on_my_protocol_request)
        _interface->on_my_protocol_request(node, incoming);
}

#endif /* OPENLCB_COMPILE_MY_PROTOCOL */
Transport Independence

Never reference CAN aliases, CAN frame types, or any can_*.h headers in a protocol handler. Protocol handlers operate on openlcb_msg_t with full 48-bit Node IDs. The transport layer handles alias resolution and frame fragmentation.

26.5 Step 4: Register with the Main Dispatcher

The main dispatcher (openlcb_main_statemachine.c) routes incoming messages to handlers based on MTI. The dispatcher uses an interface struct with function pointers for each supported MTI. To add your protocol:

flowchart LR A["Incoming openlcb_msg_t"] --> B{"MTI?"} B -->|"0x0488"| C["Verify Node ID handler"] B -->|"0x05B4"| D["Event PCER handler"] B -->|"0x1C48"| E["Datagram handler"] B -->|"0x0nnn"| F["Your handler"] B -->|"Unknown"| G["Send OIR"] style F fill:#fff3e0,stroke:#e65100
  1. Add a function pointer for your MTI to the main state machine's interface struct.
  2. Guard it with #ifdef OPENLCB_COMPILE_MY_PROTOCOL.
  3. In the dispatcher's MTI switch/if-else chain, add a case for your MTI that calls the function pointer.
  4. If the function pointer is NULL (protocol not wired), the dispatcher automatically sends an Optional Interaction Rejected (OIR) response.

26.6 Step 5: Add Application Callbacks

If your protocol needs to notify the application of events, add callback function pointers to openlcb_config_t in openlcb_config.h. Guard them with your feature flag:

// In openlcb_config.h, inside the openlcb_config_t struct:

#ifdef OPENLCB_COMPILE_MY_PROTOCOL

    // =========================================================================
    // OPTIONAL: My Protocol Callbacks (requires MY_PROTOCOL)
    // =========================================================================

        /** @brief Called when a My Protocol request is received. Optional. */
    void (*on_my_protocol_request)(openlcb_node_t *openlcb_node,
                                    /* protocol-specific parameters */);

#endif /* OPENLCB_COMPILE_MY_PROTOCOL */

26.7 Step 6: Wire in openlcb_config.c

The wiring module (openlcb_config.c) bridges the user's openlcb_config_t to the internal interface structs. Add wiring for your protocol handler:

// In openlcb_config.c:

#ifdef OPENLCB_COMPILE_MY_PROTOCOL
static protocol_my_handler_interface_t _my_protocol_interface;
#endif

// In the initialization function:
#ifdef OPENLCB_COMPILE_MY_PROTOCOL
    _my_protocol_interface.send_openlcb_msg = &_send_openlcb_msg;
    _my_protocol_interface.on_my_protocol_request =
        config->on_my_protocol_request;
    ProtocolMyHandler_initialize(&_my_protocol_interface);
#endif

26.8 Step 7: Write Google Tests

Follow the testing pattern established by the existing protocol tests. The test creates a mock interface struct, injects it via initialize(), and verifies the handler's behavior:

// gTests/test_protocol_my_handler.cpp

#include "gtest/gtest.h"

extern "C" {
#include "protocol_my_handler.h"
}

// Mock state
static openlcb_msg_t last_sent_msg;
static bool msg_was_sent = false;

static void mock_send(openlcb_msg_t *msg) {
    last_sent_msg = *msg;
    msg_was_sent = true;
}

class ProtocolMyHandlerTest : public ::testing::Test {
protected:
    void SetUp() override {
        msg_was_sent = false;

        static protocol_my_handler_interface_t mock_iface = {
            .send_openlcb_msg = &mock_send,
            .on_my_protocol_request = nullptr,
        };
        ProtocolMyHandler_initialize(&mock_iface);
    }
};

TEST_F(ProtocolMyHandlerTest, HandlesRequestAndSendsReply) {
    openlcb_statemachine_info_t info = { /* set up test context */ };
    // ... set up incoming message with MTI_MY_PROTOCOL_REQUEST ...

    ProtocolMyHandler_handle_request(&info);

    EXPECT_TRUE(info.outgoing_msg_info.valid);
    EXPECT_EQ(info.outgoing_msg_info.openlcb_msg.openlcb_msg.mti,
              MTI_MY_PROTOCOL_REPLY);
}

26.9 Checklist

StepFile(s)Action
1openlcb_defines.hAdd MTI constants and PSI bit
2openlcb_config.hAdd #error dependency check (if needed)
3openlcb/protocol_my_handler.h/.cCreate handler with interface struct pattern
4openlcb_main_statemachine.cAdd MTI case to dispatcher
5openlcb_config.hAdd application callbacks to openlcb_config_t
6openlcb_config.cWire internal interface to user config
7gTests/Write unit tests with mock interfaces
8DocumentationAdd chapter to Implementation Guide, update Appendix A and F
Reference Implementations

For a minimal example, study protocol_snip.c (simple request/reply). For a more complex example with state and sub-commands, study protocol_train_handler.c. For a datagram-based protocol, study protocol_datagram_handler.c.

← Prev: Ch 25 — Debugging Guide Next: Ch 27 — Adding a Transport →