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
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 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:
| Feature | Depends On |
|---|---|
MEMORY_CONFIGURATION | DATAGRAMS |
BROADCAST_TIME | EVENTS |
TRAIN_SEARCH | EVENTS + TRAIN |
FIRMWARE | MEMORY_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 */
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:
- Add a function pointer for your MTI to the main state machine's interface struct.
- Guard it with
#ifdef OPENLCB_COMPILE_MY_PROTOCOL. - In the dispatcher's MTI switch/if-else chain, add a case for your MTI that calls the function pointer.
- 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
| Step | File(s) | Action |
|---|---|---|
| 1 | openlcb_defines.h | Add MTI constants and PSI bit |
| 2 | openlcb_config.h | Add #error dependency check (if needed) |
| 3 | openlcb/protocol_my_handler.h/.c | Create handler with interface struct pattern |
| 4 | openlcb_main_statemachine.c | Add MTI case to dispatcher |
| 5 | openlcb_config.h | Add application callbacks to openlcb_config_t |
| 6 | openlcb_config.c | Wire internal interface to user config |
| 7 | gTests/ | Write unit tests with mock interfaces |
| 8 | Documentation | Add chapter to Implementation Guide, update Appendix A and F |
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.