Skip to main content

cadmus_core/
rtc.rs

1use anyhow::Error;
2use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike, Utc};
3use nix::{ioctl_none, ioctl_read, ioctl_write_ptr};
4use std::collections::BTreeMap;
5use std::fs::{File, OpenOptions};
6use std::mem;
7use std::os::unix::io::AsRawFd;
8use std::path::Path;
9use std::sync::{Arc, Mutex};
10
11ioctl_read!(rtc_read_alarm, b'p', 0x10, RtcWkalrm);
12ioctl_write_ptr!(rtc_write_alarm, b'p', 0x0f, RtcWkalrm);
13ioctl_none!(rtc_disable_alarm, b'p', 0x02);
14ioctl_read!(rtc_read_time, b'p', 0x09, RtcTime);
15ioctl_write_ptr!(rtc_set_time, b'p', 0x0a, RtcTime);
16
17#[repr(C)]
18#[derive(Debug, Clone)]
19pub struct RtcTime {
20    tm_sec: libc::c_int,
21    tm_min: libc::c_int,
22    tm_hour: libc::c_int,
23    tm_mday: libc::c_int,
24    tm_mon: libc::c_int,
25    tm_year: libc::c_int,
26    tm_wday: libc::c_int,
27    tm_yday: libc::c_int,
28    tm_isdst: libc::c_int,
29}
30
31impl Default for RtcWkalrm {
32    fn default() -> Self {
33        unsafe { mem::zeroed() }
34    }
35}
36
37#[repr(C)]
38#[derive(Debug, Clone)]
39pub struct RtcWkalrm {
40    enabled: libc::c_uchar,
41    pending: libc::c_uchar,
42    time: RtcTime,
43}
44
45impl RtcTime {
46    fn year(&self) -> i32 {
47        1900 + self.tm_year
48    }
49}
50
51impl TryFrom<RtcTime> for DateTime<Utc> {
52    type Error = Error;
53
54    fn try_from(rt: RtcTime) -> Result<Self, Self::Error> {
55        Utc.with_ymd_and_hms(
56            rt.year(),
57            (rt.tm_mon as u32) + 1,
58            rt.tm_mday as u32,
59            rt.tm_hour as u32,
60            rt.tm_min as u32,
61            rt.tm_sec as u32,
62        )
63        .single()
64        .ok_or_else(|| anyhow::anyhow!("invalid RTC date/time fields"))
65    }
66}
67
68impl From<DateTime<Utc>> for RtcTime {
69    fn from(dt: DateTime<Utc>) -> Self {
70        RtcTime {
71            tm_sec: dt.second() as libc::c_int,
72            tm_min: dt.minute() as libc::c_int,
73            tm_hour: dt.hour() as libc::c_int,
74            tm_mday: dt.day() as libc::c_int,
75            tm_mon: dt.month0() as libc::c_int,
76            tm_year: (dt.year() - 1900) as libc::c_int,
77            tm_wday: -1,
78            tm_yday: -1,
79            tm_isdst: -1,
80        }
81    }
82}
83
84impl RtcWkalrm {
85    /// Returns whether the alarm is currently enabled.
86    pub fn enabled(&self) -> bool {
87        self.enabled == 1
88    }
89
90    /// Returns the year field from the alarm's stored time.
91    ///
92    /// This is the full calendar year (e.g., 2024), not the offset from 1900.
93    pub fn year(&self) -> i32 {
94        self.time.year()
95    }
96}
97
98/// Interface to the hardware real-time clock device.
99///
100/// `Rtc` provides access to both time and alarm functionality of the RTC.
101/// Operations are serialized via an internal mutex to ensure thread-safe access
102/// to the underlying device file.
103pub struct Rtc(Arc<Mutex<File>>);
104
105impl Clone for Rtc {
106    fn clone(&self) -> Self {
107        Rtc(Arc::clone(&self.0))
108    }
109}
110
111impl Rtc {
112    /// Opens the RTC device and creates a new interface handle.
113    ///
114    /// # Arguments
115    ///
116    /// * `path` - Path to the RTC device file (typically `/dev/rtc0` or `/dev/rtc`)
117    ///
118    /// # Returns
119    ///
120    /// A new `Rtc` handle on success, or an error if the device cannot be opened.
121    ///
122    /// # Example
123    ///
124    /// ```no_run
125    /// # use cadmus_core::rtc::Rtc;
126    /// let rtc = Rtc::new("/dev/rtc0")?;
127    /// # Ok::<(), Box<dyn std::error::Error>>(())
128    /// ```
129    pub fn new<P: AsRef<Path>>(path: P) -> Result<Rtc, Error> {
130        let file = OpenOptions::new().read(true).write(true).open(path)?;
131        Ok(Rtc(Arc::new(Mutex::new(file))))
132    }
133
134    /// Reads the current alarm settings from the hardware.
135    ///
136    /// Returns information about the wake alarm, including whether it is enabled
137    /// and any pending alarm status. The alarm time is stored as [`RtcWkalrm`].
138    ///
139    /// # Returns
140    ///
141    /// Alarm settings on success, or an error if the ioctl fails or the lock is poisoned.
142    pub fn alarm(&self) -> Result<RtcWkalrm, Error> {
143        let mut rwa = RtcWkalrm::default();
144        let file = self
145            .0
146            .lock()
147            .map_err(|e| anyhow::anyhow!("lock poisoned: {}", e))?;
148        unsafe {
149            rtc_read_alarm(file.as_raw_fd(), &mut rwa)
150                .map(|_| rwa)
151                .map_err(|e| e.into())
152        }
153    }
154
155    /// Programs the hardware to wake at the specified time.
156    ///
157    /// Enables a single-shot alarm that will fire at the given UTC time.
158    /// If an alarm is already scheduled, it is replaced.
159    ///
160    /// # Arguments
161    ///
162    /// * `wake_time` - The UTC time when the alarm should fire
163    ///
164    /// # Returns
165    ///
166    /// A status code on success (typically 0 if supported), or an error if
167    /// the ioctl fails or the lock is poisoned.
168    pub fn set_alarm(&self, wake_time: DateTime<Utc>) -> Result<i32, Error> {
169        let rwa = RtcWkalrm {
170            enabled: 1,
171            pending: 0,
172            time: wake_time.into(),
173        };
174        let file = self
175            .0
176            .lock()
177            .map_err(|e| anyhow::anyhow!("lock poisoned: {}", e))?;
178        unsafe { rtc_write_alarm(file.as_raw_fd(), &rwa).map_err(|e| e.into()) }
179    }
180
181    /// Disables the hardware alarm.
182    ///
183    /// Clears any pending alarm without affecting the alarm time itself.
184    ///
185    /// # Returns
186    ///
187    /// A status code on success (typically 0 if supported), or an error if
188    /// the ioctl fails or the lock is poisoned.
189    pub fn disable_alarm(&self) -> Result<i32, Error> {
190        let file = self
191            .0
192            .lock()
193            .map_err(|e| anyhow::anyhow!("lock poisoned: {}", e))?;
194        unsafe { rtc_disable_alarm(file.as_raw_fd()).map_err(|e| e.into()) }
195    }
196
197    /// Reads the current time from the hardware RTC.
198    ///
199    /// # Returns
200    ///
201    /// The current UTC time on success, or an error if the ioctl fails,
202    /// the RTC fields are invalid, or the lock is poisoned.
203    ///
204    /// # Example
205    ///
206    /// ```no_run
207    /// # use cadmus_core::rtc::Rtc;
208    /// # let rtc = Rtc::new("/dev/rtc0")?;
209    /// let now = rtc.read_time()?;
210    /// println!("RTC time: {}", now);
211    /// # Ok::<(), Box<dyn std::error::Error>>(())
212    /// ```
213    pub fn read_time(&self) -> Result<DateTime<Utc>, Error> {
214        let mut rt = unsafe { mem::zeroed::<RtcTime>() };
215        let file = self
216            .0
217            .lock()
218            .map_err(|e| anyhow::anyhow!("lock poisoned: {}", e))?;
219        unsafe {
220            rtc_read_time(file.as_raw_fd(), &mut rt)?;
221        }
222        rt.try_into()
223    }
224
225    /// Sets the hardware RTC to the specified time.
226    ///
227    /// Updates the RTC with a new UTC time. This typically requires elevated privileges.
228    ///
229    /// # Arguments
230    ///
231    /// * `time` - The UTC time to set
232    ///
233    /// # Returns
234    ///
235    /// Success with no value, or an error if the ioctl fails or the lock is poisoned.
236    ///
237    /// # Example
238    ///
239    /// ```no_run
240    /// # use cadmus_core::rtc::Rtc;
241    /// # use chrono::Utc;
242    /// # let rtc = Rtc::new("/dev/rtc0")?;
243    /// rtc.set_time(Utc::now())?;
244    /// # Ok::<(), Box<dyn std::error::Error>>(())
245    /// ```
246    pub fn set_time(&self, time: DateTime<Utc>) -> Result<(), Error> {
247        let rt: RtcTime = time.into();
248        let file = self
249            .0
250            .lock()
251            .map_err(|e| anyhow::anyhow!("lock poisoned: {}", e))?;
252        unsafe {
253            rtc_set_time(file.as_raw_fd(), &rt)?;
254        }
255        Ok(())
256    }
257}
258
259/// Identifies a logical alarm managed by [`AlarmManager`].
260#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
261pub enum AlarmType {
262    AutoPowerOff,
263    CalendarUpdate,
264}
265
266/// Describes what [`AlarmManager::ensure_scheduled`] should do when an alarm
267/// exists in the map but its wake time is already in the past.
268#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub enum PastDueAction {
270    /// Cancel the stale alarm and reschedule it for `now + duration`.
271    Reschedule,
272    /// Cancel the stale alarm and return [`EnsureAlarmOutcome::PastDue`]
273    /// so the caller can decide what to do.
274    Cancel,
275}
276
277/// The outcome of an [`AlarmManager::ensure_scheduled`] call.
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum EnsureAlarmOutcome {
280    /// No alarm of this type existed; one was freshly scheduled.
281    Scheduled,
282    /// An alarm of this type already existed and its wake time is in the future.
283    AlreadyScheduled,
284    /// An alarm of this type existed but was past-due; it has been cancelled.
285    ///
286    /// Only returned when [`PastDueAction::Cancel`] was requested. When
287    /// [`PastDueAction::Reschedule`] is requested the stale alarm is replaced
288    /// and [`EnsureAlarmOutcome::Scheduled`] is returned instead.
289    PastDue,
290}
291
292impl AlarmType {
293    pub fn alarms_to_cancel_after_resume() -> [Self; 2] {
294        [Self::AutoPowerOff, Self::CalendarUpdate]
295    }
296}
297
298pub struct ScheduledAlarm {
299    pub alarm_type: AlarmType,
300    pub wake_time: DateTime<Utc>,
301}
302
303/// Multiplexes multiple logical alarms onto a single hardware RTC alarm.
304///
305/// The hardware RTC supports only one wake alarm at a time. `AlarmManager`
306/// maintains a map of logical alarms keyed by [`AlarmType`] and always
307/// programs the hardware with the earliest upcoming wake time. After each
308/// wake, [`AlarmManager::check_fired_alarms`] determines which logical alarms fired and
309/// reschedules the hardware for any remaining ones.
310pub struct AlarmManager {
311    rtc: Rtc,
312    scheduled_alarms: BTreeMap<AlarmType, ScheduledAlarm>,
313}
314
315impl AlarmManager {
316    pub fn new(rtc: Rtc) -> Self {
317        AlarmManager {
318            rtc,
319            scheduled_alarms: BTreeMap::new(),
320        }
321    }
322
323    /// Schedule a logical alarm to fire `duration` from now.
324    ///
325    /// If an alarm of the same type is already scheduled it is replaced.
326    /// The hardware RTC is updated to reflect the new earliest wake time.
327    pub fn schedule_alarm(
328        &mut self,
329        alarm_type: AlarmType,
330        duration: Duration,
331    ) -> Result<(), Error> {
332        let wake_time = Utc::now() + duration;
333        self.scheduled_alarms.insert(
334            alarm_type,
335            ScheduledAlarm {
336                alarm_type,
337                wake_time,
338            },
339        );
340        self.update_hardware_alarm()?;
341        Ok(())
342    }
343
344    /// Cancel a previously scheduled logical alarm.
345    ///
346    /// If no alarm of that type is scheduled this is a no-op. The hardware
347    /// RTC is updated to reflect the new earliest remaining wake time.
348    pub fn cancel_alarm(&mut self, alarm_type: AlarmType) -> Result<(), Error> {
349        self.scheduled_alarms.remove(&alarm_type);
350        self.update_hardware_alarm()?;
351        Ok(())
352    }
353
354    /// Returns `true` if an alarm of `alarm_type` is scheduled for a future time.
355    pub fn is_alarm_scheduled(&self, alarm_type: AlarmType) -> bool {
356        self.scheduled_alarms
357            .get(&alarm_type)
358            .map(|alarm| alarm.wake_time > Utc::now())
359            .unwrap_or(false)
360    }
361
362    /// Returns `true` if an alarm of `alarm_type` exists in the schedule.
363    pub fn has_alarm(&self, alarm_type: AlarmType) -> bool {
364        self.scheduled_alarms.contains_key(&alarm_type)
365    }
366
367    /// Ensures an alarm of `alarm_type` is active and scheduled for the future.
368    ///
369    /// - If no alarm exists, one is scheduled for `now + duration`.
370    /// - If an alarm exists and is in the future, nothing changes.
371    /// - If an alarm exists but is past-due, the stale entry is always
372    ///   cancelled. `past_due_action` then controls whether a fresh alarm is
373    ///   scheduled: [`PastDueAction::Reschedule`] schedules a new one and
374    ///   returns [`EnsureAlarmOutcome::Scheduled`]; [`PastDueAction::Cancel`]
375    ///   stops there and returns [`EnsureAlarmOutcome::PastDue`] so the caller
376    ///   can decide what action to take.
377    pub fn ensure_scheduled(
378        &mut self,
379        alarm_type: AlarmType,
380        duration: Duration,
381        past_due_action: PastDueAction,
382    ) -> Result<EnsureAlarmOutcome, Error> {
383        if !self.has_alarm(alarm_type) {
384            self.schedule_alarm(alarm_type, duration)?;
385            return Ok(EnsureAlarmOutcome::Scheduled);
386        }
387
388        if self.is_alarm_scheduled(alarm_type) {
389            return Ok(EnsureAlarmOutcome::AlreadyScheduled);
390        }
391
392        self.cancel_alarm(alarm_type)?;
393
394        match past_due_action {
395            PastDueAction::Reschedule => {
396                self.schedule_alarm(alarm_type, duration)?;
397                Ok(EnsureAlarmOutcome::Scheduled)
398            }
399            PastDueAction::Cancel => Ok(EnsureAlarmOutcome::PastDue),
400        }
401    }
402
403    /// Returns the number of seconds until `alarm_type` fires, or `None` if
404    /// it is not scheduled.
405    pub fn time_until_alarm(&self, alarm_type: AlarmType) -> Option<i64> {
406        self.scheduled_alarms.get(&alarm_type).map(|alarm| {
407            alarm
408                .wake_time
409                .signed_duration_since(Utc::now())
410                .num_seconds()
411        })
412    }
413
414    /// Determines which logical alarms fired during the last sleep cycle.
415    ///
416    /// `before` is the timestamp just before the device went to sleep and
417    /// `after` is the timestamp just after it woke. A hardware alarm is
418    /// considered fired when it is disabled or when the sleep duration is
419    /// within 3 seconds of the expected wake time (accounting for RTC
420    /// granularity). Any fired logical alarms are removed from the schedule
421    /// and the hardware is reprogrammed for the next earliest alarm.
422    pub fn check_fired_alarms(
423        &mut self,
424        before: DateTime<Utc>,
425        after: DateTime<Utc>,
426    ) -> Result<Vec<AlarmType>, Error> {
427        let mut fired_types = Vec::new();
428
429        if let Some((_, earliest_alarm)) = self
430            .scheduled_alarms
431            .iter()
432            .min_by_key(|(_, alarm)| &alarm.wake_time)
433        {
434            let expected_duration = earliest_alarm.wake_time.signed_duration_since(before);
435
436            let rwa = self.rtc.alarm()?;
437            let hardware_alarm_fired = !rwa.enabled()
438                || (rwa.year() <= 1970
439                    && ((after - before) - expected_duration).num_seconds().abs() < 3);
440
441            if hardware_alarm_fired {
442                let mut removed: Vec<(AlarmType, ScheduledAlarm)> = Vec::new();
443
444                for (alarm_type, scheduled_alarm) in &self.scheduled_alarms {
445                    if (after - scheduled_alarm.wake_time).abs().num_milliseconds() <= 3000 {
446                        fired_types.push(*alarm_type);
447                        removed.push((
448                            *alarm_type,
449                            ScheduledAlarm {
450                                alarm_type: scheduled_alarm.alarm_type,
451                                wake_time: scheduled_alarm.wake_time,
452                            },
453                        ));
454                    }
455                }
456
457                for (alarm_type, _) in &removed {
458                    self.scheduled_alarms.remove(alarm_type);
459                }
460
461                if let Err(e) = self.update_hardware_alarm() {
462                    for (alarm_type, alarm) in removed {
463                        self.scheduled_alarms.insert(alarm_type, alarm);
464                    }
465                    return Err(e);
466                }
467
468                return Ok(fired_types);
469            }
470        }
471
472        self.update_hardware_alarm()?;
473        Ok(fired_types)
474    }
475
476    fn update_hardware_alarm(&self) -> Result<(), Error> {
477        let now = Utc::now();
478
479        if let Some((_, earliest_alarm)) = self
480            .scheduled_alarms
481            .iter()
482            .filter(|(_, alarm)| alarm.wake_time > now)
483            .min_by_key(|(_, alarm)| &alarm.wake_time)
484        {
485            self.rtc.set_alarm(earliest_alarm.wake_time)?;
486        } else {
487            self.rtc.disable_alarm()?;
488        }
489
490        Ok(())
491    }
492}