Update to latest embassy, atomic-pool, smoltcp
This commit is contained in:
@ -1,80 +1,59 @@
|
||||
use embassy::util::Forever;
|
||||
use heapless::Vec;
|
||||
use smoltcp::dhcp::Dhcpv4Client;
|
||||
use smoltcp::socket::{RawPacketMetadata, RawSocketBuffer};
|
||||
use smoltcp::socket::{Dhcpv4Event, Dhcpv4Socket, SocketHandle};
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::Ipv4Address;
|
||||
|
||||
use super::*;
|
||||
use crate::device::LinkState;
|
||||
use crate::fmt::*;
|
||||
use crate::{Interface, SocketSet};
|
||||
|
||||
pub struct DhcpResources {
|
||||
rx_buffer: [u8; 900],
|
||||
tx_buffer: [u8; 600],
|
||||
rx_meta: [RawPacketMetadata; 1],
|
||||
tx_meta: [RawPacketMetadata; 1],
|
||||
}
|
||||
|
||||
pub struct DhcpConfigurator {
|
||||
client: Option<Dhcpv4Client>,
|
||||
handle: Option<SocketHandle>,
|
||||
}
|
||||
|
||||
impl DhcpConfigurator {
|
||||
pub fn new() -> Self {
|
||||
Self { client: None }
|
||||
Self { handle: None }
|
||||
}
|
||||
}
|
||||
|
||||
static DHCP_RESOURCES: Forever<DhcpResources> = Forever::new();
|
||||
|
||||
impl Configurator for DhcpConfigurator {
|
||||
fn poll(
|
||||
&mut self,
|
||||
iface: &mut Interface,
|
||||
sockets: &mut SocketSet,
|
||||
timestamp: Instant,
|
||||
) -> Option<Config> {
|
||||
if self.client.is_none() {
|
||||
let res = DHCP_RESOURCES.put(DhcpResources {
|
||||
rx_buffer: [0; 900],
|
||||
tx_buffer: [0; 600],
|
||||
rx_meta: [RawPacketMetadata::EMPTY; 1],
|
||||
tx_meta: [RawPacketMetadata::EMPTY; 1],
|
||||
});
|
||||
let rx_buffer = RawSocketBuffer::new(&mut res.rx_meta[..], &mut res.rx_buffer[..]);
|
||||
let tx_buffer = RawSocketBuffer::new(&mut res.tx_meta[..], &mut res.tx_buffer[..]);
|
||||
let dhcp = Dhcpv4Client::new(sockets, rx_buffer, tx_buffer, timestamp);
|
||||
info!("created dhcp");
|
||||
self.client = Some(dhcp)
|
||||
_timestamp: Instant,
|
||||
) -> Event {
|
||||
if self.handle.is_none() {
|
||||
let handle = sockets.add(Dhcpv4Socket::new());
|
||||
self.handle = Some(handle)
|
||||
}
|
||||
|
||||
let client = self.client.as_mut().unwrap();
|
||||
let mut socket = sockets.get::<Dhcpv4Socket>(self.handle.unwrap());
|
||||
|
||||
let link_up = iface.device_mut().device.link_state() == LinkState::Up;
|
||||
if !link_up {
|
||||
client.reset(timestamp);
|
||||
return Some(Config::Down);
|
||||
socket.reset();
|
||||
return Event::Deconfigured;
|
||||
}
|
||||
|
||||
let config = client.poll(iface, sockets, timestamp).unwrap_or(None)?;
|
||||
match socket.poll() {
|
||||
Dhcpv4Event::NoChange => Event::NoChange,
|
||||
Dhcpv4Event::Deconfigured => Event::Deconfigured,
|
||||
Dhcpv4Event::Configured(config) => {
|
||||
let mut dns_servers = Vec::new();
|
||||
for s in &config.dns_servers {
|
||||
if let Some(addr) = s {
|
||||
dns_servers.push(addr.clone()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if config.address.is_none() {
|
||||
return Some(Config::Down);
|
||||
}
|
||||
|
||||
let mut dns_servers = Vec::new();
|
||||
for s in &config.dns_servers {
|
||||
if let Some(addr) = s {
|
||||
dns_servers.push(addr.clone()).unwrap();
|
||||
Event::Configured(Config {
|
||||
address: config.address,
|
||||
gateway: config.router,
|
||||
dns_servers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return Some(Config::Up(UpConfig {
|
||||
address: config.address.unwrap(),
|
||||
gateway: config.router.unwrap_or(Ipv4Address::UNSPECIFIED),
|
||||
dns_servers,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -6,29 +6,33 @@ use smoltcp::wire::{Ipv4Address, Ipv4Cidr};
|
||||
use crate::fmt::*;
|
||||
use crate::{Interface, SocketSet};
|
||||
|
||||
mod dhcp;
|
||||
mod statik;
|
||||
pub use dhcp::DhcpConfigurator;
|
||||
pub use statik::StaticConfigurator;
|
||||
|
||||
#[cfg(feature = "dhcpv4")]
|
||||
mod dhcp;
|
||||
#[cfg(feature = "dhcpv4")]
|
||||
pub use dhcp::DhcpConfigurator;
|
||||
|
||||
/// Return value for the `Configurator::poll` function
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Config {
|
||||
Down,
|
||||
Up(UpConfig),
|
||||
pub enum Event {
|
||||
/// No change has occured to the configuration.
|
||||
NoChange,
|
||||
/// Configuration has been lost (for example, DHCP lease has expired)
|
||||
Deconfigured,
|
||||
/// Configuration has been newly acquired, or modified.
|
||||
Configured(Config),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpConfig {
|
||||
pub struct Config {
|
||||
pub address: Ipv4Cidr,
|
||||
pub gateway: Ipv4Address,
|
||||
pub gateway: Option<Ipv4Address>,
|
||||
pub dns_servers: Vec<Ipv4Address, U3>,
|
||||
}
|
||||
|
||||
pub trait Configurator {
|
||||
fn poll(
|
||||
&mut self,
|
||||
iface: &mut Interface,
|
||||
sockets: &mut SocketSet,
|
||||
timestamp: Instant,
|
||||
) -> Option<Config>;
|
||||
fn poll(&mut self, iface: &mut Interface, sockets: &mut SocketSet, timestamp: Instant)
|
||||
-> Event;
|
||||
}
|
||||
|
@ -5,12 +5,16 @@ use crate::fmt::*;
|
||||
use crate::{Interface, SocketSet};
|
||||
|
||||
pub struct StaticConfigurator {
|
||||
config: UpConfig,
|
||||
config: Config,
|
||||
returned: bool,
|
||||
}
|
||||
|
||||
impl StaticConfigurator {
|
||||
pub fn new(config: UpConfig) -> Self {
|
||||
Self { config }
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
config,
|
||||
returned: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +24,12 @@ impl Configurator for StaticConfigurator {
|
||||
_iface: &mut Interface,
|
||||
_sockets: &mut SocketSet,
|
||||
_timestamp: Instant,
|
||||
) -> Option<Config> {
|
||||
Some(Config::Up(self.config.clone()))
|
||||
) -> Event {
|
||||
if self.returned {
|
||||
Event::NoChange
|
||||
} else {
|
||||
self.returned = true;
|
||||
Event::Configured(self.config.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use smoltcp::phy::DeviceCapabilities;
|
||||
use smoltcp::time::Instant as SmolInstant;
|
||||
|
||||
use crate::fmt::*;
|
||||
use crate::packet_pool::PacketBoxExt;
|
||||
use crate::Result;
|
||||
use crate::{Packet, PacketBox, PacketBuf};
|
||||
|
||||
|
@ -7,18 +7,19 @@
|
||||
// This mod MUST go first, so that the others see its macros.
|
||||
pub(crate) mod fmt;
|
||||
|
||||
mod pool; // TODO extract to embassy, or to own crate
|
||||
|
||||
mod config;
|
||||
mod device;
|
||||
mod packet_pool;
|
||||
mod stack;
|
||||
mod tcp_socket;
|
||||
|
||||
pub use config::{Config, Configurator, DhcpConfigurator, StaticConfigurator, UpConfig};
|
||||
pub use config::{Config, Configurator, DhcpConfigurator, StaticConfigurator};
|
||||
pub use device::{Device, LinkState};
|
||||
pub use packet_pool::{Packet, PacketBox, PacketBuf};
|
||||
pub use packet_pool::{Packet, PacketBox, PacketBoxExt, PacketBuf};
|
||||
pub use stack::{init, is_init, run};
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
mod tcp_socket;
|
||||
#[cfg(feature = "tcp")]
|
||||
pub use tcp_socket::TcpSocket;
|
||||
|
||||
// smoltcp reexports
|
||||
@ -28,4 +29,4 @@ pub use smoltcp::time::Instant as SmolInstant;
|
||||
pub use smoltcp::wire::{IpAddress, IpCidr, Ipv4Address, Ipv4Cidr};
|
||||
pub type Interface = smoltcp::iface::Interface<'static, device::DeviceAdapter>;
|
||||
pub type SocketSet = smoltcp::socket::SocketSet<'static>;
|
||||
pub use smoltcp::{Error, Result};
|
||||
pub use smoltcp::{Error, Result};
|
||||
|
@ -1,7 +1,7 @@
|
||||
use as_slice::{AsMutSlice, AsSlice};
|
||||
use core::ops::{Deref, DerefMut, Range};
|
||||
|
||||
use super::pool::{BitPool, Box, StaticPool};
|
||||
use atomic_pool::{pool, Box};
|
||||
|
||||
pub const MTU: usize = 1514;
|
||||
pub const PACKET_POOL_SIZE: usize = 4;
|
||||
@ -17,8 +17,12 @@ impl Packet {
|
||||
}
|
||||
}
|
||||
|
||||
impl Box<PacketPool> {
|
||||
pub fn slice(self, range: Range<usize>) -> PacketBuf {
|
||||
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,
|
||||
|
@ -1,245 +0,0 @@
|
||||
#![macro_use]
|
||||
|
||||
use as_slice::{AsMutSlice, AsSlice};
|
||||
use core::cmp;
|
||||
use core::fmt;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use core::mem::MaybeUninit;
|
||||
use core::ops::{Deref, DerefMut};
|
||||
use core::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::fmt::{assert, *};
|
||||
|
||||
struct AtomicBitset<const N: usize>
|
||||
where
|
||||
[AtomicU32; (N + 31) / 32]: Sized,
|
||||
{
|
||||
used: [AtomicU32; (N + 31) / 32],
|
||||
}
|
||||
|
||||
impl<const N: usize> AtomicBitset<N>
|
||||
where
|
||||
[AtomicU32; (N + 31) / 32]: Sized,
|
||||
{
|
||||
const fn new() -> Self {
|
||||
const Z: AtomicU32 = AtomicU32::new(0);
|
||||
Self {
|
||||
used: [Z; (N + 31) / 32],
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc(&self) -> Option<usize> {
|
||||
for (i, val) in self.used.iter().enumerate() {
|
||||
let res = val.fetch_update(Ordering::AcqRel, Ordering::Acquire, |val| {
|
||||
let n = val.trailing_ones() as usize + i * 32;
|
||||
if n >= N {
|
||||
None
|
||||
} else {
|
||||
Some(val | (1 << n))
|
||||
}
|
||||
});
|
||||
if let Ok(val) = res {
|
||||
let n = val.trailing_ones() as usize + i * 32;
|
||||
return Some(n);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn free(&self, i: usize) {
|
||||
assert!(i < N);
|
||||
self.used[i / 32].fetch_and(!(1 << ((i % 32) as u32)), Ordering::AcqRel);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Pool<T> {
|
||||
fn alloc(&self) -> Option<*mut T>;
|
||||
unsafe fn free(&self, p: *mut T);
|
||||
}
|
||||
|
||||
pub struct BitPool<T, const N: usize>
|
||||
where
|
||||
[AtomicU32; (N + 31) / 32]: Sized,
|
||||
{
|
||||
used: AtomicBitset<N>,
|
||||
data: MaybeUninit<[T; N]>,
|
||||
}
|
||||
|
||||
impl<T, const N: usize> BitPool<T, N>
|
||||
where
|
||||
[AtomicU32; (N + 31) / 32]: Sized,
|
||||
{
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
used: AtomicBitset::new(),
|
||||
data: MaybeUninit::uninit(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, const N: usize> Pool<T> for BitPool<T, N>
|
||||
where
|
||||
[AtomicU32; (N + 31) / 32]: Sized,
|
||||
{
|
||||
fn alloc(&self) -> Option<*mut T> {
|
||||
let n = self.used.alloc()?;
|
||||
let origin = self.data.as_ptr() as *mut T;
|
||||
Some(unsafe { origin.add(n) })
|
||||
}
|
||||
|
||||
/// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet.
|
||||
unsafe fn free(&self, p: *mut T) {
|
||||
let origin = self.data.as_ptr() as *mut T;
|
||||
let n = p.offset_from(origin);
|
||||
assert!(n >= 0);
|
||||
assert!((n as usize) < N);
|
||||
self.used.free(n as usize);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait StaticPool: 'static {
|
||||
type Item: 'static;
|
||||
type Pool: Pool<Self::Item>;
|
||||
fn get() -> &'static Self::Pool;
|
||||
}
|
||||
|
||||
pub struct Box<P: StaticPool> {
|
||||
ptr: *mut P::Item,
|
||||
}
|
||||
|
||||
impl<P: StaticPool> Box<P> {
|
||||
pub fn new(item: P::Item) -> Option<Self> {
|
||||
let p = match P::get().alloc() {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
warn!("alloc failed!");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
//trace!("allocated {:u32}", p as u32);
|
||||
unsafe { p.write(item) };
|
||||
Some(Self { ptr: p })
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> Drop for Box<P> {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
//trace!("dropping {:u32}", self.ptr as u32);
|
||||
self.ptr.drop_in_place();
|
||||
P::get().free(self.ptr);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<P: StaticPool> Send for Box<P> where P::Item: Send {}
|
||||
|
||||
unsafe impl<P: StaticPool> Sync for Box<P> where P::Item: Sync {}
|
||||
|
||||
unsafe impl<P: StaticPool> stable_deref_trait::StableDeref for Box<P> {}
|
||||
|
||||
impl<P: StaticPool> AsSlice for Box<P>
|
||||
where
|
||||
P::Item: AsSlice,
|
||||
{
|
||||
type Element = <P::Item as AsSlice>::Element;
|
||||
|
||||
fn as_slice(&self) -> &[Self::Element] {
|
||||
self.deref().as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> AsMutSlice for Box<P>
|
||||
where
|
||||
P::Item: AsMutSlice,
|
||||
{
|
||||
fn as_mut_slice(&mut self) -> &mut [Self::Element] {
|
||||
self.deref_mut().as_mut_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> Deref for Box<P> {
|
||||
type Target = P::Item;
|
||||
|
||||
fn deref(&self) -> &P::Item {
|
||||
unsafe { &*self.ptr }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> DerefMut for Box<P> {
|
||||
fn deref_mut(&mut self) -> &mut P::Item {
|
||||
unsafe { &mut *self.ptr }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> fmt::Debug for Box<P>
|
||||
where
|
||||
P::Item: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
<P::Item as fmt::Debug>::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> fmt::Display for Box<P>
|
||||
where
|
||||
P::Item: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
<P::Item as fmt::Display>::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> PartialEq for Box<P>
|
||||
where
|
||||
P::Item: PartialEq,
|
||||
{
|
||||
fn eq(&self, rhs: &Box<P>) -> bool {
|
||||
<P::Item as PartialEq>::eq(self, rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> Eq for Box<P> where P::Item: Eq {}
|
||||
|
||||
impl<P: StaticPool> PartialOrd for Box<P>
|
||||
where
|
||||
P::Item: PartialOrd,
|
||||
{
|
||||
fn partial_cmp(&self, rhs: &Box<P>) -> Option<cmp::Ordering> {
|
||||
<P::Item as PartialOrd>::partial_cmp(self, rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> Ord for Box<P>
|
||||
where
|
||||
P::Item: Ord,
|
||||
{
|
||||
fn cmp(&self, rhs: &Box<P>) -> cmp::Ordering {
|
||||
<P::Item as Ord>::cmp(self, rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: StaticPool> Hash for Box<P>
|
||||
where
|
||||
P::Item: Hash,
|
||||
{
|
||||
fn hash<H>(&self, state: &mut H)
|
||||
where
|
||||
H: Hasher,
|
||||
{
|
||||
<P::Item as Hash>::hash(self, state)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! pool {
|
||||
($vis:vis $name:ident: [$ty:ty; $size:expr]) => {
|
||||
$vis struct $name;
|
||||
impl StaticPool for $name {
|
||||
type Item = $ty;
|
||||
type Pool = BitPool<$ty, $size>;
|
||||
fn get() -> &'static Self::Pool {
|
||||
static POOL: BitPool<$ty, $size> = BitPool::new();
|
||||
&POOL
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
@ -11,14 +11,12 @@ use smoltcp::phy::Device as _;
|
||||
use smoltcp::phy::Medium;
|
||||
use smoltcp::socket::SocketSetItem;
|
||||
use smoltcp::time::Instant as SmolInstant;
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address};
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Cidr};
|
||||
|
||||
use crate::device::{Device, DeviceAdapter};
|
||||
use crate::config::Configurator;
|
||||
use crate::config::Event;
|
||||
use crate::device::{Device, DeviceAdapter, LinkState};
|
||||
use crate::fmt::*;
|
||||
use crate::{
|
||||
config::{Config, Configurator},
|
||||
device::LinkState,
|
||||
};
|
||||
use crate::{Interface, SocketSet};
|
||||
|
||||
const ADDRESSES_LEN: usize = 1;
|
||||
@ -68,39 +66,41 @@ impl Stack {
|
||||
}
|
||||
|
||||
fn poll_configurator(&mut self, timestamp: SmolInstant) {
|
||||
if let Some(config) = self
|
||||
let medium = self.iface.device().capabilities().medium;
|
||||
|
||||
match self
|
||||
.configurator
|
||||
.poll(&mut self.iface, &mut self.sockets, timestamp)
|
||||
{
|
||||
let medium = self.iface.device().capabilities().medium;
|
||||
Event::NoChange => {}
|
||||
Event::Configured(config) => {
|
||||
debug!("Acquired IP configuration:");
|
||||
|
||||
let (addr, gateway) = match config {
|
||||
Config::Up(config) => (config.address.into(), Some(config.gateway)),
|
||||
Config::Down => (IpCidr::new(Ipv4Address::UNSPECIFIED.into(), 32), None),
|
||||
};
|
||||
debug!(" IP address: {}", config.address);
|
||||
set_ipv4_addr(&mut self.iface, config.address);
|
||||
|
||||
self.iface.update_ip_addrs(|addrs| {
|
||||
let curr_addr = &mut addrs[0];
|
||||
if *curr_addr != addr {
|
||||
info!("IPv4 address: {:?} -> {:?}", *curr_addr, addr);
|
||||
*curr_addr = addr;
|
||||
}
|
||||
});
|
||||
|
||||
if medium == Medium::Ethernet {
|
||||
self.iface.routes_mut().update(|r| {
|
||||
let cidr = IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0);
|
||||
let curr_gateway = r.get(&cidr).map(|r| r.via_router);
|
||||
|
||||
if curr_gateway != gateway.map(|a| a.into()) {
|
||||
info!("IPv4 gateway: {:?} -> {:?}", curr_gateway, gateway);
|
||||
if let Some(gateway) = gateway {
|
||||
r.insert(cidr, Route::new_ipv4_gateway(gateway)).unwrap();
|
||||
} else {
|
||||
r.remove(&cidr);
|
||||
}
|
||||
if medium == Medium::Ethernet {
|
||||
if let Some(gateway) = config.gateway {
|
||||
debug!(" Default gateway: {}", gateway);
|
||||
self.iface
|
||||
.routes_mut()
|
||||
.add_default_ipv4_route(gateway)
|
||||
.unwrap();
|
||||
} else {
|
||||
debug!(" Default gateway: None");
|
||||
self.iface.routes_mut().remove_default_ipv4_route();
|
||||
}
|
||||
});
|
||||
}
|
||||
for (i, s) in config.dns_servers.iter().enumerate() {
|
||||
debug!(" DNS server {}: {}", i, s);
|
||||
}
|
||||
}
|
||||
Event::Deconfigured => {
|
||||
debug!("Lost IP configuration");
|
||||
set_ipv4_addr(&mut self.iface, Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0));
|
||||
if medium == Medium::Ethernet {
|
||||
self.iface.routes_mut().remove_default_ipv4_route();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,6 +143,13 @@ impl Stack {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ipv4_addr(iface: &mut Interface, cidr: Ipv4Cidr) {
|
||||
iface.update_ip_addrs(|addrs| {
|
||||
let dest = addrs.iter_mut().next().unwrap();
|
||||
*dest = IpCidr::Ipv4(cidr);
|
||||
});
|
||||
}
|
||||
|
||||
/// Initialize embassy_net.
|
||||
/// This function must be called from thread mode.
|
||||
pub fn init(device: &'static mut dyn Device, configurator: &'static mut dyn Configurator) {
|
||||
|
Reference in New Issue
Block a user