Chapter 8 — Main Dispatcher State Machine

The main dispatcher is the central message-routing engine for the OpenLCB protocol stack. It lives in openlcb_main_statemachine.c/.h and is driven by a single entry point called as fast as possible from the application main loop.

Source files: src/openlcb/openlcb_main_statemachine.c, src/openlcb/openlcb_main_statemachine.h

8.1 Entry Point

The entire dispatch loop is driven by OpenLcbMainStatemachine_run(). Each call performs exactly one atomic operation and returns immediately, maintaining a non-blocking cooperative design. The application calls this as fast as possible:

while (1) {
    OpenLcbMainStatemachine_run();   // protocol dispatch
    OpenLcbLoginMainStatemachine_run();  // login sequencing
    CanMainStateMachine_run();       // CAN layer
}

8.2 Priority Dispatch Flowchart

Each call to _run() evaluates five priorities in order. The first one that returns true (meaning it did work) causes the function to return immediately. This ensures the highest-priority task always gets serviced first.

flowchart TD A["OpenLcbMainStatemachine_run()"] --> B{"handle_outgoing\n_openlcb_message()"} B -- "true (msg pending)" --> Z["return"] B -- "false" --> C{"handle_try\n_reenumerate()"} C -- "true (enumerate flag set)" --> Z C -- "false" --> D{"handle_try_pop_next\n_incoming_openlcb_message()"} D -- "true (popped or empty)" --> Z D -- "false" --> E{"handle_try_enumerate\n_first_node()"} E -- "true" --> Z E -- "false" --> F{"handle_try_enumerate\n_next_node()"} F -- "true" --> Z F -- "false" --> Z style A fill:#4a90d9,color:#fff style Z fill:#888,color:#fff
Priority Function Purpose
1 (highest) handle_outgoing_openlcb_message() Send the pending outgoing message to the transport layer. Also performs sibling loopback for multi-node setups.
2 handle_try_reenumerate() If a handler set the enumerate flag (multi-message response), re-enter the same handler without advancing to the next node.
3 handle_try_pop_next_incoming_openlcb_message() Pop the next message from the incoming FIFO (thread-safe under lock). Discards messages marked invalid.
4 handle_try_enumerate_first_node() Get the first node from the node pool and dispatch the incoming message to it.
5 handle_try_enumerate_next_node() Advance to the next node and dispatch. Frees the incoming message when the last node is reached.

8.3 The State Machine Context

The dispatcher maintains a single static context structure, openlcb_statemachine_info_t, that is passed to every protocol handler. This structure provides:

typedef struct {
    openlcb_msg_info_t   incoming_msg_info;   // current incoming message + enumerate flag
    openlcb_msg_info_t   outgoing_msg_info;   // worker buffer for reply + valid flag
    openlcb_node_t      *openlcb_node;        // node currently being enumerated
    uint8_t              current_tick;         // snapshot of 100ms timer
} openlcb_statemachine_info_t;
Worker buffer: The outgoing message buffer inside the context uses STREAM-sized payload (the largest available), ensuring any protocol handler can build its response without worrying about buffer size limits.

Key fields

FieldTypePurpose
incoming_msg_info.msg_ptr openlcb_msg_t* Pointer to the current incoming message popped from the FIFO.
incoming_msg_info.enumerate bool Set by handlers that need to send multiple messages (e.g., Identify Events). The dispatcher re-enters the handler until this flag is cleared.
outgoing_msg_info.msg_ptr openlcb_msg_t* Pre-allocated STREAM-sized worker buffer for building responses.
outgoing_msg_info.valid bool Set by handlers when a response is ready. Cleared after successful transmission.
openlcb_node openlcb_node_t* The node currently being processed during enumeration.
current_tick uint8_t Snapshot of the 100ms timer captured when a message is popped from the FIFO.

8.4 Node Address Filtering

Before dispatching a message to a protocol handler, the function OpenLcbMainStatemachine_does_node_process_msg() determines whether the current node should process the message. The rules are:

  1. If the node is NULL or not initialized, reject.
  2. If the message is a loopback from this same node (self-skip), reject.
  3. If the message is global (no destination address bit set), accept.
  4. If the message is addressed, accept only if the destination alias or Node ID matches this node.
  5. Special case: MTI_VERIFY_NODE_ID_GLOBAL is always accepted (it carries an optional Node ID in the payload).

8.5 Fan-Out to All Nodes

Every incoming message is dispatched to every node in the system by the enumerate-first / enumerate-next loop. Global messages reach all nodes; addressed messages are filtered by does_node_process_msg() so only the target node processes them. This fan-out design supports multi-node configurations where a single microcontroller hosts several virtual nodes.

