#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Micros(pub u64);
impl Micros {
pub const ZERO: Micros = Micros(0);
#[must_use]
pub const fn from_millis(ms: u64) -> Self {
Micros(ms.saturating_mul(1_000))
}
#[must_use]
pub const fn from_secs(s: u64) -> Self {
Micros(s.saturating_mul(1_000_000))
}
#[must_use]
pub const fn as_millis(self) -> u64 {
self.0 / 1_000
}
#[must_use]
pub const fn since(self, earlier: Micros) -> u64 {
self.0.saturating_sub(earlier.0)
}
#[must_use]
pub const fn add_micros(self, micros: u64) -> Micros {
Micros(self.0.saturating_add(micros))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WallOffset {
offset_us: i64,
error_us: Option<u64>,
measured_at: Micros,
}
impl WallOffset {
pub const UNKNOWN: WallOffset = WallOffset {
offset_us: 0,
error_us: None,
measured_at: Micros::ZERO,
};
pub fn from_exchange(
host_send_us: u64,
device_us: Micros,
host_recv_us: u64,
) -> crate::error::Result<Self> {
if host_recv_us < host_send_us {
return Err(crate::error::Error::InvalidFormat);
}
let rtt = host_recv_us - host_send_us;
let mid = host_send_us + rtt / 2;
let offset = i128::from(mid) - i128::from(device_us.0);
let offset_us = i64::try_from(offset).map_err(|_| crate::error::Error::InvalidFormat)?;
Ok(WallOffset {
offset_us,
error_us: Some(rtt.div_ceil(2)),
measured_at: device_us,
})
}
#[must_use]
pub const fn is_known(&self) -> bool {
self.error_us.is_some()
}
#[must_use]
pub const fn error_us(&self) -> Option<u64> {
self.error_us
}
#[must_use]
pub const fn measured_at(&self) -> Micros {
self.measured_at
}
#[must_use]
pub fn to_wall(&self, device: Micros) -> Option<u64> {
self.error_us?;
Some((device.0 as i64).saturating_add(self.offset_us).max(0) as u64)
}
#[must_use]
pub fn error_at(&self, device: Micros, ppm: u32) -> Option<u64> {
let base = self.error_us?;
let age = device.since(self.measured_at);
Some(base.saturating_add(
age / 1_000_000 * u64::from(ppm) + (age % 1_000_000) * u64::from(ppm) / 1_000_000,
))
}
#[must_use]
pub fn better(self, other: WallOffset) -> WallOffset {
match (self.error_us, other.error_us) {
(Some(a), Some(b)) => {
if b < a {
other
} else {
self
}
}
(Some(_), None) => self,
(None, _) => other,
}
}
}
impl core::fmt::Display for Micros {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}us", self.0)
}
}
#[cfg(test)]
mod tests {
use super::{Micros, WallOffset};
#[test]
fn an_unmeasured_offset_says_so() {
let w = WallOffset::UNKNOWN;
assert!(!w.is_known());
assert_eq!(w.error_us(), None, "infinite, never zero");
assert_eq!(w.to_wall(Micros(5)), None);
assert_eq!(w.error_at(Micros(5), 20), None);
}
#[test]
fn one_shot_exchange_places_the_device_reading_mid_round_trip() {
let w = WallOffset::from_exchange(1_000_000, Micros(500), 1_000_040).unwrap();
assert_eq!(w.error_us(), Some(20));
assert_eq!(w.to_wall(Micros(500)), Some(1_000_020));
assert_eq!(w.to_wall(Micros(1_500)), Some(1_001_020));
assert_eq!(w.measured_at(), Micros(500));
assert_eq!(w.error_at(Micros(10_000_500), 20), Some(220));
assert!(WallOffset::from_exchange(10, Micros(0), 5).is_err());
let odd = WallOffset::from_exchange(0, Micros(0), 3).unwrap();
assert_eq!(odd.error_us(), Some(2), "half a round trip rounds up");
let tight = WallOffset::from_exchange(0, Micros(0), 2).unwrap();
assert_eq!(w.better(tight).error_us(), Some(1));
assert_eq!(WallOffset::UNKNOWN.better(w), w);
assert_eq!(w.better(WallOffset::UNKNOWN), w);
}
#[test]
fn since_saturates_instead_of_wrapping() {
let a = Micros::from_millis(5);
let b = Micros::from_millis(7);
assert_eq!(b.since(a), 2_000);
assert_eq!(a.since(b), 0);
}
#[test]
fn conversions_round_trip() {
assert_eq!(Micros::from_secs(3).as_millis(), 3_000);
assert_eq!(Micros::from_millis(u64::MAX).0, u64::MAX);
}
}