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.

Source files: 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 TypeEncoded DataExtractor Function
Report Time / Set TimeHour (0-23) and minute (0-59)OpenLcbUtilities_extract_time_from_event_id()
Report Date / Set DateMonth (1-12) and day (1-31)OpenLcbUtilities_extract_date_from_event_id()
Report Year / Set YearYear (0-4095)OpenLcbUtilities_extract_year_from_event_id()
Report Rate / Set Rate12-bit signed fixed-point rateOpenLcbUtilities_extract_rate_from_event_id()
Start / Stop / Query / Date RolloverCommand only (no data)N/A
Rate encoding: The rate is a 12-bit signed fixed-point value with 2 fractional bits. A rate of 0x0004 means 1.0x real-time. The range is -512.00 to +511.75 in 0.25 increments. Negative values run the clock backward.

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:

flowchart TD A["Time event received"] --> B["Extract clock_id"] B --> C["Lookup clock slot"] C --> D{{"Clock found?"}} D -- "No" --> Z["return"] D -- "Yes" --> E["Determine event type"] E --> F{{"Event type"}} F -- "Report Time" --> G["_handle_report_time()"] F -- "Report Date" --> H["_handle_report_date()"] F -- "Report Year" --> I["_handle_report_year()"] F -- "Report Rate" --> J["_handle_report_rate()"] F -- "Set Time/Date/Year/Rate" --> K["Producer-only:\nsame as Report handlers"] F -- "Start" --> L["_handle_start()"] F -- "Stop" --> M["_handle_stop()"] F -- "Date Rollover" --> N["_handle_date_rollover()"] F -- "Query" --> O["No-op"] style A fill:#4a90d9,color:#fff
Event Type EnumValueHandlerProducer Only?
BROADCAST_TIME_EVENT_REPORT_TIME0_handle_report_time()No
BROADCAST_TIME_EVENT_REPORT_DATE1_handle_report_date()No
BROADCAST_TIME_EVENT_REPORT_YEAR2_handle_report_year()No
BROADCAST_TIME_EVENT_REPORT_RATE3_handle_report_rate()No
BROADCAST_TIME_EVENT_SET_TIME4_handle_report_time()Yes
BROADCAST_TIME_EVENT_SET_DATE5_handle_report_date()Yes
BROADCAST_TIME_EVENT_SET_YEAR6_handle_report_year()Yes
BROADCAST_TIME_EVENT_SET_RATE7_handle_report_rate()Yes
BROADCAST_TIME_EVENT_QUERY8No-op--
BROADCAST_TIME_EVENT_STOP9_handle_stop()No
BROADCAST_TIME_EVENT_START10_handle_start()No
BROADCAST_TIME_EVENT_DATE_ROLLOVER11_handle_date_rollover()No
Set vs. Report: Set commands (SET_TIME, SET_DATE, SET_YEAR, SET_RATE) are only processed if the clock is registered as a producer. These are commands from consumers asking the generator to change its state. Report events are processed by all clocks (consumer or producer).

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:

sequenceDiagram participant Consumer participant Producer as Clock Generator Consumer->>Producer: Query Event Producer-->>Consumer: 1. Start or Stop (current state) Producer-->>Consumer: 2. Report Rate Producer-->>Consumer: 3. Report Year Producer-->>Consumer: 4. Report Date Producer-->>Consumer: 5. Report Time (current) Producer-->>Consumer: 6. Report Time (next minute, as PCER) Note over Consumer: Clock is now fully synchronized

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:

CallbackWhen Fired
on_time_receivedTime-of-day updated from a Report Time or Set Time event.
on_date_receivedDate updated from a Report Date or Set Date event.
on_year_receivedYear updated from a Report Year or Set Year event.
on_rate_receivedClock rate changed from a Report Rate or Set Rate event.
on_clock_startedClock started (is_running set to true).
on_clock_stoppedClock stopped (is_running set to false).
on_date_rolloverDate rollover event received (midnight crossing).
Node index restriction: The protocol handler only processes broadcast time events for node index 0. Broadcast time events are global and do not need to be processed by every virtual node in a multi-node configuration.