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}