#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum ClockClass {
Primary = 6,
HoldoverInSpec = 7,
HoldoverOutOfSpec = 52,
Degraded = 193,
#[default]
Default = 248,
SlaveOnly = 255,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum ClockAccuracy {
Within25ns = 0x20,
Within100ns = 0x21,
Within250ns = 0x22,
Within1us = 0x23,
Within25us = 0x25,
Within100us = 0x26,
#[default]
Unknown = 0xFE,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ClockIdentity(pub [u8; 8]);
impl ClockIdentity {
#[must_use]
pub fn new(bytes: [u8; 8]) -> Self {
Self(bytes)
}
#[must_use]
pub fn from_u64(v: u64) -> Self {
Self(v.to_be_bytes())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PortIdentity {
pub clock_identity: ClockIdentity,
pub port_number: u16,
}
impl PortIdentity {
#[must_use]
pub fn new(clock_identity: ClockIdentity, port_number: u16) -> Self {
Self {
clock_identity,
port_number,
}
}
}
#[derive(Debug, Clone)]
pub struct AnnounceMessage {
pub source_port_identity: PortIdentity,
pub steps_removed: u16,
pub grandmaster_clock_class: ClockClass,
pub grandmaster_clock_accuracy: ClockAccuracy,
pub grandmaster_offset_scaled_log_variance: u16,
pub grandmaster_priority1: u8,
pub grandmaster_priority2: u8,
pub grandmaster_identity: ClockIdentity,
pub sequence_id: u16,
pub received_at: Instant,
}
impl AnnounceMessage {
#[must_use]
pub fn new(
source_port_identity: PortIdentity,
grandmaster_identity: ClockIdentity,
clock_class: ClockClass,
clock_accuracy: ClockAccuracy,
priority1: u8,
priority2: u8,
steps_removed: u16,
sequence_id: u16,
) -> Self {
Self {
source_port_identity,
steps_removed,
grandmaster_clock_class: clock_class,
grandmaster_clock_accuracy: clock_accuracy,
grandmaster_offset_scaled_log_variance: 0x4E5D,
grandmaster_priority1: priority1,
grandmaster_priority2: priority2,
grandmaster_identity,
sequence_id,
received_at: Instant::now(),
}
}
#[must_use]
pub fn comparison_vector(&self) -> (u8, u8, u16, u8, [u8; 8], u8) {
(
self.grandmaster_clock_class as u8,
self.grandmaster_clock_accuracy as u8,
self.grandmaster_offset_scaled_log_variance,
self.grandmaster_priority1,
self.grandmaster_identity.0,
self.grandmaster_priority2,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PortState {
#[default]
Initializing,
Faulty,
Disabled,
Listening,
PreMaster,
Master,
Passive,
Uncalibrated,
Slave,
}
#[derive(Debug, Clone)]
pub struct BmcaResult {
pub best_announce: Option<AnnounceMessage>,
pub is_grandmaster: bool,
}
#[derive(Debug)]
pub struct PtpBoundaryClock {
pub clock_identity: ClockIdentity,
pub priority1: u8,
pub priority2: u8,
pub clock_class: ClockClass,
pub clock_accuracy: ClockAccuracy,
pub announce_receipt_timeout: u8,
pub port_states: HashMap<u16, PortState>,
foreign_masters: HashMap<PortIdentity, Vec<AnnounceMessage>>,
pub current_grandmaster: Option<ClockIdentity>,
}
impl PtpBoundaryClock {
#[must_use]
pub fn new(
clock_identity: ClockIdentity,
priority1: u8,
priority2: u8,
clock_class: ClockClass,
clock_accuracy: ClockAccuracy,
) -> Self {
Self {
clock_identity,
priority1,
priority2,
clock_class,
clock_accuracy,
announce_receipt_timeout: 3,
port_states: HashMap::new(),
foreign_masters: HashMap::new(),
current_grandmaster: None,
}
}
pub fn add_port(&mut self, port_number: u16) {
self.port_states.insert(port_number, PortState::Listening);
}
pub fn receive_announce(&mut self, msg: AnnounceMessage) {
let key = msg.source_port_identity;
self.foreign_masters.entry(key).or_default().push(msg);
if let Some(v) = self.foreign_masters.get_mut(&key) {
if v.len() > 4 {
v.remove(0);
}
}
}
pub fn run_bmca(&mut self) -> BmcaResult {
let timeout = Duration::from_secs(4);
let now = Instant::now();
self.foreign_masters.retain(|_, msgs| {
msgs.last()
.is_some_and(|m| now.duration_since(m.received_at) < timeout)
});
let best = self
.foreign_masters
.values()
.filter_map(|msgs| msgs.last())
.min_by_key(|m| m.comparison_vector());
if let Some(b) = best {
let gm_id = b.grandmaster_identity;
let cv_foreign = b.comparison_vector();
let local_cv = (
self.clock_class as u8,
self.clock_accuracy as u8,
0x4E5Du16,
self.priority1,
self.clock_identity.0,
self.priority2,
);
if cv_foreign < local_cv {
self.current_grandmaster = Some(gm_id);
let slave_port = b.source_port_identity.port_number;
for (port, state) in &mut self.port_states {
*state = if *port == slave_port {
PortState::Slave
} else {
PortState::Master
};
}
BmcaResult {
best_announce: Some(b.clone()),
is_grandmaster: false,
}
} else {
self.become_grandmaster();
BmcaResult {
best_announce: None,
is_grandmaster: true,
}
}
} else {
self.become_grandmaster();
BmcaResult {
best_announce: None,
is_grandmaster: true,
}
}
}
fn become_grandmaster(&mut self) {
self.current_grandmaster = Some(self.clock_identity);
for state in self.port_states.values_mut() {
*state = PortState::Master;
}
}
pub fn expire_announce_receipt(&mut self, port_number: u16) {
if let Some(state) = self.port_states.get_mut(&port_number) {
if *state == PortState::Slave {
*state = PortState::Listening;
}
}
}
#[must_use]
pub fn foreign_master_count(&self) -> usize {
self.foreign_masters.len()
}
}
#[derive(Debug, Clone, Copy)]
pub struct AnnounceInterval(pub i8);
impl AnnounceInterval {
#[must_use]
pub fn to_duration(self) -> Duration {
let secs = 2.0_f64.powi(i32::from(self.0));
Duration::from_secs_f64(secs)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_identity(v: u64) -> ClockIdentity {
ClockIdentity::from_u64(v)
}
fn make_port(clock: u64, port: u16) -> PortIdentity {
PortIdentity::new(make_identity(clock), port)
}
fn make_announce(
source_clock: u64,
gm_clock: u64,
class: ClockClass,
priority1: u8,
) -> AnnounceMessage {
AnnounceMessage::new(
make_port(source_clock, 1),
make_identity(gm_clock),
class,
ClockAccuracy::Within100ns,
priority1,
128,
0,
1,
)
}
#[test]
fn test_clock_identity_roundtrip() {
let id = ClockIdentity::from_u64(0xDEAD_BEEF_1234_5678);
assert_eq!(id.0, 0xDEAD_BEEF_1234_5678u64.to_be_bytes());
}
#[test]
fn test_clock_class_ordering() {
assert!(ClockClass::Primary < ClockClass::HoldoverInSpec);
assert!(ClockClass::HoldoverInSpec < ClockClass::Degraded);
assert!(ClockClass::Degraded < ClockClass::Default);
}
#[test]
fn test_announce_message_comparison_vector() {
let msg = make_announce(1, 2, ClockClass::Primary, 128);
let cv = msg.comparison_vector();
assert_eq!(cv.0, ClockClass::Primary as u8);
assert_eq!(cv.3, 128);
}
#[test]
fn test_boundary_clock_add_port() {
let mut bc = PtpBoundaryClock::new(
make_identity(0xAA),
128,
128,
ClockClass::Default,
ClockAccuracy::Unknown,
);
bc.add_port(1);
bc.add_port(2);
assert_eq!(bc.port_states.len(), 2);
assert_eq!(bc.port_states[&1], PortState::Listening);
}
#[test]
fn test_bmca_no_foreign_masters_becomes_grandmaster() {
let mut bc = PtpBoundaryClock::new(
make_identity(0xBB),
128,
128,
ClockClass::Default,
ClockAccuracy::Unknown,
);
bc.add_port(1);
let result = bc.run_bmca();
assert!(result.is_grandmaster);
assert!(result.best_announce.is_none());
assert_eq!(bc.port_states[&1], PortState::Master);
}
#[test]
fn test_bmca_better_foreign_master_makes_slave() {
let mut bc = PtpBoundaryClock::new(
make_identity(0xCC),
128,
128,
ClockClass::Default,
ClockAccuracy::Unknown,
);
bc.add_port(1);
bc.add_port(2);
let msg = make_announce(0xDD, 0xDD, ClockClass::Primary, 64);
bc.receive_announce(msg);
let result = bc.run_bmca();
assert!(!result.is_grandmaster);
assert!(result.best_announce.is_some());
assert_eq!(bc.port_states[&1], PortState::Slave);
assert_eq!(bc.port_states[&2], PortState::Master);
}
#[test]
fn test_receive_announce_caps_at_four() {
let mut bc = PtpBoundaryClock::new(
make_identity(0xEE),
128,
128,
ClockClass::Default,
ClockAccuracy::Unknown,
);
for i in 0..6u16 {
let msg = make_announce(0xFF, 0xFF, ClockClass::Primary, 128);
let mut m = msg;
m.sequence_id = i;
bc.receive_announce(m);
}
let count: usize = bc.foreign_masters.values().map(|v| v.len()).sum();
assert_eq!(count, 4);
}
#[test]
fn test_foreign_master_count() {
let mut bc = PtpBoundaryClock::new(
make_identity(0x11),
128,
128,
ClockClass::Default,
ClockAccuracy::Unknown,
);
bc.receive_announce(make_announce(0x21, 0x21, ClockClass::Primary, 128));
bc.receive_announce(make_announce(0x22, 0x22, ClockClass::Primary, 128));
assert_eq!(bc.foreign_master_count(), 2);
}
#[test]
fn test_expire_announce_sets_listening() {
let mut bc = PtpBoundaryClock::new(
make_identity(0x33),
128,
128,
ClockClass::Default,
ClockAccuracy::Unknown,
);
bc.add_port(5);
*bc.port_states.get_mut(&5).expect("should succeed in test") = PortState::Slave;
bc.expire_announce_receipt(5);
assert_eq!(bc.port_states[&5], PortState::Listening);
}
#[test]
fn test_expire_announce_non_slave_unchanged() {
let mut bc = PtpBoundaryClock::new(
make_identity(0x44),
128,
128,
ClockClass::Default,
ClockAccuracy::Unknown,
);
bc.add_port(3);
*bc.port_states.get_mut(&3).expect("should succeed in test") = PortState::Master;
bc.expire_announce_receipt(3);
assert_eq!(bc.port_states[&3], PortState::Master);
}
#[test]
fn test_announce_interval_duration_positive() {
let interval = AnnounceInterval(1);
let d = interval.to_duration();
assert!((d.as_secs_f64() - 2.0).abs() < 1e-9);
}
#[test]
fn test_announce_interval_duration_negative() {
let interval = AnnounceInterval(-1);
let d = interval.to_duration();
assert!((d.as_secs_f64() - 0.5).abs() < 1e-9);
}
#[test]
fn test_port_state_default() {
let s = PortState::default();
assert_eq!(s, PortState::Initializing);
}
#[test]
fn test_clock_class_default() {
let c = ClockClass::default();
assert_eq!(c, ClockClass::Default);
}
#[test]
fn test_clock_accuracy_default() {
let a = ClockAccuracy::default();
assert_eq!(a, ClockAccuracy::Unknown);
}
}