diff --git a/Cargo.lock b/Cargo.lock index 185bc3e..ea5cdfa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "num-traits", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -191,7 +200,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "cyw43" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "cortex-m", "cortex-m-rt", @@ -208,7 +217,7 @@ dependencies = [ [[package]] name = "cyw43-pio" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "cyw43", "embassy-rp", @@ -303,7 +312,7 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "embassy-embedded-hal" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "embassy-futures", "embassy-sync", @@ -319,7 +328,7 @@ dependencies = [ [[package]] name = "embassy-executor" version = "0.6.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "cortex-m", "critical-section", @@ -332,7 +341,7 @@ dependencies = [ [[package]] name = "embassy-executor-macros" version = "0.5.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "darling", "proc-macro2", @@ -343,12 +352,12 @@ dependencies = [ [[package]] name = "embassy-futures" version = "0.1.1" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" [[package]] name = "embassy-hal-internal" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "cortex-m", "critical-section", @@ -358,7 +367,7 @@ dependencies = [ [[package]] name = "embassy-net" version = "0.4.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "document-features", "embassy-net-driver", @@ -374,12 +383,12 @@ dependencies = [ [[package]] name = "embassy-net-driver" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" [[package]] name = "embassy-net-driver-channel" version = "0.3.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "embassy-futures", "embassy-net-driver", @@ -389,7 +398,7 @@ dependencies = [ [[package]] name = "embassy-rp" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "atomic-polyfill", "cfg-if", @@ -426,7 +435,7 @@ dependencies = [ [[package]] name = "embassy-sync" version = "0.6.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "cfg-if", "critical-section", @@ -438,7 +447,7 @@ dependencies = [ [[package]] name = "embassy-time" version = "0.3.2" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "cfg-if", "critical-section", @@ -455,7 +464,7 @@ dependencies = [ [[package]] name = "embassy-time-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "document-features", ] @@ -463,12 +472,12 @@ dependencies = [ [[package]] name = "embassy-time-queue-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" [[package]] name = "embassy-usb" version = "0.3.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "embassy-futures", "embassy-net-driver-channel", @@ -482,12 +491,12 @@ dependencies = [ [[package]] name = "embassy-usb-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" [[package]] name = "embassy-usb-logger" version = "0.2.0" -source = "git+https://github.com/embassy-rs/embassy.git#c84495ef2eb99580fea5392b2b3aff5ad66043a0" +source = "git+https://github.com/embassy-rs/embassy.git#4f08d5bc5ff0f765198665b1f37b6372f43b8567" dependencies = [ "embassy-futures", "embassy-sync", @@ -1001,15 +1010,18 @@ dependencies = [ name = "picow" version = "0.1.0" dependencies = [ + "chrono", "cortex-m-rt", "cyw43", "cyw43-pio", "embassy-executor", "embassy-net", "embassy-rp", + "embassy-sync", "embassy-time", "embassy-usb-logger", "log", + "no-std-net", "panic-halt", "portable-atomic", "rand", @@ -1236,9 +1248,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" diff --git a/Cargo.toml b/Cargo.toml index a991e0b..e821089 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,19 +9,22 @@ serde = { version = "*", features = ["derive"] } serde_yaml = "*" [dependencies] +chrono = { version = "*", default-features = false} cortex-m-rt = "*" cyw43 = { git = "https://github.com/embassy-rs/embassy.git" } cyw43-pio = { git = "https://github.com/embassy-rs/embassy.git" } embassy-executor = { git = "https://github.com/embassy-rs/embassy.git", features = ["arch-cortex-m", "executor-interrupt", "executor-thread", "integrated-timers", "nightly"] } -embassy-net = { git = "https://github.com/embassy-rs/embassy.git", features = ["tcp", "udp", "raw", "dhcpv4", "dhcpv4-hostname", "dns", "proto-ipv4", "multicast"] } -embassy-rp = { git = "https://github.com/embassy-rs/embassy.git", features = ["critical-section-impl", "time-driver", "rp2040"] } +embassy-net = { git = "https://github.com/embassy-rs/embassy.git", features = ["raw", "dhcpv4", "dhcpv4-hostname", "dns", "multicast", "proto-ipv4", "proto-ipv6", "tcp", "udp" ] } +embassy-rp = { git = "https://github.com/embassy-rs/embassy.git", features = ["critical-section-impl", "rp2040", "time-driver" ] } +embassy-sync = { git = "https://github.com/embassy-rs/embassy.git" } embassy-time = { git = "https://github.com/embassy-rs/embassy.git" } embassy-usb-logger = { git = "https://github.com/embassy-rs/embassy.git" } -log ="*" +log = "*" +no-std-net = "*" panic-halt = "*" portable-atomic = { version = "*", features = ["critical-section"] } rand = { version = "*", default-features = false } -sntpc = { version = "*", default-features = false } +sntpc = { version = "*", default-features = false, features = ["async"] } static_cell = "*" [profile.release] diff --git a/flake.lock b/flake.lock index fe73c3e..a3164d9 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1728344376, - "narHash": "sha256-lxTce2XE6mfJH8Zk6yBbqsbu9/jpwdymbSH5cCbiVOA=", + "lastModified": 1728776144, + "narHash": "sha256-fROVjMcKRoGHofDm8dY3uDUtCMwUICh/KjBFQnuBzfg=", "owner": "ipetkov", "repo": "crane", - "rev": "fd86b78f5f35f712c72147427b1eb81a9bd55d0b", + "rev": "f876e3d905b922502f031aeec1a84490122254b7", "type": "github" }, "original": { @@ -21,11 +21,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1728628307, - "narHash": "sha256-GRMRHZyU+R0RqKPFFgi7BBMDIRFPnHaAhOIxlqyvbZQ=", + "lastModified": 1728887700, + "narHash": "sha256-i+WCARuldFmXlNW6XlEYiL8UGMzjdg5lMQ9gpACQL/A=", "owner": "nix-community", "repo": "fenix", - "rev": "b0a014d5b9dba793ebc205bcf12a93b5f6a4c66c", + "rev": "bcf74e45d5a818fe3aadf5cb3099189770e18579", "type": "github" }, "original": { @@ -95,11 +95,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1728505432, - "narHash": "sha256-QFPMazeiGLo7AGy4RREmTgko0Quch/toMVKhGUjDEeo=", + "lastModified": 1728719115, + "narHash": "sha256-SLcHmKzkIuahBbSCWr2gzu6bXo6wF12JP4myDzH1CvQ=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "0fb804acb375b02a3beeaceeb75b71969ef37b15", + "rev": "d7628c0a8b95cadefe89d9a45f9be5ee4898c6b1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 63eceb2..b73854b 100644 --- a/flake.nix +++ b/flake.nix @@ -18,7 +18,7 @@ fenixPkgs = fenix.packages.${system}; fenixToolchain = fenixPkgs.fromToolchainFile { dir = ./.; - sha256 = "sha256-HAFn+jo7K/dwbCKRHNXQU+x9b+8LJ8xlQGL/tE0rNlE="; + sha256 = "sha256-xN1aESBFCg2dq0LYOHWtU0FtIwkswFbmMoC1GoWCz9c="; }; craneLib = (crane.mkLib pkgs).overrideToolchain fenixToolchain; in { diff --git a/src/main.rs b/src/main.rs index 652c187..1f58372 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,16 +2,17 @@ #![no_std] #![no_main] -mod picow_time; +mod time; extern crate panic_halt; +use chrono::DateTime; use cyw43::JoinOptions; use cyw43_pio::PioSpi; use embassy_executor::Spawner; use embassy_net::{ dns::{DnsQueryType, DnsSocket}, - Config, DhcpConfig, IpAddress, StackResources, + Config, DhcpConfig, IpAddress, Stack, StackResources, }; use embassy_rp::{ bind_interrupts, @@ -21,17 +22,24 @@ use embassy_rp::{ pio::{self, Pio}, usb::{self, Driver}, }; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal}; use embassy_time::Timer; use log::info; -use picow_time::PicowUDPBuffer; +use no_std_net::{IpAddr, Ipv4Addr, SocketAddr}; use rand::RngCore; +use sntpc::{async_impl::get_time, NtpContext}; use static_cell::StaticCell; +use time::{PicowClock, PicowTimestampGenerator, PicowUDPBuffer}; +const DNS_WAIT_SECOND: u64 = 1 << 3; const HOSTNAME: &str = "picow"; const NTP_ENDPOINT: &str = "time.google.com"; -const WAIT_SECOND: u64 = 7; +const NTP_PORT: u16 = 123; +const NTP_WAIT_SECOND: u64 = 1 << 6; +const TIMER_WAIT_SECOND: u64 = 1 << 1; const WIRELESS_CREDENTIALS: &[(&str, &str)] = &include!(concat!(env!("OUT_DIR"), "/wireless-credentials.rs")); +const WIRELESS_SCAN_WAIT_SECOND: u64 = 1 << 3; bind_interrupts!(struct Irqs { PIO0_IRQ_0 => pio::InterruptHandler; @@ -50,6 +58,124 @@ async fn network_task( network_runner.run().await; } +#[embassy_executor::task] +async fn ntp_task( + clock: &'static PicowClock, + network_stack: Stack<'static>, + offline_signal: &'static Signal, +) -> ! { + // dns client + let dns_client = DnsSocket::new(network_stack); + + loop { + // wait for network stack + info!("Wait for network setup"); + network_stack.wait_config_up().await; + network_stack.wait_link_up().await; + info!("Successfully setup network stack"); + info!("Network configuration: {:?}", network_stack.config_v4()); + + let socket_address = match dns_client.query(NTP_ENDPOINT, DnsQueryType::A).await { + Ok(address) => match address.first() { + Some(IpAddress::Ipv4(ipv4)) => { + let octets = ipv4.octets(); + SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3])), + NTP_PORT, + ) + } + _ => { + info!("DNS A record lookup returned no IPv4 address: {NTP_ENDPOINT}",); + Timer::after_secs(DNS_WAIT_SECOND).await; + continue; + } + }, + Err(error) => { + info!("DNS A record lookup for {NTP_ENDPOINT} returned error: {error:?}",); + Timer::after_secs(DNS_WAIT_SECOND).await; + continue; + } + }; + + info!("NTP endpoint identified for {NTP_ENDPOINT}: {socket_address}"); + + loop { + // udp client + let mut udp_buffer = PicowUDPBuffer::default(); + let udp_client = udp_buffer.socket(network_stack, NTP_PORT); + let ntp_context = NtpContext::new(PicowTimestampGenerator::from_clock(clock).await); + match get_time(socket_address, udp_client, ntp_context).await { + Ok(result) => { + if let Some(now) = DateTime::from_timestamp(result.sec() as i64, 0) { + clock.set_time(now).await; + info!("Calibrated system clock with NTP time: {now}"); + } else { + info!("Invalid NTP result: {result:?}"); + } + } + Err(error) => { + info!("Failed to run NTP protocol: {error:?}"); + offline_signal.signal(()); + break; + } + } + Timer::after_secs(NTP_WAIT_SECOND).await; + } + } +} + +#[embassy_executor::task] +async fn online_task( + offline_signal: &'static Signal, + mut wireless_control: cyw43::Control<'static>, +) -> ! { + loop { + // search and join wireless network + for (ssid, password) in WIRELESS_CREDENTIALS.iter().cycle() { + info!("Search for wireless network: {ssid}"); + if wireless_control + .join(ssid, JoinOptions::new(password.as_bytes())) + .await + .is_ok() + { + info!("Successfully joined wireless network: {ssid}"); + break; + } else if ssid + == &WIRELESS_CREDENTIALS + .last() + .expect("The list of wireless credentials should not be empty") + .0 + { + info!("Unable to join any wireless network with provided credentials"); + info!("Sleep for {WIRELESS_SCAN_WAIT_SECOND} seconds before rescan"); + Timer::after_secs(WIRELESS_SCAN_WAIT_SECOND).await; + } + } + + // turn on led for connected wireless + wireless_control.gpio_set(0, true).await; + + // wait for offline signal + offline_signal.wait().await; + info!("Network is down"); + + // disconnect wireless + info!("Reset wireless"); + wireless_control.leave().await; + + // turn off led for disconnected network + wireless_control.gpio_set(0, false).await; + } +} + +#[embassy_executor::task] +async fn timer_task(clock: &'static PicowClock) -> ! { + loop { + info!("The current time is: {}", clock.now().await); + Timer::after_secs(TIMER_WAIT_SECOND).await; + } +} + #[embassy_executor::task] async fn wireless_task( wireless_runner: cyw43::Runner<'static, Output<'static>, PioSpi<'static, PIO0, 0, DMA_CH0>>, @@ -58,7 +184,11 @@ async fn wireless_task( } #[embassy_executor::main] -async fn main(spawner: Spawner) -> ! { +async fn main(spawner: Spawner) { + // system clock + static CLOCK: StaticCell = StaticCell::new(); + let clock = CLOCK.init(PicowClock::new()); + // hardware abstraction layer let hal = embassy_rp::init(Default::default()); @@ -126,73 +256,27 @@ async fn main(spawner: Spawner) -> ! { RoscRng.next_u64(), ); + // offline signal + static OFFLINE_SIGNAL: StaticCell> = StaticCell::new(); + let offline_signal = OFFLINE_SIGNAL.init(Signal::new()); + + // spawn online task + spawner + .spawn(online_task(offline_signal, wireless_control)) + .expect("Online task should not fail to spawn"); + // spawn network task spawner .spawn(network_task(network_runner)) .expect("Network task should not fail to spawn"); - // search and join wireless network - for (ssid, password) in WIRELESS_CREDENTIALS.iter().cycle() { - info!("Searching for wireless network: {ssid}"); - if let Ok(_) = wireless_control - .join(ssid, JoinOptions::new(password.as_bytes())) - .await - { - info!("Successfully joined wireless network: {ssid}"); - break; - } else if ssid - == &WIRELESS_CREDENTIALS - .last() - .expect("The list of wireless credentials should not be empty") - .0 - { - info!("Unable to join any wireless network with provided credentials"); - info!("Sleep for {WAIT_SECOND} seconds before rescan"); - Timer::after_secs(WAIT_SECOND).await; - } - } + // span ntp task + spawner + .spawn(ntp_task(clock, network_stack, offline_signal)) + .expect("NTP task should not fail to spawn"); - // wait for network stack - info!("Wait for network setup"); - network_stack.wait_config_up().await; - network_stack.wait_link_up().await; - info!("Successfully setup network stack"); - info!("Network configuration: {:?}", network_stack.config_v4()); - - // turn on led to indicate network connection - wireless_control.gpio_set(0, true).await; - - // dns client - let dns_client = DnsSocket::new(network_stack); - - // udp client - // let mut udp_buffer = PicowUDPBuffer::default(); - // let udp_client = udp_buffer.socket(network_stack); - - loop { - let ntp_ipv4 = match dns_client.query(NTP_ENDPOINT, DnsQueryType::A).await { - Ok(address) => match address.first() { - Some(IpAddress::Ipv4(ipv4)) => ipv4.octets(), - None => { - info!( - "DNS A record lookup returned no IPv4 address: {}", - NTP_ENDPOINT - ); - continue; - } - }, - Err(error) => { - info!( - "DNS A record lookup returned error: {}, {:?}", - NTP_ENDPOINT, error - ); - continue; - } - }; - info!( - "Resolved IPv4 address for {}: {}.{}.{}.{}", - NTP_ENDPOINT, ntp_ipv4[0], ntp_ipv4[1], ntp_ipv4[1], ntp_ipv4[3] - ); - Timer::after_secs(WAIT_SECOND).await; - } + // spawn timer task + spawner + .spawn(timer_task(clock)) + .expect("Timer task should not fail to spawn"); } diff --git a/src/picow_time.rs b/src/picow_time.rs deleted file mode 100644 index b45d184..0000000 --- a/src/picow_time.rs +++ /dev/null @@ -1,67 +0,0 @@ -use embassy_net::{ - udp::{PacketMetadata, UdpSocket}, - Stack, -}; -use embassy_time::{Duration, Instant}; -use sntpc::NtpTimestampGenerator; - -const UDP_BUFFER_SIZE_BYTE: usize = 1 << 12; -const UDP_METADATA_SIZE: usize = 1 << 3; - -#[derive(Copy, Clone)] -pub struct PicowTimestampGenerator { - duration: Duration, -} - -impl Default for PicowTimestampGenerator { - fn default() -> Self { - Self { - duration: Instant::now().duration_since(Instant::MIN), - } - } -} - -impl NtpTimestampGenerator for PicowTimestampGenerator { - fn init(&mut self) { - self.duration = Instant::now().duration_since(Instant::MIN); - } - - fn timestamp_sec(&self) -> u64 { - self.duration.as_secs() - } - - fn timestamp_subsec_micros(&self) -> u32 { - (self.duration.as_micros() % 1000000) as u32 - } -} - -#[derive(Debug)] -pub struct PicowUDPBuffer { - receive_buffer: [u8; UDP_BUFFER_SIZE_BYTE], - receive_metadata: [PacketMetadata; UDP_METADATA_SIZE], - transmit_buffer: [u8; UDP_BUFFER_SIZE_BYTE], - transmit_metadata: [PacketMetadata; UDP_METADATA_SIZE], -} - -impl Default for PicowUDPBuffer { - fn default() -> Self { - Self { - receive_buffer: [0; UDP_BUFFER_SIZE_BYTE], - receive_metadata: [PacketMetadata::EMPTY; UDP_METADATA_SIZE], - transmit_buffer: [0; UDP_BUFFER_SIZE_BYTE], - transmit_metadata: [PacketMetadata::EMPTY; UDP_METADATA_SIZE], - } - } -} - -impl PicowUDPBuffer { - pub fn socket<'me>(&'me mut self, network_stack: Stack<'me>) -> UdpSocket<'me> { - UdpSocket::new( - network_stack, - &mut self.receive_metadata, - &mut self.receive_buffer, - &mut self.transmit_metadata, - &mut self.transmit_buffer, - ) - } -} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..7fba7d1 --- /dev/null +++ b/src/time.rs @@ -0,0 +1,181 @@ +use core::fmt; + +use chrono::{DateTime, Duration, Utc}; +use embassy_net::{ + udp::{PacketMetadata, UdpSocket}, + IpAddress, IpEndpoint, Stack, +}; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex}; +use embassy_time::Instant; +use no_std_net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; +use sntpc::{async_impl::NtpUdpSocket, NtpTimestampGenerator}; + +const UDP_BUFFER_SIZE_BYTE: usize = 1 << 12; +const UDP_METADATA_SIZE: usize = 1 << 4; + +pub struct PicowClock { + startup_timestamp: Mutex>, +} + +impl PicowClock { + pub fn new() -> Self { + Self { + startup_timestamp: Mutex::new(DateTime::UNIX_EPOCH), + } + } + + pub async fn now(&self) -> DateTime { + *self.startup_timestamp.lock().await + + Duration::microseconds(Instant::now().as_micros() as i64) + } + + pub async fn set_time(&self, now: DateTime) { + if let Some(corrected_startup_timestamp) = + now.checked_sub_signed(Duration::microseconds(Instant::now().as_micros() as i64)) + { + *self.startup_timestamp.lock().await = corrected_startup_timestamp; + } + } +} + +#[derive(Clone, Copy)] +pub struct PicowTimestampGenerator { + now: DateTime, +} + +impl NtpTimestampGenerator for PicowTimestampGenerator { + fn init(&mut self) {} + + fn timestamp_sec(&self) -> u64 { + self.now.timestamp() as u64 + } + + fn timestamp_subsec_micros(&self) -> u32 { + self.now.timestamp_subsec_micros() + } +} + +impl PicowTimestampGenerator { + pub async fn from_clock(clock: &PicowClock) -> Self { + Self { + now: clock.now().await, + } + } +} + +pub struct PicowUDPSocket<'me> { + socket: UdpSocket<'me>, +} + +impl fmt::Debug for PicowUDPSocket<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PicowUDPSocket") + .field("socket", &self.socket.endpoint()) + .finish() + } +} + +impl<'me> NtpUdpSocket for PicowUDPSocket<'me> { + async fn send_to(&self, buf: &[u8], addr: T) -> sntpc::Result { + use sntpc::Error::*; + let socket_address = addr + .to_socket_addrs() + .map_err(|_| AddressResolve)? + .next() + .ok_or(AddressResolve)?; + + // TODO: Simplify conversion once no-std-net is deprecated + let endpoint = IpEndpoint::new( + match socket_address.ip() { + IpAddr::V4(ipv4) => { + let octets = ipv4.octets(); + IpAddress::v4(octets[0], octets[1], octets[2], octets[3]) + } + IpAddr::V6(ipv6) => { + let segments = ipv6.segments(); + IpAddress::v6( + segments[0], + segments[1], + segments[2], + segments[3], + segments[4], + segments[5], + segments[6], + segments[7], + ) + } + }, + socket_address.port(), + ); + self.socket + .send_to(buf, endpoint) + .await + .map_err(|_| Network)?; + Ok(buf.len()) + } + + async fn recv_from(&self, buf: &mut [u8]) -> sntpc::Result<(usize, SocketAddr)> { + use sntpc::Error::*; + let (size, metadata) = self.socket.recv_from(buf).await.map_err(|_| Network)?; + Ok(( + size, + SocketAddr::new( + // TODO: Simplify conversion once no-std-net is deprecated + match metadata.endpoint.addr { + IpAddress::Ipv4(ipv4) => { + let octets = ipv4.octets(); + IpAddr::V4(Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3])) + } + IpAddress::Ipv6(ipv6) => { + let segments = ipv6.segments(); + IpAddr::V6(Ipv6Addr::new( + segments[0], + segments[1], + segments[2], + segments[3], + segments[4], + segments[5], + segments[6], + segments[7], + )) + } + }, + metadata.endpoint.port, + ), + )) + } +} + +pub struct PicowUDPBuffer { + receive_buffer: [u8; UDP_BUFFER_SIZE_BYTE], + receive_metadata: [PacketMetadata; UDP_METADATA_SIZE], + transmit_buffer: [u8; UDP_BUFFER_SIZE_BYTE], + transmit_metadata: [PacketMetadata; UDP_METADATA_SIZE], +} + +impl Default for PicowUDPBuffer { + fn default() -> Self { + Self { + receive_buffer: [0; UDP_BUFFER_SIZE_BYTE], + receive_metadata: [PacketMetadata::EMPTY; UDP_METADATA_SIZE], + transmit_buffer: [0; UDP_BUFFER_SIZE_BYTE], + transmit_metadata: [PacketMetadata::EMPTY; UDP_METADATA_SIZE], + } + } +} + +impl PicowUDPBuffer { + pub fn socket<'me>(&'me mut self, network_stack: Stack<'me>, port: u16) -> PicowUDPSocket<'me> { + let mut socket = UdpSocket::new( + network_stack, + &mut self.receive_metadata, + &mut self.receive_buffer, + &mut self.transmit_metadata, + &mut self.transmit_buffer, + ); + socket + .bind(port) + .expect("UDP socket should be able to bind to local port"); + PicowUDPSocket { socket } + } +}