Skip to main content

embedded_wg_display/util/
esptime.rs

1//! UTC wall-clock time via a HTTP sync from `timeapi.io`.
2//!
3//! Time is fetched once at startup. Subsequent calls to [`EspTime::now`] compute
4//! the current time by adding the elapsed duration since the fetch.
5use crate::runtime::http_sync;
6use crate::runtime::widget::widget::clocks::Datetime;
7use esp_hal::time::Instant;
8use serde::Deserialize;
9
10#[derive(Clone, Debug)]
11pub struct EspTime {
12    fetch_time_offset: Option<Instant>,
13    fetched_time_epoch: Option<u64>,
14}
15
16/// TimeAPI response format for easy parsing.
17#[derive(Deserialize)]
18struct TimeApiResponse {
19    unix_timestamp: u64,
20}
21
22impl EspTime {
23    /// Creates a new, [`EspTime`]. Call [`fetch_time`](Self::fetch_time) before use.
24    pub fn new() -> Self {
25        Self {
26            fetch_time_offset: None,
27            fetched_time_epoch: None,
28        }
29    }
30
31    /// Fetches the current Unix timestamp from `timeapi.io`
32    /// Also gets the current time since boot to be able to compute the current time based on the elapsed time since the fetch.
33    ///
34    /// # Panics
35    /// Panics if the HTTP request fails or the response cannot be parsed.
36    pub async fn fetch_time(&mut self) {
37        // https://timeapi.io/swagger/index.html
38        // returns unix time as json: { "unix_timestamp": 1774290895 }
39        let response = http_sync::http_request_async(http_sync::HttpRequest {
40            method: reqwless::request::Method::GET,
41            url: alloc::string::String::from("https://timeapi.io/api/v1/time/current/unix"),
42            body: None,
43        })
44        .await
45        .expect("Failed to fetch time API response");
46
47        self.fetch_time_offset = Some(Instant::now());
48
49        // Parse response
50        let body = alloc::string::String::from_utf8(response.bytes)
51            .expect("Failed to parse response as UTF-8");
52        let parsed: TimeApiResponse =
53            serde_json::from_str(body.trim()).expect("Failed to parse time API JSON");
54
55        self.fetched_time_epoch = Some(parsed.unix_timestamp);
56    }
57
58    /// Returns the current UTC time as [`Datetime`] since the Unix epoch.
59    ///
60    /// Used by the host function `runtime::host_api::clock_get`.
61    pub fn now(&self) -> Option<Datetime> {
62        match (self.fetch_time_offset, self.fetched_time_epoch) {
63            (Some(offset), Some(epoch)) => {
64                let elapsed = Instant::now() - offset;
65                let seconds = epoch.saturating_add(elapsed.as_secs());
66                let nanoseconds = ((elapsed.as_micros() % 1_000_000) * 1_000) as u32;
67                Some(Datetime {
68                    seconds,
69                    nanoseconds,
70                })
71            }
72            _ => None,
73        }
74    }
75}