sequenceDiagram participant FIFO as Incoming FIFO participant SM as Main Dispatcher participant N1 as Node 1 participant N2 as Node 2 participant N3 as Node 3 SM->>FIFO: pop() FIFO-->>SM: message SM->>N1: does_node_process_msg? -> yes SM->>N1: process_main_statemachine() Note over SM: send outgoing if valid SM->>N2: does_node_process_msg? -> yes SM->>N2: process_main_statemachine() SM->>N3: does_node_process_msg? -> no (not addressed) Note over SM: free incoming message

8.6 Sibling Loopback

When a node generates a response (e.g., an Init Complete message), sibling nodes on the same microcontroller need to see it too. The dispatcher handles this with _loopback_to_siblings():

  1. After successfully sending an outgoing message, allocate a buffer copy.
  2. Copy the header and payload bytes into the new buffer.
  3. Mark the copy with state.loopback = true.
  4. Push to the head of the incoming FIFO (processed next).
One-level cascade: Loopback copies are never looped back again. The state.loopback flag prevents infinite recursion, and the self-skip check in does_node_process_msg() prevents the originating node from processing its own copy.

8.7 The Enumerate Flag

Some protocol handlers need to send multiple messages in response to a single incoming message. For example, Identify Events must report every producer and consumer event the node knows about. The mechanism:

  1. The handler builds the first response, sets outgoing_msg_info.valid = true, and sets incoming_msg_info.enumerate = true.
  2. Priority 1 sends the outgoing message.
  3. Priority 2 sees the enumerate flag and re-enters the handler.
  4. The handler builds the next response. When the last message is sent, it clears the enumerate flag.
  5. The dispatcher advances to the next node (or the next incoming message).

8.8 MTI Dispatch Table

OpenLcbMainStatemachine_process_main_statemachine() contains a large switch statement that routes each MTI to the correct protocol handler callback. If an optional handler is NULL, request-type MTIs trigger an Interaction Rejected response automatically; reply/indication MTIs are silently ignored.

MTIHandler CallbackRequired?
0x0DE8snip_simple_node_info_requestOptional (OIR if NULL)
0x0A08snip_simple_node_info_replyOptional
0x0100message_network_initialization_completeRequired
0x0101message_network_initialization_complete_simpleRequired
0x0828message_network_protocol_support_inquiryRequired
0x0668message_network_protocol_support_replyRequired
0x0488message_network_verify_node_id_addressedRequired
0x0490message_network_verify_node_id_globalRequired
0x0170/0x0171message_network_verified_node_idRequired
0x0068message_network_optional_interaction_rejectedRequired
0x00A8message_network_terminate_due_to_errorRequired
0x08F4event_transport_consumer_identifyOptional
0x04A4event_transport_consumer_range_identifiedOptional
0x04C7event_transport_consumer_identified_unknownOptional
0x04C4event_transport_consumer_identified_setOptional
0x04C5event_transport_consumer_identified_clearOptional
0x04C6event_transport_consumer_identified_reservedOptional
0x0914event_transport_producer_identifyOptional (+ train search intercept)
0x0524event_transport_producer_range_identifiedOptional
0x0547event_transport_producer_identified_unknownOptional
0x0544event_transport_producer_identified_setOptional (+ broadcast time intercept)
0x0545event_transport_producer_identified_clearOptional
0x0546event_transport_producer_identified_reservedOptional
0x0968event_transport_identify_destOptional
0x0970event_transport_identifyOptional
0x0594event_transport_learnOptional
0x05B4event_transport_pc_reportOptional (+ broadcast time/emergency intercepts)
0x05F4event_transport_pc_report_with_payloadOptional
0x05EBtrain_control_commandOptional (OIR if NULL)
0x01E9train_control_replyOptional
0x0DA8simple_train_node_ident_info_requestOptional (OIR if NULL)
0x0A48simple_train_node_ident_info_replyOptional
0x1C48datagramOptional (Datagram Rejected if NULL)
0x0A28datagram_ok_replyOptional
0x0A48datagram_rejected_replyOptional
0x0CC8stream_initiate_requestOptional (OIR if NULL)
0x0868stream_initiate_replyOptional
0x1F88stream_send_dataOptional (OIR if NULL)
0x0888stream_data_proceedOptional
0x08A8stream_data_completeOptional (OIR if NULL)
defaultload_interaction_rejected (if addressed)--

8.9 Special MTI Intercepts

Several MTIs have special routing before falling through to the normal event transport handlers:

8.10 Interaction Rejected

OpenLcbMainStatemachine_load_interaction_rejected() builds an OIR response with error code ERROR_PERMANENT_NOT_IMPLEMENTED_UNKNOWN_MTI_OR_TRANSPORT_PROTOCOL and includes the triggering MTI in the payload (bytes 2-3). This is called: