diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 753146d..7f51c9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,20 @@ jobs: run: cargo clippy --no-default-features --features="std log utils" - name: Run clippy with no_std run: cargo clippy --no-default-features --features="log" + - name: Run clippy for tokio feature + run: cargo clippy --features="tokio" + clippy_async_no_std: + runs-on: ubuntu-latest + container: + image: rust:latest + steps: + - uses: actions/checkout@v3 + - name: Install nightly toolchain + run: rustup toolchain add nightly + - name: Install clippy + run: rustup component add clippy --toolchain nightly + - name: Run clippy for async feature + run: cargo +nightly clippy --no-default-features --features="async" check_format: runs-on: ubuntu-latest steps: @@ -34,4 +48,4 @@ jobs: - name: Run tests with std run: cargo test - name: Run tests with no_std - run: cargo test --no-default-features \ No newline at end of file + run: cargo test --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 9c8d20c..6aa3ee7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ exclude = [ default = ["std"] std = [] utils = ["std", "chrono/clock"] +async = [] +async_tokio = ["std", "async", "tokio", "async-trait"] [dependencies] log = { version = "~0.4", optional = true } @@ -30,6 +32,9 @@ chrono = { version = "~0.4", default-features = false, optional = true } # requred till this https://github.com/rust-lang/rfcs/pull/2832 is not addressed no-std-net = "~0.6" +async-trait = { version = "0.1", optional = true } +tokio = { version = "1", features = ["full"], optional = true } + [dev-dependencies] simple_logger = { version = "~1.13" } smoltcp = { version = "~0.9", default-features = false, features = ["phy-tuntap_interface", "socket-udp", "proto-ipv4"] } @@ -48,4 +53,8 @@ required-features = ["utils"] [[example]] name = "smoltcp_request" -required-features = ["std"] \ No newline at end of file +required-features = ["std"] + +[[example]] +name = "tokio" +required-features = ["async_tokio"] diff --git a/README.md b/README.md index 393e4c4..4f1f938 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,15 @@ Currently there are basic `no_std` support available, thanks [`no-std-net`](http crate. There is an example available on how to use [`smoltcp`][smoltcp] stack and that should provide general idea on how to bootstrap `no_std` networking and timestamping tools for `sntpc` library usage +## `async` support + +------------------- + +Feature `async_tokio` allows to use crate together with [tokio](https://docs.rs/tokio/latest/tokio/). There is an example: `examples/tokio.rs`. + +There is also `no_std` support with feature `async`, but it requires Rust >= `1.75-nightly` version. The example can be found in [separate repository](https://github.com/vpikulik/sntpc_embassy). + + # Examples ---------- diff --git a/examples/tokio.rs b/examples/tokio.rs new file mode 100644 index 0000000..6ed0612 --- /dev/null +++ b/examples/tokio.rs @@ -0,0 +1,46 @@ +use sntpc::{ + async_impl::{get_time, NtpUdpSocket}, + Error, NtpContext, Result, StdTimestampGen, +}; +use std::net::SocketAddr; +use tokio::net::{ToSocketAddrs, UdpSocket}; + +const POOL_NTP_ADDR: &str = "pool.ntp.org:123"; + +#[derive(Debug)] +struct Socket { + sock: UdpSocket, +} + +#[async_trait::async_trait] +impl NtpUdpSocket for Socket { + async fn send_to( + &self, + buf: &[u8], + addr: T, + ) -> Result { + self.sock + .send_to(buf, addr) + .await + .map_err(|_| Error::Network) + } + + async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> { + self.sock.recv_from(buf).await.map_err(|_| Error::Network) + } +} + +#[tokio::main] +async fn main() { + let sock = UdpSocket::bind("0.0.0.0:0".parse::().unwrap()) + .await + .expect("Socket creation"); + let socket = Socket { sock: sock }; + let ntp_context = NtpContext::new(StdTimestampGen::default()); + + let res = get_time(POOL_NTP_ADDR, socket, ntp_context) + .await + .expect("get_time error"); + + println!("RESULT: {:?}", res); +} diff --git a/src/async_impl.rs b/src/async_impl.rs new file mode 100644 index 0000000..4445a3e --- /dev/null +++ b/src/async_impl.rs @@ -0,0 +1,149 @@ +use crate::types::{ + Error, NtpContext, NtpPacket, NtpResult, NtpTimestampGenerator, + RawNtpPacket, Result, SendRequestResult, +}; +use crate::{get_ntp_timestamp, process_response}; +use core::fmt::Debug; +#[cfg(feature = "log")] +use log::debug; + +#[cfg(feature = "std")] +use std::net::SocketAddr; +#[cfg(feature = "tokio")] +use tokio::net::{lookup_host, ToSocketAddrs}; + +#[cfg(not(feature = "std"))] +use no_std_net::{SocketAddr, ToSocketAddrs}; + +#[cfg(not(feature = "std"))] +async fn lookup_host(host: T) -> Result> +where + T: ToSocketAddrs, +{ + #[allow(unused_variables)] + host.to_socket_addrs().map_err(|e| { + #[cfg(feature = "log")] + debug!("ToScoketAddrs: {}", e); + Error::AddressResolve + }) +} + +#[cfg(feature = "tokio")] +#[async_trait::async_trait] +pub trait NtpUdpSocket { + async fn send_to( + &self, + buf: &[u8], + addr: T, + ) -> Result; + + async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)>; +} + +#[cfg(not(feature = "std"))] +pub trait NtpUdpSocket { + fn send_to( + &self, + buf: &[u8], + addr: T, + ) -> impl core::future::Future>; + + fn recv_from( + &self, + buf: &mut [u8], + ) -> impl core::future::Future>; +} + +pub async fn sntp_send_request( + dest: A, + socket: &U, + context: NtpContext, +) -> Result +where + A: ToSocketAddrs + Debug + Send, + U: NtpUdpSocket + Debug, + T: NtpTimestampGenerator + Copy, +{ + #[cfg(feature = "log")] + debug!("Address: {:?}, Socket: {:?}", dest, *socket); + let request = NtpPacket::new(context.timestamp_gen); + + send_request(dest, &request, socket).await?; + Ok(SendRequestResult::from(request)) +} + +async fn send_request( + dest: A, + req: &NtpPacket, + socket: &U, +) -> core::result::Result<(), Error> { + let buf = RawNtpPacket::from(req); + + match socket.send_to(&buf.0, dest).await { + Ok(size) => { + if size == buf.0.len() { + Ok(()) + } else { + Err(Error::Network) + } + } + Err(_) => Err(Error::Network), + } +} + +pub async fn sntp_process_response( + dest: A, + socket: &U, + mut context: NtpContext, + send_req_result: SendRequestResult, +) -> Result +where + A: ToSocketAddrs + Debug, + U: NtpUdpSocket + Debug, + T: NtpTimestampGenerator + Copy, +{ + let mut response_buf = RawNtpPacket::default(); + let (response, src) = socket.recv_from(response_buf.0.as_mut()).await?; + context.timestamp_gen.init(); + let recv_timestamp = get_ntp_timestamp(context.timestamp_gen); + #[cfg(feature = "log")] + debug!("Response: {}", response); + + match lookup_host(dest).await { + Err(_) => return Err(Error::AddressResolve), + Ok(mut it) => { + if !it.any(|addr| addr == src) { + return Err(Error::ResponseAddressMismatch); + } + } + } + + if response != core::mem::size_of::() { + return Err(Error::IncorrectPayload); + } + + let result = + process_response(send_req_result, response_buf, recv_timestamp); + + if let Ok(_r) = &result { + #[cfg(feature = "log")] + debug!("{:?}", _r); + } + + result +} + +pub async fn get_time( + pool_addrs: A, + socket: U, + context: NtpContext, +) -> Result +where + A: ToSocketAddrs + Copy + Debug + Send, + U: NtpUdpSocket + Debug, + T: NtpTimestampGenerator + Copy, +{ + let result = sntp_send_request(pool_addrs, &socket, context).await?; + + sntp_process_response(pool_addrs, &socket, context, result).await +} diff --git a/src/lib.rs b/src/lib.rs index a61be09..a422592 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,6 +105,9 @@ pub mod utils; mod types; pub use crate::types::*; +#[cfg(feature = "async")] +pub mod async_impl; + use core::fmt::Debug; use core::iter::Iterator; use core::marker::Copy;