diff --git a/examples/coap/Cargo.toml b/examples/coap/Cargo.toml index 99975fb02..a5d0fad16 100644 --- a/examples/coap/Cargo.toml +++ b/examples/coap/Cargo.toml @@ -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 = [] diff --git a/examples/coap/src/udp_nal/mod.rs b/examples/coap/src/udp_nal/mod.rs new file mode 100644 index 000000000..b56f7b317 --- /dev/null +++ b/examples/coap/src/udp_nal/mod.rs @@ -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>` 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, + 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 { + 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 { + // 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 { + 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), + )) + } +} diff --git a/examples/coap/src/udp_nal/util.rs b/examples/coap/src/udp_nal/util.rs new file mode 100644 index 000000000..12945284d --- /dev/null +++ b/examples/coap/src/udp_nal/util.rs @@ -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 { + 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 for Error { + fn from(err: udp::BindError) -> Self { + Self::BindError(err) + } +} +impl From for Error { + fn from(err: udp::RecvError) -> Self { + Self::RecvError(err) + } +} +impl From for Error { + fn from(err: udp::SendError) -> Self { + Self::SendError(err) + } +}