1#[cfg(target_os = "linux")]
2mod linux_phc {
3 use std::os::fd::AsRawFd;
4 use std::sync::Mutex;
5
6 use nix::sys::time::TimeSpec;
7 use nix::time::ClockId;
8 use rtime_core::clock::{Clock, ClockError};
9 use rtime_core::timestamp::{NtpDuration, NtpTimestamp, PtpTimestamp};
10
11 const CLOCKFD: libc::clockid_t = 3;
17
18 fn fd_to_clockid(fd: libc::c_int) -> libc::clockid_t {
21 (!(fd as libc::clockid_t) << 3) | CLOCKFD
22 }
23
24 pub struct PhcClock {
33 file: std::fs::File,
35 clockid: libc::clockid_t,
36 device_path: String,
37 discipline_lock: Mutex<()>,
40 }
41
42 impl std::fmt::Debug for PhcClock {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("PhcClock")
45 .field("fd", &self.file.as_raw_fd())
46 .field("clockid", &self.clockid)
47 .field("device_path", &self.device_path)
48 .finish()
49 }
50 }
51
52 impl PhcClock {
53 pub fn open(device: &str) -> Result<Self, ClockError> {
58 let file = std::fs::OpenOptions::new()
59 .read(true)
60 .write(true)
61 .open(device)
62 .map_err(|err| match err.raw_os_error() {
63 Some(libc::ENOENT) | Some(libc::ENXIO) => ClockError::DeviceNotFound,
64 Some(libc::EACCES) | Some(libc::EPERM) => ClockError::PermissionDenied,
65 _ => ClockError::Os(err),
66 })?;
67
68 let clockid = fd_to_clockid(file.as_raw_fd());
69
70 Ok(Self {
71 file,
72 clockid,
73 device_path: device.to_string(),
74 discipline_lock: Mutex::new(()),
75 })
76 }
77
78 pub fn read_time(&self) -> Result<PtpTimestamp, ClockError> {
83 let ts = nix::time::clock_gettime(ClockId::from_raw(self.clockid)).map_err(|e| {
84 let err: std::io::Error = e.into();
85 match err.raw_os_error() {
86 Some(libc::EINVAL) => ClockError::DeviceNotFound,
87 _ => ClockError::Os(err),
88 }
89 })?;
90
91 Ok(PtpTimestamp::new(ts.tv_sec() as u64, ts.tv_nsec() as u32))
92 }
93
94 pub fn device_path(&self) -> &str {
96 &self.device_path
97 }
98
99 pub fn as_raw_fd(&self) -> libc::c_int {
101 self.file.as_raw_fd()
102 }
103
104 pub fn clockid(&self) -> libc::clockid_t {
106 self.clockid
107 }
108 }
109
110 impl Clock for PhcClock {
111 fn now(&self) -> Result<NtpTimestamp, ClockError> {
112 let ptp_ts = self.read_time()?;
113 Ok(ptp_ts.to_ntp_timestamp())
114 }
115
116 fn step(&self, offset: NtpDuration) -> Result<(), ClockError> {
117 let _guard = self
119 .discipline_lock
120 .lock()
121 .map_err(|_| ClockError::Os(std::io::Error::other("discipline lock poisoned")))?;
122
123 let clock = ClockId::from_raw(self.clockid);
124 let ts = nix::time::clock_gettime(clock).map_err(|e| ClockError::Os(e.into()))?;
125
126 let nanos = offset.to_nanos();
127 let total_ns = ts.tv_sec() as i64 * 1_000_000_000 + ts.tv_nsec() as i64 + nanos;
128
129 if total_ns < 0 {
130 return Err(ClockError::Os(std::io::Error::new(
131 std::io::ErrorKind::InvalidInput,
132 "step would set clock to negative time",
133 )));
134 }
135
136 let new_ts = TimeSpec::new(
137 (total_ns / 1_000_000_000) as libc::time_t,
138 (total_ns % 1_000_000_000) as libc::c_long,
139 );
140
141 nix::time::clock_settime(clock, new_ts).map_err(|e| {
142 let err: std::io::Error = e.into();
143 match err.raw_os_error() {
144 Some(libc::EPERM) => ClockError::PermissionDenied,
145 _ => ClockError::Os(err),
146 }
147 })?;
148
149 Ok(())
150 }
151
152 fn adjust_frequency(&self, ppm: f64) -> Result<(), ClockError> {
153 let _guard = self
154 .discipline_lock
155 .lock()
156 .map_err(|_| ClockError::Os(std::io::Error::other("discipline lock poisoned")))?;
157
158 let freq = (ppm * 65536.0) as i64;
160
161 let mut tx = crate::adjtime::Timex::new();
162 tx.0.modes = libc::ADJ_FREQUENCY;
163 tx.0.freq = freq;
164
165 crate::adjtime::clock_adjtime(self.clockid, &mut tx).map_err(|err| {
166 match err.raw_os_error() {
167 Some(libc::EPERM) => ClockError::PermissionDenied,
168 Some(libc::EOPNOTSUPP) => ClockError::NotSupported,
169 _ => ClockError::Os(err),
170 }
171 })?;
172
173 Ok(())
174 }
175
176 fn frequency_offset(&self) -> Result<f64, ClockError> {
177 let mut tx = crate::adjtime::Timex::new(); crate::adjtime::clock_adjtime(self.clockid, &mut tx).map_err(ClockError::Os)?;
179 Ok(tx.0.freq as f64 / 65536.0)
180 }
181
182 fn resolution(&self) -> NtpDuration {
183 NtpDuration::from_nanos(1)
185 }
186
187 fn max_frequency_adjustment(&self) -> f64 {
188 1000.0
191 }
192
193 fn is_adjustable(&self) -> bool {
194 true
196 }
197 }
198
199 #[cfg(test)]
200 mod tests {
201 use super::*;
202
203 #[test]
204 fn fd_to_clockid_formula() {
205 let cid = fd_to_clockid(0);
208 assert_eq!(cid, -5);
209
210 let cid = fd_to_clockid(3);
212 assert_eq!(cid, (-4i32 << 3) | 3);
213 assert_eq!(cid, -29);
214 }
215
216 #[test]
217 fn open_nonexistent_device() {
218 let result = PhcClock::open("/dev/ptp_nonexistent_999");
219 assert!(result.is_err());
220 match result.unwrap_err() {
221 ClockError::DeviceNotFound => {}
222 ClockError::PermissionDenied => {
223 }
225 other => panic!("expected DeviceNotFound or PermissionDenied, got: {other}"),
226 }
227 }
228
229 #[test]
230 fn open_ptp0_if_available() {
231 match PhcClock::open("/dev/ptp0") {
233 Ok(phc) => {
234 match phc.read_time() {
236 Ok(ts) => {
237 assert!(ts.seconds > 0, "PHC time should be non-zero");
238 }
239 Err(e) => {
240 eprintln!("read_time failed (may be expected): {e}");
241 }
242 }
243 }
244 Err(_) => {
245 }
247 }
248 }
249 }
250}
251
252#[cfg(target_os = "linux")]
253pub use linux_phc::PhcClock;
254
255#[cfg(not(target_os = "linux"))]
257pub struct PhcClock;
258
259#[cfg(not(target_os = "linux"))]
260impl PhcClock {
261 pub fn open(_device: &str) -> Result<Self, rtime_core::clock::ClockError> {
263 Err(rtime_core::clock::ClockError::NotSupported)
264 }
265}