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.
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.
| 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;
STREAM-sized payload (the largest available), ensuring any protocol handler
can build its response without worrying about buffer size limits.
Key fields
| Field | Type | Purpose |
|---|---|---|
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:
- If the node is NULL or not initialized, reject.
- If the message is a loopback from this same node (self-skip), reject.
- If the message is global (no destination address bit set), accept.
- If the message is addressed, accept only if the destination alias or Node ID matches this node.
- Special case:
MTI_VERIFY_NODE_ID_GLOBALis 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.
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():
- After successfully sending an outgoing message, allocate a buffer copy.
- Copy the header and payload bytes into the new buffer.
- Mark the copy with
state.loopback = true. - Push to the head of the incoming FIFO (processed next).
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:
- The handler builds the first response, sets
outgoing_msg_info.valid = true, and setsincoming_msg_info.enumerate = true. - Priority 1 sends the outgoing message.
- Priority 2 sees the enumerate flag and re-enters the handler.
- The handler builds the next response. When the last message is sent, it clears the enumerate flag.
- 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.
| MTI | Handler Callback | Required? |
|---|---|---|
| 0x0DE8 | snip_simple_node_info_request | Optional (OIR if NULL) |
| 0x0A08 | snip_simple_node_info_reply | Optional |
| 0x0100 | message_network_initialization_complete | Required |
| 0x0101 | message_network_initialization_complete_simple | Required |
| 0x0828 | message_network_protocol_support_inquiry | Required |
| 0x0668 | message_network_protocol_support_reply | Required |
| 0x0488 | message_network_verify_node_id_addressed | Required |
| 0x0490 | message_network_verify_node_id_global | Required |
| 0x0170/0x0171 | message_network_verified_node_id | Required |
| 0x0068 | message_network_optional_interaction_rejected | Required |
| 0x00A8 | message_network_terminate_due_to_error | Required |
| 0x08F4 | event_transport_consumer_identify | Optional |
| 0x04A4 | event_transport_consumer_range_identified | Optional |
| 0x04C7 | event_transport_consumer_identified_unknown | Optional |
| 0x04C4 | event_transport_consumer_identified_set | Optional |
| 0x04C5 | event_transport_consumer_identified_clear | Optional |
| 0x04C6 | event_transport_consumer_identified_reserved | Optional |
| 0x0914 | event_transport_producer_identify | Optional (+ train search intercept) |
| 0x0524 | event_transport_producer_range_identified | Optional |
| 0x0547 | event_transport_producer_identified_unknown | Optional |
| 0x0544 | event_transport_producer_identified_set | Optional (+ broadcast time intercept) |
| 0x0545 | event_transport_producer_identified_clear | Optional |
| 0x0546 | event_transport_producer_identified_reserved | Optional |
| 0x0968 | event_transport_identify_dest | Optional |
| 0x0970 | event_transport_identify | Optional |
| 0x0594 | event_transport_learn | Optional |
| 0x05B4 | event_transport_pc_report | Optional (+ broadcast time/emergency intercepts) |
| 0x05F4 | event_transport_pc_report_with_payload | Optional |
| 0x05EB | train_control_command | Optional (OIR if NULL) |
| 0x01E9 | train_control_reply | Optional |
| 0x0DA8 | simple_train_node_ident_info_request | Optional (OIR if NULL) |
| 0x0A48 | simple_train_node_ident_info_reply | Optional |
| 0x1C48 | datagram | Optional (Datagram Rejected if NULL) |
| 0x0A28 | datagram_ok_reply | Optional |
| 0x0A48 | datagram_rejected_reply | Optional |
| 0x0CC8 | stream_initiate_request | Optional (OIR if NULL) |
| 0x0868 | stream_initiate_reply | Optional |
| 0x1F88 | stream_send_data | Optional (OIR if NULL) |
| 0x0888 | stream_data_proceed | Optional |
| 0x08A8 | stream_data_complete | Optional (OIR if NULL) |
| default | load_interaction_rejected (if addressed) | -- |
8.9 Special MTI Intercepts
Several MTIs have special routing before falling through to the normal event transport handlers:
- MTI_PRODUCER_IDENTIFY (0x0914): If the event ID is a train search
event, it is dispatched to
train_search_event_handlerfor train nodes only. If no train matches across all nodes,train_search_no_match_handlerfires on the last node. - MTI_PRODUCER_IDENTIFIED_SET (0x0544): If the event ID is a broadcast
time event, it is dispatched to
broadcast_time_event_handler(node index 0 only). - MTI_PC_EVENT_REPORT (0x05B4): Checked first for broadcast time events (node 0 only), then for emergency events (dispatched to all train nodes), before falling through to the regular event handler.
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:
- When a request-type MTI has a NULL handler callback.
- For any unknown addressed MTI in the default case.
- Unknown global MTIs are silently ignored (no OIR).