Skip to content

Commit 5fef9c2

Browse files
committed
coap: Move in embassy-net's embedded-nal traits to finish them in here
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.
1 parent 5c5b530 commit 5fef9c2

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

examples/coap/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,15 @@ embedded-io-async = "0.6.1"
1313
heapless = { workspace = true }
1414
riot-rs = { path = "../../src/riot-rs", features = ["override-network-config"] }
1515
riot-rs-boards = { path = "../../src/riot-rs-boards" }
16+
17+
# for the udp_nal mod
18+
embedded-nal-async = "0.7"
19+
# actually patched with https://github.com/smoltcp-rs/smoltcp/pull/904 but
20+
# patch.crates-io takes care of that
21+
smoltcp = { version = "0.11", default-features = false }
22+
23+
[features]
24+
default = [ "proto-ipv4" ] # shame
25+
# actually embedded-nal features, we have to match them here while developing udp_nal in here
26+
proto-ipv4 = []
27+
proto-ipv6 = []

examples/coap/src/udp_nal/mod.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//! UDP sockets usable through [embedded_nal_async]
2+
//!
3+
//! The full [embedded_nal_async::UdpStack] is *not* implemented at the moment: As its API allows
4+
//! arbitrary creation of movable sockets, embassy's [udp::UdpSocket] type could only be crated if
5+
//! the NAL stack had a pre-allocated pool of sockets with their respective buffers. Nothing rules
6+
//! out such a type, but at the moment, only the bound or connected socket types are implemented
7+
//! with their own constructors from an embassy [crate::Stack] -- for many applications, those are
8+
//! useful enough. (FIXME: Given we construct from Socket, Stack could really be implemented on
9+
//! `Cell<Option<Socket>>` by `.take()`ing, couldn't it?)
10+
//!
11+
//! The constructors of the various socket types mimick the UdpStack's socket creation functions,
12+
//! but take an owned (uninitialized) Socket instead of a shared stack.
13+
//!
14+
//! No `bind_single` style constructor is currently provided. FIXME: Not sure we have all the
15+
//! information at bind time to specialize a wildcard address into a concrete address and return
16+
//! it. Should the NAL trait be updated to disallow using wildcard addresses on `bind_single`, and
17+
//! merely allow unspecified ports to get an ephemeral one?
18+
19+
use core::future::poll_fn;
20+
21+
use embedded_nal_async as nal;
22+
use smoltcp::wire::{IpAddress, IpEndpoint};
23+
24+
use embassy_net::udp;
25+
26+
mod util;
27+
pub use util::Error;
28+
use util::{is_unspec_ip, sockaddr_nal2smol, sockaddr_smol2nal};
29+
30+
pub struct ConnectedUdp<'a> {
31+
remote: IpEndpoint,
32+
// The local port is stored in the socket, as it gets bound. This value is populated lazily:
33+
// embassy only decides at udp::Socket::dispatch time whence to send, and while we could
34+
// duplicate the code for the None case of the local_address by calling the right
35+
// get_source_address function, we'd still need an interface::Context / an interface to call
36+
// this through, and AFAICT we don't get access to that.
37+
local: Option<IpAddress>,
38+
socket: udp::UdpSocket<'a>,
39+
}
40+
41+
/// A UDP socket that has been bound locally (either to a unique address or just to a port)
42+
///
43+
/// Its operations are accessible through the [nal::UnconnectedUdp] trait.
44+
pub struct UnconnectedUdp<'a> {
45+
socket: udp::UdpSocket<'a>,
46+
}
47+
48+
impl<'a> ConnectedUdp<'a> {
49+
/// Create a ConnectedUdp by assigning it a remote and a concrete local address
50+
///
51+
/// ## Prerequisites
52+
///
53+
/// The `socket` must be open (in the sense of smoltcp's `.is_open()`) -- unbound and
54+
/// unconnected.
55+
pub async fn connect_from(
56+
mut socket: udp::UdpSocket<'a>,
57+
local: nal::SocketAddr,
58+
remote: nal::SocketAddr,
59+
) -> Result<Self, Error> {
60+
socket.bind(sockaddr_nal2smol(local)?)?;
61+
62+
Ok(ConnectedUdp {
63+
remote: sockaddr_nal2smol(remote)?,
64+
// FIXME: We could check if local was fully (or sufficiently, picking the port from the
65+
// socket) specified and store if yes -- for a first iteration, leaving that to the
66+
// fallback path we need anyway in case local is [::].
67+
local: None,
68+
socket,
69+
})
70+
}
71+
72+
/// Create a ConnectedUdp by assigning it a remote and a local address (the latter may happen
73+
/// lazily)
74+
///
75+
/// ## Prerequisites
76+
///
77+
/// The `socket` must be open (in the sense of smoltcp's `.is_open()`) -- unbound and
78+
/// unconnected.
79+
pub async fn connect(socket: udp::UdpSocket<'a> /*, ... */) -> Result<Self, udp::BindError> {
80+
// This is really just a copy of the provided `embedded_nal::udp::UdpStack::connect` method
81+
todo!()
82+
}
83+
}
84+
85+
impl<'a> UnconnectedUdp<'a> {
86+
/// Create an UnconnectedUdp.
87+
///
88+
/// The `local` address may be anything from fully specified (address and port) to fully
89+
/// unspecified (port 0, all-zeros address).
90+
///
91+
/// ## Prerequisites
92+
///
93+
/// The `socket` must be open (in the sense of smoltcp's `.is_open()`) -- unbound and
94+
/// unconnected.
95+
pub async fn bind_multiple(mut socket: udp::UdpSocket<'a>, local: nal::SocketAddr) -> Result<Self, Error> {
96+
socket.bind(sockaddr_nal2smol(local)?)?;
97+
98+
Ok(UnconnectedUdp { socket })
99+
}
100+
}
101+
102+
impl<'a> nal::UnconnectedUdp for UnconnectedUdp<'a> {
103+
type Error = Error;
104+
async fn send(
105+
&mut self,
106+
local: embedded_nal_async::SocketAddr,
107+
remote: embedded_nal_async::SocketAddr,
108+
buf: &[u8],
109+
) -> Result<(), Error> {
110+
// While the underlying layers probably don't care, we're not passing on the port
111+
// information, so the underlying layers won't even have a *chance* to care if we don't
112+
// check here.
113+
debug_assert!(
114+
local.port() == 0 || local.port() == self.socket.with(|s, _| s.endpoint().port),
115+
"Port of local address, when given, must match bound port."
116+
);
117+
118+
let remote_endpoint = smoltcp::socket::udp::UdpMetadata {
119+
local_address: if is_unspec_ip(local) {
120+
None
121+
} else {
122+
// A conversion of the addr part only might be cheaper, but would also mean we need
123+
// two functions
124+
Some(sockaddr_nal2smol(local)?.addr)
125+
},
126+
..sockaddr_nal2smol(remote)?.into()
127+
};
128+
poll_fn(move |cx| self.socket.poll_send_to(buf, remote_endpoint, cx)).await?;
129+
Ok(())
130+
}
131+
async fn receive_into(
132+
&mut self,
133+
buf: &mut [u8],
134+
) -> Result<(usize, embedded_nal_async::SocketAddr, embedded_nal_async::SocketAddr), Error> {
135+
// FIXME: The truncation is an issue -- we may need to change poll_recv_from to poll_recv
136+
// and copy from the slice ourselves to get the trait's behavior
137+
let (size, metadata) = poll_fn(|cx| self.socket.poll_recv_from(buf, cx)).await?;
138+
Ok((
139+
size,
140+
sockaddr_smol2nal(IpEndpoint {
141+
addr: metadata
142+
.local_address
143+
.expect("Local address is always populated on receive"),
144+
port: self.socket.with(|s, _| s.endpoint().port),
145+
}),
146+
sockaddr_smol2nal(metadata.endpoint),
147+
))
148+
}
149+
}

