Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,9 @@ rust-version = "1.66"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[[example]]
name = "basic"
required-features = []

[dependencies]
libc = "0.2.165"
85 changes: 85 additions & 0 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! Demonstrates the clock-steering library.
//!
//! Usage: cargo run --example basic [realtime|tai|/dev/ptpN]
//!
//! Write operations (frequency, step, leap seconds, TAI) require root privileges.

use clock_steering::{unix::UnixClock, Clock, LeapIndicator, TimeOffset};
use std::time::Duration;

fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(target_os = "linux")]
let arg = std::env::args().nth(1);

#[cfg(target_os = "linux")]
let clock: UnixClock = match arg.as_deref() {
None | Some("realtime") => UnixClock::CLOCK_REALTIME,
Some("tai") => UnixClock::CLOCK_TAI,
Some(path) if path.starts_with("/dev/") => UnixClock::open(path)?,
Some(other) => {
eprintln!("unknown clock: {other}");
eprintln!("usage: basic [realtime|tai|/dev/ptpN]");
std::process::exit(1);
}
};

#[cfg(not(target_os = "linux"))]
let clock = UnixClock::CLOCK_REALTIME;

// Read-only operations

let now = clock.now()?;
println!("now: {}.{:09}", now.seconds, now.nanos);

let res = clock.resolution()?;
println!("resolution: {}ns", res.nanos);

let caps = clock.capabilities()?;
println!("max freq: {} ppm", caps.max_frequency_adjustment_ppm);
println!("max offset: {}ns", caps.max_offset_adjustment_ns);

match clock.get_frequency() {
Ok(f) => println!("frequency: {f:.6} ms/s"),
Err(e) => println!("frequency: {e}"),
}

#[cfg(target_os = "linux")]
match clock.get_tai() {
Ok(tai) => println!("TAI offset: {tai}s"),
Err(e) => println!("TAI offset: {e}"),
}

// Write operations — require root

println!();

match clock.set_frequency(0.0) {
Ok(t) => println!("set_frequency(0.0): ok at {}.{:09}", t.seconds, t.nanos),
Err(e) => println!("set_frequency(0.0): {e}"),
}

match clock.step_clock(TimeOffset {
seconds: 0,
nanos: 0,
}) {
Ok(t) => println!("step_clock(0): ok at {}.{:09}", t.seconds, t.nanos),
Err(e) => println!("step_clock(0): {e}"),
}

match clock.set_leap_seconds(LeapIndicator::NoWarning) {
Ok(()) => println!("set_leap_seconds: ok"),
Err(e) => println!("set_leap_seconds: {e}"),
}

match clock.error_estimate_update(Duration::from_micros(100), Duration::from_millis(1)) {
Ok(()) => println!("error_estimate_update: ok"),
Err(e) => println!("error_estimate_update: {e}"),
}

match clock.disable_kernel_ntp_algorithm() {
Ok(()) => println!("disable_kernel_ntp: ok"),
Err(e) => println!("disable_kernel_ntp: {e}"),
}

Ok(())
}
32 changes: 32 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
//! This code is used in our implementations of NTP [ntpd-rs](https://github.com/pendulum-project/ntpd-rs) and PTP [statime](https://github.com/pendulum-project/statime).
use core::time::Duration;

#[cfg(target_os = "linux")]
mod linux_ioctls;

#[cfg(unix)]
pub mod unix;

Expand All @@ -17,6 +20,30 @@ pub struct Timestamp {
pub nanos: u32,
}

/// Clock adjustment capabilities
///
/// Describes the capabilities of a clock for frequency and offset adjustment.
/// Values are specified in parts-per-million (ppm) for frequency and nanoseconds for offset.
/// For realtime clocks, the values are hard-coded in the OS kernel.
/// For PTP clocks, the values are determined by the PTP hardware.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ClockCapabilities {
/// Maximum frequency adjustment capability in parts per million.
pub max_frequency_adjustment_ppm: f64,

/// Maximum offset adjustment capability in nanoseconds.
pub max_offset_adjustment_ns: u32,
}

