From 5ba0421f76fdb022e9c81968c90bed9351ce3334 Mon Sep 17 00:00:00 2001 From: Chaos Rogers Date: Tue, 21 Oct 2025 22:41:19 +0100 Subject: [PATCH] feat: add PTP support with PHC probe Co-authored-by: aider (openai/gpt-5) --- Cargo.toml | 4 ++ src/lib.rs | 1 + src/ptp.rs | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 src/lib.rs create mode 100644 src/ptp.rs diff --git a/Cargo.toml b/Cargo.toml index 1d38d1c..d04e482 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,9 @@ log = { version = "0.4", features = ["std"] } daemonize = "0.5.0" num-rational = "0.4" num-traits = "0.2" +libc = "0.2" +[[bin]] +name = "ptp_probe" +path = "src/bin/ptp_probe.rs" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d9c2650 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1 @@ +pub mod ptp; diff --git a/src/ptp.rs b/src/ptp.rs new file mode 100644 index 0000000..df36ae9 --- /dev/null +++ b/src/ptp.rs @@ -0,0 +1,123 @@ +use chrono::{DateTime, TimeZone, Utc}; + +#[derive(Debug)] +pub enum PtpError { + Io(std::io::Error), + ChronoOutOfRange, + Unsupported, +} + +impl std::fmt::Display for PtpError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PtpError::Io(e) => write!(f, "I/O error: {}", e), + PtpError::ChronoOutOfRange => write!(f, "Chrono out-of-range error"), + PtpError::Unsupported => write!(f, "PTP is unsupported on this platform"), + } + } +} + +impl std::error::Error for PtpError {} + +impl From for PtpError { + fn from(e: std::io::Error) -> Self { + PtpError::Io(e) + } +} + +#[cfg(target_os = "linux")] +mod linux { + use super::{DateTime, PtpError, TimeZone, Utc}; + use libc::{clockid_t, timespec, CLOCK_REALTIME}; + use std::fs::File; + use std::os::fd::AsRawFd; + + pub struct PtpClock { + file: File, + } + + impl PtpClock { + pub fn open(path: &str) -> Result { + Ok(Self { file: File::open(path)? }) + } + + #[inline] + fn fd_to_clockid(fd: i32) -> clockid_t { + // Linux CLOCKFD encoding: + // #define CLOCKFD 3 + // #define FD_TO_CLOCKID(fd) ((~(clockid_t)(fd) << 3) | CLOCKFD) + ((!(fd as clockid_t)) << 3) | 3 + } + + fn read_timespec(&self) -> Result { + let mut ts = timespec { tv_sec: 0, tv_nsec: 0 }; + let clk_id = Self::fd_to_clockid(self.file.as_raw_fd()); + let rc = unsafe { libc::clock_gettime(clk_id, &mut ts as *mut timespec) }; + if rc != 0 { + return Err(PtpError::Io(std::io::Error::last_os_error())); + } + Ok(ts) + } + + pub fn now_datetime(&self) -> Result, PtpError> { + let ts = self.read_timespec()?; + let dt = Utc + .timestamp_opt(ts.tv_sec as i64, ts.tv_nsec as u32) + .single() + .ok_or(PtpError::ChronoOutOfRange)?; + Ok(dt) + } + + pub fn offset_from_system_ns(&self) -> Result { + // PTP Hardware Clock (PHC) + let phc = self.read_timespec()?; + // System realtime clock + let mut sys_ts = timespec { tv_sec: 0, tv_nsec: 0 }; + let rc = unsafe { libc::clock_gettime(CLOCK_REALTIME, &mut sys_ts as *mut timespec) }; + if rc != 0 { + return Err(PtpError::Io(std::io::Error::last_os_error())); + } + + let phc_ns = (phc.tv_sec as i128) * 1_000_000_000 + (phc.tv_nsec as i128); + let sys_ns = (sys_ts.tv_sec as i128) * 1_000_000_000 + (sys_ts.tv_nsec as i128); + Ok(phc_ns - sys_ns) + } + } + + pub fn is_supported() -> bool { + true + } + + pub use PtpClock; +} + +#[cfg(not(target_os = "linux"))] +mod non_linux { + use super::{DateTime, PtpError, Utc}; + + pub struct PtpClock; + + impl PtpClock { + pub fn open(_path: &str) -> Result { + Err(PtpError::Unsupported) + } + pub fn now_datetime(&self) -> Result, PtpError> { + Err(PtpError::Unsupported) + } + pub fn offset_from_system_ns(&self) -> Result { + Err(PtpError::Unsupported) + } + } + + pub fn is_supported() -> bool { + false + } + + pub use PtpClock; +} + +#[cfg(target_os = "linux")] +pub use linux::{is_supported, PtpClock}; + +#[cfg(not(target_os = "linux"))] +pub use non_linux::{is_supported, PtpClock};