mirror of
https://github.com/embassy-rs/embassy.git
synced 2025-09-26 20:00:27 +00:00
net: Add some embedded-nal-async UDP traits
Closes: https://github.com/embassy-rs/embassy/issues/2516
This commit is contained in:
parent
a5ee98c402
commit
9a5ee03bce
@ -22,6 +22,8 @@ pub mod tcp;
|
||||
mod time;
|
||||
#[cfg(feature = "udp")]
|
||||
pub mod udp;
|
||||
#[cfg(feature = "udp")]
|
||||
pub mod udp_nal;
|
||||
|
||||
use core::cell::RefCell;
|
||||
use core::future::{poll_fn, Future};
|
||||
|
207
embassy-net/src/udp_nal.rs
Normal file
207
embassy-net/src/udp_nal.rs
Normal file
@ -0,0 +1,207 @@
|
||||
//! UDP sockets usable through [embedded_nal_async]
|
||||
//!
|
||||
//! The full [embedded_nal_async::UdpStack] is *not* implemented at the moment: As its API allows
|
||||
//! arbitrary creation of movable sockets, embassy's [udp::UdpSocket] type could only be crated if
|
||||
//! the NAL stack had a pre-allocated pool of sockets with their respective buffers. Nothing rules
|
||||
//! out such a type, but at the moment, only the bound or connected socket types are implemented
|
||||
//! with their own constructors from an embassy [crate::Stack] -- for many applications, those are
|
||||
//! useful enough. (FIXME: Given we construct from Socket, Stack could really be implemented on
|
||||
//! `Cell<Option<Socket>>` by `.take()`ing, couldn't it?)
|
||||
//!
|
||||
//! The constructors of the various socket types mimick the UdpStack's socket creation functions,
|
||||
//! but take an owned (uninitialized) Socket instead of a shared stack.
|
||||
//!
|
||||
//! No `bind_single` style constructor is currently provided. FIXME: Not sure we have all the
|
||||
//! information at bind time to specialize a wildcard address into a concrete address and return
|
||||
//! it. Should the NAL trait be updated to disallow using wildcard addresses on `bind_single`, and
|
||||
//! merely allow unspecified ports to get an ephemeral one?
|
||||
|
||||
use core::future::poll_fn;
|
||||
use core::net::SocketAddr;
|
||||
|
||||
use embedded_nal_async as nal;
|
||||
use smoltcp::wire::{IpAddress, IpEndpoint};
|
||||
|
||||
use crate::udp;
|
||||
|
||||
pub struct ConnectedUdp<'a> {
|
||||
remote: IpEndpoint,
|
||||
// The local port is stored in the socket, as it gets bound. This value is populated lazily:
|
||||
// embassy only decides at udp::Socket::dispatch time whence to send, and while we could
|
||||
// duplicate the code for the None case of the local_address by calling the right
|
||||
// get_source_address function, we'd still need an interface::Context / an interface to call
|
||||
// this through, and AFAICT we don't get access to that.
|
||||
local: Option<IpAddress>,
|
||||
socket: udp::UdpSocket<'a>,
|
||||
}
|
||||
|
||||
pub struct UnconnectedUdp<'a> {
|
||||
socket: udp::UdpSocket<'a>,
|
||||
}
|
||||
|
||||
fn sockaddr_nal2smol(sockaddr: SocketAddr) -> Result<IpEndpoint, Error> {
|
||||
match sockaddr {
|
||||
SocketAddr::V4(sockaddr) => {
|
||||
#[cfg(feature = "proto-ipv4")]
|
||||
return Ok(IpEndpoint {
|
||||
addr: IpAddress::Ipv4(*sockaddr.ip()),
|
||||
port: sockaddr.port(),
|
||||
});
|
||||
#[cfg(not(feature = "proto-ipv4"))]
|
||||
return Err(Error::AddressFamilyUnavailable);
|
||||
}
|
||||
SocketAddr::V6(sockaddr) => {
|
||||
#[cfg(feature = "proto-ipv6")]
|
||||
return Ok(IpEndpoint {
|
||||
addr: IpAddress::Ipv6(*sockaddr.ip()),
|
||||
port: sockaddr.port(),
|
||||
});
|
||||
#[cfg(not(feature = "proto-ipv6"))]
|
||||
return Err(Error::AddressFamilyUnavailable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sockaddr_smol2nal(endpoint: IpEndpoint) -> SocketAddr {
|
||||
match endpoint.addr {
|
||||
// Let's hope those are in sync; what we'll really need to know is whether smoltcp has the
|
||||
// relevant flags set (but we can't query that).
|
||||
#[cfg(feature = "proto-ipv4")]
|
||||
IpAddress::Ipv4(addr) => core::net::SocketAddrV4::new(addr.into(), endpoint.port).into(),
|
||||
#[cfg(feature = "proto-ipv6")]
|
||||
IpAddress::Ipv6(addr) => core::net::SocketAddrV6::new(addr.into(), endpoint.port, 0, 0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the IP address in this type the unspecified address?
|
||||
///
|
||||
/// FIXME: What of ::ffff:0.0.0.0? Is that expected to bind to all v4 addresses?
|
||||
fn is_unspec_ip(addr: SocketAddr) -> bool {
|
||||
match addr {
|
||||
SocketAddr::V4(sockaddr) => sockaddr.ip().octets() == [0; 4],
|
||||
SocketAddr::V6(sockaddr) => sockaddr.ip().octets() == [0; 16],
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
RecvError(udp::RecvError),
|
||||
SendError(udp::SendError),
|
||||
BindError(udp::BindError),
|
||||
AddressFamilyUnavailable,
|
||||
}
|
||||
|
||||
impl embedded_io_async::Error for Error {
|
||||
fn kind(&self) -> embedded_io_async::ErrorKind {
|
||||
match self {
|
||||
Self::SendError(udp::SendError::NoRoute) => embedded_io_async::ErrorKind::AddrNotAvailable,
|
||||
Self::BindError(udp::BindError::NoRoute) => embedded_io_async::ErrorKind::AddrNotAvailable,
|
||||
Self::AddressFamilyUnavailable => embedded_io_async::ErrorKind::AddrNotAvailable,
|
||||
// These should not happen b/c our sockets are typestated.
|
||||
Self::SendError(udp::SendError::SocketNotBound) => embedded_io_async::ErrorKind::Other,
|
||||
Self::BindError(udp::BindError::InvalidState) => embedded_io_async::ErrorKind::Other,
|
||||
// This should not happen b/c in embedded_nal_async this is not expressed through an
|
||||
// error.
|
||||
// FIXME we're not there in this impl yet.
|
||||
Self::RecvError(udp::RecvError::Truncated) => embedded_io_async::ErrorKind::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<udp::BindError> for Error {
|
||||
fn from(err: udp::BindError) -> Self {
|
||||
Self::BindError(err)
|
||||
}
|
||||
}
|
||||
impl From<udp::RecvError> for Error {
|
||||
fn from(err: udp::RecvError) -> Self {
|
||||
Self::RecvError(err)
|
||||
}
|
||||
}
|
||||
impl From<udp::SendError> for Error {
|
||||
fn from(err: udp::SendError) -> Self {
|
||||
Self::SendError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ConnectedUdp<'a> {
|
||||
/// Create a ConnectedUdp.
|
||||
///
|
||||
/// ## Prerequisites
|
||||
///
|
||||
/// The `socket` must be open (in the sense of smoltcp's `.is_open()`) -- unbound and
|
||||
/// unconnected.
|
||||
pub async fn connect_from(
|
||||
mut socket: udp::UdpSocket<'a>,
|
||||
local: SocketAddr,
|
||||
remote: SocketAddr,
|
||||
) -> Result<Self, Error> {
|
||||
socket.bind(sockaddr_nal2smol(local)?)?;
|
||||
|
||||
Ok(ConnectedUdp {
|
||||
remote: sockaddr_nal2smol(remote)?,
|
||||
// FIXME: We could check if local was fully (or sufficiently, picking the port from the
|
||||
// socket) specified and store if yes -- for a first iteration, leaving that to the
|
||||
// fallback path we need anyway in case local is [::].
|
||||
local: None,
|
||||
socket,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn connect(socket: udp::UdpSocket<'a> /*, ... */) -> Result<Self, udp::BindError> {
|
||||
// This is really just a copy of the provided `embedded_nal::udp::UdpStack::connect` method
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> UnconnectedUdp<'a> {
|
||||
/// Create an UnconnectedUdp.
|
||||
///
|
||||
/// The `local` address may be anything from fully specified (address and port) to fully
|
||||
/// unspecified (port 0, all-zeros address).
|
||||
///
|
||||
/// ## Prerequisites
|
||||
///
|
||||
/// The `socket` must be open (in the sense of smoltcp's `.is_open()`) -- unbound and
|
||||
/// unconnected.
|
||||
pub async fn bind_multiple(mut socket: udp::UdpSocket<'a>, local: SocketAddr) -> Result<Self, Error> {
|
||||
socket.bind(sockaddr_nal2smol(local)?)?;
|
||||
|
||||
Ok(UnconnectedUdp { socket })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> nal::UnconnectedUdp for UnconnectedUdp<'a> {
|
||||
type Error = Error;
|
||||
async fn send(&mut self, local: SocketAddr, remote: SocketAddr, buf: &[u8]) -> Result<(), Error> {
|
||||
// TODO: debug_assert!(local.port == 0 || local.port == self.socket.where-do-we-get-the-bound-port-here,
|
||||
// "Attempted to send from different port than the socket was bound to")
|
||||
|
||||
let remote_endpoint = smoltcp::socket::udp::UdpMetadata {
|
||||
// A conversion of the addr part only might be cheaper
|
||||
local_address: if is_unspec_ip(local) {
|
||||
None
|
||||
} else {
|
||||
Some(sockaddr_nal2smol(local)?.addr)
|
||||
},
|
||||
..sockaddr_nal2smol(remote)?.into()
|
||||
};
|
||||
poll_fn(move |cx| self.socket.poll_send_to(buf, remote_endpoint, cx)).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn receive_into(&mut self, buf: &mut [u8]) -> Result<(usize, SocketAddr, SocketAddr), Error> {
|
||||
// FIXME: The truncation is an issue -- we may need to change poll_recv_from to poll_recv
|
||||
// and copy from the slice ourselves to get the trait's behavior
|
||||
let (size, metadata) = poll_fn(move |cx| self.socket.poll_recv_from(buf, cx)).await?;
|
||||
Ok((
|
||||
size,
|
||||
sockaddr_smol2nal(IpEndpoint {
|
||||
addr: metadata
|
||||
.local_address
|
||||
.expect("Local address is always populated on receive"),
|
||||
port: 0, // FIXME obtain or persist
|
||||
}),
|
||||
sockaddr_smol2nal(metadata.endpoint),
|
||||
))
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user