impl Default for ClockCapabilities {
fn default() -> Self {
Self {
max_frequency_adjustment_ppm: 500.0, // 500 ppm
max_offset_adjustment_ns: 500_000_000, // 0.5 seconds
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct TimeOffset {
pub seconds: libc::time_t,
Expand Down Expand Up @@ -54,6 +81,11 @@ pub trait Clock {
/// unavailable.
fn resolution(&self) -> Result<Timestamp, Self::Error>;

/// Get the clock's adjustment capabilities.
///
/// This returns information about the clock's capabilities.
fn capabilities(&self) -> Result<ClockCapabilities, Self::Error>;

/// Change the frequency of the clock.
/// Returns the time at which the change was applied.
///
Expand Down
37 changes: 37 additions & 0 deletions src/linux_ioctls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! Linux ioctl definitions for PTP clock devices.
//!
//! These definitions are derived from <linux/ptp_clock.h>.

use std::os::unix::io::RawFd;

/// PTP clock capabilities as reported by the kernel.
#[repr(C)]
pub struct PtpClockCaps {
pub max_adj: libc::c_int, // Maximum frequency adjustment in parts per billion
pub n_alarm: libc::c_int, // Number of programmable alarms
pub n_ext_ts: libc::c_int, // Number of external time stamp channels
pub n_per_out: libc::c_int, // Number of programmable periodic signals
pub pps: libc::c_int, // Whether the clock supports a PPS callback
pub n_pins: libc::c_int, // Number of input/output pins
pub cross_timestamping: libc::c_int, // Whether the clock supports precise system-device cross timestamps
pub adjust_phase: libc::c_int, // Whether the clock supports adjust phase
pub max_phase_adj: libc::c_int, // Maximum offset adjustment in nanoseconds
pub rsv: [libc::c_int; 11], // Reserved for future use
}

// PTP_CLOCK_GETCAPS = _IOR('=', 1, struct ptp_clock_caps)
//
// Linux _IOR encoding: (IOC_READ << 30) | (size << 16) | (type << 8) | nr
// IOC_READ = 2, type = b'=' = 0x3D, nr = 1
const PTP_CLOCK_GETCAPS: u32 =
(2u32 << 30) | ((std::mem::size_of::<PtpClockCaps>() as u32) << 16) | ((b'=' as u32) << 8) | 1;
Comment on lines +8 to +27
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to self: this needs upstreaming to libc.


/// Query PTP clock capabilities via ioctl.
///
/// Returns 0 on success, -1 on error (check `errno` for details).
///
/// # Safety
/// `caps` must be a valid, writable pointer to a `PtpClockCaps`.
pub unsafe fn ptp_clock_getcaps(fd: RawFd, caps: *mut PtpClockCaps) -> libc::c_int {
libc::ioctl(fd, PTP_CLOCK_GETCAPS as _, caps)
}
54 changes: 52 additions & 2 deletions src/unix.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use crate::{Clock, LeapIndicator, TimeOffset, Timestamp};
#[cfg(target_os = "linux")]
use crate::linux_ioctls::{ptp_clock_getcaps, PtpClockCaps};
use crate::{Clock, ClockCapabilities, LeapIndicator, TimeOffset, Timestamp};
#[cfg(target_os = "linux")]
use std::mem::MaybeUninit;
use std::time::Duration;
#[cfg(target_os = "linux")]
use std::{
Expand Down Expand Up @@ -77,7 +81,9 @@ impl UnixClock {
.open(path)?;

// we need an owned fd. the file will be closed when the process exits.
Ok(Self::safe_from_raw_fd(file.into_raw_fd()))
let clock = Self::safe_from_raw_fd(file.into_raw_fd());

Ok(clock)
}

// Consume an fd and produce a clock id. Clock id is only valid
Expand Down Expand Up @@ -368,6 +374,32 @@ impl UnixClock {
}
}

impl UnixClock {
#[cfg(target_os = "linux")]
fn detect_ptp_capabilities(&self) -> Result<ClockCapabilities, Error> {
if let Some(fd) = self.fd {
let mut caps = MaybeUninit::<PtpClockCaps>::zeroed();

if unsafe { ptp_clock_getcaps(fd, caps.as_mut_ptr()) } == 0 {
let caps = unsafe { caps.assume_init() };

Ok(ClockCapabilities {
max_frequency_adjustment_ppm: caps.max_adj as f64 / 1000.0,
max_offset_adjustment_ns: caps.max_phase_adj as u32,
})
} else if error_number() == libc::ENOTTY {
// Not a PTP device, use system clock defaults
Ok(ClockCapabilities::default())
} else {
Err(convert_errno())
}
} else {
// System clock, use system defaults
Ok(ClockCapabilities::default())
}
}
}

impl Clock for UnixClock {
type Error = Error;

Expand All @@ -390,6 +422,17 @@ impl Clock for UnixClock {
Ok(current_time_timespec(timespec, Precision::Nano))
}

#[cfg(target_os = "linux")]
fn capabilities(&self) -> Result<ClockCapabilities, Self::Error> {
self.detect_ptp_capabilities()
}

#[cfg(not(target_os = "linux"))]
fn capabilities(&self) -> Result<ClockCapabilities, Self::Error> {
// Non-Linux systems use system clock defaults
Ok(ClockCapabilities::default())
}

fn get_frequency(&self) -> Result<f64, Self::Error> {
let mut timex = EMPTY_TIMEX;
self.adjtime(&mut timex)?;
Expand Down Expand Up @@ -817,4 +860,11 @@ mod tests {

assert_ne!(resolution, Timestamp::default());
}

#[test]
fn test_capabilities() {
let clock = UnixClock::CLOCK_REALTIME;

assert!(clock.capabilities().is_ok());
}
}
Loading