Chapter 6b — Callback Interface Pattern

The library uses a consistent dependency injection (DI) pattern across all modules. Instead of modules calling each other directly, the application wires them together through function pointer structs. This chapter explains the pattern and its conventions.

6b.1 The Pattern

Every module that depends on external behavior follows the same three-step pattern:

  1. Define an interface struct containing function pointers for each external dependency.
  2. Application creates and populates a static instance of the struct, filling in the pointers.
  3. Pass the struct to the module's *_initialize() function, which stores the pointer internally.
// Step 1: Module defines its interface (in module_header.h)
typedef struct {
    void (*send_openlcb_msg)(openlcb_msg_t *msg);        // Required
    void (*on_message_received)(openlcb_node_t *node,     // Optional
                                openlcb_msg_t *msg);
    void (*lock_shared_resources)(void);                   // Required
    void (*unlock_shared_resources)(void);                 // Required
} interface_some_module_t;

// Step 2: Application creates the struct (in main.c)
static interface_some_module_t my_interface = {
    .send_openlcb_msg = &platform_send_message,
    .on_message_received = &app_handle_message,
    .lock_shared_resources = &platform_lock,
    .unlock_shared_resources = &platform_unlock,
};

// Step 3: Initialize the module
SomeModule_initialize(&my_interface);

6b.2 Required vs. Optional Callbacks

Interface struct members fall into two categories:

CategoryIf NULLConvention
Required Module cannot function; undefined behavior or crash Must always be set. Documented as required in the header.
Optional Module checks for NULL before calling; uses a default behavior Can be set to NULL. The default behavior is typically an Optional Interaction Rejected (OIR) response or a no-op.
NULL Optional = OIR

When an optional callback is NULL and a message arrives that would trigger it, the library generates an Optional Interaction Rejected (OIR) response automatically. This tells the remote node that this protocol/feature is not supported by this node.

6b.3 Key Callback Contracts

send_openlcb_msg()

The most important callback. Every module that needs to send a message calls through this pointer. The CAN transport layer provides the implementation, which fragments the message into CAN frames and queues them for transmission.

void (*send_openlcb_msg)(openlcb_msg_t *msg);

Contract:

send_can_message()

Used by the CAN transport layer to hand a raw CAN frame to the hardware driver for transmission.

void (*send_can_message)(can_msg_t *msg);

Contract:

lock_shared_resources() / unlock_shared_resources()

Described in detail in Chapter 6. Every module that accesses shared data structures requires these callbacks.

6b.4 Node Enumeration Callbacks

Some callbacks are invoked once per node during the main state machine's node enumeration loop. The callback receives a pointer to the current node and the current message being processed:

void (*on_event_received)(openlcb_node_t *node, openlcb_msg_t *msg);
void (*on_datagram_received)(openlcb_node_t *node, openlcb_msg_t *msg);
void (*on_train_command)(openlcb_node_t *node, openlcb_msg_t *msg);

These are called from within the main dispatcher's node iteration loop, so they are always in main loop context (never from ISR).

6b.5 How Callbacks Wire Modules Together

flowchart TB subgraph APP["Application"] APP_INIT["main() initialization"] end subgraph CORE["OpenLCB Core"] MAIN_SM["Main State Machine"] LOGIN_SM["Login State Machine"] PROTO["Protocol Handlers"] end subgraph CAN["CAN Transport"] CAN_SM["CAN State Machine"] CAN_RX["CAN RX"] CAN_TX["CAN TX"] end subgraph HW["Platform Hardware"] HW_CAN["CAN Driver"] HW_TIMER["Timer ISR"] HW_LOCK["Lock/Unlock"] end APP_INIT -->|"interface structs"| MAIN_SM APP_INIT -->|"interface structs"| LOGIN_SM APP_INIT -->|"interface structs"| CAN_SM PROTO -->|"send_openlcb_msg()"| CAN_TX CAN_TX -->|"send_can_message()"| HW_CAN CAN_RX -->|"push to FIFO"| MAIN_SM MAIN_SM -->|"lock/unlock"| HW_LOCK CAN_SM -->|"lock/unlock"| HW_LOCK style APP fill:#e8f5e9,stroke:#388e3c style CORE fill:#e3f2fd,stroke:#1565c0 style CAN fill:#fff3e0,stroke:#e65100 style HW fill:#fce4ec,stroke:#c62828

6b.6 Example: Node Module Interface

The node module has a minimal interface with a single optional callback:

typedef struct {
    void (*on_100ms_timer_tick)(void);  // Optional: fired every 100ms after node updates
} interface_openlcb_node_t;

// Usage:
static interface_openlcb_node_t node_interface = {
    .on_100ms_timer_tick = &my_timer_handler,  // or NULL to skip
};
OpenLcbNode_initialize(&node_interface);

6b.7 Design Benefits

← Previous: Ch 6 — Thread Safety Next: Ch 7 — Utilities →