use chrono::{DateTime, Local, Utc};
#[cfg(feature = "json")]
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct ClockIdentity(pub [u8; 8]);
impl ClockIdentity {
pub fn new(bytes: [u8; 8]) -> Self {
Self(bytes)
}
pub fn to_hex_string(&self) -> String {
self.0
.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join(":")
}
}
impl std::fmt::Display for ClockIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_hex_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct PortIdentity {
pub clock_identity: ClockIdentity,
pub port_number: u16,
}
impl std::fmt::Display for PortIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.clock_identity, self.port_number)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "SCREAMING_SNAKE_CASE"))]
pub enum TimeSource {
AtomicClock,
Gps,
TerrestrialRadio,
Ptp,
Ntp,
HandSet,
Other,
InternalOscillator,
}
impl std::fmt::Display for TimeSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TimeSource::AtomicClock => write!(f, "ATOMIC_CLOCK"),
TimeSource::Gps => write!(f, "GPS"),
TimeSource::TerrestrialRadio => write!(f, "TERRESTRIAL_RADIO"),
TimeSource::Ptp => write!(f, "PTP"),
TimeSource::Ntp => write!(f, "NTP"),
TimeSource::HandSet => write!(f, "HAND_SET"),
TimeSource::Other => write!(f, "OTHER"),
TimeSource::InternalOscillator => write!(f, "INTERNAL_OSCILLATOR"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct ClockQuality {
pub clock_class: u8,
pub clock_accuracy: u8,
pub offset_scaled_log_variance: u16,
}
impl ClockQuality {
pub fn accuracy_description(&self) -> &'static str {
match self.clock_accuracy {
0x20 => "within 25 ns",
0x21 => "within 100 ns",
0x22 => "within 250 ns",
0x23 => "within 1 µs",
0x24 => "within 2.5 µs",
0x25 => "within 10 µs",
0x26 => "within 25 µs",
0x27 => "within 100 µs",
0x28 => "within 250 µs",
0x29 => "within 1 ms",
0x2A => "within 2.5 ms",
0x2B => "within 10 ms",
0x2C => "within 25 ms",
0x2D => "within 100 ms",
0x2E => "within 250 ms",
0x2F => "within 1 s",
0x30 => "within 10 s",
0x31 => "> 10 s",
_ => "unknown",
}
}
pub fn class_description(&self) -> &'static str {
match self.clock_class {
6 => "Primary reference (GPS/Atomic)",
7 => "Primary reference (default)",
13 => "Application-specific time source",
14 => "Alternative PTP profile",
52 => "Degraded primary reference (holdover within spec)",
58 => "Degraded primary reference (out of holdover spec)",
187 => "Default slave-only",
248 => "Default (no external reference)",
255 => "Slave-only",
_ => "Other",
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct PtpTarget {
pub name: String,
pub ip: std::net::IpAddr,
pub domain: u8,
pub event_port: u16,
pub general_port: u16,
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct PacketStats {
pub sync_sent: u32,
pub sync_received: u32,
pub follow_up_received: u32,
pub delay_req_sent: u32,
pub delay_resp_received: u32,
pub announce_received: u32,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct PtpDiagnostics {
pub master_port_identity: PortIdentity,
pub hardware_timestamping: bool,
pub timestamp_mode: String,
pub steps_removed: u16,
pub current_utc_offset: i16,
pub current_utc_offset_valid: bool,
pub leap59: bool,
pub leap61: bool,
pub time_traceable: bool,
pub frequency_traceable: bool,
pub ptp_timescale: bool,
pub packet_stats: PacketStats,
pub measurement_duration_ms: f64,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "json", derive(Serialize))]
pub struct PtpProbeResult {
pub target: PtpTarget,
pub offset_ns: i64,
pub mean_path_delay_ns: i64,
pub master_identity: ClockIdentity,
pub clock_quality: ClockQuality,
pub time_source: TimeSource,
pub utc: DateTime<Utc>,
pub local: DateTime<Local>,
pub timestamp: i64,
pub diagnostics: Option<PtpDiagnostics>,
}
impl PtpProbeResult {
pub fn offset_ms(&self) -> f64 {
self.offset_ns as f64 / 1_000_000.0
}
pub fn mean_path_delay_ms(&self) -> f64 {
self.mean_path_delay_ns as f64 / 1_000_000.0
}
pub fn offset_us(&self) -> f64 {
self.offset_ns as f64 / 1_000.0
}
pub fn mean_path_delay_us(&self) -> f64 {
self.mean_path_delay_ns as f64 / 1_000.0
}
pub fn is_ahead(&self) -> bool {
self.offset_ns > 0
}
pub fn is_behind(&self) -> bool {
self.offset_ns < 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clock_identity_display() {
let id = ClockIdentity([0x00, 0x1B, 0x21, 0xAB, 0xCD, 0xEF, 0x00, 0x01]);
assert_eq!(id.to_hex_string(), "00:1B:21:AB:CD:EF:00:01");
}
#[test]
fn test_port_identity_display() {
let port = PortIdentity {
clock_identity: ClockIdentity([0x00, 0x1B, 0x21, 0xAB, 0xCD, 0xEF, 0x00, 0x01]),
port_number: 1,
};
assert_eq!(port.to_string(), "00:1B:21:AB:CD:EF:00:01:1");
}
#[test]
fn test_offset_conversions() {
let result = PtpProbeResult {
target: PtpTarget {
name: "test".to_string(),
ip: "127.0.0.1".parse().unwrap(),
domain: 0,
event_port: 319,
general_port: 320,
},
offset_ns: 1_500_000,
mean_path_delay_ns: 500_000,
master_identity: ClockIdentity([0; 8]),
clock_quality: ClockQuality {
clock_class: 6,
clock_accuracy: 0x20,
offset_scaled_log_variance: 0,
},
time_source: TimeSource::Gps,
utc: Utc::now(),
local: Local::now(),
timestamp: 0,
diagnostics: None,
};
assert_eq!(result.offset_ms(), 1.5);
assert_eq!(result.offset_us(), 1500.0);
assert_eq!(result.mean_path_delay_ms(), 0.5);
assert_eq!(result.mean_path_delay_us(), 500.0);
assert!(result.is_ahead());
assert!(!result.is_behind());
}
#[test]
fn test_clock_quality_descriptions() {
let quality = ClockQuality {
clock_class: 6,
clock_accuracy: 0x20,
offset_scaled_log_variance: 0,
};
assert_eq!(
quality.class_description(),
"Primary reference (GPS/Atomic)"
);
assert_eq!(quality.accuracy_description(), "within 25 ns");
}
}