examples/coap/src/udp_nal/util.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//! Helpers for udp_nal -- conversion and error types
2+
3+
use embassy_net::udp;
4+
use embedded_nal_async as nal;
5+
use smoltcp::wire::{IpAddress, IpEndpoint};
6+
7+
pub(super) fn sockaddr_nal2smol(sockaddr: nal::SocketAddr) -> Result<IpEndpoint, Error> {
8+
match sockaddr {
9+
#[allow(unused)]
10+
nal::SocketAddr::V4(sockaddr) => {
11+
#[cfg(feature = "proto-ipv4")]
12+
return Ok(IpEndpoint {
13+
addr: smoltcp::wire::Ipv4Address(sockaddr.ip().octets()).into(),
14+
port: sockaddr.port(),
15+
});
16+
#[cfg(not(feature = "proto-ipv4"))]
17+
return Err(Error::AddressFamilyUnavailable);
18+
}
19+
#[allow(unused)]
20+
nal::SocketAddr::V6(sockaddr) => {
21+
#[cfg(feature = "proto-ipv6")]
22+
return Ok(IpEndpoint {
23+
addr: smoltcp::wire::Ipv6Address(sockaddr.ip().octets()).into(),
24+
port: sockaddr.port(),
25+
});
26+
#[cfg(not(feature = "proto-ipv6"))]
27+
return Err(Error::AddressFamilyUnavailable);
28+
}
29+
}
30+
}
31+
32+
pub(super) fn sockaddr_smol2nal(endpoint: IpEndpoint) -> nal::SocketAddr {
33+
match endpoint.addr {
34+
// Let's hope those are in sync; what we'll really need to know is whether smoltcp has the
35+
// relevant flags set (but we can't query that).
36+
#[cfg(feature = "proto-ipv4")]
37+
IpAddress::Ipv4(addr) => embedded_nal_async::SocketAddrV4::new(addr.0.into(), endpoint.port).into(),
38+
#[cfg(feature = "proto-ipv6")]
39+
IpAddress::Ipv6(addr) => embedded_nal_async::SocketAddrV6::new(addr.0.into(), endpoint.port).into(),
40+
}
41+
}
42+
43+
/// Is the IP address in this type the unspecified address?
44+
///
45+
/// FIXME: What of ::ffff:0.0.0.0? Is that expected to bind to all v4 addresses?
46+
pub(super) fn is_unspec_ip(addr: nal::SocketAddr) -> bool {
47+
match addr {
48+
nal::SocketAddr::V4(sockaddr) => sockaddr.ip().octets() == [0; 4],
49+
nal::SocketAddr::V6(sockaddr) => sockaddr.ip().octets() == [0; 16],
50+
}
51+
}
52+
53+
/// Unified error type for [embedded_nal_async] operations on UDP sockets
54+
#[derive(Debug)]
55+
#[non_exhaustive]
56+
pub enum Error {
57+
/// Error stemming from failure to send
58+
RecvError(udp::RecvError),
59+
/// Error stemming from failure to send
60+
SendError(udp::SendError),
61+
/// Error stemming from failure to bind to an address/port
62+
BindError(udp::BindError),
63+
/// Error stemming from failure to represent the given address family for lack of enabled
64+
/// embassy-net features
65+
AddressFamilyUnavailable,
66+
}
67+
68+
impl embedded_io_async::Error for Error {
69+
fn kind(&self) -> embedded_io_async::ErrorKind {
70+
match self {
71+
Self::SendError(udp::SendError::NoRoute) => embedded_io_async::ErrorKind::AddrNotAvailable,
72+
Self::BindError(udp::BindError::NoRoute) => embedded_io_async::ErrorKind::AddrNotAvailable,
73+
Self::AddressFamilyUnavailable => embedded_io_async::ErrorKind::AddrNotAvailable,
74+
// These should not happen b/c our sockets are typestated.
75+
Self::SendError(udp::SendError::SocketNotBound) => embedded_io_async::ErrorKind::Other,
76+
Self::BindError(udp::BindError::InvalidState) => embedded_io_async::ErrorKind::Other,
77+
// This should not happen b/c in embedded_nal_async this is not expressed through an
78+
// error.
79+
// FIXME we're not there in this impl yet.
80+
Self::RecvError(udp::RecvError::Truncated) => embedded_io_async::ErrorKind::Other,
81+
}
82+
}
83+
}
84+
impl From<udp::BindError> for Error {
85+
fn from(err: udp::BindError) -> Self {
86+
Self::BindError(err)
87+
}
88+
}
89+
impl From<udp::RecvError> for Error {
90+
fn from(err: udp::RecvError) -> Self {
91+
Self::RecvError(err)
92+
}
93+
}
94+
impl From<udp::SendError> for Error {
95+
fn from(err: udp::SendError) -> Self {
96+
Self::SendError(err)
97+
}
98+
}

0 commit comments

Comments
 (0)