sntpc_net_embassy/lib.rs
1//! Embassy async runtime UDP socket adapter for the [`sntpc`] SNTP client library.
2//!
3//! This crate provides a wrapper around [`embassy_net::udp::UdpSocket`] that implements
4//! the [`NtpUdpSocket`] trait, enabling asynchronous SNTP requests in embedded systems
5//! using the Embassy async runtime.
6//!
7//! # Design Rationale
8//!
9//! The network adapters are separated into their own crates to:
10//! - Enable independent versioning (updating Embassy doesn't require updating `sntpc` core)
11//! - Allow version flexibility (works with embassy-net 0.8.x)
12//! - Maintain `no_std` compatibility for embedded systems
13//!
14//! # Features
15//!
16//! - `ipv6`: Enables IPv6 protocol support (propagates to `embassy-net`)
17//! - `log`: Enables logging support via the `log` crate
18//! - `defmt`: Enables logging support via the `defmt` crate for embedded systems
19//!
20//! **Note**: The `log` and `defmt` features are mutually exclusive. If both are enabled,
21//! `defmt` takes priority.
22//!
23//! # Example
24//!
25//! ```ignore
26//! use sntpc::{get_time, NtpContext};
27//! use sntpc_net_embassy::UdpSocketWrapper;
28//! use embassy_net::udp::UdpSocket;
29//!
30//! // Within an Embassy async context
31//! let socket = UdpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
32//! socket.bind(local_port).unwrap();
33//! let socket = UdpSocketWrapper::from(socket);
34//!
35//! let result = get_time(server_addr, &socket, ntp_context).await;
36//! match result {
37//! Ok(time) => defmt::info!("Received time: {}.{}", time.sec(), time.sec_fraction()),
38//! Err(e) => defmt::error!("Failed to get time: {:?}", e),
39//! }
40//! ```
41//!
42//! For more examples, see the [repository examples](https://github.com/vpetrigo/sntpc/tree/master/examples/embassy-net).
43#![no_std]
44
45/// Logging module that conditionally uses either `defmt` or `log` based on feature flags.
46#[cfg(any(feature = "log", feature = "defmt"))]
47mod log {
48 use cfg_if::cfg_if;
49
50 cfg_if! {
51 if #[cfg(feature = "defmt")] {
52 pub(crate) use defmt::error;
53 } else if #[cfg(feature = "log")] {
54 pub(crate) use log::error;
55 }
56 }
57}
58
59#[cfg(any(feature = "log", feature = "defmt"))]
60use crate::log::error;
61
62use embassy_net::{IpAddress, IpEndpoint, udp::UdpSocket};
63use sntpc::{Error, NtpUdpSocket, Result};
64
65use core::net::{IpAddr, SocketAddr};
66
67/// A wrapper around [`embassy_net::udp::UdpSocket`] that implements [`NtpUdpSocket`].
68///
69/// This type allows Embassy UDP sockets to be used with the `sntpc` library for making
70/// asynchronous SNTP requests in embedded systems. It handles address conversion between
71/// standard library types and Embassy's network types.
72///
73/// The wrapper has a lifetime parameter that matches the underlying Embassy socket's
74/// lifetime, typically tied to the network stack and buffer lifetimes.
75///
76/// # Example
77///
78/// ```ignore
79/// use sntpc_net_embassy::UdpSocketWrapper;
80/// use embassy_net::udp::UdpSocket;
81///
82/// let socket = UdpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
83/// socket.bind(8123).unwrap();
84/// let wrapper = UdpSocketWrapper::new(socket);
85/// // Use wrapper with sntpc async functions
86/// ```
87pub struct UdpSocketWrapper<'a> {
88 socket: UdpSocket<'a>,
89}
90
91impl<'a> UdpSocketWrapper<'a> {
92 /// Creates a new `UdpSocketWrapper` from an [`embassy_net::udp::UdpSocket`].
93 ///
94 /// # Arguments
95 ///
96 /// * `socket` - An Embassy UDP socket to wrap
97 ///
98 /// # Example
99 ///
100 /// ```ignore
101 /// use sntpc_net_embassy::UdpSocketWrapper;
102 /// use embassy_net::udp::UdpSocket;
103 ///
104 /// let socket = UdpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
105 /// let wrapper = UdpSocketWrapper::new(socket);
106 /// ```
107 #[must_use]
108 pub fn new(socket: UdpSocket<'a>) -> Self {
109 Self { socket }
110 }
111}
112
113impl<'a> From<UdpSocket<'a>> for UdpSocketWrapper<'a> {
114 /// Converts an [`embassy_net::udp::UdpSocket`] into a `UdpSocketWrapper`.
115 ///
116 /// This provides a convenient way to create a wrapper using `.into()` or `from()`.
117 ///
118 /// # Example
119 ///
120 /// ```ignore
121 /// use sntpc_net_embassy::UdpSocketWrapper;
122 /// use embassy_net::udp::UdpSocket;
123 ///
124 /// let socket = UdpSocket::new(stack, &mut rx_buffer, &mut tx_buffer);
125 /// let wrapper: UdpSocketWrapper = socket.into();
126 /// ```
127 fn from(value: UdpSocket<'a>) -> Self {
128 UdpSocketWrapper::new(value)
129 }
130}
131
132/// Converts a standard [`SocketAddr`] to an Embassy [`IpEndpoint`].
133///
134/// This helper function handles the conversion between standard library network
135/// types and Embassy's network types. IPv6 addresses are only supported when
136/// the `ipv6` feature is enabled.
137///
138/// # Panics
139///
140/// Panics if an IPv6 address is provided without the `ipv6` feature enabled.
141fn to_endpoint(addr: SocketAddr) -> IpEndpoint {
142 // Currently smoltcp/embassy-net still has its own address enum
143 IpEndpoint::new(
144 match addr.ip() {
145 IpAddr::V4(addr) => IpAddress::Ipv4(addr),
146 #[cfg(feature = "ipv6")]
147 IpAddr::V6(addr) => IpAddress::Ipv6(addr),
148 #[cfg(not(feature = "ipv6"))]
149 _ => unreachable!(),
150 },
151 addr.port(),
152 )
153}
154
155/// Converts an Embassy [`IpEndpoint`] to a standard [`SocketAddr`].
156///
157/// This helper function handles the conversion from Embassy's network types
158/// back to standard library network types.
159fn from_endpoint(ep: IpEndpoint) -> SocketAddr {
160 SocketAddr::new(
161 match ep.addr {
162 IpAddress::Ipv4(val) => IpAddr::V4(val),
163 #[cfg(feature = "ipv6")]
164 IpAddress::Ipv6(val) => IpAddr::V6(val),
165 },
166 ep.port,
167 )
168}
169
170impl NtpUdpSocket for UdpSocketWrapper<'_> {
171 async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> Result<usize> {
172 let endpoint = to_endpoint(addr);
173
174 match self.socket.send_to(buf, endpoint).await {
175 Ok(()) => Ok(buf.len()),
176 #[allow(unused_variables)]
177 Err(e) => {
178 #[cfg(any(feature = "log", feature = "defmt"))]
179 error!("Error while sending to {}: {:?}", endpoint, e);
180 Err(Error::Network)
181 }
182 }
183 }
184
185 async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> {
186 match self.socket.recv_from(buf).await {
187 Ok((len, ep)) => Ok((len, from_endpoint(ep.endpoint))),
188 #[allow(unused_variables)]
189 Err(e) => {
190 #[cfg(any(feature = "log", feature = "defmt"))]
191 error!("Error receiving {:?}", e);
192 Err(Error::Network)
193 }
194 }
195 }
196}