Skip to content

Commit

Permalink
coap: Move in embassy-net's embedded-nal traits to finish them in here
Browse files Browse the repository at this point in the history
Moving the code avoids a constant whack-a-mole of updating dependency
versions, and enables review within RIOT-rs before moving it back into a
branch at embassy.

Code was moved from embassy-rs/embassy#2519, I
was the original author.
  • Loading branch information
chrysn committed Apr 8, 2024
1 parent 52f808a commit 3af9b87
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 0 deletions.
12 changes: 12 additions & 0 deletions examples/coap/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,15 @@ embedded-io-async = "0.6.1"
heapless = { workspace = true }
riot-rs = { path = "../../src/riot-rs", features = ["override-network-config"] }
riot-rs-boards = { path = "../../src/riot-rs-boards" }

# for the udp_nal mod
embedded-nal-async = "0.7"
# actually patched with https://github.com/smoltcp-rs/smoltcp/pull/904 but
# patch.crates-io takes care of that
smoltcp = { version = "0.11", default-features = false }

[features]
default = [ "proto-ipv4" ] # shame
# actually embedded-nal features, we have to match them here while developing udp_nal in here
proto-ipv4 = []
proto-ipv6 = []
149 changes: 149 additions & 0 deletions examples/coap/src/udp_nal/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
//! 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 embedded_nal_async as nal;
use smoltcp::wire::{IpAddress, IpEndpoint};

use embassy_net::udp;

mod util;
pub use util::Error;
use util::{is_unspec_ip, sockaddr_nal2smol, sockaddr_smol2nal};

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>,
}

/// A UDP socket that has been bound locally (either to a unique address or just to a port)
///
/// Its operations are accessible through the [nal::UnconnectedUdp] trait.
pub struct UnconnectedUdp<'a> {
socket: udp::UdpSocket<'a>,
}

impl<'a> ConnectedUdp<'a> {
/// Create a ConnectedUdp by assigning it a remote and a concrete local address
///
/// ## 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: nal::SocketAddr,
remote: nal::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,
})
}

/// Create a ConnectedUdp by assigning it a remote and a local address (the latter may happen
/// lazily)
///
/// ## Prerequisites
///
/// The `socket` must be open (in the sense of smoltcp's `.is_open()`) -- unbound and
/// unconnected.
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: nal::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: embedded_nal_async::SocketAddr,
remote: embedded_nal_async::SocketAddr,
buf: &[u8],
) -> Result<(), Error> {
// While the underlying layers probably don't care, we're not passing on the port
// information, so the underlying layers won't even have a *chance* to care if we don't
// check here.
debug_assert!(
local.port() == 0 || local.port() == self.socket.with(|s, _| s.endpoint().port),
"Port of local address, when given, must match bound port."
);

let remote_endpoint = smoltcp::socket::udp::UdpMetadata {
local_address: if is_unspec_ip(local) {
None
} else {
// A conversion of the addr part only might be cheaper, but would also mean we need
// two functions
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, embedded_nal_async::SocketAddr, embedded_nal_async::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(|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: self.socket.with(|s, _| s.endpoint().port),
}),
sockaddr_smol2nal(metadata.endpoint),
))
}
}
98 changes: 98 additions & 0 deletions examples/coap/src/udp_nal/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//! Helpers for udp_nal -- conversion and error types
use embassy_net::udp;
use embedded_nal_async as nal;
use smoltcp::wire::{IpAddress, IpEndpoint};

pub(super) fn sockaddr_nal2smol(sockaddr: nal::SocketAddr) -> Result<IpEndpoint, Error> {
match sockaddr {
#[allow(unused)]
nal::SocketAddr::V4(sockaddr) => {
#[cfg(feature = "proto-ipv4")]
return Ok(IpEndpoint {
addr: smoltcp::wire::Ipv4Address(sockaddr.ip().octets()).into(),
port: sockaddr.port(),
});
#[cfg(not(feature = "proto-ipv4"))]
return Err(Error::AddressFamilyUnavailable);
}
#[allow(unused)]
nal::SocketAddr::V6(sockaddr) => {
#[cfg(feature = "proto-ipv6")]
return Ok(IpEndpoint {
addr: smoltcp::wire::Ipv6Address(sockaddr.ip().octets()).into(),
port: sockaddr.port(),
});
#[cfg(not(feature = "proto-ipv6"))]
return Err(Error::AddressFamilyUnavailable);
}
}
}

pub(super) fn sockaddr_smol2nal(endpoint: IpEndpoint) -> nal::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) => embedded_nal_async::SocketAddrV4::new(addr.0.into(), endpoint.port).into(),
#[cfg(feature = "proto-ipv6")]
IpAddress::Ipv6(addr) => embedded_nal_async::SocketAddrV6::new(addr.0.into(), endpoint.port).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?
pub(super) fn is_unspec_ip(addr: nal::SocketAddr) -> bool {
match addr {
nal::SocketAddr::V4(sockaddr) => sockaddr.ip().octets() == [0; 4],
nal::SocketAddr::V6(sockaddr) => sockaddr.ip().octets() == [0; 16],
}
}

/// Unified error type for [embedded_nal_async] operations on UDP sockets
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// Error stemming from failure to send
RecvError(udp::RecvError),
/// Error stemming from failure to send
SendError(udp::SendError),
/// Error stemming from failure to bind to an address/port
BindError(udp::BindError),
/// Error stemming from failure to represent the given address family for lack of enabled
/// embassy-net features
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)
}
}

0 comments on commit 3af9b87

Please sign in to comment.