Shared frame types.

Remove BXCAN speciffic id and frame modules

Remove SizedClassicData
This commit is contained in:
Corey Schuhen 2024-03-07 22:48:25 +10:00
parent 35f284ec22
commit 12a3af5043
10 changed files with 138 additions and 416 deletions

View File

@ -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<Id>, data: impl Into<Data>) -> 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<Id>, 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<core::cmp::Ordering> {
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<Self> {
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);

View File

@ -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<Self> {
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<Self> {
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<StandardId> for Id {
#[inline]
fn from(id: StandardId) -> Self {
Id::Standard(id)
}
}
impl From<ExtendedId> for Id {
#[inline]
fn from(id: ExtendedId) -> Self {
Id::Extended(id)
}
}

View File

@ -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<IdReg> 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<Fra
}
// Read the frame.
let mut frame = Frame {
id: IdReg(rx.rir().read().0),
data: [0; 8].into(),
};
frame.data[0..4].copy_from_slice(&rx.rdlr().read().0.to_ne_bytes());
frame.data[4..8].copy_from_slice(&rx.rdhr().read().0.to_ne_bytes());
frame.data.len = rx.rdtr().read().dlc();
let id = IdReg(rx.rir().read().0).id();
let mut data = [0xff; 8];
data[0..4].copy_from_slice(&rx.rdlr().read().0.to_ne_bytes());
data[4..8].copy_from_slice(&rx.rdhr().read().0.to_ne_bytes());
let len = rx.rdtr().read().dlc();
// Release the mailbox.
rfr.write(|w| w.set_rfom(true));
Ok(frame)
Ok(Frame::new(
Header::new(id, len, false),
ClassicData::new(&data).unwrap(),
))
}
/// Identifies one of the two receive FIFOs.

View File

@ -6,7 +6,7 @@ use core::task::Poll;
pub mod bx;
pub use bx::{filter, Data, ExtendedId, Fifo, Frame, Id, StandardId};
pub use bx::{filter, Data, ExtendedId, Fifo, Frame, Header, Id, StandardId};
use embassy_hal_internal::{into_ref, PeripheralRef};
use futures::FutureExt;
@ -18,19 +18,20 @@ use crate::{interrupt, peripherals, Peripheral};
pub mod enums;
use enums::*;
pub mod frame;
pub mod util;
/// Contains CAN frame and additional metadata.
///
/// Timestamp is available if `time` feature is enabled.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct Envelope {
/// Reception time.
#[cfg(feature = "time")]
pub ts: embassy_time::Instant,
/// The actual CAN frame.
pub frame: crate::can::bx::Frame,
pub frame: Frame,
}
/// Interrupt handler.
@ -260,20 +261,20 @@ impl<'d, T: Instance> 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,

View File

@ -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;
}

View File

@ -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<ClassicData>) -> ClassicFrame {
ClassicFrame {
can_header,
data: data.into(),
}
}
/// Creates a new data frame.
pub fn new_data(id: impl Into<embedded_can::Id>, 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 {

View File

@ -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) => {

View File

@ -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;

View File

@ -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;
}

View File

@ -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());