net: remove packet pool.
The pool was prone to deadlocks, especially due to having a single pool for rx+tx. If the pool got full with rx'd packets it would deadlock because processing a rx packet requires doing another allocation on the pool, for the possibly tx'd response, before deallocating the rx'd packet. This also allows Device impls to allocate the packet memory in a particular RAM kind, if needed for example to do DMA. The `Device` trait is now token-based, like smoltcp's. In the end, this is better because it allows callers to manage memory however they want (including with a pool if they want to).
This commit is contained in:
parent
47747d3b73
commit
ac74613b5a
5 changed files with 105 additions and 197 deletions
|
@ -1,10 +1,7 @@
|
||||||
use core::task::Waker;
|
use core::task::Context;
|
||||||
|
|
||||||
use smoltcp::phy::{Device as SmolDevice, DeviceCapabilities};
|
use smoltcp::phy;
|
||||||
use smoltcp::time::Instant as SmolInstant;
|
pub use smoltcp::phy::{Checksum, ChecksumCapabilities, DeviceCapabilities, Medium};
|
||||||
|
|
||||||
use crate::packet_pool::PacketBoxExt;
|
|
||||||
use crate::{Packet, PacketBox, PacketBuf};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum LinkState {
|
pub enum LinkState {
|
||||||
|
@ -13,115 +10,133 @@ pub enum LinkState {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Device {
|
pub trait Device {
|
||||||
fn is_transmit_ready(&mut self) -> bool;
|
type RxToken<'a>: RxToken
|
||||||
fn transmit(&mut self, pkt: PacketBuf);
|
where
|
||||||
fn receive(&mut self) -> Option<PacketBuf>;
|
Self: 'a;
|
||||||
|
type TxToken<'a>: TxToken
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
fn register_waker(&mut self, waker: &Waker);
|
fn receive(&mut self, cx: &mut Context) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)>;
|
||||||
fn capabilities(&self) -> DeviceCapabilities;
|
fn transmit(&mut self, cx: &mut Context) -> Option<Self::TxToken<'_>>;
|
||||||
fn link_state(&mut self) -> LinkState;
|
fn link_state(&mut self, cx: &mut Context) -> LinkState;
|
||||||
|
|
||||||
|
fn capabilities(&self) -> phy::DeviceCapabilities;
|
||||||
fn ethernet_address(&self) -> [u8; 6];
|
fn ethernet_address(&self) -> [u8; 6];
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ?Sized + Device> Device for &mut T {
|
impl<T: ?Sized + Device> Device for &mut T {
|
||||||
fn is_transmit_ready(&mut self) -> bool {
|
type RxToken<'a> = T::RxToken<'a>
|
||||||
T::is_transmit_ready(self)
|
where
|
||||||
|
Self: 'a;
|
||||||
|
type TxToken<'a> = T::TxToken<'a>
|
||||||
|
where
|
||||||
|
Self: 'a;
|
||||||
|
|
||||||
|
fn transmit(&mut self, cx: &mut Context) -> Option<Self::TxToken<'_>> {
|
||||||
|
T::transmit(self, cx)
|
||||||
}
|
}
|
||||||
fn transmit(&mut self, pkt: PacketBuf) {
|
fn receive(&mut self, cx: &mut Context) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
|
||||||
T::transmit(self, pkt)
|
T::receive(self, cx)
|
||||||
}
|
}
|
||||||
fn receive(&mut self) -> Option<PacketBuf> {
|
fn capabilities(&self) -> phy::DeviceCapabilities {
|
||||||
T::receive(self)
|
|
||||||
}
|
|
||||||
fn register_waker(&mut self, waker: &Waker) {
|
|
||||||
T::register_waker(self, waker)
|
|
||||||
}
|
|
||||||
fn capabilities(&self) -> DeviceCapabilities {
|
|
||||||
T::capabilities(self)
|
T::capabilities(self)
|
||||||
}
|
}
|
||||||
fn link_state(&mut self) -> LinkState {
|
fn link_state(&mut self, cx: &mut Context) -> LinkState {
|
||||||
T::link_state(self)
|
T::link_state(self, cx)
|
||||||
}
|
}
|
||||||
fn ethernet_address(&self) -> [u8; 6] {
|
fn ethernet_address(&self) -> [u8; 6] {
|
||||||
T::ethernet_address(self)
|
T::ethernet_address(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DeviceAdapter<D: Device> {
|
/// A token to receive a single network packet.
|
||||||
pub device: D,
|
pub trait RxToken {
|
||||||
caps: DeviceCapabilities,
|
/// Consumes the token to receive a single network packet.
|
||||||
|
///
|
||||||
|
/// This method receives a packet and then calls the given closure `f` with the raw
|
||||||
|
/// packet bytes as argument.
|
||||||
|
fn consume<R, F>(self, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut [u8]) -> R;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Device> DeviceAdapter<D> {
|
/// A token to transmit a single network packet.
|
||||||
pub(crate) fn new(device: D) -> Self {
|
pub trait TxToken {
|
||||||
Self {
|
/// Consumes the token to send a single network packet.
|
||||||
caps: device.capabilities(),
|
///
|
||||||
device,
|
/// This method constructs a transmit buffer of size `len` and calls the passed
|
||||||
}
|
/// closure `f` with a mutable reference to that buffer. The closure should construct
|
||||||
}
|
/// a valid network packet (e.g. an ethernet packet) in the buffer. When the closure
|
||||||
|
/// returns, the transmit buffer is sent out.
|
||||||
|
fn consume<R, F>(self, len: usize, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut [u8]) -> R;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D: Device> SmolDevice for DeviceAdapter<D> {
|
///////////////////////////
|
||||||
type RxToken<'a> = RxToken where Self: 'a;
|
|
||||||
type TxToken<'a> = TxToken<'a, D> where Self: 'a;
|
pub(crate) struct DeviceAdapter<'d, 'c, T>
|
||||||
|
where
|
||||||
|
T: Device,
|
||||||
|
{
|
||||||
|
// must be Some when actually using this to rx/tx
|
||||||
|
pub cx: Option<&'d mut Context<'c>>,
|
||||||
|
pub inner: &'d mut T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'d, 'c, T> phy::Device for DeviceAdapter<'d, 'c, T>
|
||||||
|
where
|
||||||
|
T: Device,
|
||||||
|
{
|
||||||
|
type RxToken<'a> = RxTokenAdapter<T::RxToken<'a>> where Self: 'a;
|
||||||
|
type TxToken<'a> = TxTokenAdapter<T::TxToken<'a>> where Self: 'a;
|
||||||
|
|
||||||
fn receive(&mut self) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
|
fn receive(&mut self) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
|
||||||
let tx_pkt = PacketBox::new(Packet::new())?;
|
self.inner
|
||||||
let rx_pkt = self.device.receive()?;
|
.receive(self.cx.as_deref_mut().unwrap())
|
||||||
let rx_token = RxToken { pkt: rx_pkt };
|
.map(|(rx, tx)| (RxTokenAdapter(rx), TxTokenAdapter(tx)))
|
||||||
let tx_token = TxToken {
|
|
||||||
device: &mut self.device,
|
|
||||||
pkt: tx_pkt,
|
|
||||||
};
|
|
||||||
|
|
||||||
Some((rx_token, tx_token))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct a transmit token.
|
/// Construct a transmit token.
|
||||||
fn transmit(&mut self) -> Option<Self::TxToken<'_>> {
|
fn transmit(&mut self) -> Option<Self::TxToken<'_>> {
|
||||||
if !self.device.is_transmit_ready() {
|
self.inner.transmit(self.cx.as_deref_mut().unwrap()).map(TxTokenAdapter)
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_pkt = PacketBox::new(Packet::new())?;
|
|
||||||
Some(TxToken {
|
|
||||||
device: &mut self.device,
|
|
||||||
pkt: tx_pkt,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a description of device capabilities.
|
/// Get a description of device capabilities.
|
||||||
fn capabilities(&self) -> DeviceCapabilities {
|
fn capabilities(&self) -> phy::DeviceCapabilities {
|
||||||
self.caps.clone()
|
self.inner.capabilities()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RxToken {
|
pub(crate) struct RxTokenAdapter<T>(T)
|
||||||
pkt: PacketBuf,
|
where
|
||||||
}
|
T: RxToken;
|
||||||
|
|
||||||
impl smoltcp::phy::RxToken for RxToken {
|
impl<T> phy::RxToken for RxTokenAdapter<T>
|
||||||
fn consume<R, F>(mut self, _timestamp: SmolInstant, f: F) -> smoltcp::Result<R>
|
where
|
||||||
|
T: RxToken,
|
||||||
|
{
|
||||||
|
fn consume<R, F>(self, _timestamp: smoltcp::time::Instant, f: F) -> smoltcp::Result<R>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
|
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
|
||||||
{
|
{
|
||||||
f(&mut self.pkt)
|
self.0.consume(|buf| f(buf))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TxToken<'a, D: Device> {
|
pub(crate) struct TxTokenAdapter<T>(T)
|
||||||
device: &'a mut D,
|
where
|
||||||
pkt: PacketBox,
|
T: TxToken;
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, D: Device> smoltcp::phy::TxToken for TxToken<'a, D> {
|
impl<T> phy::TxToken for TxTokenAdapter<T>
|
||||||
fn consume<R, F>(self, _timestamp: SmolInstant, len: usize, f: F) -> smoltcp::Result<R>
|
where
|
||||||
|
T: TxToken,
|
||||||
|
{
|
||||||
|
fn consume<R, F>(self, _timestamp: smoltcp::time::Instant, len: usize, f: F) -> smoltcp::Result<R>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
|
F: FnOnce(&mut [u8]) -> smoltcp::Result<R>,
|
||||||
{
|
{
|
||||||
let mut buf = self.pkt.slice(0..len);
|
self.0.consume(len, |buf| f(buf))
|
||||||
let r = f(&mut buf)?;
|
|
||||||
self.device.transmit(buf);
|
|
||||||
Ok(r)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,9 @@
|
||||||
// This mod MUST go first, so that the others see its macros.
|
// This mod MUST go first, so that the others see its macros.
|
||||||
pub(crate) mod fmt;
|
pub(crate) mod fmt;
|
||||||
|
|
||||||
mod device;
|
pub mod device;
|
||||||
mod packet_pool;
|
|
||||||
mod stack;
|
mod stack;
|
||||||
|
|
||||||
pub use device::{Device, LinkState};
|
|
||||||
pub use packet_pool::{Packet, PacketBox, PacketBoxExt, PacketBuf, MTU};
|
|
||||||
pub use stack::{Config, ConfigStrategy, Stack, StackResources};
|
pub use stack::{Config, ConfigStrategy, Stack, StackResources};
|
||||||
|
|
||||||
#[cfg(feature = "tcp")]
|
#[cfg(feature = "tcp")]
|
||||||
|
@ -23,7 +20,6 @@ pub mod tcp;
|
||||||
pub mod udp;
|
pub mod udp;
|
||||||
|
|
||||||
// smoltcp reexports
|
// smoltcp reexports
|
||||||
pub use smoltcp::phy::{DeviceCapabilities, Medium};
|
|
||||||
pub use smoltcp::time::{Duration as SmolDuration, Instant as SmolInstant};
|
pub use smoltcp::time::{Duration as SmolDuration, Instant as SmolInstant};
|
||||||
#[cfg(feature = "medium-ethernet")]
|
#[cfg(feature = "medium-ethernet")]
|
||||||
pub use smoltcp::wire::{EthernetAddress, HardwareAddress};
|
pub use smoltcp::wire::{EthernetAddress, HardwareAddress};
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
use core::ops::{Deref, DerefMut, Range};
|
|
||||||
|
|
||||||
use as_slice::{AsMutSlice, AsSlice};
|
|
||||||
use atomic_pool::{pool, Box};
|
|
||||||
|
|
||||||
pub const MTU: usize = 1516;
|
|
||||||
|
|
||||||
#[cfg(feature = "pool-4")]
|
|
||||||
pub const PACKET_POOL_SIZE: usize = 4;
|
|
||||||
|
|
||||||
#[cfg(feature = "pool-8")]
|
|
||||||
pub const PACKET_POOL_SIZE: usize = 8;
|
|
||||||
|
|
||||||
#[cfg(feature = "pool-16")]
|
|
||||||
pub const PACKET_POOL_SIZE: usize = 16;
|
|
||||||
|
|
||||||
#[cfg(feature = "pool-32")]
|
|
||||||
pub const PACKET_POOL_SIZE: usize = 32;
|
|
||||||
|
|
||||||
#[cfg(feature = "pool-64")]
|
|
||||||
pub const PACKET_POOL_SIZE: usize = 64;
|
|
||||||
|
|
||||||
#[cfg(feature = "pool-128")]
|
|
||||||
pub const PACKET_POOL_SIZE: usize = 128;
|
|
||||||
|
|
||||||
pool!(pub PacketPool: [Packet; PACKET_POOL_SIZE]);
|
|
||||||
pub type PacketBox = Box<PacketPool>;
|
|
||||||
|
|
||||||
#[repr(align(4))]
|
|
||||||
pub struct Packet(pub [u8; MTU]);
|
|
||||||
|
|
||||||
impl Packet {
|
|
||||||
pub const fn new() -> Self {
|
|
||||||
Self([0; MTU])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PacketBoxExt {
|
|
||||||
fn slice(self, range: Range<usize>) -> PacketBuf;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PacketBoxExt for PacketBox {
|
|
||||||
fn slice(self, range: Range<usize>) -> PacketBuf {
|
|
||||||
PacketBuf { packet: self, range }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsSlice for Packet {
|
|
||||||
type Element = u8;
|
|
||||||
|
|
||||||
fn as_slice(&self) -> &[Self::Element] {
|
|
||||||
&self.deref()[..]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsMutSlice for Packet {
|
|
||||||
fn as_mut_slice(&mut self) -> &mut [Self::Element] {
|
|
||||||
&mut self.deref_mut()[..]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Packet {
|
|
||||||
type Target = [u8; MTU];
|
|
||||||
|
|
||||||
fn deref(&self) -> &[u8; MTU] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Packet {
|
|
||||||
fn deref_mut(&mut self) -> &mut [u8; MTU] {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PacketBuf {
|
|
||||||
packet: PacketBox,
|
|
||||||
range: Range<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsSlice for PacketBuf {
|
|
||||||
type Element = u8;
|
|
||||||
|
|
||||||
fn as_slice(&self) -> &[Self::Element] {
|
|
||||||
&self.packet[self.range.clone()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsMutSlice for PacketBuf {
|
|
||||||
fn as_mut_slice(&mut self) -> &mut [Self::Element] {
|
|
||||||
&mut self.packet[self.range.clone()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for PacketBuf {
|
|
||||||
type Target = [u8];
|
|
||||||
|
|
||||||
fn deref(&self) -> &[u8] {
|
|
||||||
&self.packet[self.range.clone()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for PacketBuf {
|
|
||||||
fn deref_mut(&mut self) -> &mut [u8] {
|
|
||||||
&mut self.packet[self.range.clone()]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,7 @@ use smoltcp::iface::{Interface, InterfaceBuilder, SocketSet, SocketStorage};
|
||||||
#[cfg(feature = "medium-ethernet")]
|
#[cfg(feature = "medium-ethernet")]
|
||||||
use smoltcp::iface::{Neighbor, NeighborCache, Route, Routes};
|
use smoltcp::iface::{Neighbor, NeighborCache, Route, Routes};
|
||||||
#[cfg(feature = "medium-ethernet")]
|
#[cfg(feature = "medium-ethernet")]
|
||||||
use smoltcp::phy::{Device as _, Medium};
|
use smoltcp::phy::Medium;
|
||||||
#[cfg(feature = "dhcpv4")]
|
#[cfg(feature = "dhcpv4")]
|
||||||
use smoltcp::socket::dhcpv4;
|
use smoltcp::socket::dhcpv4;
|
||||||
use smoltcp::time::Instant as SmolInstant;
|
use smoltcp::time::Instant as SmolInstant;
|
||||||
|
@ -67,7 +67,7 @@ pub struct Stack<D: Device> {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Inner<D: Device> {
|
struct Inner<D: Device> {
|
||||||
device: DeviceAdapter<D>,
|
device: D,
|
||||||
link_up: bool,
|
link_up: bool,
|
||||||
config: Option<Config>,
|
config: Option<Config>,
|
||||||
#[cfg(feature = "dhcpv4")]
|
#[cfg(feature = "dhcpv4")]
|
||||||
|
@ -83,7 +83,7 @@ pub(crate) struct SocketStack {
|
||||||
|
|
||||||
impl<D: Device + 'static> Stack<D> {
|
impl<D: Device + 'static> Stack<D> {
|
||||||
pub fn new<const ADDR: usize, const SOCK: usize, const NEIGH: usize>(
|
pub fn new<const ADDR: usize, const SOCK: usize, const NEIGH: usize>(
|
||||||
device: D,
|
mut device: D,
|
||||||
config: ConfigStrategy,
|
config: ConfigStrategy,
|
||||||
resources: &'static mut StackResources<ADDR, SOCK, NEIGH>,
|
resources: &'static mut StackResources<ADDR, SOCK, NEIGH>,
|
||||||
random_seed: u64,
|
random_seed: u64,
|
||||||
|
@ -98,8 +98,6 @@ impl<D: Device + 'static> Stack<D> {
|
||||||
[0, 0, 0, 0, 0, 0]
|
[0, 0, 0, 0, 0, 0]
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut device = DeviceAdapter::new(device);
|
|
||||||
|
|
||||||
let mut b = InterfaceBuilder::new();
|
let mut b = InterfaceBuilder::new();
|
||||||
b = b.ip_addrs(&mut resources.addresses[..]);
|
b = b.ip_addrs(&mut resources.addresses[..]);
|
||||||
b = b.random_seed(random_seed);
|
b = b.random_seed(random_seed);
|
||||||
|
@ -111,7 +109,10 @@ impl<D: Device + 'static> Stack<D> {
|
||||||
b = b.routes(Routes::new(&mut resources.routes[..]));
|
b = b.routes(Routes::new(&mut resources.routes[..]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let iface = b.finalize(&mut device);
|
let iface = b.finalize(&mut DeviceAdapter {
|
||||||
|
inner: &mut device,
|
||||||
|
cx: None,
|
||||||
|
});
|
||||||
|
|
||||||
let sockets = SocketSet::new(&mut resources.sockets[..]);
|
let sockets = SocketSet::new(&mut resources.sockets[..]);
|
||||||
|
|
||||||
|
@ -155,7 +156,7 @@ impl<D: Device + 'static> Stack<D> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ethernet_address(&self) -> [u8; 6] {
|
pub fn ethernet_address(&self) -> [u8; 6] {
|
||||||
self.with(|_s, i| i.device.device.ethernet_address())
|
self.with(|_s, i| i.device.ethernet_address())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_link_up(&self) -> bool {
|
pub fn is_link_up(&self) -> bool {
|
||||||
|
@ -238,11 +239,14 @@ impl<D: Device + 'static> Inner<D> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) {
|
fn poll(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) {
|
||||||
self.device.device.register_waker(cx.waker());
|
|
||||||
s.waker.register(cx.waker());
|
s.waker.register(cx.waker());
|
||||||
|
|
||||||
let timestamp = instant_to_smoltcp(Instant::now());
|
let timestamp = instant_to_smoltcp(Instant::now());
|
||||||
if s.iface.poll(timestamp, &mut self.device, &mut s.sockets).is_err() {
|
let mut smoldev = DeviceAdapter {
|
||||||
|
cx: Some(cx),
|
||||||
|
inner: &mut self.device,
|
||||||
|
};
|
||||||
|
if s.iface.poll(timestamp, &mut smoldev, &mut s.sockets).is_err() {
|
||||||
// If poll() returns error, it may not be done yet, so poll again later.
|
// If poll() returns error, it may not be done yet, so poll again later.
|
||||||
cx.waker().wake_by_ref();
|
cx.waker().wake_by_ref();
|
||||||
return;
|
return;
|
||||||
|
@ -250,7 +254,7 @@ impl<D: Device + 'static> Inner<D> {
|
||||||
|
|
||||||
// Update link up
|
// Update link up
|
||||||
let old_link_up = self.link_up;
|
let old_link_up = self.link_up;
|
||||||
self.link_up = self.device.device.link_state() == LinkState::Up;
|
self.link_up = self.device.link_state(cx) == LinkState::Up;
|
||||||
|
|
||||||
// Print when changed
|
// Print when changed
|
||||||
if old_link_up != self.link_up {
|
if old_link_up != self.link_up {
|
||||||
|
|
|
@ -9,8 +9,8 @@ use smoltcp::time::Duration;
|
||||||
use smoltcp::wire::{IpEndpoint, IpListenEndpoint};
|
use smoltcp::wire::{IpEndpoint, IpListenEndpoint};
|
||||||
|
|
||||||
use super::stack::Stack;
|
use super::stack::Stack;
|
||||||
|
use crate::device::Device;
|
||||||
use crate::stack::SocketStack;
|
use crate::stack::SocketStack;
|
||||||
use crate::Device;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||||
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
|
||||||
|
|
Loading…
Reference in a new issue