diff --git a/embassy-stm32/src/can/bx/frame.rs b/embassy-stm32/src/can/bx/frame.rs deleted file mode 100644 index 828f375be..000000000 --- a/embassy-stm32/src/can/bx/frame.rs +++ /dev/null @@ -1,248 +0,0 @@ -#[cfg(test)] -use core::cmp::Ordering; -use core::ops::{Deref, DerefMut}; - -use crate::can::bx::{Id, IdReg}; - -/// A CAN data or remote frame. -#[derive(Clone, Debug, Eq)] -//#[cfg_attr(feature = "defmt", derive(defmt::Format))] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Frame { - pub(crate) id: IdReg, - pub(crate) data: Data, -} - -impl Frame { - /// Creates a new data frame. - pub fn new_data(id: impl Into, data: impl Into) -> Self { - let id = match id.into() { - Id::Standard(id) => IdReg::new_standard(id), - Id::Extended(id) => IdReg::new_extended(id), - }; - - Self { id, data: data.into() } - } - - /// Creates a new remote frame with configurable data length code (DLC). - /// - /// # Panics - /// - /// This function will panic if `dlc` is not inside the valid range `0..=8`. - pub fn new_remote(id: impl Into, dlc: u8) -> Self { - assert!(dlc <= 8); - - let mut frame = Self::new_data(id, []); - // Just extend the data length, even with no data present. The API does not hand out this - // `Data` object. - frame.data.len = dlc; - frame.id = frame.id.with_rtr(true); - frame - } - - /// Returns true if this frame is an extended frame. - #[inline] - pub fn is_extended(&self) -> bool { - self.id.is_extended() - } - - /// Returns true if this frame is a standard frame. - #[inline] - pub fn is_standard(&self) -> bool { - self.id.is_standard() - } - - /// Returns true if this frame is a remote frame. - #[inline] - pub fn is_remote_frame(&self) -> bool { - self.id.rtr() - } - - /// Returns true if this frame is a data frame. - #[inline] - pub fn is_data_frame(&self) -> bool { - !self.is_remote_frame() - } - - /// Returns the frame identifier. - #[inline] - pub fn id(&self) -> Id { - self.id.to_id() - } - - /// Returns the priority of this frame. - #[inline] - pub fn priority(&self) -> FramePriority { - FramePriority(self.id) - } - - /// Returns the data length code (DLC) which is in the range 0..8. - /// - /// For data frames the DLC value always matches the length of the data. - /// Remote frames do not carry any data, yet the DLC can be greater than 0. - #[inline] - pub fn dlc(&self) -> u8 { - self.data.len() as u8 - } - - /// Returns the frame data (0..8 bytes in length) if this is a data frame. - /// - /// If this is a remote frame, returns `None`. - pub fn data(&self) -> Option<&Data> { - if self.is_data_frame() { - Some(&self.data) - } else { - None - } - } -} - -impl PartialEq for Frame { - fn eq(&self, other: &Self) -> bool { - match (self.data(), other.data()) { - (None, None) => self.id.eq(&other.id), - (Some(a), Some(b)) => self.id.eq(&other.id) && a.eq(b), - (None, Some(_)) | (Some(_), None) => false, - } - } -} - -/// Priority of a CAN frame. -/// -/// Returned by [`Frame::priority`]. -/// -/// The priority of a frame is determined by the bits that are part of the *arbitration field*. -/// These consist of the frame identifier bits (including the *IDE* bit, which is 0 for extended -/// frames and 1 for standard frames), as well as the *RTR* bit, which determines whether a frame -/// is a data or remote frame. Lower values of the *arbitration field* have higher priority. -/// -/// This struct wraps the *arbitration field* and implements `PartialOrd` and `Ord` accordingly, -/// ordering higher priorities greater than lower ones. -#[derive(Debug, Copy, Clone)] -pub struct FramePriority(IdReg); - -/// Ordering is based on the Identifier and frame type (data vs. remote) and can be used to sort -/// frames by priority. -impl Ord for FramePriority { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.0.cmp(&other.0) - } -} - -impl PartialOrd for FramePriority { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for FramePriority { - fn eq(&self, other: &Self) -> bool { - self.cmp(other) == core::cmp::Ordering::Equal - } -} - -impl Eq for FramePriority {} - -/// Payload of a CAN data frame. -/// -/// Contains 0 to 8 Bytes of data. -/// -/// `Data` implements `From<[u8; N]>` for all `N` up to 8, which provides a convenient lossless -/// conversion from fixed-length arrays. -#[derive(Debug, Copy, Clone)] -pub struct Data { - pub(crate) len: u8, - pub(crate) bytes: [u8; 8], -} - -impl Data { - /// Creates a data payload from a raw byte slice. - /// - /// Returns `None` if `data` contains more than 8 Bytes (which is the maximum). - /// - /// `Data` can also be constructed from fixed-length arrays up to length 8 via `From`/`Into`. - pub fn new(data: &[u8]) -> Option { - if data.len() > 8 { - return None; - } - - let mut bytes = [0; 8]; - bytes[..data.len()].copy_from_slice(data); - - Some(Self { - len: data.len() as u8, - bytes, - }) - } - - /// Creates an empty data payload containing 0 bytes. - #[inline] - pub const fn empty() -> Self { - Self { len: 0, bytes: [0; 8] } - } -} - -impl Deref for Data { - type Target = [u8]; - - #[inline] - fn deref(&self) -> &[u8] { - &self.bytes[..usize::from(self.len)] - } -} - -impl DerefMut for Data { - #[inline] - fn deref_mut(&mut self) -> &mut [u8] { - &mut self.bytes[..usize::from(self.len)] - } -} - -impl AsRef<[u8]> for Data { - #[inline] - fn as_ref(&self) -> &[u8] { - self.deref() - } -} - -impl AsMut<[u8]> for Data { - #[inline] - fn as_mut(&mut self) -> &mut [u8] { - self.deref_mut() - } -} - -impl PartialEq for Data { - fn eq(&self, other: &Self) -> bool { - self.as_ref() == other.as_ref() - } -} - -impl Eq for Data {} - -#[cfg(feature = "defmt")] -impl defmt::Format for Data { - fn format(&self, fmt: defmt::Formatter<'_>) { - self.as_ref().format(fmt) - } -} - -macro_rules! data_from_array { - ( $($len:literal),+ ) => { - $( - impl From<[u8; $len]> for Data { - #[inline] - fn from(arr: [u8; $len]) -> Self { - let mut bytes = [0; 8]; - bytes[..$len].copy_from_slice(&arr); - Self { - len: $len, - bytes, - } - } - } - )+ - }; -} - -data_from_array!(0, 1, 2, 3, 4, 5, 6, 7, 8); diff --git a/embassy-stm32/src/can/bx/id.rs b/embassy-stm32/src/can/bx/id.rs deleted file mode 100644 index 9fdcd8319..000000000 --- a/embassy-stm32/src/can/bx/id.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! CAN Identifiers. - -/// Standard 11-bit CAN Identifier (`0..=0x7FF`). -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct StandardId(u16); - -impl StandardId { - /// CAN ID `0`, the highest priority. - pub const ZERO: Self = Self(0); - - /// CAN ID `0x7FF`, the lowest priority. - pub const MAX: Self = Self(0x7FF); - - /// Tries to create a `StandardId` from a raw 16-bit integer. - /// - /// This will return `None` if `raw` is out of range of an 11-bit integer (`> 0x7FF`). - #[inline] - pub const fn new(raw: u16) -> Option { - if raw <= 0x7FF { - Some(Self(raw)) - } else { - None - } - } - - /// Creates a new `StandardId` without checking if it is inside the valid range. - /// - /// # Safety - /// - /// The caller must ensure that `raw` is in the valid range, otherwise the behavior is - /// undefined. - #[inline] - pub const unsafe fn new_unchecked(raw: u16) -> Self { - Self(raw) - } - - /// Returns this CAN Identifier as a raw 16-bit integer. - #[inline] - pub fn as_raw(&self) -> u16 { - self.0 - } -} - -/// Extended 29-bit CAN Identifier (`0..=1FFF_FFFF`). -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct ExtendedId(u32); - -impl ExtendedId { - /// CAN ID `0`, the highest priority. - pub const ZERO: Self = Self(0); - - /// CAN ID `0x1FFFFFFF`, the lowest priority. - pub const MAX: Self = Self(0x1FFF_FFFF); - - /// Tries to create a `ExtendedId` from a raw 32-bit integer. - /// - /// This will return `None` if `raw` is out of range of an 29-bit integer (`> 0x1FFF_FFFF`). - #[inline] - pub const fn new(raw: u32) -> Option { - if raw <= 0x1FFF_FFFF { - Some(Self(raw)) - } else { - None - } - } - - /// Creates a new `ExtendedId` without checking if it is inside the valid range. - /// - /// # Safety - /// - /// The caller must ensure that `raw` is in the valid range, otherwise the behavior is - /// undefined. - #[inline] - pub const unsafe fn new_unchecked(raw: u32) -> Self { - Self(raw) - } - - /// Returns this CAN Identifier as a raw 32-bit integer. - #[inline] - pub fn as_raw(&self) -> u32 { - self.0 - } - - /// Returns the Base ID part of this extended identifier. - pub fn standard_id(&self) -> StandardId { - // ID-28 to ID-18 - StandardId((self.0 >> 18) as u16) - } -} - -/// A CAN Identifier (standard or extended). -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Id { - /// Standard 11-bit Identifier (`0..=0x7FF`). - Standard(StandardId), - - /// Extended 29-bit Identifier (`0..=0x1FFF_FFFF`). - Extended(ExtendedId), -} - -impl From for Id { - #[inline] - fn from(id: StandardId) -> Self { - Id::Standard(id) - } -} - -impl From for Id { - #[inline] - fn from(id: ExtendedId) -> Self { - Id::Extended(id) - } -} diff --git a/embassy-stm32/src/can/bx/mod.rs b/embassy-stm32/src/can/bx/mod.rs index f639260a1..650d4414b 100644 --- a/embassy-stm32/src/can/bx/mod.rs +++ b/embassy-stm32/src/can/bx/mod.rs @@ -25,19 +25,26 @@ //mod embedded_hal; pub mod filter; -mod frame; -mod id; #[allow(clippy::all)] // generated code use core::cmp::{Ord, Ordering}; -use core::convert::{Infallible, TryInto}; +use core::convert::{Infallible, Into, TryInto}; use core::marker::PhantomData; use core::mem; -pub use id::{ExtendedId, Id, StandardId}; +pub use embedded_can::{ExtendedId, Id, StandardId}; + +/// CAN Header: includes ID and length +pub type Header = crate::can::frame::Header; + +/// Data for a CAN Frame +pub type Data = crate::can::frame::ClassicData; + +/// CAN Frame +pub type Frame = crate::can::frame::ClassicFrame; use crate::can::bx::filter::MasterFilters; -pub use crate::can::bx::frame::{Data, Frame, FramePriority}; +use crate::can::frame::ClassicData; /// A bxCAN peripheral instance. /// @@ -148,13 +155,13 @@ impl IdReg { /// Sets the remote transmission (RTR) flag. This marks the identifier as /// being part of a remote frame. #[must_use = "returns a new IdReg without modifying `self`"] - fn with_rtr(self, rtr: bool) -> IdReg { + /*fn with_rtr(self, rtr: bool) -> IdReg { if rtr { Self(self.0 | Self::RTR_MASK) } else { Self(self.0 & !Self::RTR_MASK) } - } + }*/ /// Returns the identifier. fn to_id(self) -> Id { @@ -165,15 +172,28 @@ impl IdReg { } } + /// Returns the identifier. + fn id(self) -> embedded_can::Id { + if self.is_extended() { + embedded_can::ExtendedId::new(self.0 >> Self::EXTENDED_SHIFT) + .unwrap() + .into() + } else { + embedded_can::StandardId::new((self.0 >> Self::STANDARD_SHIFT) as u16) + .unwrap() + .into() + } + } + /// Returns `true` if the identifier is an extended identifier. fn is_extended(self) -> bool { self.0 & Self::IDE_MASK != 0 } /// Returns `true` if the identifier is a standard identifier. - fn is_standard(self) -> bool { + /*fn is_standard(self) -> bool { !self.is_extended() - } + }*/ /// Returns `true` if the identifer is part of a remote frame (RTR bit set). fn rtr(self) -> bool { @@ -181,6 +201,21 @@ impl IdReg { } } +impl From<&embedded_can::Id> for IdReg { + fn from(eid: &embedded_can::Id) -> Self { + match eid { + embedded_can::Id::Standard(id) => IdReg::new_standard(StandardId::new(id.as_raw()).unwrap()), + embedded_can::Id::Extended(id) => IdReg::new_extended(ExtendedId::new(id.as_raw()).unwrap()), + } + } +} + +impl From for embedded_can::Id { + fn from(idr: IdReg) -> Self { + idr.id() + } +} + /// `IdReg` is ordered by priority. impl Ord for IdReg { fn cmp(&self, other: &Self) -> Ordering { @@ -682,9 +717,9 @@ where // The controller schedules pending frames of same priority based on the // mailbox index instead. As a workaround check all pending mailboxes // and only accept higher priority frames. - self.check_priority(0, frame.id)?; - self.check_priority(1, frame.id)?; - self.check_priority(2, frame.id)?; + self.check_priority(0, frame.id().into())?; + self.check_priority(1, frame.id().into())?; + self.check_priority(2, frame.id().into())?; let all_frames_are_pending = !tsr.tme(0) && !tsr.tme(1) && !tsr.tme(2); if all_frames_are_pending { @@ -739,14 +774,15 @@ where debug_assert!(idx < 3); let mb = self.canregs.tx(idx); - mb.tdtr().write(|w| w.set_dlc(frame.dlc() as u8)); + mb.tdtr().write(|w| w.set_dlc(frame.header().len() as u8)); mb.tdlr() - .write(|w| w.0 = u32::from_ne_bytes(frame.data.bytes[0..4].try_into().unwrap())); + .write(|w| w.0 = u32::from_ne_bytes(frame.data()[0..4].try_into().unwrap())); mb.tdhr() - .write(|w| w.0 = u32::from_ne_bytes(frame.data.bytes[4..8].try_into().unwrap())); + .write(|w| w.0 = u32::from_ne_bytes(frame.data()[4..8].try_into().unwrap())); + let id: IdReg = frame.id().into(); mb.tir().write(|w| { - w.0 = frame.id.0; + w.0 = id.0; w.set_txrq(true); }); } @@ -756,16 +792,17 @@ where debug_assert!(idx < 3); let mb = self.canregs.tx(idx); - // Read back the pending frame. - let mut pending_frame = Frame { - id: IdReg(mb.tir().read().0), - data: Data::empty(), - }; - pending_frame.data.bytes[0..4].copy_from_slice(&mb.tdlr().read().0.to_ne_bytes()); - pending_frame.data.bytes[4..8].copy_from_slice(&mb.tdhr().read().0.to_ne_bytes()); - pending_frame.data.len = mb.tdtr().read().dlc(); - Some(pending_frame) + let id = IdReg(mb.tir().read().0).id(); + let mut data = [0xff; 8]; + data[0..4].copy_from_slice(&mb.tdlr().read().0.to_ne_bytes()); + data[4..8].copy_from_slice(&mb.tdhr().read().0.to_ne_bytes()); + let len = mb.tdtr().read().dlc(); + + Some(Frame::new( + Header::new(id, len, false), + ClassicData::new(&data).unwrap(), + )) } else { // Abort request failed because the frame was already sent (or being sent) on // the bus. All mailboxes are now free. This can happen for small prescaler @@ -898,18 +935,19 @@ fn receive_fifo(canregs: crate::pac::can::Can, fifo_nr: usize) -> nb::Result Can<'d, T> { } let rir = fifo.rir().read(); - let id = if rir.ide() == Ide::STANDARD { - Id::from(StandardId::new_unchecked(rir.stid())) + let id: embedded_can::Id = if rir.ide() == Ide::STANDARD { + embedded_can::StandardId::new(rir.stid()).unwrap().into() } else { let stid = (rir.stid() & 0x7FF) as u32; let exid = rir.exid() & 0x3FFFF; let id = (stid << 18) | (exid); - Id::from(ExtendedId::new_unchecked(id)) + embedded_can::ExtendedId::new(id).unwrap().into() }; - let data_len = fifo.rdtr().read().dlc() as usize; + let data_len = fifo.rdtr().read().dlc(); let mut data: [u8; 8] = [0; 8]; data[0..4].copy_from_slice(&fifo.rdlr().read().0.to_ne_bytes()); data[4..8].copy_from_slice(&fifo.rdhr().read().0.to_ne_bytes()); - let frame = Frame::new_data(id, Data::new(&data[0..data_len]).unwrap()); + let frame = Frame::new(Header::new(id, data_len, false), Data::new(&data).unwrap()); let envelope = Envelope { #[cfg(feature = "time")] ts, diff --git a/embassy-stm32/src/can/fd/peripheral.rs b/embassy-stm32/src/can/fd/peripheral.rs index 8ec09ac12..cce4e5e8d 100644 --- a/embassy-stm32/src/can/fd/peripheral.rs +++ b/embassy-stm32/src/can/fd/peripheral.rs @@ -182,7 +182,7 @@ impl Registers { DataLength::Fdcan(len) => len, DataLength::Classic(len) => len, }; - if len as usize > ClassicFrame::MAX_DATA_LEN { + if len as usize > ClassicData::MAX_DATA_LEN { return None; } diff --git a/embassy-stm32/src/can/frame.rs b/embassy-stm32/src/can/frame.rs index 9c293035d..0dc74d299 100644 --- a/embassy-stm32/src/can/frame.rs +++ b/embassy-stm32/src/can/frame.rs @@ -9,6 +9,20 @@ pub struct Header { flags: u8, } +#[cfg(feature = "defmt")] +impl defmt::Format for Header { + fn format(&self, fmt: defmt::Formatter<'_>) { + match self.id() { + embedded_can::Id::Standard(id) => { + defmt::write!(fmt, "Can Standard ID={:x} len={}", id.as_raw(), self.len,) + } + embedded_can::Id::Extended(id) => { + defmt::write!(fmt, "Can Extended ID={:x} len={}", id.as_raw(), self.len,) + } + } + } +} + impl Header { const FLAG_RTR: usize = 0; // Remote const FLAG_FDCAN: usize = 1; // FDCan vs Classic CAN @@ -54,6 +68,14 @@ impl Header { pub fn bit_rate_switching(&self) -> bool { self.flags.get_bit(Self::FLAG_BRS) } + + /// Get priority of frame + pub(crate) fn priority(&self) -> u32 { + match self.id() { + embedded_can::Id::Standard(id) => (id.as_raw() as u32) << 18, + embedded_can::Id::Extended(id) => id.as_raw(), + } + } } /// Trait for FDCAN frame types, providing ability to construct from a Header @@ -70,11 +92,13 @@ pub trait CanHeader: Sized { /// /// Contains 0 to 8 Bytes of data. #[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ClassicData { - pub(crate) bytes: [u8; 8], + pub(crate) bytes: [u8; Self::MAX_DATA_LEN], } impl ClassicData { + pub(crate) const MAX_DATA_LEN: usize = 8; /// Creates a data payload from a raw byte slice. /// /// Returns `None` if `data` is more than 64 bytes (which is the maximum) or @@ -110,19 +134,34 @@ impl ClassicData { } } +impl From<&[u8]> for ClassicData { + fn from(d: &[u8]) -> Self { + ClassicData::new(d).unwrap() + } +} + /// Frame with up to 8 bytes of data payload as per Classic CAN #[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ClassicFrame { can_header: Header, data: ClassicData, } impl ClassicFrame { - pub(crate) const MAX_DATA_LEN: usize = 8; - /// Create a new CAN classic Frame - pub fn new(can_header: Header, data: ClassicData) -> ClassicFrame { - ClassicFrame { can_header, data } + pub fn new(can_header: Header, data: impl Into) -> ClassicFrame { + ClassicFrame { + can_header, + data: data.into(), + } + } + + /// Creates a new data frame. + pub fn new_data(id: impl Into, data: &[u8]) -> Self { + let eid: embedded_can::Id = id.into(); + let header = Header::new(eid, data.len() as u8, false); + Self::new(header, data) } /// Create new extended frame @@ -181,6 +220,11 @@ impl ClassicFrame { pub fn data(&self) -> &[u8] { &self.data.raw() } + + /// Get priority of frame + pub fn priority(&self) -> u32 { + self.header().priority() + } } impl embedded_can::Frame for ClassicFrame { diff --git a/examples/stm32f1/src/bin/can.rs b/examples/stm32f1/src/bin/can.rs index 00d61096f..a43fb4427 100644 --- a/examples/stm32f1/src/bin/can.rs +++ b/examples/stm32f1/src/bin/can.rs @@ -46,16 +46,16 @@ async fn main(_spawner: Spawner) { let mut i: u8 = 0; loop { - let tx_frame = Frame::new_data(unwrap!(StandardId::new(i as _)), [i, 0, 1, 2, 3, 4, 5, 6]); + let tx_frame = Frame::new_data(unwrap!(StandardId::new(i as _)), &[i, 0, 1, 2, 3, 4, 5, 6]); can.write(&tx_frame).await; match can.read().await { Ok(env) => match env.frame.id() { Id::Extended(id) => { - defmt::println!("Extended Frame id={:x} {:02x}", id.as_raw(), env.frame.data().unwrap()); + defmt::println!("Extended Frame id={:x} {:02x}", id.as_raw(), env.frame.data()); } Id::Standard(id) => { - defmt::println!("Standard Frame id={:x} {:02x}", id.as_raw(), env.frame.data().unwrap()); + defmt::println!("Standard Frame id={:x} {:02x}", id.as_raw(), env.frame.data()); } }, Err(err) => { diff --git a/examples/stm32f4/src/bin/can.rs b/examples/stm32f4/src/bin/can.rs index b20af8cf1..2ed631a46 100644 --- a/examples/stm32f4/src/bin/can.rs +++ b/examples/stm32f4/src/bin/can.rs @@ -51,7 +51,7 @@ async fn main(_spawner: Spawner) { let mut i: u8 = 0; loop { - let tx_frame = Frame::new_data(unwrap!(StandardId::new(i as _)), [i]); + let tx_frame = Frame::new_data(unwrap!(StandardId::new(i as _)), &[i]); let tx_ts = Instant::now(); can.write(&tx_frame).await; @@ -65,7 +65,7 @@ async fn main(_spawner: Spawner) { info!( "loopback frame {=u8}, latency: {} us", - unwrap!(envelope.frame.data())[0], + envelope.frame.data()[0], latency.as_micros() ); i += 1; diff --git a/examples/stm32f7/src/bin/can.rs b/examples/stm32f7/src/bin/can.rs index c3e14bbf4..2701196ed 100644 --- a/examples/stm32f7/src/bin/can.rs +++ b/examples/stm32f7/src/bin/can.rs @@ -26,7 +26,7 @@ bind_interrupts!(struct Irqs { #[embassy_executor::task] pub async fn send_can_message(tx: &'static mut CanTx<'static, CAN3>) { loop { - let frame = Frame::new_data(unwrap!(StandardId::new(0 as _)), [0]); + let frame = Frame::new_data(unwrap!(StandardId::new(0 as _)), &[0]); tx.write(&frame).await; embassy_time::Timer::after_secs(1).await; } diff --git a/tests/stm32/src/bin/can.rs b/tests/stm32/src/bin/can.rs index e869e8fb9..e36137b38 100644 --- a/tests/stm32/src/bin/can.rs +++ b/tests/stm32/src/bin/can.rs @@ -60,7 +60,7 @@ async fn main(_spawner: Spawner) { let mut i: u8 = 0; loop { - let tx_frame = Frame::new_data(unwrap!(StandardId::new(i as _)), [i]); + let tx_frame = Frame::new_data(unwrap!(StandardId::new(i as _)), &[i]); info!("Transmitting frame..."); let tx_ts = Instant::now(); @@ -70,7 +70,7 @@ async fn main(_spawner: Spawner) { info!("Frame received!"); info!("loopback time {}", envelope.ts); - info!("loopback frame {=u8}", envelope.frame.data().unwrap()[0]); + info!("loopback frame {=u8}", envelope.frame.data()[0]); let latency = envelope.ts.saturating_duration_since(tx_ts); info!("loopback latency {} us", latency.as_micros());