Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Time service (rohi-time, rohi-time-ntp)

rohi-time gives the firmware one source of wall-clock time and synchronization state. Components that need a real-world timestamp (persistent logs, datalog, diagnostics, measurement timestamps) ask the TimeService. They never contact an NTP server, manage a clock chip, keep clock offsets or decide whether a timestamp is trustworthy.

   NtpTimeSource            future RtcTimeSource        external time
   (rohi-time-ntp)          (hardware RTC)
        │  DNS + UDP              │  I²C / SPI
        └────────────┬────────────┴─────────────┘
                     ▼   TimeSample { unix_time, uncertainty }
              Synchronizer        background task, backoff, resync
                     ▼
              ┌─────────────┐   validation (plausibility window)
              │ TimeService │   anchor = absolute time + monotonic instant
              └──────┬──────┘   state, failure counters
                     │  now() = anchor + monotonic elapsed
        ┌────────────┼────────────┐
        ▼            ▼            ▼
     logging       datalog     control plane (GetTimeStatus)

Monotonic time versus wall-clock time

These are different concepts with different types, and there is no ambiguous timestamp().

Monotonic timeWall-clock time
Typerohi_hal::MonotonicTimestamp (from embassy-time)rohi_time::UnixTime (Unix milliseconds, UTC)
Availablealways, from bootonly after a valid sample; otherwise None
Can jumpneveryes, forward or backward, when a sample corrects it
Use fordeadlines, timeouts, intervals, retries, scheduling, elapsed time, ordering events in a bootabsolute timestamps only

Wall-clock time may step. A newly accepted sample replaces the anchor at once. The clock is not slewed and not made monotonic. Never use wall-clock time to measure durations or order events; use the monotonic clock. A future work item may add smoothing.

Guarantees

  • TimeService::now() is None until the first valid sample. Uptime is never returned as Unix time and no placeholder date (such as 1970-01-01) is ever invented.
  • now() is cheap: it adds the monotonic time elapsed since the accepted sample to that sample. No network, no hardware access, no allocation.
  • An invalid, implausible or failed sample never replaces a valid anchor.
  • Firmware startup never waits for time. Everything else runs unsynchronized and failures are only recorded.
  • Synchronization is not authentication. Plain NTP is not cryptographically protected, so the time must not be an input to security-critical decisions. TimeStatus reports synchronization quality, not authenticity. Authenticated network time (NTS) is out of scope.

Consumer API

let time = altruist_urban::time::service();

match time.now() {
    Some(unix) => attach_wall_timestamp(unix),   // real, synchronized time
    None => monotonic_only(),                     // never a fabricated date
}

let status = time.status();      // state, source, last_sync_age, uncertainty, consecutive_failures, last_failure
let snapshot = time.snapshot();  // monotonic + wall time of the same instant, plus state
TimeSyncStateMeaning
Unsynchronizedno valid sample yet; now() is None
Synchronizeda valid sample was accepted within the stale threshold
Stalethe anchor is older than the threshold (TimeConfig::stale_after, default 6 h); now() still works from the last anchor and may have drifted

TimeSource/TimeSourceKind (Ntp, Rtc, External) appear in diagnostics only; consumers do not normally branch on them. The latest valid sample of any source wins; the policy is deliberately not generalized yet, but the public API carries no NTP assumption.

Sources: the TimeSource trait

trait TimeSource {
    type Error: Into<SyncFailure>;
    fn kind(&self) -> TimeSourceKind;
    fn is_available(&self) -> bool { true }          // e.g. network ready
    async fn sample(&mut self) -> Result<TimeSample, Self::Error>;
}

A source only produces samples. A hardware RTC source would implement the same trait (HardwareRtcSource<R>), so a board with an RTC can establish the initial time before the network exists and let NTP refine it later; consumers do not change. A source must bound its own waits and be cancel-safe.

NTP source (rohi-time-ntp)

NtpTimeSource resolves a server with NetworkHandle DNS, sends one SNTPv4 datagram over a UDP socket of the same handle and returns a sample: the server’s transmit time plus half the round trip, with the round trip and the server’s root delay/dispersion as uncertainty. It uses the sntpc client for the protocol (mode, version, stratum, origin timestamp, leap indicator, kiss-o’-death, root distance and reply address checks); the result is mapped to coarse SyncFailure categories. It rotates through at most MAX_SERVERS (4) configured servers, moving on after a failure, and holds one UDP socket only for the duration of one exchange. DNS and the exchange are bounded by NtpConfig::timeout_ms (5 s). It never owns the global concept of time.

NTP era. NTP timestamps wrap every 2³² s (the next wrap is in 2036), so a wire value alone is ambiguous. The source takes a pivot, the best known current time, and picks the era closest to it. In firmware the pivot is TimeService::pivot(): the synchronized time when there is one, otherwise the firmware build time plus uptime (not a timestamp, never recorded). A pivot more than about 68 years off picks the wrong era; such a result is far outside the plausibility window and is rejected.

Validation

TimeConfig (named, documented values) decides whether a sample may become the anchor:

RuleDefault
not_before (plausibility floor)firmware build time (BUILD_TIMESTAMP, set by the firmware build.rs)
not_afterfloor + DEFAULT_HORIZON (40 years)
max_uncertaintyDEFAULT_MAX_UNCERTAINTY (5 s)
stale_afterDEFAULT_STALE_AFTER (6 h)

A rejected sample is recorded as SyncFailure::Implausible and the existing anchor is kept.

Background synchronization

Synchronizer runs in one task (altruist_urban::time::ntp_task):

  • Network down (is_available() == false): polls every 2 s, without counting a failure; the first sample is taken as soon as the network returns. Network appearing and disappearing at runtime is expected.
  • Failure (DNS, timeout, unreachable, invalid response, kiss-o’-death, implausible time): recorded (consecutive_failures, last_failure), retried with bounded exponential backoff, 15 s doubling up to 10 minutes. The anchor is untouched; there is no panic.
  • Success: backoff is reset, the failure counters are cleared and the next sample is taken after 1 hour. Oscillator drift is thereby corrected, while polling stays well within NTP etiquette (RFC 4330: never more often than every 15 s).

The intervals are SyncSchedule fields; SyncSchedule::DEFAULT holds the values above.

Control plane

GetTimeStatus (capability TIME, read-only, no secure link needed) returns the state, unix_time_ms (absent while unsynchronized), source, age since the last sync, uncertainty, consecutive failures and the category of the last failure. rohi time shows it, and rohi status includes it as a Time section (The rohi tool):

$ rohi time
Time
  State       synchronized
  Unix time   1780000000.250
  Source      ntp
  Last sync   90 s ago
  Accuracy    +-40 ms

Manual resynchronization is not offered: periodic automatic synchronization is authoritative.

Resources

All storage is static and bounded: the anchor and counters are a few words behind a critical-section mutex; NtpTimeSource keeps no state besides a server index; one UDP socket (UDP_SOCKETS = 1, UDP_BUFFER = 128 B per direction) is allocated from the NetResources pool for each exchange. See Resource limits.

Testing

Everything runs on the host without Internet access: fake clock, fake sources, and rohi_net::testing::FakeIp with a scripted NTP server (valid and malformed answers, kiss-o’-death, timeouts, DNS failure, unreachable server, era rollover, implausible time, network loss and recovery). See Host testing.

Not in scope

A hardware RTC driver, persisted last-known time, clock slewing, time zones and daylight saving, authenticated NTP (NTS), GPS/PTP, an NTP server. Persistent logging will consume TimeService::now() and attach a wall timestamp only when it is Some; it must keep working before the first synchronization.