Skip to main content

cadmus_core/
rtc.rs

1use anyhow::Error;
2use chrono::{DateTime, Datelike, Duration, Timelike, Utc};
3use nix::{ioctl_none, ioctl_read, ioctl_write_ptr};
4use std::collections::BTreeMap;
5use std::fs::File;
6use std::mem;
7use std::os::unix::io::AsRawFd;
8use std::path::Path;
9
10ioctl_read!(rtc_read_alarm, b'p', 0x10, RtcWkalrm);
11ioctl_write_ptr!(rtc_write_alarm, b'p', 0x0f, RtcWkalrm);
12ioctl_none!(rtc_disable_alarm, b'p', 0x02);
13
14#[repr(C)]
15#[derive(Debug, Clone)]
16pub struct RtcTime {
17    tm_sec: libc::c_int,
18    tm_min: libc::c_int,
19    tm_hour: libc::c_int,
20    tm_mday: libc::c_int,
21    tm_mon: libc::c_int,
22    tm_year: libc::c_int,
23    tm_wday: libc::c_int,
24    tm_yday: libc::c_int,
25    tm_isdst: libc::c_int,
26}
27
28impl Default for RtcWkalrm {
29    fn default() -> Self {
30        unsafe { mem::zeroed() }
31    }
32}
33
34#[repr(C)]
35#[derive(Debug, Clone)]
36pub struct RtcWkalrm {
37    enabled: libc::c_uchar,
38    pending: libc::c_uchar,
39    time: RtcTime,
40}
41
42impl RtcTime {
43    fn year(&self) -> i32 {
44        1900 + self.tm_year as i32
45    }
46}
47
48impl RtcWkalrm {
49    pub fn enabled(&self) -> bool {
50        self.enabled == 1
51    }
52
53    pub fn year(&self) -> i32 {
54        self.time.year()
55    }
56}
57
58pub struct Rtc(File);
59
60impl Rtc {
61    pub fn new<P: AsRef<Path>>(path: P) -> Result<Rtc, Error> {
62        let file = File::open(path)?;
63        Ok(Rtc(file))
64    }
65
66    pub fn alarm(&self) -> Result<RtcWkalrm, Error> {
67        let mut rwa = RtcWkalrm::default();
68        unsafe {
69            rtc_read_alarm(self.0.as_raw_fd(), &mut rwa)
70                .map(|_| rwa)
71                .map_err(|e| e.into())
72        }
73    }
74
75    pub fn set_alarm(&self, wake_time: DateTime<Utc>) -> Result<i32, Error> {
76        let rwa = RtcWkalrm {
77            enabled: 1,
78            pending: 0,
79            time: RtcTime {
80                tm_sec: wake_time.second() as libc::c_int,
81                tm_min: wake_time.minute() as libc::c_int,
82                tm_hour: wake_time.hour() as libc::c_int,
83                tm_mday: wake_time.day() as libc::c_int,
84                tm_mon: wake_time.month0() as libc::c_int,
85                tm_year: (wake_time.year() - 1900) as libc::c_int,
86                tm_wday: -1,
87                tm_yday: -1,
88                tm_isdst: -1,
89            },
90        };
91        unsafe { rtc_write_alarm(self.0.as_raw_fd(), &rwa).map_err(|e| e.into()) }
92    }
93
94    pub fn disable_alarm(&self) -> Result<i32, Error> {
95        unsafe { rtc_disable_alarm(self.0.as_raw_fd()).map_err(|e| e.into()) }
96    }
97}
98
99/// Identifies a logical alarm managed by [`AlarmManager`].
100#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
101pub enum AlarmType {
102    AutoPowerOff,
103    CalendarUpdate,
104}
105
106/// Describes what [`AlarmManager::ensure_scheduled`] should do when an alarm
107/// exists in the map but its wake time is already in the past.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum PastDueAction {
110    /// Cancel the stale alarm and reschedule it for `now + duration`.
111    Reschedule,
112    /// Cancel the stale alarm and return [`EnsureAlarmOutcome::PastDue`]
113    /// so the caller can decide what to do.
114    Cancel,
115}
116
117/// The outcome of an [`AlarmManager::ensure_scheduled`] call.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum EnsureAlarmOutcome {
120    /// No alarm of this type existed; one was freshly scheduled.
121    Scheduled,
122    /// An alarm of this type already existed and its wake time is in the future.
123    AlreadyScheduled,
124    /// An alarm of this type existed but was past-due; it has been cancelled.
125    ///
126    /// Only returned when [`PastDueAction::Cancel`] was requested. When
127    /// [`PastDueAction::Reschedule`] is requested the stale alarm is replaced
128    /// and [`EnsureAlarmOutcome::Scheduled`] is returned instead.
129    PastDue,
130}
131
132impl AlarmType {
133    pub fn alarms_to_cancel_after_resume() -> [Self; 2] {
134        [Self::AutoPowerOff, Self::CalendarUpdate]
135    }
136}
137
138pub struct ScheduledAlarm {
139    pub alarm_type: AlarmType,
140    pub wake_time: DateTime<Utc>,
141}
142
143/// Multiplexes multiple logical alarms onto a single hardware RTC alarm.
144///
145/// The hardware RTC supports only one wake alarm at a time. `AlarmManager`
146/// maintains a map of logical alarms keyed by [`AlarmType`] and always
147/// programs the hardware with the earliest upcoming wake time. After each
148/// wake, [`AlarmManager::check_fired_alarms`] determines which logical alarms fired and
149/// reschedules the hardware for any remaining ones.
150pub struct AlarmManager {
151    rtc: Rtc,
152    scheduled_alarms: BTreeMap<AlarmType, ScheduledAlarm>,
153}
154
155impl AlarmManager {
156    pub fn new(rtc: Rtc) -> Self {
157        AlarmManager {
158            rtc,
159            scheduled_alarms: BTreeMap::new(),
160        }
161    }
162
163    /// Schedule a logical alarm to fire `duration` from now.
164    ///
165    /// If an alarm of the same type is already scheduled it is replaced.
166    /// The hardware RTC is updated to reflect the new earliest wake time.
167    pub fn schedule_alarm(
168        &mut self,
169        alarm_type: AlarmType,
170        duration: Duration,
171    ) -> Result<(), Error> {
172        let wake_time = Utc::now() + duration;
173        self.scheduled_alarms.insert(
174            alarm_type,
175            ScheduledAlarm {
176                alarm_type,
177                wake_time,
178            },
179        );
180        self.update_hardware_alarm()?;
181        Ok(())
182    }
183
184    /// Cancel a previously scheduled logical alarm.
185    ///
186    /// If no alarm of that type is scheduled this is a no-op. The hardware
187    /// RTC is updated to reflect the new earliest remaining wake time.
188    pub fn cancel_alarm(&mut self, alarm_type: AlarmType) -> Result<(), Error> {
189        self.scheduled_alarms.remove(&alarm_type);
190        self.update_hardware_alarm()?;
191        Ok(())
192    }
193
194    /// Returns `true` if an alarm of `alarm_type` is scheduled for a future time.
195    pub fn is_alarm_scheduled(&self, alarm_type: AlarmType) -> bool {
196        self.scheduled_alarms
197            .get(&alarm_type)
198            .map(|alarm| alarm.wake_time > Utc::now())
199            .unwrap_or(false)
200    }
201
202    /// Returns `true` if an alarm of `alarm_type` exists in the schedule.
203    pub fn has_alarm(&self, alarm_type: AlarmType) -> bool {
204        self.scheduled_alarms.contains_key(&alarm_type)
205    }
206
207    /// Ensures an alarm of `alarm_type` is active and scheduled for the future.
208    ///
209    /// - If no alarm exists, one is scheduled for `now + duration`.
210    /// - If an alarm exists and is in the future, nothing changes.
211    /// - If an alarm exists but is past-due, the stale entry is always
212    ///   cancelled. `past_due_action` then controls whether a fresh alarm is
213    ///   scheduled: [`PastDueAction::Reschedule`] schedules a new one and
214    ///   returns [`EnsureAlarmOutcome::Scheduled`]; [`PastDueAction::Cancel`]
215    ///   stops there and returns [`EnsureAlarmOutcome::PastDue`] so the caller
216    ///   can decide what action to take.
217    pub fn ensure_scheduled(
218        &mut self,
219        alarm_type: AlarmType,
220        duration: Duration,
221        past_due_action: PastDueAction,
222    ) -> Result<EnsureAlarmOutcome, Error> {
223        if !self.has_alarm(alarm_type) {
224            self.schedule_alarm(alarm_type, duration)?;
225            return Ok(EnsureAlarmOutcome::Scheduled);
226        }
227
228        if self.is_alarm_scheduled(alarm_type) {
229            return Ok(EnsureAlarmOutcome::AlreadyScheduled);
230        }
231
232        self.cancel_alarm(alarm_type)?;
233
234        match past_due_action {
235            PastDueAction::Reschedule => {
236                self.schedule_alarm(alarm_type, duration)?;
237                Ok(EnsureAlarmOutcome::Scheduled)
238            }
239            PastDueAction::Cancel => Ok(EnsureAlarmOutcome::PastDue),
240        }
241    }
242
243    /// Returns the number of seconds until `alarm_type` fires, or `None` if
244    /// it is not scheduled.
245    pub fn time_until_alarm(&self, alarm_type: AlarmType) -> Option<i64> {
246        self.scheduled_alarms.get(&alarm_type).map(|alarm| {
247            alarm
248                .wake_time
249                .signed_duration_since(Utc::now())
250                .num_seconds()
251        })
252    }
253
254    /// Determines which logical alarms fired during the last sleep cycle.
255    ///
256    /// `before` is the timestamp just before the device went to sleep and
257    /// `after` is the timestamp just after it woke. A hardware alarm is
258    /// considered fired when it is disabled or when the sleep duration is
259    /// within 3 seconds of the expected wake time (accounting for RTC
260    /// granularity). Any fired logical alarms are removed from the schedule
261    /// and the hardware is reprogrammed for the next earliest alarm.
262    pub fn check_fired_alarms(
263        &mut self,
264        before: DateTime<Utc>,
265        after: DateTime<Utc>,
266    ) -> Result<Vec<AlarmType>, Error> {
267        let mut fired_types = Vec::new();
268
269        if let Some((_, earliest_alarm)) = self
270            .scheduled_alarms
271            .iter()
272            .min_by_key(|(_, alarm)| &alarm.wake_time)
273        {
274            let expected_duration = earliest_alarm.wake_time.signed_duration_since(before);
275
276            let rwa = self.rtc.alarm()?;
277            let hardware_alarm_fired = !rwa.enabled()
278                || (rwa.year() <= 1970
279                    && ((after - before) - expected_duration).num_seconds().abs() < 3);
280
281            if hardware_alarm_fired {
282                let mut removed: Vec<(AlarmType, ScheduledAlarm)> = Vec::new();
283
284                for (alarm_type, scheduled_alarm) in &self.scheduled_alarms {
285                    if (after - scheduled_alarm.wake_time).abs().num_milliseconds() <= 3000 {
286                        fired_types.push(*alarm_type);
287                        removed.push((
288                            *alarm_type,
289                            ScheduledAlarm {
290                                alarm_type: scheduled_alarm.alarm_type,
291                                wake_time: scheduled_alarm.wake_time,
292                            },
293                        ));
294                    }
295                }
296
297                for (alarm_type, _) in &removed {
298                    self.scheduled_alarms.remove(alarm_type);
299                }
300
301                if let Err(e) = self.update_hardware_alarm() {
302                    for (alarm_type, alarm) in removed {
303                        self.scheduled_alarms.insert(alarm_type, alarm);
304                    }
305                    return Err(e);
306                }
307
308                return Ok(fired_types);
309            }
310        }
311
312        self.update_hardware_alarm()?;
313        Ok(fired_types)
314    }
315
316    fn update_hardware_alarm(&self) -> Result<(), Error> {
317        let now = Utc::now();
318
319        if let Some((_, earliest_alarm)) = self
320            .scheduled_alarms
321            .iter()
322            .filter(|(_, alarm)| alarm.wake_time > now)
323            .min_by_key(|(_, alarm)| &alarm.wake_time)
324        {
325            self.rtc.set_alarm(earliest_alarm.wake_time)?;
326        } else {
327            self.rtc.disable_alarm()?;
328        }
329
330        Ok(())
331    }
332}