Chapter 18 — Broadcast Time Protocol
The Broadcast Time Protocol provides a shared fast-clock for model railroad layouts. A clock generator (producer) broadcasts time, date, year, and rate information as well-known Event IDs. Consumers decode these events and maintain a local clock state that can run at accelerated or decelerated rates.
src/openlcb/protocol_broadcast_time_handler.c,
src/openlcb/protocol_broadcast_time_handler.h,
src/openlcb/openlcb_application_broadcast_time.c,
src/openlcb/openlcb_application_broadcast_time.h
18.1 Clock ID Encoding
Each broadcast clock is identified by a 64-bit Event ID base. The upper 6 bytes
identify the clock, and the lower 2 bytes encode the specific time data or command.
Four well-known clock IDs are defined by the standard, plus up to
BROADCAST_TIME_MAX_CUSTOM_CLOCKS (default 4) application-defined clocks.
The total clock capacity is:
BROADCAST_TIME_TOTAL_CLOCK_COUNT = BROADCAST_TIME_WELLKNOWN_CLOCK_COUNT (4)
+ BROADCAST_TIME_MAX_CUSTOM_CLOCKS (4)
= 8 slots
18.2 Event ID Field Encoding
The lower 2 bytes of the Event ID encode different data types depending on the event type. The protocol handler uses utility functions to extract the encoded values:
| Event Type | Encoded Data | Extractor Function |
|---|---|---|
| Report Time / Set Time | Hour (0-23) and minute (0-59) | OpenLcbUtilities_extract_time_from_event_id() |
| Report Date / Set Date | Month (1-12) and day (1-31) | OpenLcbUtilities_extract_date_from_event_id() |
| Report Year / Set Year | Year (0-4095) | OpenLcbUtilities_extract_year_from_event_id() |
| Report Rate / Set Rate | 12-bit signed fixed-point rate | OpenLcbUtilities_extract_rate_from_event_id() |
| Start / Stop / Query / Date Rollover | Command only (no data) | N/A |
18.3 broadcast_clock_state_t
Each clock slot stores its state in a broadcast_clock_state_t structure:
typedef struct {
uint64_t clock_id; // Clock identifier (upper 6 bytes)
broadcast_time_t time; // { hour, minute, valid }
broadcast_date_t date; // { month, day, valid }
broadcast_year_t year; // { year, valid }
broadcast_rate_t rate; // { rate, valid }
bool is_running; // true = running, false = stopped
uint32_t ms_accumulator; // Internal: accumulated ms toward next minute
} broadcast_clock_state_t;
Each sub-field has a valid flag that is set when data is first received
from the network. The ms_accumulator is used internally for fast-clock
advancement and is reset whenever a new time or rate event arrives.
The clock slot wrapper adds subscription metadata:
typedef struct {
broadcast_clock_state_t state;
bool is_consumer : 1; // Registered as a consumer
bool is_producer : 1; // Registered as a producer
bool is_allocated : 1; // Slot is in use
uint8_t send_query_reply_state; // Per-clock query reply sequence state
} broadcast_clock_t;
18.4 Event Type Dispatch
ProtocolBroadcastTime_handle_time_event() is the main entry point. It
extracts the clock ID, looks up the matching clock slot, determines the event type,
and dispatches to the appropriate sub-handler:
| Event Type Enum | Value | Handler | Producer Only? |
|---|---|---|---|
| BROADCAST_TIME_EVENT_REPORT_TIME | 0 | _handle_report_time() | No |
| BROADCAST_TIME_EVENT_REPORT_DATE | 1 | _handle_report_date() | No |
| BROADCAST_TIME_EVENT_REPORT_YEAR | 2 | _handle_report_year() | No |
| BROADCAST_TIME_EVENT_REPORT_RATE | 3 | _handle_report_rate() | No |
| BROADCAST_TIME_EVENT_SET_TIME | 4 | _handle_report_time() | Yes |
| BROADCAST_TIME_EVENT_SET_DATE | 5 | _handle_report_date() | Yes |
| BROADCAST_TIME_EVENT_SET_YEAR | 6 | _handle_report_year() | Yes |
| BROADCAST_TIME_EVENT_SET_RATE | 7 | _handle_report_rate() | Yes |
| BROADCAST_TIME_EVENT_QUERY | 8 | No-op | -- |
| BROADCAST_TIME_EVENT_STOP | 9 | _handle_stop() | No |
| BROADCAST_TIME_EVENT_START | 10 | _handle_start() | No |
| BROADCAST_TIME_EVENT_DATE_ROLLOVER | 11 | _handle_date_rollover() | No |
18.5 Producer Sync Sequence (Query Reply)
When a consumer sends a Query event, the producer responds with a six-message sequence that fully synchronizes the consumer's clock state:
The function OpenLcbApplicationBroadcastTime_send_query_reply() sends
this sequence. Because the transmit buffer may fill during the sequence, the function
uses per-clock state (send_query_reply_state) and must be called
repeatedly until it returns true. Messages 1-5 are sent as Producer Identified Set
events. Message 6 is sent as a PC Event Report (the next-minute time event).
18.6 Consumer Setup
To receive broadcast time events, a consumer node registers with:
broadcast_clock_state_t *clock =
OpenLcbApplicationBroadcastTime_setup_consumer(openlcb_node, clock_id);
This allocates a clock slot, marks it as a consumer, and registers the full 32,768-event consumer and producer ranges on the node so it can receive Report events and send the Query event. After setup, the consumer sends a Query to get the current clock state from the generator.
18.7 Fast-Clock Advancement
Consumer clocks are advanced locally between network time updates. The function
OpenLcbApplicationBroadcastTime_100ms_time_tick() is called from the
main loop with the current global tick. It uses fixed-point accumulation to handle
fractional rates without floating-point arithmetic.
Each tick advances the ms_accumulator by the scaled rate. When the
accumulator crosses a minute boundary, the clock's time is incremented and the
on_time_changed callback fires. Clocks that are stopped or have a rate
of 0 are skipped. Only consumer clocks are advanced; producer clocks manage their
own time.
18.8 Application Callbacks
The protocol handler provides 7 optional callbacks in
interface_openlcb_protocol_broadcast_time_handler_t:
| Callback | When Fired |
|---|---|
on_time_received | Time-of-day updated from a Report Time or Set Time event. |
on_date_received | Date updated from a Report Date or Set Date event. |
on_year_received | Year updated from a Report Year or Set Year event. |
on_rate_received | Clock rate changed from a Report Rate or Set Rate event. |
on_clock_started | Clock started (is_running set to true). |
on_clock_stopped | Clock stopped (is_running set to false). |
on_date_rollover | Date rollover event received (midnight crossing). |