|
| 1 | +# Time Driver (HAL) |
| 2 | + |
| 3 | +## Description |
| 4 | + |
| 5 | +The time driver is a HAL component that manages the system NTP daemon (`sysntpd`) and exposes a time source capability to the rest of the system. It is responsible for all direct OS interaction related to time synchronisation: restarting the daemon, monitoring kernel hotplug NTP events via ubus, and publishing the current sync state and accuracy metadata as a capability on the bus. |
| 6 | + |
| 7 | +The time driver acts as both a driver and a time manager — it owns the full lifecycle of `sysntpd` within the running session. |
| 8 | + |
| 9 | +## Dependencies |
| 10 | + |
| 11 | +- **ubus capability** (`{'cap', 'ubus', '1', ...}`): required to listen for `hotplug.ntp` kernel events. The driver waits for the ubus capability to become available before starting NTP. |
| 12 | + |
| 13 | +## Initialisation |
| 14 | + |
| 15 | +On startup (once the ubus capability is available): |
| 16 | + |
| 17 | +1. Restart `sysntpd` via `exec.command("/etc/init.d/sysntpd", "restart"):run()`. |
| 18 | +2. Register a ubus listener for `hotplug.ntp` events. |
| 19 | +3. Publish initial capability `meta` and `state` (initially `synced = false`). |
| 20 | + |
| 21 | +## Capability |
| 22 | + |
| 23 | +The driver publishes a single time source capability. The capability id is a UUID generated at startup. In the future, additional time source capabilities may exist alongside this one. |
| 24 | + |
| 25 | +### Meta (retained) |
| 26 | + |
| 27 | +Topic: `{'cap', 'time', <uuid>, 'meta'}` |
| 28 | + |
| 29 | +```lua |
| 30 | +{ |
| 31 | + provider = 'hal', |
| 32 | + source = 'ntp', -- time source type |
| 33 | + version = 1, -- interface version |
| 34 | + accuracy_seconds = <number|nil>, -- estimated absolute error in seconds |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +`accuracy_seconds` is a coarse estimate of absolute clock error and is derived from NTP stratum. Lower values are better. It is `nil` when unsynced (stratum >= 16) or before first sync-quality data is available. |
| 39 | + |
| 40 | +### State (retained) |
| 41 | + |
| 42 | +Topic: `{'cap', 'time', <uuid>, 'state'}` |
| 43 | + |
| 44 | +```lua |
| 45 | +{ |
| 46 | + synced = true | false, |
| 47 | + stratum = <number>, -- last reported stratum, nil before first event |
| 48 | + accuracy_seconds = <number|nil>, |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +Published on every sync/unsync transition. Retained so new subscribers immediately get the current state. |
| 53 | + |
| 54 | +### Events (non-retained) |
| 55 | + |
| 56 | +#### synced |
| 57 | + |
| 58 | +Topic: `{'cap', 'time', <uuid>, 'event', 'synced'}` |
| 59 | + |
| 60 | +Fired when the NTP daemon transitions from unsynced to synced (stratum < 16). Payload: |
| 61 | + |
| 62 | +```lua |
| 63 | +{ stratum = <number>, accuracy_seconds = <number|nil> } |
| 64 | +``` |
| 65 | + |
| 66 | +#### unsynced |
| 67 | + |
| 68 | +Topic: `{'cap', 'time', <uuid>, 'event', 'unsynced'}` |
| 69 | + |
| 70 | +Fired when the NTP daemon transitions from synced to unsynced (stratum == 16 or daemon restarts). Payload: |
| 71 | + |
| 72 | +```lua |
| 73 | +{ stratum = 16, accuracy_seconds = nil } |
| 74 | +``` |
| 75 | + |
| 76 | +These events are non-retained — consumers that need current state should read `{'cap', 'time', '1', 'state'}` first, then subscribe to events for transitions. |
| 77 | + |
| 78 | +## Service Flow |
| 79 | + |
| 80 | +```mermaid |
| 81 | +flowchart TD |
| 82 | + St[Start] --> A(Install alarm handler) |
| 83 | + A --> B(Wait for ubus capability) |
| 84 | + B -->|ubus cap available| C(Restart sysntpd) |
| 85 | + C --> D{sysntpd restart ok?} |
| 86 | + D -->|error| E[Log error and stop] |
| 87 | + D -->|ok| F(Register ubus listener for hotplug.ntp) |
| 88 | + F --> G{Listener registered ok?} |
| 89 | + G -->|error| H[Log error and stop] |
| 90 | + G -->|ok| I(Publish meta + initial state: synced=false) |
| 91 | + I --> J{Wait for hotplug.ntp event or stream closed or context done} |
| 92 | + J -->|hotplug.ntp event| K{stratum < 16?} |
| 93 | + K -->|yes, was unsynced| L(Publish state synced=true + event/synced) |
| 94 | + K -->|no, was synced| M(Publish state synced=false + event/unsynced) |
| 95 | + L --> J |
| 96 | + M --> J |
| 97 | + K -->|no change| J |
| 98 | + J -->|stream closed| N[Log warning, stop] |
| 99 | + J -->|context done| O(Send stop_stream to ubus) |
| 100 | +``` |
| 101 | + |
| 102 | +## Architecture |
| 103 | + |
| 104 | +- The driver runs a single main fiber that handles the full lifecycle. No child scope is needed. |
| 105 | +- Sync and unsync events are only published on **transitions** — the driver tracks the previous sync state and only emits an event when it changes. |
| 106 | +- The retained `state` topic is always updated on any hotplug event regardless of transition, to refresh the stratum value. |
| 107 | +- A `finally` block logs the reason for shutdown and performs cleanup (stop_stream if still active). |
| 108 | +- The ubus `stream_id` must be stopped cleanly on context cancellation to avoid leaking listener registrations in the ubus driver. |
0 commit comments