autd3-rs-core 0.9.0

Core types and link abstraction shared across the AUTD3 phased-array sdk crates.
Documentation
use core::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};

use chrono::{DateTime, Utc};
use thiserror::Error;

const ECAT_EPOCH_OFFSET_NANOS: i128 = 946_684_800 * 1_000_000_000;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum DcSysTimeError {
    #[error("UTC time is out of the representable DcSysTime range (2000-01-01 0:00:00 UTC ..)")]
    OutOfRange,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct DcSysTime(u64);

impl DcSysTime {
    pub const ZERO: Self = Self(0);

    #[must_use]
    pub const fn from_nanos(ns: u64) -> Self {
        Self(ns)
    }

    #[must_use]
    pub const fn sys_time(self) -> u64 {
        self.0
    }

    fn from_unix_nanos(unix_nanos: i128) -> Result<Self, DcSysTimeError> {
        u64::try_from(unix_nanos - ECAT_EPOCH_OFFSET_NANOS)
            .map(Self)
            .map_err(|_| DcSysTimeError::OutOfRange)
    }

    pub fn now() -> Result<Self, DcSysTimeError> {
        let unix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map_err(|_| DcSysTimeError::OutOfRange)?;
        let unix_nanos = i128::try_from(unix.as_nanos()).map_err(|_| DcSysTimeError::OutOfRange)?;
        Self::from_unix_nanos(unix_nanos)
    }

    pub fn from_utc(utc: DateTime<Utc>) -> Result<Self, DcSysTimeError> {
        let unix_nanos = utc
            .timestamp_nanos_opt()
            .ok_or(DcSysTimeError::OutOfRange)?;
        let nanos = i128::from(unix_nanos) - ECAT_EPOCH_OFFSET_NANOS;
        u64::try_from(nanos)
            .map(Self)
            .map_err(|_| DcSysTimeError::OutOfRange)
    }

    #[must_use]
    pub fn with_dc_offset(self, offset_ns: i64) -> Self {
        Self(self.0.saturating_add_signed(offset_ns))
    }

    #[must_use]
    pub fn to_utc(self) -> DateTime<Utc> {
        let unix_nanos =
            i64::try_from(ECAT_EPOCH_OFFSET_NANOS + i128::from(self.0)).unwrap_or(i64::MAX);
        DateTime::from_timestamp_nanos(unix_nanos)
    }
}

impl core::ops::Add<Duration> for DcSysTime {
    type Output = Self;

    fn add(self, rhs: Duration) -> Self::Output {
        Self(
            self.0
                .saturating_add(u64::try_from(rhs.as_nanos()).unwrap_or(u64::MAX)),
        )
    }
}

impl core::ops::AddAssign<Duration> for DcSysTime {
    fn add_assign(&mut self, rhs: Duration) {
        *self = *self + rhs;
    }
}

impl core::ops::Sub<Duration> for DcSysTime {
    type Output = Self;

    fn sub(self, rhs: Duration) -> Self::Output {
        Self(
            self.0
                .saturating_sub(u64::try_from(rhs.as_nanos()).unwrap_or(u64::MAX)),
        )
    }
}

impl core::ops::SubAssign<Duration> for DcSysTime {
    fn sub_assign(&mut self, rhs: Duration) {
        *self = *self - rhs;
    }
}

impl core::ops::Sub for DcSysTime {
    type Output = Duration;

    fn sub(self, rhs: Self) -> Self::Output {
        Duration::from_nanos(self.0.saturating_sub(rhs.0))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use chrono::TimeZone;

    #[test]
    fn round_trips_nanos() {
        assert_eq!(DcSysTime::from_nanos(12_345).sys_time(), 12_345);
        assert_eq!(DcSysTime::ZERO.sys_time(), 0);
        assert_eq!(DcSysTime::default(), DcSysTime::ZERO);
    }

    #[test]
    fn now_is_after_epoch() {
        assert!(DcSysTime::now().unwrap().sys_time() > 0);
    }

    #[test]
    fn a_clock_before_the_ecat_epoch_is_an_error_rather_than_a_panic() {
        for (unix_nanos, what) in [
            (0, "1970-01-01"),
            (-1_000_000_000_000_000_000, "before the UNIX epoch"),
            (
                ECAT_EPOCH_OFFSET_NANOS - 1,
                "one nanosecond short of the epoch",
            ),
        ] {
            assert_eq!(
                DcSysTime::from_unix_nanos(unix_nanos),
                Err(DcSysTimeError::OutOfRange),
                "{what}"
            );
        }
        assert_eq!(
            DcSysTime::from_unix_nanos(ECAT_EPOCH_OFFSET_NANOS),
            Ok(DcSysTime::ZERO)
        );
        assert_eq!(
            DcSysTime::from_unix_nanos(ECAT_EPOCH_OFFSET_NANOS + 1)
                .unwrap()
                .sys_time(),
            1
        );
    }

    #[test]
    fn a_clock_beyond_the_dc_sys_time_range_is_an_error() {
        assert_eq!(
            DcSysTime::from_unix_nanos(i128::MAX),
            Err(DcSysTimeError::OutOfRange)
        );
    }

    #[test]
    fn arithmetic_saturates_instead_of_panicking() {
        assert_eq!(DcSysTime::ZERO - Duration::from_secs(1), DcSysTime::ZERO);
        let mut t = DcSysTime::ZERO;
        t -= Duration::from_secs(1);
        assert_eq!(t, DcSysTime::ZERO);
        assert_eq!(DcSysTime::ZERO - DcSysTime::from_nanos(1), Duration::ZERO);
        assert_eq!(
            (DcSysTime::from_nanos(u64::MAX) + Duration::from_secs(1)).sys_time(),
            u64::MAX
        );
        assert_eq!(
            (DcSysTime::ZERO + Duration::from_secs(u64::MAX)).sys_time(),
            u64::MAX
        );
    }

    #[test]
    fn from_utc_epoch_is_zero() {
        let epoch = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
        assert_eq!(DcSysTime::from_utc(epoch).unwrap(), DcSysTime::ZERO);
    }

    #[test]
    fn from_utc_one_second() {
        let t = Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 1).unwrap();
        assert_eq!(DcSysTime::from_utc(t).unwrap().sys_time(), 1_000_000_000);
    }

    #[test]
    fn from_utc_one_year() {
        let t = Utc.with_ymd_and_hms(2001, 1, 1, 0, 0, 0).unwrap();
        assert_eq!(
            DcSysTime::from_utc(t).unwrap().sys_time(),
            31_622_400_000_000_000
        );
    }

    #[test]
    fn from_utc_before_epoch_is_out_of_range() {
        let t = Utc.with_ymd_and_hms(1999, 1, 1, 0, 0, 1).unwrap();
        assert_eq!(DcSysTime::from_utc(t), Err(DcSysTimeError::OutOfRange));
    }

    #[test]
    fn to_utc_round_trips() {
        let t = Utc.with_ymd_and_hms(2025, 6, 30, 12, 0, 0).unwrap();
        assert_eq!(DcSysTime::from_utc(t).unwrap().to_utc(), t);
    }

    #[test]
    fn add_sub_duration() {
        let mut t = DcSysTime::ZERO + Duration::from_secs(1);
        assert_eq!(t.sys_time(), 1_000_000_000);
        t += Duration::from_secs(2);
        assert_eq!(t.sys_time(), 3_000_000_000);
        t -= Duration::from_secs(1);
        assert_eq!(t.sys_time(), 2_000_000_000);
        assert_eq!((t - Duration::from_secs(2)).sys_time(), 0);
    }

    #[test]
    fn with_dc_offset_shifts_onto_the_bus_clock() {
        let t = DcSysTime::from_nanos(1_000_000);
        assert_eq!(t.with_dc_offset(500).sys_time(), 1_000_500);
        assert_eq!(t.with_dc_offset(-500).sys_time(), 999_500);
        assert_eq!(t.with_dc_offset(0), t);
        assert_eq!(DcSysTime::ZERO.with_dc_offset(-1), DcSysTime::ZERO);
        assert_eq!(
            DcSysTime::from_nanos(u64::MAX).with_dc_offset(1).sys_time(),
            u64::MAX
        );
    }

    #[test]
    fn sub_returns_duration() {
        let a = DcSysTime::ZERO + Duration::from_secs(3);
        let b = DcSysTime::ZERO + Duration::from_secs(1);
        assert_eq!(a - b, Duration::from_secs(2));
    }
}