From e8e0da5cefc6a1ed6ec237deef29fc41896e4a9f Mon Sep 17 00:00:00 2001 From: unizippro Date: Tue, 28 Feb 2023 17:53:09 +0100 Subject: [PATCH 01/16] initial commit - tests not working --- Cargo.toml | 4 + ublox-short-range/Cargo.toml | 4 + ublox-short-range/src/client.rs | 23 +- .../src/command/custom_digest.rs | 233 +++++++++++++----- ublox-short-range/src/wifi/ap.rs | 6 +- ublox-short-range/src/wifi/dns.rs | 5 +- ublox-short-range/src/wifi/supplicant.rs | 11 +- ublox-short-range/src/wifi/tcp_stack.rs | 9 +- ublox-short-range/src/wifi/tls.rs | 5 +- ublox-short-range/src/wifi/udp_stack.rs | 9 +- 10 files changed, 205 insertions(+), 104 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d3b133..0163f9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,7 @@ [workspace] resolver = "2" members = [ "ublox-short-range" ] + + +[patch.crates-io] +atat = { path = "../async_atat/atat" } diff --git a/ublox-short-range/Cargo.toml b/ublox-short-range/Cargo.toml index 51cdbdb..5e18ef1 100644 --- a/ublox-short-range/Cargo.toml +++ b/ublox-short-range/Cargo.toml @@ -27,6 +27,10 @@ defmt = { version = "0.3" } embedded-hal = "=1.0.0-alpha.9" embedded-nal = "0.6.0" fugit = { version = "0.3", features = ["defmt"] } +fugit-timer = "0.1.2" + +[dev-dependencies] +embedded-io = "0.4" [features] default = ["odin_w2xx", "wifi_ap", "wifi_sta", "socket-udp", "socket-tcp"] diff --git a/ublox-short-range/src/client.rs b/ublox-short-range/src/client.rs index 0c03b7a..3b1ec47 100644 --- a/ublox-short-range/src/client.rs +++ b/ublox-short-range/src/client.rs @@ -29,7 +29,6 @@ use crate::{ SocketMap, }, }; -use atat::clock::Clock; use defmt::{debug, error, trace}; use embedded_hal::digital::OutputPin; use embedded_nal::{nb, IpAddr, Ipv4Addr, SocketAddr}; @@ -77,8 +76,8 @@ pub fn new_socket_num<'a, const TIMER_HZ: u32, const N: usize, const L: usize>( pub struct UbloxClient where - C: atat::AtatClient, - CLK: Clock, + C: atat::blocking::AtatClient, + CLK: fugit_timer::Timer, RST: OutputPin, { pub(crate) module_started: bool, @@ -100,8 +99,8 @@ where impl UbloxClient where - C: atat::AtatClient, - CLK: Clock, + C: atat::blocking::AtatClient, + CLK: fugit_timer::Timer, RST: OutputPin, { pub fn new(client: C, timer: CLK, config: Config) -> Self { @@ -321,7 +320,8 @@ where } pub(crate) fn clear_buffers(&mut self) -> Result<(), Error> { - self.client.reset(); + // self.client.reset(); deprecated + if let Some(ref mut sockets) = self.sockets.as_deref_mut() { sockets.prune(); } @@ -364,12 +364,9 @@ where } } - self.client.send(req).map_err(|e| match e { - nb::Error::Other(ate) => { - error!("{:?}: {=[u8]:a}", ate, req.as_bytes()); - ate.into() - } - nb::Error::WouldBlock => Error::_Unknown, + self.client.send(req).map_err(|e| { + error!("{:?}: {=[u8]:a}", e, req.as_bytes()); + e.into() }) } @@ -385,7 +382,7 @@ where let mut a = self.urc_attempts; let max = self.config.max_urc_attempts; - self.client.peek_urc_with::(|edm_urc| { + self.client.try_read_urc_with::(|edm_urc, _| { ran = true; let res = match edm_urc { EdmEvent::ATEvent(urc) => { diff --git a/ublox-short-range/src/command/custom_digest.rs b/ublox-short-range/src/command/custom_digest.rs index d292d6a..8135168 100644 --- a/ublox-short-range/src/command/custom_digest.rs +++ b/ublox-short-range/src/command/custom_digest.rs @@ -103,53 +103,63 @@ impl Digester for EdmDigester { #[cfg(test)] mod test { + use super::*; - use atat::bbqueue::BBBuffer; - use atat::IngressManager; - use atat::Response; + use atat::bbqueue::framed::FrameConsumer; + use atat::{frame::Frame, AtatIngress, Buffers, Ingress, Response}; const TEST_RX_BUF_LEN: usize = 256; const TEST_RES_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; const TEST_URC_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; - macro_rules! setup_ingressmanager { - () => {{ - static mut RES_Q: BBBuffer = BBBuffer::new(); - let (res_p, res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; - - static mut URC_Q: BBBuffer = BBBuffer::new(); - let (urc_p, urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; - - ( - IngressManager::<_, TEST_RX_BUF_LEN, TEST_RES_CAPACITY, TEST_URC_CAPACITY>::new( - res_p, - urc_p, - EdmDigester::default(), - ), - res_c, - urc_c, - ) - }}; - } + // macro_rules! setup_ingressmanager { + // () => {{ + // static mut RES_Q: BBBuffer = BBBuffer::new(); + // let (res_p, res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; + + // static mut URC_Q: BBBuffer = BBBuffer::new(); + // let (urc_p, urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; + + // ( + // IngressManager::<_, TEST_RX_BUF_LEN, TEST_RES_CAPACITY, TEST_URC_CAPACITY>::new( + // res_p, + // urc_p, + // EdmDigester::default(), + // ), + // res_c, + // urc_c, + // ) + // }}; + // } /// Removed functionality used to change OK responses to empty responses. #[test] - fn ok_response() { - let (mut at_pars, mut res_c, mut urc_c) = setup_ingressmanager!(); - - at_pars.digest(); + fn ok_response<'a>() { + let mut at_pars: Ingress< + 'a, + EdmDigester, + TEST_RX_BUF_LEN, + TEST_RES_CAPACITY, + TEST_URC_CAPACITY, + >; + let mut res_c: FrameConsumer<'a, TEST_RES_CAPACITY>; + let mut urc_c: FrameConsumer<'a, TEST_URC_CAPACITY>; + let buf = Buffers::::new(); + (at_pars, res_c, urc_c) = buf.to_ingress(EdmDigester::default()); // Payload: "OK\r\n" let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; let empty_ok_response = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; - at_pars.write(data); - - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); let mut grant = res_c.read().unwrap(); grant.auto_release(true); - let res = match Response::from(grant.as_ref()) { + let frame = Frame::decode(grant.as_ref()); + let res = match Response::from(frame) { Response::Result(r) => r, Response::Prompt(_) => Ok(&[][..]), }; @@ -160,20 +170,33 @@ mod test { #[test] fn error_response() { - let (mut at_pars, mut res_c, mut urc_c) = setup_ingressmanager!(); + let mut at_pars: Ingress< + 'static, + EdmDigester, + TEST_RX_BUF_LEN, + TEST_RES_CAPACITY, + TEST_URC_CAPACITY, + >; + let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; + let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; + (at_pars, res_c, urc_c) = + Buffers::::new() + .to_ingress(EdmDigester::default()); // Payload: "ERROR\r\n" let data = &[ 0xAA, 0x00, 0x09, 0x00, 0x45, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x0D, 0x0a, 0x55, ]; - at_pars.digest(); - at_pars.write(data); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); - at_pars.digest(); let mut grant = res_c.read().unwrap(); grant.auto_release(true); - let res = match Response::from(grant.as_ref()) { + let frame = Frame::decode(grant.as_ref()); + let res = match Response::from(frame) { Response::Result(r) => r, Response::Prompt(_) => Ok(&[][..]), }; @@ -183,8 +206,18 @@ mod test { #[test] fn regular_response_with_trailing_ok() { - let (mut at_pars, mut res_c, mut urc_c) = setup_ingressmanager!(); - at_pars.digest(); + let mut at_pars: Ingress< + 'static, + EdmDigester, + TEST_RX_BUF_LEN, + TEST_RES_CAPACITY, + TEST_URC_CAPACITY, + >; + let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; + let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; + (at_pars, res_c, urc_c) = + Buffers::::new() + .to_ingress(EdmDigester::default()); // Payload: AT\r\n let response = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x41, 0x54, 0x0D, 0x0a, 0x55]; @@ -194,12 +227,15 @@ mod test { 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, ]; - at_pars.write(data); - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); let mut grant = res_c.read().unwrap(); grant.auto_release(true); - let res = match Response::from(grant.as_ref()) { + let frame = Frame::decode(grant.as_ref()); + let res = match Response::from(frame) { Response::Result(r) => r, Response::Prompt(_) => Ok(&[][..]), }; @@ -210,7 +246,18 @@ mod test { /// Regular response with traling regular response.. #[test] fn at_urc() { - let (mut at_pars, mut res_c, mut urc_c) = setup_ingressmanager!(); + let mut at_pars: Ingress< + 'static, + EdmDigester, + TEST_RX_BUF_LEN, + TEST_RES_CAPACITY, + TEST_URC_CAPACITY, + >; + let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; + let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; + (at_pars, res_c, urc_c) = + Buffers::::new() + .to_ingress(EdmDigester::default()); let type_byte = PayloadType::ATEvent as u8; // Payload: "OK\r\n" @@ -222,10 +269,12 @@ mod test { 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, 0x3A, 0x33, 0x0D, 0x0A, 0x55, ]; - at_pars.write(data); - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); - let mut grant = urc_c.read().unwrap(); + let mut grant = res_c.read().unwrap(); grant.auto_release(true); assert_eq!(grant.as_ref(), result); assert_eq!(res_c.read(), None); @@ -233,7 +282,18 @@ mod test { #[test] fn data_event() { - let (mut at_pars, mut res_c, mut urc_c) = setup_ingressmanager!(); + let mut at_pars: Ingress< + 'static, + EdmDigester, + TEST_RX_BUF_LEN, + TEST_RES_CAPACITY, + TEST_URC_CAPACITY, + >; + let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; + let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; + (at_pars, res_c, urc_c) = + Buffers::::new() + .to_ingress(EdmDigester::default()); let type_byte = PayloadType::DataEvent as u8; // Payload: "OK\r\n" @@ -245,18 +305,32 @@ mod test { 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, 0x3A, 0x33, 0x0D, 0x0A, 0x55, ]; - at_pars.write(data); - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); - let mut grant = urc_c.read().unwrap(); + let mut grant = res_c.read().unwrap(); grant.auto_release(true); + assert_eq!(grant.as_ref(), result); assert_eq!(res_c.read(), None); } #[test] fn connect_disconnect_events() { - let (mut at_pars, mut res_c, mut urc_c) = setup_ingressmanager!(); + let mut at_pars: Ingress< + 'static, + EdmDigester, + TEST_RX_BUF_LEN, + TEST_RES_CAPACITY, + TEST_URC_CAPACITY, + >; + let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; + let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; + (at_pars, res_c, urc_c) = + Buffers::::new() + .to_ingress(EdmDigester::default()); let type_byte = PayloadType::ConnectEvent as u8; // Payload: "OK\r\n" @@ -268,9 +342,12 @@ mod test { 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, 0x3A, 0x33, 0x0D, 0x0A, 0x55, ]; - at_pars.write(data); - at_pars.digest(); - let mut grant = urc_c.read().unwrap(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); + + let mut grant = res_c.read().unwrap(); grant.auto_release(true); assert_eq!(grant.as_ref(), result); assert_eq!(res_c.read(), None); @@ -286,8 +363,13 @@ mod test { 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, 0x3A, 0x33, 0x0D, 0x0A, 0x55, ]; - at_pars.write(data); - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); + + let mut grant = res_c.read().unwrap(); + grant.auto_release(true); let mut grant = urc_c.read().unwrap(); grant.auto_release(true); @@ -297,29 +379,46 @@ mod test { #[test] fn wrong_type_packet() { - let (mut at_pars, mut res_c, mut urc_c) = setup_ingressmanager!(); + let mut at_pars: Ingress< + 'static, + EdmDigester, + TEST_RX_BUF_LEN, + TEST_RES_CAPACITY, + TEST_URC_CAPACITY, + >; + let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; + let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; + (at_pars, res_c, urc_c) = + Buffers::::new() + .to_ingress(EdmDigester::default()); let type_byte = PayloadType::Unknown as u8; // Payload: "OK\r\n" let data = &[ 0xAA, 0x00, 0x06, 0x00, type_byte, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, ]; - at_pars.write(data); - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); + + let mut grant = res_c.read().unwrap(); + grant.auto_release(true); assert_eq!(urc_c.read(), None); assert_eq!(res_c.read(), None); - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); - at_pars.write(data); - at_pars.digest(); + let mut grant = res_c.read().unwrap(); + grant.auto_release(true); assert_eq!(urc_c.read(), None); assert_eq!(res_c.read(), None); // Recovered enough to receive normal data? - at_pars.digest(); - // Payload: "OK\r\n" let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; let empty_ok_response = heapless::Vec::::from_slice(&[ @@ -327,12 +426,16 @@ mod test { ]) .unwrap(); - at_pars.write(data); - at_pars.digest(); + let ingress_buf = at_pars.write_buf(); + let len = usize::min(data.len(), ingress_buf.len()); + ingress_buf[..len].copy_from_slice(&data[..len]); + at_pars.try_advance(len); let mut grant = res_c.read().unwrap(); grant.auto_release(true); - let res = match Response::from(grant.as_ref()) { + + let frame = Frame::decode(grant.as_ref()); + let res = match Response::from(frame) { Response::Result(r) => r, Response::Prompt(_) => Ok(&[][..]), }; diff --git a/ublox-short-range/src/wifi/ap.rs b/ublox-short-range/src/wifi/ap.rs index 02f9776..54b0184 100644 --- a/ublox-short-range/src/wifi/ap.rs +++ b/ublox-short-range/src/wifi/ap.rs @@ -17,8 +17,8 @@ use crate::{ options::{ConnectionOptions, HotspotOptions}, }, }; -use atat::AtatClient; -use atat::{clock::Clock, heapless_bytes::Bytes}; +use atat::blocking::AtatClient; +use atat::heapless_bytes::Bytes; use embedded_hal::digital::OutputPin; use super::connection::{WiFiState, WifiConnection}; @@ -27,7 +27,7 @@ impl UbloxClient where C: AtatClient, - CLK: Clock, + CLK: fugit_timer::Timer, RST: OutputPin, { /// Creates wireless hotspot service for host machine. diff --git a/ublox-short-range/src/wifi/dns.rs b/ublox-short-range/src/wifi/dns.rs index b8df5fc..b75efd8 100644 --- a/ublox-short-range/src/wifi/dns.rs +++ b/ublox-short-range/src/wifi/dns.rs @@ -1,5 +1,4 @@ use crate::client::DNSState; -use atat::clock::Clock; use embedded_hal::digital::OutputPin; use embedded_nal::{nb, AddrType, Dns, IpAddr}; use fugit::ExtU32; @@ -11,8 +10,8 @@ use ublox_sockets::Error; impl Dns for UbloxClient where - C: atat::AtatClient, - CLK: Clock, + C: atat::blocking::AtatClient, + CLK: fugit_timer::Timer, RST: OutputPin, { type Error = Error; diff --git a/ublox-short-range/src/wifi/supplicant.rs b/ublox-short-range/src/wifi/supplicant.rs index 7b0cc79..9f974a5 100644 --- a/ublox-short-range/src/wifi/supplicant.rs +++ b/ublox-short-range/src/wifi/supplicant.rs @@ -59,18 +59,15 @@ pub struct Supplicant<'a, C, const N: usize> { impl<'a, C, const N: usize> Supplicant<'a, C, N> where - C: atat::AtatClient, + C: atat::blocking::AtatClient, { fn send_at, const LEN: usize>( &mut self, req: &A, ) -> Result { - self.client.send(req).map_err(|e| match e { - nb::Error::Other(ate) => { - defmt::error!("{:?}: {=[u8]:a}", ate, req.as_bytes()); - ate.into() - } - nb::Error::WouldBlock => Error::_Unknown, + self.client.send(req).map_err(|e| { + defmt::error!("{:?}: {=[u8]:a}", e, req.as_bytes()); + e.into() }) } diff --git a/ublox-short-range/src/wifi/tcp_stack.rs b/ublox-short-range/src/wifi/tcp_stack.rs index eb45c29..1c8f7f1 100644 --- a/ublox-short-range/src/wifi/tcp_stack.rs +++ b/ublox-short-range/src/wifi/tcp_stack.rs @@ -5,7 +5,6 @@ use crate::{ wifi::peer_builder::PeerUrlBuilder, UbloxClient, }; -use atat::clock::Clock; use embedded_hal::digital::OutputPin; /// Handles receiving data from sockets /// implements TCP and UDP for WiFi client @@ -18,8 +17,8 @@ use super::EGRESS_CHUNK_SIZE; impl TcpClientStack for UbloxClient where - C: atat::AtatClient, - CLK: Clock, + C: atat::blocking::AtatClient, + CLK: fugit_timer::Timer, RST: OutputPin, { type Error = Error; @@ -232,8 +231,8 @@ where // impl TcpFullStack for UbloxClient // where -// C: atat::AtatClient, -// CLK: Clock, +// C: atat::blocking::AtatClient, +// CLK: fugit_timer::Timer, // RST: OutputPin, // Generic: TryInto, // { diff --git a/ublox-short-range/src/wifi/tls.rs b/ublox-short-range/src/wifi/tls.rs index 2bd14eb..2d0b04d 100644 --- a/ublox-short-range/src/wifi/tls.rs +++ b/ublox-short-range/src/wifi/tls.rs @@ -4,7 +4,6 @@ use crate::{ error::Error, UbloxClient, }; -use atat::clock::Clock; use embedded_hal::digital::OutputPin; use heapless::String; @@ -22,8 +21,8 @@ pub trait TLS { impl TLS for UbloxClient where - C: atat::AtatClient, - CLK: Clock, + C: atat::blocking::AtatClient, + CLK: fugit_timer::Timer, RST: OutputPin, { /// Importing credentials enabeles their use for all further TCP connections diff --git a/ublox-short-range/src/wifi/udp_stack.rs b/ublox-short-range/src/wifi/udp_stack.rs index 597aa78..1c1a6da 100644 --- a/ublox-short-range/src/wifi/udp_stack.rs +++ b/ublox-short-range/src/wifi/udp_stack.rs @@ -8,7 +8,6 @@ use crate::{ wifi::peer_builder::PeerUrlBuilder, UbloxClient, }; -use atat::clock::Clock; use embedded_hal::digital::OutputPin; use embedded_nal::{nb, SocketAddr, UdpFullStack}; @@ -20,8 +19,8 @@ use super::EGRESS_CHUNK_SIZE; impl UdpClientStack for UbloxClient where - C: atat::AtatClient, - CLK: Clock, + C: atat::blocking::AtatClient, + CLK: fugit_timer::Timer, RST: OutputPin, { type Error = Error; @@ -297,8 +296,8 @@ where impl UdpFullStack for UbloxClient where - C: atat::AtatClient, - CLK: Clock, + C: atat::blocking::AtatClient, + CLK: fugit_timer::Timer, RST: OutputPin, { fn bind(&mut self, socket: &mut Self::UdpSocket, local_port: u16) -> Result<(), Self::Error> { From 28deaa4ec94cc5f3529b94e34eb04403286b5bd3 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 24 Mar 2023 07:53:53 +0100 Subject: [PATCH 02/16] Initial work on async client --- Cargo.toml | 3 +- ublox-short-range/Cargo.toml | 17 +- ublox-short-range/src/asynch/mod.rs | 115 +++ ublox-short-range/src/asynch/tcp.rs | 493 +++++++++++++ .../src/{ => blocking}/client.rs | 96 +-- .../src/{wifi => blocking}/dns.rs | 37 +- ublox-short-range/src/blocking/mod.rs | 12 + .../src/{wifi => blocking}/tcp_stack.rs | 50 +- ublox-short-range/src/blocking/timer.rs | 42 ++ .../src/{wifi => blocking}/tls.rs | 6 +- .../src/{wifi => blocking}/udp_stack.rs | 34 +- .../src/command/custom_digest.rs | 671 +++++++++--------- ublox-short-range/src/error.rs | 2 +- ublox-short-range/src/lib.rs | 13 +- ublox-short-range/src/wifi/ap.rs | 6 +- ublox-short-range/src/wifi/mod.rs | 8 - ublox-short-range/src/wifi/peer_builder.rs | 2 +- ublox-short-range/src/wifi/supplicant.rs | 1 - 18 files changed, 1106 insertions(+), 502 deletions(-) create mode 100644 ublox-short-range/src/asynch/mod.rs create mode 100644 ublox-short-range/src/asynch/tcp.rs rename ublox-short-range/src/{ => blocking}/client.rs (92%) rename ublox-short-range/src/{wifi => blocking}/dns.rs (50%) create mode 100644 ublox-short-range/src/blocking/mod.rs rename ublox-short-range/src/{wifi => blocking}/tcp_stack.rs (83%) create mode 100644 ublox-short-range/src/blocking/timer.rs rename ublox-short-range/src/{wifi => blocking}/tls.rs (94%) rename ublox-short-range/src/{wifi => blocking}/udp_stack.rs (92%) diff --git a/Cargo.toml b/Cargo.toml index 0163f9f..d9902af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ members = [ "ublox-short-range" ] [patch.crates-io] -atat = { path = "../async_atat/atat" } +atat = { path = "../atat/atat" } +ublox-sockets = { path = "../ublox-sockets" } diff --git a/ublox-short-range/Cargo.toml b/ublox-short-range/Cargo.toml index 5e18ef1..e9637bf 100644 --- a/ublox-short-range/Cargo.toml +++ b/ublox-short-range/Cargo.toml @@ -15,7 +15,8 @@ name = "ublox_short_range" doctest = false [dependencies] -atat = { version = "0.18.0", features = ["derive", "defmt", "bytes"] } +# atat = { version = "0.18.0", features = ["derive", "defmt", "bytes"] } +atat = { path = "../../atat/atat", features = ["derive", "defmt", "bytes"] } heapless = { version = "^0.7", features = ["serde", "defmt-impl"] } no-std-net = { version = "^0.5", features = ["serde"] } serde = { version = "^1", default-features = false, features = ["derive"] } @@ -26,14 +27,22 @@ hash32-derive = "^0.1.0" defmt = { version = "0.3" } embedded-hal = "=1.0.0-alpha.9" embedded-nal = "0.6.0" -fugit = { version = "0.3", features = ["defmt"] } -fugit-timer = "0.1.2" +embassy-time = "0.1" +embassy-sync = "0.1" + +embedded-hal-async = { version = "=0.2.0-alpha.0", optional = true } +embedded-nal-async = { version = "0.4", optional = true } +futures = { version = "0.3.17", default-features = false, features = [ "async-await" ] } [dev-dependencies] embedded-io = "0.4" [features] -default = ["odin_w2xx", "wifi_ap", "wifi_sta", "socket-udp", "socket-tcp"] +default = ["async", "odin_w2xx", "wifi_ap", "wifi_sta", "socket-udp", "socket-tcp"] + +async = ["dep:embedded-hal-async", "dep:embedded-nal-async", "atat/async"] + +std = [] odin_w2xx = [] nina_w1xx = [] diff --git a/ublox-short-range/src/asynch/mod.rs b/ublox-short-range/src/asynch/mod.rs new file mode 100644 index 0000000..05b87ba --- /dev/null +++ b/ublox-short-range/src/asynch/mod.rs @@ -0,0 +1,115 @@ +#[cfg(feature = "socket-tcp")] +pub mod tcp; +#[cfg(feature = "udp")] +pub mod udp; + +use core::cell::RefCell; +use core::future::{poll_fn, Future}; +use core::task::{Context, Poll}; + +use embassy_sync::waitqueue::WakerRegistration; +use embassy_time::Instant; +use futures::pin_mut; +use ublox_sockets::SocketSet; + +// pub struct StackResources { +// sockets: [SocketStorage<'static>; SOCK], +// } + +// impl StackResources { +// pub fn new() -> Self { +// Self { +// sockets: [SocketStorage::EMPTY; SOCK], +// } +// } +// } + +pub struct Stack { + pub(crate) socket: RefCell, + inner: RefCell>, +} + +struct Inner { + device: D, + link_up: bool, +} + +pub(crate) struct SocketStack { + pub(crate) sockets: SocketSet<3, 1024>, + pub(crate) waker: WakerRegistration, +} + +impl Stack { + pub fn new( + mut device: D, + // resources: &'static mut StackResources, + ) -> Self { + let sockets = SocketSet::new(); + // let sockets = SocketSet::new(&mut resources.sockets[..]); + + let mut socket = SocketStack { + sockets, + waker: WakerRegistration::new(), + }; + + let mut inner = Inner { + device, + link_up: false, + }; + + Self { + socket: RefCell::new(socket), + inner: RefCell::new(inner), + } + } + + fn with(&self, f: impl FnOnce(&SocketStack, &Inner) -> R) -> R { + f(&*self.socket.borrow(), &*self.inner.borrow()) + } + + fn with_mut(&self, f: impl FnOnce(&mut SocketStack, &mut Inner) -> R) -> R { + f( + &mut *self.socket.borrow_mut(), + &mut *self.inner.borrow_mut(), + ) + } + + pub async fn run(&self) -> ! { + poll_fn(|cx| { + self.with_mut(|s, i| i.poll(cx, s)); + Poll::<()>::Pending + }) + .await; + unreachable!() + } +} + +impl Inner { + fn poll(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) { + s.waker.register(cx.waker()); + + let timestamp = Instant::now(); + // let mut smoldev = DriverAdapter { + // cx: Some(cx), + // inner: &mut self.device, + // }; + // s.iface.poll(timestamp, &mut smoldev, &mut s.sockets); + + // Update link up + // let old_link_up = self.link_up; + // self.link_up = self.device.link_state(cx) == LinkState::Up; + + // // Print when changed + // if old_link_up != self.link_up { + // info!("link_up = {:?}", self.link_up); + // } + + // if let Some(poll_at) = s.iface.poll_at(timestamp, &mut s.sockets) { + // let t = Timer::at(instant_from_smoltcp(poll_at)); + // pin_mut!(t); + // if t.poll(cx).is_ready() { + // cx.waker().wake_by_ref(); + // } + // } + } +} diff --git a/ublox-short-range/src/asynch/tcp.rs b/ublox-short-range/src/asynch/tcp.rs new file mode 100644 index 0000000..af005d8 --- /dev/null +++ b/ublox-short-range/src/asynch/tcp.rs @@ -0,0 +1,493 @@ +use core::cell::RefCell; +use core::future::poll_fn; +use core::mem; +use core::task::Poll; + +use ublox_sockets::{SocketHandle, tcp}; + +use super::{SocketStack, Stack}; + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + ConnectionReset, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ConnectError { + /// The socket is already connected or listening. + InvalidState, + /// The remote host rejected the connection with a RST packet. + ConnectionReset, + /// Connect timed out. + TimedOut, + /// No route to host. + NoRoute, +} + +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AcceptError { + /// The socket is already connected or listening. + InvalidState, + /// Invalid listen port + InvalidPort, + /// The remote host rejected the connection with a RST packet. + ConnectionReset, +} + +pub struct TcpSocket<'a> { + io: TcpIo<'a>, +} + +pub struct TcpReader<'a> { + io: TcpIo<'a>, +} + +pub struct TcpWriter<'a> { + io: TcpIo<'a>, +} + +impl<'a> TcpReader<'a> { + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } +} + +impl<'a> TcpWriter<'a> { + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } +} + +impl<'a> TcpSocket<'a> { + pub fn new(stack: &'a Stack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8]) -> Self { + let s = &mut *stack.socket.borrow_mut(); + let rx_buffer: &'static mut [u8] = unsafe { mem::transmute(rx_buffer) }; + let tx_buffer: &'static mut [u8] = unsafe { mem::transmute(tx_buffer) }; + let handle = s.sockets.add(tcp::TcpSocket::new( + tcp::TcpSocketBuffer::new(rx_buffer), + tcp::TcpSocketBuffer::new(tx_buffer), + )); + + Self { + io: TcpIo { + stack: &stack.socket, + handle, + }, + } + } + + pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { + (TcpReader { io: self.io }, TcpWriter { io: self.io }) + } + + pub async fn connect(&mut self, remote_endpoint: T) -> Result<(), ConnectError> + where + T: Into, + { + let local_port = self.io.stack.borrow_mut().get_local_port(); + + match { + self.io + .with_mut(|s, i| s.connect(i.context(), remote_endpoint, local_port)) + } { + Ok(()) => {} + Err(tcp::ConnectError::InvalidState) => return Err(ConnectError::InvalidState), + Err(tcp::ConnectError::Unaddressable) => return Err(ConnectError::NoRoute), + } + + poll_fn(|cx| { + self.io.with_mut(|s, _| match s.state() { + tcp::State::Closed | tcp::State::TimeWait => Poll::Ready(Err(ConnectError::ConnectionReset)), + tcp::State::Listen => unreachable!(), + tcp::State::SynSent | tcp::State::SynReceived => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + _ => Poll::Ready(Ok(())), + }) + }) + .await + } + + pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + where + T: Into, + { + match self.io.with_mut(|s, _| s.listen(local_endpoint)) { + Ok(()) => {} + Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), + Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), + } + + poll_fn(|cx| { + self.io.with_mut(|s, _| match s.state() { + tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + _ => Poll::Ready(Ok(())), + }) + }) + .await + } + + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + pub async fn flush(&mut self) -> Result<(), Error> { + self.io.flush().await + } + + pub fn set_timeout(&mut self, duration: Option) { + self.io.with_mut(|s, _| s.set_timeout(duration)) + } + + pub fn set_keep_alive(&mut self, interval: Option) { + self.io.with_mut(|s, _| s.set_keep_alive(interval)) + } + + pub fn local_endpoint(&self) -> Option { + self.io.with(|s, _| s.local_endpoint()) + } + + pub fn remote_endpoint(&self) -> Option { + self.io.with(|s, _| s.remote_endpoint()) + } + + pub fn state(&self) -> tcp::State { + self.io.with(|s, _| s.state()) + } + + pub fn close(&mut self) { + self.io.with_mut(|s, _| s.close()) + } + + pub fn abort(&mut self) { + self.io.with_mut(|s, _| s.abort()) + } + + pub fn may_send(&self) -> bool { + self.io.with(|s, _| s.may_send()) + } + + pub fn may_recv(&self) -> bool { + self.io.with(|s, _| s.may_recv()) + } +} + +impl<'a> Drop for TcpSocket<'a> { + fn drop(&mut self) { + self.io.stack.borrow_mut().sockets.remove(self.io.handle); + } +} + +// ======================= + +#[derive(Copy, Clone)] +struct TcpIo<'a> { + stack: &'a RefCell, + handle: SocketHandle, +} + +impl<'d> TcpIo<'d> { + fn with(&self, f: impl FnOnce(&tcp::TcpSocket, &Interface) -> R) -> R { + let s = &*self.stack.borrow(); + let socket = s.sockets.get::(self.handle); + f(socket, &s.iface) + } + + fn with_mut(&mut self, f: impl FnOnce(&mut tcp::TcpSocket, &mut Interface) -> R) -> R { + let s = &mut *self.stack.borrow_mut(); + let socket = s.sockets.get_mut::(self.handle); + let res = f(socket, &mut s.iface); + s.waker.wake(); + res + } + + async fn read(&mut self, buf: &mut [u8]) -> Result { + poll_fn(move |cx| { + // CAUTION: smoltcp semantics around EOF are different to what you'd expect + // from posix-like IO, so we have to tweak things here. + self.with_mut(|s, _| match s.recv_slice(buf) { + // No data ready + Ok(0) => { + s.register_recv_waker(cx.waker()); + Poll::Pending + } + // Data ready! + Ok(n) => Poll::Ready(Ok(n)), + // EOF + Err(tcp::RecvError::Finished) => Poll::Ready(Ok(0)), + // Connection reset. TODO: this can also be timeouts etc, investigate. + Err(tcp::RecvError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + async fn write(&mut self, buf: &[u8]) -> Result { + poll_fn(move |cx| { + self.with_mut(|s, _| match s.send_slice(buf) { + // Not ready to send (no space in the tx buffer) + Ok(0) => { + s.register_send_waker(cx.waker()); + Poll::Pending + } + // Some data sent + Ok(n) => Poll::Ready(Ok(n)), + // Connection reset. TODO: this can also be timeouts etc, investigate. + Err(tcp::SendError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + }) + }) + .await + } + + async fn flush(&mut self) -> Result<(), Error> { + poll_fn(move |cx| { + self.with_mut(|s, _| { + // If there are outstanding send operations, register for wake up and wait + // smoltcp issues wake-ups when octets are dequeued from the send buffer + if s.send_queue() > 0 { + s.register_send_waker(cx.waker()); + Poll::Pending + // No outstanding sends, socket is flushed + } else { + Poll::Ready(Ok(())) + } + }) + }) + .await + } +} + +// #[cfg(feature = "nightly")] +// mod embedded_io_impls { +// use super::*; + +// impl embedded_io::Error for ConnectError { +// fn kind(&self) -> embedded_io::ErrorKind { +// embedded_io::ErrorKind::Other +// } +// } + +// impl embedded_io::Error for Error { +// fn kind(&self) -> embedded_io::ErrorKind { +// embedded_io::ErrorKind::Other +// } +// } + +// impl<'d> embedded_io::Io for TcpSocket<'d> { +// type Error = Error; +// } + +// impl<'d> embedded_io::asynch::Read for TcpSocket<'d> { +// async fn read(&mut self, buf: &mut [u8]) -> Result { +// self.io.read(buf).await +// } +// } + +// impl<'d> embedded_io::asynch::Write for TcpSocket<'d> { +// async fn write(&mut self, buf: &[u8]) -> Result { +// self.io.write(buf).await +// } + +// async fn flush(&mut self) -> Result<(), Self::Error> { +// self.io.flush().await +// } +// } + +// impl<'d> embedded_io::Io for TcpReader<'d> { +// type Error = Error; +// } + +// impl<'d> embedded_io::asynch::Read for TcpReader<'d> { +// async fn read(&mut self, buf: &mut [u8]) -> Result { +// self.io.read(buf).await +// } +// } + +// impl<'d> embedded_io::Io for TcpWriter<'d> { +// type Error = Error; +// } + +// impl<'d> embedded_io::asynch::Write for TcpWriter<'d> { +// async fn write(&mut self, buf: &[u8]) -> Result { +// self.io.write(buf).await +// } + +// async fn flush(&mut self) -> Result<(), Self::Error> { +// self.io.flush().await +// } +// } +// } + +// #[cfg(all(feature = "unstable-traits", feature = "nightly"))] +// pub mod client { +// use core::cell::UnsafeCell; +// use core::mem::MaybeUninit; +// use core::ptr::NonNull; + +// use atomic_polyfill::{AtomicBool, Ordering}; +// use embedded_nal_async::IpAddr; + +// use super::*; + +// /// TCP client capable of creating up to N multiple connections with tx and rx buffers according to TX_SZ and RX_SZ. +// pub struct TcpClient<'d, D: Driver, const N: usize, const TX_SZ: usize = 1024, const RX_SZ: usize = 1024> { +// stack: &'d Stack, +// state: &'d TcpClientState, +// } + +// impl<'d, D: Driver, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpClient<'d, D, N, TX_SZ, RX_SZ> { +// /// Create a new TcpClient +// pub fn new(stack: &'d Stack, state: &'d TcpClientState) -> Self { +// Self { stack, state } +// } +// } + +// impl<'d, D: Driver, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_nal_async::TcpConnect +// for TcpClient<'d, D, N, TX_SZ, RX_SZ> +// { +// type Error = Error; +// type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; + +// async fn connect<'a>( +// &'a self, +// remote: embedded_nal_async::SocketAddr, +// ) -> Result, Self::Error> +// where +// Self: 'a, +// { +// let addr: crate::IpAddress = match remote.ip() { +// IpAddr::V4(addr) => crate::IpAddress::Ipv4(crate::Ipv4Address::from_bytes(&addr.octets())), +// #[cfg(feature = "proto-ipv6")] +// IpAddr::V6(addr) => crate::IpAddress::Ipv6(crate::Ipv6Address::from_bytes(&addr.octets())), +// #[cfg(not(feature = "proto-ipv6"))] +// IpAddr::V6(_) => panic!("ipv6 support not enabled"), +// }; +// let remote_endpoint = (addr, remote.port()); +// let mut socket = TcpConnection::new(&self.stack, self.state)?; +// socket +// .socket +// .connect(remote_endpoint) +// .await +// .map_err(|_| Error::ConnectionReset)?; +// Ok(socket) +// } +// } + +// pub struct TcpConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { +// socket: TcpSocket<'d>, +// state: &'d TcpClientState, +// bufs: NonNull<([u8; TX_SZ], [u8; RX_SZ])>, +// } + +// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpConnection<'d, N, TX_SZ, RX_SZ> { +// fn new(stack: &'d Stack, state: &'d TcpClientState) -> Result { +// let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; +// Ok(Self { +// socket: unsafe { TcpSocket::new(stack, &mut bufs.as_mut().1, &mut bufs.as_mut().0) }, +// state, +// bufs, +// }) +// } +// } + +// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop for TcpConnection<'d, N, TX_SZ, RX_SZ> { +// fn drop(&mut self) { +// unsafe { +// self.socket.close(); +// self.state.pool.free(self.bufs); +// } +// } +// } + +// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::Io +// for TcpConnection<'d, N, TX_SZ, RX_SZ> +// { +// type Error = Error; +// } + +// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Read +// for TcpConnection<'d, N, TX_SZ, RX_SZ> +// { +// async fn read(&mut self, buf: &mut [u8]) -> Result { +// self.socket.read(buf).await +// } +// } + +// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Write +// for TcpConnection<'d, N, TX_SZ, RX_SZ> +// { +// async fn write(&mut self, buf: &[u8]) -> Result { +// self.socket.write(buf).await +// } + +// async fn flush(&mut self) -> Result<(), Self::Error> { +// self.socket.flush().await +// } +// } + +// /// State for TcpClient +// pub struct TcpClientState { +// pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, +// } + +// impl TcpClientState { +// pub const fn new() -> Self { +// Self { pool: Pool::new() } +// } +// } + +// unsafe impl Sync for TcpClientState {} + +// struct Pool { +// used: [AtomicBool; N], +// data: [UnsafeCell>; N], +// } + +// impl Pool { +// const VALUE: AtomicBool = AtomicBool::new(false); +// const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); + +// const fn new() -> Self { +// Self { +// used: [Self::VALUE; N], +// data: [Self::UNINIT; N], +// } +// } +// } + +// impl Pool { +// fn alloc(&self) -> Option> { +// for n in 0..N { +// if self.used[n].swap(true, Ordering::SeqCst) == false { +// let p = self.data[n].get() as *mut T; +// return Some(unsafe { NonNull::new_unchecked(p) }); +// } +// } +// None +// } + +// /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. +// unsafe fn free(&self, p: NonNull) { +// let origin = self.data.as_ptr() as *mut T; +// let n = p.as_ptr().offset_from(origin); +// assert!(n >= 0); +// assert!((n as usize) < N); +// self.used[n as usize].store(false, Ordering::SeqCst); +// } +// } +// } diff --git a/ublox-short-range/src/client.rs b/ublox-short-range/src/blocking/client.rs similarity index 92% rename from ublox-short-range/src/client.rs rename to ublox-short-range/src/blocking/client.rs index 3b1ec47..f80d704 100644 --- a/ublox-short-range/src/client.rs +++ b/ublox-short-range/src/blocking/client.rs @@ -1,6 +1,7 @@ use core::str::FromStr; use crate::{ + blocking::timer::Timer, command::{ data_mode::{ types::{IPProtocol, PeerConfigParameter}, @@ -30,9 +31,9 @@ use crate::{ }, }; use defmt::{debug, error, trace}; +use embassy_time::Duration; use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, IpAddr, Ipv4Addr, SocketAddr}; -use fugit::ExtU32; +use embedded_nal::{IpAddr, Ipv4Addr, SocketAddr}; use ublox_sockets::{ udp_listener::UdpListener, AnySocket, SocketHandle, SocketSet, SocketType, TcpSocket, TcpState, UdpSocket, UdpState, @@ -61,8 +62,8 @@ pub struct SecurityCredentials { /// Creates new socket numbers /// Properly not Async safe -pub fn new_socket_num<'a, const TIMER_HZ: u32, const N: usize, const L: usize>( - sockets: &'a SocketSet, +pub fn new_socket_num<'a, const N: usize, const L: usize>( + sockets: &'a SocketSet, ) -> Result { let mut num = 0; while sockets.socket_type(SocketHandle(num)).is_some() { @@ -74,10 +75,9 @@ pub fn new_socket_num<'a, const TIMER_HZ: u32, const N: usize, const L: usize>( Ok(num) } -pub struct UbloxClient +pub struct UbloxClient where C: atat::blocking::AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { pub(crate) module_started: bool, @@ -87,23 +87,20 @@ where pub(crate) wifi_config_active_on_startup: Option, pub(crate) client: C, pub(crate) config: Config, - pub(crate) sockets: Option<&'static mut SocketSet>, + pub(crate) sockets: Option<&'static mut SocketSet>, pub(crate) dns_state: DNSState, pub(crate) urc_attempts: u8, pub(crate) security_credentials: SecurityCredentials, - pub(crate) timer: CLK, pub(crate) socket_map: SocketMap, pub(crate) udp_listener: UdpListener<4, N>, } -impl - UbloxClient +impl UbloxClient where C: atat::blocking::AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { - pub fn new(client: C, timer: CLK, config: Config) -> Self { + pub fn new(client: C, config: Config) -> Self { UbloxClient { module_started: false, initialized: false, @@ -116,18 +113,17 @@ where dns_state: DNSState::NotResolving, urc_attempts: 0, security_credentials: SecurityCredentials::default(), - timer, socket_map: SocketMap::default(), udp_listener: UdpListener::new(), } } - pub fn set_socket_storage(&mut self, socket_set: &'static mut SocketSet) { + pub fn set_socket_storage(&mut self, socket_set: &'static mut SocketSet) { socket_set.prune(); self.sockets.replace(socket_set); } - pub fn take_socket_storage(&mut self) -> Option<&'static mut SocketSet> { + pub fn take_socket_storage(&mut self) -> Option<&'static mut SocketSet> { self.sockets.take() } @@ -150,8 +146,7 @@ where while self.serial_mode != SerialMode::ExtendedData { self.send_internal(&SwitchToEdmCommand, true).ok(); - self.timer.start(100.millis()).map_err(|_| Error::Timer)?; - nb::block!(self.timer.wait()).map_err(|_| Error::Timer)?; + Timer::after(Duration::from_millis(100)).wait(); while self.handle_urc()? {} } @@ -190,8 +185,7 @@ where while self.serial_mode != SerialMode::ExtendedData { self.send_internal(&SwitchToEdmCommand, true).ok(); - self.timer.start(100.millis()).map_err(|_| Error::Timer)?; - nb::block!(self.timer.wait()).map_err(|_| Error::Timer)?; + Timer::after(Duration::from_millis(100)).wait(); while self.handle_urc()? {} } @@ -264,24 +258,18 @@ where if let Some(ref mut pin) = self.config.rst_pin { defmt::warn!("Hard resetting Ublox Short Range"); pin.set_low().ok(); - self.timer.start(50.millis()).map_err(|_| Error::Timer)?; - nb::block!(self.timer.wait()).map_err(|_| Error::Timer)?; - + Timer::after(Duration::from_millis(50)).wait(); pin.set_high().ok(); - self.timer.start(4.secs()).map_err(|_| Error::Timer)?; - loop { - match self.timer.wait() { - Ok(()) => return Err(Error::_Unknown), - Err(nb::Error::WouldBlock) => { - self.handle_urc().ok(); - if self.module_started { - break; - } - } - Err(_) => return Err(Error::Timer), + Timer::with_timeout(Duration::from_secs(4), || { + self.handle_urc().ok(); + if self.module_started { + Some(Ok::<(), ()>(())) + } else { + None } - } + }) + .map_err(|_| Error::Timeout)?; } Ok(()) } @@ -302,33 +290,28 @@ where self.send_internal(&EdmAtCmdWrapper(RebootDCE), false)?; self.clear_buffers()?; - self.timer.start(4.secs()).map_err(|_| Error::Timer)?; - loop { - match self.timer.wait() { - Ok(()) => return Err(Error::_Unknown), - Err(nb::Error::WouldBlock) => { - self.handle_urc().ok(); - if self.module_started { - break; - } - } - Err(_) => return Err(Error::Timer), + Timer::with_timeout(Duration::from_secs(4), || { + self.handle_urc().ok(); + if self.module_started { + Some(Ok::<(), ()>(())) + } else { + None } - } + }) + .map_err(|_| Error::Timeout)?; Ok(()) } pub(crate) fn clear_buffers(&mut self) -> Result<(), Error> { // self.client.reset(); deprecated - + if let Some(ref mut sockets) = self.sockets.as_deref_mut() { sockets.prune(); } // Allow ATAT some time to clear the buffers - self.timer.start(300.millis()).map_err(|_| Error::Timer)?; - nb::block!(self.timer.wait()).map_err(|_| Error::Timer)?; + Timer::after(Duration::from_millis(300)).wait(); Ok(()) } @@ -377,7 +360,6 @@ where let socket_map = &mut self.socket_map; let udp_listener = &mut self.udp_listener; let wifi_connection = &mut self.wifi_connection; - let ts = self.timer.now(); let mut a = self.urc_attempts; let max = self.config.max_urc_attempts; @@ -422,7 +404,7 @@ where // Check if there are any sockets closed by remote, and close it // if it has exceeded its timeout, in order to recycle it. // TODO Is this correct? - if !sockets.recycle(self.timer.now()) { + if !sockets.recycle() { handled = false; } } @@ -474,7 +456,7 @@ where } IPProtocol::UDP => { // if let Ok(mut udp) = - // sockets.get::>(event.handle) + // sockets.get::>(event.handle) // { // debug!( // "Binding remote {=[u8]:a} to UDP socket {:?}", @@ -500,14 +482,14 @@ where match sockets.socket_type(*handle) { Some(SocketType::Tcp) => { if let Ok(mut tcp) = - sockets.get::>(*handle) + sockets.get::>(*handle) { - tcp.closed_by_remote(ts); + tcp.closed_by_remote(); } } Some(SocketType::Udp) => { if let Ok(mut udp) = - sockets.get::>(*handle) + sockets.get::>(*handle) { udp.close(); } @@ -758,7 +740,7 @@ where Some(SocketType::Tcp) => { // Handle tcp socket let mut tcp = sockets - .get::>(*socket_handle) + .get::>(*socket_handle) .unwrap(); if tcp.can_recv() { tcp.rx_enqueue_slice(&event.data); @@ -770,7 +752,7 @@ where Some(SocketType::Udp) => { // Handle udp socket let mut udp = sockets - .get::>(*socket_handle) + .get::>(*socket_handle) .unwrap(); if udp.can_recv() { diff --git a/ublox-short-range/src/wifi/dns.rs b/ublox-short-range/src/blocking/dns.rs similarity index 50% rename from ublox-short-range/src/wifi/dns.rs rename to ublox-short-range/src/blocking/dns.rs index b75efd8..2ee95d8 100644 --- a/ublox-short-range/src/wifi/dns.rs +++ b/ublox-short-range/src/blocking/dns.rs @@ -1,17 +1,16 @@ -use crate::client::DNSState; +use super::client::{DNSState, UbloxClient}; +use super::timer; +use embassy_time::Duration; use embedded_hal::digital::OutputPin; use embedded_nal::{nb, AddrType, Dns, IpAddr}; -use fugit::ExtU32; use heapless::String; - -use crate::{command::ping::*, UbloxClient}; use ublox_sockets::Error; -impl Dns - for UbloxClient +use crate::{blocking::timer::Timer, command::ping::*}; + +impl Dns for UbloxClient where C: atat::blocking::AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { type Error = Error; @@ -31,21 +30,23 @@ where retry_num: 1, }) .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - self.dns_state = DNSState::Resolving; - - let expiration = self.timer.now() + 8.secs(); - while self.dns_state == DNSState::Resolving { - self.spin().map_err(|_| nb::Error::Other(Error::Illegal))?; + self.dns_state = DNSState::Resolving; - if self.timer.now() >= expiration { - return Err(nb::Error::Other(Error::Timeout)); + match Timer::with_timeout(Duration::from_secs(8), || { + if self.spin().is_err() { + return Some(Err(Error::Illegal)); } - } - match self.dns_state { - DNSState::Resolved(ip) => Ok(ip), - _ => Err(nb::Error::Other(Error::Illegal)), + match self.dns_state { + DNSState::Resolving => None, + DNSState::Resolved(ip) => Some(Ok(ip)), + _ => Some(Err(Error::Illegal)), + } + }) { + Ok(ip) => Ok(ip), + Err(timer::Error::Timeout) => Err(nb::Error::Other(Error::Timeout)), + Err(timer::Error::Other(e)) => Err(nb::Error::Other(e)), } } } diff --git a/ublox-short-range/src/blocking/mod.rs b/ublox-short-range/src/blocking/mod.rs new file mode 100644 index 0000000..208e4a5 --- /dev/null +++ b/ublox-short-range/src/blocking/mod.rs @@ -0,0 +1,12 @@ +pub(crate) mod client; +mod dns; +pub mod timer; +pub mod tls; + +#[cfg(feature = "socket-udp")] +pub mod udp_stack; + +#[cfg(feature = "socket-tcp")] +pub mod tcp_stack; + +pub use client::UbloxClient; diff --git a/ublox-short-range/src/wifi/tcp_stack.rs b/ublox-short-range/src/blocking/tcp_stack.rs similarity index 83% rename from ublox-short-range/src/wifi/tcp_stack.rs rename to ublox-short-range/src/blocking/tcp_stack.rs index 1c8f7f1..6de3f59 100644 --- a/ublox-short-range/src/wifi/tcp_stack.rs +++ b/ublox-short-range/src/blocking/tcp_stack.rs @@ -1,9 +1,8 @@ +use super::{client::new_socket_num, UbloxClient}; use crate::{ - client::new_socket_num, command::data_mode::*, command::edm::{EdmAtCmdWrapper, EdmDataCommand}, wifi::peer_builder::PeerUrlBuilder, - UbloxClient, }; use embedded_hal::digital::OutputPin; /// Handles receiving data from sockets @@ -12,13 +11,11 @@ use embedded_nal::{nb, SocketAddr, TcpClientStack}; use ublox_sockets::{Error, SocketHandle, TcpSocket, TcpState}; -use super::EGRESS_CHUNK_SIZE; +use crate::wifi::EGRESS_CHUNK_SIZE; -impl TcpClientStack - for UbloxClient +impl TcpClientStack for UbloxClient where C: atat::blocking::AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { type Error = Error; @@ -35,7 +32,7 @@ where if sockets.len() >= sockets.capacity() { // Check if there are any sockets closed by remote, and close it // if it has exceeded its timeout, in order to recycle it. - if sockets.recycle(self.timer.now()) { + if sockets.recycle() { return Err(Error::SocketSetFull); } } @@ -76,7 +73,7 @@ where .sockets .as_mut() .unwrap() - .get::>(*socket) + .get::>(*socket) .map_err(Self::Error::from)?; tcp.set_state(TcpState::WaitingForConnect(remote)); @@ -94,7 +91,7 @@ where .sockets .as_mut() .unwrap() - .get::>(*socket) + .get::>(*socket) .map_err(Self::Error::from)?; tcp.set_state(TcpState::Created); return Err(nb::Error::Other(e)); @@ -110,7 +107,7 @@ where self.sockets .as_mut() .unwrap() - .get::>(*socket) + .get::>(*socket) .map_err(Self::Error::from)? .state(), TcpState::WaitingForConnect(_) @@ -125,7 +122,7 @@ where fn is_connected(&mut self, socket: &Self::TcpSocket) -> Result { self.connected_to_network().map_err(|_| Error::Illegal)?; if let Some(ref mut sockets) = self.sockets { - let tcp = sockets.get::>(*socket)?; + let tcp = sockets.get::>(*socket)?; Ok(tcp.is_connected()) } else { Err(Error::Illegal) @@ -142,7 +139,7 @@ where self.connected_to_network().map_err(|_| Error::Illegal)?; if let Some(ref mut sockets) = self.sockets { let tcp = sockets - .get::>(*socket) + .get::>(*socket) .map_err(|e| nb::Error::Other(e.into()))?; if !tcp.is_connected() { @@ -179,10 +176,10 @@ where self.spin().map_err(|_| nb::Error::Other(Error::Illegal))?; if let Some(ref mut sockets) = self.sockets { // Enable detecting closed socket from receive function - sockets.recycle(self.timer.now()); + sockets.recycle(); let mut tcp = sockets - .get::>(*socket) + .get::>(*socket) .map_err(Self::Error::from)?; Ok(tcp.recv_slice(buffer).map_err(Self::Error::from)?) @@ -196,7 +193,7 @@ where if let Some(ref mut sockets) = self.sockets { defmt::debug!("[TCP] Closing socket: {:?}", socket); // If the socket is not found it is already removed - if let Ok(ref tcp) = sockets.get::>(socket) { + if let Ok(ref tcp) = sockets.get::>(socket) { // If socket is not closed that means a connection excists which has to be closed if !matches!( tcp.state(), @@ -228,26 +225,3 @@ where } } } - -// impl TcpFullStack for UbloxClient -// where -// C: atat::blocking::AtatClient, -// CLK: fugit_timer::Timer, -// RST: OutputPin, -// Generic: TryInto, -// { -// fn bind(&mut self, socket: &mut Self::TcpSocket, local_port: u16) -> Result<(), Self::Error> { -// todo!() -// } - -// fn listen(&mut self, socket: &mut Self::TcpSocket) -> Result<(), Self::Error> { -// todo!() -// } - -// fn accept( -// &mut self, -// socket: &mut Self::TcpSocket, -// ) -> nb::Result<(Self::TcpSocket, SocketAddr), Self::Error> { -// todo!() -// } -// } diff --git a/ublox-short-range/src/blocking/timer.rs b/ublox-short-range/src/blocking/timer.rs new file mode 100644 index 0000000..7ddae99 --- /dev/null +++ b/ublox-short-range/src/blocking/timer.rs @@ -0,0 +1,42 @@ +use embassy_time::{Duration, Instant}; + +pub struct Timer { + expires_at: Instant, +} + +pub enum Error { + Timeout, + Other(E), +} + +impl Timer { + pub fn after(duration: Duration) -> Self { + Self { + expires_at: Instant::now() + duration, + } + } + + pub fn with_timeout(timeout: Duration, mut e: F) -> Result> + where + F: FnMut() -> Option>, + { + let timer = Timer::after(timeout); + + loop { + if let Some(res) = e() { + return res.map_err(Error::Other); + } + if timer.expires_at <= Instant::now() { + return Err(Error::Timeout); + } + } + } + + pub fn wait(self) { + loop { + if self.expires_at <= Instant::now() { + break; + } + } + } +} diff --git a/ublox-short-range/src/wifi/tls.rs b/ublox-short-range/src/blocking/tls.rs similarity index 94% rename from ublox-short-range/src/wifi/tls.rs rename to ublox-short-range/src/blocking/tls.rs index 2d0b04d..ed5687f 100644 --- a/ublox-short-range/src/wifi/tls.rs +++ b/ublox-short-range/src/blocking/tls.rs @@ -1,8 +1,8 @@ +use super::UbloxClient; use crate::{ command::edm::BigEdmAtCmdWrapper, command::security::{types::*, *}, error::Error, - UbloxClient, }; use embedded_hal::digital::OutputPin; use heapless::String; @@ -18,11 +18,9 @@ pub trait TLS { ) -> Result<(), Error>; } -impl TLS - for UbloxClient +impl TLS for UbloxClient where C: atat::blocking::AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { /// Importing credentials enabeles their use for all further TCP connections diff --git a/ublox-short-range/src/wifi/udp_stack.rs b/ublox-short-range/src/blocking/udp_stack.rs similarity index 92% rename from ublox-short-range/src/wifi/udp_stack.rs rename to ublox-short-range/src/blocking/udp_stack.rs index 1c1a6da..203bada 100644 --- a/ublox-short-range/src/wifi/udp_stack.rs +++ b/ublox-short-range/src/blocking/udp_stack.rs @@ -1,12 +1,12 @@ +use super::client::new_socket_num; +use super::UbloxClient; use crate::{ - client::new_socket_num, command::data_mode::*, command::{ data_mode::types::{IPVersion, ServerType, UDPBehaviour}, edm::{EdmAtCmdWrapper, EdmDataCommand}, }, wifi::peer_builder::PeerUrlBuilder, - UbloxClient, }; use embedded_hal::digital::OutputPin; use embedded_nal::{nb, SocketAddr, UdpFullStack}; @@ -14,13 +14,11 @@ use embedded_nal::{nb, SocketAddr, UdpFullStack}; use embedded_nal::UdpClientStack; use ublox_sockets::{Error, SocketHandle, UdpSocket, UdpState}; -use super::EGRESS_CHUNK_SIZE; +use crate::wifi::EGRESS_CHUNK_SIZE; -impl UdpClientStack - for UbloxClient +impl UdpClientStack for UbloxClient where C: atat::blocking::AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { type Error = Error; @@ -36,7 +34,7 @@ where if sockets.len() >= sockets.capacity() { // Check if there are any sockets closed by remote, and close it // if it has exceeded its timeout, in order to recycle it. - if sockets.recycle(self.timer.now()) { + if sockets.recycle() { return Err(Error::SocketSetFull); } } @@ -77,7 +75,7 @@ where .sockets .as_mut() .unwrap() - .get::>(*socket)?; + .get::>(*socket)?; udp.bind(remote)?; // Then connect modem @@ -95,7 +93,7 @@ where .sockets .as_mut() .unwrap() - .get::>(*socket)?; + .get::>(*socket)?; udp.close(); return Err(e); } @@ -104,7 +102,7 @@ where .sockets .as_mut() .unwrap() - .get::>(*socket)? + .get::>(*socket)? .state() == UdpState::Closed { @@ -123,7 +121,7 @@ where } let udp = sockets - .get::>(*socket) + .get::>(*socket) .map_err(Self::Error::from)?; if !udp.is_open() { @@ -175,7 +173,7 @@ where if let Some(ref mut sockets) = self.sockets { let mut udp = sockets - .get::>(*connection_handle) + .get::>(*connection_handle) .map_err(|_| Self::Error::InvalidSocket)?; let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; @@ -187,7 +185,7 @@ where // Handle reciving for udp normal sockets } else if let Some(ref mut sockets) = self.sockets { let mut udp = sockets - .get::>(*socket) + .get::>(*socket) .map_err(Self::Error::from)?; let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; @@ -255,14 +253,14 @@ where } else if let Some(ref mut sockets) = self.sockets { defmt::debug!("[UDP] Closing socket: {:?}", socket); // If no sockets exists, nothing to close. - if let Ok(ref mut udp) = sockets.get::>(socket) { + if let Ok(ref mut udp) = sockets.get::>(socket) { defmt::trace!("[UDP] Closing socket state: {:?}", udp.state()); match udp.state() { UdpState::Closed => { sockets.remove(socket).ok(); } UdpState::Established => { - udp.close(); + // FIXME:udp.close(); if let Some(peer_handle) = self.socket_map.socket_to_peer(&udp.handle()) { let peer_handle = *peer_handle; self.send_at(ClosePeerConnection { peer_handle }) @@ -293,11 +291,9 @@ where /// - The driver has to call send_to after reciving data, to release the socket bound by remote host, /// even if just sending no bytes. Else these sockets will be held open until closure of server socket. /// -impl UdpFullStack - for UbloxClient +impl UdpFullStack for UbloxClient where C: atat::blocking::AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { fn bind(&mut self, socket: &mut Self::UdpSocket, local_port: u16) -> Result<(), Self::Error> { @@ -352,7 +348,7 @@ where } let udp = sockets - .get::>(connection_socket) + .get::>(connection_socket) .map_err(Self::Error::from)?; if !udp.is_open() { diff --git a/ublox-short-range/src/command/custom_digest.rs b/ublox-short-range/src/command/custom_digest.rs index 8135168..cf6a4c4 100644 --- a/ublox-short-range/src/command/custom_digest.rs +++ b/ublox-short-range/src/command/custom_digest.rs @@ -101,345 +101,332 @@ impl Digester for EdmDigester { } } -#[cfg(test)] -mod test { - - use super::*; - use atat::bbqueue::framed::FrameConsumer; - use atat::{frame::Frame, AtatIngress, Buffers, Ingress, Response}; - - const TEST_RX_BUF_LEN: usize = 256; - const TEST_RES_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; - const TEST_URC_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; - - // macro_rules! setup_ingressmanager { - // () => {{ - // static mut RES_Q: BBBuffer = BBBuffer::new(); - // let (res_p, res_c) = unsafe { RES_Q.try_split_framed().unwrap() }; - - // static mut URC_Q: BBBuffer = BBBuffer::new(); - // let (urc_p, urc_c) = unsafe { URC_Q.try_split_framed().unwrap() }; - - // ( - // IngressManager::<_, TEST_RX_BUF_LEN, TEST_RES_CAPACITY, TEST_URC_CAPACITY>::new( - // res_p, - // urc_p, - // EdmDigester::default(), - // ), - // res_c, - // urc_c, - // ) - // }}; - // } - - /// Removed functionality used to change OK responses to empty responses. - #[test] - fn ok_response<'a>() { - let mut at_pars: Ingress< - 'a, - EdmDigester, - TEST_RX_BUF_LEN, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - >; - let mut res_c: FrameConsumer<'a, TEST_RES_CAPACITY>; - let mut urc_c: FrameConsumer<'a, TEST_URC_CAPACITY>; - let buf = Buffers::::new(); - (at_pars, res_c, urc_c) = buf.to_ingress(EdmDigester::default()); - - // Payload: "OK\r\n" - let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; - let empty_ok_response = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; - - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - let frame = Frame::decode(grant.as_ref()); - let res = match Response::from(frame) { - Response::Result(r) => r, - Response::Prompt(_) => Ok(&[][..]), - }; - - assert_eq!(res, Ok(&empty_ok_response[..])); - assert_eq!(urc_c.read(), None); - } - - #[test] - fn error_response() { - let mut at_pars: Ingress< - 'static, - EdmDigester, - TEST_RX_BUF_LEN, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - >; - let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; - let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; - (at_pars, res_c, urc_c) = - Buffers::::new() - .to_ingress(EdmDigester::default()); - - // Payload: "ERROR\r\n" - let data = &[ - 0xAA, 0x00, 0x09, 0x00, 0x45, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x0D, 0x0a, 0x55, - ]; - - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - let frame = Frame::decode(grant.as_ref()); - let res = match Response::from(frame) { - Response::Result(r) => r, - Response::Prompt(_) => Ok(&[][..]), - }; - assert_eq!(res, Err(InternalError::InvalidResponse)); - assert_eq!(urc_c.read(), None); - } - - #[test] - fn regular_response_with_trailing_ok() { - let mut at_pars: Ingress< - 'static, - EdmDigester, - TEST_RX_BUF_LEN, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - >; - let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; - let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; - (at_pars, res_c, urc_c) = - Buffers::::new() - .to_ingress(EdmDigester::default()); - - // Payload: AT\r\n - let response = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x41, 0x54, 0x0D, 0x0a, 0x55]; - // Data = response + trailing OK message - let data = &[ - 0xAA, 0x00, 0x06, 0x00, 0x45, 0x41, 0x54, 0x0D, 0x0a, 0x55, 0xAA, 0x00, 0x06, 0x00, - 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, - ]; - - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - let frame = Frame::decode(grant.as_ref()); - let res = match Response::from(frame) { - Response::Result(r) => r, - Response::Prompt(_) => Ok(&[][..]), - }; - assert_eq!(res, Ok(&response[..])); - assert_eq!(urc_c.read(), None); - } - - /// Regular response with traling regular response.. - #[test] - fn at_urc() { - let mut at_pars: Ingress< - 'static, - EdmDigester, - TEST_RX_BUF_LEN, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - >; - let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; - let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; - (at_pars, res_c, urc_c) = - Buffers::::new() - .to_ingress(EdmDigester::default()); - - let type_byte = PayloadType::ATEvent as u8; - // Payload: "OK\r\n" - let data = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let result = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - assert_eq!(grant.as_ref(), result); - assert_eq!(res_c.read(), None); - } - - #[test] - fn data_event() { - let mut at_pars: Ingress< - 'static, - EdmDigester, - TEST_RX_BUF_LEN, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - >; - let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; - let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; - (at_pars, res_c, urc_c) = - Buffers::::new() - .to_ingress(EdmDigester::default()); - - let type_byte = PayloadType::DataEvent as u8; - // Payload: "OK\r\n" - let data = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let result = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - - assert_eq!(grant.as_ref(), result); - assert_eq!(res_c.read(), None); - } - - #[test] - fn connect_disconnect_events() { - let mut at_pars: Ingress< - 'static, - EdmDigester, - TEST_RX_BUF_LEN, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - >; - let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; - let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; - (at_pars, res_c, urc_c) = - Buffers::::new() - .to_ingress(EdmDigester::default()); - - let type_byte = PayloadType::ConnectEvent as u8; - // Payload: "OK\r\n" - let data = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let result = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - assert_eq!(grant.as_ref(), result); - assert_eq!(res_c.read(), None); - drop(grant); - - let type_byte = PayloadType::DisconnectEvent as u8; - // Payload: "OK\r\n" - let data = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let result = &[ - 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, - 0x3A, 0x33, 0x0D, 0x0A, 0x55, - ]; - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - - let mut grant = urc_c.read().unwrap(); - grant.auto_release(true); - assert_eq!(grant.as_ref(), result); - assert_eq!(res_c.read(), None); - } - - #[test] - fn wrong_type_packet() { - let mut at_pars: Ingress< - 'static, - EdmDigester, - TEST_RX_BUF_LEN, - TEST_RES_CAPACITY, - TEST_URC_CAPACITY, - >; - let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; - let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; - (at_pars, res_c, urc_c) = - Buffers::::new() - .to_ingress(EdmDigester::default()); - - let type_byte = PayloadType::Unknown as u8; - // Payload: "OK\r\n" - let data = &[ - 0xAA, 0x00, 0x06, 0x00, type_byte, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, - ]; - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - - assert_eq!(urc_c.read(), None); - assert_eq!(res_c.read(), None); - - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - assert_eq!(urc_c.read(), None); - assert_eq!(res_c.read(), None); - - // Recovered enough to receive normal data? - // Payload: "OK\r\n" - let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; - let empty_ok_response = heapless::Vec::::from_slice(&[ - 0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, - ]) - .unwrap(); - - let ingress_buf = at_pars.write_buf(); - let len = usize::min(data.len(), ingress_buf.len()); - ingress_buf[..len].copy_from_slice(&data[..len]); - at_pars.try_advance(len); - - let mut grant = res_c.read().unwrap(); - grant.auto_release(true); - - let frame = Frame::decode(grant.as_ref()); - let res = match Response::from(frame) { - Response::Result(r) => r, - Response::Prompt(_) => Ok(&[][..]), - }; - assert_eq!(res, Ok(&empty_ok_response[..])); - assert_eq!(urc_c.read(), None); - } -} +// #[cfg(test)] +// mod test { +// use super::*; +// use atat::Config; +// use atat::{AtatIngress, Buffers, Response, blocking::AtatClient}; + +// const TEST_RX_BUF_LEN: usize = 256; +// const TEST_RES_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; +// const TEST_URC_CAPACITY: usize = 3 * TEST_RX_BUF_LEN; + +// struct MockWriter; + +// impl embedded_io::Io for MockWriter { +// type Error = (); +// } + +// impl embedded_io::blocking::Write for MockWriter { +// fn write(&mut self, buf: &[u8]) -> Result { +// Ok(buf.len()) +// } + +// fn flush(&mut self) -> Result<(), Self::Error> { +// Ok(()) +// } +// } + +// /// Removed functionality used to change OK responses to empty responses. +// #[test] +// fn ok_response<'a>() { +// let buf = Buffers::::new(); +// (at_pars, client) = buf.split_blocking(MockWriter, EdmDigester::default(), Config::new()); + +// // Payload: "OK\r\n" +// let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; +// let empty_ok_response = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; + +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len).unwrap(); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); +// let frame = Frame::decode(grant.as_ref()); +// let res = match Response::from(frame) { +// Response::Result(r) => r, +// Response::Prompt(_) => Ok(&[][..]), +// }; + +// assert_eq!(res, Ok(&empty_ok_response[..])); +// assert_eq!(urc_c.read(), None); +// } +// } + +// #[test] +// fn error_response() { +// let mut at_pars: Ingress< +// 'static, +// EdmDigester, +// TEST_RX_BUF_LEN, +// TEST_RES_CAPACITY, +// TEST_URC_CAPACITY, +// >; +// let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; +// let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; +// (at_pars, res_c, urc_c) = +// Buffers::::new() +// .to_ingress(EdmDigester::default()); + +// // Payload: "ERROR\r\n" +// let data = &[ +// 0xAA, 0x00, 0x09, 0x00, 0x45, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x0D, 0x0a, 0x55, +// ]; + +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); +// let frame = Frame::decode(grant.as_ref()); +// let res = match Response::from(frame) { +// Response::Result(r) => r, +// Response::Prompt(_) => Ok(&[][..]), +// }; +// assert_eq!(res, Err(InternalError::InvalidResponse)); +// assert_eq!(urc_c.read(), None); +// } + +// #[test] +// fn regular_response_with_trailing_ok() { +// let mut at_pars: Ingress< +// 'static, +// EdmDigester, +// TEST_RX_BUF_LEN, +// TEST_RES_CAPACITY, +// TEST_URC_CAPACITY, +// >; +// let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; +// let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; +// (at_pars, res_c, urc_c) = +// Buffers::::new() +// .to_ingress(EdmDigester::default()); + +// // Payload: AT\r\n +// let response = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x41, 0x54, 0x0D, 0x0a, 0x55]; +// // Data = response + trailing OK message +// let data = &[ +// 0xAA, 0x00, 0x06, 0x00, 0x45, 0x41, 0x54, 0x0D, 0x0a, 0x55, 0xAA, 0x00, 0x06, 0x00, +// 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, +// ]; + +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); +// let frame = Frame::decode(grant.as_ref()); +// let res = match Response::from(frame) { +// Response::Result(r) => r, +// Response::Prompt(_) => Ok(&[][..]), +// }; +// assert_eq!(res, Ok(&response[..])); +// assert_eq!(urc_c.read(), None); +// } + +// /// Regular response with traling regular response.. +// #[test] +// fn at_urc() { +// let mut at_pars: Ingress< +// 'static, +// EdmDigester, +// TEST_RX_BUF_LEN, +// TEST_RES_CAPACITY, +// TEST_URC_CAPACITY, +// >; +// let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; +// let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; +// (at_pars, res_c, urc_c) = +// Buffers::::new() +// .to_ingress(EdmDigester::default()); + +// let type_byte = PayloadType::ATEvent as u8; +// // Payload: "OK\r\n" +// let data = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let result = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); +// assert_eq!(grant.as_ref(), result); +// assert_eq!(res_c.read(), None); +// } + +// #[test] +// fn data_event() { +// let mut at_pars: Ingress< +// 'static, +// EdmDigester, +// TEST_RX_BUF_LEN, +// TEST_RES_CAPACITY, +// TEST_URC_CAPACITY, +// >; +// let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; +// let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; +// (at_pars, res_c, urc_c) = +// Buffers::::new() +// .to_ingress(EdmDigester::default()); + +// let type_byte = PayloadType::DataEvent as u8; +// // Payload: "OK\r\n" +// let data = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let result = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); + +// assert_eq!(grant.as_ref(), result); +// assert_eq!(res_c.read(), None); +// } + +// #[test] +// fn connect_disconnect_events() { +// let mut at_pars: Ingress< +// 'static, +// EdmDigester, +// TEST_RX_BUF_LEN, +// TEST_RES_CAPACITY, +// TEST_URC_CAPACITY, +// >; +// let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; +// let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; +// (at_pars, res_c, urc_c) = +// Buffers::::new() +// .to_ingress(EdmDigester::default()); + +// let type_byte = PayloadType::ConnectEvent as u8; +// // Payload: "OK\r\n" +// let data = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let result = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); +// assert_eq!(grant.as_ref(), result); +// assert_eq!(res_c.read(), None); +// drop(grant); + +// let type_byte = PayloadType::DisconnectEvent as u8; +// // Payload: "OK\r\n" +// let data = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let result = &[ +// 0xAA, 0x00, 0x0E, 0x00, type_byte, 0x0D, 0x0A, 0x2B, 0x55, 0x55, 0x44, 0x50, 0x44, +// 0x3A, 0x33, 0x0D, 0x0A, 0x55, +// ]; +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); + +// let mut grant = urc_c.read().unwrap(); +// grant.auto_release(true); +// assert_eq!(grant.as_ref(), result); +// assert_eq!(res_c.read(), None); +// } + +// #[test] +// fn wrong_type_packet() { +// let mut at_pars: Ingress< +// 'static, +// EdmDigester, +// TEST_RX_BUF_LEN, +// TEST_RES_CAPACITY, +// TEST_URC_CAPACITY, +// >; +// let mut res_c: FrameConsumer<'static, TEST_RES_CAPACITY>; +// let mut urc_c: FrameConsumer<'static, TEST_URC_CAPACITY>; +// (at_pars, res_c, urc_c) = +// Buffers::::new() +// .to_ingress(EdmDigester::default()); + +// let type_byte = PayloadType::Unknown as u8; +// // Payload: "OK\r\n" +// let data = &[ +// 0xAA, 0x00, 0x06, 0x00, type_byte, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, +// ]; +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); + +// assert_eq!(urc_c.read(), None); +// assert_eq!(res_c.read(), None); + +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); +// assert_eq!(urc_c.read(), None); +// assert_eq!(res_c.read(), None); + +// // Recovered enough to receive normal data? +// // Payload: "OK\r\n" +// let data = &[0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55]; +// let empty_ok_response = heapless::Vec::::from_slice(&[ +// 0xAA, 0x00, 0x06, 0x00, 0x45, 0x4f, 0x4b, 0x0D, 0x0a, 0x55, +// ]) +// .unwrap(); + +// let ingress_buf = at_pars.write_buf(); +// let len = usize::min(data.len(), ingress_buf.len()); +// ingress_buf[..len].copy_from_slice(&data[..len]); +// at_pars.try_advance(len); + +// let mut grant = res_c.read().unwrap(); +// grant.auto_release(true); + +// let frame = Frame::decode(grant.as_ref()); +// let res = match Response::from(frame) { +// Response::Result(r) => r, +// Response::Prompt(_) => Ok(&[][..]), +// }; +// assert_eq!(res, Ok(&empty_ok_response[..])); +// assert_eq!(urc_c.read(), None); +// } +// } diff --git a/ublox-short-range/src/error.rs b/ublox-short-range/src/error.rs index 2f2aaa9..5fe038c 100644 --- a/ublox-short-range/src/error.rs +++ b/ublox-short-range/src/error.rs @@ -27,7 +27,7 @@ pub enum Error { SocketMemory, SocketMapMemory, Supplicant, - Timer, + Timeout, ShadowStoreBug, _Unknown, } diff --git a/ublox-short-range/src/lib.rs b/ublox-short-range/src/lib.rs index b8305db..e641989 100644 --- a/ublox-short-range/src/lib.rs +++ b/ublox-short-range/src/lib.rs @@ -1,10 +1,15 @@ -#![cfg_attr(not(test), no_std)] +#![cfg_attr(all(not(test), not(feature = "std")), no_std)] +#![cfg_attr(feature = "async", allow(incomplete_features))] +#![cfg_attr(feature = "async", feature(generic_const_exprs))] +#![cfg_attr(feature = "async", feature(async_fn_in_trait))] -mod client; +#[cfg(feature = "async")] +mod asynch; + +mod blocking; mod hex; pub use atat; -pub use client::UbloxClient; pub mod command; pub mod config; @@ -15,4 +20,4 @@ pub mod wifi; mod test_helper; #[cfg(any(feature = "socket-udp", feature = "socket-tcp"))] -pub use wifi::tls::TLS; +pub use blocking::tls::TLS; diff --git a/ublox-short-range/src/wifi/ap.rs b/ublox-short-range/src/wifi/ap.rs index 54b0184..5995140 100644 --- a/ublox-short-range/src/wifi/ap.rs +++ b/ublox-short-range/src/wifi/ap.rs @@ -1,5 +1,5 @@ use crate::{ - client::UbloxClient, + blocking::UbloxClient, command::{ edm::EdmAtCmdWrapper, wifi::{ @@ -23,11 +23,9 @@ use embedded_hal::digital::OutputPin; use super::connection::{WiFiState, WifiConnection}; -impl - UbloxClient +impl UbloxClient where C: AtatClient, - CLK: fugit_timer::Timer, RST: OutputPin, { /// Creates wireless hotspot service for host machine. diff --git a/ublox-short-range/src/wifi/mod.rs b/ublox-short-range/src/wifi/mod.rs index 7407c7f..33df278 100644 --- a/ublox-short-range/src/wifi/mod.rs +++ b/ublox-short-range/src/wifi/mod.rs @@ -5,20 +5,12 @@ use crate::command::edm::types::ChannelId; pub mod ap; pub mod connection; -pub mod dns; pub mod network; pub mod options; pub mod supplicant; -pub mod tls; pub mod peer_builder; -#[cfg(feature = "socket-udp")] -pub mod udp_stack; - -#[cfg(feature = "socket-tcp")] -pub mod tcp_stack; - pub(crate) const EGRESS_CHUNK_SIZE: usize = 512; /// The socket map, keeps mappings between `ublox::sockets`s `SocketHandle`, /// and the modems `PeerHandle` and `ChannelId`. The peer handle is used diff --git a/ublox-short-range/src/wifi/peer_builder.rs b/ublox-short-range/src/wifi/peer_builder.rs index b4dfb62..f163a1b 100644 --- a/ublox-short-range/src/wifi/peer_builder.rs +++ b/ublox-short-range/src/wifi/peer_builder.rs @@ -1,4 +1,4 @@ -use crate::{client::SecurityCredentials, error::Error}; +use crate::{blocking::client::SecurityCredentials, error::Error}; use core::fmt::Write; /// Handles receiving data from sockets /// implements TCP and UDP for WiFi client diff --git a/ublox-short-range/src/wifi/supplicant.rs b/ublox-short-range/src/wifi/supplicant.rs index 9f974a5..b11e40f 100644 --- a/ublox-short-range/src/wifi/supplicant.rs +++ b/ublox-short-range/src/wifi/supplicant.rs @@ -1,4 +1,3 @@ -use embedded_nal::nb; use heapless::Vec; use crate::{ From d8b32cdcb16dd3ebb241944ecdcbc68181ed0758 Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 15 Jun 2023 08:47:06 +0200 Subject: [PATCH 03/16] Working async network management & TCP socket stack --- Cargo.toml | 20 +- examples/rpi-pico/.cargo/config.toml | 5 +- examples/rpi-pico/Cargo.toml | 10 +- examples/rpi-pico/src/bin/embassy-async.rs | 271 +++++++--- src/asynch/channel.rs | 579 +++++++++++++++++++++ src/asynch/control.rs | 371 +++++++------ src/asynch/mod.rs | 51 +- src/asynch/runner.rs | 444 ++++++++++------ src/asynch/ublox_stack/dns.rs | 72 +++ src/asynch/ublox_stack/mod.rs | 405 ++++++++++++-- src/asynch/ublox_stack/tcp.rs | 559 ++++++++++---------- src/command/data_mode/mod.rs | 3 +- src/command/data_mode/responses.rs | 2 +- src/command/data_mode/types.rs | 13 + src/command/data_mode/urc.rs | 6 +- src/command/edm/mod.rs | 1 + src/command/edm/types.rs | 21 +- src/command/edm/urc.rs | 8 +- src/command/ethernet/types.rs | 2 +- src/command/gpio/mod.rs | 2 +- src/command/mod.rs | 21 +- src/command/network/types.rs | 2 +- src/command/ping/urc.rs | 3 +- src/command/wifi/types.rs | 2 +- src/config.rs | 61 --- src/{wifi => }/connection.rs | 21 +- src/error.rs | 1 + src/lib.rs | 11 +- src/{wifi => }/network.rs | 16 + src/{wifi => }/peer_builder.rs | 100 ++-- src/wifi/mod.rs | 3 +- 31 files changed, 2160 insertions(+), 926 deletions(-) create mode 100644 src/asynch/channel.rs create mode 100644 src/asynch/ublox_stack/dns.rs delete mode 100644 src/config.rs rename src/{wifi => }/connection.rs (62%) rename src/{wifi => }/network.rs (76%) rename src/{wifi => }/peer_builder.rs (58%) diff --git a/Cargo.toml b/Cargo.toml index 6572870..4dbbaf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,21 +18,21 @@ doctest = false atat = { version = "0.18.0", features = ["derive", "defmt", "bytes"] } # atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "70283be", features = ["derive", "defmt", "bytes"] } heapless = { version = "^0.7", features = ["serde", "defmt-impl"] } -no-std-net = { version = "^0.5", features = ["serde"] } +no-std-net = { version = "0.6", features = ["serde"] } serde = { version = "^1", default-features = false, features = ["derive"] } -ublox-sockets = { version = "0.5", features = ["defmt"], optional = true } -hash32 = "^0.2.1" -hash32-derive = "^0.1.0" +ublox-sockets = { version = "0.5", features = ["defmt", "edm"], optional = true } postcard = "1.0.4" smoltcp = { version = "0.9.1", default-features = false, optional = true } +atomic-polyfill = "1.0.2" defmt = { version = "0.3" } embedded-hal = "=1.0.0-alpha.10" -embedded-nal = "0.6.0" +# embedded-nal = "0.6.0" embassy-time = "0.1" embassy-sync = "0.2" embassy-futures = "0.1" -embassy-net-driver-channel = "0.1.0" +embassy-hal-common = "0.1" +embassy-net-driver = "0.1" embedded-nal-async = { version = "0.4", optional = true } futures = { version = "0.3.17", default-features = false, features = [ "async-await" ] } @@ -40,12 +40,14 @@ futures = { version = "0.3.17", default-features = false, features = [ "async-aw embedded-io = "0.4" [features] -default = ["async", "odin_w2xx", "ublox-sockets", "socket-udp", "socket-tcp"] +default = ["async", "odin_w2xx", "ublox-sockets", "socket-tcp"] async = ["dep:embedded-nal-async", "atat/async", "ublox-sockets?/async"] std = [] +defmt = ["postcard/use-defmt"] + odin_w2xx = [] nina_w1xx = [] nina_b1xx = [] @@ -65,4 +67,6 @@ exclude = ["examples"] [patch.crates-io] atat = { path = "../atat/atat" } ublox-sockets = { path = "../ublox-sockets" } -embassy-net-driver-channel = { path = "../embassy/embassy-net-driver-channel" } \ No newline at end of file +no-std-net = { path = "../no-std-net" } +embassy-net-driver = { path = "../embassy/embassy-net-driver" } +embassy-hal-common = { path = "../embassy/embassy-hal-common" } \ No newline at end of file diff --git a/examples/rpi-pico/.cargo/config.toml b/examples/rpi-pico/.cargo/config.toml index f757b35..3217579 100644 --- a/examples/rpi-pico/.cargo/config.toml +++ b/examples/rpi-pico/.cargo/config.toml @@ -1,8 +1,9 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -runner = "probe-rs-cli run --chip RP2040" +# runner = "probe-rs-cli run --chip RP2040" +runner = "probe-run --chip RP2040" [build] target = "thumbv6m-none-eabi" [env] -DEFMT_LOG = "debug" \ No newline at end of file +DEFMT_LOG = "debug,atat=warn" \ No newline at end of file diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index ddfdcf6..78d4b1a 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -5,12 +5,14 @@ edition = "2021" [dependencies] -ublox-short-range-rs = { path = "../../", features = ["async", "odin_w2xx", "ublox-sockets", "socket-udp", "socket-tcp"] } +ublox-short-range-rs = { path = "../../", features = ["async", "defmt", "odin_w2xx", "ublox-sockets", "socket-tcp"] } embassy-executor = { version = "0.1.0", features = ["defmt", "integrated-timers", "nightly"] } embassy-time = { version = "0.1.0", features = ["defmt", "defmt-timestamp-uptime"] } embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-traits", "nightly", "unstable-pac", "time-driver"] } +embassy-futures = { version = "0.1.0" } atomic-polyfill = "1.0.2" -static_cell = "1.0" +static_cell = "1.1" +no-std-net = { version = "0.6", features = ["serde"] } defmt = "0.3.4" defmt-rtt = "0.3" @@ -34,13 +36,15 @@ heapless = "0.7.15" embassy-executor = { path = "../../../embassy/embassy-executor" } +embassy-hal-common = { path = "../../../embassy/embassy-hal-common" } embassy-time = { path = "../../../embassy/embassy-time" } embassy-futures = { path = "../../../embassy/embassy-futures" } embassy-sync = { path = "../../../embassy/embassy-sync" } embassy-rp = { path = "../../../embassy/embassy-rp" } -embassy-net-driver-channel = { path = "../../../embassy/embassy-net-driver-channel" } +embassy-net-driver = { path = "../../../embassy/embassy-net-driver" } atat = { path = "../../../atat/atat" } ublox-sockets = { path = "../../../ublox-sockets" } +no-std-net = { path = "../../../no-std-net" } [profile.dev] debug = 2 diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index 9571302..30140a5 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -4,20 +4,26 @@ #![feature(async_fn_in_trait)] #![allow(incomplete_features)] -use defmt::*; +use core::fmt::Write as _; use embassy_executor::Spawner; -use embassy_rp::gpio::{Level, Output}; +use embassy_futures::select::{select, Either}; +use embassy_rp::gpio::{Input, Level, Output, Pull}; use embassy_rp::peripherals::{PIN_26, UART1}; -use embassy_rp::{interrupt, uart}; +use embassy_rp::uart::BufferedInterruptHandler; +use embassy_rp::{bind_interrupts, uart}; use embassy_time::{Duration, Timer}; +use embedded_io::asynch::Write; +use no_std_net::{Ipv4Addr, SocketAddr}; use static_cell::StaticCell; use ublox_short_range::asynch::runner::Runner; +use ublox_short_range::asynch::ublox_stack::dns::DnsSocket; +use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; use ublox_short_range::asynch::{new, State}; use ublox_short_range::atat::{self, AtatIngress, AtatUrcChannel}; use ublox_short_range::command::custom_digest::EdmDigester; use ublox_short_range::command::edm::urc::EdmEvent; -use ublox_short_range::command::gpio::types::{GPIOId, GPIOValue}; +use ublox_short_range::embedded_nal_async::AddrType; use {defmt_rtt as _, panic_probe as _}; const RX_BUF_LEN: usize = 1024; @@ -42,6 +48,7 @@ async fn wifi_task( RX_BUF_LEN, >, Output<'static, PIN_26>, + 8, >, ) -> ! { runner.run().await @@ -52,6 +59,80 @@ async fn net_task(stack: &'static UbloxStack) -> ! { stack.run().await } +#[embassy_executor::task(pool_size = 2)] +async fn echo_task( + stack: &'static UbloxStack, + hostname: &'static str, + port: u16, + write_interval: Duration, +) { + let mut rx_buffer = [0; 128]; + let mut tx_buffer = [0; 128]; + let mut buf = [0; 128]; + let mut cnt = 0u32; + let mut msg = heapless::String::<64>::new(); + Timer::after(Duration::from_secs(1)).await; + + let ip_addr = match DnsSocket::new(stack).query(hostname, AddrType::IPv4).await { + Ok(ip) => ip, + Err(_) => { + defmt::error!("[{}] Failed to resolve IP addr", hostname); + return; + } + }; + + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + + defmt::info!( + "[{}] Connecting... {}", + hostname, + defmt::Debug2Format(&ip_addr) + ); + if let Err(e) = socket.connect((ip_addr, port)).await { + defmt::warn!("[{}] connect error: {:?}", hostname, e); + return; + } + defmt::info!( + "[{}] Connected to {:?}", + hostname, + defmt::Debug2Format(&socket.remote_endpoint()) + ); + + loop { + match select(Timer::after(write_interval), socket.read(&mut buf)).await { + Either::First(_) => { + msg.clear(); + write!(msg, "Hello {}! {}\n", ip_addr, cnt).unwrap(); + cnt = cnt.wrapping_add(1); + if let Err(e) = socket.write_all(msg.as_bytes()).await { + defmt::warn!("[{}] write error: {:?}", hostname, e); + break; + } + defmt::info!("[{}] txd: {}", hostname, msg); + Timer::after(Duration::from_millis(400)).await; + } + Either::Second(res) => { + let n = match res { + Ok(0) => { + defmt::warn!("[{}] read EOF", hostname); + break; + } + Ok(n) => n, + Err(e) => { + defmt::warn!("[{}] {:?}", hostname, e); + break; + } + }; + defmt::info!( + "[{}] rxd {}", + hostname, + core::str::from_utf8(&buf[..n]).unwrap() + ); + } + } + } +} + #[embassy_executor::task] async fn ingress_task( mut ingress: atat::Ingress<'static, EdmDigester, EdmEvent, RX_BUF_LEN, URC_CAPACITY, 1>, @@ -60,9 +141,13 @@ async fn ingress_task( ingress.read_from(&mut rx).await } +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + #[embassy_executor::main] async fn main(spawner: Spawner) { - info!("Hello World!"); + defmt::info!("Hello World!"); let p = embassy_rp::init(Default::default()); @@ -70,13 +155,13 @@ async fn main(spawner: Spawner) { let (tx_pin, rx_pin, rts_pin, cts_pin, uart) = (p.PIN_24, p.PIN_25, p.PIN_23, p.PIN_22, p.UART1); + let mut btn = Input::new(p.PIN_27, Pull::Up); - let irq = interrupt::take!(UART1_IRQ); let tx_buf = &mut singleton!([0u8; 64])[..]; let rx_buf = &mut singleton!([0u8; 64])[..]; let uart = uart::BufferedUart::new_with_rtscts( uart, - irq, + Irqs, tx_pin, rx_pin, rts_pin, @@ -88,83 +173,139 @@ async fn main(spawner: Spawner) { let (rx, tx) = uart.split(); let buffers = &*singleton!(atat::Buffers::new()); + let urc_channel = buffers.urc_channel.subscribe().unwrap(); let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); - unwrap!(spawner.spawn(ingress_task(ingress, rx))); + defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); let state = singleton!(State::new(client)); - let (net_device, mut control, runner) = - new(state, buffers.urc_channel.subscribe().unwrap(), rst).await; + let (net_device, mut control, runner) = new(state, urc_channel, rst).await; - unwrap!(spawner.spawn(wifi_task(runner))); + defmt::unwrap!(spawner.spawn(wifi_task(runner))); - // control.init(clm).await; - // control - // .set_power_management(cyw43::PowerManagementMode::PowerSave) - // .await; + control + .set_hostname("Factbird-duo-wifi-test") + .await + .unwrap(); // Init network stack let stack = &*singleton!(UbloxStack::new( net_device, - singleton!(StackResources::<2>::new()), + singleton!(StackResources::<4>::new()), )); - unwrap!(spawner.spawn(net_task(stack))); + defmt::unwrap!(spawner.spawn(net_task(stack))); + + // And now we can use it! + defmt::info!("Device initialized!"); + + // spawner + // .spawn(echo_task( + // &stack, + // "tcpbin.com", + // 4242, + // Duration::from_millis(500), + // )) + // .unwrap(); + + let mut rx_buffer = [0; 256]; + let mut tx_buffer = [0; 256]; + let mut buf = [0; 256]; + let mut cnt = 0u32; + let mut msg = heapless::String::<64>::new(); loop { - match control.join_wpa2("test", "1234abcd").await { - Ok(_) => break, - Err(err) => { - info!("join failed with error={:?}", err); + loop { + match control.join_wpa2("test", "1234abcd").await { + Ok(_) => { + defmt::info!("Network connected!"); + spawner + .spawn(echo_task( + &stack, + "echo.u-blox.com", + 7, + Duration::from_secs(1), + )) + .unwrap(); + break; + } + Err(err) => { + defmt::info!("join failed with error={:?}. Retrying in 1 second", err); + Timer::after(Duration::from_secs(1)).await; + } } } - } + 'outer: loop { + Timer::after(Duration::from_secs(1)).await; - // And now we can use it! + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // // socket.set_timeout(Some(Duration::from_secs(10))); - let mut rx_buffer = [0; 4096]; - let mut tx_buffer = [0; 4096]; - let mut buf = [0; 4096]; + let remote: SocketAddr = (Ipv4Addr::new(192, 168, 73, 183), 4444).into(); + defmt::info!("Connecting... {}", defmt::Debug2Format(&remote)); + if let Err(e) = socket.connect(remote).await { + defmt::warn!("connect error: {:?}", e); + continue; + } + defmt::info!( + "Connected to {:?}", + defmt::Debug2Format(&socket.remote_endpoint()) + ); - defmt::info!("Device initialized!"); + 'inner: loop { + match select(Timer::after(Duration::from_secs(3)), socket.read(&mut buf)).await { + Either::First(_) => { + msg.clear(); + write!(msg, "Hello world! {}\n", cnt).unwrap(); + cnt = cnt.wrapping_add(1); + if let Err(e) = socket.write_all(msg.as_bytes()).await { + defmt::warn!("write error: {:?}", e); + break; + } + defmt::info!("txd: {}", msg); + Timer::after(Duration::from_millis(400)).await; + } + Either::Second(res) => { + let n = match res { + Ok(0) => { + defmt::warn!("read EOF"); + break; + } + Ok(n) => n, + Err(e) => { + defmt::warn!("{:?}", e); + break; + } + }; + defmt::info!("rxd [{}] {}", n, core::str::from_utf8(&buf[..n]).unwrap()); - loop { - Timer::after(Duration::from_millis(1000)).await; - // let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); - // // socket.set_timeout(Some(embassy_net::SmolDuration::from_secs(10))); - - // control.gpio_set(GPIOId::A10, GPIOValue::Low).await; - // // info!("Listening on TCP:1234..."); - // // if let Err(e) = socket.accept(1234).await { - // // warn!("accept error: {:?}", e); - // // continue; - // // } - - // // info!("Received connection from {:?}", socket.remote_endpoint()); - // control.gpio_set(GPIOId::A10, GPIOValue::High).await; - - // loop { - // let n = match socket.read(&mut buf).await { - // Ok(0) => { - // warn!("read EOF"); - // break; - // } - // Ok(n) => n, - // Err(e) => { - // warn!("read error: {:?}", e); - // break; - // } - // }; - - // info!("rxd {}", from_utf8(&buf[..n]).unwrap()); - - // match socket.write_all(&buf[..n]).await { - // Ok(()) => {} - // Err(e) => { - // warn!("write error: {:?}", e); - // break; - // } - // }; - // } + match &buf[..n] { + b"c\n" => { + socket.close(); + break 'inner; + } + b"a\n" => { + socket.abort(); + break 'inner; + } + b"d\n" => { + drop(socket); + break 'inner; + } + b"f\n" => { + control.disconnect().await.unwrap(); + break 'outer; + } + _ => {} + } + } + } + } + defmt::info!("Press USER button to reconnect socket!"); + btn.wait_for_any_edge().await; + continue; + } + defmt::info!("Press USER button to reconnect to WiFi!"); + btn.wait_for_any_edge().await; } } diff --git a/src/asynch/channel.rs b/src/asynch/channel.rs new file mode 100644 index 0000000..2c582a1 --- /dev/null +++ b/src/asynch/channel.rs @@ -0,0 +1,579 @@ +#![allow(dead_code)] + +use core::cell::RefCell; +use core::mem::MaybeUninit; +use core::task::{Context, Poll}; + +use defmt::unwrap; +pub use embassy_net_driver as driver; +use embassy_net_driver::{Capabilities, LinkState, Medium}; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use embassy_sync::waitqueue::WakerRegistration; + +pub struct State { + rx: [PacketBuf; N_RX], + tx: [PacketBuf; N_TX], + inner: MaybeUninit>, +} + +impl State { + const NEW_PACKET: PacketBuf = PacketBuf::new(); + + pub const fn new() -> Self { + Self { + rx: [Self::NEW_PACKET; N_RX], + tx: [Self::NEW_PACKET; N_TX], + inner: MaybeUninit::uninit(), + } + } +} + +struct StateInner<'d, const MTU: usize> { + rx: zerocopy_channel::Channel<'d, NoopRawMutex, PacketBuf>, + tx: zerocopy_channel::Channel<'d, NoopRawMutex, PacketBuf>, + shared: Mutex>, +} + +/// State of the LinkState +struct Shared { + link_state: LinkState, + waker: WakerRegistration, + ethernet_address: [u8; 6], +} + +pub struct Runner<'d, const MTU: usize> { + tx_chan: zerocopy_channel::Receiver<'d, NoopRawMutex, PacketBuf>, + rx_chan: zerocopy_channel::Sender<'d, NoopRawMutex, PacketBuf>, + shared: &'d Mutex>, +} + +#[derive(Clone, Copy)] +pub struct StateRunner<'d> { + shared: &'d Mutex>, +} + +pub struct RxRunner<'d, const MTU: usize> { + rx_chan: zerocopy_channel::Sender<'d, NoopRawMutex, PacketBuf>, +} + +pub struct TxRunner<'d, const MTU: usize> { + tx_chan: zerocopy_channel::Receiver<'d, NoopRawMutex, PacketBuf>, +} + +impl<'d, const MTU: usize> Runner<'d, MTU> { + pub fn split(self) -> (StateRunner<'d>, RxRunner<'d, MTU>, TxRunner<'d, MTU>) { + ( + StateRunner { + shared: self.shared, + }, + RxRunner { + rx_chan: self.rx_chan, + }, + TxRunner { + tx_chan: self.tx_chan, + }, + ) + } + + pub fn state_runner(&self) -> StateRunner<'d> { + StateRunner { + shared: self.shared, + } + } + + pub fn set_link_state(&mut self, state: LinkState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = state; + s.waker.wake(); + }); + } + + pub fn set_ethernet_address(&mut self, address: [u8; 6]) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.ethernet_address = address; + s.waker.wake(); + }); + } + + pub async fn rx_buf(&mut self) -> &mut [u8] { + let p = self.rx_chan.send().await; + &mut p.buf + } + + pub fn try_rx_buf(&mut self) -> Option<&mut [u8]> { + let p = self.rx_chan.try_send()?; + Some(&mut p.buf) + } + + pub fn poll_rx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { + match self.rx_chan.poll_send(cx) { + Poll::Ready(p) => Poll::Ready(&mut p.buf), + Poll::Pending => Poll::Pending, + } + } + + pub fn rx_done(&mut self, len: usize) { + let p = self.rx_chan.try_send().unwrap(); + p.len = len; + self.rx_chan.send_done(); + } + + pub async fn tx_buf(&mut self) -> &mut [u8] { + let p = self.tx_chan.recv().await; + &mut p.buf[..p.len] + } + + pub fn try_tx_buf(&mut self) -> Option<&mut [u8]> { + let p = self.tx_chan.try_recv()?; + Some(&mut p.buf[..p.len]) + } + + pub fn poll_tx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { + match self.tx_chan.poll_recv(cx) { + Poll::Ready(p) => Poll::Ready(&mut p.buf[..p.len]), + Poll::Pending => Poll::Pending, + } + } + + pub fn tx_done(&mut self) { + self.tx_chan.recv_done(); + } +} + +impl<'d> StateRunner<'d> { + pub fn set_link_state(&self, state: LinkState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = state; + s.waker.wake(); + }); + } + + pub fn set_ethernet_address(&self, address: [u8; 6]) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.ethernet_address = address; + s.waker.wake(); + }); + } + + pub fn link_state(&mut self, cx: &mut Context) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.waker.register(cx.waker()); + s.link_state + }) + } +} + +impl<'d, const MTU: usize> RxRunner<'d, MTU> { + pub async fn rx_buf(&mut self) -> &mut [u8] { + let p = self.rx_chan.send().await; + &mut p.buf + } + + pub fn try_rx_buf(&mut self) -> Option<&mut [u8]> { + let p = self.rx_chan.try_send()?; + Some(&mut p.buf) + } + + pub fn poll_rx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { + match self.rx_chan.poll_send(cx) { + Poll::Ready(p) => Poll::Ready(&mut p.buf), + Poll::Pending => Poll::Pending, + } + } + + pub fn rx_done(&mut self, len: usize) { + let p = self.rx_chan.try_send().unwrap(); + p.len = len; + self.rx_chan.send_done(); + } +} + +impl<'d, const MTU: usize> TxRunner<'d, MTU> { + pub async fn tx_buf(&mut self) -> &mut [u8] { + let p = self.tx_chan.recv().await; + &mut p.buf[..p.len] + } + + pub fn try_tx_buf(&mut self) -> Option<&mut [u8]> { + let p = self.tx_chan.try_recv()?; + Some(&mut p.buf[..p.len]) + } + + pub fn poll_tx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { + match self.tx_chan.poll_recv(cx) { + Poll::Ready(p) => Poll::Ready(&mut p.buf[..p.len]), + Poll::Pending => Poll::Pending, + } + } + + pub fn tx_done(&mut self) { + self.tx_chan.recv_done(); + } +} + +pub fn new<'d, const MTU: usize, const N_RX: usize, const N_TX: usize>( + state: &'d mut State, + ethernet_address: [u8; 6], +) -> (Runner<'d, MTU>, Device<'d, MTU>) { + let mut caps = Capabilities::default(); + caps.max_transmission_unit = MTU; + caps.medium = Medium::Ethernet; + + // safety: this is a self-referential struct, however: + // - it can't move while the `'d` borrow is active. + // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. + let state_uninit: *mut MaybeUninit> = + (&mut state.inner as *mut MaybeUninit>).cast(); + let state = unsafe { &mut *state_uninit }.write(StateInner { + rx: zerocopy_channel::Channel::new(&mut state.rx[..]), + tx: zerocopy_channel::Channel::new(&mut state.tx[..]), + shared: Mutex::new(RefCell::new(Shared { + link_state: LinkState::Down, + ethernet_address, + waker: WakerRegistration::new(), + })), + }); + + let (rx_sender, rx_receiver) = state.rx.split(); + let (tx_sender, tx_receiver) = state.tx.split(); + + ( + Runner { + tx_chan: tx_receiver, + rx_chan: rx_sender, + shared: &state.shared, + }, + Device { + caps, + shared: &state.shared, + rx: rx_receiver, + tx: tx_sender, + }, + ) +} + +pub struct PacketBuf { + len: usize, + buf: [u8; MTU], +} + +impl PacketBuf { + pub const fn new() -> Self { + Self { + len: 0, + buf: [0; MTU], + } + } +} + +pub struct Device<'d, const MTU: usize> { + rx: zerocopy_channel::Receiver<'d, NoopRawMutex, PacketBuf>, + tx: zerocopy_channel::Sender<'d, NoopRawMutex, PacketBuf>, + shared: &'d Mutex>, + caps: Capabilities, +} + +impl<'d, const MTU: usize> embassy_net_driver::Driver for Device<'d, MTU> { + type RxToken<'a> = RxToken<'a, MTU> where Self: 'a ; + type TxToken<'a> = TxToken<'a, MTU> where Self: 'a ; + + fn receive(&mut self, cx: &mut Context) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + if self.rx.poll_recv(cx).is_ready() && self.tx.poll_send(cx).is_ready() { + Some(( + RxToken { + rx: self.rx.borrow(), + }, + TxToken { + tx: self.tx.borrow(), + }, + )) + } else { + None + } + } + + /// Construct a transmit token. + fn transmit(&mut self, cx: &mut Context) -> Option> { + if self.tx.poll_send(cx).is_ready() { + Some(TxToken { + tx: self.tx.borrow(), + }) + } else { + None + } + } + + /// Get a description of device capabilities. + fn capabilities(&self) -> Capabilities { + self.caps.clone() + } + + fn ethernet_address(&self) -> [u8; 6] { + self.shared.lock(|s| s.borrow().ethernet_address) + } + + fn link_state(&mut self, cx: &mut Context) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.waker.register(cx.waker()); + s.link_state + }) + } +} + +pub struct RxToken<'a, const MTU: usize> { + rx: zerocopy_channel::Receiver<'a, NoopRawMutex, PacketBuf>, +} + +impl<'a, const MTU: usize> embassy_net_driver::RxToken for RxToken<'a, MTU> { + fn consume(mut self, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + // NOTE(unwrap): we checked the queue wasn't full when creating the token. + let pkt = unwrap!(self.rx.try_recv()); + let r = f(&mut pkt.buf[..pkt.len]); + self.rx.recv_done(); + r + } +} + +pub struct TxToken<'a, const MTU: usize> { + tx: zerocopy_channel::Sender<'a, NoopRawMutex, PacketBuf>, +} + +impl<'a, const MTU: usize> embassy_net_driver::TxToken for TxToken<'a, MTU> { + fn consume(mut self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + // NOTE(unwrap): we checked the queue wasn't full when creating the token. + let pkt = unwrap!(self.tx.try_send()); + let r = f(&mut pkt.buf[..len]); + pkt.len = len; + self.tx.send_done(); + r + } +} + +mod zerocopy_channel { + use core::cell::RefCell; + use core::future::poll_fn; + use core::marker::PhantomData; + use core::task::{Context, Poll}; + + use embassy_sync::blocking_mutex::raw::RawMutex; + use embassy_sync::blocking_mutex::Mutex; + use embassy_sync::waitqueue::WakerRegistration; + + pub struct Channel<'a, M: RawMutex, T> { + buf: *mut T, + phantom: PhantomData<&'a mut T>, + state: Mutex>, + } + + impl<'a, M: RawMutex, T> Channel<'a, M, T> { + pub fn new(buf: &'a mut [T]) -> Self { + let len = buf.len(); + assert!(len != 0); + + Self { + buf: buf.as_mut_ptr(), + phantom: PhantomData, + state: Mutex::new(RefCell::new(State { + len, + front: 0, + back: 0, + full: false, + send_waker: WakerRegistration::new(), + recv_waker: WakerRegistration::new(), + })), + } + } + + pub fn split(&mut self) -> (Sender<'_, M, T>, Receiver<'_, M, T>) { + (Sender { channel: self }, Receiver { channel: self }) + } + } + + pub struct Sender<'a, M: RawMutex, T> { + channel: &'a Channel<'a, M, T>, + } + + impl<'a, M: RawMutex, T> Sender<'a, M, T> { + pub fn borrow(&mut self) -> Sender<'_, M, T> { + Sender { + channel: self.channel, + } + } + + pub fn try_send(&mut self) -> Option<&mut T> { + self.channel.state.lock(|s| { + let s = &mut *s.borrow_mut(); + match s.push_index() { + Some(i) => Some(unsafe { &mut *self.channel.buf.add(i) }), + None => None, + } + }) + } + + pub fn poll_send(&mut self, cx: &mut Context) -> Poll<&mut T> { + self.channel.state.lock(|s| { + let s = &mut *s.borrow_mut(); + match s.push_index() { + Some(i) => Poll::Ready(unsafe { &mut *self.channel.buf.add(i) }), + None => { + s.recv_waker.register(cx.waker()); + Poll::Pending + } + } + }) + } + + pub async fn send(&mut self) -> &mut T { + let i = poll_fn(|cx| { + self.channel.state.lock(|s| { + let s = &mut *s.borrow_mut(); + match s.push_index() { + Some(i) => Poll::Ready(i), + None => { + s.recv_waker.register(cx.waker()); + Poll::Pending + } + } + }) + }) + .await; + unsafe { &mut *self.channel.buf.add(i) } + } + + pub fn send_done(&mut self) { + self.channel.state.lock(|s| s.borrow_mut().push_done()) + } + } + pub struct Receiver<'a, M: RawMutex, T> { + channel: &'a Channel<'a, M, T>, + } + + impl<'a, M: RawMutex, T> Receiver<'a, M, T> { + pub fn borrow(&mut self) -> Receiver<'_, M, T> { + Receiver { + channel: self.channel, + } + } + + pub fn try_recv(&mut self) -> Option<&mut T> { + self.channel.state.lock(|s| { + let s = &mut *s.borrow_mut(); + match s.pop_index() { + Some(i) => Some(unsafe { &mut *self.channel.buf.add(i) }), + None => None, + } + }) + } + + pub fn poll_recv(&mut self, cx: &mut Context) -> Poll<&mut T> { + self.channel.state.lock(|s| { + let s = &mut *s.borrow_mut(); + match s.pop_index() { + Some(i) => Poll::Ready(unsafe { &mut *self.channel.buf.add(i) }), + None => { + s.send_waker.register(cx.waker()); + Poll::Pending + } + } + }) + } + + pub async fn recv(&mut self) -> &mut T { + let i = poll_fn(|cx| { + self.channel.state.lock(|s| { + let s = &mut *s.borrow_mut(); + match s.pop_index() { + Some(i) => Poll::Ready(i), + None => { + s.send_waker.register(cx.waker()); + Poll::Pending + } + } + }) + }) + .await; + unsafe { &mut *self.channel.buf.add(i) } + } + + pub fn recv_done(&mut self) { + self.channel.state.lock(|s| s.borrow_mut().pop_done()) + } + } + + struct State { + len: usize, + + /// Front index. Always 0..=(N-1) + front: usize, + /// Back index. Always 0..=(N-1). + back: usize, + + /// Used to distinguish "empty" and "full" cases when `front == back`. + /// May only be `true` if `front == back`, always `false` otherwise. + full: bool, + + send_waker: WakerRegistration, + recv_waker: WakerRegistration, + } + + impl State { + fn increment(&self, i: usize) -> usize { + if i + 1 == self.len { + 0 + } else { + i + 1 + } + } + + fn is_full(&self) -> bool { + self.full + } + + fn is_empty(&self) -> bool { + self.front == self.back && !self.full + } + + fn push_index(&mut self) -> Option { + match self.is_full() { + true => None, + false => Some(self.back), + } + } + + fn push_done(&mut self) { + assert!(!self.is_full()); + self.back = self.increment(self.back); + if self.back == self.front { + self.full = true; + } + self.send_waker.wake(); + } + + fn pop_index(&mut self) -> Option { + match self.is_empty() { + true => None, + false => Some(self.front), + } + } + + fn pop_done(&mut self) { + assert!(!self.is_empty()); + self.front = self.increment(self.front); + self.full = false; + self.recv_waker.wake(); + } + } +} diff --git a/src/asynch/control.rs b/src/asynch/control.rs index f7a8dce..20f7b8c 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -1,48 +1,51 @@ +use core::future::poll_fn; +use core::task::Poll; + use atat::asynch::AtatClient; -use ch::driver::LinkState; -use embassy_net_driver_channel as ch; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_sync::mutex::Mutex; -use embassy_time::{with_timeout, Duration, Timer}; +use embassy_net_driver::LinkState; +use embassy_time::{with_timeout, Duration}; +use crate::command::network::SetNetworkHostName; use crate::command::wifi::types::{ Authentication, StatusId, WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, }; use crate::command::wifi::{ ExecWifiStationAction, GetWifiMac, GetWifiStatus, SetWifiStationConfig, }; -use crate::command::{ - edm::EdmAtCmdWrapper, - gpio::{ +use crate::command::OnOff; +use crate::error::Error; +use crate::{ + command::gpio::{ types::{GPIOId, GPIOValue}, WriteGPIO, }, + hex, }; -use crate::error::Error; + +use super::{channel, AtHandle}; + +const CONFIG_ID: u8 = 0; pub struct Control<'a, AT: AtatClient> { - state_ch: ch::StateRunner<'a>, - at_client: &'a Mutex, + state_ch: channel::StateRunner<'a>, + at: AtHandle<'a, AT>, } impl<'a, AT: AtatClient> Control<'a, AT> { - pub(crate) fn new( - state_ch: ch::StateRunner<'a>, - at_client: &'a Mutex, - ) -> Self { - Self { - state_ch, - at_client, - } + pub(crate) fn new(state_ch: channel::StateRunner<'a>, at: AtHandle<'a, AT>) -> Self { + Self { state_ch, at } } pub(crate) async fn init(&mut self) -> Result<(), Error> { defmt::debug!("Initalizing ublox control"); // read MAC addr. - let resp = self.send_edm(GetWifiMac).await?; - // FIXME: MAC length here? - // self.state_ch - // .set_ethernet_address(resp.mac_addr.as_slice().try_into().unwrap()); + let mut resp = self.at.send_edm(GetWifiMac).await?; + self.state_ch.set_ethernet_address( + hex::from_hex(resp.mac_addr.as_mut_slice()) + .unwrap() + .try_into() + .unwrap(), + ); // let country = countries::WORLD_WIDE_XX; // let country_info = CountryInfo { @@ -83,32 +86,81 @@ impl<'a, AT: AtatClient> Control<'a, AT> { Ok(()) } - pub async fn join_open(&mut self, ssid: &str) -> Result<(), Error> { - let config_id = 0; + pub async fn set_hostname(&mut self, hostname: &str) -> Result<(), Error> { + self.at + .send_edm(SetNetworkHostName { + host_name: hostname, + }) + .await?; + Ok(()) + } - self.send_edm(ExecWifiStationAction { - config_id, - action: WifiStationAction::Reset, - }) - .await?; + async fn get_wifi_status(&mut self) -> Result { + match self + .at + .send_edm(GetWifiStatus { + status_id: StatusId::Status, + }) + .await? + .status_id + { + WifiStatus::Status(s) => Ok(s), + _ => Err(Error::AT(atat::Error::InvalidResponse)), + } + } - self.send_edm(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::SSID(heapless::String::from(ssid)), - }) - .await?; + async fn get_connected_ssid(&mut self) -> Result, Error> { + match self + .at + .send_edm(GetWifiStatus { + status_id: StatusId::SSID, + }) + .await? + .status_id + { + WifiStatus::SSID(s) => Ok(s), + _ => Err(Error::AT(atat::Error::InvalidResponse)), + } + } - self.send_edm(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::Authentication(Authentication::Open), - }) - .await?; + pub async fn join_open(&mut self, ssid: &str) -> Result<(), Error> { + if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { + // Wifi already connected. Check if the SSID is the same + let current_ssid = self.get_connected_ssid().await?; + if current_ssid.as_str() == ssid { + return Ok(()); + } else { + self.disconnect().await?; + }; + } - self.send_edm(ExecWifiStationAction { - config_id, - action: WifiStationAction::Activate, - }) - .await?; + self.at + .send_edm(SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), + }) + .await?; + + self.at + .send_edm(SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::SSID(heapless::String::from(ssid)), + }) + .await?; + + self.at + .send_edm(SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::Open), + }) + .await?; + + self.at + .send_edm(ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Activate, + }) + .await?; with_timeout(Duration::from_secs(10), self.wait_for_join(ssid)) .await @@ -118,37 +170,52 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } pub async fn join_wpa2(&mut self, ssid: &str, passphrase: &str) -> Result<(), Error> { - let config_id = 0; - - self.send_edm(ExecWifiStationAction { - config_id, - action: WifiStationAction::Reset, - }) - .await?; - - self.send_edm(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::SSID(heapless::String::from(ssid)), - }) - .await?; - - self.send_edm(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }) - .await?; - - self.send_edm(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::WpaPskOrPassphrase(heapless::String::from(passphrase)), - }) - .await?; + if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { + // Wifi already connected. Check if the SSID is the same + let current_ssid = self.get_connected_ssid().await?; + if current_ssid.as_str() == ssid { + return Ok(()); + } else { + self.disconnect().await?; + }; + } - self.send_edm(ExecWifiStationAction { - config_id, - action: WifiStationAction::Activate, - }) - .await?; + self.at + .send_edm(SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), + }) + .await?; + + self.at + .send_edm(SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::SSID(heapless::String::from(ssid)), + }) + .await?; + + self.at + .send_edm(SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + }) + .await?; + + self.at + .send_edm(SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::WpaPskOrPassphrase(heapless::String::from( + passphrase, + )), + }) + .await?; + + self.at + .send_edm(ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Activate, + }) + .await?; with_timeout(Duration::from_secs(10), self.wait_for_join(ssid)) .await @@ -157,135 +224,49 @@ impl<'a, AT: AtatClient> Control<'a, AT> { Ok(()) } - async fn wait_for_join(&mut self, ssid: &str) -> Result<(), Error> { - loop { - let status = self - .send_edm(GetWifiStatus { - status_id: StatusId::Status, - }) - .await?; - - if matches!( - status.status_id, - WifiStatus::Status(WifiStatusVal::Connected) - ) { - let connected_ssid = self - .send_edm(GetWifiStatus { - status_id: StatusId::SSID, + pub async fn disconnect(&mut self) -> Result<(), Error> { + match self.get_wifi_status().await? { + WifiStatusVal::Disabled => {} + WifiStatusVal::Disconnected | WifiStatusVal::Connected => { + self.at + .send_edm(ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Deactivate, }) .await?; - - match connected_ssid.status_id { - WifiStatus::SSID(s) if s.as_str() == ssid => { - self.state_ch.set_link_state(LinkState::Up); - defmt::debug!("JOINED"); - return Ok(()); - } - _ => return Err(Error::Network), - } } - - Timer::after(Duration::from_millis(500)).await; } - } - pub async fn gpio_set(&mut self, id: GPIOId, value: GPIOValue) -> Result<(), Error> { - self.send_edm(WriteGPIO { id, value }).await?; + let wait_for_disconnect = poll_fn(|cx| match self.state_ch.link_state(cx) { + LinkState::Up => Poll::Pending, + LinkState::Down => Poll::Ready(()), + }); + + with_timeout(Duration::from_secs(10), wait_for_disconnect) + .await + .map_err(|_| Error::Timeout)?; + Ok(()) } - // pub async fn start_ap_open(&mut self, ssid: &str, channel: u8) { - // self.start_ap(ssid, "", Security::OPEN, channel).await; - // } - - // pub async fn start_ap_wpa2(&mut self, ssid: &str, passphrase: &str, channel: u8) { - // self.start_ap(ssid, passphrase, Security::WPA2_AES_PSK, channel) - // .await; - // } - - // async fn start_ap(&mut self, ssid: &str, passphrase: &str, security: Security, channel: u8) { - // if security != Security::OPEN - // && (passphrase.as_bytes().len() < MIN_PSK_LEN - // || passphrase.as_bytes().len() > MAX_PSK_LEN) - // { - // panic!("Passphrase is too short or too long"); - // } - - // // Temporarily set wifi down - // self.ioctl(ControlType::Set, IOCTL_CMD_DOWN, 0, &mut []) - // .await; - - // // Turn off APSTA mode - // self.set_iovar_u32("apsta", 0).await; - - // // Set wifi up again - // self.ioctl(ControlType::Set, IOCTL_CMD_UP, 0, &mut []).await; - - // // Turn on AP mode - // self.ioctl_set_u32(IOCTL_CMD_SET_AP, 0, 1).await; - - // // Set SSID - // let mut i = SsidInfoWithIndex { - // index: 0, - // ssid_info: SsidInfo { - // len: ssid.as_bytes().len() as _, - // ssid: [0; 32], - // }, - // }; - // i.ssid_info.ssid[..ssid.as_bytes().len()].copy_from_slice(ssid.as_bytes()); - // self.set_iovar("bsscfg:ssid", &i.to_bytes()).await; - - // // Set channel number - // self.ioctl_set_u32(IOCTL_CMD_SET_CHANNEL, 0, channel as u32) - // .await; - - // // Set security - // self.set_iovar_u32x2("bsscfg:wsec", 0, (security as u32) & 0xFF) - // .await; - - // if security != Security::OPEN { - // self.set_iovar_u32x2("bsscfg:wpa_auth", 0, 0x0084).await; // wpa_auth = WPA2_AUTH_PSK | WPA_AUTH_PSK - - // Timer::after(Duration::from_millis(100)).await; - - // // Set passphrase - // let mut pfi = PassphraseInfo { - // len: passphrase.as_bytes().len() as _, - // flags: 1, // WSEC_PASSPHRASE - // passphrase: [0; 64], - // }; - // pfi.passphrase[..passphrase.as_bytes().len()].copy_from_slice(passphrase.as_bytes()); - // self.ioctl( - // ControlType::Set, - // IOCTL_CMD_SET_PASSPHRASE, - // 0, - // &mut pfi.to_bytes(), - // ) - // .await; - // } - - // // Change mutlicast rate from 1 Mbps to 11 Mbps - // self.set_iovar_u32("2g_mrate", 11000000 / 500000).await; - - // // Start AP - // self.set_iovar_u32x2("bss", 0, 1).await; // bss = BSS_UP - // } - - async fn send_edm, const LEN: usize>( - &mut self, - cmd: Cmd, - ) -> Result { - self.send(EdmAtCmdWrapper(cmd)).await + async fn wait_for_join(&mut self, ssid: &str) -> Result<(), Error> { + poll_fn(|cx| match self.state_ch.link_state(cx) { + LinkState::Down => Poll::Pending, + LinkState::Up => Poll::Ready(()), + }) + .await; + + // Check that SSID matches + let current_ssid = self.get_connected_ssid().await?; + if ssid != current_ssid.as_str() { + return Err(Error::Network); + } + + Ok(()) } - async fn send, const LEN: usize>( - &mut self, - cmd: Cmd, - ) -> Result { - self.at_client - .lock() - .await - .send_retry::(&cmd) - .await + pub async fn gpio_set(&mut self, id: GPIOId, value: GPIOValue) -> Result<(), Error> { + self.at.send_edm(WriteGPIO { id, value }).await?; + Ok(()) } } diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 2ca2593..48c239a 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -3,45 +3,76 @@ pub mod runner; #[cfg(feature = "ublox-sockets")] pub mod ublox_stack; -use crate::command::edm::urc::EdmEvent; +pub(crate) mod channel; + +use crate::command::edm::{urc::EdmEvent, EdmAtCmdWrapper}; use atat::{asynch::AtatClient, UrcSubscription}; -use ch::Device; -use embassy_net_driver_channel as ch; +use channel::Device; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; use embedded_hal::digital::OutputPin; use runner::Runner; use self::control::Control; +// NOTE: Must be pow(2) due to internal usage of `FnvIndexMap` +const MAX_CONNS: usize = 8; + +pub struct AtHandle<'d, AT: AtatClient>(&'d Mutex); + +impl<'d, AT: AtatClient> AtHandle<'d, AT> { + async fn send_edm, const LEN: usize>( + &mut self, + cmd: Cmd, + ) -> Result { + self.send(EdmAtCmdWrapper(cmd)).await + } + + async fn send, const LEN: usize>( + &mut self, + cmd: Cmd, + ) -> Result { + self.0.lock().await.send_retry::(&cmd).await + } +} + pub struct State { - ch: ch::State, + ch: channel::State, at_handle: Mutex, } impl State { pub fn new(at_handle: AT) -> Self { Self { - ch: ch::State::new(), + ch: channel::State::new(), at_handle: Mutex::new(at_handle), } } } -pub const MTU: usize = 4096 + 2; // DATA_PACKAGE_SIZE +pub const MTU: usize = 4096 + 4; // DATA_PACKAGE_SIZE pub async fn new<'a, AT: AtatClient, RST: OutputPin>( state: &'a mut State, urc_subscription: UrcSubscription<'a, EdmEvent>, reset: RST, -) -> (Device<'a, MTU>, Control<'a, AT>, Runner<'a, AT, RST>) { - let (ch_runner, net_device) = ch::new(&mut state.ch, [0; 6]); +) -> ( + Device<'a, MTU>, + Control<'a, AT>, + Runner<'a, AT, RST, MAX_CONNS>, +) { + let (ch_runner, net_device) = channel::new(&mut state.ch, [0; 6]); let state_ch = ch_runner.state_runner(); - let mut runner = Runner::new(ch_runner, &state.at_handle, reset, urc_subscription); + let mut runner = Runner::new( + ch_runner, + AtHandle(&state.at_handle), + reset, + urc_subscription, + ); runner.init().await.unwrap(); - let mut control = Control::new(state_ch, &state.at_handle); + let mut control = Control::new(state_ch, AtHandle(&state.at_handle)); control.init().await.unwrap(); (net_device, control, runner) diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index 9ce4b7d..13620d7 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -1,60 +1,71 @@ -use core::{future::poll_fn, task::Poll}; +use core::str::FromStr; +use super::channel::{self, driver::LinkState}; use crate::{ + asynch::ublox_stack::Disconnect, command::{ - data_mode::{types::PeerConfigParameter, SetPeerConfiguration}, - edm::{urc::EdmEvent, EdmAtCmdWrapper, EdmDataCommand, SwitchToEdmCommand}, - network::SetNetworkHostName, + data_mode::{ + responses::ConnectPeerResponse, urc::PeerDisconnected, ClosePeerConnection, ConnectPeer, + }, + edm::{urc::EdmEvent, EdmDataCommand, SwitchToEdmCommand}, + network::{ + responses::NetworkStatusResponse, + types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, + urc::{NetworkDown, NetworkUp}, + GetNetworkStatus, + }, + ping::Ping, system::{ - types::{BaudRate, ChangeAfterConfirm, FlowControl, Parity, StopBits}, - RebootDCE, SetRS232Settings, StoreCurrentConfig, + types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, + RebootDCE, SetEcho, SetRS232Settings, StoreCurrentConfig, }, wifi::{ - types::{StatusId, WifiConfig, WifiStatus, WifiStatusVal}, - GetWifiStatus, SetWifiConfig, + types::DisconnectReason, + urc::{WifiLinkConnected, WifiLinkDisconnected}, }, Urc, }, - config::Config, + connection::{WiFiState, WifiConnection}, error::Error, + network::WifiNetwork, }; use atat::{asynch::AtatClient, UrcSubscription}; -use ch::driver::LinkState; use embassy_futures::select::{select, Either}; -use embassy_net_driver_channel as ch; -use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; use embassy_time::{with_timeout, Duration, Timer}; use embedded_hal::digital::OutputPin; +use no_std_net::{Ipv4Addr, Ipv6Addr}; use super::{ - ublox_stack::{DataPacket, SocketEvent}, - MTU, + ublox_stack::{Connect, DataPacket, SocketRx, SocketTx}, + AtHandle, MTU, }; /// Background runner for the Ublox Module. /// /// You must call `.run()` in a background task for the Ublox Module to operate. -pub struct Runner<'d, AT: AtatClient, RST: OutputPin> { - ch: ch::Runner<'d, MTU>, - at_handle: &'d Mutex, +pub struct Runner<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> { + ch: channel::Runner<'d, MTU>, + at: AtHandle<'d, AT>, reset: RST, - config: Config, + wifi_connection: Option, + // connections: FnvIndexMap, urc_subscription: UrcSubscription<'d, EdmEvent>, } -impl<'d, AT: AtatClient, RST: OutputPin> Runner<'d, AT, RST> { +impl<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> Runner<'d, AT, RST, MAX_CONNS> { pub(crate) fn new( - ch: ch::Runner<'d, MTU>, - at_handle: &'d Mutex, + ch: channel::Runner<'d, MTU>, + at: AtHandle<'d, AT>, reset: RST, urc_subscription: UrcSubscription<'d, EdmEvent>, ) -> Self { Self { ch, - at_handle, + at, reset, - config: Config::default(), + wifi_connection: None, urc_subscription, + // connections: IndexMap::new(), } } @@ -64,100 +75,49 @@ impl<'d, AT: AtatClient, RST: OutputPin> Runner<'d, AT, RST> { // Hard reset module self.reset().await?; - // Switch to EDM on Init. If in EDM, fail and check with autosense - self.send(SwitchToEdmCommand).await.ok(); - - loop { - let urc = self.urc_subscription.next_message_pure().await; - if matches!(urc, EdmEvent::StartUp) { - break; - } - // Ignore AT results until we are successful in EDM mode - self.send(SwitchToEdmCommand).await.ok(); - Timer::after(Duration::from_millis(100)).await; - } - // TODO: handle EDM settings quirk see EDM datasheet: 2.2.5.1 AT Request Serial settings - self.send_edm(SetRS232Settings { - baud_rate: BaudRate::B115200, - flow_control: FlowControl::On, - data_bits: 8, - stop_bits: StopBits::One, - parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, - }) - .await?; - - if let Some(hostname) = self.config.hostname.clone() { - self.send_edm(SetNetworkHostName { - host_name: hostname.as_str(), + self.at + .send_edm(SetRS232Settings { + baud_rate: BaudRate::B115200, + flow_control: FlowControl::On, + data_bits: 8, + stop_bits: StopBits::One, + parity: Parity::None, + change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, }) .await?; - } - // self.send_edm(SetWifiConfig { - // config_param: WifiConfig::RemainOnChannel(0), - // }) - // .await?; + // self.restart(true).await?; - self.send_edm(StoreCurrentConfig).await?; - - // self.software_reset().await?; - - // // FIXME: Prevent infinite loop - // loop { - // let urc = self.urc_subscription.next_message_pure().await; - // if matches!(urc, EdmEvent::StartUp) { - // break; - // } - // self.send(SwitchToEdmCommand).await.ok(); - // Timer::after(Duration::from_millis(100)).await; + // Move to control + // if let Some(size) = self.config.tls_in_buffer_size { + // self.at + // .send_edm(SetPeerConfiguration { + // parameter: PeerConfigParameter::TlsInBuffer(size), + // }) + // .await?; // } - if let Some(size) = self.config.tls_in_buffer_size { - self.send_edm(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsInBuffer(size), - }) - .await?; - } - - if let Some(size) = self.config.tls_out_buffer_size { - self.send_edm(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsOutBuffer(size), - }) - .await?; - } + // if let Some(size) = self.config.tls_out_buffer_size { + // self.at + // .send_edm(SetPeerConfiguration { + // parameter: PeerConfigParameter::TlsOutBuffer(size), + // }) + // .await?; + // } Ok(()) } - async fn send_edm, const LEN: usize>( - &mut self, - cmd: Cmd, - ) -> Result { - self.send(EdmAtCmdWrapper(cmd)).await - } - - async fn send, const LEN: usize>( - &mut self, - cmd: Cmd, - ) -> Result { - self.at_handle - .lock() - .await - .send_retry::(&cmd) - .await - } - async fn wait_startup(&mut self, timeout: Duration) -> Result<(), Error> { - let fut = poll_fn(|_cx| { - if let Some(EdmEvent::ATEvent(Urc::StartUp)) | Some(EdmEvent::StartUp) = - self.urc_subscription.try_next_message_pure() - { - return Poll::Ready(()); + let fut = async { + loop { + match self.urc_subscription.next_message_pure().await { + EdmEvent::ATEvent(Urc::StartUp) | EdmEvent::StartUp => return, + _ => {} + } } - Poll::Pending - }); + }; with_timeout(timeout, fut).await.map_err(|_| Error::Timeout) } @@ -170,29 +130,68 @@ impl<'d, AT: AtatClient, RST: OutputPin> Runner<'d, AT, RST> { self.wait_startup(Duration::from_secs(4)).await?; + self.enter_edm(Duration::from_secs(4)).await?; + Ok(()) } - pub async fn software_reset(&mut self) -> Result<(), Error> { + pub async fn restart(&mut self, store: bool) -> Result<(), Error> { defmt::warn!("Soft resetting Ublox Short Range"); - self.send_edm(RebootDCE).await?; + if store { + self.at.send_edm(StoreCurrentConfig).await?; + } + + self.at.send_edm(RebootDCE).await?; + + Timer::after(Duration::from_millis(3500)).await; + + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } - self.wait_startup(Duration::from_secs(10)).await?; + pub async fn enter_edm(&mut self, timeout: Duration) -> Result<(), Error> { + // Switch to EDM on Init. If in EDM, fail and check with autosense + let fut = async { + loop { + // Ignore AT results until we are successful in EDM mode + self.at.send(SwitchToEdmCommand).await.ok(); + + match select( + self.urc_subscription.next_message_pure(), + Timer::after(Duration::from_millis(100)), + ) + .await + { + Either::First(EdmEvent::StartUp) => break, + _ => {} + }; + } + }; + + with_timeout(timeout, fut) + .await + .map_err(|_| Error::Timeout)?; + + self.at.send_edm(SetEcho { on: EchoOn::Off }).await?; Ok(()) } pub async fn is_link_up(&mut self) -> Result { - let status = self - .send_edm(GetWifiStatus { - status_id: StatusId::Status, - }) - .await?; + // Determine link state + let link_state = match self.wifi_connection { + Some(ref conn) + if conn.network_up && matches!(conn.wifi_state, WiFiState::Connected) => + { + LinkState::Up + } + _ => LinkState::Down, + }; + + self.ch.set_link_state(link_state); - Ok(matches!( - status.status_id, - WifiStatus::Status(WifiStatusVal::Connected) - )) + Ok(link_state == LinkState::Up) } pub async fn run(mut self) -> ! { @@ -202,65 +201,134 @@ impl<'d, AT: AtatClient, RST: OutputPin> Runner<'d, AT, RST> { match select(tx, urc).await { Either::First(p) => { - if let Ok(packet) = postcard::from_bytes::(p) { - self.at_handle - .lock() - .await - .send_retry(&EdmDataCommand { - channel: packet.edm_channel, - data: packet.payload, - }) - .await - .ok(); + match postcard::from_bytes::(p) { + Ok(SocketTx::Data(packet)) => { + self.at + .send(EdmDataCommand { + channel: packet.edm_channel, + data: packet.payload, + }) + .await + .ok(); + } + Ok(SocketTx::Connect(Connect { url, socket_handle })) => { + if let Ok(ConnectPeerResponse { peer_handle }) = + self.at.send_edm(ConnectPeer { url }).await + { + self.rx(SocketRx::PeerHandle(socket_handle, peer_handle)) + .await; + } + } + Ok(SocketTx::Disconnect(peer_handle)) => { + self.at + .send_edm(ClosePeerConnection { peer_handle }) + .await + .ok(); + } + Ok(SocketTx::Dns(hostname)) => { + if self + .at + .send_edm(Ping { + retry_num: 1, + hostname, + }) + .await + .is_err() + { + self.rx(SocketRx::Ping(Err(()))).await; + } + } + Err(_) => {} } self.ch.tx_done(); } Either::Second(p) => match p { EdmEvent::BluetoothConnectEvent(_) => {} - EdmEvent::ATEvent(urc) => self.handle_urc(urc), - EdmEvent::StartUp => todo!(), + EdmEvent::ATEvent(urc) => self.handle_urc(urc).await.unwrap(), + EdmEvent::StartUp => { + defmt::error!("EDM startup event?! Device restarted unintentionally!"); + } // All below events needs to be conveyed to `self.ch.rx` - EdmEvent::IPv4ConnectEvent(ev) => self.rx(ev), - EdmEvent::IPv6ConnectEvent(ev) => self.rx(ev), - EdmEvent::DisconnectEvent(channel_id) => self.rx(channel_id), + EdmEvent::IPv4ConnectEvent(ev) => self.rx(ev).await, + EdmEvent::IPv6ConnectEvent(ev) => self.rx(ev).await, + EdmEvent::DisconnectEvent(channel_id) => self.rx(channel_id).await, EdmEvent::DataEvent(ev) => { let packet = DataPacket { edm_channel: ev.channel_id, payload: ev.data.as_slice(), }; - self.rx(packet); + self.rx(packet).await; } }, } } } - fn rx<'a>(&mut self, packet: impl Into>) { - match self.ch.try_rx_buf() { - Some(buf) => { - let event: SocketEvent = packet.into(); - let used = postcard::to_slice(&event, buf).unwrap(); - let len = used.len(); - self.ch.rx_done(len); - } - None => { - defmt::warn!("failed to push rxd packet to the channel.") - } - } + async fn rx<'a>(&mut self, packet: impl Into>) { + let pkg = packet.into(); + let buf = self.ch.rx_buf().await; + let used = defmt::unwrap!(postcard::to_slice(&pkg, buf)); + let len = used.len(); + self.ch.rx_done(len); } - fn handle_urc(&mut self, urc: Urc) { + async fn handle_urc(&mut self, urc: Urc) -> Result<(), Error> { match urc { - Urc::StartUp => {} - Urc::PeerConnected(_) => todo!(), - Urc::PeerDisconnected(_) => todo!(), - Urc::WifiLinkConnected(_) => { - self.ch.set_link_state(LinkState::Up); + Urc::StartUp => { + defmt::error!("AT startup event?! Device restarted unintentionally!"); + } + Urc::PeerConnected(pc) => { + defmt::info!("Peer connected! {}", pc); + // if self.connections.insert(handle, connection_type).is_err() { + // defmt::warn!("Out of connection entries"); + // } + } + Urc::PeerDisconnected(PeerDisconnected { handle }) => { + defmt::info!("Peer disconnected!"); + self.rx(SocketRx::Disconnect(Disconnect::Peer(handle))) + .await; + // self.connections.remove(&handle); } - Urc::WifiLinkDisconnected(_) => { - self.ch.set_link_state(LinkState::Down); + Urc::WifiLinkConnected(WifiLinkConnected { + connection_id: _, + bssid, + channel, + }) => { + if let Some(ref mut con) = self.wifi_connection { + con.wifi_state = WiFiState::Connected; + con.network.bssid = bssid; + con.network.channel = channel; + } else { + defmt::debug!("[URC] Active network config discovered"); + self.wifi_connection.replace( + WifiConnection::new( + WifiNetwork::new_station(bssid, channel), + WiFiState::Connected, + 255, + ) + .activate(), + ); + } + self.is_link_up().await?; + } + Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { + if let Some(ref mut con) = self.wifi_connection { + match reason { + DisconnectReason::NetworkDisabled => { + con.wifi_state = WiFiState::Inactive; + } + DisconnectReason::SecurityProblems => { + defmt::error!("Wifi Security Problems"); + } + _ => { + con.wifi_state = WiFiState::NotConnected; + } + } + } + + self.is_link_up().await?; } Urc::WifiAPUp(_) => todo!(), Urc::WifiAPDown(_) => todo!(), @@ -268,11 +336,75 @@ impl<'d, AT: AtatClient, RST: OutputPin> Runner<'d, AT, RST> { Urc::WifiAPStationDisconnected(_) => todo!(), Urc::EthernetLinkUp(_) => todo!(), Urc::EthernetLinkDown(_) => todo!(), - Urc::NetworkUp(_) => {} - Urc::NetworkDown(_) => {} + Urc::NetworkUp(NetworkUp { interface_id }) => { + self.network_status_callback(interface_id).await?; + } + Urc::NetworkDown(NetworkDown { interface_id }) => { + self.network_status_callback(interface_id).await?; + } Urc::NetworkError(_) => todo!(), - Urc::PingResponse(_) => todo!(), - Urc::PingErrorResponse(_) => todo!(), + Urc::PingResponse(resp) => self.rx(SocketRx::Ping(Ok(resp.ip))).await, + Urc::PingErrorResponse(_) => self.rx(SocketRx::Ping(Err(()))).await, } + Ok(()) + } + + async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { + let NetworkStatusResponse { + status: NetworkStatus::InterfaceType(InterfaceType::WifiStation), + .. + } = self + .at.send_edm(GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::InterfaceType, + }) + .await? else { + return Err(Error::Network); + }; + + let NetworkStatusResponse { + status: NetworkStatus::Gateway(ipv4), + .. + } = self + .at.send_edm(GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::Gateway, + }) + .await? else { + return Err(Error::Network); + }; + + let ipv4_up = core::str::from_utf8(ipv4.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + let NetworkStatusResponse { + status: NetworkStatus::IPv6LinkLocalAddress(ipv6), + .. + } = self + .at.send_edm(GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6LinkLocalAddress, + }) + .await? else { + return Err(Error::Network); + }; + + let ipv6_up = core::str::from_utf8(ipv6.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + // Use `ipv4_up` & `ipv6_up` to determine link state + if let Some(ref mut con) = self.wifi_connection { + con.network_up = ipv4_up && ipv6_up; + } + + self.is_link_up().await?; + + Ok(()) } } diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs new file mode 100644 index 0000000..e402dd3 --- /dev/null +++ b/src/asynch/ublox_stack/dns.rs @@ -0,0 +1,72 @@ +//! DNS client compatible with the `embedded-nal-async` traits. +//! +//! This exists only for compatibility with crates that use `embedded-nal-async`. +//! Prefer using [`Stack::dns_query`](crate::Stack::dns_query) directly if you're +//! not using `embedded-nal-async`. + +use no_std_net::IpAddr; + +use super::UbloxStack; + +/// Errors returned by DnsSocket. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Error { + /// Invalid name + InvalidName, + /// Name too long + NameTooLong, + /// Name lookup failed + Failed, +} + +/// DNS client compatible with the `embedded-nal-async` traits. +/// +/// This exists only for compatibility with crates that use `embedded-nal-async`. +/// Prefer using [`Stack::dns_query`](crate::Stack::dns_query) directly if you're +/// not using `embedded-nal-async`. +pub struct DnsSocket<'a> { + stack: &'a UbloxStack, +} + +impl<'a> DnsSocket<'a> { + /// Create a new DNS socket using the provided stack. + /// + /// NOTE: If using DHCP, make sure it has reconfigured the stack to ensure the DNS servers are updated. + pub fn new(stack: &'a UbloxStack) -> Self { + Self { stack } + } + + /// Make a query for a given name and return the corresponding IP addresses. + pub async fn query( + &self, + name: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + let mut addrs = self.stack.dns_query(name, addr_type).await?; + if addrs.len() < 1 { + return Err(Error::Failed); + } + Ok(addrs.swap_remove(0)) + } +} + +// #[cfg(all(feature = "unstable-traits", feature = "nightly"))] +impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { + type Error = Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + self.query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: embedded_nal_async::IpAddr, + ) -> Result, Self::Error> { + todo!() + } +} diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index 67366f9..4d05d4c 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -1,22 +1,30 @@ -// #[cfg(feature = "socket-tcp")] -// pub mod tcp; +#[cfg(feature = "socket-tcp")] +pub mod tcp; // #[cfg(feature = "socket-udp")] // pub mod udp; +pub mod dns; + use core::cell::RefCell; use core::future::poll_fn; use core::task::{Context, Poll}; -use crate::command::edm::types::{ChannelId, IPv4ConnectEvent, IPv6ConnectEvent}; +use crate::command::edm::types::{IPv4ConnectEvent, IPv6ConnectEvent, Protocol}; +use crate::peer_builder::PeerUrlBuilder; -use super::MTU; +use super::{channel, MTU}; -use ch::driver::{Driver, LinkState, RxToken}; -use embassy_net_driver_channel as ch; +use super::channel::driver::{Driver, LinkState, RxToken, TxToken}; use embassy_sync::waitqueue::WakerRegistration; -use embassy_time::Instant; +use embassy_time::Timer; +use embedded_nal_async::SocketAddr; +use futures::{pin_mut, Future}; +use heapless::Vec; +use no_std_net::IpAddr; use serde::{Deserialize, Serialize}; -use ublox_sockets::{SocketSet, SocketStorage}; +use ublox_sockets::{ + AnySocket, ChannelId, PeerHandle, Socket, SocketHandle, SocketSet, SocketStorage, TcpState, +}; pub struct StackResources { sockets: [SocketStorage<'static>; SOCK], @@ -36,18 +44,21 @@ pub struct UbloxStack { } struct Inner { - device: ch::Device<'static, MTU>, + device: channel::Device<'static, MTU>, link_up: bool, + dns_result: Option>, + dns_waker: WakerRegistration, } pub(crate) struct SocketStack { pub(crate) sockets: SocketSet<'static>, pub(crate) waker: WakerRegistration, + dropped_sockets: heapless::Vec, } impl UbloxStack { pub fn new( - device: ch::Device<'static, MTU>, + device: channel::Device<'static, MTU>, resources: &'static mut StackResources, ) -> Self { let sockets = SocketSet::new(&mut resources.sockets[..]); @@ -55,11 +66,14 @@ impl UbloxStack { let socket = SocketStack { sockets, waker: WakerRegistration::new(), + dropped_sockets: heapless::Vec::new(), }; let inner = Inner { device, link_up: false, + dns_result: None, + dns_waker: WakerRegistration::new(), }; Self { @@ -68,6 +82,7 @@ impl UbloxStack { } } + #[allow(dead_code)] fn with(&self, f: impl FnOnce(&SocketStack, &Inner) -> R) -> R { f(&*self.socket.borrow(), &*self.inner.borrow()) } @@ -87,28 +102,67 @@ impl UbloxStack { .await; unreachable!() } + + /// Make a query for a given name and return the corresponding IP addresses. + // #[cfg(feature = "dns")] + pub async fn dns_query( + &self, + name: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result, dns::Error> { + // For A and AAAA queries we try detect whether `name` is just an IP address + match addr_type { + embedded_nal_async::AddrType::IPv4 => { + if let Ok(ip) = name.parse().map(IpAddr::V4) { + return Ok([ip].into_iter().collect()); + } + } + embedded_nal_async::AddrType::IPv6 => { + if let Ok(ip) = name.parse().map(IpAddr::V6) { + return Ok([ip].into_iter().collect()); + } + } + _ => {} + } + + poll_fn(|cx| { + self.with_mut(|_s, i| { + i.dns_result = None; + Poll::Ready( + i.send_packet(cx, SocketTx::Dns(name)) + .map_err(|_| dns::Error::Failed), + ) + }) + }) + .await?; + + poll_fn(|cx| { + self.with_mut(|_s, i| match i.dns_result { + Some(Ok(ip)) => Poll::Ready(Ok([ip].into_iter().collect())), + Some(Err(_)) => Poll::Ready(Err(dns::Error::Failed)), + None => { + i.dns_waker.register(cx.waker()); + Poll::Pending + } + }) + }) + .await + } } impl Inner { fn poll(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) { s.waker.register(cx.waker()); - let timestamp = Instant::now(); - // let mut smoldev = DriverAdapter { - // cx: Some(cx), - // inner: &mut self.device, - // }; - // s.iface.poll(timestamp, &mut smoldev, &mut s.sockets); - - if let Some((rx, _)) = self.device.receive(cx) { - rx.consume(|a| { - s.sockets - .get_mut::(ublox_sockets::SocketHandle(0)) - .recv_slice(a) - }) - .ok(); - } + self.socket_rx(cx, s); + self.socket_tx(cx, s); + // Handle delayed close-by-drop here + while let Some(dropped_peer_handle) = s.dropped_sockets.pop() { + defmt::warn!("Handling dropped socket {}", dropped_peer_handle); + self.send_packet(cx, SocketTx::Disconnect(dropped_peer_handle)) + .ok(); + } // Update link up let old_link_up = self.link_up; self.link_up = self.device.link_state(cx) == LinkState::Up; @@ -117,45 +171,318 @@ impl Inner { if old_link_up != self.link_up { defmt::info!("link_up = {:?}", self.link_up); } + } + + fn socket_rx(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) { + while let Some((rx_token, _)) = self.device.receive(cx) { + if let Err(e) = rx_token.consume(|a| { + match postcard::from_bytes::(a)? { + SocketRx::Data(packet) => { + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) + if udp.edm_channel == Some(packet.edm_channel) + && udp.may_recv() => + { + udp.rx_enqueue_slice(&packet.payload); + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) + if tcp.edm_channel == Some(packet.edm_channel) + && tcp.may_recv() => + { + tcp.rx_enqueue_slice(&packet.payload); + break; + } + _ => {} + } + } + } + SocketRx::Ipv4Connect(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, s); + } + SocketRx::Ipv6Connect(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, s); + } + SocketRx::Disconnect(Disconnect::EdmChannel(channel_id)) => { + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.edm_channel == Some(channel_id) => { + udp.edm_channel = None; + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.edm_channel == Some(channel_id) => { + tcp.edm_channel = None; + break; + } + _ => {} + } + } + } + SocketRx::Disconnect(Disconnect::Peer(peer_handle)) => { + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.peer_handle == Some(peer_handle) => { + tcp.peer_handle = None; + udp.set_state(UdpState::TimeWait); + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.peer_handle == Some(peer_handle) => { + tcp.peer_handle = None; + tcp.set_state(TcpState::TimeWait); + break; + } + _ => {} + } + } + } + SocketRx::PeerHandle(socket_handle, peer_handle) => { + for (handle, socket) in s.sockets.iter_mut() { + if handle == socket_handle { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) => { + udp.peer_handle = Some(peer_handle); + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) => { + tcp.peer_handle = Some(peer_handle); + } + _ => {} + } + break; + } + } + } + SocketRx::Ping(res) => { + self.dns_result = Some(res); + self.dns_waker.wake(); + } + } + Ok::<_, postcard::Error>(()) + }) { + defmt::error!("Socket RX failed {:?}", e) + }; + } + } + + fn socket_tx(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) { + for (handle, socket) in s.sockets.iter_mut() { + // if !socket.egress_permitted(self.inner.now, |ip_addr| self.inner.has_neighbor(&ip_addr)) + // { + // continue; + // } + + let result = match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) => todo!(), + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) => { + let res = match tcp.poll() { + Ok(TcpState::Closed) => { + if let Some(addr) = tcp.remote_endpoint() { + let url = PeerUrlBuilder::new() + .address(&addr) + .set_local_port(tcp.local_port) + .tcp::<128>() + .unwrap(); - // if let Some(poll_at) = s.iface.poll_at(timestamp, &mut s.sockets) { - // let t = Timer::at(instant_from_smoltcp(poll_at)); - // pin_mut!(t); - // if t.poll(cx).is_ready() { - // cx.waker().wake_by_ref(); - // } - // } + let pkt = SocketTx::Connect(Connect { + socket_handle: handle, + url: &url, + }); + + self.send_packet(cx, pkt).and_then(|r| { + tcp.set_state(TcpState::SynSent); + Ok(r) + }) + } else { + Ok(()) + } + } + // We transmit data in all states where we may have data in the buffer, + // or the transmit half of the connection is still open. + Ok(TcpState::Established) + | Ok(TcpState::CloseWait) + | Ok(TcpState::LastAck) => { + if let Some(edm_channel) = tcp.edm_channel { + tcp.tx_dequeue(|payload| { + if payload.len() > 0 { + let pkt = SocketTx::Data(DataPacket { + edm_channel, + payload, + }); + + (payload.len(), self.send_packet(cx, pkt)) + } else { + (0, Ok(())) + } + }) + } else { + Ok(()) + } + } + Ok(TcpState::FinWait1) => { + let pkt = SocketTx::Disconnect(tcp.peer_handle.unwrap()); + self.send_packet(cx, pkt) + } + Ok(TcpState::Listen) => todo!(), + Ok(TcpState::SynReceived) => todo!(), + Err(_) => Err(()), + _ => Ok(()), + }; + + if let Some(poll_at) = tcp.poll_at() { + let t = Timer::at(poll_at); + pin_mut!(t); + if t.poll(cx).is_ready() { + cx.waker().wake_by_ref(); + } + } + + res + } + _ => Ok(()), + }; + + match result { + Err(_) => { + break; + } // Device buffer full. + Ok(()) => {} + } + } + } + + pub(crate) fn send_packet(&mut self, cx: &mut Context, pkt: SocketTx) -> Result<(), ()> { + let Some(tx_token) = self.device.transmit(cx) else { + return Err(()); + }; + + let len = postcard::experimental::serialized_size(&pkt).map_err(drop)?; + tx_token.consume(len, |tx_buf| { + postcard::to_slice(&pkt, tx_buf).map(drop).map_err(drop) + })?; + + Ok(()) + } + + fn connect_event( + channel_id: ChannelId, + protocol: Protocol, + endpoint: SocketAddr, + s: &mut SocketStack, + ) { + for (_handle, socket) in s.sockets.iter_mut() { + match protocol { + #[cfg(feature = "socket-tcp")] + Protocol::TCP => match ublox_sockets::tcp::Socket::downcast_mut(socket) { + Some(tcp) if tcp.remote_endpoint == Some(endpoint) => { + tcp.edm_channel = Some(channel_id); + tcp.set_state(TcpState::Established); + break; + } + _ => {} + }, + #[cfg(feature = "socket-udp")] + Protocol::UDP => match ublox_sockets::udp::Socket::downcast_mut(socket) { + Some(udp) if udp.remote_endpoint == Some(endpoint) => { + udp.edm_channel = Some(channel_id); + udp.set_state(ublox_sockets::UdpState::Established); + break; + } + _ => {} + }, + _ => {} + } + } } } #[derive(Serialize, Deserialize)] -pub enum SocketEvent<'a> { +pub enum SocketTx<'a> { #[serde(borrow)] Data(DataPacket<'a>), + #[serde(borrow)] + Connect(Connect<'a>), + Disconnect(PeerHandle), + Dns(&'a str), +} + +impl<'a> defmt::Format for SocketTx<'a> { + fn format(&self, fmt: defmt::Formatter) { + match self { + SocketTx::Data(_) => defmt::write!(fmt, "SocketTx::Data"), + SocketTx::Connect(_) => defmt::write!(fmt, "SocketTx::Connect"), + SocketTx::Disconnect(_) => defmt::write!(fmt, "SocketTx::Disconnect"), + SocketTx::Dns(_) => defmt::write!(fmt, "SocketTx::Dns"), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct Connect<'a> { + pub url: &'a str, + pub socket_handle: SocketHandle, +} + +#[derive(Serialize, Deserialize)] +pub enum SocketRx<'a> { + #[serde(borrow)] + Data(DataPacket<'a>), + PeerHandle(SocketHandle, PeerHandle), Ipv4Connect(IPv4ConnectEvent), Ipv6Connect(IPv6ConnectEvent), - Disconnect(ChannelId), + Disconnect(Disconnect), + Ping(Result), +} + +#[derive(Serialize, Deserialize)] +pub enum Disconnect { + EdmChannel(ChannelId), + Peer(PeerHandle), +} + +impl<'a> defmt::Format for SocketRx<'a> { + fn format(&self, fmt: defmt::Formatter) { + match self { + SocketRx::Data(_) => defmt::write!(fmt, "SocketRx::Data"), + SocketRx::PeerHandle(_, _) => defmt::write!(fmt, "SocketRx::PeerHandle"), + SocketRx::Ipv4Connect(_) => defmt::write!(fmt, "SocketRx::Ipv4Connect"), + SocketRx::Ipv6Connect(_) => defmt::write!(fmt, "SocketRx::Ipv6Connect"), + SocketRx::Disconnect(_) => defmt::write!(fmt, "SocketRx::Disconnect"), + SocketRx::Ping(_) => defmt::write!(fmt, "SocketRx::Ping"), + } + } } -impl From for SocketEvent<'_> { +impl From for SocketRx<'_> { fn from(value: IPv4ConnectEvent) -> Self { Self::Ipv4Connect(value) } } -impl From for SocketEvent<'_> { +impl From for SocketRx<'_> { fn from(value: IPv6ConnectEvent) -> Self { Self::Ipv6Connect(value) } } -impl From for SocketEvent<'_> { +impl From for SocketRx<'_> { fn from(value: ChannelId) -> Self { - Self::Disconnect(value) + Self::Disconnect(Disconnect::EdmChannel(value)) } } -impl<'a> From> for SocketEvent<'a> { +impl<'a> From> for SocketRx<'a> { fn from(value: DataPacket<'a>) -> Self { Self::Data(value) } diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index 6f5338e..db7d242 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -3,9 +3,8 @@ use core::future::poll_fn; use core::mem; use core::task::Poll; -use embassy_time::Duration; use embedded_nal_async::SocketAddr; -use ublox_sockets::{tcp, SocketHandle}; +use ublox_sockets::{tcp, SocketHandle, TcpState}; use super::{SocketStack, UbloxStack}; @@ -93,22 +92,17 @@ impl<'a> TcpSocket<'a> { where T: Into, { - match { - self.io - .with_mut(|s, i| s.connect(i.context(), remote_endpoint)) - } { + match { self.io.with_mut(|s| s.connect(remote_endpoint, None)) } { Ok(()) => {} Err(_) => return Err(ConnectError::InvalidState), // Err(tcp::ConnectError::Unaddressable) => return Err(ConnectError::NoRoute), } poll_fn(|cx| { - self.io.with_mut(|s, _| match s.state() { - tcp::State::Closed | tcp::State::TimeWait => { - Poll::Ready(Err(ConnectError::ConnectionReset)) - } + self.io.with_mut(|s| match s.state() { + tcp::State::TimeWait => Poll::Ready(Err(ConnectError::ConnectionReset)), tcp::State::Listen => unreachable!(), - tcp::State::SynSent | tcp::State::SynReceived => { + tcp::State::Closed | tcp::State::SynSent | tcp::State::SynReceived => { s.register_send_waker(cx.waker()); Poll::Pending } @@ -118,27 +112,28 @@ impl<'a> TcpSocket<'a> { .await } - pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> - where - T: Into, - { - match self.io.with_mut(|s, _| s.listen(local_endpoint)) { - Ok(()) => {} - Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), - Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), - } - - poll_fn(|cx| { - self.io.with_mut(|s, _| match s.state() { - tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { - s.register_send_waker(cx.waker()); - Poll::Pending - } - _ => Poll::Ready(Ok(())), - }) - }) - .await - } + // FIXME: + // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + // where + // T: Into, + // { + // match self.io.with_mut(|s, _| s.listen(local_endpoint)) { + // Ok(()) => {} + // Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), + // Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), + // } + + // poll_fn(|cx| { + // self.io.with_mut(|s, _| match s.state() { + // tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + // s.register_send_waker(cx.waker()); + // Poll::Pending + // } + // _ => Poll::Ready(Ok(())), + // }) + // }) + // .await + // } pub async fn read(&mut self, buf: &mut [u8]) -> Result { self.io.read(buf).await @@ -152,45 +147,55 @@ impl<'a> TcpSocket<'a> { self.io.flush().await } - pub fn set_timeout(&mut self, duration: Option) { - self.io.with_mut(|s, _| s.set_timeout(duration)) - } + // pub fn set_timeout(&mut self, duration: Option) { + // self.io.with_mut(|s| s.set_timeout(duration)) + // } - pub fn set_keep_alive(&mut self, interval: Option) { - self.io.with_mut(|s, _| s.set_keep_alive(interval)) - } + // pub fn set_keep_alive(&mut self, interval: Option) { + // self.io.with_mut(|s| s.set_keep_alive(interval)) + // } // pub fn local_endpoint(&self) -> Option { // self.io.with(|s, _| s.local_endpoint()) // } - // pub fn remote_endpoint(&self) -> Option { - // self.io.with(|s, _| s.remote_endpoint()) - // } + pub fn remote_endpoint(&self) -> Option { + self.io.with(|s| s.remote_endpoint()) + } pub fn state(&self) -> tcp::State { - self.io.with(|s, _| s.state()) + self.io.with(|s| s.state()) } pub fn close(&mut self) { - self.io.with_mut(|s, _| s.close()) + self.io.with_mut(|s| s.close()) } pub fn abort(&mut self) { - self.io.with_mut(|s, _| s.abort()) + self.io.with_mut(|s| s.abort()) } pub fn may_send(&self) -> bool { - self.io.with(|s, _| s.may_send()) + self.io.with(|s| s.may_send()) } pub fn may_recv(&self) -> bool { - self.io.with(|s, _| s.may_recv()) + self.io.with(|s| s.may_recv()) } } impl<'a> Drop for TcpSocket<'a> { fn drop(&mut self) { + if matches!(self.state(), TcpState::Listen | TcpState::Established) { + if let Some(peer_handle) = self.io.with(|s| s.peer_handle) { + self.io + .stack + .borrow_mut() + .dropped_sockets + .push(peer_handle) + .ok(); + } + } self.io.stack.borrow_mut().sockets.remove(self.io.handle); } } @@ -204,16 +209,16 @@ struct TcpIo<'a> { } impl<'d> TcpIo<'d> { - fn with(&self, f: impl FnOnce(&tcp::Socket, &Interface) -> R) -> R { + fn with(&self, f: impl FnOnce(&tcp::Socket) -> R) -> R { let s = &*self.stack.borrow(); - let socket = s.sockets.get::(self.handle).unwrap(); - f(socket, &s.iface) + let socket = s.sockets.get::(self.handle); + f(socket) } - fn with_mut(&mut self, f: impl FnOnce(&mut tcp::Socket, &mut Interface) -> R) -> R { + fn with_mut(&mut self, f: impl FnOnce(&mut tcp::Socket) -> R) -> R { let s = &mut *self.stack.borrow_mut(); let socket = s.sockets.get_mut::(self.handle); - let res = f(socket, &mut s.iface); + let res = f(socket); s.waker.wake(); res } @@ -222,7 +227,7 @@ impl<'d> TcpIo<'d> { poll_fn(move |cx| { // CAUTION: smoltcp semantics around EOF are different to what you'd expect // from posix-like IO, so we have to tweak things here. - self.with_mut(|s, _| match s.recv_slice(buf) { + self.with_mut(|s| match s.recv_slice(buf) { // No data ready Ok(0) => { s.register_recv_waker(cx.waker()); @@ -231,9 +236,11 @@ impl<'d> TcpIo<'d> { // Data ready! Ok(n) => Poll::Ready(Ok(n)), // EOF - Err(tcp::RecvError::Finished) => Poll::Ready(Ok(0)), + Err(_) => Poll::Ready(Ok(0)), + // FIXME: + // Err(tcp::RecvError::Finished) => Poll::Ready(Ok(0)), // Connection reset. TODO: this can also be timeouts etc, investigate. - Err(tcp::RecvError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + // Err(tcp::RecvError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), }) }) .await @@ -241,7 +248,7 @@ impl<'d> TcpIo<'d> { async fn write(&mut self, buf: &[u8]) -> Result { poll_fn(move |cx| { - self.with_mut(|s, _| match s.send_slice(buf) { + self.with_mut(|s| match s.send_slice(buf) { // Not ready to send (no space in the tx buffer) Ok(0) => { s.register_send_waker(cx.waker()); @@ -250,7 +257,9 @@ impl<'d> TcpIo<'d> { // Some data sent Ok(n) => Poll::Ready(Ok(n)), // Connection reset. TODO: this can also be timeouts etc, investigate. - Err(tcp::SendError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), + Err(_) => Poll::Ready(Err(Error::ConnectionReset)), + // FIXME: + // Err(tcp::SendError::InvalidState) => Poll::Ready(Err(Error::ConnectionReset)), }) }) .await @@ -258,7 +267,7 @@ impl<'d> TcpIo<'d> { async fn flush(&mut self) -> Result<(), Error> { poll_fn(move |cx| { - self.with_mut(|s, _| { + self.with_mut(|s| { // If there are outstanding send operations, register for wake up and wait // smoltcp issues wake-ups when octets are dequeued from the send buffer if s.send_queue() > 0 { @@ -275,221 +284,225 @@ impl<'d> TcpIo<'d> { } // #[cfg(feature = "nightly")] -// mod embedded_io_impls { -// use super::*; - -// impl embedded_io::Error for ConnectError { -// fn kind(&self) -> embedded_io::ErrorKind { -// embedded_io::ErrorKind::Other -// } -// } - -// impl embedded_io::Error for Error { -// fn kind(&self) -> embedded_io::ErrorKind { -// embedded_io::ErrorKind::Other -// } -// } - -// impl<'d> embedded_io::Io for TcpSocket<'d> { -// type Error = Error; -// } - -// impl<'d> embedded_io::asynch::Read for TcpSocket<'d> { -// async fn read(&mut self, buf: &mut [u8]) -> Result { -// self.io.read(buf).await -// } -// } - -// impl<'d> embedded_io::asynch::Write for TcpSocket<'d> { -// async fn write(&mut self, buf: &[u8]) -> Result { -// self.io.write(buf).await -// } - -// async fn flush(&mut self) -> Result<(), Self::Error> { -// self.io.flush().await -// } -// } - -// impl<'d> embedded_io::Io for TcpReader<'d> { -// type Error = Error; -// } - -// impl<'d> embedded_io::asynch::Read for TcpReader<'d> { -// async fn read(&mut self, buf: &mut [u8]) -> Result { -// self.io.read(buf).await -// } -// } - -// impl<'d> embedded_io::Io for TcpWriter<'d> { -// type Error = Error; -// } - -// impl<'d> embedded_io::asynch::Write for TcpWriter<'d> { -// async fn write(&mut self, buf: &[u8]) -> Result { -// self.io.write(buf).await -// } - -// async fn flush(&mut self) -> Result<(), Self::Error> { -// self.io.flush().await -// } -// } -// } +mod embedded_io_impls { + use super::*; + + impl embedded_io::Error for ConnectError { + fn kind(&self) -> embedded_io::ErrorKind { + embedded_io::ErrorKind::Other + } + } + + impl embedded_io::Error for Error { + fn kind(&self) -> embedded_io::ErrorKind { + embedded_io::ErrorKind::Other + } + } + + impl<'d> embedded_io::Io for TcpSocket<'d> { + type Error = Error; + } + + impl<'d> embedded_io::asynch::Read for TcpSocket<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + } + + impl<'d> embedded_io::asynch::Write for TcpSocket<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.io.flush().await + } + } + + impl<'d> embedded_io::Io for TcpReader<'d> { + type Error = Error; + } + + impl<'d> embedded_io::asynch::Read for TcpReader<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.io.read(buf).await + } + } + + impl<'d> embedded_io::Io for TcpWriter<'d> { + type Error = Error; + } + + impl<'d> embedded_io::asynch::Write for TcpWriter<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.io.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.io.flush().await + } + } +} // #[cfg(all(feature = "unstable-traits", feature = "nightly"))] -// pub mod client { -// use core::cell::UnsafeCell; -// use core::mem::MaybeUninit; -// use core::ptr::NonNull; - -// use atomic_polyfill::{AtomicBool, Ordering}; -// use embedded_nal_async::IpAddr; - -// use super::*; - -// /// TCP client capable of creating up to N multiple connections with tx and rx buffers according to TX_SZ and RX_SZ. -// pub struct TcpClient<'d, D: Driver, const N: usize, const TX_SZ: usize = 1024, const RX_SZ: usize = 1024> { -// stack: &'d UbloxStack, -// state: &'d TcpClientState, -// } - -// impl<'d, D: Driver, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpClient<'d, D, N, TX_SZ, RX_SZ> { -// /// Create a new TcpClient -// pub fn new(stack: &'d UbloxStack, state: &'d TcpClientState) -> Self { -// Self { stack, state } -// } -// } - -// impl<'d, D: Driver, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_nal_async::TcpConnect -// for TcpClient<'d, D, N, TX_SZ, RX_SZ> -// { -// type Error = Error; -// type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; - -// async fn connect<'a>( -// &'a self, -// remote: embedded_nal_async::SocketAddr, -// ) -> Result, Self::Error> -// where -// Self: 'a, -// { -// let addr: crate::IpAddress = match remote.ip() { -// IpAddr::V4(addr) => crate::IpAddress::Ipv4(crate::Ipv4Address::from_bytes(&addr.octets())), -// #[cfg(feature = "proto-ipv6")] -// IpAddr::V6(addr) => crate::IpAddress::Ipv6(crate::Ipv6Address::from_bytes(&addr.octets())), -// #[cfg(not(feature = "proto-ipv6"))] -// IpAddr::V6(_) => panic!("ipv6 support not enabled"), -// }; -// let remote_endpoint = (addr, remote.port()); -// let mut socket = TcpConnection::new(&self.stack, self.state)?; -// socket -// .socket -// .connect(remote_endpoint) -// .await -// .map_err(|_| Error::ConnectionReset)?; -// Ok(socket) -// } -// } - -// pub struct TcpConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { -// socket: TcpSocket<'d>, -// state: &'d TcpClientState, -// bufs: NonNull<([u8; TX_SZ], [u8; RX_SZ])>, -// } - -// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpConnection<'d, N, TX_SZ, RX_SZ> { -// fn new(stack: &'d UbloxStack, state: &'d TcpClientState) -> Result { -// let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; -// Ok(Self { -// socket: unsafe { TcpSocket::new(stack, &mut bufs.as_mut().1, &mut bufs.as_mut().0) }, -// state, -// bufs, -// }) -// } -// } - -// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop for TcpConnection<'d, N, TX_SZ, RX_SZ> { -// fn drop(&mut self) { -// unsafe { -// self.socket.close(); -// self.state.pool.free(self.bufs); -// } -// } -// } - -// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::Io -// for TcpConnection<'d, N, TX_SZ, RX_SZ> -// { -// type Error = Error; -// } - -// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Read -// for TcpConnection<'d, N, TX_SZ, RX_SZ> -// { -// async fn read(&mut self, buf: &mut [u8]) -> Result { -// self.socket.read(buf).await -// } -// } - -// impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Write -// for TcpConnection<'d, N, TX_SZ, RX_SZ> -// { -// async fn write(&mut self, buf: &[u8]) -> Result { -// self.socket.write(buf).await -// } - -// async fn flush(&mut self) -> Result<(), Self::Error> { -// self.socket.flush().await -// } -// } - -// /// State for TcpClient -// pub struct TcpClientState { -// pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, -// } - -// impl TcpClientState { -// pub const fn new() -> Self { -// Self { pool: Pool::new() } -// } -// } - -// unsafe impl Sync for TcpClientState {} - -// struct Pool { -// used: [AtomicBool; N], -// data: [UnsafeCell>; N], -// } - -// impl Pool { -// const VALUE: AtomicBool = AtomicBool::new(false); -// const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); - -// const fn new() -> Self { -// Self { -// used: [Self::VALUE; N], -// data: [Self::UNINIT; N], -// } -// } -// } - -// impl Pool { -// fn alloc(&self) -> Option> { -// for n in 0..N { -// if self.used[n].swap(true, Ordering::SeqCst) == false { -// let p = self.data[n].get() as *mut T; -// return Some(unsafe { NonNull::new_unchecked(p) }); -// } -// } -// None -// } - -// /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. -// unsafe fn free(&self, p: NonNull) { -// let origin = self.data.as_ptr() as *mut T; -// let n = p.as_ptr().offset_from(origin); -// assert!(n >= 0); -// assert!((n as usize) < N); -// self.used[n as usize].store(false, Ordering::SeqCst); -// } -// } -// } +pub mod client { + use core::cell::UnsafeCell; + use core::mem::MaybeUninit; + use core::ptr::NonNull; + + use atomic_polyfill::{AtomicBool, Ordering}; + + use super::*; + + /// TCP client capable of creating up to N multiple connections with tx and rx buffers according to TX_SZ and RX_SZ. + pub struct TcpClient<'d, const N: usize, const TX_SZ: usize = 1024, const RX_SZ: usize = 1024> { + pub(crate) stack: &'d UbloxStack, + pub(crate) state: &'d TcpClientState, + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpClient<'d, N, TX_SZ, RX_SZ> { + /// Create a new TcpClient + pub fn new(stack: &'d UbloxStack, state: &'d TcpClientState) -> Self { + Self { stack, state } + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_nal_async::TcpConnect + for TcpClient<'d, N, TX_SZ, RX_SZ> + { + type Error = Error; + type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; + + async fn connect<'a>( + &'a self, + remote: embedded_nal_async::SocketAddr, + ) -> Result, Self::Error> + where + Self: 'a, + { + let remote_endpoint = (remote.ip(), remote.port()); + let mut socket = TcpConnection::new(&self.stack, self.state)?; + socket + .socket + .connect(remote_endpoint) + .await + .map_err(|_| Error::ConnectionReset)?; + Ok(socket) + } + } + + pub struct TcpConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { + socket: TcpSocket<'d>, + state: &'d TcpClientState, + bufs: NonNull<([u8; TX_SZ], [u8; RX_SZ])>, + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> + TcpConnection<'d, N, TX_SZ, RX_SZ> + { + fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + ) -> Result { + let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; + Ok(Self { + socket: unsafe { + TcpSocket::new(stack, &mut bufs.as_mut().1, &mut bufs.as_mut().0) + }, + state, + bufs, + }) + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + fn drop(&mut self) { + unsafe { + self.socket.close(); + self.state.pool.free(self.bufs); + } + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::Io + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + type Error = Error; + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Read + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.socket.read(buf).await + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Write + for TcpConnection<'d, N, TX_SZ, RX_SZ> + { + async fn write(&mut self, buf: &[u8]) -> Result { + self.socket.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.socket.flush().await + } + } + + /// State for TcpClient + pub struct TcpClientState { + pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, + } + + impl TcpClientState { + pub const fn new() -> Self { + Self { pool: Pool::new() } + } + } + + unsafe impl Sync + for TcpClientState + { + } + + struct Pool { + used: [AtomicBool; N], + data: [UnsafeCell>; N], + } + + impl Pool { + const VALUE: AtomicBool = AtomicBool::new(false); + const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); + + const fn new() -> Self { + Self { + used: [Self::VALUE; N], + data: [Self::UNINIT; N], + } + } + } + + impl Pool { + fn alloc(&self) -> Option> { + for n in 0..N { + if self.used[n].swap(true, Ordering::SeqCst) == false { + let p = self.data[n].get() as *mut T; + return Some(unsafe { NonNull::new_unchecked(p) }); + } + } + None + } + + /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. + unsafe fn free(&self, p: NonNull) { + let origin = self.data.as_ptr() as *mut T; + let n = p.as_ptr().offset_from(origin); + assert!(n >= 0); + assert!((n as usize) < N); + self.used[n as usize].store(false, Ordering::SeqCst); + } + } +} diff --git a/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs index 18062e9..5aa156d 100644 --- a/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -7,8 +7,9 @@ use atat::atat_derive::AtatCmd; use heapless::String; use responses::*; use types::*; +use ublox_sockets::PeerHandle; -use super::{NoResponse, PeerHandle}; +use super::NoResponse; /// 5.1 Enter data mode O /// diff --git a/src/command/data_mode/responses.rs b/src/command/data_mode/responses.rs index 3f64833..0c8832c 100644 --- a/src/command/data_mode/responses.rs +++ b/src/command/data_mode/responses.rs @@ -1,7 +1,7 @@ //! Responses for Data Mode -use super::PeerHandle; use atat::atat_derive::AtatResp; use heapless::String; +use ublox_sockets::PeerHandle; /// 5.2 Connect peer +UDCP #[derive(Clone, AtatResp)] diff --git a/src/command/data_mode/types.rs b/src/command/data_mode/types.rs index 9bbc5f6..6db7c29 100644 --- a/src/command/data_mode/types.rs +++ b/src/command/data_mode/types.rs @@ -5,6 +5,7 @@ use heapless::String; use crate::command::OnOff; #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum Mode { /// Command mode @@ -19,6 +20,7 @@ pub enum Mode { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ConnectScheme { /// Always connected - Keep the peer connected when not in command mode. @@ -38,11 +40,13 @@ pub enum ConnectScheme { Both = 0b110, } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ServerConfig { Type(ServerType), Url(String<128>), } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ServerType { #[at_arg(value = 0)] Disabled, @@ -63,6 +67,7 @@ pub enum ServerType { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum Interface { TCP = 1, @@ -75,6 +80,7 @@ pub enum Interface { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum UDPBehaviour { /// No connect. This will trigger an +UUDPC URC immediately (with @@ -90,6 +96,7 @@ pub enum UDPBehaviour { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ImmediateFlush { Disable = 0, @@ -97,6 +104,7 @@ pub enum ImmediateFlush { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum IPVersion { /// Default @@ -105,6 +113,7 @@ pub enum IPVersion { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum RemoteConfiguration { Disable = 0, @@ -112,6 +121,7 @@ pub enum RemoteConfiguration { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum WatchdogSetting { /// SPP (and all SPP based protocols like DUN) write timeout: is the time in @@ -158,6 +168,7 @@ pub enum WatchdogSetting { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum PeerConfigParameter { /// Keep remote peer in the command mode /// - Off: Disconnect peers when entering the command mode @@ -218,6 +229,7 @@ pub enum PeerConfigParameter { } #[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum ConnectionType { Bluetooth = 1, @@ -236,6 +248,7 @@ pub enum ConnectionType { // } #[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum IPProtocol { TCP = 0, diff --git a/src/command/data_mode/urc.rs b/src/command/data_mode/urc.rs index cdda3af..35eb098 100644 --- a/src/command/data_mode/urc.rs +++ b/src/command/data_mode/urc.rs @@ -1,12 +1,12 @@ //! Unsolicited responses for Data mode Commands -use crate::command::PeerHandle; - use super::types::*; use atat::atat_derive::AtatResp; use atat::heapless_bytes::Bytes; +use ublox_sockets::PeerHandle; /// 5.10 Peer connected +UUDPC #[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PeerConnected { #[at_arg(position = 0)] pub handle: PeerHandle, @@ -17,12 +17,14 @@ pub struct PeerConnected { // #[at_arg(position = 3)] // pub local_address: IpAddr, #[at_arg(position = 3)] + #[defmt(Debug2Format)] pub local_address: Bytes<40>, #[at_arg(position = 4)] pub local_port: u16, // #[at_arg(position = 5)] // pub remote_address: IpAddr, #[at_arg(position = 5)] + #[defmt(Debug2Format)] pub remote_address: Bytes<40>, #[at_arg(position = 6)] pub remote_port: u16, diff --git a/src/command/edm/mod.rs b/src/command/edm/mod.rs index 712a0a5..64021f1 100644 --- a/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -11,6 +11,7 @@ use crate::command::{NoResponse, Urc}; use atat::AtatCmd; use heapless::Vec; use types::*; +use ublox_sockets::ChannelId; pub(crate) fn calc_payload_len(resp: &[u8]) -> usize { (u16::from_be_bytes(resp[1..3].try_into().unwrap()) & EDM_FULL_SIZE_FILTER) as usize diff --git a/src/command/edm/types.rs b/src/command/edm/types.rs index d40025b..08690cf 100644 --- a/src/command/edm/types.rs +++ b/src/command/edm/types.rs @@ -1,28 +1,11 @@ -use atat::atat_derive::AtatLen; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::Vec; +use no_std_net::{Ipv4Addr, Ipv6Addr}; use serde::{Deserialize, Serialize}; +use ublox_sockets::ChannelId; /// Start byte, Length: u16, Id+Type: u16, Endbyte // type EdmAtCmdOverhead = (u8, u16, u16, u8); -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - AtatLen, - Ord, - Default, - Serialize, - Deserialize, - defmt::Format, - hash32_derive::Hash32, -)] -pub struct ChannelId(pub u8); - pub const DATA_PACKAGE_SIZE: usize = 4096; pub const STARTBYTE: u8 = 0xAA; pub const ENDBYTE: u8 = 0x55; diff --git a/src/command/edm/urc.rs b/src/command/edm/urc.rs index f1d0ed9..bc011a3 100644 --- a/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -3,8 +3,9 @@ use super::types::*; use super::Urc; use atat::helpers::LossyStr; use atat::AtatUrc; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::Vec; +use no_std_net::{Ipv4Addr, Ipv6Addr}; +use ublox_sockets::ChannelId; #[derive(Debug, Clone, PartialEq)] pub enum EdmEvent { @@ -166,10 +167,9 @@ impl AtatUrc for EdmEvent { #[cfg(test)] mod test { use super::*; - use crate::command::{ - data_mode::urc::PeerConnected, edm::types::DATA_PACKAGE_SIZE, PeerHandle, Urc, - }; + use crate::command::{data_mode::urc::PeerConnected, edm::types::DATA_PACKAGE_SIZE, Urc}; use atat::{heapless::Vec, heapless_bytes::Bytes, AtatUrc}; + use ublox_sockets::PeerHandle; #[test] fn parse_at_urc() { diff --git a/src/command/ethernet/types.rs b/src/command/ethernet/types.rs index eca3ffe..8c15549 100644 --- a/src/command/ethernet/types.rs +++ b/src/command/ethernet/types.rs @@ -1,7 +1,7 @@ //! Argument and parameter types used by Ethernet Commands and Responses use atat::atat_derive::AtatEnum; -use embedded_nal::Ipv4Addr; +use no_std_net::Ipv4Addr; use crate::command::OnOff; diff --git a/src/command/gpio/mod.rs b/src/command/gpio/mod.rs index a5d4ab5..b73c4f2 100644 --- a/src/command/gpio/mod.rs +++ b/src/command/gpio/mod.rs @@ -41,7 +41,7 @@ pub struct ReadGPIO { /// Writes the value of an enabled GPIO pin configured as output. /// Supported by ODIN-W2 from software version 3.0.0 onwards only. #[derive(Clone, AtatCmd)] -#[at_cmd("+UGPIOW", NoResponse, value_sep = false, timeout_ms = 1000)] +#[at_cmd("+UGPIOW", NoResponse, timeout_ms = 1000)] pub struct WriteGPIO { #[at_arg(position = 0)] pub id: GPIOId, diff --git a/src/command/mod.rs b/src/command/mod.rs index 67470da..bb12501 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -13,8 +13,7 @@ pub mod security; pub mod system; pub mod wifi; -use atat::atat_derive::{AtatCmd, AtatEnum, AtatLen, AtatResp, AtatUrc}; -use serde::{Deserialize, Serialize}; +use atat::atat_derive::{AtatCmd, AtatEnum, AtatResp, AtatUrc}; #[derive(Debug, Clone, AtatResp, PartialEq)] pub struct NoResponse; @@ -23,23 +22,6 @@ pub struct NoResponse; #[at_cmd("", NoResponse, timeout_ms = 1000)] pub struct AT; -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - PartialOrd, - AtatLen, - Ord, - Default, - Serialize, - Deserialize, - defmt::Format, - hash32_derive::Hash32, -)] -pub struct PeerHandle(pub u8); - #[derive(Debug, PartialEq, Clone, AtatUrc)] pub enum Urc { /// Startup Message @@ -91,6 +73,7 @@ pub enum Urc { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum OnOff { On = 1, diff --git a/src/command/network/types.rs b/src/command/network/types.rs index d27d7cc..97c8a4a 100644 --- a/src/command/network/types.rs +++ b/src/command/network/types.rs @@ -31,7 +31,7 @@ pub enum NetworkStatus { /// 101: The is the currently used IPv4_Addr (omitted if no IP address has /// been acquired). #[at_arg(value = 101)] - IPv4Address(#[at_arg(len = 16)] Bytes<40>), + IPv4Address(#[at_arg(len = 16)] Bytes<16>), /// 102: The is the currently used subnet mask (omitted if no IP address /// has been acquired). #[at_arg(value = 102)] diff --git a/src/command/ping/urc.rs b/src/command/ping/urc.rs index 28231cf..856d63c 100644 --- a/src/command/ping/urc.rs +++ b/src/command/ping/urc.rs @@ -1,8 +1,8 @@ //! Responses for Ping Commands use super::types::*; use atat::atat_derive::AtatResp; -use embedded_nal::IpAddr; use heapless::String; +use no_std_net::IpAddr; /// 16.1 Ping command +UPING /// /// The ping command is the common method to know if a remote host is reachable on the Internet. @@ -24,7 +24,6 @@ use heapless::String; /// in another way. #[derive(Debug, PartialEq, Clone, AtatResp)] pub struct PingResponse { - /// Text string that identifies the serial number. #[at_arg(position = 0)] pub retrynum: u32, #[at_arg(position = 1)] diff --git a/src/command/wifi/types.rs b/src/command/wifi/types.rs index 57b39b9..5772825 100644 --- a/src/command/wifi/types.rs +++ b/src/command/wifi/types.rs @@ -3,8 +3,8 @@ use crate::command::OnOff; use atat::atat_derive::AtatEnum; use atat::heapless_bytes::Bytes; -use embedded_nal::{Ipv4Addr, Ipv6Addr}; use heapless::{String, Vec}; +use no_std_net::{Ipv4Addr, Ipv6Addr}; use serde::Deserialize; #[derive(Clone, PartialEq, AtatEnum)] diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 76cd509..0000000 --- a/src/config.rs +++ /dev/null @@ -1,61 +0,0 @@ -use heapless::String; - -#[derive(Debug)] -pub struct Config { - pub(crate) hostname: Option>, - pub(crate) tls_in_buffer_size: Option, - pub(crate) tls_out_buffer_size: Option, -} - -impl Default for Config { - fn default() -> Self { - Config { - hostname: None, - tls_in_buffer_size: None, - tls_out_buffer_size: None, - } - } -} - -impl Config { - pub fn new() -> Self { - Config { - hostname: None, - tls_in_buffer_size: None, - tls_out_buffer_size: None, - } - } - - pub fn with_hostname(self, hostname: &str) -> Self { - Config { - hostname: Some(String::from(hostname)), - ..self - } - } - - /// Experimental use of undocumented setting for TLS buffers - /// - /// For Odin: - /// Minimum is 512 and maximum is 16K (16384). - /// DEFAULT_TLS_IN_BUFFER_SIZE (7800) - pub fn tls_in_buffer_size(self, bytes: u16) -> Self { - assert!(bytes > 512); - Config { - tls_in_buffer_size: Some(bytes), - ..self - } - } - - /// Experimental use of undocumented setting for TLS buffers - /// - /// For Odin: - /// Minimum is 512 and maximum is 16K (16384). - /// DEFAULT_TLS_OUT_BUFFER_SIZE (3072) - pub fn tls_out_buffer_size(self, bytes: u16) -> Self { - assert!(bytes > 512); - Config { - tls_out_buffer_size: Some(bytes), - ..self - } - } -} diff --git a/src/wifi/connection.rs b/src/connection.rs similarity index 62% rename from src/wifi/connection.rs rename to src/connection.rs index b16eb23..e657393 100644 --- a/src/wifi/connection.rs +++ b/src/connection.rs @@ -1,4 +1,4 @@ -use crate::wifi::network::{WifiMode, WifiNetwork}; +use crate::network::{WifiMode, WifiNetwork}; #[derive(Debug, Clone, Copy, PartialEq, defmt::Format)] pub enum WiFiState { @@ -8,22 +8,13 @@ pub enum WiFiState { Connected, } -/// Describes whether device is connected to a network and has an IP or not. -/// It is possible to be attached to a network but have no Wifi connection. -#[derive(Debug, Clone, Copy, PartialEq, defmt::Format)] -pub enum NetworkState { - Attached, - AlmostAttached, - Unattached, -} - // Fold into wifi connectivity pub struct WifiConnection { /// Keeps track of connection state on module pub wifi_state: WiFiState, - pub network_state: NetworkState, + pub network_up: bool, pub network: WifiNetwork, - /// Numbre from 0-9. 255 used for unknown + /// Number from 0-9. 255 used for unknown pub config_id: u8, /// Keeps track of activation of the config by driver pub activated: bool, @@ -33,17 +24,13 @@ impl WifiConnection { pub(crate) fn new(network: WifiNetwork, wifi_state: WiFiState, config_id: u8) -> Self { WifiConnection { wifi_state, - network_state: NetworkState::Unattached, + network_up: false, network, config_id, activated: false, } } - pub(crate) fn is_connected(&self) -> bool { - self.network_state == NetworkState::Attached && self.wifi_state == WiFiState::Connected - } - pub fn is_station(&self) -> bool { self.network.mode == WifiMode::Station } diff --git a/src/error.rs b/src/error.rs index ac50ee1..8fe05fb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,6 +29,7 @@ pub enum Error { Supplicant, Timeout, ShadowStoreBug, + AlreadyConnected, _Unknown, } diff --git a/src/lib.rs b/src/lib.rs index 6508583..ccf77d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,16 +2,23 @@ #![cfg_attr(feature = "async", allow(incomplete_features))] #![cfg_attr(feature = "async", feature(generic_const_exprs))] #![cfg_attr(feature = "async", feature(async_fn_in_trait))] +#![cfg_attr(feature = "async", feature(impl_trait_projections))] #[cfg(feature = "async")] pub mod asynch; +#[cfg(feature = "async")] +pub use embedded_nal_async; + +mod connection; +mod network; +mod peer_builder; + // mod blocking; -// mod hex; +mod hex; pub use atat; pub mod command; -pub mod config; pub mod error; // pub mod wifi; diff --git a/src/wifi/network.rs b/src/network.rs similarity index 76% rename from src/wifi/network.rs rename to src/network.rs index df8e532..e980dc9 100644 --- a/src/wifi/network.rs +++ b/src/network.rs @@ -25,6 +25,22 @@ pub struct WifiNetwork { pub mode: WifiMode, } +impl WifiNetwork { + pub fn new_station(bssid: Bytes<20>, channel: u8) -> Self { + Self { + bssid, + op_mode: OperationMode::Infrastructure, + ssid: String::new(), + channel, + rssi: 1, + authentication_suites: 0, + unicast_ciphers: 0, + group_ciphers: 0, + mode: WifiMode::Station, + } + } +} + impl TryFrom for WifiNetwork { type Error = WifiError; diff --git a/src/wifi/peer_builder.rs b/src/peer_builder.rs similarity index 58% rename from src/wifi/peer_builder.rs rename to src/peer_builder.rs index f163a1b..8334b53 100644 --- a/src/wifi/peer_builder.rs +++ b/src/peer_builder.rs @@ -1,16 +1,14 @@ -use crate::{blocking::client::SecurityCredentials, error::Error}; +use crate::error::Error; use core::fmt::Write; -/// Handles receiving data from sockets -/// implements TCP and UDP for WiFi client -use embedded_nal::{IpAddr, SocketAddr}; use heapless::String; +use no_std_net::{IpAddr, SocketAddr}; #[derive(Default)] pub(crate) struct PeerUrlBuilder<'a> { hostname: Option<&'a str>, ip_addr: Option, port: Option, - creds: Option, + // creds: Option, local_port: Option, } @@ -20,7 +18,7 @@ impl<'a> PeerUrlBuilder<'a> { Self::default() } - pub fn write_domain(&self, s: &mut String<128>) -> Result<(), Error> { + fn write_domain(&self, s: &mut String) -> Result<(), Error> { let port = self.port.ok_or(Error::Network)?; let addr = self .ip_addr @@ -32,7 +30,7 @@ impl<'a> PeerUrlBuilder<'a> { addr.xor(host).ok_or(Error::Network) } - pub fn udp(&self) -> Result, Error> { + pub fn udp(&self) -> Result, Error> { let mut s = String::new(); write!(&mut s, "udp://").ok(); self.write_domain(&mut s)?; @@ -47,7 +45,7 @@ impl<'a> PeerUrlBuilder<'a> { Ok(s) } - pub fn tcp(&mut self) -> Result, Error> { + pub fn tcp(&mut self) -> Result, Error> { let mut s = String::new(); write!(&mut s, "tcp://").ok(); self.write_domain(&mut s)?; @@ -56,20 +54,20 @@ impl<'a> PeerUrlBuilder<'a> { write!(&mut s, "?").ok(); self.local_port .map(|v| write!(&mut s, "local_port={}&", v).ok()); - self.creds.as_ref().map(|creds| { - creds - .ca_cert_name - .as_ref() - .map(|v| write!(&mut s, "ca={}&", v).ok()); - creds - .c_cert_name - .as_ref() - .map(|v| write!(&mut s, "cert={}&", v).ok()); - creds - .c_key_name - .as_ref() - .map(|v| write!(&mut s, "privKey={}&", v).ok()); - }); + // self.creds.as_ref().map(|creds| { + // creds + // .ca_cert_name + // .as_ref() + // .map(|v| write!(&mut s, "ca={}&", v).ok()); + // creds + // .c_cert_name + // .as_ref() + // .map(|v| write!(&mut s, "cert={}&", v).ok()); + // creds + // .c_key_name + // .as_ref() + // .map(|v| write!(&mut s, "privKey={}&", v).ok()); + // }); // Remove trailing '&' or '?' if no query. s.pop(); @@ -85,27 +83,47 @@ impl<'a> PeerUrlBuilder<'a> { self } + pub fn set_hostname(&mut self, hostname: Option<&'a str>) -> &mut Self { + self.hostname = hostname; + self + } + /// maximum length 64 pub fn ip_addr(&mut self, ip_addr: IpAddr) -> &mut Self { self.ip_addr.replace(ip_addr); self } + pub fn set_ip_addr(&mut self, ip_addr: Option) -> &mut Self { + self.ip_addr = ip_addr; + self + } + /// port number pub fn port(&mut self, port: u16) -> &mut Self { self.port.replace(port); self } - pub fn creds(&mut self, creds: SecurityCredentials) -> &mut Self { - self.creds.replace(creds); + pub fn set_port(&mut self, port: Option) -> &mut Self { + self.port = port; self } + // pub fn creds(&mut self, creds: SecurityCredentials) -> &mut Self { + // self.creds.replace(creds); + // self + // } + pub fn local_port(&mut self, local_port: u16) -> &mut Self { self.local_port.replace(local_port); self } + + pub fn set_local_port(&mut self, local_port: Option) -> &mut Self { + self.local_port = local_port; + self + } } #[cfg(test)] @@ -139,21 +157,21 @@ mod test { assert_eq!(url, "udp://example.org:2000/?local_port=2001"); } - #[test] - fn tcp_certs() { - let url = PeerUrlBuilder::new() - .hostname("example.org") - .port(2000) - .creds(SecurityCredentials { - c_cert_name: Some(heapless::String::from("client.crt")), - ca_cert_name: Some(heapless::String::from("ca.crt")), - c_key_name: Some(heapless::String::from("client.key")), - }) - .tcp() - .unwrap(); - assert_eq!( - url, - "tcp://example.org:2000/?ca=ca.crt&cert=client.crt&privKey=client.key" - ); - } + // #[test] + // fn tcp_certs() { + // let url = PeerUrlBuilder::new() + // .hostname("example.org") + // .port(2000) + // .creds(SecurityCredentials { + // c_cert_name: Some(heapless::String::from("client.crt")), + // ca_cert_name: Some(heapless::String::from("ca.crt")), + // c_key_name: Some(heapless::String::from("client.key")), + // }) + // .tcp() + // .unwrap(); + // assert_eq!( + // url, + // "tcp://example.org:2000/?ca=ca.crt&cert=client.crt&privKey=client.key" + // ); + // } } diff --git a/src/wifi/mod.rs b/src/wifi/mod.rs index 33df278..623f2ca 100644 --- a/src/wifi/mod.rs +++ b/src/wifi/mod.rs @@ -1,5 +1,4 @@ -use crate::command::PeerHandle; -pub use ublox_sockets::SocketHandle; +pub use ublox_sockets::{PeerHandle, SocketHandle}; use crate::command::edm::types::ChannelId; From 071587f9150b91f93d7ef38809891f1d74c1f1cb Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 22 Jun 2023 16:41:33 +0200 Subject: [PATCH 04/16] Attempt to rid channel --- Cargo.toml | 11 +- examples/rpi-pico/Cargo.toml | 2 +- examples/rpi-pico/src/bin/embassy-async.rs | 70 +-- examples/rpi-pico/src/bin/embassy-perf.rs | 292 ++++++++++ src/asynch/channel.rs | 579 ------------------- src/asynch/control.rs | 37 +- src/asynch/mod.rs | 26 +- src/asynch/runner.rs | 229 +++----- src/asynch/state.rs | 136 +++++ src/asynch/ublox_stack/dns.rs | 83 ++- src/asynch/ublox_stack/mod.rs | 612 ++++++++------------- src/asynch/ublox_stack/tcp.rs | 31 +- src/lib.rs | 1 + 13 files changed, 881 insertions(+), 1228 deletions(-) create mode 100644 examples/rpi-pico/src/bin/embassy-perf.rs delete mode 100644 src/asynch/channel.rs create mode 100644 src/asynch/state.rs diff --git a/Cargo.toml b/Cargo.toml index 4dbbaf2..c4a07b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,10 @@ atat = { version = "0.18.0", features = ["derive", "defmt", "bytes"] } heapless = { version = "^0.7", features = ["serde", "defmt-impl"] } no-std-net = { version = "0.6", features = ["serde"] } serde = { version = "^1", default-features = false, features = ["derive"] } -ublox-sockets = { version = "0.5", features = ["defmt", "edm"], optional = true } +ublox-sockets = { version = "0.5", features = [ + "defmt", + "edm", +], optional = true } postcard = "1.0.4" smoltcp = { version = "0.9.1", default-features = false, optional = true } atomic-polyfill = "1.0.2" @@ -35,7 +38,9 @@ embassy-hal-common = "0.1" embassy-net-driver = "0.1" embedded-nal-async = { version = "0.4", optional = true } -futures = { version = "0.3.17", default-features = false, features = [ "async-await" ] } +futures = { version = "0.3.17", default-features = false, features = [ + "async-await", +] } embedded-io = "0.4" @@ -69,4 +74,4 @@ atat = { path = "../atat/atat" } ublox-sockets = { path = "../ublox-sockets" } no-std-net = { path = "../no-std-net" } embassy-net-driver = { path = "../embassy/embassy-net-driver" } -embassy-hal-common = { path = "../embassy/embassy-hal-common" } \ No newline at end of file +embassy-hal-common = { path = "../embassy/embassy-hal-common" } diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index 78d4b1a..8787886 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -11,9 +11,9 @@ embassy-time = { version = "0.1.0", features = ["defmt", "defmt-timestamp-uptim embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-traits", "nightly", "unstable-pac", "time-driver"] } embassy-futures = { version = "0.1.0" } atomic-polyfill = "1.0.2" -static_cell = "1.1" no-std-net = { version = "0.6", features = ["serde"] } +static_cell = { version = "1.1", features = ["nightly"] } defmt = "0.3.4" defmt-rtt = "0.3" panic-probe = { version = "0.3", features = ["print-defmt"] } diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index 30140a5..4dd0816 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -14,13 +14,13 @@ use embassy_rp::{bind_interrupts, uart}; use embassy_time::{Duration, Timer}; use embedded_io::asynch::Write; use no_std_net::{Ipv4Addr, SocketAddr}; -use static_cell::StaticCell; +use static_cell::make_static; use ublox_short_range::asynch::runner::Runner; use ublox_short_range::asynch::ublox_stack::dns::DnsSocket; use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; use ublox_short_range::asynch::{new, State}; -use ublox_short_range::atat::{self, AtatIngress, AtatUrcChannel}; +use ublox_short_range::atat::{self, AtatIngress}; use ublox_short_range::command::custom_digest::EdmDigester; use ublox_short_range::command::edm::urc::EdmEvent; use ublox_short_range::embedded_nal_async::AddrType; @@ -29,39 +29,25 @@ use {defmt_rtt as _, panic_probe as _}; const RX_BUF_LEN: usize = 1024; const URC_CAPACITY: usize = 3; -macro_rules! singleton { - ($val:expr) => {{ - type T = impl Sized; - static STATIC_CELL: StaticCell = StaticCell::new(); - let (x,) = STATIC_CELL.init(($val,)); - x - }}; -} +type AtClient = ublox_short_range::atat::asynch::Client< + 'static, + uart::BufferedUartTx<'static, UART1>, + RX_BUF_LEN, +>; #[embassy_executor::task] -async fn wifi_task( - runner: Runner< - 'static, - ublox_short_range::atat::asynch::Client< - 'static, - uart::BufferedUartTx<'static, UART1>, - RX_BUF_LEN, - >, - Output<'static, PIN_26>, - 8, - >, -) -> ! { +async fn wifi_task(runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8>) -> ! { runner.run().await } #[embassy_executor::task] -async fn net_task(stack: &'static UbloxStack) -> ! { +async fn net_task(stack: &'static UbloxStack) -> ! { stack.run().await } #[embassy_executor::task(pool_size = 2)] async fn echo_task( - stack: &'static UbloxStack, + stack: &'static UbloxStack, hostname: &'static str, port: u16, write_interval: Duration, @@ -135,7 +121,7 @@ async fn echo_task( #[embassy_executor::task] async fn ingress_task( - mut ingress: atat::Ingress<'static, EdmDigester, EdmEvent, RX_BUF_LEN, URC_CAPACITY, 1>, + mut ingress: atat::Ingress<'static, EdmDigester, EdmEvent, RX_BUF_LEN, URC_CAPACITY, 2>, mut rx: uart::BufferedUartRx<'static, UART1>, ) -> ! { ingress.read_from(&mut rx).await @@ -152,13 +138,13 @@ async fn main(spawner: Spawner) { let p = embassy_rp::init(Default::default()); let rst = Output::new(p.PIN_26, Level::High); + let mut btn = Input::new(p.PIN_27, Pull::Up); let (tx_pin, rx_pin, rts_pin, cts_pin, uart) = (p.PIN_24, p.PIN_25, p.PIN_23, p.PIN_22, p.UART1); - let mut btn = Input::new(p.PIN_27, Pull::Up); - let tx_buf = &mut singleton!([0u8; 64])[..]; - let rx_buf = &mut singleton!([0u8; 64])[..]; + let tx_buf = &mut make_static!([0u8; 64])[..]; + let rx_buf = &mut make_static!([0u8; 64])[..]; let uart = uart::BufferedUart::new_with_rtscts( uart, Irqs, @@ -172,14 +158,12 @@ async fn main(spawner: Spawner) { ); let (rx, tx) = uart.split(); - let buffers = &*singleton!(atat::Buffers::new()); - let urc_channel = buffers.urc_channel.subscribe().unwrap(); + let buffers = &*make_static!(atat::Buffers::new()); let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); - defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); - let state = singleton!(State::new(client)); - let (net_device, mut control, runner) = new(state, urc_channel, rst).await; + let state = make_static!(State::new(client)); + let (net_device, mut control, runner) = new(state, &buffers.urc_channel, rst).await; defmt::unwrap!(spawner.spawn(wifi_task(runner))); @@ -189,9 +173,9 @@ async fn main(spawner: Spawner) { .unwrap(); // Init network stack - let stack = &*singleton!(UbloxStack::new( + let stack = &*make_static!(UbloxStack::new( net_device, - singleton!(StackResources::<4>::new()), + make_static!(StackResources::<4>::new()), )); defmt::unwrap!(spawner.spawn(net_task(stack))); @@ -219,14 +203,14 @@ async fn main(spawner: Spawner) { match control.join_wpa2("test", "1234abcd").await { Ok(_) => { defmt::info!("Network connected!"); - spawner - .spawn(echo_task( - &stack, - "echo.u-blox.com", - 7, - Duration::from_secs(1), - )) - .unwrap(); + // spawner + // .spawn(echo_task( + // &stack, + // "echo.u-blox.com", + // 7, + // Duration::from_secs(1), + // )) + // .unwrap(); break; } Err(err) => { diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs new file mode 100644 index 0000000..0a52ec2 --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -0,0 +1,292 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(async_fn_in_trait)] +#![allow(incomplete_features)] + +use embassy_executor::Spawner; +use embassy_futures::join::join; +use embassy_rp::gpio::{Level, Output}; +use embassy_rp::peripherals::{PIN_26, UART1}; +use embassy_rp::uart::BufferedInterruptHandler; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{with_timeout, Duration}; +use no_std_net::Ipv4Addr; +use static_cell::make_static; +use ublox_short_range::asynch::runner::Runner; +use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; +use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; +use ublox_short_range::asynch::{new, State}; +use ublox_short_range::atat::{self, AtatIngress}; +use ublox_short_range::command::custom_digest::EdmDigester; +use ublox_short_range::command::edm::urc::EdmEvent; +use {defmt_rtt as _, panic_probe as _}; + +const RX_BUF_LEN: usize = 1024; +const URC_CAPACITY: usize = 3; + +type AtClient = ublox_short_range::atat::asynch::Client< + 'static, + uart::BufferedUartTx<'static, UART1>, + RX_BUF_LEN, +>; + +#[embassy_executor::task] +async fn wifi_task(runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8>) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn net_task(stack: &'static UbloxStack) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn ingress_task( + mut ingress: atat::Ingress<'static, EdmDigester, EdmEvent, RX_BUF_LEN, URC_CAPACITY, 2>, + mut rx: uart::BufferedUartRx<'static, UART1>, +) -> ! { + ingress.read_from(&mut rx).await +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + defmt::info!("Hello World!"); + + let p = embassy_rp::init(Default::default()); + + let rst = Output::new(p.PIN_26, Level::High); + + let (tx_pin, rx_pin, rts_pin, cts_pin, uart) = + (p.PIN_24, p.PIN_25, p.PIN_23, p.PIN_22, p.UART1); + + let tx_buf = &mut make_static!([0u8; 64])[..]; + let rx_buf = &mut make_static!([0u8; 64])[..]; + let uart = uart::BufferedUart::new_with_rtscts( + uart, + Irqs, + tx_pin, + rx_pin, + rts_pin, + cts_pin, + tx_buf, + rx_buf, + uart::Config::default(), + ); + let (rx, tx) = uart.split(); + + let buffers = &*make_static!(atat::Buffers::new()); + let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); + defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); + + let state = make_static!(State::new(client)); + let (net_device, mut control, runner) = new(state, &buffers.urc_channel, rst).await; + + defmt::unwrap!(spawner.spawn(wifi_task(runner))); + + // Init network stack + let stack = &*make_static!(UbloxStack::new( + net_device, + make_static!(StackResources::<4>::new()), + )); + + defmt::unwrap!(spawner.spawn(net_task(stack))); + + loop { + match control.join_wpa2(WIFI_NETWORK, WIFI_PASSWORD).await { + Ok(_) => break, + Err(err) => { + defmt::panic!("join failed with status={}", err); + } + } + } + + // And now we can use it! + defmt::info!("Device initialized!"); + + let down = test_download(stack).await; + let up = test_upload(stack).await; + let updown = test_upload_download(stack).await; + + assert!(down > TEST_EXPECTED_DOWNLOAD_KBPS); + assert!(up > TEST_EXPECTED_UPLOAD_KBPS); + assert!(updown > TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS); + + defmt::info!("Test OK"); + cortex_m::asm::bkpt(); +} + +// Test-only wifi network, no internet access! +const WIFI_NETWORK: &str = "EmbassyTest"; +const WIFI_PASSWORD: &str = "V8YxhKt5CdIAJFud"; + +const TEST_DURATION: usize = 10; +const TEST_EXPECTED_DOWNLOAD_KBPS: usize = 300; +const TEST_EXPECTED_UPLOAD_KBPS: usize = 300; +const TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS: usize = 300; +const RX_BUFFER_SIZE: usize = 4096; +const TX_BUFFER_SIZE: usize = 4096; +const SERVER_ADDRESS: Ipv4Addr = Ipv4Addr::new(192, 168, 2, 2); +const DOWNLOAD_PORT: u16 = 4321; +const UPLOAD_PORT: u16 = 4322; +const UPLOAD_DOWNLOAD_PORT: u16 = 4323; + +async fn test_download(stack: &'static UbloxStack) -> usize { + defmt::info!("Testing download..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + defmt::info!( + "connecting to {:?}:{}...", + defmt::Debug2Format(&SERVER_ADDRESS), + DOWNLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, DOWNLOAD_PORT)).await { + defmt::error!("connect error: {:?}", e); + return 0; + } + defmt::info!("connected, testing..."); + + let mut rx_buf = [0; 4096]; + let mut total: usize = 0; + with_timeout(Duration::from_secs(TEST_DURATION as _), async { + loop { + match socket.read(&mut rx_buf).await { + Ok(0) => { + defmt::error!("read EOF"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + defmt::error!("read error: {:?}", e); + return 0; + } + } + } + }) + .await + .ok(); + + let kbps = (total + 512) / 1024 / TEST_DURATION; + defmt::info!("download: {} kB/s", kbps); + kbps +} + +async fn test_upload(stack: &'static UbloxStack) -> usize { + defmt::info!("Testing upload..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + defmt::info!( + "connecting to {:?}:{}...", + defmt::Debug2Format(&SERVER_ADDRESS), + UPLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_PORT)).await { + defmt::error!("connect error: {:?}", e); + return 0; + } + defmt::info!("connected, testing..."); + + let buf = [0; 4096]; + let mut total: usize = 0; + with_timeout(Duration::from_secs(TEST_DURATION as _), async { + loop { + match socket.write(&buf).await { + Ok(0) => { + defmt::error!("write zero?!??!?!"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + defmt::error!("write error: {:?}", e); + return 0; + } + } + } + }) + .await + .ok(); + + let kbps = (total + 512) / 1024 / TEST_DURATION; + defmt::info!("upload: {} kB/s", kbps); + kbps +} + +async fn test_upload_download(stack: &'static UbloxStack) -> usize { + defmt::info!("Testing upload+download..."); + + let mut rx_buffer = [0; RX_BUFFER_SIZE]; + let mut tx_buffer = [0; TX_BUFFER_SIZE]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + // socket.set_timeout(Some(Duration::from_secs(10))); + + defmt::info!( + "connecting to {:?}:{}...", + defmt::Debug2Format(&SERVER_ADDRESS), + UPLOAD_DOWNLOAD_PORT + ); + if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_DOWNLOAD_PORT)).await { + defmt::error!("connect error: {:?}", e); + return 0; + } + defmt::info!("connected, testing..."); + + let (mut reader, mut writer) = socket.split(); + + let tx_buf = [0; 4096]; + let mut rx_buf = [0; 4096]; + let mut total: usize = 0; + let tx_fut = async { + loop { + match writer.write(&tx_buf).await { + Ok(0) => { + defmt::error!("write zero?!??!?!"); + return 0; + } + Ok(_) => {} + Err(e) => { + defmt::error!("write error: {:?}", e); + return 0; + } + } + } + }; + + let rx_fut = async { + loop { + match reader.read(&mut rx_buf).await { + Ok(0) => { + defmt::error!("read EOF"); + return 0; + } + Ok(n) => total += n, + Err(e) => { + defmt::error!("read error: {:?}", e); + return 0; + } + } + } + }; + + with_timeout( + Duration::from_secs(TEST_DURATION as _), + join(tx_fut, rx_fut), + ) + .await + .ok(); + + let kbps = (total + 512) / 1024 / TEST_DURATION; + defmt::info!("upload+download: {} kB/s", kbps); + kbps +} diff --git a/src/asynch/channel.rs b/src/asynch/channel.rs deleted file mode 100644 index 2c582a1..0000000 --- a/src/asynch/channel.rs +++ /dev/null @@ -1,579 +0,0 @@ -#![allow(dead_code)] - -use core::cell::RefCell; -use core::mem::MaybeUninit; -use core::task::{Context, Poll}; - -use defmt::unwrap; -pub use embassy_net_driver as driver; -use embassy_net_driver::{Capabilities, LinkState, Medium}; -use embassy_sync::blocking_mutex::raw::NoopRawMutex; -use embassy_sync::blocking_mutex::Mutex; -use embassy_sync::waitqueue::WakerRegistration; - -pub struct State { - rx: [PacketBuf; N_RX], - tx: [PacketBuf; N_TX], - inner: MaybeUninit>, -} - -impl State { - const NEW_PACKET: PacketBuf = PacketBuf::new(); - - pub const fn new() -> Self { - Self { - rx: [Self::NEW_PACKET; N_RX], - tx: [Self::NEW_PACKET; N_TX], - inner: MaybeUninit::uninit(), - } - } -} - -struct StateInner<'d, const MTU: usize> { - rx: zerocopy_channel::Channel<'d, NoopRawMutex, PacketBuf>, - tx: zerocopy_channel::Channel<'d, NoopRawMutex, PacketBuf>, - shared: Mutex>, -} - -/// State of the LinkState -struct Shared { - link_state: LinkState, - waker: WakerRegistration, - ethernet_address: [u8; 6], -} - -pub struct Runner<'d, const MTU: usize> { - tx_chan: zerocopy_channel::Receiver<'d, NoopRawMutex, PacketBuf>, - rx_chan: zerocopy_channel::Sender<'d, NoopRawMutex, PacketBuf>, - shared: &'d Mutex>, -} - -#[derive(Clone, Copy)] -pub struct StateRunner<'d> { - shared: &'d Mutex>, -} - -pub struct RxRunner<'d, const MTU: usize> { - rx_chan: zerocopy_channel::Sender<'d, NoopRawMutex, PacketBuf>, -} - -pub struct TxRunner<'d, const MTU: usize> { - tx_chan: zerocopy_channel::Receiver<'d, NoopRawMutex, PacketBuf>, -} - -impl<'d, const MTU: usize> Runner<'d, MTU> { - pub fn split(self) -> (StateRunner<'d>, RxRunner<'d, MTU>, TxRunner<'d, MTU>) { - ( - StateRunner { - shared: self.shared, - }, - RxRunner { - rx_chan: self.rx_chan, - }, - TxRunner { - tx_chan: self.tx_chan, - }, - ) - } - - pub fn state_runner(&self) -> StateRunner<'d> { - StateRunner { - shared: self.shared, - } - } - - pub fn set_link_state(&mut self, state: LinkState) { - self.shared.lock(|s| { - let s = &mut *s.borrow_mut(); - s.link_state = state; - s.waker.wake(); - }); - } - - pub fn set_ethernet_address(&mut self, address: [u8; 6]) { - self.shared.lock(|s| { - let s = &mut *s.borrow_mut(); - s.ethernet_address = address; - s.waker.wake(); - }); - } - - pub async fn rx_buf(&mut self) -> &mut [u8] { - let p = self.rx_chan.send().await; - &mut p.buf - } - - pub fn try_rx_buf(&mut self) -> Option<&mut [u8]> { - let p = self.rx_chan.try_send()?; - Some(&mut p.buf) - } - - pub fn poll_rx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { - match self.rx_chan.poll_send(cx) { - Poll::Ready(p) => Poll::Ready(&mut p.buf), - Poll::Pending => Poll::Pending, - } - } - - pub fn rx_done(&mut self, len: usize) { - let p = self.rx_chan.try_send().unwrap(); - p.len = len; - self.rx_chan.send_done(); - } - - pub async fn tx_buf(&mut self) -> &mut [u8] { - let p = self.tx_chan.recv().await; - &mut p.buf[..p.len] - } - - pub fn try_tx_buf(&mut self) -> Option<&mut [u8]> { - let p = self.tx_chan.try_recv()?; - Some(&mut p.buf[..p.len]) - } - - pub fn poll_tx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { - match self.tx_chan.poll_recv(cx) { - Poll::Ready(p) => Poll::Ready(&mut p.buf[..p.len]), - Poll::Pending => Poll::Pending, - } - } - - pub fn tx_done(&mut self) { - self.tx_chan.recv_done(); - } -} - -impl<'d> StateRunner<'d> { - pub fn set_link_state(&self, state: LinkState) { - self.shared.lock(|s| { - let s = &mut *s.borrow_mut(); - s.link_state = state; - s.waker.wake(); - }); - } - - pub fn set_ethernet_address(&self, address: [u8; 6]) { - self.shared.lock(|s| { - let s = &mut *s.borrow_mut(); - s.ethernet_address = address; - s.waker.wake(); - }); - } - - pub fn link_state(&mut self, cx: &mut Context) -> LinkState { - self.shared.lock(|s| { - let s = &mut *s.borrow_mut(); - s.waker.register(cx.waker()); - s.link_state - }) - } -} - -impl<'d, const MTU: usize> RxRunner<'d, MTU> { - pub async fn rx_buf(&mut self) -> &mut [u8] { - let p = self.rx_chan.send().await; - &mut p.buf - } - - pub fn try_rx_buf(&mut self) -> Option<&mut [u8]> { - let p = self.rx_chan.try_send()?; - Some(&mut p.buf) - } - - pub fn poll_rx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { - match self.rx_chan.poll_send(cx) { - Poll::Ready(p) => Poll::Ready(&mut p.buf), - Poll::Pending => Poll::Pending, - } - } - - pub fn rx_done(&mut self, len: usize) { - let p = self.rx_chan.try_send().unwrap(); - p.len = len; - self.rx_chan.send_done(); - } -} - -impl<'d, const MTU: usize> TxRunner<'d, MTU> { - pub async fn tx_buf(&mut self) -> &mut [u8] { - let p = self.tx_chan.recv().await; - &mut p.buf[..p.len] - } - - pub fn try_tx_buf(&mut self) -> Option<&mut [u8]> { - let p = self.tx_chan.try_recv()?; - Some(&mut p.buf[..p.len]) - } - - pub fn poll_tx_buf(&mut self, cx: &mut Context) -> Poll<&mut [u8]> { - match self.tx_chan.poll_recv(cx) { - Poll::Ready(p) => Poll::Ready(&mut p.buf[..p.len]), - Poll::Pending => Poll::Pending, - } - } - - pub fn tx_done(&mut self) { - self.tx_chan.recv_done(); - } -} - -pub fn new<'d, const MTU: usize, const N_RX: usize, const N_TX: usize>( - state: &'d mut State, - ethernet_address: [u8; 6], -) -> (Runner<'d, MTU>, Device<'d, MTU>) { - let mut caps = Capabilities::default(); - caps.max_transmission_unit = MTU; - caps.medium = Medium::Ethernet; - - // safety: this is a self-referential struct, however: - // - it can't move while the `'d` borrow is active. - // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. - let state_uninit: *mut MaybeUninit> = - (&mut state.inner as *mut MaybeUninit>).cast(); - let state = unsafe { &mut *state_uninit }.write(StateInner { - rx: zerocopy_channel::Channel::new(&mut state.rx[..]), - tx: zerocopy_channel::Channel::new(&mut state.tx[..]), - shared: Mutex::new(RefCell::new(Shared { - link_state: LinkState::Down, - ethernet_address, - waker: WakerRegistration::new(), - })), - }); - - let (rx_sender, rx_receiver) = state.rx.split(); - let (tx_sender, tx_receiver) = state.tx.split(); - - ( - Runner { - tx_chan: tx_receiver, - rx_chan: rx_sender, - shared: &state.shared, - }, - Device { - caps, - shared: &state.shared, - rx: rx_receiver, - tx: tx_sender, - }, - ) -} - -pub struct PacketBuf { - len: usize, - buf: [u8; MTU], -} - -impl PacketBuf { - pub const fn new() -> Self { - Self { - len: 0, - buf: [0; MTU], - } - } -} - -pub struct Device<'d, const MTU: usize> { - rx: zerocopy_channel::Receiver<'d, NoopRawMutex, PacketBuf>, - tx: zerocopy_channel::Sender<'d, NoopRawMutex, PacketBuf>, - shared: &'d Mutex>, - caps: Capabilities, -} - -impl<'d, const MTU: usize> embassy_net_driver::Driver for Device<'d, MTU> { - type RxToken<'a> = RxToken<'a, MTU> where Self: 'a ; - type TxToken<'a> = TxToken<'a, MTU> where Self: 'a ; - - fn receive(&mut self, cx: &mut Context) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { - if self.rx.poll_recv(cx).is_ready() && self.tx.poll_send(cx).is_ready() { - Some(( - RxToken { - rx: self.rx.borrow(), - }, - TxToken { - tx: self.tx.borrow(), - }, - )) - } else { - None - } - } - - /// Construct a transmit token. - fn transmit(&mut self, cx: &mut Context) -> Option> { - if self.tx.poll_send(cx).is_ready() { - Some(TxToken { - tx: self.tx.borrow(), - }) - } else { - None - } - } - - /// Get a description of device capabilities. - fn capabilities(&self) -> Capabilities { - self.caps.clone() - } - - fn ethernet_address(&self) -> [u8; 6] { - self.shared.lock(|s| s.borrow().ethernet_address) - } - - fn link_state(&mut self, cx: &mut Context) -> LinkState { - self.shared.lock(|s| { - let s = &mut *s.borrow_mut(); - s.waker.register(cx.waker()); - s.link_state - }) - } -} - -pub struct RxToken<'a, const MTU: usize> { - rx: zerocopy_channel::Receiver<'a, NoopRawMutex, PacketBuf>, -} - -impl<'a, const MTU: usize> embassy_net_driver::RxToken for RxToken<'a, MTU> { - fn consume(mut self, f: F) -> R - where - F: FnOnce(&mut [u8]) -> R, - { - // NOTE(unwrap): we checked the queue wasn't full when creating the token. - let pkt = unwrap!(self.rx.try_recv()); - let r = f(&mut pkt.buf[..pkt.len]); - self.rx.recv_done(); - r - } -} - -pub struct TxToken<'a, const MTU: usize> { - tx: zerocopy_channel::Sender<'a, NoopRawMutex, PacketBuf>, -} - -impl<'a, const MTU: usize> embassy_net_driver::TxToken for TxToken<'a, MTU> { - fn consume(mut self, len: usize, f: F) -> R - where - F: FnOnce(&mut [u8]) -> R, - { - // NOTE(unwrap): we checked the queue wasn't full when creating the token. - let pkt = unwrap!(self.tx.try_send()); - let r = f(&mut pkt.buf[..len]); - pkt.len = len; - self.tx.send_done(); - r - } -} - -mod zerocopy_channel { - use core::cell::RefCell; - use core::future::poll_fn; - use core::marker::PhantomData; - use core::task::{Context, Poll}; - - use embassy_sync::blocking_mutex::raw::RawMutex; - use embassy_sync::blocking_mutex::Mutex; - use embassy_sync::waitqueue::WakerRegistration; - - pub struct Channel<'a, M: RawMutex, T> { - buf: *mut T, - phantom: PhantomData<&'a mut T>, - state: Mutex>, - } - - impl<'a, M: RawMutex, T> Channel<'a, M, T> { - pub fn new(buf: &'a mut [T]) -> Self { - let len = buf.len(); - assert!(len != 0); - - Self { - buf: buf.as_mut_ptr(), - phantom: PhantomData, - state: Mutex::new(RefCell::new(State { - len, - front: 0, - back: 0, - full: false, - send_waker: WakerRegistration::new(), - recv_waker: WakerRegistration::new(), - })), - } - } - - pub fn split(&mut self) -> (Sender<'_, M, T>, Receiver<'_, M, T>) { - (Sender { channel: self }, Receiver { channel: self }) - } - } - - pub struct Sender<'a, M: RawMutex, T> { - channel: &'a Channel<'a, M, T>, - } - - impl<'a, M: RawMutex, T> Sender<'a, M, T> { - pub fn borrow(&mut self) -> Sender<'_, M, T> { - Sender { - channel: self.channel, - } - } - - pub fn try_send(&mut self) -> Option<&mut T> { - self.channel.state.lock(|s| { - let s = &mut *s.borrow_mut(); - match s.push_index() { - Some(i) => Some(unsafe { &mut *self.channel.buf.add(i) }), - None => None, - } - }) - } - - pub fn poll_send(&mut self, cx: &mut Context) -> Poll<&mut T> { - self.channel.state.lock(|s| { - let s = &mut *s.borrow_mut(); - match s.push_index() { - Some(i) => Poll::Ready(unsafe { &mut *self.channel.buf.add(i) }), - None => { - s.recv_waker.register(cx.waker()); - Poll::Pending - } - } - }) - } - - pub async fn send(&mut self) -> &mut T { - let i = poll_fn(|cx| { - self.channel.state.lock(|s| { - let s = &mut *s.borrow_mut(); - match s.push_index() { - Some(i) => Poll::Ready(i), - None => { - s.recv_waker.register(cx.waker()); - Poll::Pending - } - } - }) - }) - .await; - unsafe { &mut *self.channel.buf.add(i) } - } - - pub fn send_done(&mut self) { - self.channel.state.lock(|s| s.borrow_mut().push_done()) - } - } - pub struct Receiver<'a, M: RawMutex, T> { - channel: &'a Channel<'a, M, T>, - } - - impl<'a, M: RawMutex, T> Receiver<'a, M, T> { - pub fn borrow(&mut self) -> Receiver<'_, M, T> { - Receiver { - channel: self.channel, - } - } - - pub fn try_recv(&mut self) -> Option<&mut T> { - self.channel.state.lock(|s| { - let s = &mut *s.borrow_mut(); - match s.pop_index() { - Some(i) => Some(unsafe { &mut *self.channel.buf.add(i) }), - None => None, - } - }) - } - - pub fn poll_recv(&mut self, cx: &mut Context) -> Poll<&mut T> { - self.channel.state.lock(|s| { - let s = &mut *s.borrow_mut(); - match s.pop_index() { - Some(i) => Poll::Ready(unsafe { &mut *self.channel.buf.add(i) }), - None => { - s.send_waker.register(cx.waker()); - Poll::Pending - } - } - }) - } - - pub async fn recv(&mut self) -> &mut T { - let i = poll_fn(|cx| { - self.channel.state.lock(|s| { - let s = &mut *s.borrow_mut(); - match s.pop_index() { - Some(i) => Poll::Ready(i), - None => { - s.send_waker.register(cx.waker()); - Poll::Pending - } - } - }) - }) - .await; - unsafe { &mut *self.channel.buf.add(i) } - } - - pub fn recv_done(&mut self) { - self.channel.state.lock(|s| s.borrow_mut().pop_done()) - } - } - - struct State { - len: usize, - - /// Front index. Always 0..=(N-1) - front: usize, - /// Back index. Always 0..=(N-1). - back: usize, - - /// Used to distinguish "empty" and "full" cases when `front == back`. - /// May only be `true` if `front == back`, always `false` otherwise. - full: bool, - - send_waker: WakerRegistration, - recv_waker: WakerRegistration, - } - - impl State { - fn increment(&self, i: usize) -> usize { - if i + 1 == self.len { - 0 - } else { - i + 1 - } - } - - fn is_full(&self) -> bool { - self.full - } - - fn is_empty(&self) -> bool { - self.front == self.back && !self.full - } - - fn push_index(&mut self) -> Option { - match self.is_full() { - true => None, - false => Some(self.back), - } - } - - fn push_done(&mut self) { - assert!(!self.is_full()); - self.back = self.increment(self.back); - if self.back == self.front { - self.full = true; - } - self.send_waker.wake(); - } - - fn pop_index(&mut self) -> Option { - match self.is_empty() { - true => None, - false => Some(self.front), - } - } - - fn pop_done(&mut self) { - assert!(!self.is_empty()); - self.front = self.increment(self.front); - self.full = false; - self.recv_waker.wake(); - } - } -} diff --git a/src/asynch/control.rs b/src/asynch/control.rs index 20f7b8c..289207f 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -2,50 +2,45 @@ use core::future::poll_fn; use core::task::Poll; use atat::asynch::AtatClient; -use embassy_net_driver::LinkState; use embassy_time::{with_timeout, Duration}; +use crate::command::gpio::{ + types::{GPIOId, GPIOValue}, + WriteGPIO, +}; use crate::command::network::SetNetworkHostName; use crate::command::wifi::types::{ Authentication, StatusId, WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, }; -use crate::command::wifi::{ - ExecWifiStationAction, GetWifiMac, GetWifiStatus, SetWifiStationConfig, -}; +use crate::command::wifi::{ExecWifiStationAction, GetWifiStatus, SetWifiStationConfig}; use crate::command::OnOff; use crate::error::Error; -use crate::{ - command::gpio::{ - types::{GPIOId, GPIOValue}, - WriteGPIO, - }, - hex, -}; -use super::{channel, AtHandle}; +use super::state::LinkState; +use super::{state, AtHandle}; const CONFIG_ID: u8 = 0; pub struct Control<'a, AT: AtatClient> { - state_ch: channel::StateRunner<'a>, + state_ch: state::StateRunner<'a>, at: AtHandle<'a, AT>, } impl<'a, AT: AtatClient> Control<'a, AT> { - pub(crate) fn new(state_ch: channel::StateRunner<'a>, at: AtHandle<'a, AT>) -> Self { + pub(crate) fn new(state_ch: state::StateRunner<'a>, at: AtHandle<'a, AT>) -> Self { Self { state_ch, at } } pub(crate) async fn init(&mut self) -> Result<(), Error> { defmt::debug!("Initalizing ublox control"); // read MAC addr. - let mut resp = self.at.send_edm(GetWifiMac).await?; - self.state_ch.set_ethernet_address( - hex::from_hex(resp.mac_addr.as_mut_slice()) - .unwrap() - .try_into() - .unwrap(), - ); + // let mut resp = self.at.send_edm(GetWifiMac).await?; + // self.state_ch.set_ethernet_address( + // hex::from_hex(resp.mac_addr.as_mut_slice()) + // .unwrap() + // .try_into() + // .unwrap(), + // ); // let country = countries::WORLD_WIDE_XX; // let country_info = CountryInfo { diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 48c239a..56204dd 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -3,14 +3,14 @@ pub mod runner; #[cfg(feature = "ublox-sockets")] pub mod ublox_stack; -pub(crate) mod channel; +pub(crate) mod state; use crate::command::edm::{urc::EdmEvent, EdmAtCmdWrapper}; -use atat::{asynch::AtatClient, UrcSubscription}; -use channel::Device; +use atat::{asynch::AtatClient, AtatUrcChannel}; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; use embedded_hal::digital::OutputPin; use runner::Runner; +use state::Device; use self::control::Control; @@ -36,38 +36,40 @@ impl<'d, AT: AtatClient> AtHandle<'d, AT> { } pub struct State { - ch: channel::State, + ch: state::State, at_handle: Mutex, } impl State { pub fn new(at_handle: AT) -> Self { Self { - ch: channel::State::new(), + ch: state::State::new(), at_handle: Mutex::new(at_handle), } } } -pub const MTU: usize = 4096 + 4; // DATA_PACKAGE_SIZE - -pub async fn new<'a, AT: AtatClient, RST: OutputPin>( +pub async fn new<'a, AT: AtatClient, SUB: AtatUrcChannel, RST: OutputPin>( state: &'a mut State, - urc_subscription: UrcSubscription<'a, EdmEvent>, + subscriber: &'a SUB, reset: RST, ) -> ( - Device<'a, MTU>, + Device<'a, AT>, Control<'a, AT>, Runner<'a, AT, RST, MAX_CONNS>, ) { - let (ch_runner, net_device) = channel::new(&mut state.ch, [0; 6]); + let (ch_runner, net_device) = state::new( + &mut state.ch, + AtHandle(&state.at_handle), + subscriber.subscribe().unwrap(), + ); let state_ch = ch_runner.state_runner(); let mut runner = Runner::new( ch_runner, AtHandle(&state.at_handle), reset, - urc_subscription, + subscriber.subscribe().unwrap(), ); runner.init().await.unwrap(); diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index 13620d7..aca6a24 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -1,20 +1,15 @@ use core::str::FromStr; -use super::channel::{self, driver::LinkState}; +use super::state::{self, LinkState}; use crate::{ - asynch::ublox_stack::Disconnect, command::{ - data_mode::{ - responses::ConnectPeerResponse, urc::PeerDisconnected, ClosePeerConnection, ConnectPeer, - }, - edm::{urc::EdmEvent, EdmDataCommand, SwitchToEdmCommand}, + edm::{urc::EdmEvent, SwitchToEdmCommand}, network::{ responses::NetworkStatusResponse, types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, urc::{NetworkDown, NetworkUp}, GetNetworkStatus, }, - ping::Ping, system::{ types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, RebootDCE, SetEcho, SetRS232Settings, StoreCurrentConfig, @@ -30,21 +25,17 @@ use crate::{ network::WifiNetwork, }; use atat::{asynch::AtatClient, UrcSubscription}; -use embassy_futures::select::{select, Either}; use embassy_time::{with_timeout, Duration, Timer}; use embedded_hal::digital::OutputPin; use no_std_net::{Ipv4Addr, Ipv6Addr}; -use super::{ - ublox_stack::{Connect, DataPacket, SocketRx, SocketTx}, - AtHandle, MTU, -}; +use super::AtHandle; /// Background runner for the Ublox Module. /// /// You must call `.run()` in a background task for the Ublox Module to operate. pub struct Runner<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> { - ch: channel::Runner<'d, MTU>, + ch: state::Runner<'d>, at: AtHandle<'d, AT>, reset: RST, wifi_connection: Option, @@ -54,7 +45,7 @@ pub struct Runner<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> { impl<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> Runner<'d, AT, RST, MAX_CONNS> { pub(crate) fn new( - ch: channel::Runner<'d, MTU>, + ch: state::Runner<'d>, at: AtHandle<'d, AT>, reset: RST, urc_subscription: UrcSubscription<'d, EdmEvent>, @@ -157,15 +148,14 @@ impl<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> Runner<'d, AT, // Ignore AT results until we are successful in EDM mode self.at.send(SwitchToEdmCommand).await.ok(); - match select( + if let Ok(EdmEvent::StartUp) = with_timeout( + Duration::from_millis(300), self.urc_subscription.next_message_pure(), - Timer::after(Duration::from_millis(100)), ) .await { - Either::First(EdmEvent::StartUp) => break, - _ => {} - }; + break; + } } }; @@ -196,157 +186,72 @@ impl<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> Runner<'d, AT, pub async fn run(mut self) -> ! { loop { - let tx = self.ch.tx_buf(); - let urc = self.urc_subscription.next_message_pure(); - - match select(tx, urc).await { - Either::First(p) => { - match postcard::from_bytes::(p) { - Ok(SocketTx::Data(packet)) => { - self.at - .send(EdmDataCommand { - channel: packet.edm_channel, - data: packet.payload, - }) - .await - .ok(); - } - Ok(SocketTx::Connect(Connect { url, socket_handle })) => { - if let Ok(ConnectPeerResponse { peer_handle }) = - self.at.send_edm(ConnectPeer { url }).await - { - self.rx(SocketRx::PeerHandle(socket_handle, peer_handle)) - .await; + let event = self.urc_subscription.next_message_pure().await; + match event { + EdmEvent::ATEvent(Urc::StartUp) => { + defmt::error!("AT startup event?! Device restarted unintentionally!"); + } + EdmEvent::ATEvent(Urc::WifiLinkConnected(WifiLinkConnected { + connection_id: _, + bssid, + channel, + })) => { + if let Some(ref mut con) = self.wifi_connection { + con.wifi_state = WiFiState::Connected; + con.network.bssid = bssid; + con.network.channel = channel; + } else { + defmt::debug!("[URC] Active network config discovered"); + self.wifi_connection.replace( + WifiConnection::new( + WifiNetwork::new_station(bssid, channel), + WiFiState::Connected, + 255, + ) + .activate(), + ); + } + self.is_link_up().await.unwrap(); + } + EdmEvent::ATEvent(Urc::WifiLinkDisconnected(WifiLinkDisconnected { + reason, + .. + })) => { + if let Some(ref mut con) = self.wifi_connection { + match reason { + DisconnectReason::NetworkDisabled => { + con.wifi_state = WiFiState::Inactive; } - } - Ok(SocketTx::Disconnect(peer_handle)) => { - self.at - .send_edm(ClosePeerConnection { peer_handle }) - .await - .ok(); - } - Ok(SocketTx::Dns(hostname)) => { - if self - .at - .send_edm(Ping { - retry_num: 1, - hostname, - }) - .await - .is_err() - { - self.rx(SocketRx::Ping(Err(()))).await; + DisconnectReason::SecurityProblems => { + defmt::error!("Wifi Security Problems"); + } + _ => { + con.wifi_state = WiFiState::NotConnected; } } - Err(_) => {} } - self.ch.tx_done(); - } - Either::Second(p) => match p { - EdmEvent::BluetoothConnectEvent(_) => {} - EdmEvent::ATEvent(urc) => self.handle_urc(urc).await.unwrap(), - EdmEvent::StartUp => { - defmt::error!("EDM startup event?! Device restarted unintentionally!"); - } - - // All below events needs to be conveyed to `self.ch.rx` - EdmEvent::IPv4ConnectEvent(ev) => self.rx(ev).await, - EdmEvent::IPv6ConnectEvent(ev) => self.rx(ev).await, - EdmEvent::DisconnectEvent(channel_id) => self.rx(channel_id).await, - EdmEvent::DataEvent(ev) => { - let packet = DataPacket { - edm_channel: ev.channel_id, - payload: ev.data.as_slice(), - }; - - self.rx(packet).await; - } - }, - } - } - } - async fn rx<'a>(&mut self, packet: impl Into>) { - let pkg = packet.into(); - let buf = self.ch.rx_buf().await; - let used = defmt::unwrap!(postcard::to_slice(&pkg, buf)); - let len = used.len(); - self.ch.rx_done(len); - } - - async fn handle_urc(&mut self, urc: Urc) -> Result<(), Error> { - match urc { - Urc::StartUp => { - defmt::error!("AT startup event?! Device restarted unintentionally!"); - } - Urc::PeerConnected(pc) => { - defmt::info!("Peer connected! {}", pc); - // if self.connections.insert(handle, connection_type).is_err() { - // defmt::warn!("Out of connection entries"); - // } - } - Urc::PeerDisconnected(PeerDisconnected { handle }) => { - defmt::info!("Peer disconnected!"); - self.rx(SocketRx::Disconnect(Disconnect::Peer(handle))) - .await; - // self.connections.remove(&handle); - } - Urc::WifiLinkConnected(WifiLinkConnected { - connection_id: _, - bssid, - channel, - }) => { - if let Some(ref mut con) = self.wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = bssid; - con.network.channel = channel; - } else { - defmt::debug!("[URC] Active network config discovered"); - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork::new_station(bssid, channel), - WiFiState::Connected, - 255, - ) - .activate(), - ); + self.is_link_up().await.unwrap(); } - self.is_link_up().await?; - } - Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { - if let Some(ref mut con) = self.wifi_connection { - match reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - defmt::error!("Wifi Security Problems"); - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } - } + EdmEvent::ATEvent(Urc::WifiAPUp(_)) => todo!(), + EdmEvent::ATEvent(Urc::WifiAPDown(_)) => todo!(), + EdmEvent::ATEvent(Urc::WifiAPStationConnected(_)) => todo!(), + EdmEvent::ATEvent(Urc::WifiAPStationDisconnected(_)) => todo!(), + EdmEvent::ATEvent(Urc::EthernetLinkUp(_)) => todo!(), + EdmEvent::ATEvent(Urc::EthernetLinkDown(_)) => todo!(), + EdmEvent::ATEvent(Urc::NetworkUp(NetworkUp { interface_id })) => { + self.network_status_callback(interface_id).await.unwrap(); } - - self.is_link_up().await?; - } - Urc::WifiAPUp(_) => todo!(), - Urc::WifiAPDown(_) => todo!(), - Urc::WifiAPStationConnected(_) => todo!(), - Urc::WifiAPStationDisconnected(_) => todo!(), - Urc::EthernetLinkUp(_) => todo!(), - Urc::EthernetLinkDown(_) => todo!(), - Urc::NetworkUp(NetworkUp { interface_id }) => { - self.network_status_callback(interface_id).await?; - } - Urc::NetworkDown(NetworkDown { interface_id }) => { - self.network_status_callback(interface_id).await?; - } - Urc::NetworkError(_) => todo!(), - Urc::PingResponse(resp) => self.rx(SocketRx::Ping(Ok(resp.ip))).await, - Urc::PingErrorResponse(_) => self.rx(SocketRx::Ping(Err(()))).await, + EdmEvent::ATEvent(Urc::NetworkDown(NetworkDown { interface_id })) => { + self.network_status_callback(interface_id).await.unwrap(); + } + EdmEvent::ATEvent(Urc::NetworkError(_)) => todo!(), + EdmEvent::StartUp => { + defmt::error!("EDM startup event?! Device restarted unintentionally!"); + } + _ => {} + }; } - Ok(()) } async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { diff --git a/src/asynch/state.rs b/src/asynch/state.rs new file mode 100644 index 0000000..22e115b --- /dev/null +++ b/src/asynch/state.rs @@ -0,0 +1,136 @@ +#![allow(dead_code)] + +use core::cell::RefCell; +use core::mem::MaybeUninit; +use core::task::Context; + +use atat::asynch::AtatClient; +use atat::UrcSubscription; +use embassy_sync::blocking_mutex::raw::NoopRawMutex; +use embassy_sync::blocking_mutex::Mutex; +use embassy_sync::waitqueue::WakerRegistration; + +use crate::command::edm::urc::EdmEvent; + +/// The link state of a network device. +#[derive(PartialEq, Eq, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum LinkState { + /// The link is down. + Down, + /// The link is up. + Up, +} + +use super::AtHandle; + +pub struct State { + inner: MaybeUninit, +} + +impl State { + pub const fn new() -> Self { + Self { + inner: MaybeUninit::uninit(), + } + } +} + +struct StateInner { + shared: Mutex>, +} + +/// State of the LinkState +pub struct Shared { + link_state: LinkState, + waker: WakerRegistration, +} + +pub struct Runner<'d> { + shared: &'d Mutex>, +} + +#[derive(Clone, Copy)] +pub struct StateRunner<'d> { + shared: &'d Mutex>, +} + +impl<'d> Runner<'d> { + pub fn state_runner(&self) -> StateRunner<'d> { + StateRunner { + shared: self.shared, + } + } + + pub fn set_link_state(&mut self, state: LinkState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = state; + s.waker.wake(); + }); + } +} + +impl<'d> StateRunner<'d> { + pub fn set_link_state(&self, state: LinkState) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.link_state = state; + s.waker.wake(); + }); + } + + pub fn link_state(&mut self, cx: &mut Context) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.waker.register(cx.waker()); + s.link_state + }) + } +} + +pub fn new<'d, AT: AtatClient>( + state: &'d mut State, + at: AtHandle<'d, AT>, + urc_subscription: UrcSubscription<'d, EdmEvent>, +) -> (Runner<'d>, Device<'d, AT>) { + // safety: this is a self-referential struct, however: + // - it can't move while the `'d` borrow is active. + // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. + let state_uninit: *mut MaybeUninit = + (&mut state.inner as *mut MaybeUninit).cast(); + + let state = unsafe { &mut *state_uninit }.write(StateInner { + shared: Mutex::new(RefCell::new(Shared { + link_state: LinkState::Down, + waker: WakerRegistration::new(), + })), + }); + + ( + Runner { + shared: &state.shared, + }, + Device { + shared: &state.shared, + urc_subscription, + at, + }, + ) +} + +pub struct Device<'d, AT: AtatClient> { + pub(crate) shared: &'d Mutex>, + pub(crate) at: AtHandle<'d, AT>, + pub(crate) urc_subscription: UrcSubscription<'d, EdmEvent>, +} + +impl<'d, AT: AtatClient> Device<'d, AT> { + pub fn link_state(&self, cx: &mut Context) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + s.waker.register(cx.waker()); + s.link_state + }) + } +} diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index e402dd3..97cfc79 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -1,13 +1,37 @@ -//! DNS client compatible with the `embedded-nal-async` traits. -//! -//! This exists only for compatibility with crates that use `embedded-nal-async`. -//! Prefer using [`Stack::dns_query`](crate::Stack::dns_query) directly if you're -//! not using `embedded-nal-async`. +use core::task::Poll; +use atat::asynch::AtatClient; +use embedded_nal_async::AddrType; +use futures::Future; use no_std_net::IpAddr; +use crate::command::ping::Ping; + use super::UbloxStack; +struct DnsFuture<'a, AT: AtatClient + 'static> { + stack: &'a UbloxStack, +} + +impl<'a, AT: AtatClient> Future for DnsFuture<'a, AT> { + type Output = Result; + + fn poll( + self: core::pin::Pin<&mut Self>, + cx: &mut core::task::Context<'_>, + ) -> Poll { + // let i = &mut *self.stack.inner.borrow_mut(); + // match i.dns_result { + // Some(Ok(ip)) => Poll::Ready(Ok(ip)), + // Some(Err(_)) => Poll::Ready(Err(Error::Failed)), + // None => { + // i.dns_waker.register(cx.waker()); + Poll::Pending + // } + // } + } +} + /// Errors returned by DnsSocket. #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -25,40 +49,57 @@ pub enum Error { /// This exists only for compatibility with crates that use `embedded-nal-async`. /// Prefer using [`Stack::dns_query`](crate::Stack::dns_query) directly if you're /// not using `embedded-nal-async`. -pub struct DnsSocket<'a> { - stack: &'a UbloxStack, +pub struct DnsSocket<'a, AT: AtatClient + 'static> { + stack: &'a UbloxStack, } -impl<'a> DnsSocket<'a> { +impl<'a, AT: AtatClient> DnsSocket<'a, AT> { /// Create a new DNS socket using the provided stack. /// /// NOTE: If using DHCP, make sure it has reconfigured the stack to ensure the DNS servers are updated. - pub fn new(stack: &'a UbloxStack) -> Self { + pub fn new(stack: &'a UbloxStack) -> Self { Self { stack } } /// Make a query for a given name and return the corresponding IP addresses. - pub async fn query( - &self, - name: &str, - addr_type: embedded_nal_async::AddrType, - ) -> Result { - let mut addrs = self.stack.dns_query(name, addr_type).await?; - if addrs.len() < 1 { - return Err(Error::Failed); + pub async fn query(&self, name: &str, addr_type: AddrType) -> Result { + match addr_type { + AddrType::IPv4 => { + if let Ok(ip) = name.parse().map(IpAddr::V4) { + return Ok(ip); + } + } + AddrType::IPv6 => { + if let Ok(ip) = name.parse().map(IpAddr::V6) { + return Ok(ip); + } + } + _ => {} } - Ok(addrs.swap_remove(0)) + + // let i = &mut *self.stack.inner.borrow_mut(); + // i.dns_result = None; + // i.device + // .at + // .send_edm(Ping { + // hostname: name, + // retry_num: 1, + // }) + // .await + // .map_err(|_| Error::Failed)?; + + DnsFuture { stack: self.stack }.await } } // #[cfg(all(feature = "unstable-traits", feature = "nightly"))] -impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { +impl<'a, AT: AtatClient> embedded_nal_async::Dns for DnsSocket<'a, AT> { type Error = Error; async fn get_host_by_name( &self, host: &str, - addr_type: embedded_nal_async::AddrType, + addr_type: AddrType, ) -> Result { self.query(host, addr_type).await } @@ -67,6 +108,6 @@ impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { &self, _addr: embedded_nal_async::IpAddr, ) -> Result, Self::Error> { - todo!() + unimplemented!() } } diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index 4d05d4c..1f7bcc7 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -7,24 +7,33 @@ pub mod dns; use core::cell::RefCell; use core::future::poll_fn; -use core::task::{Context, Poll}; - -use crate::command::edm::types::{IPv4ConnectEvent, IPv6ConnectEvent, Protocol}; +use core::task::Poll; + +use crate::command::data_mode::responses::ConnectPeerResponse; +use crate::command::data_mode::urc::PeerDisconnected; +use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; +use crate::command::edm::types::{DataEvent, Protocol, DATA_PACKAGE_SIZE}; +use crate::command::edm::urc::EdmEvent; +use crate::command::edm::EdmDataCommand; +use crate::command::ping::urc::{PingErrorResponse, PingResponse}; +use crate::command::Urc; use crate::peer_builder::PeerUrlBuilder; -use super::{channel, MTU}; +use self::dns::DnsSocket; + +use super::state::{self, LinkState}; -use super::channel::driver::{Driver, LinkState, RxToken, TxToken}; +use atat::asynch::AtatClient; +use embassy_futures::select::{select, select3, Either, Either3}; use embassy_sync::waitqueue::WakerRegistration; -use embassy_time::Timer; +use embassy_time::{Duration, Instant, Timer}; use embedded_nal_async::SocketAddr; use futures::{pin_mut, Future}; -use heapless::Vec; use no_std_net::IpAddr; -use serde::{Deserialize, Serialize}; -use ublox_sockets::{ - AnySocket, ChannelId, PeerHandle, Socket, SocketHandle, SocketSet, SocketStorage, TcpState, -}; +use ublox_sockets::{AnySocket, ChannelId, PeerHandle, Socket, SocketSet, SocketStorage}; + +#[cfg(feature = "socket-tcp")] +use ublox_sockets::TcpState; pub struct StackResources { sockets: [SocketStorage<'static>; SOCK], @@ -38,17 +47,17 @@ impl StackResources { } } -pub struct UbloxStack { +pub struct UbloxStack { pub(crate) socket: RefCell, - inner: RefCell, + device: RefCell>, } -struct Inner { - device: channel::Device<'static, MTU>, - link_up: bool, - dns_result: Option>, - dns_waker: WakerRegistration, -} +// struct Inner { +// device: state::Device<'static, AT>, +// link_up: bool, +// dns_result: Option>, +// dns_waker: WakerRegistration, +// } pub(crate) struct SocketStack { pub(crate) sockets: SocketSet<'static>, @@ -56,9 +65,9 @@ pub(crate) struct SocketStack { dropped_sockets: heapless::Vec, } -impl UbloxStack { +impl UbloxStack { pub fn new( - device: channel::Device<'static, MTU>, + device: state::Device<'static, AT>, resources: &'static mut StackResources, ) -> Self { let sockets = SocketSet::new(&mut resources.sockets[..]); @@ -69,38 +78,50 @@ impl UbloxStack { dropped_sockets: heapless::Vec::new(), }; - let inner = Inner { - device, - link_up: false, - dns_result: None, - dns_waker: WakerRegistration::new(), - }; + // let inner = Inner { + // device, + // link_up: false, + // dns_result: None, + // dns_waker: WakerRegistration::new(), + // }; Self { socket: RefCell::new(socket), - inner: RefCell::new(inner), + device: RefCell::new(device), } } - #[allow(dead_code)] - fn with(&self, f: impl FnOnce(&SocketStack, &Inner) -> R) -> R { - f(&*self.socket.borrow(), &*self.inner.borrow()) - } - - fn with_mut(&self, f: impl FnOnce(&mut SocketStack, &mut Inner) -> R) -> R { - f( - &mut *self.socket.borrow_mut(), - &mut *self.inner.borrow_mut(), - ) - } - pub async fn run(&self) -> ! { - poll_fn(|cx| { - self.with_mut(|s, i| i.poll(cx, s)); - Poll::<()>::Pending - }) - .await; - unreachable!() + loop { + let poll_at = Timer::at(Instant::now() + Duration::from_millis(10)); + pin_mut!(poll_at); + + let mut device = self.device.borrow_mut(); + + match select( + device.urc_subscription.next_message_pure(), + poll_at, + // poll_fn(|cx| Poll::Ready(device.link_state(cx))), + ) + .await + { + Either::First(event) => { + self.socket_rx(event).await; + } + Either::Second(_) => { + self.socket_tx().await; + } // Either3::Third(new_state) => { + // Update link up + // let old_link_up = self.link_up; + // self.link_up = new_state == LinkState::Up; + + // // Print when changed + // if old_link_up != self.link_up { + // defmt::info!("link_up = {:?}", self.link_up); + // } + // } + } + } } /// Make a query for a given name and return the corresponding IP addresses. @@ -109,270 +130,194 @@ impl UbloxStack { &self, name: &str, addr_type: embedded_nal_async::AddrType, - ) -> Result, dns::Error> { - // For A and AAAA queries we try detect whether `name` is just an IP address - match addr_type { - embedded_nal_async::AddrType::IPv4 => { - if let Ok(ip) = name.parse().map(IpAddr::V4) { - return Ok([ip].into_iter().collect()); - } - } - embedded_nal_async::AddrType::IPv6 => { - if let Ok(ip) = name.parse().map(IpAddr::V6) { - return Ok([ip].into_iter().collect()); - } - } - _ => {} - } - - poll_fn(|cx| { - self.with_mut(|_s, i| { - i.dns_result = None; - Poll::Ready( - i.send_packet(cx, SocketTx::Dns(name)) - .map_err(|_| dns::Error::Failed), - ) - }) - }) - .await?; - - poll_fn(|cx| { - self.with_mut(|_s, i| match i.dns_result { - Some(Ok(ip)) => Poll::Ready(Ok([ip].into_iter().collect())), - Some(Err(_)) => Poll::Ready(Err(dns::Error::Failed)), - None => { - i.dns_waker.register(cx.waker()); - Poll::Pending - } - }) - }) - .await - } -} - -impl Inner { - fn poll(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) { - s.waker.register(cx.waker()); - - self.socket_rx(cx, s); - self.socket_tx(cx, s); - - // Handle delayed close-by-drop here - while let Some(dropped_peer_handle) = s.dropped_sockets.pop() { - defmt::warn!("Handling dropped socket {}", dropped_peer_handle); - self.send_packet(cx, SocketTx::Disconnect(dropped_peer_handle)) - .ok(); - } - // Update link up - let old_link_up = self.link_up; - self.link_up = self.device.link_state(cx) == LinkState::Up; - - // Print when changed - if old_link_up != self.link_up { - defmt::info!("link_up = {:?}", self.link_up); - } + ) -> Result { + DnsSocket::new(self).query(name, addr_type).await } - fn socket_rx(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) { - while let Some((rx_token, _)) = self.device.receive(cx) { - if let Err(e) = rx_token.consume(|a| { - match postcard::from_bytes::(a)? { - SocketRx::Data(packet) => { - for (_handle, socket) in s.sockets.iter_mut() { - match socket { - #[cfg(feature = "socket-udp")] - Socket::Udp(udp) - if udp.edm_channel == Some(packet.edm_channel) - && udp.may_recv() => - { - udp.rx_enqueue_slice(&packet.payload); - break; - } - #[cfg(feature = "socket-tcp")] - Socket::Tcp(tcp) - if tcp.edm_channel == Some(packet.edm_channel) - && tcp.may_recv() => - { - tcp.rx_enqueue_slice(&packet.payload); - break; - } - _ => {} - } + async fn socket_rx(&self, event: EdmEvent) { + match event { + EdmEvent::IPv4ConnectEvent(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + let mut s = self.socket.borrow_mut(); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, &mut s); + } + EdmEvent::IPv6ConnectEvent(ev) => { + let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); + let mut s = self.socket.borrow_mut(); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, &mut s); + } + EdmEvent::DisconnectEvent(channel_id) => { + let mut s = self.socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.edm_channel == Some(channel_id) => { + udp.edm_channel = None; + break; } - } - SocketRx::Ipv4Connect(ev) => { - let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); - Self::connect_event(ev.channel_id, ev.protocol, endpoint, s); - } - SocketRx::Ipv6Connect(ev) => { - let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); - Self::connect_event(ev.channel_id, ev.protocol, endpoint, s); - } - SocketRx::Disconnect(Disconnect::EdmChannel(channel_id)) => { - for (_handle, socket) in s.sockets.iter_mut() { - match socket { - #[cfg(feature = "socket-udp")] - Socket::Udp(udp) if udp.edm_channel == Some(channel_id) => { - udp.edm_channel = None; - break; - } - #[cfg(feature = "socket-tcp")] - Socket::Tcp(tcp) if tcp.edm_channel == Some(channel_id) => { - tcp.edm_channel = None; - break; - } - _ => {} - } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.edm_channel == Some(channel_id) => { + tcp.edm_channel = None; + break; } + _ => {} } - SocketRx::Disconnect(Disconnect::Peer(peer_handle)) => { - for (_handle, socket) in s.sockets.iter_mut() { - match socket { - #[cfg(feature = "socket-udp")] - Socket::Udp(udp) if udp.peer_handle == Some(peer_handle) => { - tcp.peer_handle = None; - udp.set_state(UdpState::TimeWait); - break; - } - #[cfg(feature = "socket-tcp")] - Socket::Tcp(tcp) if tcp.peer_handle == Some(peer_handle) => { - tcp.peer_handle = None; - tcp.set_state(TcpState::TimeWait); - break; - } - _ => {} + } + } + EdmEvent::DataEvent(DataEvent { channel_id, data }) => { + let mut s = self.socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) + if udp.edm_channel == Some(channel_id) && udp.may_recv() => + { + let n = udp.rx_enqueue_slice(&data); + if n < data.len() { + defmt::error!( + "[{}] UDP RX data overflow! Discarding {} bytes", + udp.peer_handle, + data.len() - n + ); } + break; } - } - SocketRx::PeerHandle(socket_handle, peer_handle) => { - for (handle, socket) in s.sockets.iter_mut() { - if handle == socket_handle { - match socket { - #[cfg(feature = "socket-udp")] - Socket::Udp(udp) => { - udp.peer_handle = Some(peer_handle); - } - #[cfg(feature = "socket-tcp")] - Socket::Tcp(tcp) => { - tcp.peer_handle = Some(peer_handle); - } - _ => {} - } - break; + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) + if tcp.edm_channel == Some(channel_id) && tcp.may_recv() => + { + let n = tcp.rx_enqueue_slice(&data); + if n < data.len() { + defmt::error!( + "[{}] TCP RX data overflow! Discarding {} bytes", + tcp.peer_handle, + data.len() - n + ); } + break; } + _ => {} } - SocketRx::Ping(res) => { - self.dns_result = Some(res); - self.dns_waker.wake(); + } + } + EdmEvent::ATEvent(Urc::PeerDisconnected(PeerDisconnected { handle })) => { + let mut s = self.socket.borrow_mut(); + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) if udp.peer_handle == Some(handle) => { + udp.peer_handle = None; + udp.set_state(UdpState::TimeWait); + break; + } + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) if tcp.peer_handle == Some(handle) => { + tcp.peer_handle = None; + tcp.set_state(TcpState::TimeWait); + break; + } + _ => {} } } - Ok::<_, postcard::Error>(()) - }) { - defmt::error!("Socket RX failed {:?}", e) - }; + } + EdmEvent::ATEvent(Urc::PingResponse(PingResponse { ip, .. })) => { + // TODO: Check that the result corresponds to the requested hostname? + // self.dns_result = Some(Ok(ip)); + // self.dns_waker.wake(); + } + EdmEvent::ATEvent(Urc::PingErrorResponse(PingErrorResponse { error: _ })) => { + // self.dns_result = Some(Err(())); + // self.dns_waker.wake(); + } + _ => {} } } - fn socket_tx(&mut self, cx: &mut Context<'_>, s: &mut SocketStack) { - for (handle, socket) in s.sockets.iter_mut() { - // if !socket.egress_permitted(self.inner.now, |ip_addr| self.inner.has_neighbor(&ip_addr)) - // { - // continue; + async fn socket_tx(&self) { + poll_fn(|cx| { + // Handle delayed close-by-drop here + // while let Some(dropped_peer_handle) = s.dropped_sockets.pop() { + // defmt::warn!("Handling dropped socket {}", dropped_peer_handle); + // self.device + // .at + // .send_edm(ClosePeerConnection { + // peer_handle: dropped_peer_handle, + // }) + // .await + // .ok(); // } - - let result = match socket { - #[cfg(feature = "socket-udp")] - Socket::Udp(udp) => todo!(), - #[cfg(feature = "socket-tcp")] - Socket::Tcp(tcp) => { - let res = match tcp.poll() { - Ok(TcpState::Closed) => { - if let Some(addr) = tcp.remote_endpoint() { - let url = PeerUrlBuilder::new() - .address(&addr) - .set_local_port(tcp.local_port) - .tcp::<128>() - .unwrap(); - - let pkt = SocketTx::Connect(Connect { - socket_handle: handle, - url: &url, - }); - - self.send_packet(cx, pkt).and_then(|r| { - tcp.set_state(TcpState::SynSent); - Ok(r) - }) - } else { - Ok(()) - } - } - // We transmit data in all states where we may have data in the buffer, - // or the transmit half of the connection is still open. - Ok(TcpState::Established) - | Ok(TcpState::CloseWait) - | Ok(TcpState::LastAck) => { - if let Some(edm_channel) = tcp.edm_channel { - tcp.tx_dequeue(|payload| { - if payload.len() > 0 { - let pkt = SocketTx::Data(DataPacket { - edm_channel, - payload, - }); - - (payload.len(), self.send_packet(cx, pkt)) - } else { - (0, Ok(())) + let mut s = self.socket.borrow_mut(); + + for (_handle, socket) in s.sockets.iter_mut() { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) => todo!(), + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) => { + tcp.poll(); + + match tcp.state() { + TcpState::Closed => { + if let Some(addr) = tcp.remote_endpoint() { + let url = PeerUrlBuilder::new() + .address(&addr) + .set_local_port(tcp.local_port) + .tcp::<128>() + .unwrap(); + + let mut device = self.device.borrow_mut(); + let fut = device.at.send_edm(ConnectPeer { url: &url }); + pin_mut!(fut); + if let Ok(ConnectPeerResponse { peer_handle }) = + core::task::ready!(fut.poll(cx)) + { + tcp.peer_handle = Some(peer_handle); + tcp.set_state(TcpState::SynSent); } - }) - } else { - Ok(()) + } } - } - Ok(TcpState::FinWait1) => { - let pkt = SocketTx::Disconnect(tcp.peer_handle.unwrap()); - self.send_packet(cx, pkt) - } - Ok(TcpState::Listen) => todo!(), - Ok(TcpState::SynReceived) => todo!(), - Err(_) => Err(()), - _ => Ok(()), - }; - - if let Some(poll_at) = tcp.poll_at() { - let t = Timer::at(poll_at); - pin_mut!(t); - if t.poll(cx).is_ready() { - cx.waker().wake_by_ref(); - } + // We transmit data in all states where we may have data in the buffer, + // or the transmit half of the connection is still open. + TcpState::Established | TcpState::CloseWait | TcpState::LastAck => { + // if let Some(edm_channel) = tcp.edm_channel { + // defmt::error!("Sending data on {}", edm_channel); + // tcp.async_tx_dequeue(|payload| async { + // let len = core::cmp::max(payload.len(), DATA_PACKAGE_SIZE); + // let res = self + // .device + // .at + // .send(EdmDataCommand { + // channel: edm_channel, + // data: &payload[..len], + // }) + // .await; + + // (len, res) + // }) + // .await + // .ok(); + // } + } + TcpState::FinWait1 => { + // self.device + // .at + // .send_edm(ClosePeerConnection { + // peer_handle: tcp.peer_handle.unwrap(), + // }) + // .await + // .ok(); + } + TcpState::Listen => todo!(), + TcpState::SynReceived => todo!(), + _ => {} + }; } - - res - } - _ => Ok(()), - }; - - match result { - Err(_) => { - break; - } // Device buffer full. - Ok(()) => {} + _ => {} + }; } - } - } - - pub(crate) fn send_packet(&mut self, cx: &mut Context, pkt: SocketTx) -> Result<(), ()> { - let Some(tx_token) = self.device.transmit(cx) else { - return Err(()); - }; - - let len = postcard::experimental::serialized_size(&pkt).map_err(drop)?; - tx_token.consume(len, |tx_buf| { - postcard::to_slice(&pkt, tx_buf).map(drop).map_err(drop) - })?; - - Ok(()) + Poll::Ready(()) + }) + .await; } fn connect_event( @@ -406,90 +351,3 @@ impl Inner { } } } - -#[derive(Serialize, Deserialize)] -pub enum SocketTx<'a> { - #[serde(borrow)] - Data(DataPacket<'a>), - #[serde(borrow)] - Connect(Connect<'a>), - Disconnect(PeerHandle), - Dns(&'a str), -} - -impl<'a> defmt::Format for SocketTx<'a> { - fn format(&self, fmt: defmt::Formatter) { - match self { - SocketTx::Data(_) => defmt::write!(fmt, "SocketTx::Data"), - SocketTx::Connect(_) => defmt::write!(fmt, "SocketTx::Connect"), - SocketTx::Disconnect(_) => defmt::write!(fmt, "SocketTx::Disconnect"), - SocketTx::Dns(_) => defmt::write!(fmt, "SocketTx::Dns"), - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct Connect<'a> { - pub url: &'a str, - pub socket_handle: SocketHandle, -} - -#[derive(Serialize, Deserialize)] -pub enum SocketRx<'a> { - #[serde(borrow)] - Data(DataPacket<'a>), - PeerHandle(SocketHandle, PeerHandle), - Ipv4Connect(IPv4ConnectEvent), - Ipv6Connect(IPv6ConnectEvent), - Disconnect(Disconnect), - Ping(Result), -} - -#[derive(Serialize, Deserialize)] -pub enum Disconnect { - EdmChannel(ChannelId), - Peer(PeerHandle), -} - -impl<'a> defmt::Format for SocketRx<'a> { - fn format(&self, fmt: defmt::Formatter) { - match self { - SocketRx::Data(_) => defmt::write!(fmt, "SocketRx::Data"), - SocketRx::PeerHandle(_, _) => defmt::write!(fmt, "SocketRx::PeerHandle"), - SocketRx::Ipv4Connect(_) => defmt::write!(fmt, "SocketRx::Ipv4Connect"), - SocketRx::Ipv6Connect(_) => defmt::write!(fmt, "SocketRx::Ipv6Connect"), - SocketRx::Disconnect(_) => defmt::write!(fmt, "SocketRx::Disconnect"), - SocketRx::Ping(_) => defmt::write!(fmt, "SocketRx::Ping"), - } - } -} - -impl From for SocketRx<'_> { - fn from(value: IPv4ConnectEvent) -> Self { - Self::Ipv4Connect(value) - } -} - -impl From for SocketRx<'_> { - fn from(value: IPv6ConnectEvent) -> Self { - Self::Ipv6Connect(value) - } -} - -impl From for SocketRx<'_> { - fn from(value: ChannelId) -> Self { - Self::Disconnect(Disconnect::EdmChannel(value)) - } -} - -impl<'a> From> for SocketRx<'a> { - fn from(value: DataPacket<'a>) -> Self { - Self::Data(value) - } -} - -#[derive(Serialize, Deserialize)] -pub struct DataPacket<'a> { - pub edm_channel: ChannelId, - pub payload: &'a [u8], -} diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index db7d242..fa9c3e1 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -3,6 +3,7 @@ use core::future::poll_fn; use core::mem; use core::task::Poll; +use atat::asynch::AtatClient; use embedded_nal_async::SocketAddr; use ublox_sockets::{tcp, SocketHandle, TcpState}; @@ -67,7 +68,11 @@ impl<'a> TcpWriter<'a> { } impl<'a> TcpSocket<'a> { - pub fn new(stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8]) -> Self { + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + ) -> Self { let s = &mut *stack.socket.borrow_mut(); let rx_buffer: &'static mut [u8] = unsafe { mem::transmute(rx_buffer) }; let tx_buffer: &'static mut [u8] = unsafe { mem::transmute(tx_buffer) }; @@ -355,20 +360,28 @@ pub mod client { use super::*; /// TCP client capable of creating up to N multiple connections with tx and rx buffers according to TX_SZ and RX_SZ. - pub struct TcpClient<'d, const N: usize, const TX_SZ: usize = 1024, const RX_SZ: usize = 1024> { - pub(crate) stack: &'d UbloxStack, + pub struct TcpClient< + 'd, + AT: AtatClient + 'static, + const N: usize, + const TX_SZ: usize = 1024, + const RX_SZ: usize = 1024, + > { + pub(crate) stack: &'d UbloxStack, pub(crate) state: &'d TcpClientState, } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpClient<'d, N, TX_SZ, RX_SZ> { + impl<'d, AT: AtatClient, const N: usize, const TX_SZ: usize, const RX_SZ: usize> + TcpClient<'d, AT, N, TX_SZ, RX_SZ> + { /// Create a new TcpClient - pub fn new(stack: &'d UbloxStack, state: &'d TcpClientState) -> Self { + pub fn new(stack: &'d UbloxStack, state: &'d TcpClientState) -> Self { Self { stack, state } } } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_nal_async::TcpConnect - for TcpClient<'d, N, TX_SZ, RX_SZ> + impl<'d, AT: AtatClient, const N: usize, const TX_SZ: usize, const RX_SZ: usize> + embedded_nal_async::TcpConnect for TcpClient<'d, AT, N, TX_SZ, RX_SZ> { type Error = Error; type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; @@ -400,8 +413,8 @@ pub mod client { impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpConnection<'d, N, TX_SZ, RX_SZ> { - fn new( - stack: &'d UbloxStack, + fn new( + stack: &'d UbloxStack, state: &'d TcpClientState, ) -> Result { let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; diff --git a/src/lib.rs b/src/lib.rs index ccf77d6..54c07cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![cfg_attr(feature = "async", feature(generic_const_exprs))] #![cfg_attr(feature = "async", feature(async_fn_in_trait))] #![cfg_attr(feature = "async", feature(impl_trait_projections))] +#![cfg_attr(feature = "async", feature(type_alias_impl_trait))] #[cfg(feature = "async")] pub mod asynch; From 2230340ec116703d49814b5be3f5e0bd8e4ac3e7 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 5 Jul 2023 13:01:03 +0200 Subject: [PATCH 05/16] Working dns + tcp at 11/6kbps --- examples/rpi-pico/src/bin/embassy-async.rs | 38 +- examples/rpi-pico/src/bin/embassy-perf.rs | 37 +- src/asynch/control.rs | 7 + src/asynch/state.rs | 16 +- src/asynch/ublox_stack/dns.rs | 126 +++--- src/asynch/ublox_stack/mod.rs | 434 ++++++++++++++------- src/asynch/ublox_stack/tcp.rs | 4 +- src/command/data_mode/mod.rs | 2 +- 8 files changed, 436 insertions(+), 228 deletions(-) diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index 4dd0816..85d0702 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -183,15 +183,6 @@ async fn main(spawner: Spawner) { // And now we can use it! defmt::info!("Device initialized!"); - // spawner - // .spawn(echo_task( - // &stack, - // "tcpbin.com", - // 4242, - // Duration::from_millis(500), - // )) - // .unwrap(); - let mut rx_buffer = [0; 256]; let mut tx_buffer = [0; 256]; let mut buf = [0; 256]; @@ -203,14 +194,25 @@ async fn main(spawner: Spawner) { match control.join_wpa2("test", "1234abcd").await { Ok(_) => { defmt::info!("Network connected!"); - // spawner - // .spawn(echo_task( - // &stack, - // "echo.u-blox.com", - // 7, - // Duration::from_secs(1), - // )) - // .unwrap(); + spawner + .spawn(echo_task( + &stack, + // "echo.u-blox.com", + // 7, + "tcpbin.com", + 4242, + Duration::from_secs(1), + )) + .unwrap(); + + spawner + .spawn(echo_task( + &stack, + "tcpbin.com", + 4242, + Duration::from_millis(500), + )) + .unwrap(); break; } Err(err) => { @@ -225,7 +227,7 @@ async fn main(spawner: Spawner) { let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); // // socket.set_timeout(Some(Duration::from_secs(10))); - let remote: SocketAddr = (Ipv4Addr::new(192, 168, 73, 183), 4444).into(); + let remote: SocketAddr = (Ipv4Addr::new(192, 168, 1, 183), 4444).into(); defmt::info!("Connecting... {}", defmt::Debug2Format(&remote)); if let Err(e) = socket.connect(remote).await { defmt::warn!("connect error: {:?}", e); diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs index 0a52ec2..b500ffd 100644 --- a/examples/rpi-pico/src/bin/embassy-perf.rs +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -10,7 +10,7 @@ use embassy_rp::gpio::{Level, Output}; use embassy_rp::peripherals::{PIN_26, UART1}; use embassy_rp::uart::BufferedInterruptHandler; use embassy_rp::{bind_interrupts, uart}; -use embassy_time::{with_timeout, Duration}; +use embassy_time::{with_timeout, Duration, Timer}; use no_std_net::Ipv4Addr; use static_cell::make_static; use ublox_short_range::asynch::runner::Runner; @@ -66,16 +66,10 @@ async fn main(spawner: Spawner) { let tx_buf = &mut make_static!([0u8; 64])[..]; let rx_buf = &mut make_static!([0u8; 64])[..]; + let mut config = uart::Config::default(); + config.baudrate = 115200; let uart = uart::BufferedUart::new_with_rtscts( - uart, - Irqs, - tx_pin, - rx_pin, - rts_pin, - cts_pin, - tx_buf, - rx_buf, - uart::Config::default(), + uart, Irqs, tx_pin, rx_pin, rts_pin, cts_pin, tx_buf, rx_buf, config, ); let (rx, tx) = uart.split(); @@ -109,28 +103,32 @@ async fn main(spawner: Spawner) { defmt::info!("Device initialized!"); let down = test_download(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; let up = test_upload(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; let updown = test_upload_download(stack).await; + Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; - assert!(down > TEST_EXPECTED_DOWNLOAD_KBPS); - assert!(up > TEST_EXPECTED_UPLOAD_KBPS); - assert!(updown > TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS); + // assert!(down > TEST_EXPECTED_DOWNLOAD_KBPS); + // assert!(up > TEST_EXPECTED_UPLOAD_KBPS); + // assert!(updown > TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS); defmt::info!("Test OK"); cortex_m::asm::bkpt(); } // Test-only wifi network, no internet access! -const WIFI_NETWORK: &str = "EmbassyTest"; -const WIFI_PASSWORD: &str = "V8YxhKt5CdIAJFud"; +const WIFI_NETWORK: &str = "WiFimodem-7A76"; +const WIFI_PASSWORD: &str = "ndzwqzyhhd"; const TEST_DURATION: usize = 10; +const SETTLE_TIME: usize = 5; const TEST_EXPECTED_DOWNLOAD_KBPS: usize = 300; const TEST_EXPECTED_UPLOAD_KBPS: usize = 300; const TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS: usize = 300; const RX_BUFFER_SIZE: usize = 4096; const TX_BUFFER_SIZE: usize = 4096; -const SERVER_ADDRESS: Ipv4Addr = Ipv4Addr::new(192, 168, 2, 2); +const SERVER_ADDRESS: Ipv4Addr = Ipv4Addr::new(192, 168, 0, 8); const DOWNLOAD_PORT: u16 = 4321; const UPLOAD_PORT: u16 = 4322; const UPLOAD_DOWNLOAD_PORT: u16 = 4323; @@ -279,12 +277,15 @@ async fn test_upload_download(stack: &'static UbloxStack) -> usize { } }; - with_timeout( + if with_timeout( Duration::from_secs(TEST_DURATION as _), join(tx_fut, rx_fut), ) .await - .ok(); + .is_err() + { + defmt::error!("Test timed out"); + } let kbps = (total + 512) / 1024 / TEST_DURATION; defmt::info!("upload+download: {} kB/s", kbps); diff --git a/src/asynch/control.rs b/src/asynch/control.rs index 289207f..96dc9d5 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -175,6 +175,13 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }; } + self.at + .send_edm(ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Reset, + }) + .await?; + self.at .send_edm(SetWifiStationConfig { config_id: CONFIG_ID, diff --git a/src/asynch/state.rs b/src/asynch/state.rs index 22e115b..f63366d 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -112,22 +112,28 @@ pub fn new<'d, AT: AtatClient>( shared: &state.shared, }, Device { - shared: &state.shared, + shared: TestShared { + inner: &state.shared, + }, urc_subscription, at, }, ) } +pub struct TestShared<'d> { + inner: &'d Mutex>, +} + pub struct Device<'d, AT: AtatClient> { - pub(crate) shared: &'d Mutex>, + pub(crate) shared: TestShared<'d>, pub(crate) at: AtHandle<'d, AT>, pub(crate) urc_subscription: UrcSubscription<'d, EdmEvent>, } -impl<'d, AT: AtatClient> Device<'d, AT> { - pub fn link_state(&self, cx: &mut Context) -> LinkState { - self.shared.lock(|s| { +impl<'d> TestShared<'d> { + pub fn link_state(&mut self, cx: &mut Context) -> LinkState { + self.inner.lock(|s| { let s = &mut *s.borrow_mut(); s.waker.register(cx.waker()); s.link_state diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index 97cfc79..5c0dbf4 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -1,36 +1,12 @@ -use core::task::Poll; +use core::{cell::RefCell, future::poll_fn, task::Poll}; use atat::asynch::AtatClient; use embedded_nal_async::AddrType; -use futures::Future; use no_std_net::IpAddr; -use crate::command::ping::Ping; +use crate::asynch::ublox_stack::DnsState; -use super::UbloxStack; - -struct DnsFuture<'a, AT: AtatClient + 'static> { - stack: &'a UbloxStack, -} - -impl<'a, AT: AtatClient> Future for DnsFuture<'a, AT> { - type Output = Result; - - fn poll( - self: core::pin::Pin<&mut Self>, - cx: &mut core::task::Context<'_>, - ) -> Poll { - // let i = &mut *self.stack.inner.borrow_mut(); - // match i.dns_result { - // Some(Ok(ip)) => Poll::Ready(Ok(ip)), - // Some(Err(_)) => Poll::Ready(Err(Error::Failed)), - // None => { - // i.dns_waker.register(cx.waker()); - Poll::Pending - // } - // } - } -} +use super::{DnsQuery, SocketStack, UbloxStack}; /// Errors returned by DnsSocket. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -49,16 +25,16 @@ pub enum Error { /// This exists only for compatibility with crates that use `embedded-nal-async`. /// Prefer using [`Stack::dns_query`](crate::Stack::dns_query) directly if you're /// not using `embedded-nal-async`. -pub struct DnsSocket<'a, AT: AtatClient + 'static> { - stack: &'a UbloxStack, +pub struct DnsSocket<'a> { + stack: &'a RefCell, } -impl<'a, AT: AtatClient> DnsSocket<'a, AT> { +impl<'a> DnsSocket<'a> { /// Create a new DNS socket using the provided stack. - /// - /// NOTE: If using DHCP, make sure it has reconfigured the stack to ensure the DNS servers are updated. - pub fn new(stack: &'a UbloxStack) -> Self { - Self { stack } + pub fn new(stack: &'a UbloxStack) -> Self { + Self { + stack: &stack.socket, + } } /// Make a query for a given name and return the corresponding IP addresses. @@ -77,23 +53,77 @@ impl<'a, AT: AtatClient> DnsSocket<'a, AT> { _ => {} } - // let i = &mut *self.stack.inner.borrow_mut(); - // i.dns_result = None; - // i.device - // .at - // .send_edm(Ping { - // hostname: name, - // retry_num: 1, - // }) - // .await - // .map_err(|_| Error::Failed)?; - - DnsFuture { stack: self.stack }.await + { + let mut s = self.stack.borrow_mut(); + if s.dns_queries + .insert(heapless::String::from(name), DnsQuery::new()) + .is_err() + { + defmt::error!( + "Attempted to start more simultaneous DNS requests than the (4) supported" + ); + } + s.waker.wake(); + } + + #[must_use = "to delay the drop handler invocation to the end of the scope"] + struct OnDrop { + f: core::mem::MaybeUninit, + } + + impl OnDrop { + fn new(f: F) -> Self { + Self { + f: core::mem::MaybeUninit::new(f), + } + } + + fn defuse(self) { + core::mem::forget(self) + } + } + + impl Drop for OnDrop { + fn drop(&mut self) { + unsafe { self.f.as_ptr().read()() } + } + } + + let drop = OnDrop::new(|| { + let mut s = self.stack.borrow_mut(); + s.dns_queries.remove(&heapless::String::from(name)); + }); + + let res = poll_fn(|cx| { + let mut s = self.stack.borrow_mut(); + let query = s + .dns_queries + .get_mut(&heapless::String::from(name)) + .unwrap(); + match query.state { + DnsState::Ok(ip) => { + s.dns_queries.remove(&heapless::String::from(name)); + return Poll::Ready(Ok(ip)); + } + DnsState::Err => { + s.dns_queries.remove(&heapless::String::from(name)); + return Poll::Ready(Err(Error::Failed)); + } + _ => { + query.waker.register(cx.waker()); + Poll::Pending + } + } + }) + .await; + + drop.defuse(); + + res } } -// #[cfg(all(feature = "unstable-traits", feature = "nightly"))] -impl<'a, AT: AtatClient> embedded_nal_async::Dns for DnsSocket<'a, AT> { +impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { type Error = Error; async fn get_host_by_name( diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index 1f7bcc7..b8b14bf 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -7,8 +7,10 @@ pub mod dns; use core::cell::RefCell; use core::future::poll_fn; +use core::ops::{DerefMut, Rem}; use core::task::Poll; +use crate::asynch::state::Device; use crate::command::data_mode::responses::ConnectPeerResponse; use crate::command::data_mode::urc::PeerDisconnected; use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; @@ -16,25 +18,32 @@ use crate::command::edm::types::{DataEvent, Protocol, DATA_PACKAGE_SIZE}; use crate::command::edm::urc::EdmEvent; use crate::command::edm::EdmDataCommand; use crate::command::ping::urc::{PingErrorResponse, PingResponse}; +use crate::command::ping::Ping; use crate::command::Urc; use crate::peer_builder::PeerUrlBuilder; use self::dns::DnsSocket; use super::state::{self, LinkState}; +use super::AtHandle; use atat::asynch::AtatClient; -use embassy_futures::select::{select, select3, Either, Either3}; +use atomic_polyfill::{AtomicBool, AtomicU8, Ordering}; +use embassy_futures::select::{select4, Either4}; use embassy_sync::waitqueue::WakerRegistration; -use embassy_time::{Duration, Instant, Timer}; +use embassy_time::{Duration, Ticker}; use embedded_nal_async::SocketAddr; -use futures::{pin_mut, Future}; +use futures::pin_mut; use no_std_net::IpAddr; -use ublox_sockets::{AnySocket, ChannelId, PeerHandle, Socket, SocketSet, SocketStorage}; +use ublox_sockets::{ + AnySocket, ChannelId, PeerHandle, Socket, SocketHandle, SocketSet, SocketStorage, +}; #[cfg(feature = "socket-tcp")] use ublox_sockets::TcpState; +const MAX_HOSTNAME_LEN: usize = 64; + pub struct StackResources { sockets: [SocketStorage<'static>; SOCK], } @@ -48,24 +57,42 @@ impl StackResources { } pub struct UbloxStack { - pub(crate) socket: RefCell, + socket: RefCell, device: RefCell>, + last_tx_socket: AtomicU8, + should_tx: AtomicBool, + link_up: AtomicBool, } -// struct Inner { -// device: state::Device<'static, AT>, -// link_up: bool, -// dns_result: Option>, -// dns_waker: WakerRegistration, -// } +enum DnsState { + New, + Pending, + Ok(IpAddr), + Err, +} -pub(crate) struct SocketStack { - pub(crate) sockets: SocketSet<'static>, - pub(crate) waker: WakerRegistration, +struct DnsQuery { + state: DnsState, + waker: WakerRegistration, +} + +impl DnsQuery { + pub fn new() -> Self { + Self { + state: DnsState::New, + waker: WakerRegistration::new(), + } + } +} + +struct SocketStack { + sockets: SocketSet<'static>, + waker: WakerRegistration, + dns_queries: heapless::FnvIndexMap, DnsQuery, 4>, dropped_sockets: heapless::Vec, } -impl UbloxStack { +impl UbloxStack { pub fn new( device: state::Device<'static, AT>, resources: &'static mut StackResources, @@ -74,52 +101,78 @@ impl UbloxStack { let socket = SocketStack { sockets, + dns_queries: heapless::IndexMap::new(), waker: WakerRegistration::new(), dropped_sockets: heapless::Vec::new(), }; - // let inner = Inner { - // device, - // link_up: false, - // dns_result: None, - // dns_waker: WakerRegistration::new(), - // }; - Self { socket: RefCell::new(socket), device: RefCell::new(device), + last_tx_socket: AtomicU8::new(0), + link_up: AtomicBool::new(false), + should_tx: AtomicBool::new(false), } } pub async fn run(&self) -> ! { loop { - let poll_at = Timer::at(Instant::now() + Duration::from_millis(10)); - pin_mut!(poll_at); + // FIXME: It feels like this can be written smarter/simpler? + let should_tx = poll_fn(|cx| match self.should_tx.load(Ordering::Relaxed) { + true => { + self.should_tx.store(false, Ordering::Relaxed); + Poll::Ready(()) + } + false => { + self.should_tx.store(true, Ordering::Relaxed); + self.socket.borrow_mut().waker.register(cx.waker()); + Poll::<()>::Pending + } + }); + + let ticker = Ticker::every(Duration::from_millis(100)); + pin_mut!(ticker); let mut device = self.device.borrow_mut(); + let Device { + ref mut urc_subscription, + ref mut shared, + ref mut at, + } = device.deref_mut(); - match select( - device.urc_subscription.next_message_pure(), - poll_at, - // poll_fn(|cx| Poll::Ready(device.link_state(cx))), + match select4( + urc_subscription.next_message_pure(), + should_tx, + ticker.next(), + poll_fn( + |cx| match (self.link_up.load(Ordering::Relaxed), shared.link_state(cx)) { + (true, LinkState::Down) => Poll::Ready(LinkState::Down), + (false, LinkState::Up) => Poll::Ready(LinkState::Up), + _ => Poll::Pending, + }, + ), ) .await { - Either::First(event) => { - self.socket_rx(event).await; + Either4::First(event) => { + Self::socket_rx(event, &self.socket); + } + Either4::Second(_) | Either4::Third(_) => { + if let Some(ev) = self.tx_event() { + Self::socket_tx(ev, &self.socket, at).await; + } + } + Either4::Fourth(new_state) => { + // Update link up + let old_link_up = self.link_up.load(Ordering::Relaxed); + let new_link_up = new_state == LinkState::Up; + self.link_up.store(new_link_up, Ordering::Relaxed); + + // Print when changed + if old_link_up != new_link_up { + defmt::info!("link_up = {:?}", new_link_up); + } } - Either::Second(_) => { - self.socket_tx().await; - } // Either3::Third(new_state) => { - // Update link up - // let old_link_up = self.link_up; - // self.link_up = new_state == LinkState::Up; - - // // Print when changed - // if old_link_up != self.link_up { - // defmt::info!("link_up = {:?}", self.link_up); - // } - // } } } } @@ -134,20 +187,18 @@ impl UbloxStack { DnsSocket::new(self).query(name, addr_type).await } - async fn socket_rx(&self, event: EdmEvent) { + fn socket_rx(event: EdmEvent, socket: &RefCell) { match event { EdmEvent::IPv4ConnectEvent(ev) => { let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); - let mut s = self.socket.borrow_mut(); - Self::connect_event(ev.channel_id, ev.protocol, endpoint, &mut s); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, socket); } EdmEvent::IPv6ConnectEvent(ev) => { let endpoint = SocketAddr::new(ev.remote_ip.into(), ev.remote_port); - let mut s = self.socket.borrow_mut(); - Self::connect_event(ev.channel_id, ev.protocol, endpoint, &mut s); + Self::connect_event(ev.channel_id, ev.protocol, endpoint, socket); } EdmEvent::DisconnectEvent(channel_id) => { - let mut s = self.socket.borrow_mut(); + let mut s = socket.borrow_mut(); for (_handle, socket) in s.sockets.iter_mut() { match socket { #[cfg(feature = "socket-udp")] @@ -165,7 +216,7 @@ impl UbloxStack { } } EdmEvent::DataEvent(DataEvent { channel_id, data }) => { - let mut s = self.socket.borrow_mut(); + let mut s = socket.borrow_mut(); for (_handle, socket) in s.sockets.iter_mut() { match socket { #[cfg(feature = "socket-udp")] @@ -201,7 +252,7 @@ impl UbloxStack { } } EdmEvent::ATEvent(Urc::PeerDisconnected(PeerDisconnected { handle })) => { - let mut s = self.socket.borrow_mut(); + let mut s = socket.borrow_mut(); for (_handle, socket) in s.sockets.iter_mut() { match socket { #[cfg(feature = "socket-udp")] @@ -220,112 +271,191 @@ impl UbloxStack { } } } - EdmEvent::ATEvent(Urc::PingResponse(PingResponse { ip, .. })) => { - // TODO: Check that the result corresponds to the requested hostname? - // self.dns_result = Some(Ok(ip)); - // self.dns_waker.wake(); + EdmEvent::ATEvent(Urc::PingResponse(PingResponse { + ip, hostname, rtt, .. + })) => { + let mut s = socket.borrow_mut(); + if let Some(query) = s.dns_queries.get_mut(&hostname) { + match query.state { + DnsState::Pending if rtt == -1 => { + // According to AT manual, rtt = -1 means the PING has timed out + query.state = DnsState::Err; + query.waker.wake(); + } + DnsState::Pending => { + query.state = DnsState::Ok(ip); + query.waker.wake(); + } + _ => {} + } + } } EdmEvent::ATEvent(Urc::PingErrorResponse(PingErrorResponse { error: _ })) => { - // self.dns_result = Some(Err(())); - // self.dns_waker.wake(); + let mut s = socket.borrow_mut(); + for (_, query) in s.dns_queries.iter_mut() { + match query.state { + DnsState::Pending => { + query.state = DnsState::Err; + query.waker.wake(); + } + _ => {} + } + } } _ => {} } } - async fn socket_tx(&self) { - poll_fn(|cx| { - // Handle delayed close-by-drop here - // while let Some(dropped_peer_handle) = s.dropped_sockets.pop() { - // defmt::warn!("Handling dropped socket {}", dropped_peer_handle); - // self.device - // .at - // .send_edm(ClosePeerConnection { - // peer_handle: dropped_peer_handle, - // }) - // .await - // .ok(); - // } - let mut s = self.socket.borrow_mut(); - - for (_handle, socket) in s.sockets.iter_mut() { - match socket { - #[cfg(feature = "socket-udp")] - Socket::Udp(udp) => todo!(), - #[cfg(feature = "socket-tcp")] - Socket::Tcp(tcp) => { - tcp.poll(); - - match tcp.state() { - TcpState::Closed => { - if let Some(addr) = tcp.remote_endpoint() { - let url = PeerUrlBuilder::new() - .address(&addr) - .set_local_port(tcp.local_port) - .tcp::<128>() - .unwrap(); - - let mut device = self.device.borrow_mut(); - let fut = device.at.send_edm(ConnectPeer { url: &url }); - pin_mut!(fut); - if let Ok(ConnectPeerResponse { peer_handle }) = - core::task::ready!(fut.poll(cx)) - { - tcp.peer_handle = Some(peer_handle); - tcp.set_state(TcpState::SynSent); - } - } + fn tx_event(&self) -> Option { + let mut s = self.socket.borrow_mut(); + for (hostname, query) in s.dns_queries.iter_mut() { + if let DnsState::New = query.state { + query.state = DnsState::Pending; + return Some(TxEvent::Dns { + hostname: hostname.clone(), + }); + } + } + + // Handle delayed close-by-drop here + if let Some(dropped_peer_handle) = s.dropped_sockets.pop() { + defmt::warn!("Handling dropped socket {}", dropped_peer_handle); + return Some(TxEvent::Close { + peer_handle: dropped_peer_handle, + }); + } + + // Make sure to give all sockets an even opportunity to TX + let skip = self + .last_tx_socket + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| { + let next = v + 1; + Some(next.rem(s.sockets.sockets.len() as u8)) + }) + .unwrap(); + + for (handle, socket) in s.sockets.iter_mut().skip(skip as usize) { + match socket { + #[cfg(feature = "socket-udp")] + Socket::Udp(udp) => todo!(), + #[cfg(feature = "socket-tcp")] + Socket::Tcp(tcp) => { + tcp.poll(); + + match tcp.state() { + TcpState::Closed => { + if let Some(addr) = tcp.remote_endpoint() { + let url = PeerUrlBuilder::new() + .address(&addr) + .set_local_port(tcp.local_port) + .tcp::<128>() + .unwrap(); + + return Some(TxEvent::Connect { + socket_handle: handle, + url, + }); } - // We transmit data in all states where we may have data in the buffer, - // or the transmit half of the connection is still open. - TcpState::Established | TcpState::CloseWait | TcpState::LastAck => { - // if let Some(edm_channel) = tcp.edm_channel { - // defmt::error!("Sending data on {}", edm_channel); - // tcp.async_tx_dequeue(|payload| async { - // let len = core::cmp::max(payload.len(), DATA_PACKAGE_SIZE); - // let res = self - // .device - // .at - // .send(EdmDataCommand { - // channel: edm_channel, - // data: &payload[..len], - // }) - // .await; - - // (len, res) - // }) - // .await - // .ok(); - // } + } + // We transmit data in all states where we may have data in the buffer, + // or the transmit half of the connection is still open. + TcpState::Established | TcpState::CloseWait | TcpState::LastAck => { + if let Some(edm_channel) = tcp.edm_channel { + defmt::warn!("{}", tcp); + return tcp.tx_dequeue(|payload| { + let len = core::cmp::min(payload.len(), DATA_PACKAGE_SIZE); + let res = if len != 0 { + Some(TxEvent::Send { + edm_channel, + data: heapless::Vec::from_slice(payload).unwrap(), + }) + } else { + None + }; + + (len, res) + }); } - TcpState::FinWait1 => { - // self.device - // .at - // .send_edm(ClosePeerConnection { - // peer_handle: tcp.peer_handle.unwrap(), - // }) - // .await - // .ok(); + } + TcpState::FinWait1 => { + return Some(TxEvent::Close { + peer_handle: tcp.peer_handle.unwrap(), + }); + } + TcpState::Listen => todo!(), + TcpState::SynReceived => todo!(), + _ => {} + }; + } + _ => {} + }; + } + + None + } + + async fn socket_tx(ev: TxEvent, socket: &RefCell, at: &mut AtHandle<'_, AT>) { + match ev { + TxEvent::Connect { socket_handle, url } => { + match at.send_edm(ConnectPeer { url: &url }).await { + Ok(ConnectPeerResponse { peer_handle }) => { + let mut s = socket.borrow_mut(); + let tcp = s + .sockets + .get_mut::(socket_handle); + tcp.peer_handle = Some(peer_handle); + tcp.set_state(TcpState::SynSent); + } + Err(e) => { + defmt::error!("Failed to connect?! {}", e) + } + } + } + TxEvent::Send { edm_channel, data } => { + defmt::warn!("Sending {} bytes on {}", data.len(), edm_channel); + at.send(EdmDataCommand { + channel: edm_channel, + data: &data, + }) + .await + .ok(); + } + TxEvent::Close { peer_handle } => { + at.send_edm(ClosePeerConnection { peer_handle }).await.ok(); + } + TxEvent::Dns { hostname } => { + match at + .send_edm(Ping { + hostname: &hostname, + retry_num: 1, + }) + .await + { + Ok(_) => {} + Err(_) => { + let mut s = socket.borrow_mut(); + if let Some(query) = s.dns_queries.get_mut(&hostname) { + match query.state { + DnsState::Pending => { + query.state = DnsState::Err; + query.waker.wake(); + } + _ => {} } - TcpState::Listen => todo!(), - TcpState::SynReceived => todo!(), - _ => {} - }; + } } - _ => {} - }; + } } - Poll::Ready(()) - }) - .await; + } } fn connect_event( channel_id: ChannelId, protocol: Protocol, endpoint: SocketAddr, - s: &mut SocketStack, + socket: &RefCell, ) { + let mut s = socket.borrow_mut(); for (_handle, socket) in s.sockets.iter_mut() { match protocol { #[cfg(feature = "socket-tcp")] @@ -351,3 +481,33 @@ impl UbloxStack { } } } + +// TODO: This extra data clone step can probably be avoided by adding a +// waker/context based API to ATAT. +enum TxEvent { + Connect { + socket_handle: SocketHandle, + url: heapless::String<128>, + }, + Send { + edm_channel: ChannelId, + data: heapless::Vec, + }, + Close { + peer_handle: PeerHandle, + }, + Dns { + hostname: heapless::String, + }, +} + +impl defmt::Format for TxEvent { + fn format(&self, fmt: defmt::Formatter) { + match self { + TxEvent::Connect { .. } => defmt::write!(fmt, "TxEvent::Connect"), + TxEvent::Send { .. } => defmt::write!(fmt, "TxEvent::Send"), + TxEvent::Close { .. } => defmt::write!(fmt, "TxEvent::Close"), + TxEvent::Dns { .. } => defmt::write!(fmt, "TxEvent::Dns"), + } + } +} diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index fa9c3e1..5875d3e 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -201,7 +201,9 @@ impl<'a> Drop for TcpSocket<'a> { .ok(); } } - self.io.stack.borrow_mut().sockets.remove(self.io.handle); + let mut stack = self.io.stack.borrow_mut(); + stack.sockets.remove(self.io.handle); + stack.waker.wake(); } } diff --git a/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs index 5aa156d..4c2b3e1 100644 --- a/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -29,7 +29,7 @@ pub struct ChangeMode { /// service on a remote device, it implicitly registers to receive the "Connection Closed" /// event. #[derive(Clone, AtatCmd)] -#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 1000)] +#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 3000)] pub struct ConnectPeer<'a> { #[at_arg(position = 0, len = 128)] pub url: &'a str, From 64bab1718f39df68e8378b580e42122545106136 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 26 Jul 2023 13:46:18 +0200 Subject: [PATCH 06/16] Add performance testing example --- Cargo.toml | 4 +- examples/rpi-pico/src/bin/embassy-async.rs | 19 ++++++-- examples/rpi-pico/src/bin/embassy-perf.rs | 23 ++++++--- examples/rpi-pico/src/common.rs | 56 ++++++++++++++++++++++ src/asynch/mod.rs | 12 +++-- src/asynch/runner.rs | 44 +++++++++++++---- src/asynch/state.rs | 10 ++-- src/asynch/ublox_stack/dns.rs | 4 +- src/asynch/ublox_stack/mod.rs | 8 ++-- src/asynch/ublox_stack/tcp.rs | 36 ++++++++++---- src/command/data_mode/mod.rs | 2 +- src/command/system/types.rs | 2 +- src/lib.rs | 13 +++++ 13 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 examples/rpi-pico/src/common.rs diff --git a/Cargo.toml b/Cargo.toml index c4a07b6..4387243 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "ublox_short_range" doctest = false [dependencies] -atat = { version = "0.18.0", features = ["derive", "defmt", "bytes"] } +atat = { version = "0.19.0", features = ["derive", "defmt", "bytes"] } # atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "70283be", features = ["derive", "defmt", "bytes"] } heapless = { version = "^0.7", features = ["serde", "defmt-impl"] } no-std-net = { version = "0.6", features = ["serde"] } @@ -29,7 +29,7 @@ smoltcp = { version = "0.9.1", default-features = false, optional = true } atomic-polyfill = "1.0.2" defmt = { version = "0.3" } -embedded-hal = "=1.0.0-alpha.10" +embedded-hal = "=1.0.0-alpha.11" # embedded-nal = "0.6.0" embassy-time = "0.1" embassy-sync = "0.2" diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index 85d0702..8699327 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -4,6 +4,9 @@ #![feature(async_fn_in_trait)] #![allow(incomplete_features)] +#[path = "../common.rs"] +mod common; + use core::fmt::Write as _; use embassy_executor::Spawner; use embassy_futures::select::{select, Either}; @@ -31,23 +34,25 @@ const URC_CAPACITY: usize = 3; type AtClient = ublox_short_range::atat::asynch::Client< 'static, - uart::BufferedUartTx<'static, UART1>, + common::TxWrap>, RX_BUF_LEN, >; #[embassy_executor::task] -async fn wifi_task(runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8>) -> ! { +async fn wifi_task( + runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8, URC_CAPACITY>, +) -> ! { runner.run().await } #[embassy_executor::task] -async fn net_task(stack: &'static UbloxStack) -> ! { +async fn net_task(stack: &'static UbloxStack) -> ! { stack.run().await } #[embassy_executor::task(pool_size = 2)] async fn echo_task( - stack: &'static UbloxStack, + stack: &'static UbloxStack, hostname: &'static str, port: u16, write_interval: Duration, @@ -159,7 +164,11 @@ async fn main(spawner: Spawner) { let (rx, tx) = uart.split(); let buffers = &*make_static!(atat::Buffers::new()); - let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); + let (ingress, client) = buffers.split( + common::TxWrap(tx), + EdmDigester::default(), + atat::Config::new(), + ); defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); let state = make_static!(State::new(client)); diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs index b500ffd..2ba83d4 100644 --- a/examples/rpi-pico/src/bin/embassy-perf.rs +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -4,6 +4,9 @@ #![feature(async_fn_in_trait)] #![allow(incomplete_features)] +#[path = "../common.rs"] +mod common; + use embassy_executor::Spawner; use embassy_futures::join::join; use embassy_rp::gpio::{Level, Output}; @@ -27,17 +30,19 @@ const URC_CAPACITY: usize = 3; type AtClient = ublox_short_range::atat::asynch::Client< 'static, - uart::BufferedUartTx<'static, UART1>, + common::TxWrap>, RX_BUF_LEN, >; #[embassy_executor::task] -async fn wifi_task(runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8>) -> ! { +async fn wifi_task( + runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8, URC_CAPACITY>, +) -> ! { runner.run().await } #[embassy_executor::task] -async fn net_task(stack: &'static UbloxStack) -> ! { +async fn net_task(stack: &'static UbloxStack) -> ! { stack.run().await } @@ -74,7 +79,11 @@ async fn main(spawner: Spawner) { let (rx, tx) = uart.split(); let buffers = &*make_static!(atat::Buffers::new()); - let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); + let (ingress, client) = buffers.split( + common::TxWrap(tx), + EdmDigester::default(), + atat::Config::new(), + ); defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); let state = make_static!(State::new(client)); @@ -133,7 +142,7 @@ const DOWNLOAD_PORT: u16 = 4321; const UPLOAD_PORT: u16 = 4322; const UPLOAD_DOWNLOAD_PORT: u16 = 4323; -async fn test_download(stack: &'static UbloxStack) -> usize { +async fn test_download(stack: &'static UbloxStack) -> usize { defmt::info!("Testing download..."); let mut rx_buffer = [0; RX_BUFFER_SIZE]; @@ -177,7 +186,7 @@ async fn test_download(stack: &'static UbloxStack) -> usize { kbps } -async fn test_upload(stack: &'static UbloxStack) -> usize { +async fn test_upload(stack: &'static UbloxStack) -> usize { defmt::info!("Testing upload..."); let mut rx_buffer = [0; RX_BUFFER_SIZE]; @@ -221,7 +230,7 @@ async fn test_upload(stack: &'static UbloxStack) -> usize { kbps } -async fn test_upload_download(stack: &'static UbloxStack) -> usize { +async fn test_upload_download(stack: &'static UbloxStack) -> usize { defmt::info!("Testing upload+download..."); let mut rx_buffer = [0; RX_BUFFER_SIZE]; diff --git a/examples/rpi-pico/src/common.rs b/examples/rpi-pico/src/common.rs new file mode 100644 index 0000000..0c82b77 --- /dev/null +++ b/examples/rpi-pico/src/common.rs @@ -0,0 +1,56 @@ +use embassy_rp::uart; +use ublox_short_range::atat; + +pub struct TxWrap(pub TX); + +impl embedded_io::Io for TxWrap { + type Error = ::Error; +} + +impl embedded_io::asynch::Write for TxWrap { + async fn write(&mut self, buf: &[u8]) -> Result { + self.0.write(buf).await + } +} + +impl atat::UartExt for TxWrap> { + type Error = (); + + fn set_baudrate(&mut self, baud: u32) -> Result<(), Self::Error> { + let r = T::regs(); + + let clk_base = 125_000_000; + + let baud_rate_div = (8 * clk_base) / baud; + let mut baud_ibrd = baud_rate_div >> 7; + let mut baud_fbrd = ((baud_rate_div & 0x7f) + 1) / 2; + + if baud_ibrd == 0 { + baud_ibrd = 1; + baud_fbrd = 0; + } else if baud_ibrd >= 65535 { + baud_ibrd = 65535; + baud_fbrd = 0; + } + + r.uartcr().modify(|m| { + m.set_uarten(false); + }); + + // Load PL011's baud divisor registers + r.uartibrd() + .write_value(embassy_rp::pac::uart::regs::Uartibrd(baud_ibrd)); + r.uartfbrd() + .write_value(embassy_rp::pac::uart::regs::Uartfbrd(baud_fbrd)); + + // PL011 needs a (dummy) line control register write to latch in the + // divisors. We don't want to actually change LCR contents here. + r.uartlcr_h().modify(|_| {}); + + r.uartcr().modify(|m| { + m.set_uarten(true); + }); + + Ok(()) + } +} diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 56204dd..38a01ed 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -49,14 +49,20 @@ impl State { } } -pub async fn new<'a, AT: AtatClient, SUB: AtatUrcChannel, RST: OutputPin>( +pub async fn new< + 'a, + AT: AtatClient + atat::UartExt, + SUB: AtatUrcChannel, + RST: OutputPin, + const URC_CAPACITY: usize, +>( state: &'a mut State, subscriber: &'a SUB, reset: RST, ) -> ( - Device<'a, AT>, + Device<'a, AT, URC_CAPACITY>, Control<'a, AT>, - Runner<'a, AT, RST, MAX_CONNS>, + Runner<'a, AT, RST, MAX_CONNS, URC_CAPACITY>, ) { let (ch_runner, net_device) = state::new( &mut state.ch, diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index aca6a24..a801f68 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -34,21 +34,34 @@ use super::AtHandle; /// Background runner for the Ublox Module. /// /// You must call `.run()` in a background task for the Ublox Module to operate. -pub struct Runner<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> { +pub struct Runner< + 'd, + AT: AtatClient, + RST: OutputPin, + const MAX_CONNS: usize, + const URC_CAPACITY: usize, +> { ch: state::Runner<'d>, at: AtHandle<'d, AT>, reset: RST, wifi_connection: Option, // connections: FnvIndexMap, - urc_subscription: UrcSubscription<'d, EdmEvent>, + urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, } -impl<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> Runner<'d, AT, RST, MAX_CONNS> { +impl< + 'd, + AT: AtatClient + atat::UartExt, + RST: OutputPin, + const MAX_CONNS: usize, + const URC_CAPACITY: usize, + > Runner<'d, AT, RST, MAX_CONNS, URC_CAPACITY> +{ pub(crate) fn new( ch: state::Runner<'d>, at: AtHandle<'d, AT>, reset: RST, - urc_subscription: UrcSubscription<'d, EdmEvent>, + urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, ) -> Self { Self { ch, @@ -66,19 +79,34 @@ impl<'d, AT: AtatClient, RST: OutputPin, const MAX_CONNS: usize> Runner<'d, AT, // Hard reset module self.reset().await?; - // TODO: handle EDM settings quirk see EDM datasheet: 2.2.5.1 AT Request Serial settings + // ## 2.2.6.1 AT request serial settings (EDM mode) + // + // The AT+UMRS command to change serial settings does not work exactly + // the same as in command mode. When executed in the extended data mode, + // it is not possible to change the settings directly using the + // parameter. Instead, the + // parameter must be set to 0 and the serial settings will take effect + // when the module is reset. + let baud_rate = BaudRate::B3000000; self.at .send_edm(SetRS232Settings { - baud_rate: BaudRate::B115200, + baud_rate, flow_control: FlowControl::On, data_bits: 8, stop_bits: StopBits::One, parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, + change_after_confirm: ChangeAfterConfirm::StoreAndReset, }) .await?; - // self.restart(true).await?; + self.restart(true).await?; + + self.at + .0 + .lock() + .await + .set_baudrate(baud_rate as u32) + .map_err(|_| Error::BaudDetection)?; // Move to control // if let Some(size) = self.config.tls_in_buffer_size { diff --git a/src/asynch/state.rs b/src/asynch/state.rs index f63366d..7b45214 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -89,11 +89,11 @@ impl<'d> StateRunner<'d> { } } -pub fn new<'d, AT: AtatClient>( +pub fn new<'d, AT: AtatClient, const URC_CAPACITY: usize>( state: &'d mut State, at: AtHandle<'d, AT>, - urc_subscription: UrcSubscription<'d, EdmEvent>, -) -> (Runner<'d>, Device<'d, AT>) { + urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, +) -> (Runner<'d>, Device<'d, AT, URC_CAPACITY>) { // safety: this is a self-referential struct, however: // - it can't move while the `'d` borrow is active. // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. @@ -125,10 +125,10 @@ pub struct TestShared<'d> { inner: &'d Mutex>, } -pub struct Device<'d, AT: AtatClient> { +pub struct Device<'d, AT: AtatClient, const URC_CAPACITY: usize> { pub(crate) shared: TestShared<'d>, pub(crate) at: AtHandle<'d, AT>, - pub(crate) urc_subscription: UrcSubscription<'d, EdmEvent>, + pub(crate) urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, } impl<'d> TestShared<'d> { diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index 5c0dbf4..fc9175b 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -31,7 +31,9 @@ pub struct DnsSocket<'a> { impl<'a> DnsSocket<'a> { /// Create a new DNS socket using the provided stack. - pub fn new(stack: &'a UbloxStack) -> Self { + pub fn new( + stack: &'a UbloxStack, + ) -> Self { Self { stack: &stack.socket, } diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index b8b14bf..4e4f595 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -56,9 +56,9 @@ impl StackResources { } } -pub struct UbloxStack { +pub struct UbloxStack { socket: RefCell, - device: RefCell>, + device: RefCell>, last_tx_socket: AtomicU8, should_tx: AtomicBool, link_up: AtomicBool, @@ -92,9 +92,9 @@ struct SocketStack { dropped_sockets: heapless::Vec, } -impl UbloxStack { +impl UbloxStack { pub fn new( - device: state::Device<'static, AT>, + device: state::Device<'static, AT, URC_CAPACITY>, resources: &'static mut StackResources, ) -> Self { let sockets = SocketSet::new(&mut resources.sockets[..]); diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index 5875d3e..4d343cb 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -68,8 +68,8 @@ impl<'a> TcpWriter<'a> { } impl<'a> TcpSocket<'a> { - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8], ) -> Self { @@ -366,24 +366,40 @@ pub mod client { 'd, AT: AtatClient + 'static, const N: usize, + const URC_CAPACITY: usize, const TX_SZ: usize = 1024, const RX_SZ: usize = 1024, > { - pub(crate) stack: &'d UbloxStack, + pub(crate) stack: &'d UbloxStack, pub(crate) state: &'d TcpClientState, } - impl<'d, AT: AtatClient, const N: usize, const TX_SZ: usize, const RX_SZ: usize> - TcpClient<'d, AT, N, TX_SZ, RX_SZ> + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> { /// Create a new TcpClient - pub fn new(stack: &'d UbloxStack, state: &'d TcpClientState) -> Self { + pub fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + ) -> Self { Self { stack, state } } } - impl<'d, AT: AtatClient, const N: usize, const TX_SZ: usize, const RX_SZ: usize> - embedded_nal_async::TcpConnect for TcpClient<'d, AT, N, TX_SZ, RX_SZ> + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::TcpConnect for TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> { type Error = Error; type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; @@ -415,8 +431,8 @@ pub mod client { impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpConnection<'d, N, TX_SZ, RX_SZ> { - fn new( - stack: &'d UbloxStack, + fn new( + stack: &'d UbloxStack, state: &'d TcpClientState, ) -> Result { let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; diff --git a/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs index 4c2b3e1..9221b24 100644 --- a/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -41,7 +41,7 @@ pub struct ConnectPeer<'a> { #[derive(Clone, AtatCmd)] #[at_cmd("+UDCPC", NoResponse, timeout_ms = 1000)] pub struct ClosePeerConnection { - #[at_arg(position = 0)] + #[at_arg(position = 0, len = 1)] pub peer_handle: PeerHandle, } diff --git a/src/command/system/types.rs b/src/command/system/types.rs index 9d4b17d..dbec961 100644 --- a/src/command/system/types.rs +++ b/src/command/system/types.rs @@ -98,7 +98,7 @@ pub enum StatusID { SavedStatus = 1, } -#[derive(Debug, Clone, PartialEq, AtatEnum)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, AtatEnum)] #[at_enum(u32)] /// ODIN-W2: /// 19200 - 5250000. The module will set a baud rate as close as possible to the diff --git a/src/lib.rs b/src/lib.rs index 54c07cf..867ba7d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,3 +23,16 @@ pub use atat; pub mod command; pub mod error; // pub mod wifi; + +// TODO: +// - UDP stack +// - Secure sockets +// - Network scan +// - AP Mode (control) +// - TCP listener stack +// - (Blocking client?) +// - +// +// FIXME: +// - PWR/Restart stuff doesn't fully work +// - From 8370f5684f09e079638a22eb20228d6e35993f80 Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 29 Aug 2023 08:47:57 +0200 Subject: [PATCH 07/16] Comment out UartExt trait --- examples/rpi-pico/src/common.rs | 82 ++++++++++++++++----------------- src/asynch/mod.rs | 3 +- src/asynch/runner.rs | 43 ++++++++--------- 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/examples/rpi-pico/src/common.rs b/examples/rpi-pico/src/common.rs index 0c82b77..b924ca3 100644 --- a/examples/rpi-pico/src/common.rs +++ b/examples/rpi-pico/src/common.rs @@ -13,44 +13,44 @@ impl embedded_io::asynch::Write for TxWrap { } } -impl atat::UartExt for TxWrap> { - type Error = (); - - fn set_baudrate(&mut self, baud: u32) -> Result<(), Self::Error> { - let r = T::regs(); - - let clk_base = 125_000_000; - - let baud_rate_div = (8 * clk_base) / baud; - let mut baud_ibrd = baud_rate_div >> 7; - let mut baud_fbrd = ((baud_rate_div & 0x7f) + 1) / 2; - - if baud_ibrd == 0 { - baud_ibrd = 1; - baud_fbrd = 0; - } else if baud_ibrd >= 65535 { - baud_ibrd = 65535; - baud_fbrd = 0; - } - - r.uartcr().modify(|m| { - m.set_uarten(false); - }); - - // Load PL011's baud divisor registers - r.uartibrd() - .write_value(embassy_rp::pac::uart::regs::Uartibrd(baud_ibrd)); - r.uartfbrd() - .write_value(embassy_rp::pac::uart::regs::Uartfbrd(baud_fbrd)); - - // PL011 needs a (dummy) line control register write to latch in the - // divisors. We don't want to actually change LCR contents here. - r.uartlcr_h().modify(|_| {}); - - r.uartcr().modify(|m| { - m.set_uarten(true); - }); - - Ok(()) - } -} +// impl atat::UartExt for TxWrap> { +// type Error = (); + +// fn set_baudrate(&mut self, baud: u32) -> Result<(), Self::Error> { +// let r = T::regs(); + +// let clk_base = 125_000_000; + +// let baud_rate_div = (8 * clk_base) / baud; +// let mut baud_ibrd = baud_rate_div >> 7; +// let mut baud_fbrd = ((baud_rate_div & 0x7f) + 1) / 2; + +// if baud_ibrd == 0 { +// baud_ibrd = 1; +// baud_fbrd = 0; +// } else if baud_ibrd >= 65535 { +// baud_ibrd = 65535; +// baud_fbrd = 0; +// } + +// r.uartcr().modify(|m| { +// m.set_uarten(false); +// }); + +// // Load PL011's baud divisor registers +// r.uartibrd() +// .write_value(embassy_rp::pac::uart::regs::Uartibrd(baud_ibrd)); +// r.uartfbrd() +// .write_value(embassy_rp::pac::uart::regs::Uartfbrd(baud_fbrd)); + +// // PL011 needs a (dummy) line control register write to latch in the +// // divisors. We don't want to actually change LCR contents here. +// r.uartlcr_h().modify(|_| {}); + +// r.uartcr().modify(|m| { +// m.set_uarten(true); +// }); + +// Ok(()) +// } +// } diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 38a01ed..19c918e 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -51,7 +51,8 @@ impl State { pub async fn new< 'a, - AT: AtatClient + atat::UartExt, + AT: AtatClient, + // AT: AtatClient + atat::UartExt, SUB: AtatUrcChannel, RST: OutputPin, const URC_CAPACITY: usize, diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index a801f68..83cecb8 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -51,7 +51,8 @@ pub struct Runner< impl< 'd, - AT: AtatClient + atat::UartExt, + AT: AtatClient, + // AT: AtatClient + atat::UartExt, RST: OutputPin, const MAX_CONNS: usize, const URC_CAPACITY: usize, @@ -87,26 +88,26 @@ impl< // parameter. Instead, the // parameter must be set to 0 and the serial settings will take effect // when the module is reset. - let baud_rate = BaudRate::B3000000; - self.at - .send_edm(SetRS232Settings { - baud_rate, - flow_control: FlowControl::On, - data_bits: 8, - stop_bits: StopBits::One, - parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::StoreAndReset, - }) - .await?; - - self.restart(true).await?; - - self.at - .0 - .lock() - .await - .set_baudrate(baud_rate as u32) - .map_err(|_| Error::BaudDetection)?; + // let baud_rate = BaudRate::B3000000; + // self.at + // .send_edm(SetRS232Settings { + // baud_rate, + // flow_control: FlowControl::On, + // data_bits: 8, + // stop_bits: StopBits::One, + // parity: Parity::None, + // change_after_confirm: ChangeAfterConfirm::StoreAndReset, + // }) + // .await?; + + // self.restart(true).await?; + + // self.at + // .0 + // .lock() + // .await + // .set_baudrate(baud_rate as u32) + // .map_err(|_| Error::BaudDetection)?; // Move to control // if let Some(size) = self.config.tls_in_buffer_size { From 0e927036663ecb631b5f9f12df999d1081a638fe Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 31 Aug 2023 12:02:31 +0200 Subject: [PATCH 08/16] Update to latest embassy and embedded-io 0.5 --- Cargo.toml | 9 ++++----- examples/rpi-pico/Cargo.toml | 4 ++-- examples/rpi-pico/src/bin/embassy-async.rs | 8 ++++---- examples/rpi-pico/src/common.rs | 18 +++++++++--------- src/asynch/ublox_stack/tcp.rs | 20 ++++++++++---------- 5 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4387243..15ba63b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,20 +29,20 @@ smoltcp = { version = "0.9.1", default-features = false, optional = true } atomic-polyfill = "1.0.2" defmt = { version = "0.3" } -embedded-hal = "=1.0.0-alpha.11" +embedded-hal = "=1.0.0-rc.1" # embedded-nal = "0.6.0" embassy-time = "0.1" embassy-sync = "0.2" embassy-futures = "0.1" -embassy-hal-common = "0.1" embassy-net-driver = "0.1" -embedded-nal-async = { version = "0.4", optional = true } +embedded-nal-async = { version = "0.5", optional = true } futures = { version = "0.3.17", default-features = false, features = [ "async-await", ] } -embedded-io = "0.4" +embedded-io = "0.5" +embedded-io-async = "0.5" [features] default = ["async", "odin_w2xx", "ublox-sockets", "socket-tcp"] @@ -74,4 +74,3 @@ atat = { path = "../atat/atat" } ublox-sockets = { path = "../ublox-sockets" } no-std-net = { path = "../no-std-net" } embassy-net-driver = { path = "../embassy/embassy-net-driver" } -embassy-hal-common = { path = "../embassy/embassy-hal-common" } diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index 8787886..e6c5c10 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -22,7 +22,7 @@ cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } cortex-m-rt = "0.7.0" futures = { version = "0.3.17", default-features = false, features = ["async-await", "cfg-target-has-atomic", "unstable"] } -embedded-io = { version = "0.4.0", features = ["async", "defmt"] } +embedded-io-async = { version = "0.5.0" } heapless = "0.7.15" @@ -36,7 +36,7 @@ heapless = "0.7.15" embassy-executor = { path = "../../../embassy/embassy-executor" } -embassy-hal-common = { path = "../../../embassy/embassy-hal-common" } +embassy-hal-internal = { path = "../../../embassy/embassy-hal-internal" } embassy-time = { path = "../../../embassy/embassy-time" } embassy-futures = { path = "../../../embassy/embassy-futures" } embassy-sync = { path = "../../../embassy/embassy-sync" } diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index 8699327..498292f 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -15,7 +15,7 @@ use embassy_rp::peripherals::{PIN_26, UART1}; use embassy_rp::uart::BufferedInterruptHandler; use embassy_rp::{bind_interrupts, uart}; use embassy_time::{Duration, Timer}; -use embedded_io::asynch::Write; +use embedded_io_async::Write; use no_std_net::{Ipv4Addr, SocketAddr}; use static_cell::make_static; use ublox_short_range::asynch::runner::Runner; @@ -29,12 +29,12 @@ use ublox_short_range::command::edm::urc::EdmEvent; use ublox_short_range::embedded_nal_async::AddrType; use {defmt_rtt as _, panic_probe as _}; -const RX_BUF_LEN: usize = 1024; +const RX_BUF_LEN: usize = 4096; const URC_CAPACITY: usize = 3; type AtClient = ublox_short_range::atat::asynch::Client< 'static, - common::TxWrap>, + uart::BufferedUartTx<'static, UART1>, RX_BUF_LEN, >; @@ -165,7 +165,7 @@ async fn main(spawner: Spawner) { let buffers = &*make_static!(atat::Buffers::new()); let (ingress, client) = buffers.split( - common::TxWrap(tx), + tx, EdmDigester::default(), atat::Config::new(), ); diff --git a/examples/rpi-pico/src/common.rs b/examples/rpi-pico/src/common.rs index b924ca3..e341bb1 100644 --- a/examples/rpi-pico/src/common.rs +++ b/examples/rpi-pico/src/common.rs @@ -1,17 +1,17 @@ use embassy_rp::uart; use ublox_short_range::atat; -pub struct TxWrap(pub TX); +// pub struct TxWrap(pub TX); -impl embedded_io::Io for TxWrap { - type Error = ::Error; -} +// impl embedded_io::Io for TxWrap { +// type Error = ::Error; +// } -impl embedded_io::asynch::Write for TxWrap { - async fn write(&mut self, buf: &[u8]) -> Result { - self.0.write(buf).await - } -} +// impl embedded_io::asynch::Write for TxWrap { +// async fn write(&mut self, buf: &[u8]) -> Result { +// self.0.write(buf).await +// } +// } // impl atat::UartExt for TxWrap> { // type Error = (); diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index 4d343cb..58d7586 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -306,17 +306,17 @@ mod embedded_io_impls { } } - impl<'d> embedded_io::Io for TcpSocket<'d> { + impl<'d> embedded_io::ErrorType for TcpSocket<'d> { type Error = Error; } - impl<'d> embedded_io::asynch::Read for TcpSocket<'d> { + impl<'d> embedded_io_async::Read for TcpSocket<'d> { async fn read(&mut self, buf: &mut [u8]) -> Result { self.io.read(buf).await } } - impl<'d> embedded_io::asynch::Write for TcpSocket<'d> { + impl<'d> embedded_io_async::Write for TcpSocket<'d> { async fn write(&mut self, buf: &[u8]) -> Result { self.io.write(buf).await } @@ -326,21 +326,21 @@ mod embedded_io_impls { } } - impl<'d> embedded_io::Io for TcpReader<'d> { + impl<'d> embedded_io::ErrorType for TcpReader<'d> { type Error = Error; } - impl<'d> embedded_io::asynch::Read for TcpReader<'d> { + impl<'d> embedded_io_async::Read for TcpReader<'d> { async fn read(&mut self, buf: &mut [u8]) -> Result { self.io.read(buf).await } } - impl<'d> embedded_io::Io for TcpWriter<'d> { + impl<'d> embedded_io::ErrorType for TcpWriter<'d> { type Error = Error; } - impl<'d> embedded_io::asynch::Write for TcpWriter<'d> { + impl<'d> embedded_io_async::Write for TcpWriter<'d> { async fn write(&mut self, buf: &[u8]) -> Result { self.io.write(buf).await } @@ -457,13 +457,13 @@ pub mod client { } } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::Io + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::ErrorType for TcpConnection<'d, N, TX_SZ, RX_SZ> { type Error = Error; } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Read + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Read for TcpConnection<'d, N, TX_SZ, RX_SZ> { async fn read(&mut self, buf: &mut [u8]) -> Result { @@ -471,7 +471,7 @@ pub mod client { } } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::asynch::Write + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Write for TcpConnection<'d, N, TX_SZ, RX_SZ> { async fn write(&mut self, buf: &[u8]) -> Result { From 8d14964b3e9490097303da6ca324a76637bc5505 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 29 Dec 2023 13:45:55 +0100 Subject: [PATCH 09/16] Update dependencies and fix tests --- Cargo.toml | 55 +++-- examples/rpi-pico/Cargo.toml | 17 +- examples/rpi-pico/src/bin/embassy-async.rs | 61 ++--- examples/rpi-pico/src/bin/embassy-perf.rs | 60 ++--- rust-toolchain.toml | 4 +- src/asynch/control.rs | 16 +- src/asynch/mod.rs | 14 +- src/asynch/runner.rs | 44 ++-- src/asynch/ublox_stack/dns.rs | 24 +- src/asynch/ublox_stack/mod.rs | 17 +- src/asynch/ublox_stack/tcp.rs | 9 +- src/blocking/client.rs | 20 +- src/blocking/dns.rs | 2 +- src/blocking/tcp_stack.rs | 12 +- src/blocking/udp_stack.rs | 31 ++- src/command/custom_digest.rs | 6 +- src/command/data_mode/urc.rs | 4 +- src/command/edm/mod.rs | 166 +++++-------- src/command/edm/urc.rs | 8 +- src/command/general/types.rs | 3 +- src/command/ping/types.rs | 3 +- src/command/ping/urc.rs | 3 +- src/connection.rs | 3 +- src/error.rs | 12 +- src/fmt.rs | 258 +++++++++++++++++++++ src/lib.rs | 2 + src/peer_builder.rs | 12 +- src/wifi/mod.rs | 8 +- src/wifi/options.rs | 3 +- src/wifi/supplicant.rs | 25 +- 30 files changed, 559 insertions(+), 343 deletions(-) create mode 100644 src/fmt.rs diff --git a/Cargo.toml b/Cargo.toml index 15ba63b..36bb672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,34 +15,31 @@ name = "ublox_short_range" doctest = false [dependencies] -atat = { version = "0.19.0", features = ["derive", "defmt", "bytes"] } +atat = { version = "0.20.0", features = ["derive", "bytes"] } # atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "70283be", features = ["derive", "defmt", "bytes"] } -heapless = { version = "^0.7", features = ["serde", "defmt-impl"] } +heapless = { version = "^0.8", features = ["serde"] } no-std-net = { version = "0.6", features = ["serde"] } serde = { version = "^1", default-features = false, features = ["derive"] } -ublox-sockets = { version = "0.5", features = [ - "defmt", - "edm", -], optional = true } +ublox-sockets = { version = "0.5", features = ["edm"], optional = true } postcard = "1.0.4" -smoltcp = { version = "0.9.1", default-features = false, optional = true } -atomic-polyfill = "1.0.2" - -defmt = { version = "0.3" } -embedded-hal = "=1.0.0-rc.1" -# embedded-nal = "0.6.0" -embassy-time = "0.1" -embassy-sync = "0.2" +portable-atomic = "1.5" + +defmt = { version = "0.3", optional = true } +log = { version = "0.4.14", optional = true } + +embedded-hal = "=1.0.0-rc.3" +embassy-time = "0.2" +embassy-sync = "0.5" embassy-futures = "0.1" -embassy-net-driver = "0.1" +embassy-net-driver = "0.2" -embedded-nal-async = { version = "0.5", optional = true } +embedded-nal-async = { version = "0.7", optional = true } futures = { version = "0.3.17", default-features = false, features = [ "async-await", ] } -embedded-io = "0.5" -embedded-io-async = "0.5" +embedded-io = "0.6" +embedded-io-async = "0.6" [features] default = ["async", "odin_w2xx", "ublox-sockets", "socket-tcp"] @@ -51,7 +48,13 @@ async = ["dep:embedded-nal-async", "atat/async", "ublox-sockets?/async"] std = [] -defmt = ["postcard/use-defmt"] +defmt = [ + "dep:defmt", + "postcard/use-defmt", + "heapless/defmt-03", + "atat/defmt", + "ublox-sockets/defmt", +] odin_w2xx = [] nina_w1xx = [] @@ -60,8 +63,14 @@ anna_b1xx = [] nina_b2xx = [] nina_b3xx = [] -socket-tcp = ["ublox-sockets?/socket-tcp", "smoltcp?/socket-tcp"] -socket-udp = ["ublox-sockets?/socket-udp", "smoltcp?/socket-udp"] +socket-tcp = [ + "ublox-sockets?/socket-tcp", + # "smoltcp?/socket-tcp" +] +socket-udp = [ + "ublox-sockets?/socket-udp", + # "smoltcp?/socket-udp" +] [workspace] members = [] @@ -73,4 +82,8 @@ exclude = ["examples"] atat = { path = "../atat/atat" } ublox-sockets = { path = "../ublox-sockets" } no-std-net = { path = "../no-std-net" } + +embassy-time = { path = "../embassy/embassy-time" } +embassy-sync = { path = "../embassy/embassy-sync" } +embassy-futures = { path = "../embassy/embassy-futures" } embassy-net-driver = { path = "../embassy/embassy-net-driver" } diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index e6c5c10..db9c02d 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -5,25 +5,24 @@ edition = "2021" [dependencies] -ublox-short-range-rs = { path = "../../", features = ["async", "defmt", "odin_w2xx", "ublox-sockets", "socket-tcp"] } -embassy-executor = { version = "0.1.0", features = ["defmt", "integrated-timers", "nightly"] } -embassy-time = { version = "0.1.0", features = ["defmt", "defmt-timestamp-uptime"] } -embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-traits", "nightly", "unstable-pac", "time-driver"] } +ublox-short-range-rs = { path = "../../", features = ["async", "odin_w2xx", "ublox-sockets", "socket-tcp"] } +embassy-executor = { version = "0.4", features = ["defmt", "integrated-timers", "nightly"] } +embassy-time = { version = "0.2", features = ["defmt", "defmt-timestamp-uptime"] } +embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-pac", "time-driver"] } embassy-futures = { version = "0.1.0" } -atomic-polyfill = "1.0.2" no-std-net = { version = "0.6", features = ["serde"] } -static_cell = { version = "1.1", features = ["nightly"] } +static_cell = { version = "2", features = ["nightly"] } defmt = "0.3.4" -defmt-rtt = "0.3" +defmt-rtt = "0.4" panic-probe = { version = "0.3", features = ["print-defmt"] } cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } cortex-m-rt = "0.7.0" futures = { version = "0.3.17", default-features = false, features = ["async-await", "cfg-target-has-atomic", "unstable"] } -embedded-io-async = { version = "0.5.0" } -heapless = "0.7.15" +embedded-io-async = { version = "0.6" } +heapless = "0.8" [patch.crates-io] diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index 498292f..f1467cd 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -67,26 +67,22 @@ async fn echo_task( let ip_addr = match DnsSocket::new(stack).query(hostname, AddrType::IPv4).await { Ok(ip) => ip, Err(_) => { - defmt::error!("[{}] Failed to resolve IP addr", hostname); + error!("[{}] Failed to resolve IP addr", hostname); return; } }; let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); - defmt::info!( - "[{}] Connecting... {}", - hostname, - defmt::Debug2Format(&ip_addr) - ); + info!("[{}] Connecting... {}", hostname, debug2Format(&ip_addr)); if let Err(e) = socket.connect((ip_addr, port)).await { - defmt::warn!("[{}] connect error: {:?}", hostname, e); + warn!("[{}] connect error: {:?}", hostname, e); return; } - defmt::info!( + info!( "[{}] Connected to {:?}", hostname, - defmt::Debug2Format(&socket.remote_endpoint()) + debug2Format(&socket.remote_endpoint()) ); loop { @@ -96,25 +92,25 @@ async fn echo_task( write!(msg, "Hello {}! {}\n", ip_addr, cnt).unwrap(); cnt = cnt.wrapping_add(1); if let Err(e) = socket.write_all(msg.as_bytes()).await { - defmt::warn!("[{}] write error: {:?}", hostname, e); + warn!("[{}] write error: {:?}", hostname, e); break; } - defmt::info!("[{}] txd: {}", hostname, msg); + info!("[{}] txd: {}", hostname, msg); Timer::after(Duration::from_millis(400)).await; } Either::Second(res) => { let n = match res { Ok(0) => { - defmt::warn!("[{}] read EOF", hostname); + warn!("[{}] read EOF", hostname); break; } Ok(n) => n, Err(e) => { - defmt::warn!("[{}] {:?}", hostname, e); + warn!("[{}] {:?}", hostname, e); break; } }; - defmt::info!( + info!( "[{}] rxd {}", hostname, core::str::from_utf8(&buf[..n]).unwrap() @@ -138,7 +134,7 @@ bind_interrupts!(struct Irqs { #[embassy_executor::main] async fn main(spawner: Spawner) { - defmt::info!("Hello World!"); + info!("Hello World!"); let p = embassy_rp::init(Default::default()); @@ -164,11 +160,7 @@ async fn main(spawner: Spawner) { let (rx, tx) = uart.split(); let buffers = &*make_static!(atat::Buffers::new()); - let (ingress, client) = buffers.split( - tx, - EdmDigester::default(), - atat::Config::new(), - ); + let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); let state = make_static!(State::new(client)); @@ -190,7 +182,7 @@ async fn main(spawner: Spawner) { defmt::unwrap!(spawner.spawn(net_task(stack))); // And now we can use it! - defmt::info!("Device initialized!"); + info!("Device initialized!"); let mut rx_buffer = [0; 256]; let mut tx_buffer = [0; 256]; @@ -202,7 +194,7 @@ async fn main(spawner: Spawner) { loop { match control.join_wpa2("test", "1234abcd").await { Ok(_) => { - defmt::info!("Network connected!"); + info!("Network connected!"); spawner .spawn(echo_task( &stack, @@ -225,7 +217,7 @@ async fn main(spawner: Spawner) { break; } Err(err) => { - defmt::info!("join failed with error={:?}. Retrying in 1 second", err); + info!("join failed with error={:?}. Retrying in 1 second", err); Timer::after(Duration::from_secs(1)).await; } } @@ -237,15 +229,12 @@ async fn main(spawner: Spawner) { // // socket.set_timeout(Some(Duration::from_secs(10))); let remote: SocketAddr = (Ipv4Addr::new(192, 168, 1, 183), 4444).into(); - defmt::info!("Connecting... {}", defmt::Debug2Format(&remote)); + info!("Connecting... {}", debug2Format(&remote)); if let Err(e) = socket.connect(remote).await { - defmt::warn!("connect error: {:?}", e); + warn!("connect error: {:?}", e); continue; } - defmt::info!( - "Connected to {:?}", - defmt::Debug2Format(&socket.remote_endpoint()) - ); + info!("Connected to {:?}", debug2Format(&socket.remote_endpoint())); 'inner: loop { match select(Timer::after(Duration::from_secs(3)), socket.read(&mut buf)).await { @@ -254,25 +243,25 @@ async fn main(spawner: Spawner) { write!(msg, "Hello world! {}\n", cnt).unwrap(); cnt = cnt.wrapping_add(1); if let Err(e) = socket.write_all(msg.as_bytes()).await { - defmt::warn!("write error: {:?}", e); + warn!("write error: {:?}", e); break; } - defmt::info!("txd: {}", msg); + info!("txd: {}", msg); Timer::after(Duration::from_millis(400)).await; } Either::Second(res) => { let n = match res { Ok(0) => { - defmt::warn!("read EOF"); + warn!("read EOF"); break; } Ok(n) => n, Err(e) => { - defmt::warn!("{:?}", e); + warn!("{:?}", e); break; } }; - defmt::info!("rxd [{}] {}", n, core::str::from_utf8(&buf[..n]).unwrap()); + info!("rxd [{}] {}", n, core::str::from_utf8(&buf[..n]).unwrap()); match &buf[..n] { b"c\n" => { @@ -296,11 +285,11 @@ async fn main(spawner: Spawner) { } } } - defmt::info!("Press USER button to reconnect socket!"); + info!("Press USER button to reconnect socket!"); btn.wait_for_any_edge().await; continue; } - defmt::info!("Press USER button to reconnect to WiFi!"); + info!("Press USER button to reconnect to WiFi!"); btn.wait_for_any_edge().await; } } diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs index 2ba83d4..796fd84 100644 --- a/examples/rpi-pico/src/bin/embassy-perf.rs +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -60,7 +60,7 @@ bind_interrupts!(struct Irqs { #[embassy_executor::main] async fn main(spawner: Spawner) { - defmt::info!("Hello World!"); + info!("Hello World!"); let p = embassy_rp::init(Default::default()); @@ -109,7 +109,7 @@ async fn main(spawner: Spawner) { } // And now we can use it! - defmt::info!("Device initialized!"); + info!("Device initialized!"); let down = test_download(stack).await; Timer::after(Duration::from_secs(SETTLE_TIME as _)).await; @@ -122,7 +122,7 @@ async fn main(spawner: Spawner) { // assert!(up > TEST_EXPECTED_UPLOAD_KBPS); // assert!(updown > TEST_EXPECTED_UPLOAD_DOWNLOAD_KBPS); - defmt::info!("Test OK"); + info!("Test OK"); cortex_m::asm::bkpt(); } @@ -143,23 +143,23 @@ const UPLOAD_PORT: u16 = 4322; const UPLOAD_DOWNLOAD_PORT: u16 = 4323; async fn test_download(stack: &'static UbloxStack) -> usize { - defmt::info!("Testing download..."); + info!("Testing download..."); let mut rx_buffer = [0; RX_BUFFER_SIZE]; let mut tx_buffer = [0; TX_BUFFER_SIZE]; let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); // socket.set_timeout(Some(Duration::from_secs(10))); - defmt::info!( + info!( "connecting to {:?}:{}...", - defmt::Debug2Format(&SERVER_ADDRESS), + debug2Format(&SERVER_ADDRESS), DOWNLOAD_PORT ); if let Err(e) = socket.connect((SERVER_ADDRESS, DOWNLOAD_PORT)).await { - defmt::error!("connect error: {:?}", e); + error!("connect error: {:?}", e); return 0; } - defmt::info!("connected, testing..."); + info!("connected, testing..."); let mut rx_buf = [0; 4096]; let mut total: usize = 0; @@ -167,12 +167,12 @@ async fn test_download(stack: &'static UbloxStack) -> us loop { match socket.read(&mut rx_buf).await { Ok(0) => { - defmt::error!("read EOF"); + error!("read EOF"); return 0; } Ok(n) => total += n, Err(e) => { - defmt::error!("read error: {:?}", e); + error!("read error: {:?}", e); return 0; } } @@ -182,28 +182,28 @@ async fn test_download(stack: &'static UbloxStack) -> us .ok(); let kbps = (total + 512) / 1024 / TEST_DURATION; - defmt::info!("download: {} kB/s", kbps); + info!("download: {} kB/s", kbps); kbps } async fn test_upload(stack: &'static UbloxStack) -> usize { - defmt::info!("Testing upload..."); + info!("Testing upload..."); let mut rx_buffer = [0; RX_BUFFER_SIZE]; let mut tx_buffer = [0; TX_BUFFER_SIZE]; let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); // socket.set_timeout(Some(Duration::from_secs(10))); - defmt::info!( + info!( "connecting to {:?}:{}...", - defmt::Debug2Format(&SERVER_ADDRESS), + debug2Format(&SERVER_ADDRESS), UPLOAD_PORT ); if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_PORT)).await { - defmt::error!("connect error: {:?}", e); + error!("connect error: {:?}", e); return 0; } - defmt::info!("connected, testing..."); + info!("connected, testing..."); let buf = [0; 4096]; let mut total: usize = 0; @@ -211,12 +211,12 @@ async fn test_upload(stack: &'static UbloxStack) -> usiz loop { match socket.write(&buf).await { Ok(0) => { - defmt::error!("write zero?!??!?!"); + error!("write zero?!??!?!"); return 0; } Ok(n) => total += n, Err(e) => { - defmt::error!("write error: {:?}", e); + error!("write error: {:?}", e); return 0; } } @@ -226,28 +226,28 @@ async fn test_upload(stack: &'static UbloxStack) -> usiz .ok(); let kbps = (total + 512) / 1024 / TEST_DURATION; - defmt::info!("upload: {} kB/s", kbps); + info!("upload: {} kB/s", kbps); kbps } async fn test_upload_download(stack: &'static UbloxStack) -> usize { - defmt::info!("Testing upload+download..."); + info!("Testing upload+download..."); let mut rx_buffer = [0; RX_BUFFER_SIZE]; let mut tx_buffer = [0; TX_BUFFER_SIZE]; let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); // socket.set_timeout(Some(Duration::from_secs(10))); - defmt::info!( + info!( "connecting to {:?}:{}...", - defmt::Debug2Format(&SERVER_ADDRESS), + debug2Format(&SERVER_ADDRESS), UPLOAD_DOWNLOAD_PORT ); if let Err(e) = socket.connect((SERVER_ADDRESS, UPLOAD_DOWNLOAD_PORT)).await { - defmt::error!("connect error: {:?}", e); + error!("connect error: {:?}", e); return 0; } - defmt::info!("connected, testing..."); + info!("connected, testing..."); let (mut reader, mut writer) = socket.split(); @@ -258,12 +258,12 @@ async fn test_upload_download(stack: &'static UbloxStack loop { match writer.write(&tx_buf).await { Ok(0) => { - defmt::error!("write zero?!??!?!"); + error!("write zero?!??!?!"); return 0; } Ok(_) => {} Err(e) => { - defmt::error!("write error: {:?}", e); + error!("write error: {:?}", e); return 0; } } @@ -274,12 +274,12 @@ async fn test_upload_download(stack: &'static UbloxStack loop { match reader.read(&mut rx_buf).await { Ok(0) => { - defmt::error!("read EOF"); + error!("read EOF"); return 0; } Ok(n) => total += n, Err(e) => { - defmt::error!("read error: {:?}", e); + error!("read error: {:?}", e); return 0; } } @@ -293,10 +293,10 @@ async fn test_upload_download(stack: &'static UbloxStack .await .is_err() { - defmt::error!("Test timed out"); + error!("Test timed out"); } let kbps = (total + 512) / 1024 / TEST_DURATION; - defmt::info!("upload+download: {} kB/s", kbps); + info!("upload+download: {} kB/s", kbps); kbps } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4bf55ae..41845fc 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,6 +1,6 @@ [toolchain] -channel = "nightly-2023-02-07" -components = [ "rust-src", "rustfmt", "llvm-tools-preview", "clippy" ] +channel = "nightly-2023-12-24" +components = [ "rust-src", "rustfmt", "llvm-tools" ] targets = [ "thumbv6m-none-eabi", "thumbv7em-none-eabihf" diff --git a/src/asynch/control.rs b/src/asynch/control.rs index 96dc9d5..be438d9 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -32,7 +32,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } pub(crate) async fn init(&mut self) -> Result<(), Error> { - defmt::debug!("Initalizing ublox control"); + debug!("Initalizing ublox control"); // read MAC addr. // let mut resp = self.at.send_edm(GetWifiMac).await?; // self.state_ch.set_ethernet_address( @@ -139,7 +139,9 @@ impl<'a, AT: AtatClient> Control<'a, AT> { self.at .send_edm(SetWifiStationConfig { config_id: CONFIG_ID, - config_param: WifiStationConfig::SSID(heapless::String::from(ssid)), + config_param: WifiStationConfig::SSID( + heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, + ), }) .await?; @@ -192,7 +194,9 @@ impl<'a, AT: AtatClient> Control<'a, AT> { self.at .send_edm(SetWifiStationConfig { config_id: CONFIG_ID, - config_param: WifiStationConfig::SSID(heapless::String::from(ssid)), + config_param: WifiStationConfig::SSID( + heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, + ), }) .await?; @@ -206,9 +210,9 @@ impl<'a, AT: AtatClient> Control<'a, AT> { self.at .send_edm(SetWifiStationConfig { config_id: CONFIG_ID, - config_param: WifiStationConfig::WpaPskOrPassphrase(heapless::String::from( - passphrase, - )), + config_param: WifiStationConfig::WpaPskOrPassphrase( + heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, + ), }) .await?; diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 19c918e..12a16f8 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -6,7 +6,7 @@ pub mod ublox_stack; pub(crate) mod state; use crate::command::edm::{urc::EdmEvent, EdmAtCmdWrapper}; -use atat::{asynch::AtatClient, AtatUrcChannel}; +use atat::asynch::AtatClient; use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; use embedded_hal::digital::OutputPin; use runner::Runner; @@ -20,18 +20,15 @@ const MAX_CONNS: usize = 8; pub struct AtHandle<'d, AT: AtatClient>(&'d Mutex); impl<'d, AT: AtatClient> AtHandle<'d, AT> { - async fn send_edm, const LEN: usize>( + async fn send_edm( &mut self, cmd: Cmd, ) -> Result { self.send(EdmAtCmdWrapper(cmd)).await } - async fn send, const LEN: usize>( - &mut self, - cmd: Cmd, - ) -> Result { - self.0.lock().await.send_retry::(&cmd).await + async fn send(&mut self, cmd: Cmd) -> Result { + self.0.lock().await.send_retry::(&cmd).await } } @@ -53,12 +50,11 @@ pub async fn new< 'a, AT: AtatClient, // AT: AtatClient + atat::UartExt, - SUB: AtatUrcChannel, RST: OutputPin, const URC_CAPACITY: usize, >( state: &'a mut State, - subscriber: &'a SUB, + subscriber: &'a atat::UrcChannel, reset: RST, ) -> ( Device<'a, AT, URC_CAPACITY>, diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index 83cecb8..2ef6ed2 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -76,7 +76,7 @@ impl< pub(crate) async fn init(&mut self) -> Result<(), Error> { // Initilize a new ublox device to a known state (set RS232 settings) - defmt::debug!("Initializing module"); + debug!("Initializing module"); // Hard reset module self.reset().await?; @@ -143,7 +143,7 @@ impl< } pub async fn reset(&mut self) -> Result<(), Error> { - defmt::warn!("Hard resetting Ublox Short Range"); + warn!("Hard resetting Ublox Short Range"); self.reset.set_low().ok(); Timer::after(Duration::from_millis(100)).await; self.reset.set_high().ok(); @@ -156,7 +156,7 @@ impl< } pub async fn restart(&mut self, store: bool) -> Result<(), Error> { - defmt::warn!("Soft resetting Ublox Short Range"); + warn!("Soft resetting Ublox Short Range"); if store { self.at.send_edm(StoreCurrentConfig).await?; } @@ -218,7 +218,7 @@ impl< let event = self.urc_subscription.next_message_pure().await; match event { EdmEvent::ATEvent(Urc::StartUp) => { - defmt::error!("AT startup event?! Device restarted unintentionally!"); + error!("AT startup event?! Device restarted unintentionally!"); } EdmEvent::ATEvent(Urc::WifiLinkConnected(WifiLinkConnected { connection_id: _, @@ -230,7 +230,7 @@ impl< con.network.bssid = bssid; con.network.channel = channel; } else { - defmt::debug!("[URC] Active network config discovered"); + debug!("[URC] Active network config discovered"); self.wifi_connection.replace( WifiConnection::new( WifiNetwork::new_station(bssid, channel), @@ -252,7 +252,7 @@ impl< con.wifi_state = WiFiState::Inactive; } DisconnectReason::SecurityProblems => { - defmt::error!("Wifi Security Problems"); + error!("Wifi Security Problems"); } _ => { con.wifi_state = WiFiState::NotConnected; @@ -276,7 +276,7 @@ impl< } EdmEvent::ATEvent(Urc::NetworkError(_)) => todo!(), EdmEvent::StartUp => { - defmt::error!("EDM startup event?! Device restarted unintentionally!"); + error!("EDM startup event?! Device restarted unintentionally!"); } _ => {} }; @@ -288,25 +288,29 @@ impl< status: NetworkStatus::InterfaceType(InterfaceType::WifiStation), .. } = self - .at.send_edm(GetNetworkStatus { + .at + .send_edm(GetNetworkStatus { interface_id, status: NetworkStatusParameter::InterfaceType, }) - .await? else { - return Err(Error::Network); - }; + .await? + else { + return Err(Error::Network); + }; let NetworkStatusResponse { status: NetworkStatus::Gateway(ipv4), .. } = self - .at.send_edm(GetNetworkStatus { + .at + .send_edm(GetNetworkStatus { interface_id, status: NetworkStatusParameter::Gateway, }) - .await? else { - return Err(Error::Network); - }; + .await? + else { + return Err(Error::Network); + }; let ipv4_up = core::str::from_utf8(ipv4.as_slice()) .ok() @@ -318,13 +322,15 @@ impl< status: NetworkStatus::IPv6LinkLocalAddress(ipv6), .. } = self - .at.send_edm(GetNetworkStatus { + .at + .send_edm(GetNetworkStatus { interface_id, status: NetworkStatusParameter::IPv6LinkLocalAddress, }) - .await? else { - return Err(Error::Network); - }; + .await? + else { + return Err(Error::Network); + }; let ipv6_up = core::str::from_utf8(ipv6.as_slice()) .ok() diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index fc9175b..734e740 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -55,15 +55,15 @@ impl<'a> DnsSocket<'a> { _ => {} } + let name_string = heapless::String::try_from(name).map_err(|_| Error::NameTooLong)?; + { let mut s = self.stack.borrow_mut(); if s.dns_queries - .insert(heapless::String::from(name), DnsQuery::new()) + .insert(name_string.clone(), DnsQuery::new()) .is_err() { - defmt::error!( - "Attempted to start more simultaneous DNS requests than the (4) supported" - ); + error!("Attempted to start more simultaneous DNS requests than the (4) supported"); } s.waker.wake(); } @@ -93,22 +93,19 @@ impl<'a> DnsSocket<'a> { let drop = OnDrop::new(|| { let mut s = self.stack.borrow_mut(); - s.dns_queries.remove(&heapless::String::from(name)); + s.dns_queries.remove(&name_string); }); let res = poll_fn(|cx| { let mut s = self.stack.borrow_mut(); - let query = s - .dns_queries - .get_mut(&heapless::String::from(name)) - .unwrap(); + let query = s.dns_queries.get_mut(&name_string).unwrap(); match query.state { DnsState::Ok(ip) => { - s.dns_queries.remove(&heapless::String::from(name)); + s.dns_queries.remove(&name_string); return Poll::Ready(Ok(ip)); } DnsState::Err => { - s.dns_queries.remove(&heapless::String::from(name)); + s.dns_queries.remove(&name_string); return Poll::Ready(Err(Error::Failed)); } _ => { @@ -138,8 +135,9 @@ impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { async fn get_host_by_address( &self, - _addr: embedded_nal_async::IpAddr, - ) -> Result, Self::Error> { + addr: IpAddr, + result: &mut [u8], + ) -> Result { unimplemented!() } } diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index 4e4f595..ad32373 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -28,13 +28,13 @@ use super::state::{self, LinkState}; use super::AtHandle; use atat::asynch::AtatClient; -use atomic_polyfill::{AtomicBool, AtomicU8, Ordering}; use embassy_futures::select::{select4, Either4}; use embassy_sync::waitqueue::WakerRegistration; use embassy_time::{Duration, Ticker}; use embedded_nal_async::SocketAddr; use futures::pin_mut; use no_std_net::IpAddr; +use portable_atomic::{AtomicBool, AtomicU8, Ordering}; use ublox_sockets::{ AnySocket, ChannelId, PeerHandle, Socket, SocketHandle, SocketSet, SocketStorage, }; @@ -170,7 +170,7 @@ impl UbloxStack UbloxStack UbloxStack UbloxStack UbloxStack { if let Some(edm_channel) = tcp.edm_channel { - defmt::warn!("{}", tcp); + warn!("{}", tcp); return tcp.tx_dequeue(|payload| { let len = core::cmp::min(payload.len(), DATA_PACKAGE_SIZE); let res = if len != 0 { @@ -407,12 +407,12 @@ impl UbloxStack { - defmt::error!("Failed to connect?! {}", e) + error!("Failed to connect?! {}", e) } } } TxEvent::Send { edm_channel, data } => { - defmt::warn!("Sending {} bytes on {}", data.len(), edm_channel); + warn!("Sending {} bytes on {}", data.len(), edm_channel); at.send(EdmDataCommand { channel: edm_channel, data: &data, @@ -501,6 +501,7 @@ enum TxEvent { }, } +#[cfg(feature = "defmt")] impl defmt::Format for TxEvent { fn format(&self, fmt: defmt::Formatter) { match self { diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index 58d7586..d76013c 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -357,7 +357,7 @@ pub mod client { use core::mem::MaybeUninit; use core::ptr::NonNull; - use atomic_polyfill::{AtomicBool, Ordering}; + use portable_atomic::{AtomicBool, Ordering}; use super::*; @@ -406,11 +406,8 @@ pub mod client { async fn connect<'a>( &'a self, - remote: embedded_nal_async::SocketAddr, - ) -> Result, Self::Error> - where - Self: 'a, - { + remote: SocketAddr, + ) -> Result, Self::Error> { let remote_endpoint = (remote.ip(), remote.port()); let mut socket = TcpConnection::new(&self.stack, self.state)?; socket diff --git a/src/blocking/client.rs b/src/blocking/client.rs index f80d704..762d279 100644 --- a/src/blocking/client.rs +++ b/src/blocking/client.rs @@ -222,13 +222,9 @@ where Ok(response.version) } - pub fn retry_send( - &mut self, - cmd: &A, - attempts: usize, - ) -> Result + pub fn retry_send(&mut self, cmd: &A, attempts: usize) -> Result where - A: atat::AtatCmd, + A: atat::AtatCmd, { for _ in 0..attempts { match self.send_internal(cmd, true) { @@ -256,7 +252,7 @@ where self.clear_buffers()?; if let Some(ref mut pin) = self.config.rst_pin { - defmt::warn!("Hard resetting Ublox Short Range"); + warn!("Hard resetting Ublox Short Range"); pin.set_low().ok(); Timer::after(Duration::from_millis(50)).wait(); pin.set_high().ok(); @@ -286,7 +282,7 @@ where self.socket_map = SocketMap::default(); self.udp_listener = UdpListener::new(); - defmt::warn!("Soft resetting Ublox Short Range"); + warn!("Soft resetting Ublox Short Range"); self.send_internal(&EdmAtCmdWrapper(RebootDCE), false)?; self.clear_buffers()?; @@ -333,13 +329,13 @@ where Ok(()) } - pub(crate) fn send_internal( + pub(crate) fn send_internal( &mut self, req: &A, check_urc: bool, ) -> Result where - A: atat::AtatCmd, + A: atat::AtatCmd, { if check_urc { if let Err(e) = self.handle_urc() { @@ -795,9 +791,9 @@ where /// Send AT command /// Automaticaly waraps commands in EDM context - pub fn send_at(&mut self, cmd: A) -> Result + pub fn send_at(&mut self, cmd: A) -> Result where - A: atat::AtatCmd, + A: atat::AtatCmd, { if !self.initialized { self.init()?; diff --git a/src/blocking/dns.rs b/src/blocking/dns.rs index 2ee95d8..48b9904 100644 --- a/src/blocking/dns.rs +++ b/src/blocking/dns.rs @@ -24,7 +24,7 @@ where hostname: &str, _addr_type: AddrType, ) -> nb::Result { - defmt::debug!("Lookup hostname: {}", hostname); + debug!("Lookup hostname: {}", hostname); self.send_at(Ping { hostname, retry_num: 1, diff --git a/src/blocking/tcp_stack.rs b/src/blocking/tcp_stack.rs index 6de3f59..fb72ce3 100644 --- a/src/blocking/tcp_stack.rs +++ b/src/blocking/tcp_stack.rs @@ -37,11 +37,11 @@ where } } - defmt::debug!("[TCP] Opening socket"); + debug!("[TCP] Opening socket"); let socket_id = new_socket_num(sockets).unwrap(); sockets.add(TcpSocket::new(socket_id)).map_err(|e| { - defmt::error!("[TCP] Opening socket Error: {:?}", e); + error!("[TCP] Opening socket Error: {:?}", e); e }) } else { @@ -59,7 +59,7 @@ where return Err(Error::Illegal.into()); } - defmt::debug!("[TCP] Connect socket"); + debug!("[TCP] Connect socket"); self.connected_to_network().map_err(|_| Error::Illegal)?; let url = PeerUrlBuilder::new() @@ -98,7 +98,7 @@ where } } - defmt::trace!("[TCP] Connecting socket: {:?} to url: {=str}", socket, url); + trace!("[TCP] Connecting socket: {:?} to url: {=str}", socket, url); // TODO: Timeout? // TODO: Fix the fact that it doesen't wait for both connect messages @@ -191,7 +191,7 @@ where /// Close an existing TCP socket. fn close(&mut self, socket: Self::TcpSocket) -> Result<(), Self::Error> { if let Some(ref mut sockets) = self.sockets { - defmt::debug!("[TCP] Closing socket: {:?}", socket); + debug!("[TCP] Closing socket: {:?}", socket); // If the socket is not found it is already removed if let Ok(ref tcp) = sockets.get::>(socket) { // If socket is not closed that means a connection excists which has to be closed @@ -208,7 +208,7 @@ where Err(_) => return Err(Error::Unaddressable), } } else { - defmt::error!( + error!( "Illigal state! Socket connected but not in socket map: {:?}", tcp.handle() ); diff --git a/src/blocking/udp_stack.rs b/src/blocking/udp_stack.rs index 203bada..795bc93 100644 --- a/src/blocking/udp_stack.rs +++ b/src/blocking/udp_stack.rs @@ -40,13 +40,13 @@ where } let socket_id = new_socket_num(sockets).unwrap(); - defmt::debug!("[UDP] Opening socket"); + debug!("[UDP] Opening socket"); sockets.add(UdpSocket::new(socket_id)).map_err(|_| { - defmt::error!("[UDP] Opening socket Error: Socket set full"); + error!("[UDP] Opening socket Error: Socket set full"); Error::SocketSetFull }) } else { - defmt::error!("[UDP] Opening socket Error: Missing socket set"); + error!("[UDP] Opening socket Error: Missing socket set"); Err(Error::Illegal) } } @@ -59,14 +59,14 @@ where remote: SocketAddr, ) -> Result<(), Self::Error> { if self.sockets.is_none() { - defmt::error!("[UDP] Connecting socket Error: Missing socket set"); + error!("[UDP] Connecting socket Error: Missing socket set"); return Err(Error::Illegal); } let url = PeerUrlBuilder::new() .address(&remote) .udp() .map_err(|_| Error::Unaddressable)?; - defmt::debug!("[UDP] Connecting Socket: {:?} to URL: {=str}", socket, url); + debug!("[UDP] Connecting Socket: {:?} to URL: {=str}", socket, url); self.connected_to_network().map_err(|_| Error::Illegal)?; @@ -202,7 +202,7 @@ where self.spin().ok(); // Close server socket if self.udp_listener.is_bound(socket) { - defmt::debug!("[UDP] Closing Server socket: {:?}", socket); + debug!("[UDP] Closing Server socket: {:?}", socket); // ID 2 used by UDP server self.send_internal( @@ -218,7 +218,7 @@ where if let Some(ref mut sockets) = self.sockets { // If socket in socket set close if sockets.remove(socket).is_err() { - defmt::error!( + error!( "[UDP] Closing server socket error: No socket matching: {:?}", socket ); @@ -231,19 +231,19 @@ where // Close incomming connections while self.udp_listener.available(socket).unwrap_or(false) { if let Ok((connection_handle, _)) = self.udp_listener.get_remote(socket) { - defmt::debug!( + debug!( "[UDP] Closing incomming socket for Server: {:?}", connection_handle ); self.close(connection_handle)?; } else { - defmt::error!("[UDP] Incomming socket for server error - Listener says available, while nothing present"); + error!("[UDP] Incomming socket for server error - Listener says available, while nothing present"); } } // Unbind server socket in listener self.udp_listener.unbind(socket).map_err(|_| { - defmt::error!( + error!( "[UDP] Closing socket error: No server socket matching: {:?}", socket ); @@ -251,10 +251,10 @@ where }) // Handle normal sockets } else if let Some(ref mut sockets) = self.sockets { - defmt::debug!("[UDP] Closing socket: {:?}", socket); + debug!("[UDP] Closing socket: {:?}", socket); // If no sockets exists, nothing to close. if let Ok(ref mut udp) = sockets.get::>(socket) { - defmt::trace!("[UDP] Closing socket state: {:?}", udp.state()); + trace!("[UDP] Closing socket state: {:?}", udp.state()); match udp.state() { UdpState::Closed => { sockets.remove(socket).ok(); @@ -269,7 +269,7 @@ where } } } else { - defmt::error!( + error!( "[UDP] Closing socket error: No socket matching: {:?}", socket ); @@ -301,10 +301,9 @@ where return Err(Error::Illegal); } - defmt::debug!( + debug!( "[UDP] binding socket: {:?} to port: {:?}", - socket, - local_port + socket, local_port ); // ID 2 used by UDP server diff --git a/src/command/custom_digest.rs b/src/command/custom_digest.rs index cf6a4c4..a7b8dd5 100644 --- a/src/command/custom_digest.rs +++ b/src/command/custom_digest.rs @@ -18,12 +18,12 @@ impl Digester for EdmDigester { return (DigestResult::None, 0); } - defmt::trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(&buf)); if buf.len() >= STARTUPMESSAGE.len() && buf[..2] == *b"\r\n" { if let Some(i) = buf[2..].windows(2).position(|x| x == *b"\r\n") { // Two for starting position, one for index -> len and one for the window size. let len = i + 4; - defmt::trace!("Digest common at {:?}; i: {:?}", LossyStr(&buf[..len]), i); + trace!("Digest common at {:?}; i: {:?}", LossyStr(&buf[..len]), i); if buf[..len] == *STARTUPMESSAGE { return ( DigestResult::Urc(&buf[..STARTUPMESSAGE.len()]), @@ -67,7 +67,7 @@ impl Digester for EdmDigester { // Debug statement for trace properly if !buf.is_empty() { - defmt::trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(&buf)); } // Filter message by payload diff --git a/src/command/data_mode/urc.rs b/src/command/data_mode/urc.rs index 35eb098..8eb2441 100644 --- a/src/command/data_mode/urc.rs +++ b/src/command/data_mode/urc.rs @@ -17,14 +17,14 @@ pub struct PeerConnected { // #[at_arg(position = 3)] // pub local_address: IpAddr, #[at_arg(position = 3)] - #[defmt(Debug2Format)] + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] pub local_address: Bytes<40>, #[at_arg(position = 4)] pub local_port: u16, // #[at_arg(position = 5)] // pub remote_address: IpAddr, #[at_arg(position = 5)] - #[defmt(Debug2Format)] + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] pub remote_address: Bytes<40>, #[at_arg(position = 6)] pub remote_port: u16, diff --git a/src/command/edm/mod.rs b/src/command/edm/mod.rs index 64021f1..ae46aea 100644 --- a/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -23,97 +23,30 @@ pub(crate) fn calc_payload_len(resp: &[u8]) -> usize { // using the parameter. Instead the parameter must // be set to 0 and the serial settings will take effect when the module is reset. #[derive(Debug, Clone)] -pub(crate) struct EdmAtCmdWrapper, const LEN: usize>(pub T); +pub(crate) struct EdmAtCmdWrapper(pub T); -impl atat::AtatCmd<1024> for EdmAtCmdWrapper -where - T: AtatCmd, -{ +impl atat::AtatCmd for EdmAtCmdWrapper { type Response = T::Response; + const MAX_LEN: usize = T::MAX_LEN + 6; + const MAX_TIMEOUT_MS: u32 = T::MAX_TIMEOUT_MS; - fn as_bytes(&self) -> Vec { - let at_vec = self.0.as_bytes(); - let payload_len = (at_vec.len() + 2) as u16; - [ + fn write(&self, buf: &mut [u8]) -> usize { + let at_len = self.0.write(&mut buf[5..]); + let payload_len = (at_len + 2) as u16; + + buf[0..5].copy_from_slice(&[ STARTBYTE, (payload_len >> 8) as u8 & EDM_SIZE_FILTER, (payload_len & 0xffu16) as u8, 0x00, PayloadType::ATRequest as u8, - ] - .iter() - .cloned() - .chain(at_vec) - .chain(core::iter::once(ENDBYTE)) - .collect() - } + ]); - fn parse( - &self, - resp: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - let resp = resp.and_then(|resp| { - if resp.len() < PAYLOAD_OVERHEAD - || !resp.starts_with(&[STARTBYTE]) - || !resp.ends_with(&[ENDBYTE]) - { - return Err(atat::InternalError::InvalidResponse); - }; + buf[5 + at_len] = ENDBYTE; - let payload_len = calc_payload_len(resp); - - if resp.len() != payload_len + EDM_OVERHEAD - || resp[4] != PayloadType::ATConfirmation as u8 - { - return Err(atat::InternalError::InvalidResponse); - } - - // Recieved OK response code in EDM response? - match resp - .windows(b"\r\nOK".len()) - .position(|window| window == b"\r\nOK") - { - // Cutting OK out leaves an empth string for NoResponse, for - // other responses just removes "\r\nOK\r\n" - Some(pos) => Ok(&resp[AT_COMMAND_POSITION..pos]), - // Isolate the AT_response - None => Ok(&resp[AT_COMMAND_POSITION..PAYLOAD_POSITION + payload_len]), - } - }); - - self.0.parse(resp) - } -} - -/////////////////////// Temp Solution for fixed size /////////////////////// -#[derive(Debug, Clone)] -pub(crate) struct BigEdmAtCmdWrapper, const LEN: usize>(pub T); - -impl atat::AtatCmd<2054> for BigEdmAtCmdWrapper -where - T: AtatCmd, -{ - type Response = T::Response; - - const MAX_TIMEOUT_MS: u32 = T::MAX_TIMEOUT_MS; - - fn as_bytes(&self) -> Vec { - let at_vec = self.0.as_bytes(); - let payload_len = (at_vec.len() + 2) as u16; - [ - STARTBYTE, - (payload_len >> 8) as u8 & EDM_SIZE_FILTER, - (payload_len & 0xffu16) as u8, - 0x00, - PayloadType::ATRequest as u8, - ] - .iter() - .cloned() - .chain(at_vec) - .chain(core::iter::once(ENDBYTE)) - .collect() + 5 + at_len + 1 } fn parse( @@ -152,7 +85,6 @@ where self.0.parse(resp) } } -////////////////////////////////////////////////////////////////////////////////////////////// #[derive(Debug, Clone)] pub struct EdmDataCommand<'a> { @@ -160,54 +92,57 @@ pub struct EdmDataCommand<'a> { pub data: &'a [u8], } // wifi::socket::EGRESS_CHUNK_SIZE + PAYLOAD_OVERHEAD = 512 + 6 + 1 = 519 -impl<'a> atat::AtatCmd<{ DATA_PACKAGE_SIZE + 7 }> for EdmDataCommand<'a> { +impl<'a> atat::AtatCmd for EdmDataCommand<'a> { type Response = NoResponse; + const MAX_LEN: usize = DATA_PACKAGE_SIZE + 7; + const EXPECTS_RESPONSE_CODE: bool = false; - fn as_bytes(&self) -> Vec { + fn parse( + &self, + _resp: Result<&[u8], atat::InternalError>, + ) -> core::result::Result { + Ok(NoResponse) + } + + fn write(&self, buf: &mut [u8]) -> usize { let payload_len = (self.data.len() + 3) as u16; - [ + buf[0..6].copy_from_slice(&[ STARTBYTE, (payload_len >> 8) as u8 & EDM_SIZE_FILTER, (payload_len & 0xffu16) as u8, 0x00, PayloadType::DataCommand as u8, self.channel.0, - ] - .iter() - .cloned() - .chain(self.data.iter().cloned()) - .chain(core::iter::once(ENDBYTE)) - .collect() - } + ]); - fn parse( - &self, - _resp: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - Ok(NoResponse) + buf[6..6 + self.data.len()].copy_from_slice(self.data); + buf[6 + self.data.len()] = ENDBYTE; + + 6 + self.data.len() + 1 } } #[derive(Debug, Clone)] pub struct EdmResendConnectEventsCommand; -impl atat::AtatCmd<6> for EdmResendConnectEventsCommand { +impl atat::AtatCmd for EdmResendConnectEventsCommand { type Response = NoResponse; - fn as_bytes(&self) -> Vec { - [ + const MAX_LEN: usize = 6; + + fn write(&self, buf: &mut [u8]) -> usize { + buf[0..6].copy_from_slice(&[ STARTBYTE, 0x00, 0x02, 0x00, PayloadType::ResendConnectEventsCommand as u8, ENDBYTE, - ] - .iter() - .cloned() - .collect() + ]); + + 6 } fn parse( @@ -221,18 +156,18 @@ impl atat::AtatCmd<6> for EdmResendConnectEventsCommand { #[derive(Debug, Clone)] pub struct SwitchToEdmCommand; -impl atat::AtatCmd<6> for SwitchToEdmCommand { +impl atat::AtatCmd for SwitchToEdmCommand { type Response = NoResponse; + const MAX_LEN: usize = 6; + const MAX_TIMEOUT_MS: u32 = 2000; - fn as_bytes(&self) -> Vec { + fn write(&self, buf: &mut [u8]) -> usize { ChangeMode { mode: data_mode::types::Mode::ExtendedDataMode, } - .as_bytes() - .into_iter() - .collect() + .write(buf) } fn parse( @@ -279,7 +214,11 @@ mod test { PayloadType::ATConfirmation as u8, 0x55, ]; - assert_eq!(parse.as_bytes(), correct_cmd); + + let mut buf = [0u8; as AtatCmd>::MAX_LEN]; + let len = parse.write(&mut buf); + + assert_eq!(buf[..len], correct_cmd); assert_eq!(parse.parse(Ok(response)), Ok(correct_response)); let parse = EdmAtCmdWrapper(SystemStatus { @@ -319,7 +258,10 @@ mod test { 0x0A, 0x55, ]; - assert_eq!(parse.as_bytes(), correct); + let mut buf = [0u8; as AtatCmd>::MAX_LEN]; + let len = parse.write(&mut buf); + + assert_eq!(buf[..len], correct); assert_eq!(parse.parse(Ok(response)), Ok(correct_response)); } @@ -451,7 +393,11 @@ mod test { fn change_to_edm_cmd() { let resp = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; let correct = Vec::<_, 6>::from_slice(b"ATO2\r\n").unwrap(); - assert_eq!(SwitchToEdmCommand.as_bytes(), correct); + + let mut buf = [0u8; SwitchToEdmCommand::MAX_LEN]; + let len = SwitchToEdmCommand.write(&mut buf); + + assert_eq!(buf[..len], correct); assert_eq!(SwitchToEdmCommand.parse(Ok(resp)).unwrap(), NoResponse); } } diff --git a/src/command/edm/urc.rs b/src/command/edm/urc.rs index bc011a3..dbe3c5a 100644 --- a/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -26,7 +26,7 @@ impl AtatUrc for EdmEvent { /// Parse the response into a `Self::Response` instance. fn parse(resp: &[u8]) -> Option { - defmt::trace!("[Parse URC] {:?}", LossyStr(resp)); + trace!("[Parse URC] {:?}", LossyStr(resp)); // Startup message? // TODO: simplify mayby no packet check. if resp.len() >= STARTUPMESSAGE.len() @@ -59,12 +59,12 @@ impl AtatUrc for EdmEvent { || !resp.starts_with(&[STARTBYTE]) || !resp.ends_with(&[ENDBYTE]) { - defmt::error!("[Parse URC Start/End byte Error] {:?}", LossyStr(&resp)); + error!("[Parse URC Start/End byte Error] {:?}", LossyStr(&resp)); return None; }; let payload_len = calc_payload_len(resp); if resp.len() != payload_len + EDM_OVERHEAD { - defmt::error!("[Parse URC lenght Error] {:?}", LossyStr(resp)); + error!("[Parse URC lenght Error] {:?}", LossyStr(resp)); return None; } @@ -157,7 +157,7 @@ impl AtatUrc for EdmEvent { PayloadType::StartEvent => EdmEvent::StartUp.into(), _ => { - defmt::error!("[Parse URC Error] {:?}", LossyStr(resp)); + error!("[Parse URC Error] {:?}", LossyStr(resp)); None } } diff --git a/src/command/general/types.rs b/src/command/general/types.rs index 80dfb95..ac24d4d 100644 --- a/src/command/general/types.rs +++ b/src/command/general/types.rs @@ -89,7 +89,7 @@ impl core::str::FromStr for FirmwareVersion { let (patch, meta) = match patch_meta.split_once('-') { Some((patch_str, meta)) => ( patch_str.parse().map_err(|_| DeserializeError)?, - Some(heapless::String::from(meta)), + heapless::String::try_from(meta).ok(), ), None => (patch_meta.parse().map_err(|_| DeserializeError)?, None), }; @@ -103,6 +103,7 @@ impl core::str::FromStr for FirmwareVersion { } } +#[cfg(feature = "defmt")] impl defmt::Format for FirmwareVersion { fn format(&self, fmt: defmt::Formatter) { if let Some(meta) = &self.meta { diff --git a/src/command/ping/types.rs b/src/command/ping/types.rs index cfe102c..16fdaa8 100644 --- a/src/command/ping/types.rs +++ b/src/command/ping/types.rs @@ -25,7 +25,8 @@ use atat::atat_derive::AtatEnum; /// - Default value: 1000 // pub type Inteval = u16; -#[derive(Debug, PartialEq, Clone, Copy, AtatEnum, defmt::Format)] +#[derive(Debug, PartialEq, Clone, Copy, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum PingError { /// 1 - 6: Internal error (ping level) diff --git a/src/command/ping/urc.rs b/src/command/ping/urc.rs index 856d63c..0abc5c7 100644 --- a/src/command/ping/urc.rs +++ b/src/command/ping/urc.rs @@ -38,7 +38,8 @@ pub struct PingResponse { pub rtt: i32, } -#[derive(Debug, PartialEq, Clone, AtatResp, defmt::Format)] +#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PingErrorResponse { #[at_arg(position = 0)] pub error: PingError, diff --git a/src/connection.rs b/src/connection.rs index e657393..c270887 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,6 +1,7 @@ use crate::network::{WifiMode, WifiNetwork}; -#[derive(Debug, Clone, Copy, PartialEq, defmt::Format)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WiFiState { Inactive, /// Searching for Wifi diff --git a/src/error.rs b/src/error.rs index 8fe05fb..5c4d426 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ pub use ublox_sockets::Error as SocketError; -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { Overflow, SetState, @@ -46,7 +47,8 @@ impl From for Error { } /// Error that occurs when attempting to connect to a wireless network. -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiConnectionError { /// Failed to connect to wireless network. FailedToConnect, @@ -72,7 +74,8 @@ impl From for WifiConnectionError { } } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiError { // The specified wifi is currently disabled. Try switching it on. WifiDisabled, @@ -88,7 +91,8 @@ pub enum WifiError { // Other, } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiHotspotError { /// Failed to ceate wireless hotspot. CreationFailed, diff --git a/src/fmt.rs b/src/fmt.rs new file mode 100644 index 0000000..3476f4c --- /dev/null +++ b/src/fmt.rs @@ -0,0 +1,258 @@ +#![macro_use] +#![allow(unused_macros)] + +use core::fmt::{Debug, Display, LowerHex}; + +#[cfg(all(feature = "defmt", feature = "log"))] +compile_error!("You may not enable both `defmt` and `log` features."); + +macro_rules! assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert!($($x)*); + } + }; +} + +macro_rules! assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_eq!($($x)*); + } + }; +} + +macro_rules! assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::assert_ne!($($x)*); + } + }; +} + +macro_rules! debug_assert { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert!($($x)*); + #[cfg(feature = "defmt")] + ::debug_assert!($($x)*); + } + }; +} + +macro_rules! debug_assert_eq { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_eq!($($x)*); + #[cfg(feature = "defmt")] + ::debug_assert_eq!($($x)*); + } + }; +} + +macro_rules! debug_assert_ne { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::debug_assert_ne!($($x)*); + #[cfg(feature = "defmt")] + ::debug_assert_ne!($($x)*); + } + }; +} + +macro_rules! todo { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::todo!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::todo!($($x)*); + } + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unreachable { + ($($x:tt)*) => { + ::core::unreachable!($($x)*) + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unreachable { + ($($x:tt)*) => { + ::defmt::unreachable!($($x)*) + }; +} + +macro_rules! panic { + ($($x:tt)*) => { + { + #[cfg(not(feature = "defmt"))] + ::core::panic!($($x)*); + #[cfg(feature = "defmt")] + ::defmt::panic!($($x)*); + } + }; +} + +macro_rules! trace { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::trace!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::trace!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! debug { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::debug!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::debug!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! info { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::info!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::info!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! warn { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::warn!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::warn!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +macro_rules! error { + ($s:literal $(, $x:expr)* $(,)?) => { + { + #[cfg(feature = "log")] + ::log::error!($s $(, $x)*); + #[cfg(feature = "defmt")] + ::error!($s $(, $x)*); + #[cfg(not(any(feature = "log", feature="defmt")))] + let _ = ($( & $x ),*); + } + }; +} + +#[cfg(feature = "defmt")] +macro_rules! unwrap { + ($($x:tt)*) => { + ::defmt::unwrap!($($x)*) + }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! unwrap { + ($arg:expr) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {:?}", ::core::stringify!($arg), e); + } + } + }; + ($arg:expr, $($msg:expr),+ $(,)? ) => { + match $crate::fmt::Try::into_result($arg) { + ::core::result::Result::Ok(t) => t, + ::core::result::Result::Err(e) => { + ::core::panic!("unwrap of `{}` failed: {}: {:?}", ::core::stringify!($arg), ::core::format_args!($($msg,)*), e); + } + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct NoneError; + +pub trait Try { + type Ok; + type Error; + fn into_result(self) -> Result; +} + +impl Try for Option { + type Ok = T; + type Error = NoneError; + + #[inline] + fn into_result(self) -> Result { + self.ok_or(NoneError) + } +} + +impl Try for Result { + type Ok = T; + type Error = E; + + #[inline] + fn into_result(self) -> Self { + self + } +} + +#[allow(unused)] +pub(crate) struct Bytes<'a>(pub &'a [u8]); + +impl<'a> Debug for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> Display for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> LowerHex for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for Bytes<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} diff --git a/src/lib.rs b/src/lib.rs index 867ba7d..9f5cb6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,8 @@ #![cfg_attr(feature = "async", feature(impl_trait_projections))] #![cfg_attr(feature = "async", feature(type_alias_impl_trait))] +mod fmt; + #[cfg(feature = "async")] pub mod asynch; diff --git a/src/peer_builder.rs b/src/peer_builder.rs index 8334b53..02f18bf 100644 --- a/src/peer_builder.rs +++ b/src/peer_builder.rs @@ -133,7 +133,10 @@ mod test { #[test] fn udp_ipv4_url() { let address = "192.168.0.1:8080".parse().unwrap(); - let url = PeerUrlBuilder::new().address(&address).udp().unwrap(); + let url = PeerUrlBuilder::new() + .address(&address) + .udp::<128>() + .unwrap(); assert_eq!(url, "udp://192.168.0.1:8080/"); } @@ -142,7 +145,10 @@ mod test { let address = "[FE80:0000:0000:0000:0202:B3FF:FE1E:8329]:8080" .parse() .unwrap(); - let url = PeerUrlBuilder::new().address(&address).udp().unwrap(); + let url = PeerUrlBuilder::new() + .address(&address) + .udp::<128>() + .unwrap(); assert_eq!(url, "udp://[fe80::202:b3ff:fe1e:8329]:8080/"); } @@ -152,7 +158,7 @@ mod test { .hostname("example.org") .port(2000) .local_port(2001) - .udp() + .udp::<128>() .unwrap(); assert_eq!(url, "udp://example.org:2000/?local_port=2001"); } diff --git a/src/wifi/mod.rs b/src/wifi/mod.rs index 623f2ca..15f6b48 100644 --- a/src/wifi/mod.rs +++ b/src/wifi/mod.rs @@ -39,7 +39,7 @@ impl SocketMap { channel_id: ChannelId, socket_handle: SocketHandle, ) -> Result<(), ()> { - defmt::trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, channel_id); + trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, channel_id); self.channel_map .insert(channel_id, socket_handle) .map_err(drop)?; @@ -47,7 +47,7 @@ impl SocketMap { } pub fn remove_channel(&mut self, channel_id: &ChannelId) -> Result<(), ()> { - defmt::trace!("[SOCK_MAP] {:?} removed", channel_id); + trace!("[SOCK_MAP] {:?} removed", channel_id); self.channel_map.remove(channel_id).ok_or(())?; Ok(()) } @@ -63,13 +63,13 @@ impl SocketMap { } pub fn insert_peer(&mut self, peer: PeerHandle, socket_handle: SocketHandle) -> Result<(), ()> { - defmt::trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, peer); + trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, peer); self.peer_map.insert(peer, socket_handle).map_err(drop)?; Ok(()) } pub fn remove_peer(&mut self, peer: &PeerHandle) -> Result<(), ()> { - defmt::trace!("[SOCK_MAP] {:?} removed", peer); + trace!("[SOCK_MAP] {:?} removed", peer); self.peer_map.remove(peer).ok_or(())?; Ok(()) } diff --git a/src/wifi/options.rs b/src/wifi/options.rs index f76b581..aac9dd3 100644 --- a/src/wifi/options.rs +++ b/src/wifi/options.rs @@ -55,7 +55,8 @@ impl HotspotOptions { } } -#[derive(Debug, Clone, Default, Serialize, Deserialize, defmt::Format)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct ConnectionOptions { pub ssid: String<64>, pub password: Option>, diff --git a/src/wifi/supplicant.rs b/src/wifi/supplicant.rs index b11e40f..dabd5bf 100644 --- a/src/wifi/supplicant.rs +++ b/src/wifi/supplicant.rs @@ -22,7 +22,7 @@ use super::{ options::ConnectionOptions, }; -use defmt::debug; +use debug; /// Supplicant is used to /// @@ -60,12 +60,9 @@ impl<'a, C, const N: usize> Supplicant<'a, C, N> where C: atat::blocking::AtatClient, { - fn send_at, const LEN: usize>( - &mut self, - req: &A, - ) -> Result { + fn send_at(&mut self, req: &A) -> Result { self.client.send(req).map_err(|e| { - defmt::error!("{:?}: {=[u8]:a}", e, req.as_bytes()); + error!("{:?}: {=[u8]:a}", e, req.as_bytes()); e.into() }) } @@ -130,7 +127,7 @@ where // but should this be the case, the module is already having unexpected behaviour } else { // This causes unexpected behaviour - defmt::error!("Two configs are active on startup!"); + error!("Two configs are active on startup!"); return Err(Error::Supplicant); } } else if load.is_err() { @@ -145,7 +142,7 @@ where if let WifiStationConfigR::SSID(ssid) = parameter { if !ssid.is_empty() { - defmt::error!("Shadow store bug!"); + error!("Shadow store bug!"); // This should fix the issue self.remove_connection(config_id) .map_err(|_| Error::Supplicant)?; @@ -279,7 +276,7 @@ where debug!("[SUP] Remove connection: {:?}", config_id); // check for active if self.is_config_in_use(config_id) { - defmt::error!("Config id is active!"); + error!("Config id is active!"); return Err(WifiConnectionError::Illegal); } @@ -315,7 +312,7 @@ where // check for active if self.is_config_in_use(config_id) { - defmt::error!("Config id is active!"); + error!("Config id is active!"); return Err(WifiConnectionError::Illegal); } @@ -522,12 +519,12 @@ where } // check for active connection if self.is_config_in_use(active_on_startup) { - defmt::error!("Active on startup is active!"); + error!("Active on startup is active!"); return Err(WifiConnectionError::Illegal); } } if self.is_config_in_use(config_id) { - defmt::error!("Config id is active!"); + error!("Config id is active!"); return Err(WifiConnectionError::Illegal); } @@ -570,7 +567,7 @@ where if let Some(active_on_startup) = self.active_on_startup.clone() { // check for active connection if self.is_config_in_use(active_on_startup) { - defmt::error!("Active on startup is active!"); + error!("Active on startup is active!"); return Err(WifiConnectionError::Illegal); } // if any active remove this asset. @@ -597,7 +594,7 @@ where } } else if let Some(ref con) = self.wifi_connection { if con.activated && con.config_id == config_id { - defmt::error!("One of the IDs being changed is activated!"); + error!("One of the IDs being changed is activated!"); return true; } } From 28434928867a57a30279c2f88ae6a4e2e96d123d Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 13 Jan 2024 12:16:06 +0100 Subject: [PATCH 10/16] Add TlsSocket --- Cargo.toml | 11 +- examples/rpi-pico/Cargo.toml | 6 +- examples/rpi-pico/src/bin/embassy-async.rs | 2 +- rust-toolchain.toml | 2 +- src/asynch/mod.rs | 8 +- src/asynch/ublox_stack/dns.rs | 137 ++++--- src/asynch/ublox_stack/mod.rs | 175 ++++++--- src/asynch/ublox_stack/tcp.rs | 413 +++++++++++++++++--- src/asynch/ublox_stack/tls.rs | 426 +++++++++++++++++++++ src/asynch/ublox_stack/udp.rs | 249 ++++++++++++ src/command/edm/urc.rs | 2 +- src/fmt.rs | 38 +- src/lib.rs | 10 +- src/peer_builder.rs | 94 ++--- 14 files changed, 1315 insertions(+), 258 deletions(-) create mode 100644 src/asynch/ublox_stack/tls.rs create mode 100644 src/asynch/ublox_stack/udp.rs diff --git a/Cargo.toml b/Cargo.toml index 36bb672..681f4b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ name = "ublox_short_range" doctest = false [dependencies] -atat = { version = "0.20.0", features = ["derive", "bytes"] } +atat = { version = "0.21", features = ["derive", "bytes"] } # atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "70283be", features = ["derive", "defmt", "bytes"] } heapless = { version = "^0.8", features = ["serde"] } no-std-net = { version = "0.6", features = ["serde"] } @@ -27,13 +27,13 @@ portable-atomic = "1.5" defmt = { version = "0.3", optional = true } log = { version = "0.4.14", optional = true } -embedded-hal = "=1.0.0-rc.3" -embassy-time = "0.2" +embedded-hal = "1.0" +embassy-time = "0.3" embassy-sync = "0.5" embassy-futures = "0.1" embassy-net-driver = "0.2" -embedded-nal-async = { version = "0.7", optional = true } +embedded-nal-async = { version = "0.7" } futures = { version = "0.3.17", default-features = false, features = [ "async-await", ] } @@ -42,9 +42,8 @@ embedded-io = "0.6" embedded-io-async = "0.6" [features] -default = ["async", "odin_w2xx", "ublox-sockets", "socket-tcp"] +default = ["odin_w2xx", "ublox-sockets", "socket-tcp", "socket-udp"] -async = ["dep:embedded-nal-async", "atat/async", "ublox-sockets?/async"] std = [] diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index db9c02d..0839378 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" [dependencies] -ublox-short-range-rs = { path = "../../", features = ["async", "odin_w2xx", "ublox-sockets", "socket-tcp"] } -embassy-executor = { version = "0.4", features = ["defmt", "integrated-timers", "nightly"] } -embassy-time = { version = "0.2", features = ["defmt", "defmt-timestamp-uptime"] } +ublox-short-range-rs = { path = "../../", features = ["odin_w2xx", "ublox-sockets", "socket-tcp"] } +embassy-executor = { version = "0.5", features = ["defmt", "integrated-timers", "nightly"] } +embassy-time = { version = "0.3", features = ["defmt", "defmt-timestamp-uptime"] } embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-pac", "time-driver"] } embassy-futures = { version = "0.1.0" } no-std-net = { version = "0.6", features = ["serde"] } diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index f1467cd..7ef8b7a 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -226,7 +226,7 @@ async fn main(spawner: Spawner) { Timer::after(Duration::from_secs(1)).await; let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); - // // socket.set_timeout(Some(Duration::from_secs(10))); + // socket.set_timeout(Some(Duration::from_secs(10))); let remote: SocketAddr = (Ipv4Addr::new(192, 168, 1, 183), 4444).into(); info!("Connecting... {}", debug2Format(&remote)); diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 41845fc..b4220cb 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "nightly-2023-12-24" +channel = "1.75" components = [ "rust-src", "rustfmt", "llvm-tools" ] targets = [ "thumbv6m-none-eabi", diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 12a16f8..2e6776b 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -46,13 +46,7 @@ impl State { } } -pub async fn new< - 'a, - AT: AtatClient, - // AT: AtatClient + atat::UartExt, - RST: OutputPin, - const URC_CAPACITY: usize, ->( +pub async fn new<'a, AT: AtatClient, RST: OutputPin, const URC_CAPACITY: usize>( state: &'a mut State, subscriber: &'a atat::UrcChannel, reset: RST, diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index 734e740..72e43d0 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -1,12 +1,13 @@ use core::{cell::RefCell, future::poll_fn, task::Poll}; use atat::asynch::AtatClient; +use embassy_sync::waitqueue::WakerRegistration; use embedded_nal_async::AddrType; use no_std_net::IpAddr; -use crate::asynch::ublox_stack::DnsState; +use crate::command::ping::types::PingError; -use super::{DnsQuery, SocketStack, UbloxStack}; +use super::{SocketStack, UbloxStack}; /// Errors returned by DnsSocket. #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -20,6 +21,89 @@ pub enum Error { Failed, } +/// From u-connectXpress AT commands manual: +/// depends on the . For internet domain names, the maximum +/// length is 64 characters. +/// Domain name length is 128 for NINA-W13 and NINA-W15 software version 4.0 +/// .0 or later. +#[cfg(not(feature = "nina_w1xx"))] +pub const MAX_DOMAIN_NAME_LENGTH: usize = 64; + +#[cfg(feature = "nina_w1xx")] +pub const MAX_DOMAIN_NAME_LENGTH: usize = 128; + +pub struct DnsTableEntry { + pub domain_name: heapless::String, + pub state: DnsState, + pub waker: WakerRegistration, +} + +#[derive(PartialEq, Clone)] +pub enum DnsState { + New, + Pending, + Resolved(IpAddr), + Error(PingError), +} + +impl DnsTableEntry { + pub const fn new(domain_name: heapless::String) -> Self { + Self { + domain_name, + state: DnsState::New, + waker: WakerRegistration::new(), + } + } +} + +pub struct DnsTable { + pub table: heapless::Deque, +} + +impl DnsTable { + pub const fn new() -> Self { + Self { + table: heapless::Deque::new(), + } + } + pub fn upsert(&mut self, new_entry: DnsTableEntry) { + if let Some(entry) = self + .table + .iter_mut() + .find(|e| e.domain_name == new_entry.domain_name) + { + entry.state = new_entry.state; + return; + } + + if self.table.is_full() { + self.table.pop_front(); + } + unsafe { + self.table.push_back_unchecked(new_entry); + } + } + + pub fn get(&self, domain_name: &str) -> Option<&DnsTableEntry> { + self.table + .iter() + .find(|e| e.domain_name.as_str() == domain_name) + } + + pub fn get_mut(&mut self, domain_name: &str) -> Option<&mut DnsTableEntry> { + self.table + .iter_mut() + .find(|e| e.domain_name.as_str() == domain_name) + } + + pub fn reverse_lookup(&self, ip: IpAddr) -> Option<&str> { + self.table + .iter() + .find(|e| e.state == DnsState::Resolved(ip)) + .map(|e| e.domain_name.as_str()) + } +} + /// DNS client compatible with the `embedded-nal-async` traits. /// /// This exists only for compatibility with crates that use `embedded-nal-async`. @@ -59,53 +143,18 @@ impl<'a> DnsSocket<'a> { { let mut s = self.stack.borrow_mut(); - if s.dns_queries - .insert(name_string.clone(), DnsQuery::new()) - .is_err() - { - error!("Attempted to start more simultaneous DNS requests than the (4) supported"); - } + s.dns_table.upsert(DnsTableEntry::new(name_string.clone())); s.waker.wake(); } - #[must_use = "to delay the drop handler invocation to the end of the scope"] - struct OnDrop { - f: core::mem::MaybeUninit, - } - - impl OnDrop { - fn new(f: F) -> Self { - Self { - f: core::mem::MaybeUninit::new(f), - } - } - - fn defuse(self) { - core::mem::forget(self) - } - } - - impl Drop for OnDrop { - fn drop(&mut self) { - unsafe { self.f.as_ptr().read()() } - } - } - - let drop = OnDrop::new(|| { - let mut s = self.stack.borrow_mut(); - s.dns_queries.remove(&name_string); - }); - let res = poll_fn(|cx| { let mut s = self.stack.borrow_mut(); - let query = s.dns_queries.get_mut(&name_string).unwrap(); + let query = s.dns_table.get_mut(&name_string).unwrap(); match query.state { - DnsState::Ok(ip) => { - s.dns_queries.remove(&name_string); + DnsState::Resolved(ip) => { return Poll::Ready(Ok(ip)); } - DnsState::Err => { - s.dns_queries.remove(&name_string); + DnsState::Error(_e) => { return Poll::Ready(Err(Error::Failed)); } _ => { @@ -116,8 +165,6 @@ impl<'a> DnsSocket<'a> { }) .await; - drop.defuse(); - res } } @@ -135,8 +182,8 @@ impl<'a> embedded_nal_async::Dns for DnsSocket<'a> { async fn get_host_by_address( &self, - addr: IpAddr, - result: &mut [u8], + _addr: IpAddr, + _result: &mut [u8], ) -> Result { unimplemented!() } diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index ad32373..fce89b7 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -1,7 +1,9 @@ #[cfg(feature = "socket-tcp")] pub mod tcp; -// #[cfg(feature = "socket-udp")] -// pub mod udp; +#[cfg(feature = "socket-tcp")] +pub mod tls; +#[cfg(feature = "socket-udp")] +pub mod udp; pub mod dns; @@ -17,12 +19,15 @@ use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; use crate::command::edm::types::{DataEvent, Protocol, DATA_PACKAGE_SIZE}; use crate::command::edm::urc::EdmEvent; use crate::command::edm::EdmDataCommand; +use crate::command::ping::types::PingError; use crate::command::ping::urc::{PingErrorResponse, PingResponse}; use crate::command::ping::Ping; +use crate::command::security::types::SecurityDataType; +use crate::command::security::{PrepareSecurityDataImport, SendSecurityDataImport}; use crate::command::Urc; -use crate::peer_builder::PeerUrlBuilder; +use crate::peer_builder::{PeerUrlBuilder, SecurityCredentials}; -use self::dns::DnsSocket; +use self::dns::{DnsSocket, DnsState, DnsTable, MAX_DOMAIN_NAME_LENGTH}; use super::state::{self, LinkState}; use super::AtHandle; @@ -42,7 +47,8 @@ use ublox_sockets::{ #[cfg(feature = "socket-tcp")] use ublox_sockets::TcpState; -const MAX_HOSTNAME_LEN: usize = 64; +#[cfg(feature = "socket-udp")] +use ublox_sockets::UdpState; pub struct StackResources { sockets: [SocketStorage<'static>; SOCK], @@ -64,32 +70,12 @@ pub struct UbloxStack { link_up: AtomicBool, } -enum DnsState { - New, - Pending, - Ok(IpAddr), - Err, -} - -struct DnsQuery { - state: DnsState, - waker: WakerRegistration, -} - -impl DnsQuery { - pub fn new() -> Self { - Self { - state: DnsState::New, - waker: WakerRegistration::new(), - } - } -} - struct SocketStack { sockets: SocketSet<'static>, waker: WakerRegistration, - dns_queries: heapless::FnvIndexMap, DnsQuery, 4>, + dns_table: DnsTable, dropped_sockets: heapless::Vec, + credential_map: heapless::FnvIndexMap, } impl UbloxStack { @@ -101,9 +87,10 @@ impl UbloxStack UbloxStack Result<(), atat::Error> { + let mut device = self.device.borrow_mut(); + + assert!(root_ca.0.len() < 16); + assert!(cert.0.len() < 16); + assert!(priv_key.0.len() < 16); + + device + .at + .send_edm(PrepareSecurityDataImport { + data_type: SecurityDataType::TrustedRootCA, + data_size: root_ca.1.len(), + internal_name: root_ca.0, + password: None, + }) + .await?; + + device + .at + .send_edm(SendSecurityDataImport { + data: atat::serde_bytes::Bytes::new(root_ca.1), + }) + .await?; + + device + .at + .send_edm(PrepareSecurityDataImport { + data_type: SecurityDataType::ClientCertificate, + data_size: cert.1.len(), + internal_name: cert.0, + password: None, + }) + .await?; + + device + .at + .send_edm(SendSecurityDataImport { + data: atat::serde_bytes::Bytes::new(cert.1), + }) + .await?; + + device + .at + .send_edm(PrepareSecurityDataImport { + data_type: SecurityDataType::ClientPrivateKey, + data_size: priv_key.1.len(), + internal_name: priv_key.0, + password: None, + }) + .await?; + + device + .at + .send_edm(SendSecurityDataImport { + data: atat::serde_bytes::Bytes::new(priv_key.1), + }) + .await?; + + // FIXME: + // self.socket.borrow_mut().credential_map.insert(key, value); + + Ok(()) + } + /// Make a query for a given name and return the corresponding IP addresses. // #[cfg(feature = "dns")] pub async fn dns_query( @@ -221,7 +278,9 @@ impl UbloxStack + if udp.edm_channel == Some(channel_id) => + // FIXME: + // if udp.edm_channel == Some(channel_id) && udp.may_recv() => { let n = udp.rx_enqueue_slice(&data); if n < data.len() { @@ -258,7 +317,8 @@ impl UbloxStack { udp.peer_handle = None; - udp.set_state(UdpState::TimeWait); + // FIXME: + // udp.set_state(UdpState::TimeWait); break; } #[cfg(feature = "socket-tcp")] @@ -275,27 +335,27 @@ impl UbloxStack { let mut s = socket.borrow_mut(); - if let Some(query) = s.dns_queries.get_mut(&hostname) { + if let Some(query) = s.dns_table.get_mut(&hostname) { match query.state { DnsState::Pending if rtt == -1 => { // According to AT manual, rtt = -1 means the PING has timed out - query.state = DnsState::Err; + query.state = DnsState::Error(PingError::Timeout); query.waker.wake(); } DnsState::Pending => { - query.state = DnsState::Ok(ip); + query.state = DnsState::Resolved(ip); query.waker.wake(); } _ => {} } } } - EdmEvent::ATEvent(Urc::PingErrorResponse(PingErrorResponse { error: _ })) => { + EdmEvent::ATEvent(Urc::PingErrorResponse(PingErrorResponse { error })) => { let mut s = socket.borrow_mut(); - for (_, query) in s.dns_queries.iter_mut() { + for query in s.dns_table.table.iter_mut() { match query.state { DnsState::Pending => { - query.state = DnsState::Err; + query.state = DnsState::Error(error); query.waker.wake(); } _ => {} @@ -308,11 +368,11 @@ impl UbloxStack Option { let mut s = self.socket.borrow_mut(); - for (hostname, query) in s.dns_queries.iter_mut() { + for query in s.dns_table.table.iter_mut() { if let DnsState::New = query.state { query.state = DnsState::Pending; return Some(TxEvent::Dns { - hostname: hostname.clone(), + hostname: query.domain_name.clone(), }); } } @@ -334,7 +394,14 @@ impl UbloxStack todo!(), @@ -345,11 +412,20 @@ impl UbloxStack { if let Some(addr) = tcp.remote_endpoint() { - let url = PeerUrlBuilder::new() - .address(&addr) - .set_local_port(tcp.local_port) - .tcp::<128>() - .unwrap(); + let mut builder = PeerUrlBuilder::new(); + + if let Some(hostname) = dns_table.reverse_lookup(addr.ip()) { + builder.hostname(hostname).port(addr.port()) + } else { + builder.address(&addr) + }; + + if let Some(creds) = credential_map.remove(&handle) { + builder.creds(creds); + } + + let url = + builder.set_local_port(tcp.local_port).tcp::<128>().unwrap(); return Some(TxEvent::Connect { socket_handle: handle, @@ -361,7 +437,6 @@ impl UbloxStack { if let Some(edm_channel) = tcp.edm_channel { - warn!("{}", tcp); return tcp.tx_dequeue(|payload| { let len = core::cmp::min(payload.len(), DATA_PACKAGE_SIZE); let res = if len != 0 { @@ -434,10 +509,10 @@ impl UbloxStack {} Err(_) => { let mut s = socket.borrow_mut(); - if let Some(query) = s.dns_queries.get_mut(&hostname) { + if let Some(query) = s.dns_table.get_mut(&hostname) { match query.state { DnsState::Pending => { - query.state = DnsState::Err; + query.state = DnsState::Error(PingError::Other); query.waker.wake(); } _ => {} @@ -469,9 +544,9 @@ impl UbloxStack match ublox_sockets::udp::Socket::downcast_mut(socket) { - Some(udp) if udp.remote_endpoint == Some(endpoint) => { + Some(udp) if udp.endpoint == Some(endpoint) => { udp.edm_channel = Some(channel_id); - udp.set_state(ublox_sockets::UdpState::Established); + udp.set_state(UdpState::Established); break; } _ => {} @@ -497,7 +572,7 @@ enum TxEvent { peer_handle: PeerHandle, }, Dns { - hostname: heapless::String, + hostname: heapless::String, }, } diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index d76013c..756fa70 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -4,17 +4,23 @@ use core::mem; use core::task::Poll; use atat::asynch::AtatClient; +use embassy_time::Duration; use embedded_nal_async::SocketAddr; use ublox_sockets::{tcp, SocketHandle, TcpState}; use super::{SocketStack, UbloxStack}; +/// Error returned by TcpSocket read/write functions. #[derive(PartialEq, Eq, Clone, Copy, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { + /// The connection was reset. + /// + /// This can happen on receiving a RST packet, or on timeout. ConnectionReset, } +/// Error returned by [`TcpSocket::connect`]. #[derive(PartialEq, Eq, Clone, Copy, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ConnectError { @@ -28,6 +34,7 @@ pub enum ConnectError { NoRoute, } +/// Error returned by [`TcpSocket::accept`]. #[derive(PartialEq, Eq, Clone, Copy, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum AcceptError { @@ -39,35 +46,83 @@ pub enum AcceptError { ConnectionReset, } +/// A TCP socket. pub struct TcpSocket<'a> { - io: TcpIo<'a>, + pub(crate) io: TcpIo<'a>, } +/// The reader half of a TCP socket. pub struct TcpReader<'a> { - io: TcpIo<'a>, + pub(crate) io: TcpIo<'a>, } +/// The writer half of a TCP socket. pub struct TcpWriter<'a> { - io: TcpIo<'a>, + pub(crate) io: TcpIo<'a>, } impl<'a> TcpReader<'a> { + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. pub async fn read(&mut self, buf: &mut [u8]) -> Result { self.io.read(buf).await } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.read_with(f).await + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn recv_capacity(&self) -> usize { + self.io.recv_capacity() + } } impl<'a> TcpWriter<'a> { + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. pub async fn write(&mut self, buf: &[u8]) -> Result { self.io.write(buf).await } + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TcpSocket::abort) it will wait for the TCP RST packet to be sent. pub async fn flush(&mut self) -> Result<(), Error> { self.io.flush().await } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.write_with(f).await + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.io.send_capacity() + } } impl<'a> TcpSocket<'a> { + /// Create a new TCP socket on the given stack, with the given buffers. pub fn new( stack: &'a UbloxStack, rx_buffer: &'a mut [u8], @@ -89,10 +144,44 @@ impl<'a> TcpSocket<'a> { } } + /// Return the maximum number of bytes inside the recv buffer. + pub fn recv_capacity(&self) -> usize { + self.io.recv_capacity() + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.io.send_capacity() + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.write_with(f).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.io.read_with(f).await + } + + /// Split the socket into reader and a writer halves. pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { (TcpReader { io: self.io }, TcpWriter { io: self.io }) } + /// Connect to a remote host. pub async fn connect(&mut self, remote_endpoint: T) -> Result<(), ConnectError> where T: Into, @@ -117,76 +206,143 @@ impl<'a> TcpSocket<'a> { .await } - // FIXME: + // /// Accept a connection from a remote host. + // /// + // /// This function puts the socket in listening mode, and waits until a connection is received. // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> // where // T: Into, // { - // match self.io.with_mut(|s, _| s.listen(local_endpoint)) { - // Ok(()) => {} - // Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), - // Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), - // } - - // poll_fn(|cx| { - // self.io.with_mut(|s, _| match s.state() { - // tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { - // s.register_send_waker(cx.waker()); - // Poll::Pending - // } - // _ => Poll::Ready(Ok(())), - // }) - // }) - // .await + // todo!() + // // match self.io.with_mut(|s, _| s.listen(local_endpoint)) { + // // Ok(()) => {} + // // Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), + // // Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), + // // } + + // // poll_fn(|cx| { + // // self.io.with_mut(|s, _| match s.state() { + // // tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + // // s.register_send_waker(cx.waker()); + // // Poll::Pending + // // } + // // _ => Poll::Ready(Ok(())), + // // }) + // // }) + // // .await // } + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. pub async fn read(&mut self, buf: &mut [u8]) -> Result { self.io.read(buf).await } + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. pub async fn write(&mut self, buf: &[u8]) -> Result { self.io.write(buf).await } + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TcpSocket::abort) it will wait for the TCP RST packet to be sent. pub async fn flush(&mut self) -> Result<(), Error> { self.io.flush().await } - // pub fn set_timeout(&mut self, duration: Option) { - // self.io.with_mut(|s| s.set_timeout(duration)) - // } + /// Set the timeout for the socket. + /// + /// If the timeout is set, the socket will be closed if no data is received for the + /// specified duration. + pub fn set_timeout(&mut self, duration: Option) { + todo!() + // self.io.with_mut(|s| s.set_timeout(duration)) + } - // pub fn set_keep_alive(&mut self, interval: Option) { - // self.io.with_mut(|s| s.set_keep_alive(interval)) - // } + /// Set the keep-alive interval for the socket. + /// + /// If the keep-alive interval is set, the socket will send keep-alive packets after + /// the specified duration of inactivity. + /// + /// If not set, the socket will not send keep-alive packets. + pub fn set_keep_alive(&mut self, interval: Option) { + todo!() + // self.io + // .with_mut(|s| s.set_keep_alive(interval.map(duration_to_smoltcp))) + } - // pub fn local_endpoint(&self) -> Option { - // self.io.with(|s, _| s.local_endpoint()) + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.io.with_mut(|s| s.set_hop_limit(hop_limit)) // } + /// Get the local endpoint of the socket. + /// + /// Returns `None` if the socket is not bound (listening) or not connected. + pub fn local_endpoint(&self) -> Option { + todo!() + // self.io.with(|s| s.local_endpoint()) + } + + /// Get the remote endpoint of the socket. + /// + /// Returns `None` if the socket is not connected. pub fn remote_endpoint(&self) -> Option { self.io.with(|s| s.remote_endpoint()) } - pub fn state(&self) -> tcp::State { + /// Get the state of the socket. + pub fn state(&self) -> TcpState { self.io.with(|s| s.state()) } + /// Close the write half of the socket. + /// + /// This closes only the write half of the socket. The read half side remains open, the + /// socket can still receive data. + /// + /// Data that has been written to the socket and not yet sent (or not yet ACKed) will still + /// still sent. The last segment of the pending to send data is sent with the FIN flag set. pub fn close(&mut self) { self.io.with_mut(|s| s.close()) } + /// Forcibly close the socket. + /// + /// This instantly closes both the read and write halves of the socket. Any pending data + /// that has not been sent will be lost. + /// + /// Note that the TCP RST packet is not sent immediately - if the `TcpSocket` is dropped too soon + /// the remote host may not know the connection has been closed. + /// `abort()` callers should wait for a [`flush()`](TcpSocket::flush) call to complete before + /// dropping or reusing the socket. pub fn abort(&mut self) { self.io.with_mut(|s| s.abort()) } + /// Get whether the socket is ready to send data, i.e. whether there is space in the send buffer. pub fn may_send(&self) -> bool { self.io.with(|s| s.may_send()) } + /// return whether the recieve half of the full-duplex connection is open. + /// This function returns true if it’s possible to receive data from the remote endpoint. + /// It will return true while there is data in the receive buffer, and if there isn’t, + /// as long as the remote endpoint has not closed the connection. pub fn may_recv(&self) -> bool { self.io.with(|s| s.may_recv()) } + + /// Get whether the socket is ready to receive data, i.e. whether there is some pending data in the receive buffer. + pub fn can_recv(&self) -> bool { + self.io.with(|s| s.can_recv()) + } } impl<'a> Drop for TcpSocket<'a> { @@ -210,9 +366,9 @@ impl<'a> Drop for TcpSocket<'a> { // ======================= #[derive(Copy, Clone)] -struct TcpIo<'a> { - stack: &'a RefCell, - handle: SocketHandle, +pub(crate) struct TcpIo<'a> { + pub(crate) stack: &'a RefCell, + pub(crate) handle: SocketHandle, } impl<'d> TcpIo<'d> { @@ -235,6 +391,13 @@ impl<'d> TcpIo<'d> { // CAUTION: smoltcp semantics around EOF are different to what you'd expect // from posix-like IO, so we have to tweak things here. self.with_mut(|s| match s.recv_slice(buf) { + // No data ready + Ok(0) if buf.is_empty() => { + // embedded_io_async::Read's contract is to not block if buf is empty. While + // this function is not a direct implementor of the trait method, we still don't + // want our future to never resolve. + Poll::Ready(Ok(0)) + } // No data ready Ok(0) => { s.register_recv_waker(cx.waker()); @@ -272,6 +435,67 @@ impl<'d> TcpIo<'d> { .await } + async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + let mut f = Some(f); + + poll_fn(move |cx| { + self.with_mut(|s| { + if !s.can_send() { + if s.may_send() { + // socket buffer is full wait until it has atleast one byte free + s.register_send_waker(cx.waker()); + Poll::Pending + } else { + // if we can't transmit because the transmit half of the duplex connection is closed then return an error + Poll::Ready(Err(Error::ConnectionReset)) + } + } else { + Poll::Ready(match s.send(f.take().unwrap()) { + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::SendError::InvalidState) => Err(Error::ConnectionReset), + Err(_) => Err(Error::ConnectionReset), + Ok(r) => Ok(r), + }) + } + }) + }) + .await + } + + async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + let mut f = Some(f); + poll_fn(move |cx| { + self.with_mut(|s| { + if !s.can_recv() { + if s.may_recv() { + // socket buffer is empty wait until it has atleast one byte has arrived + s.register_recv_waker(cx.waker()); + Poll::Pending + } else { + // if we can't receive because the recieve half of the duplex connection is closed then return an error + Poll::Ready(Err(Error::ConnectionReset)) + } + } else { + Poll::Ready(match s.recv(f.take().unwrap()) { + // Connection reset. TODO: this can also be timeouts etc, investigate. + // Err(tcp::RecvError::Finished) | Err(tcp::RecvError::InvalidState) => { + // Err(Error::ConnectionReset) + // } + Err(_) => Err(Error::ConnectionReset), + Ok(r) => Ok(r), + }) + } + }) + }) + .await + } + async fn flush(&mut self) -> Result<(), Error> { poll_fn(move |cx| { self.with_mut(|s| { @@ -288,25 +512,39 @@ impl<'d> TcpIo<'d> { }) .await } + + fn recv_capacity(&self) -> usize { + self.with(|s| s.recv_capacity()) + } + + fn send_capacity(&self) -> usize { + self.with(|s| s.send_capacity()) + } } -// #[cfg(feature = "nightly")] mod embedded_io_impls { use super::*; - impl embedded_io::Error for ConnectError { - fn kind(&self) -> embedded_io::ErrorKind { - embedded_io::ErrorKind::Other + impl embedded_io_async::Error for ConnectError { + fn kind(&self) -> embedded_io_async::ErrorKind { + match self { + ConnectError::ConnectionReset => embedded_io_async::ErrorKind::ConnectionReset, + ConnectError::TimedOut => embedded_io_async::ErrorKind::TimedOut, + ConnectError::NoRoute => embedded_io_async::ErrorKind::NotConnected, + ConnectError::InvalidState => embedded_io_async::ErrorKind::Other, + } } } - impl embedded_io::Error for Error { - fn kind(&self) -> embedded_io::ErrorKind { - embedded_io::ErrorKind::Other + impl embedded_io_async::Error for Error { + fn kind(&self) -> embedded_io_async::ErrorKind { + match self { + Error::ConnectionReset => embedded_io_async::ErrorKind::ConnectionReset, + } } } - impl<'d> embedded_io::ErrorType for TcpSocket<'d> { + impl<'d> embedded_io_async::ErrorType for TcpSocket<'d> { type Error = Error; } @@ -316,6 +554,12 @@ mod embedded_io_impls { } } + impl<'d> embedded_io_async::ReadReady for TcpSocket<'d> { + fn read_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_recv())) + } + } + impl<'d> embedded_io_async::Write for TcpSocket<'d> { async fn write(&mut self, buf: &[u8]) -> Result { self.io.write(buf).await @@ -326,7 +570,13 @@ mod embedded_io_impls { } } - impl<'d> embedded_io::ErrorType for TcpReader<'d> { + impl<'d> embedded_io_async::WriteReady for TcpSocket<'d> { + fn write_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_send())) + } + } + + impl<'d> embedded_io_async::ErrorType for TcpReader<'d> { type Error = Error; } @@ -336,7 +586,13 @@ mod embedded_io_impls { } } - impl<'d> embedded_io::ErrorType for TcpWriter<'d> { + impl<'d> embedded_io_async::ReadReady for TcpReader<'d> { + fn read_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_recv())) + } + } + + impl<'d> embedded_io_async::ErrorType for TcpWriter<'d> { type Error = Error; } @@ -349,19 +605,29 @@ mod embedded_io_impls { self.io.flush().await } } + + impl<'d> embedded_io_async::WriteReady for TcpWriter<'d> { + fn write_ready(&mut self) -> Result { + Ok(self.io.with(|s| s.may_send())) + } + } } -// #[cfg(all(feature = "unstable-traits", feature = "nightly"))] +/// TCP client compatible with `embedded-nal-async` traits. pub mod client { - use core::cell::UnsafeCell; + use core::cell::{Cell, UnsafeCell}; use core::mem::MaybeUninit; use core::ptr::NonNull; use portable_atomic::{AtomicBool, Ordering}; + use crate::asynch::ublox_stack::dns::DnsSocket; + use super::*; - /// TCP client capable of creating up to N multiple connections with tx and rx buffers according to TX_SZ and RX_SZ. + /// TCP client connection pool compatible with `embedded-nal-async` traits. + /// + /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. pub struct TcpClient< 'd, AT: AtatClient + 'static, @@ -374,6 +640,34 @@ pub mod client { pub(crate) state: &'d TcpClientState, } + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::Dns for TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + { + type Error = crate::asynch::ublox_stack::dns::Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(&self.stack).query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: no_std_net::IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } + } + impl< 'd, AT: AtatClient, @@ -383,7 +677,7 @@ pub mod client { const RX_SZ: usize, > TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> { - /// Create a new TcpClient + /// Create a new `TcpClient`. pub fn new( stack: &'d UbloxStack, state: &'d TcpClientState, @@ -419,6 +713,7 @@ pub mod client { } } + /// Opened TCP connection in a [`TcpClient`]. pub struct TcpConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { socket: TcpSocket<'d>, state: &'d TcpClientState, @@ -482,30 +777,26 @@ pub mod client { /// State for TcpClient pub struct TcpClientState { - pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, + pub(crate) pool: Pool<([u8; TX_SZ], [u8; RX_SZ]), N>, } impl TcpClientState { + /// Create a new `TcpClientState`. pub const fn new() -> Self { Self { pool: Pool::new() } } } - unsafe impl Sync - for TcpClientState - { - } - - struct Pool { - used: [AtomicBool; N], + pub(crate) struct Pool { + used: [Cell; N], data: [UnsafeCell>; N], } impl Pool { - const VALUE: AtomicBool = AtomicBool::new(false); + const VALUE: Cell = Cell::new(false); const UNINIT: UnsafeCell> = UnsafeCell::new(MaybeUninit::uninit()); - const fn new() -> Self { + pub(crate) const fn new() -> Self { Self { used: [Self::VALUE; N], data: [Self::UNINIT; N], @@ -514,9 +805,11 @@ pub mod client { } impl Pool { - fn alloc(&self) -> Option> { + pub(crate) fn alloc(&self) -> Option> { for n in 0..N { - if self.used[n].swap(true, Ordering::SeqCst) == false { + // this can't race because Pool is not Sync. + if !self.used[n].get() { + self.used[n].set(true); let p = self.data[n].get() as *mut T; return Some(unsafe { NonNull::new_unchecked(p) }); } @@ -525,12 +818,12 @@ pub mod client { } /// safety: p must be a pointer obtained from self.alloc that hasn't been freed yet. - unsafe fn free(&self, p: NonNull) { + pub(crate) unsafe fn free(&self, p: NonNull) { let origin = self.data.as_ptr() as *mut T; let n = p.as_ptr().offset_from(origin); assert!(n >= 0); assert!((n as usize) < N); - self.used[n as usize].store(false, Ordering::SeqCst); + self.used[n as usize].set(false); } } } diff --git a/src/asynch/ublox_stack/tls.rs b/src/asynch/ublox_stack/tls.rs new file mode 100644 index 0000000..5cd109c --- /dev/null +++ b/src/asynch/ublox_stack/tls.rs @@ -0,0 +1,426 @@ +use atat::asynch::AtatClient; +use embassy_time::Duration; +use no_std_net::SocketAddr; +use ublox_sockets::TcpState as State; + +use crate::peer_builder::SecurityCredentials; + +use super::{ + tcp::{ConnectError, Error, TcpIo, TcpReader, TcpSocket, TcpWriter}, + UbloxStack, +}; + +pub struct TlsSocket<'a> { + inner: TcpSocket<'a>, +} + +impl<'a> TlsSocket<'a> { + /// Create a new TCP socket on the given stack, with the given buffers. + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + credentials: SecurityCredentials, + ) -> Self { + let tcp_socket = TcpSocket::new(stack, rx_buffer, tx_buffer); + + let TcpIo { stack, handle } = tcp_socket.io; + + let s = &mut *stack.borrow_mut(); + s.credential_map.insert(handle, credentials); + + Self { inner: tcp_socket } + } + + /// Return the maximum number of bytes inside the recv buffer. + pub fn recv_capacity(&self) -> usize { + self.inner.recv_capacity() + } + + /// Return the maximum number of bytes inside the transmit buffer. + pub fn send_capacity(&self) -> usize { + self.inner.send_capacity() + } + + /// Call `f` with the largest contiguous slice of octets in the transmit buffer, + /// and enqueue the amount of elements returned by `f`. + /// + /// If the socket is not ready to accept data, it waits until it is. + pub async fn write_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.inner.write_with(f).await + } + + /// Call `f` with the largest contiguous slice of octets in the receive buffer, + /// and dequeue the amount of elements returned by `f`. + /// + /// If no data is available, it waits until there is at least one byte available. + pub async fn read_with(&mut self, f: F) -> Result + where + F: FnOnce(&mut [u8]) -> (usize, R), + { + self.inner.read_with(f).await + } + + /// Split the socket into reader and a writer halves. + pub fn split(&mut self) -> (TcpReader<'_>, TcpWriter<'_>) { + ( + TcpReader { io: self.inner.io }, + TcpWriter { io: self.inner.io }, + ) + } + + /// Connect to a remote host. + pub async fn connect(&mut self, remote_endpoint: T) -> Result<(), ConnectError> + where + T: Into, + { + self.inner.connect(remote_endpoint).await + } + + // /// Accept a connection from a remote host. + // /// + // /// This function puts the socket in listening mode, and waits until a connection is received. + // pub async fn accept(&mut self, local_endpoint: T) -> Result<(), AcceptError> + // where + // T: Into, + // { + // todo!() + // // match self.io.with_mut(|s, _| s.listen(local_endpoint)) { + // // Ok(()) => {} + // // Err(tcp::ListenError::InvalidState) => return Err(AcceptError::InvalidState), + // // Err(tcp::ListenError::Unaddressable) => return Err(AcceptError::InvalidPort), + // // } + + // // poll_fn(|cx| { + // // self.io.with_mut(|s, _| match s.state() { + // // tcp::State::Listen | tcp::State::SynSent | tcp::State::SynReceived => { + // // s.register_send_waker(cx.waker()); + // // Poll::Pending + // // } + // // _ => Poll::Ready(Ok(())), + // // }) + // // }) + // // .await + // } + + /// Read data from the socket. + /// + /// Returns how many bytes were read, or an error. If no data is available, it waits + /// until there is at least one byte available. + pub async fn read(&mut self, buf: &mut [u8]) -> Result { + self.inner.read(buf).await + } + + /// Write data to the socket. + /// + /// Returns how many bytes were written, or an error. If the socket is not ready to + /// accept data, it waits until it is. + pub async fn write(&mut self, buf: &[u8]) -> Result { + self.inner.write(buf).await + } + + /// Flushes the written data to the socket. + /// + /// This waits until all data has been sent, and ACKed by the remote host. For a connection + /// closed with [`abort()`](TlsSocket::abort) it will wait for the TCP RST packet to be sent. + pub async fn flush(&mut self) -> Result<(), Error> { + self.inner.flush().await + } + + /// Set the timeout for the socket. + /// + /// If the timeout is set, the socket will be closed if no data is received for the + /// specified duration. + pub fn set_timeout(&mut self, duration: Option) { + todo!() + // self.inner.set_timeout(duration) + } + + /// Set the keep-alive interval for the socket. + /// + /// If the keep-alive interval is set, the socket will send keep-alive packets after + /// the specified duration of inactivity. + /// + /// If not set, the socket will not send keep-alive packets. + pub fn set_keep_alive(&mut self, interval: Option) { + self.inner.set_keep_alive(interval) + } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.inner.set_hop_limit() + // } + + /// Get the local endpoint of the socket. + /// + /// Returns `None` if the socket is not bound (listening) or not connected. + pub fn local_endpoint(&self) -> Option { + todo!() + // self.inner.local_endpoint() + } + + /// Get the remote endpoint of the socket. + /// + /// Returns `None` if the socket is not connected. + pub fn remote_endpoint(&self) -> Option { + self.inner.remote_endpoint() + } + + /// Get the state of the socket. + pub fn state(&self) -> State { + self.inner.state() + } + + /// Close the write half of the socket. + /// + /// This closes only the write half of the socket. The read half side remains open, the + /// socket can still receive data. + /// + /// Data that has been written to the socket and not yet sent (or not yet ACKed) will still + /// still sent. The last segment of the pending to send data is sent with the FIN flag set. + pub fn close(&mut self) { + self.inner.close() + } + + /// Forcibly close the socket. + /// + /// This instantly closes both the read and write halves of the socket. Any pending data + /// that has not been sent will be lost. + /// + /// Note that the TCP RST packet is not sent immediately - if the `TlsSocket` is dropped too soon + /// the remote host may not know the connection has been closed. + /// `abort()` callers should wait for a [`flush()`](TlsSocket::flush) call to complete before + /// dropping or reusing the socket. + pub fn abort(&mut self) { + self.inner.abort() + } + + /// Get whether the socket is ready to send data, i.e. whether there is space in the send buffer. + pub fn may_send(&self) -> bool { + self.inner.may_send() + } + + /// return whether the recieve half of the full-duplex connection is open. + /// This function returns true if it’s possible to receive data from the remote endpoint. + /// It will return true while there is data in the receive buffer, and if there isn’t, + /// as long as the remote endpoint has not closed the connection. + pub fn may_recv(&self) -> bool { + self.inner.may_recv() + } + + /// Get whether the socket is ready to receive data, i.e. whether there is some pending data in the receive buffer. + pub fn can_recv(&self) -> bool { + self.inner.can_recv() + } +} + +mod embedded_io_impls { + use super::*; + + impl<'d> embedded_io_async::ErrorType for TlsSocket<'d> { + type Error = Error; + } + + impl<'d> embedded_io_async::Read for TlsSocket<'d> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.inner.read(buf).await + } + } + + impl<'d> embedded_io_async::ReadReady for TlsSocket<'d> { + fn read_ready(&mut self) -> Result { + self.inner.read_ready() + } + } + + impl<'d> embedded_io_async::Write for TlsSocket<'d> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.inner.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.inner.flush().await + } + } + + impl<'d> embedded_io_async::WriteReady for TlsSocket<'d> { + fn write_ready(&mut self) -> Result { + self.inner.write_ready() + } + } +} + +/// TLS client compatible with `embedded-nal-async` traits. +pub mod client { + use core::ptr::NonNull; + + use crate::asynch::ublox_stack::dns::DnsSocket; + use crate::asynch::ublox_stack::tcp::client::TcpClientState; + + use super::*; + + /// TLS client connection pool compatible with `embedded-nal-async` traits. + /// + /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. + pub struct TlsClient< + 'd, + AT: AtatClient + 'static, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize = 1024, + const RX_SZ: usize = 1024, + > { + pub(crate) stack: &'d UbloxStack, + pub(crate) state: &'d TcpClientState, + pub(crate) credentials: SecurityCredentials, + } + + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::Dns for TlsClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + { + type Error = crate::asynch::ublox_stack::dns::Error; + + async fn get_host_by_name( + &self, + host: &str, + addr_type: embedded_nal_async::AddrType, + ) -> Result { + DnsSocket::new(&self.stack).query(host, addr_type).await + } + + async fn get_host_by_address( + &self, + _addr: no_std_net::IpAddr, + _result: &mut [u8], + ) -> Result { + unimplemented!() + } + } + + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > TlsClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + { + /// Create a new `TlsClient`. + pub fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + credentials: SecurityCredentials, + ) -> Self { + Self { + stack, + state, + credentials, + } + } + } + + impl< + 'd, + AT: AtatClient, + const N: usize, + const URC_CAPACITY: usize, + const TX_SZ: usize, + const RX_SZ: usize, + > embedded_nal_async::TcpConnect for TlsClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + { + type Error = Error; + type Connection<'m> = TlsConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; + + async fn connect<'a>( + &'a self, + remote: SocketAddr, + ) -> Result, Self::Error> { + let remote_endpoint = (remote.ip(), remote.port()); + let mut socket = TlsConnection::new(&self.stack, self.state, self.credentials.clone())?; + socket + .socket + .connect(remote_endpoint) + .await + .map_err(|_| Error::ConnectionReset)?; + Ok(socket) + } + } + + /// Opened TLS connection in a [`TlsClient`]. + pub struct TlsConnection<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> { + socket: TlsSocket<'d>, + state: &'d TcpClientState, + bufs: NonNull<([u8; TX_SZ], [u8; RX_SZ])>, + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> + TlsConnection<'d, N, TX_SZ, RX_SZ> + { + fn new( + stack: &'d UbloxStack, + state: &'d TcpClientState, + credentials: SecurityCredentials, + ) -> Result { + let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; + Ok(Self { + socket: unsafe { + TlsSocket::new( + stack, + &mut bufs.as_mut().1, + &mut bufs.as_mut().0, + credentials, + ) + }, + state, + bufs, + }) + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> Drop + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + fn drop(&mut self) { + unsafe { + self.socket.close(); + self.state.pool.free(self.bufs); + } + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::ErrorType + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + type Error = Error; + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Read + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + async fn read(&mut self, buf: &mut [u8]) -> Result { + self.socket.read(buf).await + } + } + + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::Write + for TlsConnection<'d, N, TX_SZ, RX_SZ> + { + async fn write(&mut self, buf: &[u8]) -> Result { + self.socket.write(buf).await + } + + async fn flush(&mut self) -> Result<(), Self::Error> { + self.socket.flush().await + } + } +} diff --git a/src/asynch/ublox_stack/udp.rs b/src/asynch/ublox_stack/udp.rs new file mode 100644 index 0000000..8ff4e93 --- /dev/null +++ b/src/asynch/ublox_stack/udp.rs @@ -0,0 +1,249 @@ +//! UDP sockets. +use core::cell::RefCell; +use core::future::poll_fn; +use core::mem; +use core::task::Poll; + +use atat::asynch::AtatClient; +use embedded_nal_async::SocketAddr; +use ublox_sockets::{udp, SocketHandle, UdpState}; + +use super::{SocketStack, UbloxStack}; + +/// Error returned by [`UdpSocket::bind`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum BindError { + /// The socket was already open. + InvalidState, + /// No route to host. + NoRoute, +} + +/// Error returned by [`UdpSocket::recv_from`] and [`UdpSocket::send_to`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SendError { + /// No route to host. + NoRoute, + /// Socket not bound to an outgoing port. + SocketNotBound, +} + +/// Error returned by [`UdpSocket::recv_from`] and [`UdpSocket::send_to`]. +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum RecvError { + /// Provided buffer was smaller than the received packet. + Truncated, +} + +/// An UDP socket. +pub struct UdpSocket<'a> { + stack: &'a RefCell, + handle: SocketHandle, +} + +impl<'a> UdpSocket<'a> { + /// Create a new UDP socket using the provided stack and buffers. + pub fn new( + stack: &'a UbloxStack, + rx_buffer: &'a mut [u8], + tx_buffer: &'a mut [u8], + ) -> Self { + let s = &mut *stack.socket.borrow_mut(); + let rx_buffer: &'static mut [u8] = unsafe { mem::transmute(rx_buffer) }; + let tx_buffer: &'static mut [u8] = unsafe { mem::transmute(tx_buffer) }; + let handle = s.sockets.add(udp::Socket::new( + udp::SocketBuffer::new(rx_buffer), + udp::SocketBuffer::new(tx_buffer), + )); + + Self { + stack: &stack.socket, + handle, + } + } + + // /// Bind the socket to a local endpoint. + // pub fn bind(&mut self, endpoint: T) -> Result<(), BindError> + // where + // T: Into, + // { + // let mut endpoint = endpoint.into(); + + // if endpoint.port == 0 { + // // If user didn't specify port allocate a dynamic port. + // endpoint.port = self.stack.borrow_mut().get_local_port(); + // } + + // match self.with_mut(|s| s.bind(endpoint)) { + // Ok(()) => Ok(()), + // // Err(udp::BindError::InvalidState) => Err(BindError::InvalidState), + // // Err(udp::BindError::Unaddressable) => Err(BindError::NoRoute), + // Err(_) => Err(BindError::InvalidState), + // } + // } + + fn with(&self, f: impl FnOnce(&udp::Socket) -> R) -> R { + let s = &*self.stack.borrow(); + let socket = s.sockets.get::(self.handle); + f(socket) + } + + fn with_mut(&self, f: impl FnOnce(&mut udp::Socket) -> R) -> R { + let s = &mut *self.stack.borrow_mut(); + let socket = s.sockets.get_mut::(self.handle); + let res = f(socket); + s.waker.wake(); + res + } + + // /// Receive a datagram. + // /// + // /// This method will wait until a datagram is received. + // /// + // /// Returns the number of bytes received and the remote endpoint. + // pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, IpEndpoint), RecvError> { + // poll_fn(move |cx| self.poll_recv_from(buf, cx)).await + // } + + // /// Receive a datagram. + // /// + // /// When no datagram is available, this method will return `Poll::Pending` and + // /// register the current task to be notified when a datagram is received. + // /// + // /// When a datagram is received, this method will return `Poll::Ready` with the + // /// number of bytes received and the remote endpoint. + // pub fn poll_recv_from( + // &self, + // buf: &mut [u8], + // cx: &mut Context<'_>, + // ) -> Poll> { + // self.with_mut(|s| match s.recv_slice(buf) { + // Ok((n, meta)) => Poll::Ready(Ok((n, meta.endpoint))), + // // No data ready + // // Err(udp::RecvError::Truncated) => Poll::Ready(Err(RecvError::Truncated)), + // // Err(udp::RecvError::Exhausted) => { + // Err(_) => { + // s.register_recv_waker(cx.waker()); + // Poll::Pending + // } + // }) + // } + + // /// Send a datagram to the specified remote endpoint. + // /// + // /// This method will wait until the datagram has been sent. + // /// + // /// When the remote endpoint is not reachable, this method will return `Err(SendError::NoRoute)` + // pub async fn send_to(&self, buf: &[u8], remote_endpoint: T) -> Result<(), SendError> + // where + // T: Into, + // { + // let remote_endpoint: IpEndpoint = remote_endpoint.into(); + // poll_fn(move |cx| self.poll_send_to(buf, remote_endpoint, cx)).await + // } + + // /// Send a datagram to the specified remote endpoint. + // /// + // /// When the datagram has been sent, this method will return `Poll::Ready(Ok())`. + // /// + // /// When the socket's send buffer is full, this method will return `Poll::Pending` + // /// and register the current task to be notified when the buffer has space available. + // /// + // /// When the remote endpoint is not reachable, this method will return `Poll::Ready(Err(Error::NoRoute))`. + // pub fn poll_send_to( + // &self, + // buf: &[u8], + // remote_endpoint: T, + // cx: &mut Context<'_>, + // ) -> Poll> + // where + // T: Into, + // { + // self.with_mut(|s| match s.send_slice(buf, remote_endpoint) { + // // Entire datagram has been sent + // Ok(()) => Poll::Ready(Ok(())), + // Err(udp::SendError::BufferFull) => { + // s.register_send_waker(cx.waker()); + // Poll::Pending + // } + // Err(udp::SendError::Unaddressable) => { + // // If no sender/outgoing port is specified, there is not really "no route" + // if s.endpoint().port == 0 { + // Poll::Ready(Err(SendError::SocketNotBound)) + // } else { + // Poll::Ready(Err(SendError::NoRoute)) + // } + // } + // }) + // } + + /// Returns the local endpoint of the socket. + pub fn endpoint(&self) -> Option { + self.with(|s| s.endpoint()) + } + + /// Returns whether the socket is open. + pub fn is_open(&self) -> bool { + self.with(|s| s.is_open()) + } + + /// Close the socket. + pub fn close(&mut self) { + self.with_mut(|s| s.close()) + } + + // /// Returns whether the socket is ready to send data, i.e. it has enough buffer space to hold a packet. + // pub fn may_send(&self) -> bool { + // self.with(|s| s.can_send()) + // } + + /// Returns whether the socket is ready to receive data, i.e. it has received a packet that's now in the buffer. + pub fn may_recv(&self) -> bool { + self.with(|s| s.can_recv()) + } + + // /// Return the maximum number packets the socket can receive. + // pub fn packet_recv_capacity(&self) -> usize { + // self.with(|s| s.packet_recv_capacity()) + // } + + // /// Return the maximum number packets the socket can receive. + // pub fn packet_send_capacity(&self) -> usize { + // self.with(|s| s.packet_send_capacity()) + // } + + // /// Return the maximum number of bytes inside the recv buffer. + // pub fn payload_recv_capacity(&self) -> usize { + // self.with(|s| s.payload_recv_capacity()) + // } + + // /// Return the maximum number of bytes inside the transmit buffer. + // pub fn payload_send_capacity(&self) -> usize { + // self.with(|s| s.payload_send_capacity()) + // } + + // /// Set the hop limit field in the IP header of sent packets. + // pub fn set_hop_limit(&mut self, hop_limit: Option) { + // self.with_mut(|s| s.set_hop_limit(hop_limit)) + // } +} + +impl<'a> Drop for UdpSocket<'a> { + fn drop(&mut self) { + if matches!(self.with(|s| s.state()), UdpState::Established) { + if let Some(peer_handle) = self.with(|s| s.peer_handle) { + self.stack + .borrow_mut() + .dropped_sockets + .push(peer_handle) + .ok(); + } + } + let mut stack = self.stack.borrow_mut(); + stack.sockets.remove(self.handle); + stack.waker.wake(); + } +} diff --git a/src/command/edm/urc.rs b/src/command/edm/urc.rs index dbe3c5a..fe24c1e 100644 --- a/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -22,7 +22,7 @@ pub enum EdmEvent { impl AtatUrc for EdmEvent { /// The type of the response. Usually the enum this trait is implemented on. - type Response = EdmEvent; + type Response = Self; /// Parse the response into a `Self::Response` instance. fn parse(resp: &[u8]) -> Option { diff --git a/src/fmt.rs b/src/fmt.rs index 3476f4c..1542a6c 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -114,7 +114,7 @@ macro_rules! trace { #[cfg(feature = "log")] ::log::trace!($s $(, $x)*); #[cfg(feature = "defmt")] - ::trace!($s $(, $x)*); + ::defmt::trace!($s $(, $x)*); #[cfg(not(any(feature = "log", feature="defmt")))] let _ = ($( & $x ),*); } @@ -127,7 +127,7 @@ macro_rules! debug { #[cfg(feature = "log")] ::log::debug!($s $(, $x)*); #[cfg(feature = "defmt")] - ::debug!($s $(, $x)*); + ::defmt::debug!($s $(, $x)*); #[cfg(not(any(feature = "log", feature="defmt")))] let _ = ($( & $x ),*); } @@ -140,7 +140,7 @@ macro_rules! info { #[cfg(feature = "log")] ::log::info!($s $(, $x)*); #[cfg(feature = "defmt")] - ::info!($s $(, $x)*); + ::defmt::info!($s $(, $x)*); #[cfg(not(any(feature = "log", feature="defmt")))] let _ = ($( & $x ),*); } @@ -153,7 +153,7 @@ macro_rules! warn { #[cfg(feature = "log")] ::log::warn!($s $(, $x)*); #[cfg(feature = "defmt")] - ::warn!($s $(, $x)*); + ::defmt::warn!($s $(, $x)*); #[cfg(not(any(feature = "log", feature="defmt")))] let _ = ($( & $x ),*); } @@ -166,7 +166,7 @@ macro_rules! error { #[cfg(feature = "log")] ::log::error!($s $(, $x)*); #[cfg(feature = "defmt")] - ::error!($s $(, $x)*); + ::defmt::error!($s $(, $x)*); #[cfg(not(any(feature = "log", feature="defmt")))] let _ = ($( & $x ),*); } @@ -228,31 +228,3 @@ impl Try for Result { self } } - -#[allow(unused)] -pub(crate) struct Bytes<'a>(pub &'a [u8]); - -impl<'a> Debug for Bytes<'a> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{:#02x?}", self.0) - } -} - -impl<'a> Display for Bytes<'a> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{:#02x?}", self.0) - } -} - -impl<'a> LowerHex for Bytes<'a> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{:#02x?}", self.0) - } -} - -#[cfg(feature = "defmt")] -impl<'a> defmt::Format for Bytes<'a> { - fn format(&self, fmt: defmt::Formatter) { - defmt::write!(fmt, "{:02x}", self.0) - } -} diff --git a/src/lib.rs b/src/lib.rs index 9f5cb6a..6262ebd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,14 @@ #![cfg_attr(all(not(test), not(feature = "std")), no_std)] -#![cfg_attr(feature = "async", allow(incomplete_features))] -#![cfg_attr(feature = "async", feature(generic_const_exprs))] -#![cfg_attr(feature = "async", feature(async_fn_in_trait))] -#![cfg_attr(feature = "async", feature(impl_trait_projections))] -#![cfg_attr(feature = "async", feature(type_alias_impl_trait))] +#![allow(async_fn_in_trait)] mod fmt; -#[cfg(feature = "async")] pub mod asynch; -#[cfg(feature = "async")] pub use embedded_nal_async; +pub use ublox_sockets; + mod connection; mod network; mod peer_builder; diff --git a/src/peer_builder.rs b/src/peer_builder.rs index 02f18bf..eab3ce3 100644 --- a/src/peer_builder.rs +++ b/src/peer_builder.rs @@ -3,12 +3,19 @@ use core::fmt::Write; use heapless::String; use no_std_net::{IpAddr, SocketAddr}; +#[derive(PartialEq, Clone, Default)] +pub struct SecurityCredentials { + pub ca_cert_name: heapless::String<16>, + pub c_cert_name: heapless::String<16>, + pub c_key_name: heapless::String<16>, +} + #[derive(Default)] pub(crate) struct PeerUrlBuilder<'a> { hostname: Option<&'a str>, ip_addr: Option, port: Option, - // creds: Option, + creds: Option, local_port: Option, } @@ -32,13 +39,16 @@ impl<'a> PeerUrlBuilder<'a> { pub fn udp(&self) -> Result, Error> { let mut s = String::new(); - write!(&mut s, "udp://").ok(); + write!(&mut s, "udp://").map_err(|_| Error::Overflow)?; self.write_domain(&mut s)?; // Start writing query parameters - write!(&mut s, "?").ok(); - self.local_port - .map(|v| write!(&mut s, "local_port={}&", v).ok()); + write!(&mut s, "?").map_err(|_| Error::Overflow)?; + + if let Some(v) = self.local_port { + write!(&mut s, "local_port={}&", v).map_err(|_| Error::Overflow)?; + } + // Remove trailing '&' or '?' if no query. s.pop(); @@ -47,27 +57,22 @@ impl<'a> PeerUrlBuilder<'a> { pub fn tcp(&mut self) -> Result, Error> { let mut s = String::new(); - write!(&mut s, "tcp://").ok(); + write!(&mut s, "tcp://").map_err(|_| Error::Overflow)?; self.write_domain(&mut s)?; // Start writing query parameters - write!(&mut s, "?").ok(); - self.local_port - .map(|v| write!(&mut s, "local_port={}&", v).ok()); - // self.creds.as_ref().map(|creds| { - // creds - // .ca_cert_name - // .as_ref() - // .map(|v| write!(&mut s, "ca={}&", v).ok()); - // creds - // .c_cert_name - // .as_ref() - // .map(|v| write!(&mut s, "cert={}&", v).ok()); - // creds - // .c_key_name - // .as_ref() - // .map(|v| write!(&mut s, "privKey={}&", v).ok()); - // }); + write!(&mut s, "?").map_err(|_| Error::Overflow)?; + + if let Some(v) = self.local_port { + write!(&mut s, "local_port={}&", v).map_err(|_| Error::Overflow)?; + } + + if let Some(creds) = self.creds.as_ref() { + write!(&mut s, "ca={}&", creds.ca_cert_name).map_err(|_| Error::Overflow)?; + write!(&mut s, "cert={}&", creds.c_cert_name).map_err(|_| Error::Overflow)?; + write!(&mut s, "privKey={}&", creds.c_key_name).map_err(|_| Error::Overflow)?; + }; + // Remove trailing '&' or '?' if no query. s.pop(); @@ -110,10 +115,10 @@ impl<'a> PeerUrlBuilder<'a> { self } - // pub fn creds(&mut self, creds: SecurityCredentials) -> &mut Self { - // self.creds.replace(creds); - // self - // } + pub fn creds(&mut self, creds: SecurityCredentials) -> &mut Self { + self.creds.replace(creds); + self + } pub fn local_port(&mut self, local_port: u16) -> &mut Self { self.local_port.replace(local_port); @@ -163,21 +168,22 @@ mod test { assert_eq!(url, "udp://example.org:2000/?local_port=2001"); } - // #[test] - // fn tcp_certs() { - // let url = PeerUrlBuilder::new() - // .hostname("example.org") - // .port(2000) - // .creds(SecurityCredentials { - // c_cert_name: Some(heapless::String::from("client.crt")), - // ca_cert_name: Some(heapless::String::from("ca.crt")), - // c_key_name: Some(heapless::String::from("client.key")), - // }) - // .tcp() - // .unwrap(); - // assert_eq!( - // url, - // "tcp://example.org:2000/?ca=ca.crt&cert=client.crt&privKey=client.key" - // ); - // } + #[test] + fn tcp_certs() { + let url = PeerUrlBuilder::new() + .hostname("example.org") + .port(2000) + .creds(&SecurityCredentials { + c_cert_name: heapless::String::try_from("client.crt").unwrap(), + ca_cert_name: heapless::String::try_from("ca.crt").unwrap(), + c_key_name: heapless::String::try_from("client.key").unwrap(), + }) + .tcp() + .unwrap(); + + assert_eq!( + url, + "tcp://example.org:2000/?ca=ca.crt&cert=client.crt&privKey=client.key" + ); + } } From 76938666531ec77042749d14a6149f777835045f Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 13 Jan 2024 12:18:43 +0100 Subject: [PATCH 11/16] Fix clippy warnings --- src/asynch/runner.rs | 4 ++-- src/asynch/ublox_stack/dns.rs | 12 ++++++------ src/asynch/ublox_stack/mod.rs | 8 +++++++- src/asynch/ublox_stack/tcp.rs | 10 +++++----- src/asynch/ublox_stack/tls.rs | 6 +++--- src/asynch/ublox_stack/udp.rs | 4 ++-- src/command/custom_digest.rs | 4 ++-- src/command/edm/mod.rs | 2 +- src/command/edm/urc.rs | 2 +- src/command/general/types.rs | 2 +- src/command/mod.rs | 10 +++++----- src/fmt.rs | 2 +- src/peer_builder.rs | 4 ++-- 13 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index 2ef6ed2..d366869 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -11,8 +11,8 @@ use crate::{ GetNetworkStatus, }, system::{ - types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, - RebootDCE, SetEcho, SetRS232Settings, StoreCurrentConfig, + types::{EchoOn}, + RebootDCE, SetEcho, StoreCurrentConfig, }, wifi::{ types::DisconnectReason, diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index 72e43d0..a71e18c 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -147,15 +147,17 @@ impl<'a> DnsSocket<'a> { s.waker.wake(); } - let res = poll_fn(|cx| { + + + poll_fn(|cx| { let mut s = self.stack.borrow_mut(); let query = s.dns_table.get_mut(&name_string).unwrap(); match query.state { DnsState::Resolved(ip) => { - return Poll::Ready(Ok(ip)); + Poll::Ready(Ok(ip)) } DnsState::Error(_e) => { - return Poll::Ready(Err(Error::Failed)); + Poll::Ready(Err(Error::Failed)) } _ => { query.waker.register(cx.waker()); @@ -163,9 +165,7 @@ impl<'a> DnsSocket<'a> { } } }) - .await; - - res + .await } } diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index fce89b7..89ff839 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -54,6 +54,12 @@ pub struct StackResources { sockets: [SocketStorage<'static>; SOCK], } +impl Default for StackResources { + fn default() -> Self { + Self::new() + } +} + impl StackResources { pub fn new() -> Self { Self { @@ -404,7 +410,7 @@ impl UbloxStack todo!(), + Socket::Udp(_udp) => todo!(), #[cfg(feature = "socket-tcp")] Socket::Tcp(tcp) => { tcp.poll(); diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index 756fa70..f6ef363 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -260,7 +260,7 @@ impl<'a> TcpSocket<'a> { /// /// If the timeout is set, the socket will be closed if no data is received for the /// specified duration. - pub fn set_timeout(&mut self, duration: Option) { + pub fn set_timeout(&mut self, _duration: Option) { todo!() // self.io.with_mut(|s| s.set_timeout(duration)) } @@ -271,7 +271,7 @@ impl<'a> TcpSocket<'a> { /// the specified duration of inactivity. /// /// If not set, the socket will not send keep-alive packets. - pub fn set_keep_alive(&mut self, interval: Option) { + pub fn set_keep_alive(&mut self, _interval: Option) { todo!() // self.io // .with_mut(|s| s.set_keep_alive(interval.map(duration_to_smoltcp))) @@ -619,7 +619,7 @@ pub mod client { use core::mem::MaybeUninit; use core::ptr::NonNull; - use portable_atomic::{AtomicBool, Ordering}; + use crate::asynch::ublox_stack::dns::DnsSocket; @@ -656,7 +656,7 @@ pub mod client { host: &str, addr_type: embedded_nal_async::AddrType, ) -> Result { - DnsSocket::new(&self.stack).query(host, addr_type).await + DnsSocket::new(self.stack).query(host, addr_type).await } async fn get_host_by_address( @@ -703,7 +703,7 @@ pub mod client { remote: SocketAddr, ) -> Result, Self::Error> { let remote_endpoint = (remote.ip(), remote.port()); - let mut socket = TcpConnection::new(&self.stack, self.state)?; + let mut socket = TcpConnection::new(self.stack, self.state)?; socket .socket .connect(remote_endpoint) diff --git a/src/asynch/ublox_stack/tls.rs b/src/asynch/ublox_stack/tls.rs index 5cd109c..a39b138 100644 --- a/src/asynch/ublox_stack/tls.rs +++ b/src/asynch/ublox_stack/tls.rs @@ -134,7 +134,7 @@ impl<'a> TlsSocket<'a> { /// /// If the timeout is set, the socket will be closed if no data is received for the /// specified duration. - pub fn set_timeout(&mut self, duration: Option) { + pub fn set_timeout(&mut self, _duration: Option) { todo!() // self.inner.set_timeout(duration) } @@ -294,7 +294,7 @@ pub mod client { host: &str, addr_type: embedded_nal_async::AddrType, ) -> Result { - DnsSocket::new(&self.stack).query(host, addr_type).await + DnsSocket::new(self.stack).query(host, addr_type).await } async fn get_host_by_address( @@ -346,7 +346,7 @@ pub mod client { remote: SocketAddr, ) -> Result, Self::Error> { let remote_endpoint = (remote.ip(), remote.port()); - let mut socket = TlsConnection::new(&self.stack, self.state, self.credentials.clone())?; + let mut socket = TlsConnection::new(self.stack, self.state, self.credentials.clone())?; socket .socket .connect(remote_endpoint) diff --git a/src/asynch/ublox_stack/udp.rs b/src/asynch/ublox_stack/udp.rs index 8ff4e93..285a880 100644 --- a/src/asynch/ublox_stack/udp.rs +++ b/src/asynch/ublox_stack/udp.rs @@ -1,8 +1,8 @@ //! UDP sockets. use core::cell::RefCell; -use core::future::poll_fn; + use core::mem; -use core::task::Poll; + use atat::asynch::AtatClient; use embedded_nal_async::SocketAddr; diff --git a/src/command/custom_digest.rs b/src/command/custom_digest.rs index a7b8dd5..888c06f 100644 --- a/src/command/custom_digest.rs +++ b/src/command/custom_digest.rs @@ -18,7 +18,7 @@ impl Digester for EdmDigester { return (DigestResult::None, 0); } - trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(buf)); if buf.len() >= STARTUPMESSAGE.len() && buf[..2] == *b"\r\n" { if let Some(i) = buf[2..].windows(2).position(|x| x == *b"\r\n") { // Two for starting position, one for index -> len and one for the window size. @@ -67,7 +67,7 @@ impl Digester for EdmDigester { // Debug statement for trace properly if !buf.is_empty() { - trace!("Digest {:?}", LossyStr(&buf)); + trace!("Digest {:?}", LossyStr(buf)); } // Filter message by payload diff --git a/src/command/edm/mod.rs b/src/command/edm/mod.rs index ae46aea..c1dd312 100644 --- a/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -9,7 +9,7 @@ use crate::command::{NoResponse, Urc}; // use crate::wifi::EGRESS_CHUNK_SIZE; /// Containing EDM structs with custom serialaization and deserilaisation. use atat::AtatCmd; -use heapless::Vec; + use types::*; use ublox_sockets::ChannelId; diff --git a/src/command/edm/urc.rs b/src/command/edm/urc.rs index fe24c1e..7d382d8 100644 --- a/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -59,7 +59,7 @@ impl AtatUrc for EdmEvent { || !resp.starts_with(&[STARTBYTE]) || !resp.ends_with(&[ENDBYTE]) { - error!("[Parse URC Start/End byte Error] {:?}", LossyStr(&resp)); + error!("[Parse URC Start/End byte Error] {:?}", LossyStr(resp)); return None; }; let payload_len = calc_payload_len(resp); diff --git a/src/command/general/types.rs b/src/command/general/types.rs index ac24d4d..58f3aa2 100644 --- a/src/command/general/types.rs +++ b/src/command/general/types.rs @@ -27,7 +27,7 @@ pub enum IdentificationInfoEnum { MCUID = 10, } -#[derive(Debug, Clone, PartialEq, Eq, Ord)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FirmwareVersion { major: u8, minor: u8, diff --git a/src/command/mod.rs b/src/command/mod.rs index bb12501..083b282 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -89,11 +89,11 @@ impl From for OnOff { } } -impl Into for OnOff { - fn into(self) -> bool { - match self { - Self::On => true, - Self::Off => false, +impl From for bool { + fn from(val: OnOff) -> Self { + match val { + OnOff::On => true, + OnOff::Off => false, } } } diff --git a/src/fmt.rs b/src/fmt.rs index 1542a6c..3f3ed3c 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,7 +1,7 @@ #![macro_use] #![allow(unused_macros)] -use core::fmt::{Debug, Display, LowerHex}; +use core::fmt::{Debug}; #[cfg(all(feature = "defmt", feature = "log"))] compile_error!("You may not enable both `defmt` and `log` features."); diff --git a/src/peer_builder.rs b/src/peer_builder.rs index eab3ce3..7c5e6d3 100644 --- a/src/peer_builder.rs +++ b/src/peer_builder.rs @@ -173,12 +173,12 @@ mod test { let url = PeerUrlBuilder::new() .hostname("example.org") .port(2000) - .creds(&SecurityCredentials { + .creds(SecurityCredentials { c_cert_name: heapless::String::try_from("client.crt").unwrap(), ca_cert_name: heapless::String::try_from("ca.crt").unwrap(), c_key_name: heapless::String::try_from("client.key").unwrap(), }) - .tcp() + .tcp::<128>() .unwrap(); assert_eq!( From 6c31682924367fc793a25d7ffe4bb9e00c9ab58c Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 18 Jan 2024 11:21:46 +0100 Subject: [PATCH 12/16] Fix TlsSocket and module restart with EDM --- Cargo.toml | 13 ++---- src/asynch/control.rs | 50 ++++++++++++++++++++--- src/asynch/runner.rs | 63 ++++++++++++++-------------- src/asynch/ublox_stack/dns.rs | 10 +---- src/asynch/ublox_stack/mod.rs | 77 ++--------------------------------- src/asynch/ublox_stack/tcp.rs | 2 - src/asynch/ublox_stack/tls.rs | 8 ++++ src/asynch/ublox_stack/udp.rs | 1 - src/command/custom_digest.rs | 3 +- src/command/data_mode/mod.rs | 2 +- src/command/edm/mod.rs | 16 ++++---- src/command/security/mod.rs | 2 +- src/fmt.rs | 2 +- src/lib.rs | 1 + src/peer_builder.rs | 9 ++-- 15 files changed, 109 insertions(+), 150 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 681f4b9..33cd4f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,8 @@ atat = { version = "0.21", features = ["derive", "bytes"] } heapless = { version = "^0.8", features = ["serde"] } no-std-net = { version = "0.6", features = ["serde"] } serde = { version = "^1", default-features = false, features = ["derive"] } -ublox-sockets = { version = "0.5", features = ["edm"], optional = true } +# ublox-sockets = { version = "0.5", features = ["edm"], optional = true } +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", features = ["edm"], optional = true } postcard = "1.0.4" portable-atomic = "1.5" @@ -78,11 +79,5 @@ exclude = ["examples"] [patch.crates-io] -atat = { path = "../atat/atat" } -ublox-sockets = { path = "../ublox-sockets" } -no-std-net = { path = "../no-std-net" } - -embassy-time = { path = "../embassy/embassy-time" } -embassy-sync = { path = "../embassy/embassy-sync" } -embassy-futures = { path = "../embassy/embassy-futures" } -embassy-net-driver = { path = "../embassy/embassy-net-driver" } +no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } +atat = { path = "../atat/atat" } \ No newline at end of file diff --git a/src/asynch/control.rs b/src/asynch/control.rs index be438d9..e826bf5 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -4,16 +4,21 @@ use core::task::Poll; use atat::asynch::AtatClient; use embassy_time::{with_timeout, Duration}; -use crate::command::gpio::{ - types::{GPIOId, GPIOValue}, - WriteGPIO, -}; use crate::command::network::SetNetworkHostName; +use crate::command::security::types::SecurityDataType; +use crate::command::security::SendSecurityDataImport; use crate::command::wifi::types::{ Authentication, StatusId, WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, }; use crate::command::wifi::{ExecWifiStationAction, GetWifiStatus, SetWifiStationConfig}; use crate::command::OnOff; +use crate::command::{ + gpio::{ + types::{GPIOId, GPIOValue}, + WriteGPIO, + }, + security::PrepareSecurityDataImport, +}; use crate::error::Error; use super::state::LinkState; @@ -223,7 +228,7 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }) .await?; - with_timeout(Duration::from_secs(10), self.wait_for_join(ssid)) + with_timeout(Duration::from_secs(20), self.wait_for_join(ssid)) .await .map_err(|_| Error::Timeout)??; @@ -275,4 +280,39 @@ impl<'a, AT: AtatClient> Control<'a, AT> { self.at.send_edm(WriteGPIO { id, value }).await?; Ok(()) } + + // FIXME: This could probably be improved + pub async fn import_credentials( + &mut self, + data_type: SecurityDataType, + name: &str, + data: &[u8], + md5_sum: Option<&str>, + ) -> Result<(), atat::Error> { + assert!(name.len() < 16); + + info!("Importing {:?} bytes as {:?}", data.len(), name); + + self.at + .send_edm(PrepareSecurityDataImport { + data_type, + data_size: data.len(), + internal_name: name, + password: None, + }) + .await?; + + let import_data = self + .at + .send_edm(SendSecurityDataImport { + data: atat::serde_bytes::Bytes::new(data), + }) + .await?; + + if let Some(hash) = md5_sum { + assert_eq!(import_data.md5_string.as_str(), hash); + } + + Ok(()) + } } diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index d366869..bf3d6f3 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -4,6 +4,7 @@ use super::state::{self, LinkState}; use crate::{ command::{ edm::{urc::EdmEvent, SwitchToEdmCommand}, + general::SoftwareVersion, network::{ responses::NetworkStatusResponse, types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, @@ -11,8 +12,8 @@ use crate::{ GetNetworkStatus, }, system::{ - types::{EchoOn}, - RebootDCE, SetEcho, StoreCurrentConfig, + types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, + RebootDCE, SetEcho, SetRS232Settings, StoreCurrentConfig, }, wifi::{ types::DisconnectReason, @@ -88,26 +89,21 @@ impl< // parameter. Instead, the // parameter must be set to 0 and the serial settings will take effect // when the module is reset. - // let baud_rate = BaudRate::B3000000; - // self.at - // .send_edm(SetRS232Settings { - // baud_rate, - // flow_control: FlowControl::On, - // data_bits: 8, - // stop_bits: StopBits::One, - // parity: Parity::None, - // change_after_confirm: ChangeAfterConfirm::StoreAndReset, - // }) - // .await?; - - // self.restart(true).await?; - - // self.at - // .0 - // .lock() - // .await - // .set_baudrate(baud_rate as u32) - // .map_err(|_| Error::BaudDetection)?; + let baud_rate = BaudRate::B115200; + self.at + .send_edm(SetRS232Settings { + baud_rate, + flow_control: FlowControl::On, + data_bits: 8, + stop_bits: StopBits::One, + parity: Parity::None, + change_after_confirm: ChangeAfterConfirm::StoreAndReset, + }) + .await?; + + self.restart(true).await?; + + self.at.send_edm(SoftwareVersion).await?; // Move to control // if let Some(size) = self.config.tls_in_buffer_size { @@ -133,7 +129,7 @@ impl< let fut = async { loop { match self.urc_subscription.next_message_pure().await { - EdmEvent::ATEvent(Urc::StartUp) | EdmEvent::StartUp => return, + EdmEvent::ATEvent(Urc::StartUp) => return, _ => {} } } @@ -163,28 +159,29 @@ impl< self.at.send_edm(RebootDCE).await?; - Timer::after(Duration::from_millis(3500)).await; + self.wait_startup(Duration::from_secs(10)).await?; + info!("Module started again"); self.enter_edm(Duration::from_secs(4)).await?; Ok(()) } pub async fn enter_edm(&mut self, timeout: Duration) -> Result<(), Error> { + info!("Entering EDM mode"); + // Switch to EDM on Init. If in EDM, fail and check with autosense let fut = async { loop { // Ignore AT results until we are successful in EDM mode - self.at.send(SwitchToEdmCommand).await.ok(); - - if let Ok(EdmEvent::StartUp) = with_timeout( - Duration::from_millis(300), - self.urc_subscription.next_message_pure(), - ) - .await - { + if let Ok(_) = self.at.send(SwitchToEdmCommand).await { + // After executing the data mode command or the extended data + // mode command, a delay of 50 ms is required before start of + // data transmission. + Timer::after(Duration::from_millis(50)).await; break; } + Timer::after(Duration::from_millis(10)).await; } }; @@ -192,7 +189,7 @@ impl< .await .map_err(|_| Error::Timeout)?; - self.at.send_edm(SetEcho { on: EchoOn::Off }).await?; + self.at.send_edm(SetEcho { on: EchoOn::On }).await?; Ok(()) } diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index a71e18c..ab28ee2 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -147,18 +147,12 @@ impl<'a> DnsSocket<'a> { s.waker.wake(); } - - poll_fn(|cx| { let mut s = self.stack.borrow_mut(); let query = s.dns_table.get_mut(&name_string).unwrap(); match query.state { - DnsState::Resolved(ip) => { - Poll::Ready(Ok(ip)) - } - DnsState::Error(_e) => { - Poll::Ready(Err(Error::Failed)) - } + DnsState::Resolved(ip) => Poll::Ready(Ok(ip)), + DnsState::Error(_e) => Poll::Ready(Err(Error::Failed)), _ => { query.waker.register(cx.waker()); Poll::Pending diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index 89ff839..5074181 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -22,8 +22,6 @@ use crate::command::edm::EdmDataCommand; use crate::command::ping::types::PingError; use crate::command::ping::urc::{PingErrorResponse, PingResponse}; use crate::command::ping::Ping; -use crate::command::security::types::SecurityDataType; -use crate::command::security::{PrepareSecurityDataImport, SendSecurityDataImport}; use crate::command::Urc; use crate::peer_builder::{PeerUrlBuilder, SecurityCredentials}; @@ -81,7 +79,7 @@ struct SocketStack { waker: WakerRegistration, dns_table: DnsTable, dropped_sockets: heapless::Vec, - credential_map: heapless::FnvIndexMap, + credential_map: heapless::FnvIndexMap, } impl UbloxStack { @@ -170,76 +168,6 @@ impl UbloxStack Result<(), atat::Error> { - let mut device = self.device.borrow_mut(); - - assert!(root_ca.0.len() < 16); - assert!(cert.0.len() < 16); - assert!(priv_key.0.len() < 16); - - device - .at - .send_edm(PrepareSecurityDataImport { - data_type: SecurityDataType::TrustedRootCA, - data_size: root_ca.1.len(), - internal_name: root_ca.0, - password: None, - }) - .await?; - - device - .at - .send_edm(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(root_ca.1), - }) - .await?; - - device - .at - .send_edm(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientCertificate, - data_size: cert.1.len(), - internal_name: cert.0, - password: None, - }) - .await?; - - device - .at - .send_edm(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(cert.1), - }) - .await?; - - device - .at - .send_edm(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientPrivateKey, - data_size: priv_key.1.len(), - internal_name: priv_key.0, - password: None, - }) - .await?; - - device - .at - .send_edm(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(priv_key.1), - }) - .await?; - - // FIXME: - // self.socket.borrow_mut().credential_map.insert(key, value); - - Ok(()) - } - /// Make a query for a given name and return the corresponding IP addresses. // #[cfg(feature = "dns")] pub async fn dns_query( @@ -426,7 +354,8 @@ impl UbloxStack TlsSocket<'a> { let TcpIo { stack, handle } = tcp_socket.io; let s = &mut *stack.borrow_mut(); + info!("Associating credentials {} with {}", credentials, handle); s.credential_map.insert(handle, credentials); Self { inner: tcp_socket } @@ -217,6 +218,13 @@ impl<'a> TlsSocket<'a> { } } +impl<'a> Drop for TlsSocket<'a> { + fn drop(&mut self) { + let mut stack = self.inner.io.stack.borrow_mut(); + stack.credential_map.remove(&self.inner.io.handle); + } +} + mod embedded_io_impls { use super::*; diff --git a/src/asynch/ublox_stack/udp.rs b/src/asynch/ublox_stack/udp.rs index 285a880..8348b3d 100644 --- a/src/asynch/ublox_stack/udp.rs +++ b/src/asynch/ublox_stack/udp.rs @@ -3,7 +3,6 @@ use core::cell::RefCell; use core::mem; - use atat::asynch::AtatClient; use embedded_nal_async::SocketAddr; use ublox_sockets::{udp, SocketHandle, UdpState}; diff --git a/src/command/custom_digest.rs b/src/command/custom_digest.rs index 888c06f..d591a08 100644 --- a/src/command/custom_digest.rs +++ b/src/command/custom_digest.rs @@ -84,8 +84,7 @@ impl Digester for EdmDigester { }; (return_val, edm_len) } - PayloadType::StartEvent => (DigestResult::Urc(&buf[..edm_len]), edm_len), - // PayloadType::StartEvent => (DigestResult::Response(Ok(&buf[..edm_len])), edm_len), + PayloadType::StartEvent => (DigestResult::Response(Ok(&buf[..edm_len])), edm_len), PayloadType::ATEvent | PayloadType::ConnectEvent | PayloadType::DataEvent diff --git a/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs index 9221b24..fa9da8d 100644 --- a/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -29,7 +29,7 @@ pub struct ChangeMode { /// service on a remote device, it implicitly registers to receive the "Connection Closed" /// event. #[derive(Clone, AtatCmd)] -#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 3000)] +#[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 5000)] pub struct ConnectPeer<'a> { #[at_arg(position = 0, len = 128)] pub url: &'a str, diff --git a/src/command/edm/mod.rs b/src/command/edm/mod.rs index c1dd312..691c47b 100644 --- a/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -172,16 +172,14 @@ impl atat::AtatCmd for SwitchToEdmCommand { fn parse( &self, - _resp: Result<&[u8], atat::InternalError>, + resp: Result<&[u8], atat::InternalError>, ) -> core::result::Result { - // let resp = resp?; - // // Parse EDM startup command - // let correct = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; // &[0xAA,0x00,0x06,0x00,0x45,0x4f,0x4b,0x0D,0x0a,0x55]; // AA 00 06 00 44 41 54 0D 0A 0D 0A 4F 4B 0D 0A 55 ? - // if resp.len() != correct.len() - // || resp[.. correct.len()] != *correct { - // // TODO: check this - // return Err(atat::Error::InvalidResponse); - // } + let resp = resp?; + // Parse EDM startup command + let correct = &[0xAA, 0x00, 0x02, 0x00, 0x71, 0x55]; + if resp.len() != correct.len() || resp[..correct.len()] != *correct { + return Err(atat::Error::InvalidResponse); + } Ok(NoResponse) } } diff --git a/src/command/security/mod.rs b/src/command/security/mod.rs index 388057a..81fd730 100644 --- a/src/command/security/mod.rs +++ b/src/command/security/mod.rs @@ -41,7 +41,7 @@ pub struct PrepareSecurityDataImport<'a> { "", SecurityDataImport, value_sep = false, - timeout_ms = 1000, + timeout_ms = 3000, cmd_prefix = "", termination = "" )] diff --git a/src/fmt.rs b/src/fmt.rs index 3f3ed3c..81c9940 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,7 +1,7 @@ #![macro_use] #![allow(unused_macros)] -use core::fmt::{Debug}; +use core::fmt::Debug; #[cfg(all(feature = "defmt", feature = "log"))] compile_error!("You may not enable both `defmt` and `log` features."); diff --git a/src/lib.rs b/src/lib.rs index 6262ebd..c1adaa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub use atat; pub mod command; pub mod error; // pub mod wifi; +pub use peer_builder::SecurityCredentials; // TODO: // - UDP stack diff --git a/src/peer_builder.rs b/src/peer_builder.rs index 7c5e6d3..2e65114 100644 --- a/src/peer_builder.rs +++ b/src/peer_builder.rs @@ -3,7 +3,8 @@ use core::fmt::Write; use heapless::String; use no_std_net::{IpAddr, SocketAddr}; -#[derive(PartialEq, Clone, Default)] +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct SecurityCredentials { pub ca_cert_name: heapless::String<16>, pub c_cert_name: heapless::String<16>, @@ -15,7 +16,7 @@ pub(crate) struct PeerUrlBuilder<'a> { hostname: Option<&'a str>, ip_addr: Option, port: Option, - creds: Option, + creds: Option<&'a SecurityCredentials>, local_port: Option, } @@ -115,7 +116,7 @@ impl<'a> PeerUrlBuilder<'a> { self } - pub fn creds(&mut self, creds: SecurityCredentials) -> &mut Self { + pub fn creds(&mut self, creds: &'a SecurityCredentials) -> &mut Self { self.creds.replace(creds); self } @@ -173,7 +174,7 @@ mod test { let url = PeerUrlBuilder::new() .hostname("example.org") .port(2000) - .creds(SecurityCredentials { + .creds(&SecurityCredentials { c_cert_name: heapless::String::try_from("client.crt").unwrap(), ca_cert_name: heapless::String::try_from("ca.crt").unwrap(), c_key_name: heapless::String::try_from("client.key").unwrap(), From 6ad40f543cc4d4d7d46dd31d4fa112e2d4cb4084 Mon Sep 17 00:00:00 2001 From: Mathias Date: Wed, 24 Jan 2024 12:20:28 +0100 Subject: [PATCH 13/16] Reduce stack usage from holding large resources across await points --- .vscode/settings.json | 6 +- Cargo.toml | 2 +- src/asynch/runner.rs | 132 ++++++++++++++++++---------------- src/asynch/ublox_stack/mod.rs | 53 ++++++++------ 4 files changed, 106 insertions(+), 87 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 67d222c..bd3b577 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,12 +4,8 @@ "editor.formatOnSave": false }, "rust-analyzer.cargo.target": "thumbv6m-none-eabi", - "rust-analyzer.cargo.noDefaultFeatures": true, "rust-analyzer.check.allTargets": false, - "rust-analyzer.check.noDefaultFeatures": true, - "rust-analyzer.linkedProjects": [ - "examples/rpi-pico/Cargo.toml", - ], + "rust-analyzer.linkedProjects": [], "rust-analyzer.server.extraEnv": { "WIFI_NETWORK": "foo", "WIFI_PASSWORD": "foo", diff --git a/Cargo.toml b/Cargo.toml index 33cd4f2..521c679 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ defmt = [ "postcard/use-defmt", "heapless/defmt-03", "atat/defmt", - "ublox-sockets/defmt", + "ublox-sockets?/defmt", ] odin_w2xx = [] diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index bf3d6f3..260b190 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -212,71 +212,83 @@ impl< pub async fn run(mut self) -> ! { loop { - let event = self.urc_subscription.next_message_pure().await; - match event { - EdmEvent::ATEvent(Urc::StartUp) => { - error!("AT startup event?! Device restarted unintentionally!"); - } - EdmEvent::ATEvent(Urc::WifiLinkConnected(WifiLinkConnected { - connection_id: _, - bssid, - channel, - })) => { - if let Some(ref mut con) = self.wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = bssid; - con.network.channel = channel; - } else { - debug!("[URC] Active network config discovered"); - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork::new_station(bssid, channel), - WiFiState::Connected, - 255, - ) - .activate(), - ); + let wait_link_up = { + let event = self.urc_subscription.next_message_pure().await; + match event { + EdmEvent::ATEvent(Urc::StartUp) => { + error!("AT startup event?! Device restarted unintentionally!"); + false } - self.is_link_up().await.unwrap(); - } - EdmEvent::ATEvent(Urc::WifiLinkDisconnected(WifiLinkDisconnected { - reason, - .. - })) => { - if let Some(ref mut con) = self.wifi_connection { - match reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } + EdmEvent::ATEvent(Urc::WifiLinkConnected(WifiLinkConnected { + connection_id: _, + bssid, + channel, + })) => { + if let Some(ref mut con) = self.wifi_connection { + con.wifi_state = WiFiState::Connected; + con.network.bssid = bssid; + con.network.channel = channel; + } else { + debug!("[URC] Active network config discovered"); + self.wifi_connection.replace( + WifiConnection::new( + WifiNetwork::new_station(bssid, channel), + WiFiState::Connected, + 255, + ) + .activate(), + ); } + true } + EdmEvent::ATEvent(Urc::WifiLinkDisconnected(WifiLinkDisconnected { + reason, + .. + })) => { + if let Some(ref mut con) = self.wifi_connection { + match reason { + DisconnectReason::NetworkDisabled => { + con.wifi_state = WiFiState::Inactive; + } + DisconnectReason::SecurityProblems => { + error!("Wifi Security Problems"); + } + _ => { + con.wifi_state = WiFiState::NotConnected; + } + } + } - self.is_link_up().await.unwrap(); - } - EdmEvent::ATEvent(Urc::WifiAPUp(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPDown(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPStationConnected(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPStationDisconnected(_)) => todo!(), - EdmEvent::ATEvent(Urc::EthernetLinkUp(_)) => todo!(), - EdmEvent::ATEvent(Urc::EthernetLinkDown(_)) => todo!(), - EdmEvent::ATEvent(Urc::NetworkUp(NetworkUp { interface_id })) => { - self.network_status_callback(interface_id).await.unwrap(); - } - EdmEvent::ATEvent(Urc::NetworkDown(NetworkDown { interface_id })) => { - self.network_status_callback(interface_id).await.unwrap(); - } - EdmEvent::ATEvent(Urc::NetworkError(_)) => todo!(), - EdmEvent::StartUp => { - error!("EDM startup event?! Device restarted unintentionally!"); + true + } + EdmEvent::ATEvent(Urc::WifiAPUp(_)) => todo!(), + EdmEvent::ATEvent(Urc::WifiAPDown(_)) => todo!(), + EdmEvent::ATEvent(Urc::WifiAPStationConnected(_)) => todo!(), + EdmEvent::ATEvent(Urc::WifiAPStationDisconnected(_)) => todo!(), + EdmEvent::ATEvent(Urc::EthernetLinkUp(_)) => todo!(), + EdmEvent::ATEvent(Urc::EthernetLinkDown(_)) => todo!(), + EdmEvent::ATEvent(Urc::NetworkUp(NetworkUp { interface_id })) => { + drop(event); + self.network_status_callback(interface_id).await.unwrap(); + true + } + EdmEvent::ATEvent(Urc::NetworkDown(NetworkDown { interface_id })) => { + drop(event); + self.network_status_callback(interface_id).await.unwrap(); + true + } + EdmEvent::ATEvent(Urc::NetworkError(_)) => todo!(), + EdmEvent::StartUp => { + error!("EDM startup event?! Device restarted unintentionally!"); + false + } + _ => false, } - _ => {} }; + + if wait_link_up { + self.is_link_up().await.unwrap(); + } } } @@ -340,8 +352,6 @@ impl< con.network_up = ipv4_up && ipv6_up; } - self.is_link_up().await?; - Ok(()) } } diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index 5074181..b5f3b8d 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -16,7 +16,7 @@ use crate::asynch::state::Device; use crate::command::data_mode::responses::ConnectPeerResponse; use crate::command::data_mode::urc::PeerDisconnected; use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; -use crate::command::edm::types::{DataEvent, Protocol, DATA_PACKAGE_SIZE}; +use crate::command::edm::types::{DataEvent, Protocol}; use crate::command::edm::urc::EdmEvent; use crate::command::edm::EdmDataCommand; use crate::command::ping::types::PingError; @@ -25,13 +25,13 @@ use crate::command::ping::Ping; use crate::command::Urc; use crate::peer_builder::{PeerUrlBuilder, SecurityCredentials}; -use self::dns::{DnsSocket, DnsState, DnsTable, MAX_DOMAIN_NAME_LENGTH}; +use self::dns::{DnsSocket, DnsState, DnsTable}; use super::state::{self, LinkState}; use super::AtHandle; use atat::asynch::AtatClient; -use embassy_futures::select::{select4, Either4}; +use embassy_futures::select; use embassy_sync::waitqueue::WakerRegistration; use embassy_time::{Duration, Ticker}; use embedded_nal_async::SocketAddr; @@ -48,6 +48,8 @@ use ublox_sockets::TcpState; #[cfg(feature = "socket-udp")] use ublox_sockets::UdpState; +const MAX_EGRESS_SIZE: usize = 2048; + pub struct StackResources { sockets: [SocketStorage<'static>; SOCK], } @@ -107,6 +109,8 @@ impl UbloxStack ! { + let mut tx_buf = [0u8; MAX_EGRESS_SIZE]; + loop { // FIXME: It feels like this can be written smarter/simpler? let should_tx = poll_fn(|cx| match self.should_tx.load(Ordering::Relaxed) { @@ -131,7 +135,7 @@ impl UbloxStack UbloxStack { + select::Either4::First(event) => { Self::socket_rx(event, &self.socket); } - Either4::Second(_) | Either4::Third(_) => { - if let Some(ev) = self.tx_event() { + select::Either4::Second(_) | select::Either4::Third(_) => { + if let Some(ev) = self.tx_event(&mut tx_buf) { Self::socket_tx(ev, &self.socket, at).await; } } - Either4::Fourth(new_state) => { + select::Either4::Fourth(new_state) => { // Update link up let old_link_up = self.link_up.load(Ordering::Relaxed); let new_link_up = new_state == LinkState::Up; @@ -300,13 +304,14 @@ impl UbloxStack Option { + fn tx_event<'data>(&self, buf: &'data mut [u8]) -> Option> { let mut s = self.socket.borrow_mut(); for query in s.dns_table.table.iter_mut() { if let DnsState::New = query.state { query.state = DnsState::Pending; + buf[..query.domain_name.len()].copy_from_slice(query.domain_name.as_bytes()); return Some(TxEvent::Dns { - hostname: query.domain_name.clone(), + hostname: core::str::from_utf8(&buf[..query.domain_name.len()]).unwrap(), }); } } @@ -362,9 +367,12 @@ impl UbloxStack().unwrap(); + // FIXME: Write directly into `buf` instead + buf[..url.len()].copy_from_slice(url.as_bytes()); + return Some(TxEvent::Connect { socket_handle: handle, - url, + url: core::str::from_utf8(&buf[..url.len()]).unwrap(), }); } } @@ -373,11 +381,12 @@ impl UbloxStack { if let Some(edm_channel) = tcp.edm_channel { return tcp.tx_dequeue(|payload| { - let len = core::cmp::min(payload.len(), DATA_PACKAGE_SIZE); + let len = core::cmp::min(payload.len(), MAX_EGRESS_SIZE); let res = if len != 0 { + buf[..len].copy_from_slice(&payload[..len]); Some(TxEvent::Send { edm_channel, - data: heapless::Vec::from_slice(payload).unwrap(), + data: &buf[..len], }) } else { None @@ -404,7 +413,11 @@ impl UbloxStack, at: &mut AtHandle<'_, AT>) { + async fn socket_tx<'data>( + ev: TxEvent<'data>, + socket: &RefCell, + at: &mut AtHandle<'_, AT>, + ) { match ev { TxEvent::Connect { socket_handle, url } => { match at.send_edm(ConnectPeer { url: &url }).await { @@ -425,7 +438,7 @@ impl UbloxStack UbloxStack { Connect { socket_handle: SocketHandle, - url: heapless::String<128>, + url: &'data str, }, Send { edm_channel: ChannelId, - data: heapless::Vec, + data: &'data [u8], }, Close { peer_handle: PeerHandle, }, Dns { - hostname: heapless::String, + hostname: &'data str, }, } #[cfg(feature = "defmt")] -impl defmt::Format for TxEvent { +impl defmt::Format for TxEvent<'_> { fn format(&self, fmt: defmt::Formatter) { match self { TxEvent::Connect { .. } => defmt::write!(fmt, "TxEvent::Connect"), From 8ab161227432ce3aa713a00b80695c8d73e7c823 Mon Sep 17 00:00:00 2001 From: Mathias Date: Sat, 3 Feb 2024 20:42:14 +0100 Subject: [PATCH 14/16] Correctly handle closing a dropped socket in FinWait1 state --- src/asynch/runner.rs | 1 + src/asynch/ublox_stack/tcp.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index 260b190..bab1368 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -252,6 +252,7 @@ impl< } DisconnectReason::SecurityProblems => { error!("Wifi Security Problems"); + con.wifi_state = WiFiState::NotConnected; } _ => { con.wifi_state = WiFiState::NotConnected; diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index 33a16f8..aef9f89 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -347,7 +347,10 @@ impl<'a> TcpSocket<'a> { impl<'a> Drop for TcpSocket<'a> { fn drop(&mut self) { - if matches!(self.state(), TcpState::Listen | TcpState::Established) { + if matches!( + self.state(), + TcpState::Listen | TcpState::Established | TcpState::FinWait1 + ) { if let Some(peer_handle) = self.io.with(|s| s.peer_handle) { self.io .stack From 67807ba4c3ccea4dc821d9713924370a349ff215 Mon Sep 17 00:00:00 2001 From: Mathias Koch Date: Tue, 16 Jul 2024 14:25:23 +0200 Subject: [PATCH 15/16] Add support for PPP mode (#81) * Simplify initialization of both ppp mode and ublox mode, by providing batteries included new functions that sets up ATAT and all related resources * Refactor async completely for a more intuitive API. URCs over PPP UDP socket is still not working properly * Bump embassy-sync to 0.6 * Fix internal-network-stack compiling * Rework runner, add Proxy client and add working Control handle * Working control handle for connect and disconnect, with ppp udp bridge * Add a large number of convenience functions to Control and cleanup runner patterns --- .github/workflows/audit.yml | 14 - .github/workflows/{lint.yml => ci.yml} | 61 +- .github/workflows/docs.yml | 47 - .github/workflows/grcov.yml | 78 -- .github/workflows/test.yml | 35 - .vscode/settings.json | 5 + Cargo.toml | 71 +- Design_diagram.drawio | 1 - Design_diagram.png | Bin 135242 -> 0 bytes README.md | 38 +- examples/linux.rs | 152 ---- examples/rpi-pico/.cargo/config.toml | 3 +- examples/rpi-pico/.vscode/settings.json | 16 + examples/rpi-pico/Cargo.toml | 59 +- examples/rpi-pico/rust-toolchain.toml | 7 + examples/rpi-pico/src/bin/embassy-async.rs | 90 +- examples/rpi-pico/src/bin/embassy-perf.rs | 68 +- .../rpi-pico/src/bin/embassy-smoltcp-ppp.rs | 196 ++++ examples/rpi-pico/src/common.rs | 56 -- rust-toolchain.toml | 2 +- src/asynch/at_udp_socket.rs | 70 ++ src/asynch/control.rs | 767 ++++++++++++---- src/asynch/mod.rs | 82 +- src/asynch/network.rs | 321 +++++++ src/asynch/resources.rs | 39 + src/asynch/runner.rs | 728 +++++++++------ src/asynch/state.rs | 240 +++-- src/asynch/ublox_stack/device.rs | 11 + src/asynch/ublox_stack/dns.rs | 9 +- src/asynch/ublox_stack/mod.rs | 108 +-- src/{ => asynch/ublox_stack}/peer_builder.rs | 0 src/asynch/ublox_stack/tcp.rs | 43 +- src/asynch/ublox_stack/tls.rs | 45 +- src/asynch/ublox_stack/udp.rs | 5 +- src/blocking/client.rs | 859 ------------------ src/blocking/dns.rs | 52 -- src/blocking/mod.rs | 12 - src/blocking/tcp_stack.rs | 227 ----- src/blocking/timer.rs | 42 - src/blocking/tls.rs | 105 --- src/blocking/udp_stack.rs | 393 -------- src/command/custom_digest.rs | 22 +- src/command/data_mode/mod.rs | 8 +- src/command/data_mode/responses.rs | 14 +- src/command/data_mode/urc.rs | 18 +- src/command/edm/mod.rs | 2 +- src/command/edm/urc.rs | 11 +- src/command/general/mod.rs | 12 +- src/command/general/responses.rs | 6 +- src/command/gpio/responses.rs | 4 +- src/command/mod.rs | 6 +- src/command/network/mod.rs | 2 +- src/command/ping/types.rs | 4 +- src/command/system/mod.rs | 16 +- src/command/system/responses.rs | 4 +- src/command/system/types.rs | 11 +- src/command/wifi/types.rs | 6 +- src/config.rs | 31 + src/connection.rs | 76 +- src/error.rs | 12 +- src/fmt.rs | 54 +- src/lib.rs | 41 +- src/network.rs | 4 +- src/wifi/ap.rs | 227 ----- src/wifi/mod.rs | 86 -- src/wifi/options.rs | 137 --- src/wifi/supplicant.rs | 603 ------------ 67 files changed, 2404 insertions(+), 4170 deletions(-) delete mode 100644 .github/workflows/audit.yml rename .github/workflows/{lint.yml => ci.yml} (56%) delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/grcov.yml delete mode 100644 .github/workflows/test.yml delete mode 100644 Design_diagram.drawio delete mode 100644 Design_diagram.png delete mode 100644 examples/linux.rs create mode 100644 examples/rpi-pico/.vscode/settings.json create mode 100644 examples/rpi-pico/rust-toolchain.toml create mode 100644 examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs delete mode 100644 examples/rpi-pico/src/common.rs create mode 100644 src/asynch/at_udp_socket.rs create mode 100644 src/asynch/network.rs create mode 100644 src/asynch/resources.rs create mode 100644 src/asynch/ublox_stack/device.rs rename src/{ => asynch/ublox_stack}/peer_builder.rs (100%) delete mode 100644 src/blocking/client.rs delete mode 100644 src/blocking/dns.rs delete mode 100644 src/blocking/mod.rs delete mode 100644 src/blocking/tcp_stack.rs delete mode 100644 src/blocking/timer.rs delete mode 100644 src/blocking/tls.rs delete mode 100644 src/blocking/udp_stack.rs create mode 100644 src/config.rs delete mode 100644 src/wifi/ap.rs delete mode 100644 src/wifi/mod.rs delete mode 100644 src/wifi/options.rs delete mode 100644 src/wifi/supplicant.rs diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml deleted file mode 100644 index 4974b63..0000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Security audit -on: - push: - paths: - - '**/Cargo.toml' - - '**/Cargo.lock' -jobs: - security_audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions-rs/audit-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/ci.yml similarity index 56% rename from .github/workflows/lint.yml rename to .github/workflows/ci.yml index 338dbda..4ea3ed4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Lint +name: CI on: push: @@ -10,9 +10,6 @@ defaults: run: shell: bash -env: - CLIPPY_PARAMS: -W clippy::all -W clippy::pedantic -W clippy::nursery -W clippy::cargo - jobs: rustfmt: name: rustfmt @@ -35,33 +32,6 @@ jobs: command: fmt args: --all -- --check --verbose - # tomlfmt: - # name: tomlfmt - # runs-on: ubuntu-latest - # steps: - # - name: Checkout source code - # uses: actions/checkout@v2 - - # - name: Install Rust - # uses: actions-rs/toolchain@v1 - # with: - # profile: minimal - # toolchain: nightly - # override: true - - # - name: Install tomlfmt - # uses: actions-rs/install@v0.1 - # with: - # crate: cargo-tomlfmt - # version: latest - # use-tool-cache: true - - # - name: Run Tomlfmt - # uses: actions-rs/cargo@v1 - # with: - # command: tomlfmt - # args: --dryrun - clippy: name: clippy runs-on: ubuntu-latest @@ -81,4 +51,31 @@ jobs: uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: -- ${{ env.CLIPPY_PARAMS }} + args: --features odin-w2xx,ppp + + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: thumbv7m-none-eabi + override: true + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --all --target thumbv7m-none-eabi --features odin-w2xx,ppp + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test + args: --lib --features odin-w2xx,ppp diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index c9b0dba..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Documentation - -on: - push: - branches: - - master - -jobs: - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v2 - with: - persist-credentials: false - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - - - name: Build documentation - uses: actions-rs/cargo@v1 - with: - command: doc - args: --verbose --no-deps - - # - name: Finalize documentation - # run: | - # CRATE_NAME=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]' | cut -f2 -d"/") - # echo "" > target/doc/index.html - # touch target/doc/.nojekyll - # - name: Upload as artifact - # uses: actions/upload-artifact@v2 - # with: - # name: Documentation - # path: target/doc - - # - name: Deploy - # uses: JamesIves/github-pages-deploy-action@releases/v3 - # with: - # ACCESS_TOKEN: ${{ secrets.GH_PAT }} - # BRANCH: gh-pages - # FOLDER: target/doc diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml deleted file mode 100644 index af13453..0000000 --- a/.github/workflows/grcov.yml +++ /dev/null @@ -1,78 +0,0 @@ -# name: Coverage - -# on: -# push: -# branches: -# - master -# pull_request: - -# jobs: -# grcov: -# name: Coverage -# runs-on: ubuntu-latest -# steps: -# - name: Checkout source code -# uses: actions/checkout@v2 - -# - name: Install Rust -# uses: actions-rs/toolchain@v1 -# with: -# profile: minimal -# toolchain: nightly -# target: thumbv7m-none-eabi -# override: true - -# - name: Install grcov -# uses: actions-rs/cargo@v1 -# # uses: actions-rs/install@v0.1 -# with: -# # crate: grcov -# # version: latest -# # use-tool-cache: true -# command: install -# args: grcov --git https://github.com/mozilla/grcov - -# - name: Test -# uses: actions-rs/cargo@v1 -# with: -# command: test -# args: --lib --no-fail-fast -# env: -# CARGO_INCREMENTAL: "0" -# RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=unwind -Zpanic_abort_tests" -# RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=unwind -Zpanic_abort_tests" - -# - name: Generate coverage data -# id: grcov -# # uses: actions-rs/grcov@v0.1 -# run: | -# grcov target/debug/ \ -# --branch \ -# --llvm \ -# --source-dir . \ -# --output-file lcov.info \ -# --ignore='/**' \ -# --ignore='C:/**' \ -# --ignore='../**' \ -# --ignore-not-existing \ -# --excl-line "#\\[derive\\(" \ -# --excl-br-line "(#\\[derive\\()|(debug_assert)" \ -# --excl-start "#\\[cfg\\(test\\)\\]" \ -# --excl-br-start "#\\[cfg\\(test\\)\\]" \ -# --commit-sha ${{ github.sha }} \ -# --service-job-id ${{ github.job }} \ -# --service-name "GitHub Actions" \ -# --service-number ${{ github.run_id }} -# - name: Upload coverage as artifact -# uses: actions/upload-artifact@v2 -# with: -# name: lcov.info -# # path: ${{ steps.grcov.outputs.report }} -# path: lcov.info - -# - name: Upload coverage to codecov.io -# uses: codecov/codecov-action@v1 -# with: -# # file: ${{ steps.grcov.outputs.report }} -# file: lcov.info -# fail_ci_if_error: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 9884357..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Test - -on: - push: - branches: - - master - pull_request: - -jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - name: Checkout source code - uses: actions/checkout@v2 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - target: thumbv7m-none-eabi - override: true - - - name: Build - uses: actions-rs/cargo@v1 - with: - command: build - args: --all --target thumbv7m-none-eabi - - - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: --lib diff --git a/.vscode/settings.json b/.vscode/settings.json index bd3b577..8fc7548 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,11 @@ "rust-analyzer.cargo.target": "thumbv6m-none-eabi", "rust-analyzer.check.allTargets": false, "rust-analyzer.linkedProjects": [], + "rust-analyzer.cargo.features": [ + "odin-w2xx", + // "internal-network-stack" + "ppp" + ], "rust-analyzer.server.extraEnv": { "WIFI_NETWORK": "foo", "WIFI_PASSWORD": "foo", diff --git a/Cargo.toml b/Cargo.toml index 521c679..650b27a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,62 +15,66 @@ name = "ublox_short_range" doctest = false [dependencies] -atat = { version = "0.21", features = ["derive", "bytes"] } -# atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "70283be", features = ["derive", "defmt", "bytes"] } +atat = { version = "0.23", features = ["derive", "bytes"] } + heapless = { version = "^0.8", features = ["serde"] } no-std-net = { version = "0.6", features = ["serde"] } serde = { version = "^1", default-features = false, features = ["derive"] } -# ublox-sockets = { version = "0.5", features = ["edm"], optional = true } -ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", features = ["edm"], optional = true } -postcard = "1.0.4" -portable-atomic = "1.5" +# ublox-sockets = { version = "0.5", optional = true } +ublox-sockets = { git = "https://github.com/BlackbirdHQ/ublox-sockets", rev = "9f7fe54", optional = true } +portable-atomic = "1.6" -defmt = { version = "0.3", optional = true } -log = { version = "0.4.14", optional = true } +log = { version = "^0.4", default-features = false, optional = true } +defmt = { version = "^0.3", optional = true } embedded-hal = "1.0" embassy-time = "0.3" -embassy-sync = "0.5" +embassy-sync = "0.6" embassy-futures = "0.1" -embassy-net-driver = "0.2" embedded-nal-async = { version = "0.7" } -futures = { version = "0.3.17", default-features = false, features = [ - "async-await", -] } +futures-util = { version = "0.3.29", default-features = false } -embedded-io = "0.6" embedded-io-async = "0.6" +embassy-net-ppp = { version = "0.1", optional = true } +embassy-net = { version = "0.4", features = [ + "proto-ipv4", + "medium-ip", +], optional = true } + + [features] -default = ["odin_w2xx", "ublox-sockets", "socket-tcp", "socket-udp"] +default = ["socket-tcp", "socket-udp"] + +internal-network-stack = ["dep:ublox-sockets", "edm"] +edm = ["ublox-sockets?/edm"] +ipv6 = ["embassy-net?/proto-ipv6"] -std = [] +# PPP mode requires UDP sockets enabled, to be able to do AT commands over UDP port 23 +ppp = ["dep:embassy-net-ppp", "dep:embassy-net", "socket-udp"] + +socket-tcp = ["ublox-sockets?/socket-tcp", "embassy-net?/tcp"] +socket-udp = ["ublox-sockets?/socket-udp", "embassy-net?/udp"] defmt = [ "dep:defmt", - "postcard/use-defmt", "heapless/defmt-03", "atat/defmt", "ublox-sockets?/defmt", + "embassy-net-ppp?/defmt", + "embassy-net?/defmt", ] +log = ["dep:log", "ublox-sockets?/log", "atat/log"] -odin_w2xx = [] -nina_w1xx = [] -nina_b1xx = [] -anna_b1xx = [] -nina_b2xx = [] -nina_b3xx = [] - -socket-tcp = [ - "ublox-sockets?/socket-tcp", - # "smoltcp?/socket-tcp" -] -socket-udp = [ - "ublox-sockets?/socket-udp", - # "smoltcp?/socket-udp" -] +# Supported Ublox modules +odin-w2xx = [] +nina-w1xx = [] +nina-b1xx = [] +anna-b1xx = [] +nina-b2xx = [] +nina-b3xx = [] [workspace] members = [] @@ -80,4 +84,5 @@ exclude = ["examples"] [patch.crates-io] no-std-net = { git = "https://github.com/rushmorem/no-std-net", branch = "issue-15" } -atat = { path = "../atat/atat" } \ No newline at end of file +atat = { git = "https://github.com/BlackbirdHQ/atat", rev = "a466836" } +# atat = { path = "../atat/atat" } \ No newline at end of file diff --git a/Design_diagram.drawio b/Design_diagram.drawio deleted file mode 100644 index 016b7ea..0000000 --- a/Design_diagram.drawio +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/Design_diagram.png b/Design_diagram.png deleted file mode 100644 index e73bc905ee4f8cfc3c2d60fcd095a2ec59f0b7fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135242 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

(_#$PkcfV+@iQa8 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 Z@|`iG@k(En + [no-std-badge]: https://img.shields.io/badge/no__std-yes-blue [test]: https://github.com/BlackbirdHQ/ublox-short-range-rs/workflows/Test/badge.svg [codecov-badge]: https://codecov.io/gh/BlackbirdHQ/ublox-short-range-rs/branch/master/graph/badge.svg diff --git a/examples/linux.rs b/examples/linux.rs deleted file mode 100644 index 52804e9..0000000 --- a/examples/linux.rs +++ /dev/null @@ -1,152 +0,0 @@ -// use std::sync::{Arc, Mutex}; -// use std::thread; -// use std::time::Duration; - -// use linux_embedded_hal::Serial; -// use serial::{self, core::SerialPort}; - -// extern crate at_rs as at; -// extern crate env_logger; -// extern crate nb; - -// // Note this useful idiom: importing names from outer (for mod tests) scope. -// use ublox_short_range::command::*; -// use ublox_short_range::prelude::*; -// use ublox_short_range::wifi; - -// use heapless::{consts::*, spsc::Queue, String}; -// #[allow(unused_imports)] -// use defmt::{error, info, warn}; - -// #[derive(Clone, Copy)] -// struct MilliSeconds(u32); - -// trait U32Ext { -// fn s(self) -> MilliSeconds; -// fn ms(self) -> MilliSeconds; -// } - -// impl U32Ext for u32 { -// fn s(self) -> MilliSeconds { -// MilliSeconds(self / 1000) -// } -// fn ms(self) -> MilliSeconds { -// MilliSeconds(self) -// } -// } - -// struct Timer; - -// impl embedded_hal::timer::CountDown for Timer { -// type Time = MilliSeconds; -// fn start(&mut self, _duration: T) -// where -// T: Into, -// { -// // let dur = duration.into(); -// // self.timeout_time = Instant::now().checked_add(Duration::from_millis(dur.0.into())).expect(""); -// } - -// fn wait(&mut self) -> ::nb::Result<(), void::Void> { -// // if self.timeout_time - Instant::now() < Duration::from_secs(0) { -// // Ok(()) -// // } else { -// Err(nb::Error::WouldBlock) -// // } -// } -// } - -// impl embedded_hal::timer::Cancel for Timer { -// type Error = (); -// fn cancel(&mut self) -> Result<(), Self::Error> { -// Ok(()) -// } -// } - -// type SerialRxBufferLen = U4096; -// type ATRequestQueueLen = U5; -// type ATResponseQueueLen = U5; - -// static mut WIFI_REQ_Q: Option> = None; -// static mut WIFI_RES_Q: Option, ATResponseQueueLen, u8>> = -// None; - -// fn main() { -// env_logger::builder() -// .filter_level(defmt::LevelFilter::Trace) -// .init(); - -// // Serial port settings -// let settings = serial::PortSettings { -// baud_rate: serial::Baud115200, -// char_size: serial::Bits8, -// parity: serial::ParityNone, -// stop_bits: serial::Stop1, -// flow_control: serial::FlowNone, -// }; - -// // Open serial port -// let mut port = serial::open("/dev/ttyACM0").expect("Could not open serial port"); -// port.configure(&settings) -// .expect("Could not configure serial port"); - -// port.set_timeout(Duration::from_millis(2)) -// .expect("Could not set serial port timeout"); - -// unsafe { WIFI_REQ_Q = Some(Queue::u8()) }; -// unsafe { WIFI_RES_Q = Some(Queue::u8()) }; - -// let (wifi_client, parser) = at::new::<_, _, _, SerialRxBufferLen, _, _>( -// unsafe { (WIFI_REQ_Q.as_mut().unwrap(), WIFI_RES_Q.as_mut().unwrap()) }, -// Serial(port), -// Timer, -// 1000.ms(), -// ); - -// let ublox = ublox_short_range::UbloxClient::new(wifi_client); - -// let at_parser_arc = Arc::new(Mutex::new(parser)); - -// let at_parser = at_parser_arc.clone(); -// let serial_irq = thread::Builder::new() -// .name("serial_irq".to_string()) -// .spawn(move || loop { -// thread::sleep(Duration::from_millis(1)); -// if let Ok(mut at) = at_parser.lock() { -// at.handle_irq() -// } -// }) -// .unwrap(); - -// let serial_loop = thread::Builder::new() -// .name("serial_loop".to_string()) -// .spawn(move || loop { -// thread::sleep(Duration::from_millis(100)); -// if let Ok(mut at) = at_parser_arc.lock() { -// at.spin() -// } -// }) -// .unwrap(); - -// let main_loop = thread::Builder::new() -// .name("main_loop".to_string()) -// .spawn(move || { -// // let networks = wifi_client.scan().unwrap(); -// // networks.iter().for_each(|n| info!("{:?}", n.ssid)); - -// let options = wifi::options::ConnectionOptions::new() -// .ssid(String::from("E-NET1")) -// .password(String::from("pakhus47")); - -// // Attempt to connect to a wifi -// let connection = ublox.connect(options).expect("Cannot connect!"); -// info!("Connected! {:?}", connection.network); -// }) -// .unwrap(); - -// // needed otherwise it does not block till -// // the threads actually have been run -// serial_irq.join().unwrap(); -// serial_loop.join().unwrap(); -// main_loop.join().unwrap(); -// } diff --git a/examples/rpi-pico/.cargo/config.toml b/examples/rpi-pico/.cargo/config.toml index 3217579..f7e22c1 100644 --- a/examples/rpi-pico/.cargo/config.toml +++ b/examples/rpi-pico/.cargo/config.toml @@ -1,6 +1,5 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -# runner = "probe-rs-cli run --chip RP2040" -runner = "probe-run --chip RP2040" +runner = "probe-rs run --chip RP2040" [build] target = "thumbv6m-none-eabi" diff --git a/examples/rpi-pico/.vscode/settings.json b/examples/rpi-pico/.vscode/settings.json new file mode 100644 index 0000000..e786a02 --- /dev/null +++ b/examples/rpi-pico/.vscode/settings.json @@ -0,0 +1,16 @@ +{ + "editor.formatOnSave": true, + "[toml]": { + "editor.formatOnSave": false + }, + "rust-analyzer.cargo.features": [ + "ppp", + ], + "rust-analyzer.cargo.target": "thumbv6m-none-eabi", + "rust-analyzer.check.allTargets": false, + "rust-analyzer.linkedProjects": [], + "rust-analyzer.server.extraEnv": { + "WIFI_NETWORK": "foo", + "WIFI_PASSWORD": "foo", + } +} \ No newline at end of file diff --git a/examples/rpi-pico/Cargo.toml b/examples/rpi-pico/Cargo.toml index 0839378..3a26943 100644 --- a/examples/rpi-pico/Cargo.toml +++ b/examples/rpi-pico/Cargo.toml @@ -5,10 +5,24 @@ edition = "2021" [dependencies] -ublox-short-range-rs = { path = "../../", features = ["odin_w2xx", "ublox-sockets", "socket-tcp"] } -embassy-executor = { version = "0.5", features = ["defmt", "integrated-timers", "nightly"] } -embassy-time = { version = "0.3", features = ["defmt", "defmt-timestamp-uptime"] } -embassy-rp = { version = "0.1.0", features = ["defmt", "unstable-pac", "time-driver"] } +ublox-short-range-rs = { path = "../../", features = ["odin-w2xx", "defmt"] } +embassy-executor = { version = "0.5", features = [ + "defmt", + "integrated-timers", + "nightly", + "arch-cortex-m", + "executor-thread", +] } +embassy-time = { version = "0.3", features = [ + "defmt", + "defmt-timestamp-uptime", +] } +embassy-sync = { version = "0.6" } +embassy-rp = { version = "0.1.0", features = [ + "defmt", + "unstable-pac", + "time-driver", +] } embassy-futures = { version = "0.1.0" } no-std-net = { version = "0.6", features = ["serde"] } @@ -19,11 +33,34 @@ panic-probe = { version = "0.3", features = ["print-defmt"] } cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } cortex-m-rt = "0.7.0" -futures = { version = "0.3.17", default-features = false, features = ["async-await", "cfg-target-has-atomic", "unstable"] } +futures = { version = "0.3.17", default-features = false, features = [ + "async-await", + "cfg-target-has-atomic", + "unstable", +] } embedded-io-async = { version = "0.6" } heapless = "0.8" +portable-atomic = { version = "*", features = ["unsafe-assume-single-core"] } +embassy-net = { version = "0.4", optional = true, features = [ + "defmt", + "proto-ipv4", + "medium-ip", + "tcp", + "udp", + "dns" +] } +embassy-net-ppp = { version = "0.1", optional = true, features = ["defmt"] } +reqwless = { git = "https://github.com/drogue-iot/reqwless", features = ["defmt"] } +smoltcp = { version = "*", default-features = false, features = ["dns-max-server-count-4"]} +rand_chacha = { version = "0.3", default-features = false } +embedded-tls = { path = "../../../embedded-tls", default-features = false, features = ["defmt"] } + + +[features] +internal-network-stack = ["ublox-short-range-rs/internal-network-stack"] +ppp = ["dep:embassy-net", "dep:embassy-net-ppp", "ublox-short-range-rs/ppp"] [patch.crates-io] # embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } @@ -34,16 +71,16 @@ heapless = "0.8" # embassy-net-driver-channel = { git = "https://github.com/embassy-rs/embassy", rev = "03d6363d5af5dcaf21b52734994a466ca593d2b6" } -embassy-executor = { path = "../../../embassy/embassy-executor" } -embassy-hal-internal = { path = "../../../embassy/embassy-hal-internal" } +embassy-rp = { path = "../../../embassy/embassy-rp" } embassy-time = { path = "../../../embassy/embassy-time" } -embassy-futures = { path = "../../../embassy/embassy-futures" } embassy-sync = { path = "../../../embassy/embassy-sync" } -embassy-rp = { path = "../../../embassy/embassy-rp" } -embassy-net-driver = { path = "../../../embassy/embassy-net-driver" } -atat = { path = "../../../atat/atat" } +embassy-net = { path = "../../../embassy/embassy-net" } +embassy-net-ppp = { path = "../../../embassy/embassy-net-ppp" } +embassy-futures = { path = "../../../embassy/embassy-futures" } +embassy-executor = { path = "../../../embassy/embassy-executor" } ublox-sockets = { path = "../../../ublox-sockets" } no-std-net = { path = "../../../no-std-net" } +atat = { path = "../../../atat/atat" } [profile.dev] debug = 2 diff --git a/examples/rpi-pico/rust-toolchain.toml b/examples/rpi-pico/rust-toolchain.toml new file mode 100644 index 0000000..4e3b270 --- /dev/null +++ b/examples/rpi-pico/rust-toolchain.toml @@ -0,0 +1,7 @@ +[toolchain] +channel = "nightly-2024-01-17" +components = [ "rust-src", "rustfmt", "llvm-tools" ] +targets = [ + "thumbv6m-none-eabi", + "thumbv7em-none-eabihf" +] diff --git a/examples/rpi-pico/src/bin/embassy-async.rs b/examples/rpi-pico/src/bin/embassy-async.rs index 7ef8b7a..81090c4 100644 --- a/examples/rpi-pico/src/bin/embassy-async.rs +++ b/examples/rpi-pico/src/bin/embassy-async.rs @@ -1,18 +1,16 @@ +#![cfg(feature = "internal-network-stack")] #![no_std] #![no_main] #![feature(type_alias_impl_trait)] #![feature(async_fn_in_trait)] #![allow(incomplete_features)] -#[path = "../common.rs"] -mod common; - use core::fmt::Write as _; use embassy_executor::Spawner; use embassy_futures::select::{select, Either}; -use embassy_rp::gpio::{Input, Level, Output, Pull}; +use embassy_rp::gpio::{AnyPin, Input, Level, Output, Pull}; use embassy_rp::peripherals::{PIN_26, UART1}; -use embassy_rp::uart::BufferedInterruptHandler; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUartTx}; use embassy_rp::{bind_interrupts, uart}; use embassy_time::{Duration, Timer}; use embedded_io_async::Write; @@ -22,25 +20,33 @@ use ublox_short_range::asynch::runner::Runner; use ublox_short_range::asynch::ublox_stack::dns::DnsSocket; use ublox_short_range::asynch::ublox_stack::tcp::TcpSocket; use ublox_short_range::asynch::ublox_stack::{StackResources, UbloxStack}; -use ublox_short_range::asynch::{new, State}; +use ublox_short_range::asynch::{new, Resources, State}; use ublox_short_range::atat::{self, AtatIngress}; use ublox_short_range::command::custom_digest::EdmDigester; use ublox_short_range::command::edm::urc::EdmEvent; use ublox_short_range::embedded_nal_async::AddrType; use {defmt_rtt as _, panic_probe as _}; -const RX_BUF_LEN: usize = 4096; -const URC_CAPACITY: usize = 3; +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 1024; +const URC_CAPACITY: usize = 2; type AtClient = ublox_short_range::atat::asynch::Client< 'static, uart::BufferedUartTx<'static, UART1>, - RX_BUF_LEN, + INGRESS_BUF_SIZE, >; #[embassy_executor::task] async fn wifi_task( - runner: Runner<'static, AtClient, Output<'static, PIN_26>, 8, URC_CAPACITY>, + runner: InternalRunner< + 'a, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + Output<'static, AnyPin>, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, ) -> ! { runner.run().await } @@ -120,14 +126,6 @@ async fn echo_task( } } -#[embassy_executor::task] -async fn ingress_task( - mut ingress: atat::Ingress<'static, EdmDigester, EdmEvent, RX_BUF_LEN, URC_CAPACITY, 2>, - mut rx: uart::BufferedUartRx<'static, UART1>, -) -> ! { - ingress.read_from(&mut rx).await -} - bind_interrupts!(struct Irqs { UART1_IRQ => BufferedInterruptHandler; }); @@ -141,46 +139,50 @@ async fn main(spawner: Spawner) { let rst = Output::new(p.PIN_26, Level::High); let mut btn = Input::new(p.PIN_27, Pull::Up); - let (tx_pin, rx_pin, rts_pin, cts_pin, uart) = - (p.PIN_24, p.PIN_25, p.PIN_23, p.PIN_22, p.UART1); + static TX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 16]> = StaticCell::new(); - let tx_buf = &mut make_static!([0u8; 64])[..]; - let rx_buf = &mut make_static!([0u8; 64])[..]; let uart = uart::BufferedUart::new_with_rtscts( - uart, + p.UART1, Irqs, - tx_pin, - rx_pin, - rts_pin, - cts_pin, - tx_buf, - rx_buf, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 16]), + RX_BUF.init([0; 16]), uart::Config::default(), ); - let (rx, tx) = uart.split(); + let (uart_rx, uart_tx) = uart.split(); + + static RESOURCES: StaticCell< + Resources, CMD_BUF_SIZE, INGRESS_BUF_SIZE, URC_CAPACITY>, + > = StaticCell::new(); + + let (net_device, mut control, runner) = ublox_short_range::asynch::new_internal( + uart_rx, + uart_tx, + RESOURCES.init(Resources::new()), + rst, + ); - let buffers = &*make_static!(atat::Buffers::new()); - let (ingress, client) = buffers.split(tx, EdmDigester::default(), atat::Config::new()); - defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); - let state = make_static!(State::new(client)); - let (net_device, mut control, runner) = new(state, &buffers.urc_channel, rst).await; + let stack = &*STACK.init(UbloxStack::new( + net_device, + STACK_RESOURCES.init(StackResources::new()), + )); - defmt::unwrap!(spawner.spawn(wifi_task(runner))); + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(wifi_task(runner)).unwrap(); control .set_hostname("Factbird-duo-wifi-test") .await .unwrap(); - // Init network stack - let stack = &*make_static!(UbloxStack::new( - net_device, - make_static!(StackResources::<4>::new()), - )); - - defmt::unwrap!(spawner.spawn(net_task(stack))); - // And now we can use it! info!("Device initialized!"); diff --git a/examples/rpi-pico/src/bin/embassy-perf.rs b/examples/rpi-pico/src/bin/embassy-perf.rs index 796fd84..630415a 100644 --- a/examples/rpi-pico/src/bin/embassy-perf.rs +++ b/examples/rpi-pico/src/bin/embassy-perf.rs @@ -4,9 +4,6 @@ #![feature(async_fn_in_trait)] #![allow(incomplete_features)] -#[path = "../common.rs"] -mod common; - use embassy_executor::Spawner; use embassy_futures::join::join; use embassy_rp::gpio::{Level, Output}; @@ -64,40 +61,57 @@ async fn main(spawner: Spawner) { let p = embassy_rp::init(Default::default()); - let rst = Output::new(p.PIN_26, Level::High); - - let (tx_pin, rx_pin, rts_pin, cts_pin, uart) = - (p.PIN_24, p.PIN_25, p.PIN_23, p.PIN_22, p.UART1); - - let tx_buf = &mut make_static!([0u8; 64])[..]; - let rx_buf = &mut make_static!([0u8; 64])[..]; - let mut config = uart::Config::default(); - config.baudrate = 115200; - let uart = uart::BufferedUart::new_with_rtscts( - uart, Irqs, tx_pin, rx_pin, rts_pin, cts_pin, tx_buf, rx_buf, config, + let rst_pin = OutputOpenDrain::new(p.PIN_26.degrade(), Level::High); + + static TX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + let wifi_uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 32]), + RX_BUF.init([0; 32]), + uart::Config::default(), ); - let (rx, tx) = uart.split(); - let buffers = &*make_static!(atat::Buffers::new()); - let (ingress, client) = buffers.split( - common::TxWrap(tx), - EdmDigester::default(), - atat::Config::new(), + static RESOURCES: StaticCell> = + StaticCell::new(); + + let mut runner = Runner::new( + wifi_uart.split(), + RESOURCES.init(Resources::new()), + WifiConfig { rst_pin }, ); - defmt::unwrap!(spawner.spawn(ingress_task(ingress, rx))); - let state = make_static!(State::new(client)); - let (net_device, mut control, runner) = new(state, &buffers.urc_channel, rst).await; + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); - defmt::unwrap!(spawner.spawn(wifi_task(runner))); + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guaranteed to be random. // Init network stack - let stack = &*make_static!(UbloxStack::new( + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( net_device, - make_static!(StackResources::<4>::new()), + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, )); - defmt::unwrap!(spawner.spawn(net_task(stack))); + static CONTROL_RESOURCES: StaticCell = StaticCell::new(); + let mut control = runner.control(CONTROL_RESOURCES.init(ControlResources::new()), &stack); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(ppp_task(runner, &stack)).unwrap(); + + stack.wait_config_up().await; + + Timer::after(Duration::from_secs(1)).await; loop { match control.join_wpa2(WIFI_NETWORK, WIFI_PASSWORD).await { diff --git a/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs new file mode 100644 index 0000000..e25d51e --- /dev/null +++ b/examples/rpi-pico/src/bin/embassy-smoltcp-ppp.rs @@ -0,0 +1,196 @@ +#![no_std] +#![no_main] +#![feature(type_alias_impl_trait)] +#![feature(impl_trait_in_assoc_type)] + +#[cfg(not(feature = "ppp"))] +compile_error!("You must enable the `ppp` feature flag to build this example"); + +use defmt::*; +use embassy_executor::Spawner; +use embassy_net::tcp::TcpSocket; +use embassy_net::{Ipv4Address, Stack, StackResources}; +use embassy_rp::gpio::{AnyPin, Level, Output, OutputOpenDrain, Pin}; +use embassy_rp::peripherals::UART1; +use embassy_rp::uart::{BufferedInterruptHandler, BufferedUart, BufferedUartRx, BufferedUartTx}; +use embassy_rp::{bind_interrupts, uart}; +use embassy_time::{Duration, Timer}; +use embedded_tls::TlsConfig; +use embedded_tls::TlsConnection; +use embedded_tls::TlsContext; +use embedded_tls::UnsecureProvider; +use embedded_tls::{Aes128GcmSha256, MaxFragmentLength}; +use rand_chacha::rand_core::SeedableRng; +use rand_chacha::ChaCha8Rng; +use reqwless::headers::ContentType; +use reqwless::request::Request; +use reqwless::request::RequestBuilder as _; +use reqwless::response::Response; +use static_cell::StaticCell; +use ublox_short_range::asynch::control::ControlResources; +use ublox_short_range::asynch::{Resources, Runner}; +use {defmt_rtt as _, panic_probe as _}; + +const CMD_BUF_SIZE: usize = 128; +const INGRESS_BUF_SIZE: usize = 512; +const URC_CAPACITY: usize = 2; + +pub struct WifiConfig { + pub rst_pin: OutputOpenDrain<'static>, +} + +impl<'a> ublox_short_range::WifiConfig<'a> for WifiConfig { + type ResetPin = OutputOpenDrain<'static>; + + const PPP_CONFIG: embassy_net_ppp::Config<'a> = embassy_net_ppp::Config { + username: b"", + password: b"", + }; + + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + Some(&mut self.rst_pin) + } +} + +#[embassy_executor::task] +async fn net_task(stack: &'static Stack>) -> ! { + stack.run().await +} + +#[embassy_executor::task] +async fn ppp_task( + mut runner: Runner< + 'static, + BufferedUartRx<'static, UART1>, + BufferedUartTx<'static, UART1>, + WifiConfig, + INGRESS_BUF_SIZE, + URC_CAPACITY, + >, + stack: &'static embassy_net::Stack>, +) -> ! { + runner.run(stack).await +} + +bind_interrupts!(struct Irqs { + UART1_IRQ => BufferedInterruptHandler; +}); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + + let rst_pin = OutputOpenDrain::new(p.PIN_26.degrade(), Level::High); + + static TX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 32]> = StaticCell::new(); + let wifi_uart = uart::BufferedUart::new_with_rtscts( + p.UART1, + Irqs, + p.PIN_24, + p.PIN_25, + p.PIN_23, + p.PIN_22, + TX_BUF.init([0; 32]), + RX_BUF.init([0; 32]), + uart::Config::default(), + ); + + static RESOURCES: StaticCell> = + StaticCell::new(); + + let mut runner = Runner::new( + wifi_uart.split(), + RESOURCES.init(Resources::new()), + WifiConfig { rst_pin }, + ); + + static PPP_STATE: StaticCell> = StaticCell::new(); + let net_device = runner.ppp_stack(PPP_STATE.init(embassy_net_ppp::State::new())); + + // Generate random seed + let seed = 0x0123_4567_89ab_cdef; // chosen by fair dice roll. guaranteed to be random. + + // Init network stack + static STACK: StaticCell>> = StaticCell::new(); + static STACK_RESOURCES: StaticCell> = StaticCell::new(); + + let stack = &*STACK.init(Stack::new( + net_device, + embassy_net::Config::default(), + STACK_RESOURCES.init(StackResources::new()), + seed, + )); + + static CONTROL_RESOURCES: StaticCell = StaticCell::new(); + let mut control = runner.control(CONTROL_RESOURCES.init(ControlResources::new()), &stack); + + spawner.spawn(net_task(stack)).unwrap(); + spawner.spawn(ppp_task(runner, &stack)).unwrap(); + + stack.wait_config_up().await; + + Timer::after(Duration::from_secs(1)).await; + + control.set_hostname("Ublox-wifi-test").await.ok(); + + control.join_wpa2("MyAccessPoint", "12345678").await; + + info!("We have network!"); + + let mut rx_buffer = [0; 4096]; + let mut tx_buffer = [0; 4096]; + let mut socket = TcpSocket::new(stack, &mut rx_buffer, &mut tx_buffer); + socket.set_timeout(Some(Duration::from_secs(10))); + + let hostname = "ecdsa-test.germancoding.com"; + + let mut remote = stack + .dns_query(hostname, smoltcp::wire::DnsQueryType::A) + .await + .unwrap(); + let remote_endpoint = (remote.pop().unwrap(), 443); + info!("connecting to {:?}...", remote_endpoint); + let r = socket.connect(remote_endpoint).await; + if let Err(e) = r { + warn!("connect error: {:?}", e); + return; + } + info!("TCP connected!"); + + let mut read_record_buffer = [0; 16384]; + let mut write_record_buffer = [0; 16384]; + let config = TlsConfig::new() + // .with_max_fragment_length(MaxFragmentLength::Bits11) + .with_server_name(hostname); + let mut tls = TlsConnection::new(socket, &mut read_record_buffer, &mut write_record_buffer); + + tls.open(TlsContext::new( + &config, + UnsecureProvider::new::(ChaCha8Rng::seed_from_u64(seed)), + )) + .await + .expect("error establishing TLS connection"); + + info!("TLS Established!"); + + let request = Request::get("/") + .host(hostname) + .content_type(ContentType::TextPlain) + .build(); + request.write(&mut tls).await.unwrap(); + + let mut rx_buf = [0; 1024]; + let mut body_buf = [0; 8192]; + let response = Response::read(&mut tls, reqwless::request::Method::GET, &mut rx_buf) + .await + .unwrap(); + let len = response + .body() + .reader() + .read_to_end(&mut body_buf) + .await + .unwrap(); + + info!("{=[u8]:a}", &body_buf[..len]); +} diff --git a/examples/rpi-pico/src/common.rs b/examples/rpi-pico/src/common.rs deleted file mode 100644 index e341bb1..0000000 --- a/examples/rpi-pico/src/common.rs +++ /dev/null @@ -1,56 +0,0 @@ -use embassy_rp::uart; -use ublox_short_range::atat; - -// pub struct TxWrap(pub TX); - -// impl embedded_io::Io for TxWrap { -// type Error = ::Error; -// } - -// impl embedded_io::asynch::Write for TxWrap { -// async fn write(&mut self, buf: &[u8]) -> Result { -// self.0.write(buf).await -// } -// } - -// impl atat::UartExt for TxWrap> { -// type Error = (); - -// fn set_baudrate(&mut self, baud: u32) -> Result<(), Self::Error> { -// let r = T::regs(); - -// let clk_base = 125_000_000; - -// let baud_rate_div = (8 * clk_base) / baud; -// let mut baud_ibrd = baud_rate_div >> 7; -// let mut baud_fbrd = ((baud_rate_div & 0x7f) + 1) / 2; - -// if baud_ibrd == 0 { -// baud_ibrd = 1; -// baud_fbrd = 0; -// } else if baud_ibrd >= 65535 { -// baud_ibrd = 65535; -// baud_fbrd = 0; -// } - -// r.uartcr().modify(|m| { -// m.set_uarten(false); -// }); - -// // Load PL011's baud divisor registers -// r.uartibrd() -// .write_value(embassy_rp::pac::uart::regs::Uartibrd(baud_ibrd)); -// r.uartfbrd() -// .write_value(embassy_rp::pac::uart::regs::Uartfbrd(baud_fbrd)); - -// // PL011 needs a (dummy) line control register write to latch in the -// // divisors. We don't want to actually change LCR contents here. -// r.uartlcr_h().modify(|_| {}); - -// r.uartcr().modify(|m| { -// m.set_uarten(true); -// }); - -// Ok(()) -// } -// } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b4220cb..1dca89f 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.75" +channel = "1.79" components = [ "rust-src", "rustfmt", "llvm-tools" ] targets = [ "thumbv6m-none-eabi", diff --git a/src/asynch/at_udp_socket.rs b/src/asynch/at_udp_socket.rs new file mode 100644 index 0000000..4428181 --- /dev/null +++ b/src/asynch/at_udp_socket.rs @@ -0,0 +1,70 @@ +use embassy_net::{udp::UdpSocket, Ipv4Address}; +use embedded_io_async::{Read, Write}; + +use crate::config::Transport; + +pub struct AtUdpSocket<'a>(pub(crate) UdpSocket<'a>); + +impl<'a> AtUdpSocket<'a> { + pub(crate) const PPP_AT_PORT: u16 = 23; +} + +impl<'a> embedded_io_async::ErrorType for &AtUdpSocket<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> Read for &AtUdpSocket<'a> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let (len, _) = self.0.recv_from(buf).await.unwrap(); + Ok(len) + } +} + +impl<'a> Write for &AtUdpSocket<'a> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.0 + .send_to( + buf, + (Ipv4Address::new(172, 30, 0, 251), AtUdpSocket::PPP_AT_PORT), + ) + .await + .unwrap(); + + Ok(buf.len()) + } +} + +impl<'a> Transport for AtUdpSocket<'a> { + fn set_baudrate(&mut self, _baudrate: u32) { + // Nothing to do here + } + + fn split_ref(&mut self) -> (impl Write, impl Read) { + (&*self, &*self) + } +} + +impl<'a> embedded_io_async::ErrorType for AtUdpSocket<'a> { + type Error = core::convert::Infallible; +} + +impl<'a> Read for AtUdpSocket<'a> { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let (len, _) = self.0.recv_from(buf).await.unwrap(); + Ok(len) + } +} + +impl<'a> Write for AtUdpSocket<'a> { + async fn write(&mut self, buf: &[u8]) -> Result { + self.0 + .send_to( + buf, + (Ipv4Address::new(172, 30, 0, 251), AtUdpSocket::PPP_AT_PORT), + ) + .await + .unwrap(); + + Ok(buf.len()) + } +} diff --git a/src/asynch/control.rs b/src/asynch/control.rs index e826bf5..8f03d79 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -1,104 +1,194 @@ -use core::future::poll_fn; -use core::task::Poll; - -use atat::asynch::AtatClient; -use embassy_time::{with_timeout, Duration}; - -use crate::command::network::SetNetworkHostName; -use crate::command::security::types::SecurityDataType; -use crate::command::security::SendSecurityDataImport; -use crate::command::wifi::types::{ - Authentication, StatusId, WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, -}; +use core::cell::Cell; +use core::str::FromStr as _; + +use atat::AtatCmd; +use atat::{asynch::AtatClient, response_slot::ResponseSlotGuard, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Sender}; +use embassy_time::{with_timeout, Duration, Timer}; +use heapless::Vec; +use no_std_net::Ipv4Addr; + +use crate::command::general::responses::SoftwareVersionResponse; +use crate::command::general::types::FirmwareVersion; +use crate::command::general::SoftwareVersion; +use crate::command::gpio::responses::ReadGPIOResponse; +use crate::command::gpio::types::GPIOMode; +use crate::command::gpio::ConfigureGPIO; +use crate::command::network::responses::NetworkStatusResponse; +use crate::command::network::types::{NetworkStatus, NetworkStatusParameter}; +use crate::command::network::GetNetworkStatus; +use crate::command::ping::Ping; +use crate::command::system::responses::LocalAddressResponse; +use crate::command::system::types::InterfaceID; +use crate::command::system::GetLocalAddress; use crate::command::wifi::{ExecWifiStationAction, GetWifiStatus, SetWifiStationConfig}; use crate::command::OnOff; +use crate::command::{ + gpio::ReadGPIO, + wifi::{ + types::{ + AccessPointAction, Authentication, SecurityMode, SecurityModePSK, StatusId, + WifiStationAction, WifiStationConfig, WifiStatus, WifiStatusVal, + }, + WifiAPAction, + }, +}; use crate::command::{ gpio::{ types::{GPIOId, GPIOValue}, WriteGPIO, }, - security::PrepareSecurityDataImport, + wifi::SetWifiAPConfig, +}; +use crate::command::{network::SetNetworkHostName, wifi::types::AccessPointConfig}; +use crate::command::{ + system::{RebootDCE, ResetToFactoryDefaults}, + wifi::types::AccessPointId, }; +use crate::connection::{DnsServers, StaticConfigV4, WiFiState}; use crate::error::Error; +use super::runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}; use super::state::LinkState; -use super::{state, AtHandle}; +use super::{state, UbloxUrc}; + +enum WifiAuthentication<'a> { + None, + Wpa2Passphrase(&'a str), + Wpa2Psk(&'a [u8; 32]), +} const CONFIG_ID: u8 = 0; -pub struct Control<'a, AT: AtatClient> { - state_ch: state::StateRunner<'a>, - at: AtHandle<'a, AT>, +pub(crate) struct ProxyClient<'a, const INGRESS_BUF_SIZE: usize> { + pub(crate) req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + pub(crate) res_slot: &'a atat::ResponseSlot, + cooldown_timer: Cell>, } -impl<'a, AT: AtatClient> Control<'a, AT> { - pub(crate) fn new(state_ch: state::StateRunner<'a>, at: AtHandle<'a, AT>) -> Self { - Self { state_ch, at } +impl<'a, const INGRESS_BUF_SIZE: usize> ProxyClient<'a, INGRESS_BUF_SIZE> { + pub fn new( + req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + res_slot: &'a atat::ResponseSlot, + ) -> Self { + Self { + req_sender, + res_slot, + cooldown_timer: Cell::new(None), + } } - pub(crate) async fn init(&mut self) -> Result<(), Error> { - debug!("Initalizing ublox control"); - // read MAC addr. - // let mut resp = self.at.send_edm(GetWifiMac).await?; - // self.state_ch.set_ethernet_address( - // hex::from_hex(resp.mac_addr.as_mut_slice()) - // .unwrap() - // .try_into() - // .unwrap(), - // ); - - // let country = countries::WORLD_WIDE_XX; - // let country_info = CountryInfo { - // country_abbrev: [country.code[0], country.code[1], 0, 0], - // country_code: [country.code[0], country.code[1], 0, 0], - // rev: if country.rev == 0 { - // -1 - // } else { - // country.rev as _ - // }, - // }; - // self.set_iovar("country", &country_info.to_bytes()).await; - - // // set country takes some time, next ioctls fail if we don't wait. - // Timer::after(Duration::from_millis(100)).await; - - // // Set antenna to chip antenna - // self.ioctl_set_u32(IOCTL_CMD_ANTDIV, 0, 0).await; - - // self.set_iovar_u32("bus:txglom", 0).await; - // Timer::after(Duration::from_millis(100)).await; - // //self.set_iovar_u32("apsta", 1).await; // this crashes, also we already did it before...?? - // //Timer::after(Duration::from_millis(100)).await; - // self.set_iovar_u32("ampdu_ba_wsize", 8).await; - // Timer::after(Duration::from_millis(100)).await; - // self.set_iovar_u32("ampdu_mpdu", 4).await; - // Timer::after(Duration::from_millis(100)).await; - // //self.set_iovar_u32("ampdu_rx_factor", 0).await; // this crashes - - // // set wifi up - // self.ioctl(ControlType::Set, IOCTL_CMD_UP, 0, &mut []).await; - - // Timer::after(Duration::from_millis(100)).await; - - // self.ioctl_set_u32(110, 0, 1).await; // SET_GMODE = auto - // self.ioctl_set_u32(142, 0, 0).await; // SET_BAND = any + async fn wait_response( + &self, + timeout: Duration, + ) -> Result, atat::Error> { + with_timeout(timeout, self.res_slot.get()) + .await + .map_err(|_| atat::Error::Timeout) + } +} - Ok(()) +impl<'a, const INGRESS_BUF_SIZE: usize> atat::asynch::AtatClient + for &ProxyClient<'a, INGRESS_BUF_SIZE> +{ + async fn send(&mut self, cmd: &Cmd) -> Result { + let mut buf = [0u8; MAX_CMD_LEN]; + let len = cmd.write(&mut buf); + + if len < 50 { + debug!( + "Sending command: {:?}", + atat::helpers::LossyStr(&buf[..len]) + ); + } else { + debug!("Sending command with long payload ({} bytes)", len); + } + + if let Some(cooldown) = self.cooldown_timer.take() { + cooldown.await + } + + // TODO: Guard against race condition! + with_timeout( + Duration::from_secs(1), + self.req_sender.send(Vec::try_from(&buf[..len]).unwrap()), + ) + .await + .map_err(|_| atat::Error::Timeout)?; + + self.cooldown_timer.set(Some(Timer::after_millis(20))); + + if !Cmd::EXPECTS_RESPONSE_CODE { + cmd.parse(Ok(&[])) + } else { + let response = self + .wait_response(Duration::from_millis(Cmd::MAX_TIMEOUT_MS.into())) + .await?; + let response: &atat::Response = &response.borrow(); + cmd.parse(response.into()) + } } +} - pub async fn set_hostname(&mut self, hostname: &str) -> Result<(), Error> { - self.at - .send_edm(SetNetworkHostName { +pub struct Control<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + state_ch: state::Runner<'a>, + at_client: ProxyClient<'a, INGRESS_BUF_SIZE>, + urc_channel: &'a UrcChannel, +} + +impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY> +{ + pub(crate) fn new( + state_ch: state::Runner<'a>, + urc_channel: &'a UrcChannel, + req_sender: Sender<'a, NoopRawMutex, Vec, 1>, + res_slot: &'a atat::ResponseSlot, + ) -> Self { + Self { + state_ch, + at_client: ProxyClient::new(req_sender, res_slot), + urc_channel: urc_channel, + } + } + + /// Set the hostname of the device + pub async fn set_hostname(&self, hostname: &str) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + (&self.at_client) + .send_retry(&SetNetworkHostName { host_name: hostname, }) .await?; Ok(()) } - async fn get_wifi_status(&mut self) -> Result { - match self - .at - .send_edm(GetWifiStatus { + /// Gets the firmware version of the device + pub async fn get_version(&self) -> Result { + self.state_ch.wait_for_initialized().await; + + let SoftwareVersionResponse { version } = + (&self.at_client).send_retry(&SoftwareVersion).await?; + Ok(version) + } + + /// Gets the MAC address of the device + pub async fn hardware_address(&mut self) -> Result<[u8; 6], Error> { + self.state_ch.wait_for_initialized().await; + + let LocalAddressResponse { mac } = (&self.at_client) + .send_retry(&GetLocalAddress { + interface_id: InterfaceID::WiFi, + }) + .await?; + + Ok(mac.to_be_bytes()[2..].try_into().unwrap()) + } + + async fn get_wifi_status(&self) -> Result { + match (&self.at_client) + .send_retry(&GetWifiStatus { status_id: StatusId::Status, }) .await? @@ -109,10 +199,93 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } } - async fn get_connected_ssid(&mut self) -> Result, Error> { - match self - .at - .send_edm(GetWifiStatus { + pub async fn wait_for_link_state(&self, link_state: LinkState) { + self.state_ch.wait_for_link_state(link_state).await + } + + pub async fn config_v4(&self) -> Result, Error> { + let NetworkStatusResponse { + status: NetworkStatus::IPv4Address(ipv4), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::IPv4Address, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv4_addr = core::str::from_utf8(ipv4.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::Gateway(gateway), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::Gateway, + }) + .await? + else { + return Err(Error::Network); + }; + + let gateway_addr = core::str::from_utf8(gateway.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::PrimaryDNS(primary), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::PrimaryDNS, + }) + .await? + else { + return Err(Error::Network); + }; + + let primary = core::str::from_utf8(primary.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + let NetworkStatusResponse { + status: NetworkStatus::SecondaryDNS(secondary), + .. + } = (&self.at_client) + .send_retry(&GetNetworkStatus { + interface_id: 0, + status: NetworkStatusParameter::SecondaryDNS, + }) + .await? + else { + return Err(Error::Network); + }; + + let secondary = core::str::from_utf8(secondary.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .and_then(|ip| (!ip.is_unspecified()).then_some(ip)); + + Ok(ipv4_addr.map(|address| StaticConfigV4 { + address, + gateway: gateway_addr, + dns_servers: DnsServers { primary, secondary }, + })) + } + + pub async fn get_connected_ssid(&self) -> Result, Error> { + match (&self.at_client) + .send_retry(&GetWifiStatus { status_id: StatusId::SSID, }) .await? @@ -123,81 +296,186 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } } - pub async fn join_open(&mut self, ssid: &str) -> Result<(), Error> { - if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { - // Wifi already connected. Check if the SSID is the same - let current_ssid = self.get_connected_ssid().await?; - if current_ssid.as_str() == ssid { - return Ok(()); - } else { - self.disconnect().await?; - }; - } + pub async fn factory_reset(&self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; - self.at - .send_edm(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), + (&self.at_client) + .send_retry(&ResetToFactoryDefaults) + .await?; + (&self.at_client).send_retry(&RebootDCE).await?; + + Ok(()) + } + + async fn start_ap(&self, ssid: &str) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + + // Deactivate network id 0 + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Deactivate, }) .await?; - self.at - .send_edm(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::SSID( + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Reset, + }) + .await?; + + // // Disable DHCP Server (static IP address will be used) + // if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::IPv4Mode(IPv4Mode::Static), + // }) + // .await?; + // } + + // // Network IP address + // if let Some(ip) = options.ip { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::IPv4Address(ip), + // }) + // .await?; + // } + // // Network Subnet mask + // if let Some(subnet) = options.subnet { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::SubnetMask(subnet), + // }) + // .await?; + // } + // // Network Default gateway + // if let Some(gateway) = options.gateway { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::DefaultGateway(gateway), + // }) + // .await?; + // } + + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::DHCPServer(true.into()), + // }) + // .await?; + + // Wifi part + // Set the Network SSID to connect to + (&self.at_client) + .send_retry(&SetWifiAPConfig { + ap_config_id: AccessPointId::Id0, + ap_config_param: AccessPointConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, ), }) .await?; - self.at - .send_edm(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::Authentication(Authentication::Open), + // if let Some(pass) = options.password.clone() { + // // Use WPA2 as authentication type + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::SecurityMode( + // SecurityMode::Wpa2AesCcmp, + // SecurityModePSK::PSK, + // ), + // }) + // .await?; + + // // Input passphrase + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::PSKPassphrase(PasskeyR::Passphrase(pass)), + // }) + // .await?; + // } else { + (&self.at_client) + .send_retry(&SetWifiAPConfig { + ap_config_id: AccessPointId::Id0, + ap_config_param: AccessPointConfig::SecurityMode( + SecurityMode::Open, + SecurityModePSK::Open, + ), }) .await?; - - self.at - .send_edm(ExecWifiStationAction { - config_id: CONFIG_ID, - action: WifiStationAction::Activate, + // } + + // if let Some(channel) = configuration.channel { + // (&self.at_client) + // .send_retry(&SetWifiAPConfig { + // ap_config_id: AccessPointId::Id0, + // ap_config_param: AccessPointConfig::Channel(channel as u8), + // }) + // .await?; + // } + + (&self.at_client) + .send_retry(&WifiAPAction { + ap_config_id: AccessPointId::Id0, + ap_action: AccessPointAction::Activate, }) .await?; - with_timeout(Duration::from_secs(10), self.wait_for_join(ssid)) - .await - .map_err(|_| Error::Timeout)??; - Ok(()) } - pub async fn join_wpa2(&mut self, ssid: &str, passphrase: &str) -> Result<(), Error> { + /// Start open access point. + pub async fn start_ap_open(&mut self, ssid: &str, channel: u8) { + todo!() + } + + /// Start WPA2 protected access point. + pub async fn start_ap_wpa2(&mut self, ssid: &str, passphrase: &str, channel: u8) { + todo!() + } + + /// Closes access point. + pub async fn close_ap(&self) -> Result<(), Error> { + todo!() + } + + async fn join_sta(&self, ssid: &str, auth: WifiAuthentication<'_>) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + if matches!(self.get_wifi_status().await?, WifiStatusVal::Connected) { // Wifi already connected. Check if the SSID is the same let current_ssid = self.get_connected_ssid().await?; if current_ssid.as_str() == ssid { + self.state_ch.set_should_connect(true); return Ok(()); } else { - self.disconnect().await?; + self.leave().await?; }; } - self.at - .send_edm(ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Reset, }) .await?; - self.at - .send_edm(SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::ActiveOnStartup(OnOff::Off), }) .await?; - self.at - .send_edm(SetWifiStationConfig { + (&self.at_client) + .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, config_param: WifiStationConfig::SSID( heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, @@ -205,42 +483,88 @@ impl<'a, AT: AtatClient> Control<'a, AT> { }) .await?; - self.at - .send_edm(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }) - .await?; + match auth { + WifiAuthentication::None => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::Open), + }) + .await?; + } + WifiAuthentication::Wpa2Passphrase(passphrase) => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + }) + .await?; - self.at - .send_edm(SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::WpaPskOrPassphrase( - heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, - ), - }) - .await?; + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::WpaPskOrPassphrase( + heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, + ), + }) + .await?; + } + WifiAuthentication::Wpa2Psk(psk) => { + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + }) + .await?; + + (&self.at_client) + .send_retry(&SetWifiStationConfig { + config_id: CONFIG_ID, + config_param: WifiStationConfig::WpaPskOrPassphrase(todo!("hex values?!")), + }) + .await?; + } + } - self.at - .send_edm(ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Activate, }) .await?; - with_timeout(Duration::from_secs(20), self.wait_for_join(ssid)) - .await - .map_err(|_| Error::Timeout)??; + self.wait_for_join(ssid, Duration::from_secs(20)).await?; + self.state_ch.set_should_connect(true); Ok(()) } - pub async fn disconnect(&mut self) -> Result<(), Error> { + /// Join an unprotected network with the provided ssid. + pub async fn join_open(&self, ssid: &str) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::None).await + } + + /// Join a protected network with the provided ssid and passphrase. + pub async fn join_wpa2(&self, ssid: &str, passphrase: &str) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::Wpa2Passphrase(passphrase)) + .await + } + + /// Join a protected network with the provided ssid and precomputed PSK. + pub async fn join_wpa2_psk(&mut self, ssid: &str, psk: &[u8; 32]) -> Result<(), Error> { + self.join_sta(ssid, WifiAuthentication::Wpa2Psk(psk)).await + } + + /// Leave the wifi, with which we are currently associated. + pub async fn leave(&self) -> Result<(), Error> { + self.state_ch.wait_for_initialized().await; + self.state_ch.set_should_connect(false); + match self.get_wifi_status().await? { WifiStatusVal::Disabled => {} WifiStatusVal::Disconnected | WifiStatusVal::Connected => { - self.at - .send_edm(ExecWifiStationAction { + (&self.at_client) + .send_retry(&ExecWifiStationAction { config_id: CONFIG_ID, action: WifiStationAction::Deactivate, }) @@ -248,71 +572,136 @@ impl<'a, AT: AtatClient> Control<'a, AT> { } } - let wait_for_disconnect = poll_fn(|cx| match self.state_ch.link_state(cx) { - LinkState::Up => Poll::Pending, - LinkState::Down => Poll::Ready(()), - }); - - with_timeout(Duration::from_secs(10), wait_for_disconnect) - .await - .map_err(|_| Error::Timeout)?; + with_timeout( + Duration::from_secs(10), + self.state_ch.wait_connection_down(), + ) + .await + .map_err(|_| Error::Timeout)?; Ok(()) } - async fn wait_for_join(&mut self, ssid: &str) -> Result<(), Error> { - poll_fn(|cx| match self.state_ch.link_state(cx) { - LinkState::Down => Poll::Pending, - LinkState::Up => Poll::Ready(()), - }) - .await; + pub async fn wait_for_join(&self, ssid: &str, timeout: Duration) -> Result<(), Error> { + match with_timeout(timeout, self.state_ch.wait_for_link_state(LinkState::Up)).await { + Ok(_) => { + // Check that SSID matches + let current_ssid = self.get_connected_ssid().await?; + if ssid != current_ssid.as_str() { + return Err(Error::Network); + } - // Check that SSID matches - let current_ssid = self.get_connected_ssid().await?; - if ssid != current_ssid.as_str() { - return Err(Error::Network); + Ok(()) + } + Err(_) if self.state_ch.wifi_state(None) == WiFiState::SecurityProblems => { + let _ = (&self.at_client) + .send_retry(&ExecWifiStationAction { + config_id: CONFIG_ID, + action: WifiStationAction::Deactivate, + }) + .await; + Err(Error::SecurityProblems) + } + Err(_) => Err(Error::Timeout), } + } + + // /// Start a wifi scan + // /// + // /// Returns a `Stream` of networks found by the device + // /// + // /// # Note + // /// Device events are currently implemented using a bounded queue. + // /// To not miss any events, you should make sure to always await the stream. + // pub async fn scan(&mut self, scan_opts: ScanOptions) -> Scanner<'_> { + // todo!() + // } + + pub async fn send_at(&self, cmd: &Cmd) -> Result { + self.state_ch.wait_for_initialized().await; + Ok((&self.at_client).send_retry(cmd).await?) + } + pub async fn gpio_configure(&self, id: GPIOId, mode: GPIOMode) -> Result<(), Error> { + self.send_at(&ConfigureGPIO { id, mode }).await?; Ok(()) } - pub async fn gpio_set(&mut self, id: GPIOId, value: GPIOValue) -> Result<(), Error> { - self.at.send_edm(WriteGPIO { id, value }).await?; + pub async fn gpio_set(&self, id: GPIOId, value: bool) -> Result<(), Error> { + let value = if value { + GPIOValue::High + } else { + GPIOValue::Low + }; + + self.send_at(&WriteGPIO { id, value }).await?; Ok(()) } - // FIXME: This could probably be improved - pub async fn import_credentials( - &mut self, - data_type: SecurityDataType, - name: &str, - data: &[u8], - md5_sum: Option<&str>, - ) -> Result<(), atat::Error> { - assert!(name.len() < 16); - - info!("Importing {:?} bytes as {:?}", data.len(), name); - - self.at - .send_edm(PrepareSecurityDataImport { - data_type, - data_size: data.len(), - internal_name: name, - password: None, - }) - .await?; + pub async fn gpio_get(&self, id: GPIOId) -> Result { + let ReadGPIOResponse { value, .. } = self.send_at(&ReadGPIO { id }).await?; + Ok(value as u8 != 0) + } - let import_data = self - .at - .send_edm(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(data), - }) - .await?; + #[cfg(feature = "ppp")] + pub async fn ping( + &self, + hostname: &str, + ) -> Result { + let mut urc_sub = self.urc_channel.subscribe().map_err(|_| Error::Overflow)?; - if let Some(hash) = md5_sum { - assert_eq!(import_data.md5_string.as_str(), hash); - } + self.send_at(&Ping { + hostname, + retry_num: 1, + }) + .await?; + + let result_fut = async { + loop { + match urc_sub.next_message_pure().await { + crate::command::Urc::PingResponse(r) => return Ok(r), + crate::command::Urc::PingErrorResponse(e) => return Err(Error::Dns(e.error)), + _ => {} + } + } + }; - Ok(()) + with_timeout(Duration::from_secs(15), result_fut).await? } + + // FIXME: This could probably be improved + // #[cfg(feature = "internal-network-stack")] + // pub async fn import_credentials( + // &mut self, + // data_type: SecurityDataType, + // name: &str, + // data: &[u8], + // md5_sum: Option<&str>, + // ) -> Result<(), atat::Error> { + // assert!(name.len() < 16); + + // info!("Importing {:?} bytes as {:?}", data.len(), name); + + // (&self.at_client) + // .send_retry(&PrepareSecurityDataImport { + // data_type, + // data_size: data.len(), + // internal_name: name, + // password: None, + // }) + // .await?; + + // let import_data = self + // .at_client + // .send_retry(&SendSecurityDataImport { + // data: atat::serde_bytes::Bytes::new(data), + // }) + // .await?; + + // if let Some(hash) = md5_sum { + // assert_eq!(import_data.md5_string.as_str(), hash); + // } + + // Ok(()) + // } } diff --git a/src/asynch/mod.rs b/src/asynch/mod.rs index 2e6776b..afb3f0f 100644 --- a/src/asynch/mod.rs +++ b/src/asynch/mod.rs @@ -1,78 +1,20 @@ +#[cfg(feature = "ppp")] +mod at_udp_socket; pub mod control; +pub mod network; +mod resources; pub mod runner; -#[cfg(feature = "ublox-sockets")] +#[cfg(feature = "internal-network-stack")] pub mod ublox_stack; pub(crate) mod state; -use crate::command::edm::{urc::EdmEvent, EdmAtCmdWrapper}; -use atat::asynch::AtatClient; -use embassy_sync::{blocking_mutex::raw::NoopRawMutex, mutex::Mutex}; -use embedded_hal::digital::OutputPin; -use runner::Runner; -use state::Device; +pub use resources::Resources; +pub use runner::Runner; +pub use state::LinkState; -use self::control::Control; +#[cfg(feature = "edm")] +pub type UbloxUrc = crate::command::edm::urc::EdmEvent; -// NOTE: Must be pow(2) due to internal usage of `FnvIndexMap` -const MAX_CONNS: usize = 8; - -pub struct AtHandle<'d, AT: AtatClient>(&'d Mutex); - -impl<'d, AT: AtatClient> AtHandle<'d, AT> { - async fn send_edm( - &mut self, - cmd: Cmd, - ) -> Result { - self.send(EdmAtCmdWrapper(cmd)).await - } - - async fn send(&mut self, cmd: Cmd) -> Result { - self.0.lock().await.send_retry::(&cmd).await - } -} - -pub struct State { - ch: state::State, - at_handle: Mutex, -} - -impl State { - pub fn new(at_handle: AT) -> Self { - Self { - ch: state::State::new(), - at_handle: Mutex::new(at_handle), - } - } -} - -pub async fn new<'a, AT: AtatClient, RST: OutputPin, const URC_CAPACITY: usize>( - state: &'a mut State, - subscriber: &'a atat::UrcChannel, - reset: RST, -) -> ( - Device<'a, AT, URC_CAPACITY>, - Control<'a, AT>, - Runner<'a, AT, RST, MAX_CONNS, URC_CAPACITY>, -) { - let (ch_runner, net_device) = state::new( - &mut state.ch, - AtHandle(&state.at_handle), - subscriber.subscribe().unwrap(), - ); - let state_ch = ch_runner.state_runner(); - - let mut runner = Runner::new( - ch_runner, - AtHandle(&state.at_handle), - reset, - subscriber.subscribe().unwrap(), - ); - - runner.init().await.unwrap(); - - let mut control = Control::new(state_ch, AtHandle(&state.at_handle)); - control.init().await.unwrap(); - - (net_device, control, runner) -} +#[cfg(not(feature = "edm"))] +pub type UbloxUrc = crate::command::Urc; diff --git a/src/asynch/network.rs b/src/asynch/network.rs new file mode 100644 index 0000000..79532b0 --- /dev/null +++ b/src/asynch/network.rs @@ -0,0 +1,321 @@ +use core::str::FromStr as _; + +use atat::{asynch::AtatClient, UrcChannel, UrcSubscription}; +use embassy_time::{with_timeout, Duration, Timer}; +use embedded_hal::digital::OutputPin as _; +use no_std_net::{Ipv4Addr, Ipv6Addr}; + +use crate::{ + command::{ + network::{ + responses::NetworkStatusResponse, + types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, + urc::{NetworkDown, NetworkUp}, + GetNetworkStatus, + }, + system::{RebootDCE, StoreCurrentConfig}, + wifi::{ + types::DisconnectReason, + urc::{WifiLinkConnected, WifiLinkDisconnected}, + }, + Urc, + }, + connection::WiFiState, + error::Error, + network::WifiNetwork, + WifiConfig, +}; + +use super::{runner::URC_SUBSCRIBERS, state, UbloxUrc}; + +pub(crate) struct NetDevice<'a, 'b, C, A, const URC_CAPACITY: usize> { + ch: &'b state::Runner<'a>, + config: &'b mut C, + at_client: A, + urc_subscription: UrcSubscription<'a, UbloxUrc, URC_CAPACITY, { URC_SUBSCRIBERS }>, +} + +impl<'a, 'b, C, A, const URC_CAPACITY: usize> NetDevice<'a, 'b, C, A, URC_CAPACITY> +where + C: WifiConfig<'a>, + A: AtatClient, +{ + pub fn new( + ch: &'b state::Runner<'a>, + config: &'b mut C, + at_client: A, + urc_channel: &'a UrcChannel, + ) -> Self { + Self { + ch, + config, + at_client, + urc_subscription: urc_channel.subscribe().unwrap(), + } + } + + pub async fn run(&mut self) -> Result<(), Error> { + loop { + match embassy_futures::select::select( + self.urc_subscription.next_message_pure(), + self.ch.wait_for_wifi_state_change(), + ) + .await + { + embassy_futures::select::Either::First(event) => { + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + self.handle_urc(event).await?; + } + _ => {} + } + + if self.ch.wifi_state(None) == WiFiState::Inactive && self.ch.connection_down(None) { + return Ok(()); + } + } + } + + async fn handle_urc(&mut self, event: Urc) -> Result<(), Error> { + match event { + Urc::StartUp => { + error!("AT startup event?! Device restarted unintentionally!"); + } + Urc::WifiLinkConnected(WifiLinkConnected { + connection_id: _, + bssid, + channel, + }) => self.ch.update_connection_with(|con| { + con.wifi_state = WiFiState::Connected; + con.network + .replace(WifiNetwork::new_station(bssid, channel)); + }), + Urc::WifiLinkDisconnected(WifiLinkDisconnected { reason, .. }) => { + self.ch.update_connection_with(|con| { + con.wifi_state = match reason { + DisconnectReason::NetworkDisabled => { + con.network.take(); + warn!("Wifi network disabled!"); + WiFiState::Inactive + } + DisconnectReason::SecurityProblems => { + error!("Wifi Security Problems"); + WiFiState::SecurityProblems + } + _ => WiFiState::NotConnected, + } + }) + } + Urc::WifiAPUp(_) => warn!("Not yet implemented [WifiAPUp]"), + Urc::WifiAPDown(_) => warn!("Not yet implemented [WifiAPDown]"), + Urc::WifiAPStationConnected(_) => warn!("Not yet implemented [WifiAPStationConnected]"), + Urc::WifiAPStationDisconnected(_) => { + warn!("Not yet implemented [WifiAPStationDisconnected]") + } + Urc::EthernetLinkUp(_) => warn!("Not yet implemented [EthernetLinkUp]"), + Urc::EthernetLinkDown(_) => warn!("Not yet implemented [EthernetLinkDown]"), + Urc::NetworkUp(NetworkUp { interface_id }) => { + self.network_status_callback(interface_id).await?; + } + Urc::NetworkDown(NetworkDown { interface_id }) => { + self.network_status_callback(interface_id).await?; + } + Urc::NetworkError(_) => warn!("Not yet implemented [NetworkError]"), + _ => {} + } + + Ok(()) + } + + async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { + // Normally a check for this interface type being + // `InterfaceType::WifiStation`` should be made but there is a bug in + // uConnect which gives the type `InterfaceType::Unknown` when the + // credentials have been restored from persistent memory. This although + // the wifi station has been started. So we assume that this type is + // also ok. + let NetworkStatusResponse { + status: + NetworkStatus::InterfaceType(InterfaceType::WifiStation | InterfaceType::Unknown), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::InterfaceType, + }) + .await? + else { + return Err(Error::Network); + }; + + let NetworkStatusResponse { + status: NetworkStatus::IPv4Address(ipv4), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv4Address, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv4_up = core::str::from_utf8(ipv4.as_slice()) + .ok() + .and_then(|s| Ipv4Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + #[cfg(feature = "ipv6")] + let ipv6_up = { + let NetworkStatusResponse { + status: NetworkStatus::IPv6Address1(ipv6), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6Address1, + }) + .await? + else { + return Err(Error::Network); + }; + + core::str::from_utf8(ipv6.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default() + }; + + let NetworkStatusResponse { + status: NetworkStatus::IPv6LinkLocalAddress(ipv6_link_local), + .. + } = self + .at_client + .send_retry(&GetNetworkStatus { + interface_id, + status: NetworkStatusParameter::IPv6LinkLocalAddress, + }) + .await? + else { + return Err(Error::Network); + }; + + let ipv6_link_local_up = core::str::from_utf8(ipv6_link_local.as_slice()) + .ok() + .and_then(|s| Ipv6Addr::from_str(s).ok()) + .map(|ip| !ip.is_unspecified()) + .unwrap_or_default(); + + // Use `ipv4_addr` & `ipv6_addr` to determine link state + self.ch.update_connection_with(|con| { + con.ipv6_link_local_up = ipv6_link_local_up; + con.ipv4_up = ipv4_up; + + #[cfg(feature = "ipv6")] + { + con.ipv6_up = ipv6_up + } + }); + + Ok(()) + } + + async fn wait_startup(&mut self, timeout: Duration) -> Result<(), Error> { + let fut = async { + loop { + let event = self.urc_subscription.next_message_pure().await; + + #[cfg(feature = "edm")] + let Some(event) = event.extract_urc() else { + continue; + }; + + if let Urc::StartUp = event { + return; + } + } + }; + + with_timeout(timeout, fut).await.map_err(|_| Error::Timeout) + } + + pub async fn reset(&mut self) -> Result<(), Error> { + if let Some(reset_pin) = self.config.reset_pin() { + warn!("Reset pin found! Hard resetting Ublox Short Range"); + reset_pin.set_low().ok(); + Timer::after(Duration::from_millis(100)).await; + reset_pin.set_high().ok(); + } else { + warn!("No reset pin found! Soft resetting Ublox Short Range"); + self.at_client.send_retry(&RebootDCE).await?; + } + + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; + + #[cfg(feature = "edm")] + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } + + #[allow(dead_code)] + pub async fn restart(&mut self, store: bool) -> Result<(), Error> { + warn!("Soft resetting Ublox Short Range"); + if store { + self.at_client.send_retry(&StoreCurrentConfig).await?; + } + + self.at_client.send_retry(&RebootDCE).await?; + + self.ch.mark_uninitialized(); + + self.wait_startup(Duration::from_secs(5)).await?; + + info!("Module started again"); + #[cfg(feature = "edm")] + self.enter_edm(Duration::from_secs(4)).await?; + + Ok(()) + } + + #[cfg(feature = "edm")] + pub async fn enter_edm(&mut self, timeout: Duration) -> Result<(), Error> { + info!("Entering EDM mode"); + + // Switch to EDM on Init. If in EDM, fail and check with autosense + let fut = async { + loop { + // Ignore AT results until we are successful in EDM mode + if let Ok(_) = self + .at_client + .send_retry(&crate::command::edm::SwitchToEdmCommand) + .await + { + // After executing the data mode command or the extended data + // mode command, a delay of 50 ms is required before start of + // data transmission. + Timer::after(Duration::from_millis(50)).await; + break; + } + Timer::after(Duration::from_millis(10)).await; + } + }; + + with_timeout(timeout, fut) + .await + .map_err(|_| Error::Timeout)?; + + Ok(()) + } +} diff --git a/src/asynch/resources.rs b/src/asynch/resources.rs new file mode 100644 index 0000000..20db742 --- /dev/null +++ b/src/asynch/resources.rs @@ -0,0 +1,39 @@ +use atat::{ResponseSlot, UrcChannel}; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; + +use super::{ + runner::{MAX_CMD_LEN, URC_SUBSCRIBERS}, + state, UbloxUrc, +}; + +pub struct Resources { + pub(crate) ch: state::State, + + pub(crate) res_slot: ResponseSlot, + pub(crate) req_slot: Channel, 1>, + pub(crate) urc_channel: UrcChannel, + pub(crate) ingress_buf: [u8; INGRESS_BUF_SIZE], +} + +impl Default + for Resources +{ + fn default() -> Self { + Self::new() + } +} + +impl + Resources +{ + pub fn new() -> Self { + Self { + ch: state::State::new(), + + res_slot: ResponseSlot::new(), + req_slot: Channel::new(), + urc_channel: UrcChannel::new(), + ingress_buf: [0; INGRESS_BUF_SIZE], + } + } +} diff --git a/src/asynch/runner.rs b/src/asynch/runner.rs index bab1368..5153b77 100644 --- a/src/asynch/runner.rs +++ b/src/asynch/runner.rs @@ -1,358 +1,486 @@ -use core::str::FromStr; - -use super::state::{self, LinkState}; +use super::{control::Control, network::NetDevice, state, Resources, UbloxUrc}; use crate::{ + asynch::control::ProxyClient, command::{ - edm::{urc::EdmEvent, SwitchToEdmCommand}, + data_mode::{self, ChangeMode}, general::SoftwareVersion, - network::{ - responses::NetworkStatusResponse, - types::{InterfaceType, NetworkStatus, NetworkStatusParameter}, - urc::{NetworkDown, NetworkUp}, - GetNetworkStatus, - }, system::{ types::{BaudRate, ChangeAfterConfirm, EchoOn, FlowControl, Parity, StopBits}, - RebootDCE, SetEcho, SetRS232Settings, StoreCurrentConfig, + SetEcho, SetRS232Settings, }, wifi::{ - types::DisconnectReason, - urc::{WifiLinkConnected, WifiLinkDisconnected}, + types::{PowerSaveMode, WifiConfig as WifiConfigParam}, + SetWifiConfig, }, - Urc, + OnOff, AT, }, - connection::{WiFiState, WifiConnection}, + config::Transport, error::Error, - network::WifiNetwork, + WifiConfig, DEFAULT_BAUD_RATE, +}; +use atat::{ + asynch::{AtatClient as _, SimpleClient}, + AtatIngress as _, UrcChannel, }; -use atat::{asynch::AtatClient, UrcSubscription}; -use embassy_time::{with_timeout, Duration, Timer}; -use embedded_hal::digital::OutputPin; -use no_std_net::{Ipv4Addr, Ipv6Addr}; +use embassy_futures::select::Either; +use embassy_sync::{blocking_mutex::raw::NoopRawMutex, channel::Channel}; +use embassy_time::{Duration, Timer}; +use embedded_io_async::{BufRead, Write}; + +#[cfg(feature = "ppp")] +pub(crate) const URC_SUBSCRIBERS: usize = 2; +#[cfg(feature = "ppp")] +type Digester = atat::AtDigester; + +#[cfg(feature = "internal-network-stack")] +pub(crate) const URC_SUBSCRIBERS: usize = 3; +#[cfg(feature = "internal-network-stack")] +type Digester = crate::command::custom_digest::EdmDigester; + +pub(crate) const MAX_CMD_LEN: usize = 256; + +async fn at_bridge<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize>( + transport: &mut impl Transport, + req_slot: &Channel, 1>, + ingress: &mut atat::Ingress< + 'a, + Digester, + UbloxUrc, + INGRESS_BUF_SIZE, + URC_CAPACITY, + { URC_SUBSCRIBERS }, + >, +) -> ! { + ingress.clear(); + + let (mut tx, rx) = transport.split_ref(); + + let tx_fut = async { + loop { + let msg = req_slot.receive().await; + let _ = tx.write_all(&msg).await; + } + }; -use super::AtHandle; + embassy_futures::join::join(tx_fut, ingress.read_from(rx)).await; + + unreachable!() +} /// Background runner for the Ublox Module. /// /// You must call `.run()` in a background task for the Ublox Module to operate. -pub struct Runner< - 'd, - AT: AtatClient, - RST: OutputPin, - const MAX_CONNS: usize, - const URC_CAPACITY: usize, -> { - ch: state::Runner<'d>, - at: AtHandle<'d, AT>, - reset: RST, - wifi_connection: Option, - // connections: FnvIndexMap, - urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, +pub struct Runner<'a, T: Transport, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + transport: T, + + ch: state::Runner<'a>, + config: C, + + pub urc_channel: &'a UrcChannel, + + pub ingress: + atat::Ingress<'a, Digester, UbloxUrc, INGRESS_BUF_SIZE, URC_CAPACITY, { URC_SUBSCRIBERS }>, + pub res_slot: &'a atat::ResponseSlot, + pub req_slot: &'a Channel, 1>, + + #[cfg(feature = "ppp")] + ppp_runner: Option>, } -impl< - 'd, - AT: AtatClient, - // AT: AtatClient + atat::UartExt, - RST: OutputPin, - const MAX_CONNS: usize, - const URC_CAPACITY: usize, - > Runner<'d, AT, RST, MAX_CONNS, URC_CAPACITY> +impl<'a, T, C, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> + Runner<'a, T, C, INGRESS_BUF_SIZE, URC_CAPACITY> +where + T: Transport + BufRead, + C: WifiConfig<'a> + 'a, { - pub(crate) fn new( - ch: state::Runner<'d>, - at: AtHandle<'d, AT>, - reset: RST, - urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, - ) -> Self { - Self { - ch, - at, - reset, - wifi_connection: None, - urc_subscription, - // connections: IndexMap::new(), - } + pub fn new( + transport: T, + resources: &'a mut Resources, + config: C, + ) -> (Self, Control<'a, INGRESS_BUF_SIZE, URC_CAPACITY>) { + let ch_runner = state::Runner::new(&mut resources.ch); + + let ingress = atat::Ingress::new( + Digester::new(), + &mut resources.ingress_buf, + &resources.res_slot, + &resources.urc_channel, + ); + + let control = Control::new( + ch_runner.clone(), + &resources.urc_channel, + resources.req_slot.sender(), + &resources.res_slot, + ); + + ( + Self { + transport, + + ch: ch_runner, + config, + urc_channel: &resources.urc_channel, + + ingress, + res_slot: &resources.res_slot, + req_slot: &resources.req_slot, + + #[cfg(feature = "ppp")] + ppp_runner: None, + }, + control, + ) } - pub(crate) async fn init(&mut self) -> Result<(), Error> { - // Initilize a new ublox device to a known state (set RS232 settings) - debug!("Initializing module"); - // Hard reset module - self.reset().await?; - - // ## 2.2.6.1 AT request serial settings (EDM mode) - // - // The AT+UMRS command to change serial settings does not work exactly - // the same as in command mode. When executed in the extended data mode, - // it is not possible to change the settings directly using the - // parameter. Instead, the - // parameter must be set to 0 and the serial settings will take effect - // when the module is reset. - let baud_rate = BaudRate::B115200; - self.at - .send_edm(SetRS232Settings { - baud_rate, - flow_control: FlowControl::On, - data_bits: 8, - stop_bits: StopBits::One, - parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::StoreAndReset, - }) - .await?; - - self.restart(true).await?; - - self.at.send_edm(SoftwareVersion).await?; - - // Move to control - // if let Some(size) = self.config.tls_in_buffer_size { - // self.at - // .send_edm(SetPeerConfiguration { - // parameter: PeerConfigParameter::TlsInBuffer(size), - // }) - // .await?; - // } - - // if let Some(size) = self.config.tls_out_buffer_size { - // self.at - // .send_edm(SetPeerConfiguration { - // parameter: PeerConfigParameter::TlsOutBuffer(size), - // }) - // .await?; - // } + #[cfg(feature = "ppp")] + pub fn ppp_stack<'d: 'a, const N_RX: usize, const N_TX: usize>( + &mut self, + ppp_state: &'d mut embassy_net_ppp::State, + ) -> embassy_net_ppp::Device<'d> { + let (net_device, ppp_runner) = embassy_net_ppp::new(ppp_state); + self.ppp_runner.replace(ppp_runner); + net_device + } - Ok(()) + #[cfg(feature = "internal-network-stack")] + pub fn internal_stack( + &mut self, + ) -> super::ublox_stack::Device<'a, INGRESS_BUF_SIZE, URC_CAPACITY> { + super::ublox_stack::Device { + state_ch: self.ch.clone(), + at_client: core::cell::RefCell::new(ProxyClient::new( + self.req_slot.sender(), + &self.res_slot, + )), + urc_channel: &self.urc_channel, + } } - async fn wait_startup(&mut self, timeout: Duration) -> Result<(), Error> { - let fut = async { - loop { - match self.urc_subscription.next_message_pure().await { - EdmEvent::ATEvent(Urc::StartUp) => return, - _ => {} - } - } - }; + /// Probe a given baudrate with the goal of establishing initial + /// communication with the module, so we can reconfigure it for desired + /// baudrate + async fn probe_baud(&mut self, baudrate: BaudRate) -> Result<(), Error> { + info!("Probing wifi module using baud rate: {}", baudrate as u32); + self.transport.set_baudrate(baudrate as u32); - with_timeout(timeout, fut).await.map_err(|_| Error::Timeout) - } + let baud_fut = async { + let at_client = ProxyClient::new(self.req_slot.sender(), self.res_slot); - pub async fn reset(&mut self) -> Result<(), Error> { - warn!("Hard resetting Ublox Short Range"); - self.reset.set_low().ok(); - Timer::after(Duration::from_millis(100)).await; - self.reset.set_high().ok(); + // Hard reset module + NetDevice::new(&self.ch, &mut self.config, &at_client, self.urc_channel) + .reset() + .await?; - self.wait_startup(Duration::from_secs(4)).await?; + (&at_client).send_retry(&AT).await?; - self.enter_edm(Duration::from_secs(4)).await?; + // Lets take a shortcut if we are probing for the desired baudrate + if baudrate == C::BAUD_RATE { + info!("Successfully shortcut the baud probing!"); + return Ok(None); + } - Ok(()) - } + let flow_control = if C::FLOW_CONTROL { + FlowControl::On + } else { + FlowControl::Off + }; + + (&at_client) + .send_retry(&SetRS232Settings { + baud_rate: C::BAUD_RATE, + flow_control, + data_bits: 8, + stop_bits: StopBits::One, + parity: Parity::None, + change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, + }) + .await?; + + Ok::<_, Error>(Some(C::BAUD_RATE)) + }; - pub async fn restart(&mut self, store: bool) -> Result<(), Error> { - warn!("Soft resetting Ublox Short Range"); - if store { - self.at.send_edm(StoreCurrentConfig).await?; + match embassy_futures::select::select( + baud_fut, + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await + { + Either::First(Ok(Some(baud))) => { + self.transport.set_baudrate(baud as u32); + Timer::after_millis(40).await; + Ok(()) + } + Either::First(r) => r.map(drop), + Either::Second(_) => unreachable!(), } + } - self.at.send_edm(RebootDCE).await?; + async fn init(&mut self) -> Result<(), Error> { + // Initialize a new ublox device to a known state + debug!("Initializing WiFi module"); - self.wait_startup(Duration::from_secs(10)).await?; + // Probe all possible baudrates with the goal of establishing initial + // communication with the module, so we can reconfigure it for desired + // baudrate. + // + // Start with the two most likely + let mut found_baudrate = false; + + for baudrate in [ + C::BAUD_RATE, + DEFAULT_BAUD_RATE, + BaudRate::B9600, + BaudRate::B14400, + BaudRate::B19200, + BaudRate::B28800, + BaudRate::B38400, + BaudRate::B57600, + BaudRate::B76800, + BaudRate::B115200, + BaudRate::B230400, + BaudRate::B250000, + BaudRate::B460800, + BaudRate::B921600, + BaudRate::B3000000, + BaudRate::B5250000, + ] { + if self.probe_baud(baudrate).await.is_ok() { + if baudrate != C::BAUD_RATE { + // Attempt to store the desired baudrate, so we can shortcut + // this probing next time. Ignore any potential failures, as + // this is purely an optimization. + let _ = embassy_futures::select::select( + NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), self.res_slot), + self.urc_channel, + ) + .restart(true), + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await; + } + found_baudrate = true; + break; + } + } - info!("Module started again"); - self.enter_edm(Duration::from_secs(4)).await?; + if !found_baudrate { + return Err(Error::BaudDetection); + } - Ok(()) - } + let at_client = ProxyClient::new(self.req_slot.sender(), self.res_slot); + + let setup_fut = async { + (&at_client).send_retry(&SoftwareVersion).await?; + + (&at_client) + .send_retry(&SetEcho { on: EchoOn::Off }) + .await?; + (&at_client) + .send_retry(&SetWifiConfig { + config_param: WifiConfigParam::DropNetworkOnLinkLoss(OnOff::On), + }) + .await?; + + // Disable all power savings for now + (&at_client) + .send_retry(&SetWifiConfig { + config_param: WifiConfigParam::PowerSaveMode(PowerSaveMode::ActiveMode), + }) + .await?; + + #[cfg(feature = "internal-network-stack")] + if let Some(size) = C::TLS_IN_BUFFER_SIZE { + (&at_client) + .send_retry(&crate::command::data_mode::SetPeerConfiguration { + parameter: crate::command::data_mode::types::PeerConfigParameter::TlsInBuffer( + size, + ), + }) + .await?; + } - pub async fn enter_edm(&mut self, timeout: Duration) -> Result<(), Error> { - info!("Entering EDM mode"); - - // Switch to EDM on Init. If in EDM, fail and check with autosense - let fut = async { - loop { - // Ignore AT results until we are successful in EDM mode - if let Ok(_) = self.at.send(SwitchToEdmCommand).await { - // After executing the data mode command or the extended data - // mode command, a delay of 50 ms is required before start of - // data transmission. - Timer::after(Duration::from_millis(50)).await; - break; - } - Timer::after(Duration::from_millis(10)).await; + #[cfg(feature = "internal-network-stack")] + if let Some(size) = C::TLS_OUT_BUFFER_SIZE { + (&at_client) + .send_retry(&crate::command::data_mode::SetPeerConfiguration { + parameter: + crate::command::data_mode::types::PeerConfigParameter::TlsOutBuffer( + size, + ), + }) + .await?; } + + Ok::<(), Error>(()) }; - with_timeout(timeout, fut) - .await - .map_err(|_| Error::Timeout)?; + match embassy_futures::select::select( + setup_fut, + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + ) + .await + { + Either::First(r) => r?, + Either::Second(_) => unreachable!(), + } - self.at.send_edm(SetEcho { on: EchoOn::On }).await?; + self.ch.mark_initialized(); Ok(()) } - pub async fn is_link_up(&mut self) -> Result { - // Determine link state - let link_state = match self.wifi_connection { - Some(ref conn) - if conn.network_up && matches!(conn.wifi_state, WiFiState::Connected) => - { - LinkState::Up + #[cfg(feature = "internal-network-stack")] + pub async fn run(&mut self) -> ! { + loop { + if self.init().await.is_err() { + continue; } - _ => LinkState::Down, - }; - self.ch.set_link_state(link_state); - - Ok(link_state == LinkState::Up) + embassy_futures::select::select( + NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), &self.res_slot), + self.urc_channel, + ) + .run(), + at_bridge(&mut self.transport, &self.req_slot, &mut self.ingress), + ) + .await; + } } - pub async fn run(mut self) -> ! { + #[cfg(feature = "ppp")] + pub async fn run( + &mut self, + stack: &embassy_net::Stack, + ) -> ! { loop { - let wait_link_up = { - let event = self.urc_subscription.next_message_pure().await; - match event { - EdmEvent::ATEvent(Urc::StartUp) => { - error!("AT startup event?! Device restarted unintentionally!"); - false - } - EdmEvent::ATEvent(Urc::WifiLinkConnected(WifiLinkConnected { - connection_id: _, - bssid, - channel, - })) => { - if let Some(ref mut con) = self.wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = bssid; - con.network.channel = channel; - } else { - debug!("[URC] Active network config discovered"); - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork::new_station(bssid, channel), - WiFiState::Connected, - 255, - ) - .activate(), - ); - } - true - } - EdmEvent::ATEvent(Urc::WifiLinkDisconnected(WifiLinkDisconnected { - reason, - .. - })) => { - if let Some(ref mut con) = self.wifi_connection { - match reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - con.wifi_state = WiFiState::NotConnected; - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } - } - } - - true - } - EdmEvent::ATEvent(Urc::WifiAPUp(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPDown(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPStationConnected(_)) => todo!(), - EdmEvent::ATEvent(Urc::WifiAPStationDisconnected(_)) => todo!(), - EdmEvent::ATEvent(Urc::EthernetLinkUp(_)) => todo!(), - EdmEvent::ATEvent(Urc::EthernetLinkDown(_)) => todo!(), - EdmEvent::ATEvent(Urc::NetworkUp(NetworkUp { interface_id })) => { - drop(event); - self.network_status_callback(interface_id).await.unwrap(); - true - } - EdmEvent::ATEvent(Urc::NetworkDown(NetworkDown { interface_id })) => { - drop(event); - self.network_status_callback(interface_id).await.unwrap(); - true - } - EdmEvent::ATEvent(Urc::NetworkError(_)) => todo!(), - EdmEvent::StartUp => { - error!("EDM startup event?! Device restarted unintentionally!"); - false - } - _ => false, - } - }; - - if wait_link_up { - self.is_link_up().await.unwrap(); + if self.init().await.is_err() { + continue; } - } - } - async fn network_status_callback(&mut self, interface_id: u8) -> Result<(), Error> { - let NetworkStatusResponse { - status: NetworkStatus::InterfaceType(InterfaceType::WifiStation), - .. - } = self - .at - .send_edm(GetNetworkStatus { - interface_id, - status: NetworkStatusParameter::InterfaceType, - }) - .await? - else { - return Err(Error::Network); - }; + debug!("Done initializing WiFi module"); + + let network_fut = async { + // Allow control to send/receive AT commands directly on the + // UART, until we are ready to establish connection using PPP + let _ = embassy_futures::select::select( + at_bridge(&mut self.transport, self.req_slot, &mut self.ingress), + self.ch.wait_connected(), + ) + .await; + + #[cfg(feature = "ppp")] + let ppp_fut = async { + self.ch.wait_for_link_state(state::LinkState::Up).await; + + { + let mut buf = [0u8; 8]; + let mut at_client = SimpleClient::new( + &mut self.transport, + atat::AtDigester::::new(), + &mut buf, + C::AT_CONFIG, + ); + + // Send AT command `ATO3` to enter PPP mode + let res = at_client + .send_retry(&ChangeMode { + mode: data_mode::types::Mode::PPPMode, + }) + .await; + + if let Err(e) = res { + warn!("ppp dial failed {:?}", e); + return; + } - let NetworkStatusResponse { - status: NetworkStatus::Gateway(ipv4), - .. - } = self - .at - .send_edm(GetNetworkStatus { - interface_id, - status: NetworkStatusParameter::Gateway, - }) - .await? - else { - return Err(Error::Network); - }; + // Drain the UART + let _ = embassy_time::with_timeout(Duration::from_millis(500), async { + loop { + self.transport.read(&mut buf).await.ok(); + } + }) + .await; + } - let ipv4_up = core::str::from_utf8(ipv4.as_slice()) - .ok() - .and_then(|s| Ipv4Addr::from_str(s).ok()) - .map(|ip| !ip.is_unspecified()) - .unwrap_or_default(); - - let NetworkStatusResponse { - status: NetworkStatus::IPv6LinkLocalAddress(ipv6), - .. - } = self - .at - .send_edm(GetNetworkStatus { - interface_id, - status: NetworkStatusParameter::IPv6LinkLocalAddress, - }) - .await? - else { - return Err(Error::Network); - }; + info!("RUNNING PPP"); + let _ = self + .ppp_runner + .as_mut() + .unwrap() + .run(&mut self.transport, C::PPP_CONFIG, |ipv4| { + debug!("Running on_ipv4_up for wifi!"); + let Some(addr) = ipv4.address else { + warn!("PPP did not provide an IP address."); + return; + }; + let mut dns_servers = heapless::Vec::new(); + for s in ipv4.dns_servers.iter().flatten() { + let _ = + dns_servers.push(embassy_net::Ipv4Address::from_bytes(&s.0)); + } + let config = + embassy_net::ConfigV4::Static(embassy_net::StaticConfigV4 { + address: embassy_net::Ipv4Cidr::new( + embassy_net::Ipv4Address::from_bytes(&addr.0), + 0, + ), + gateway: None, + dns_servers, + }); + + stack.set_config_v4(config); + }) + .await; + + info!("ppp failed"); + }; + + let at_fut = async { + use crate::asynch::at_udp_socket::AtUdpSocket; + use embassy_net::udp::{PacketMetadata, UdpSocket}; + + let mut rx_meta = [PacketMetadata::EMPTY; 1]; + let mut tx_meta = [PacketMetadata::EMPTY; 1]; + let mut socket_rx_buf = [0u8; 64]; + let mut socket_tx_buf = [0u8; 64]; + let mut socket = UdpSocket::new( + stack, + &mut rx_meta, + &mut socket_rx_buf, + &mut tx_meta, + &mut socket_tx_buf, + ); + + socket.bind(AtUdpSocket::PPP_AT_PORT).unwrap(); + let mut at_socket = AtUdpSocket(socket); + + at_bridge(&mut at_socket, self.req_slot, &mut self.ingress).await; + }; + + embassy_futures::select::select(ppp_fut, at_fut).await; + }; - let ipv6_up = core::str::from_utf8(ipv6.as_slice()) - .ok() - .and_then(|s| Ipv6Addr::from_str(s).ok()) - .map(|ip| !ip.is_unspecified()) - .unwrap_or_default(); + let device_fut = async { + let _ = NetDevice::new( + &self.ch, + &mut self.config, + &ProxyClient::new(self.req_slot.sender(), self.res_slot), + self.urc_channel, + ) + .run() + .await; + + warn!("Breaking to reboot device"); + }; - // Use `ipv4_up` & `ipv6_up` to determine link state - if let Some(ref mut con) = self.wifi_connection { - con.network_up = ipv4_up && ipv6_up; + embassy_futures::select::select(device_fut, network_fut).await; } - - Ok(()) } } diff --git a/src/asynch/state.rs b/src/asynch/state.rs index 7b45214..4dc3784 100644 --- a/src/asynch/state.rs +++ b/src/asynch/state.rs @@ -1,142 +1,216 @@ #![allow(dead_code)] use core::cell::RefCell; -use core::mem::MaybeUninit; -use core::task::Context; +use core::future::poll_fn; +use core::task::{Context, Poll}; -use atat::asynch::AtatClient; -use atat::UrcSubscription; use embassy_sync::blocking_mutex::raw::NoopRawMutex; use embassy_sync::blocking_mutex::Mutex; use embassy_sync::waitqueue::WakerRegistration; -use crate::command::edm::urc::EdmEvent; +use crate::connection::{WiFiState, WifiConnection}; /// The link state of a network device. #[derive(PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum LinkState { + /// Device is not yet initialized. + Uninitialized, /// The link is down. Down, /// The link is up. Up, } -use super::AtHandle; - -pub struct State { - inner: MaybeUninit, +pub(crate) struct State { + shared: Mutex>, } impl State { - pub const fn new() -> Self { + pub(crate) const fn new() -> Self { Self { - inner: MaybeUninit::uninit(), + shared: Mutex::new(RefCell::new(Shared { + should_connect: false, + link_state: LinkState::Uninitialized, + wifi_connection: WifiConnection::new(), + state_waker: WakerRegistration::new(), + connection_waker: WakerRegistration::new(), + })), } } } -struct StateInner { - shared: Mutex>, -} - /// State of the LinkState -pub struct Shared { +pub(crate) struct Shared { link_state: LinkState, - waker: WakerRegistration, -} - -pub struct Runner<'d> { - shared: &'d Mutex>, + should_connect: bool, + wifi_connection: WifiConnection, + state_waker: WakerRegistration, + connection_waker: WakerRegistration, } -#[derive(Clone, Copy)] -pub struct StateRunner<'d> { +#[derive(Clone)] +pub(crate) struct Runner<'d> { shared: &'d Mutex>, } impl<'d> Runner<'d> { - pub fn state_runner(&self) -> StateRunner<'d> { - StateRunner { - shared: self.shared, + pub(crate) fn new(state: &'d mut State) -> Self { + Self { + shared: &state.shared, } } - pub fn set_link_state(&mut self, state: LinkState) { + pub(crate) fn mark_initialized(&self) { self.shared.lock(|s| { let s = &mut *s.borrow_mut(); - s.link_state = state; - s.waker.wake(); - }); + s.link_state = LinkState::Down; + s.state_waker.wake(); + }) } -} -impl<'d> StateRunner<'d> { - pub fn set_link_state(&self, state: LinkState) { + pub(crate) fn mark_uninitialized(&self) { self.shared.lock(|s| { let s = &mut *s.borrow_mut(); - s.link_state = state; - s.waker.wake(); - }); + s.link_state = LinkState::Uninitialized; + s.state_waker.wake(); + }) } - pub fn link_state(&mut self, cx: &mut Context) -> LinkState { + pub(crate) fn set_should_connect(&self, should_connect: bool) { self.shared.lock(|s| { let s = &mut *s.borrow_mut(); - s.waker.register(cx.waker()); + s.connection_waker.wake(); + s.should_connect = should_connect; + }) + } + + pub(crate) async fn wait_for_initialized(&self) { + if self.link_state(None) != LinkState::Uninitialized { + return; + } + + poll_fn(|cx| { + if self.link_state(Some(cx)) != LinkState::Uninitialized { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn link_state(&self, cx: Option<&mut Context>) -> LinkState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.state_waker.register(cx.waker()); + } s.link_state }) } -} -pub fn new<'d, AT: AtatClient, const URC_CAPACITY: usize>( - state: &'d mut State, - at: AtHandle<'d, AT>, - urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, -) -> (Runner<'d>, Device<'d, AT, URC_CAPACITY>) { - // safety: this is a self-referential struct, however: - // - it can't move while the `'d` borrow is active. - // - when the borrow ends, the dangling references inside the MaybeUninit will never be used again. - let state_uninit: *mut MaybeUninit = - (&mut state.inner as *mut MaybeUninit).cast(); - - let state = unsafe { &mut *state_uninit }.write(StateInner { - shared: Mutex::new(RefCell::new(Shared { - link_state: LinkState::Down, - waker: WakerRegistration::new(), - })), - }); - - ( - Runner { - shared: &state.shared, - }, - Device { - shared: TestShared { - inner: &state.shared, - }, - urc_subscription, - at, - }, - ) -} + pub(crate) async fn wait_for_link_state(&self, ls: LinkState) { + if self.link_state(None) == ls { + return; + } -pub struct TestShared<'d> { - inner: &'d Mutex>, -} + poll_fn(|cx| { + if self.link_state(Some(cx)) == ls { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } -pub struct Device<'d, AT: AtatClient, const URC_CAPACITY: usize> { - pub(crate) shared: TestShared<'d>, - pub(crate) at: AtHandle<'d, AT>, - pub(crate) urc_subscription: UrcSubscription<'d, EdmEvent, URC_CAPACITY, 2>, -} + pub(crate) fn update_connection_with(&self, f: impl FnOnce(&mut WifiConnection)) { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + f(&mut s.wifi_connection); + info!( + "Connection status changed! Connected: {:?}", + s.wifi_connection.is_connected() + ); + + s.link_state = if s.wifi_connection.is_connected() { + LinkState::Up + } else { + LinkState::Down + }; + + s.state_waker.wake(); + s.connection_waker.wake(); + }) + } -impl<'d> TestShared<'d> { - pub fn link_state(&mut self, cx: &mut Context) -> LinkState { - self.inner.lock(|s| { + pub(crate) fn connection_down(&self, cx: Option<&mut Context>) -> bool { + self.shared.lock(|s| { let s = &mut *s.borrow_mut(); - s.waker.register(cx.waker()); - s.link_state + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + !s.wifi_connection.ipv4_up && !s.wifi_connection.ipv6_link_local_up + }) + } + + pub(crate) async fn wait_connection_down(&self) { + if self.connection_down(None) { + return; + } + + poll_fn(|cx| { + if self.connection_down(Some(cx)) { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn is_connected(&self, cx: Option<&mut Context>) -> bool { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + s.wifi_connection.is_connected() && s.should_connect + }) + } + + pub(crate) async fn wait_connected(&self) { + if self.is_connected(None) { + return; + } + + poll_fn(|cx| { + if self.is_connected(Some(cx)) { + return Poll::Ready(()); + } + Poll::Pending + }) + .await + } + + pub(crate) fn wifi_state(&self, cx: Option<&mut Context>) -> WiFiState { + self.shared.lock(|s| { + let s = &mut *s.borrow_mut(); + if let Some(cx) = cx { + s.connection_waker.register(cx.waker()); + } + s.wifi_connection.wifi_state + }) + } + + pub(crate) async fn wait_for_wifi_state_change(&self) -> WiFiState { + let old_state = self.wifi_state(None); + + poll_fn(|cx| { + let new_state = self.wifi_state(Some(cx)); + if old_state != new_state { + return Poll::Ready(new_state); + } + Poll::Pending }) + .await } } diff --git a/src/asynch/ublox_stack/device.rs b/src/asynch/ublox_stack/device.rs new file mode 100644 index 0000000..bf728c5 --- /dev/null +++ b/src/asynch/ublox_stack/device.rs @@ -0,0 +1,11 @@ +use core::cell::RefCell; + +use atat::UrcChannel; + +use crate::asynch::{control::ProxyClient, runner::URC_SUBSCRIBERS, state, UbloxUrc}; + +pub struct Device<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> { + pub(crate) state_ch: state::Runner<'a>, + pub(crate) at_client: RefCell>, + pub(crate) urc_channel: &'a UrcChannel, +} diff --git a/src/asynch/ublox_stack/dns.rs b/src/asynch/ublox_stack/dns.rs index ab28ee2..00550a2 100644 --- a/src/asynch/ublox_stack/dns.rs +++ b/src/asynch/ublox_stack/dns.rs @@ -1,6 +1,5 @@ use core::{cell::RefCell, future::poll_fn, task::Poll}; -use atat::asynch::AtatClient; use embassy_sync::waitqueue::WakerRegistration; use embedded_nal_async::AddrType; use no_std_net::IpAddr; @@ -26,10 +25,10 @@ pub enum Error { /// length is 64 characters. /// Domain name length is 128 for NINA-W13 and NINA-W15 software version 4.0 /// .0 or later. -#[cfg(not(feature = "nina_w1xx"))] +#[cfg(not(feature = "nina-w1xx"))] pub const MAX_DOMAIN_NAME_LENGTH: usize = 64; -#[cfg(feature = "nina_w1xx")] +#[cfg(feature = "nina-w1xx")] pub const MAX_DOMAIN_NAME_LENGTH: usize = 128; pub struct DnsTableEntry { @@ -115,8 +114,8 @@ pub struct DnsSocket<'a> { impl<'a> DnsSocket<'a> { /// Create a new DNS socket using the provided stack. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, ) -> Self { Self { stack: &stack.socket, diff --git a/src/asynch/ublox_stack/mod.rs b/src/asynch/ublox_stack/mod.rs index b5f3b8d..07247b6 100644 --- a/src/asynch/ublox_stack/mod.rs +++ b/src/asynch/ublox_stack/mod.rs @@ -5,37 +5,37 @@ pub mod tls; #[cfg(feature = "socket-udp")] pub mod udp; +mod device; pub mod dns; +mod peer_builder; + +pub use device::Device; use core::cell::RefCell; use core::future::poll_fn; use core::ops::{DerefMut, Rem}; use core::task::Poll; -use crate::asynch::state::Device; use crate::command::data_mode::responses::ConnectPeerResponse; use crate::command::data_mode::urc::PeerDisconnected; use crate::command::data_mode::{ClosePeerConnection, ConnectPeer}; use crate::command::edm::types::{DataEvent, Protocol}; use crate::command::edm::urc::EdmEvent; -use crate::command::edm::EdmDataCommand; +use crate::command::edm::{EdmAtCmdWrapper, EdmDataCommand}; use crate::command::ping::types::PingError; use crate::command::ping::urc::{PingErrorResponse, PingResponse}; use crate::command::ping::Ping; use crate::command::Urc; -use crate::peer_builder::{PeerUrlBuilder, SecurityCredentials}; +use peer_builder::{PeerUrlBuilder, SecurityCredentials}; use self::dns::{DnsSocket, DnsState, DnsTable}; -use super::state::{self, LinkState}; -use super::AtHandle; +use super::control::ProxyClient; -use atat::asynch::AtatClient; use embassy_futures::select; use embassy_sync::waitqueue::WakerRegistration; use embassy_time::{Duration, Ticker}; use embedded_nal_async::SocketAddr; -use futures::pin_mut; use no_std_net::IpAddr; use portable_atomic::{AtomicBool, AtomicU8, Ordering}; use ublox_sockets::{ @@ -68,15 +68,14 @@ impl StackResources { } } -pub struct UbloxStack { +pub struct UbloxStack { socket: RefCell, - device: RefCell>, + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, last_tx_socket: AtomicU8, should_tx: AtomicBool, - link_up: AtomicBool, } -struct SocketStack { +pub(crate) struct SocketStack { sockets: SocketSet<'static>, waker: WakerRegistration, dns_table: DnsTable, @@ -84,9 +83,11 @@ struct SocketStack { credential_map: heapless::FnvIndexMap, } -impl UbloxStack { +impl + UbloxStack +{ pub fn new( - device: state::Device<'static, AT, URC_CAPACITY>, + device: Device<'static, INGRESS_BUF_SIZE, URC_CAPACITY>, resources: &'static mut StackResources, ) -> Self { let sockets = SocketSet::new(&mut resources.sockets[..]); @@ -101,9 +102,8 @@ impl UbloxStack UbloxStack ! { let mut tx_buf = [0u8; MAX_EGRESS_SIZE]; + let Device { + urc_channel, + state_ch, + at_client, + } = &self.device; + + let mut urc_subscription = urc_channel.subscribe().unwrap(); + loop { // FIXME: It feels like this can be written smarter/simpler? let should_tx = poll_fn(|cx| match self.should_tx.load(Ordering::Relaxed) { @@ -126,46 +134,21 @@ impl UbloxStack Poll::Ready(LinkState::Down), - (false, LinkState::Up) => Poll::Ready(LinkState::Up), - _ => Poll::Pending, - }, - ), ) .await { - select::Either4::First(event) => { + select::Either3::First(event) => { Self::socket_rx(event, &self.socket); } - select::Either4::Second(_) | select::Either4::Third(_) => { + select::Either3::Second(_) | select::Either3::Third(_) => { if let Some(ev) = self.tx_event(&mut tx_buf) { - Self::socket_tx(ev, &self.socket, at).await; - } - } - select::Either4::Fourth(new_state) => { - // Update link up - let old_link_up = self.link_up.load(Ordering::Relaxed); - let new_link_up = new_state == LinkState::Up; - self.link_up.store(new_link_up, Ordering::Relaxed); - - // Print when changed - if old_link_up != new_link_up { - info!("link_up = {:?}", new_link_up); + Self::socket_tx(ev, &self.socket, &at_client).await; } } } @@ -325,13 +308,14 @@ impl UbloxStack UbloxStack( ev: TxEvent<'data>, socket: &RefCell, - at: &mut AtHandle<'_, AT>, + at_client: &RefCell>, ) { + use atat::asynch::AtatClient; + + let mut at = at_client.borrow_mut(); match ev { TxEvent::Connect { socket_handle, url } => { - match at.send_edm(ConnectPeer { url: &url }).await { + match at + .send_retry(&EdmAtCmdWrapper(ConnectPeer { url: &url })) + .await + { Ok(ConnectPeerResponse { peer_handle }) => { let mut s = socket.borrow_mut(); let tcp = s @@ -436,7 +426,7 @@ impl UbloxStack { warn!("Sending {} bytes on {}", data.len(), edm_channel); - at.send(EdmDataCommand { + at.send_retry(&EdmDataCommand { channel: edm_channel, data, }) @@ -444,14 +434,16 @@ impl UbloxStack { - at.send_edm(ClosePeerConnection { peer_handle }).await.ok(); + at.send_retry(&EdmAtCmdWrapper(ClosePeerConnection { peer_handle })) + .await + .ok(); } TxEvent::Dns { hostname } => { match at - .send_edm(Ping { + .send_retry(&EdmAtCmdWrapper(Ping { hostname: &hostname, retry_num: 1, - }) + })) .await { Ok(_) => {} diff --git a/src/peer_builder.rs b/src/asynch/ublox_stack/peer_builder.rs similarity index 100% rename from src/peer_builder.rs rename to src/asynch/ublox_stack/peer_builder.rs diff --git a/src/asynch/ublox_stack/tcp.rs b/src/asynch/ublox_stack/tcp.rs index aef9f89..8706313 100644 --- a/src/asynch/ublox_stack/tcp.rs +++ b/src/asynch/ublox_stack/tcp.rs @@ -3,7 +3,6 @@ use core::future::poll_fn; use core::mem; use core::task::Poll; -use atat::asynch::AtatClient; use embassy_time::Duration; use embedded_nal_async::SocketAddr; use ublox_sockets::{tcp, SocketHandle, TcpState}; @@ -123,8 +122,8 @@ impl<'a> TcpWriter<'a> { impl<'a> TcpSocket<'a> { /// Create a new TCP socket on the given stack, with the given buffers. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8], ) -> Self { @@ -331,7 +330,7 @@ impl<'a> TcpSocket<'a> { self.io.with(|s| s.may_send()) } - /// return whether the recieve half of the full-duplex connection is open. + /// return whether the receive half of the full-duplex connection is open. /// This function returns true if it’s possible to receive data from the remote endpoint. /// It will return true while there is data in the receive buffer, and if there isn’t, /// as long as the remote endpoint has not closed the connection. @@ -481,7 +480,7 @@ impl<'d> TcpIo<'d> { s.register_recv_waker(cx.waker()); Poll::Pending } else { - // if we can't receive because the recieve half of the duplex connection is closed then return an error + // if we can't receive because the receive half of the duplex connection is closed then return an error Poll::Ready(Err(Error::ConnectionReset)) } } else { @@ -631,24 +630,25 @@ pub mod client { /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. pub struct TcpClient< 'd, - AT: AtatClient + 'static, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize = 1024, const RX_SZ: usize = 1024, > { - pub(crate) stack: &'d UbloxStack, + pub(crate) stack: &'d UbloxStack, pub(crate) state: &'d TcpClientState, } impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > embedded_nal_async::Dns for TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > embedded_nal_async::Dns + for TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { type Error = crate::asynch::ublox_stack::dns::Error; @@ -671,16 +671,16 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { /// Create a new `TcpClient`. pub fn new( - stack: &'d UbloxStack, + stack: &'d UbloxStack, state: &'d TcpClientState, ) -> Self { Self { stack, state } @@ -689,12 +689,13 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > embedded_nal_async::TcpConnect for TcpClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > embedded_nal_async::TcpConnect + for TcpClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { type Error = Error; type Connection<'m> = TcpConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; @@ -724,8 +725,8 @@ pub mod client { impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TcpConnection<'d, N, TX_SZ, RX_SZ> { - fn new( - stack: &'d UbloxStack, + fn new( + stack: &'d UbloxStack, state: &'d TcpClientState, ) -> Result { let mut bufs = state.pool.alloc().ok_or(Error::ConnectionReset)?; @@ -750,7 +751,7 @@ pub mod client { } } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::ErrorType + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::ErrorType for TcpConnection<'d, N, TX_SZ, RX_SZ> { type Error = Error; diff --git a/src/asynch/ublox_stack/tls.rs b/src/asynch/ublox_stack/tls.rs index 55434d2..327f811 100644 --- a/src/asynch/ublox_stack/tls.rs +++ b/src/asynch/ublox_stack/tls.rs @@ -1,9 +1,8 @@ -use atat::asynch::AtatClient; use embassy_time::Duration; use no_std_net::SocketAddr; use ublox_sockets::TcpState as State; -use crate::peer_builder::SecurityCredentials; +use super::peer_builder::SecurityCredentials; use super::{ tcp::{ConnectError, Error, TcpIo, TcpReader, TcpSocket, TcpWriter}, @@ -16,8 +15,8 @@ pub struct TlsSocket<'a> { impl<'a> TlsSocket<'a> { /// Create a new TCP socket on the given stack, with the given buffers. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8], credentials: SecurityCredentials, @@ -28,7 +27,7 @@ impl<'a> TlsSocket<'a> { let s = &mut *stack.borrow_mut(); info!("Associating credentials {} with {}", credentials, handle); - s.credential_map.insert(handle, credentials); + s.credential_map.insert(handle, credentials).unwrap(); Self { inner: tcp_socket } } @@ -204,7 +203,7 @@ impl<'a> TlsSocket<'a> { self.inner.may_send() } - /// return whether the recieve half of the full-duplex connection is open. + /// return whether the receive half of the full-duplex connection is open. /// This function returns true if it’s possible to receive data from the remote endpoint. /// It will return true while there is data in the receive buffer, and if there isn’t, /// as long as the remote endpoint has not closed the connection. @@ -275,25 +274,26 @@ pub mod client { /// The pool is capable of managing up to N concurrent connections with tx and rx buffers according to TX_SZ and RX_SZ. pub struct TlsClient< 'd, - AT: AtatClient + 'static, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize = 1024, const RX_SZ: usize = 1024, > { - pub(crate) stack: &'d UbloxStack, + pub(crate) stack: &'d UbloxStack, pub(crate) state: &'d TcpClientState, pub(crate) credentials: SecurityCredentials, } impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > embedded_nal_async::Dns for TlsClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > embedded_nal_async::Dns + for TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { type Error = crate::asynch::ublox_stack::dns::Error; @@ -316,16 +316,16 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > TlsClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { /// Create a new `TlsClient`. pub fn new( - stack: &'d UbloxStack, + stack: &'d UbloxStack, state: &'d TcpClientState, credentials: SecurityCredentials, ) -> Self { @@ -339,12 +339,13 @@ pub mod client { impl< 'd, - AT: AtatClient, - const N: usize, + const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize, + const N: usize, const TX_SZ: usize, const RX_SZ: usize, - > embedded_nal_async::TcpConnect for TlsClient<'d, AT, N, URC_CAPACITY, TX_SZ, RX_SZ> + > embedded_nal_async::TcpConnect + for TlsClient<'d, INGRESS_BUF_SIZE, URC_CAPACITY, N, TX_SZ, RX_SZ> { type Error = Error; type Connection<'m> = TlsConnection<'m, N, TX_SZ, RX_SZ> where Self: 'm; @@ -374,8 +375,8 @@ pub mod client { impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> TlsConnection<'d, N, TX_SZ, RX_SZ> { - fn new( - stack: &'d UbloxStack, + fn new( + stack: &'d UbloxStack, state: &'d TcpClientState, credentials: SecurityCredentials, ) -> Result { @@ -406,7 +407,7 @@ pub mod client { } } - impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io::ErrorType + impl<'d, const N: usize, const TX_SZ: usize, const RX_SZ: usize> embedded_io_async::ErrorType for TlsConnection<'d, N, TX_SZ, RX_SZ> { type Error = Error; diff --git a/src/asynch/ublox_stack/udp.rs b/src/asynch/ublox_stack/udp.rs index 8348b3d..f561f69 100644 --- a/src/asynch/ublox_stack/udp.rs +++ b/src/asynch/ublox_stack/udp.rs @@ -3,7 +3,6 @@ use core::cell::RefCell; use core::mem; -use atat::asynch::AtatClient; use embedded_nal_async::SocketAddr; use ublox_sockets::{udp, SocketHandle, UdpState}; @@ -45,8 +44,8 @@ pub struct UdpSocket<'a> { impl<'a> UdpSocket<'a> { /// Create a new UDP socket using the provided stack and buffers. - pub fn new( - stack: &'a UbloxStack, + pub fn new( + stack: &'a UbloxStack, rx_buffer: &'a mut [u8], tx_buffer: &'a mut [u8], ) -> Self { diff --git a/src/blocking/client.rs b/src/blocking/client.rs deleted file mode 100644 index 762d279..0000000 --- a/src/blocking/client.rs +++ /dev/null @@ -1,859 +0,0 @@ -use core::str::FromStr; - -use crate::{ - blocking::timer::Timer, - command::{ - data_mode::{ - types::{IPProtocol, PeerConfigParameter}, - SetPeerConfiguration, - }, - edm::{types::Protocol, urc::EdmEvent, EdmAtCmdWrapper, SwitchToEdmCommand}, - general::{types::FirmwareVersion, SoftwareVersion}, - network::SetNetworkHostName, - ping::types::PingError, - system::{ - types::{BaudRate, ChangeAfterConfirm, FlowControl, Parity, StopBits}, - RebootDCE, SetRS232Settings, StoreCurrentConfig, - }, - wifi::{ - types::{DisconnectReason, WifiConfig}, - SetWifiConfig, - }, - Urc, - }, - config::Config, - error::Error, - wifi::{ - connection::{NetworkState, WiFiState, WifiConnection}, - network::{WifiMode, WifiNetwork}, - supplicant::Supplicant, - SocketMap, - }, -}; -use defmt::{debug, error, trace}; -use embassy_time::Duration; -use embedded_hal::digital::OutputPin; -use embedded_nal::{IpAddr, Ipv4Addr, SocketAddr}; -use ublox_sockets::{ - udp_listener::UdpListener, AnySocket, SocketHandle, SocketSet, SocketType, TcpSocket, TcpState, - UdpSocket, UdpState, -}; - -#[derive(PartialEq, Copy, Clone)] -pub enum SerialMode { - Cmd, - ExtendedData, -} - -#[derive(PartialEq, Copy, Clone)] -pub enum DNSState { - NotResolving, - Resolving, - Resolved(IpAddr), - Error(PingError), -} - -#[derive(PartialEq, Clone, Default)] -pub struct SecurityCredentials { - pub ca_cert_name: Option>, - pub c_cert_name: Option>, // TODO: Make &str with lifetime - pub c_key_name: Option>, -} - -/// Creates new socket numbers -/// Properly not Async safe -pub fn new_socket_num<'a, const N: usize, const L: usize>( - sockets: &'a SocketSet, -) -> Result { - let mut num = 0; - while sockets.socket_type(SocketHandle(num)).is_some() { - num += 1; - if num == u8::MAX { - return Err(()); - } - } - Ok(num) -} - -pub struct UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - pub(crate) module_started: bool, - pub(crate) initialized: bool, - serial_mode: SerialMode, - pub(crate) wifi_connection: Option, - pub(crate) wifi_config_active_on_startup: Option, - pub(crate) client: C, - pub(crate) config: Config, - pub(crate) sockets: Option<&'static mut SocketSet>, - pub(crate) dns_state: DNSState, - pub(crate) urc_attempts: u8, - pub(crate) security_credentials: SecurityCredentials, - pub(crate) socket_map: SocketMap, - pub(crate) udp_listener: UdpListener<4, N>, -} - -impl UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - pub fn new(client: C, config: Config) -> Self { - UbloxClient { - module_started: false, - initialized: false, - serial_mode: SerialMode::Cmd, - wifi_connection: None, - wifi_config_active_on_startup: None, - client, - config, - sockets: None, - dns_state: DNSState::NotResolving, - urc_attempts: 0, - security_credentials: SecurityCredentials::default(), - socket_map: SocketMap::default(), - udp_listener: UdpListener::new(), - } - } - - pub fn set_socket_storage(&mut self, socket_set: &'static mut SocketSet) { - socket_set.prune(); - self.sockets.replace(socket_set); - } - - pub fn take_socket_storage(&mut self) -> Option<&'static mut SocketSet> { - self.sockets.take() - } - - pub fn has_socket_storage(&self) -> bool { - self.sockets.is_some() - } - - pub fn init(&mut self) -> Result<(), Error> { - // Initilize a new ublox device to a known state (set RS232 settings) - - debug!("Initializing wifi"); - // Hard reset module - self.reset()?; - - // Switch to EDM on Init. If in EDM, fail and check with autosense - // if self.serial_mode != SerialMode::ExtendedData { - // self.retry_send(&SwitchToEdmCommand, 5)?; - // self.serial_mode = SerialMode::ExtendedData; - // } - - while self.serial_mode != SerialMode::ExtendedData { - self.send_internal(&SwitchToEdmCommand, true).ok(); - Timer::after(Duration::from_millis(100)).wait(); - while self.handle_urc()? {} - } - - // TODO: handle EDM settings quirk see EDM datasheet: 2.2.5.1 AT Request Serial settings - self.send_internal( - &EdmAtCmdWrapper(SetRS232Settings { - baud_rate: BaudRate::B115200, - flow_control: FlowControl::On, - data_bits: 8, - stop_bits: StopBits::One, - parity: Parity::None, - change_after_confirm: ChangeAfterConfirm::ChangeAfterOK, - }), - false, - )?; - - if let Some(hostname) = self.config.hostname.clone() { - self.send_internal( - &EdmAtCmdWrapper(SetNetworkHostName { - host_name: hostname.as_str(), - }), - false, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(SetWifiConfig { - config_param: WifiConfig::RemainOnChannel(0), - }), - false, - )?; - - self.send_internal(&EdmAtCmdWrapper(StoreCurrentConfig), false)?; - - self.software_reset()?; - - while self.serial_mode != SerialMode::ExtendedData { - self.send_internal(&SwitchToEdmCommand, true).ok(); - Timer::after(Duration::from_millis(100)).wait(); - while self.handle_urc()? {} - } - - if self.firmware_version()? < FirmwareVersion::new(8, 0, 0) { - self.config.network_up_bug = true; - } else { - if let Some(size) = self.config.tls_in_buffer_size { - self.send_internal( - &EdmAtCmdWrapper(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsInBuffer(size), - }), - false, - )?; - } - - if let Some(size) = self.config.tls_out_buffer_size { - self.send_internal( - &EdmAtCmdWrapper(SetPeerConfiguration { - parameter: PeerConfigParameter::TlsOutBuffer(size), - }), - false, - )?; - } - } - - self.initialized = true; - self.supplicant::<10>()?.init()?; - - Ok(()) - } - - pub fn firmware_version(&mut self) -> Result { - let response = self.send_internal(&EdmAtCmdWrapper(SoftwareVersion), false)?; - Ok(response.version) - } - - pub fn retry_send(&mut self, cmd: &A, attempts: usize) -> Result - where - A: atat::AtatCmd, - { - for _ in 0..attempts { - match self.send_internal(cmd, true) { - Ok(resp) => { - return Ok(resp); - } - Err(_e) => {} - }; - } - Err(Error::BaudDetection) - } - - pub fn reset(&mut self) -> Result<(), Error> { - self.serial_mode = SerialMode::Cmd; - self.initialized = false; - self.module_started = false; - self.wifi_connection = None; - self.wifi_config_active_on_startup = None; - self.dns_state = DNSState::NotResolving; - self.urc_attempts = 0; - self.security_credentials = SecurityCredentials::default(); - self.socket_map = SocketMap::default(); - self.udp_listener = UdpListener::new(); - - self.clear_buffers()?; - - if let Some(ref mut pin) = self.config.rst_pin { - warn!("Hard resetting Ublox Short Range"); - pin.set_low().ok(); - Timer::after(Duration::from_millis(50)).wait(); - pin.set_high().ok(); - - Timer::with_timeout(Duration::from_secs(4), || { - self.handle_urc().ok(); - if self.module_started { - Some(Ok::<(), ()>(())) - } else { - None - } - }) - .map_err(|_| Error::Timeout)?; - } - Ok(()) - } - - pub fn software_reset(&mut self) -> Result<(), Error> { - self.serial_mode = SerialMode::Cmd; - self.initialized = false; - self.module_started = false; - self.wifi_connection = None; - self.wifi_config_active_on_startup = None; - self.dns_state = DNSState::NotResolving; - self.urc_attempts = 0; - self.security_credentials = SecurityCredentials::default(); - self.socket_map = SocketMap::default(); - self.udp_listener = UdpListener::new(); - - warn!("Soft resetting Ublox Short Range"); - self.send_internal(&EdmAtCmdWrapper(RebootDCE), false)?; - self.clear_buffers()?; - - Timer::with_timeout(Duration::from_secs(4), || { - self.handle_urc().ok(); - if self.module_started { - Some(Ok::<(), ()>(())) - } else { - None - } - }) - .map_err(|_| Error::Timeout)?; - - Ok(()) - } - - pub(crate) fn clear_buffers(&mut self) -> Result<(), Error> { - // self.client.reset(); deprecated - - if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - sockets.prune(); - } - - // Allow ATAT some time to clear the buffers - Timer::after(Duration::from_millis(300)).wait(); - - Ok(()) - } - - pub fn spin(&mut self) -> Result<(), Error> { - if !self.initialized { - return Err(Error::Uninitialized); - } - - while self.handle_urc()? {} - - self.connected_to_network()?; - - // TODO: Is this smart? - // if let Some(ref mut sockets) = self.sockets.as_deref_mut() { - // sockets.recycle(self.timer.now()); - // } - - Ok(()) - } - - pub(crate) fn send_internal( - &mut self, - req: &A, - check_urc: bool, - ) -> Result - where - A: atat::AtatCmd, - { - if check_urc { - if let Err(e) = self.handle_urc() { - error!("Failed handle URC: {:?}", e); - } - } - - self.client.send(req).map_err(|e| { - error!("{:?}: {=[u8]:a}", e, req.as_bytes()); - e.into() - }) - } - - fn handle_urc(&mut self) -> Result { - let mut ran = false; - let socket_set = self.sockets.as_deref_mut(); - let dns_state = &mut self.dns_state; - let socket_map = &mut self.socket_map; - let udp_listener = &mut self.udp_listener; - let wifi_connection = &mut self.wifi_connection; - - let mut a = self.urc_attempts; - let max = self.config.max_urc_attempts; - - self.client.try_read_urc_with::(|edm_urc, _| { - ran = true; - let res = match edm_urc { - EdmEvent::ATEvent(urc) => { - match urc { - Urc::StartUp => { - debug!("[URC] Startup"); - self.module_started = true; - self.initialized = false; - self.serial_mode = SerialMode::Cmd; - true - } - Urc::PeerConnected(event) => { - debug!("[URC] PeerConnected"); - - // TODO: - // - // We should probably move - // `tcp.set_state(TcpState::Connected(endpoint));` - // + `udp.set_state(UdpState::Established);` as - // well as `tcp.update_handle(*socket);` + - // `udp.update_handle(*socket);` here, to make - // sure that part also works without EDM mode - - - if let Some(sockets) = socket_set { - let remote_ip = Ipv4Addr::from_str( - core::str::from_utf8(event.remote_address.as_slice()).unwrap(), - ) - .unwrap(); - - let remote = SocketAddr::new(remote_ip.into(), event.remote_port); - - if let Some(queue) = udp_listener.incoming(event.local_port) { - trace!("[UDP Server] Server socket incomming"); - let mut handled = true; - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - // TODO Is this correct? - if !sockets.recycle() { - handled = false; - } - } - let peer_handle = event.handle; - let socket_handle = SocketHandle(new_socket_num(sockets).unwrap()); - let mut new_socket = UdpSocket::new(socket_handle.0); - new_socket.set_state(UdpState::Established); - if new_socket.bind(remote).is_err(){ - error!("[UDP_URC] Binding connecting socket Error"); - handled = false - } - if sockets.add(new_socket).map_err(|_| { - error!("[UDP_URC] Opening socket Error: Socket set full"); - Error::SocketMemory - }).is_err(){ - handled = false; - } - - if socket_map.insert_peer(peer_handle, socket_handle).map_err(|_| { - error!("[UDP_URC] Opening socket Error: Socket Map full"); - Error::SocketMapMemory - }).is_err(){ - handled = false; - } - debug!( - "[URC] Binding remote {=[u8]:a} to UDP server on port: {:?} with handle: {:?}", - event.remote_address.as_slice(), - event.local_port, - socket_handle - ); - if queue.enqueue((socket_handle, remote)).is_err(){ - handled = false - } - handled - } else { - match event.protocol { - IPProtocol::TCP => { - // if let Ok(mut tcp) = - // sockets.get::>(event.handle) - // { - // debug!( - // "Binding remote {=[u8]:a} to TCP socket {:?}", - // event.remote_address.as_slice(), - // event.handle - // ); - // tcp.set_state(TcpState::Connected(remote)); - // return true; - // } - } - IPProtocol::UDP => { - // if let Ok(mut udp) = - // sockets.get::>(event.handle) - // { - // debug!( - // "Binding remote {=[u8]:a} to UDP socket {:?}", - // event.remote_address.as_slice(), - // event.handle - // ); - // udp.bind(remote).unwrap(); - // udp.set_state(UdpState::Established); - // return true; - // } - } - } - true - } - } else { - true - } - } - Urc::PeerDisconnected(msg) => { - debug!("[URC] PeerDisconnected"); - if let Some(sockets) = socket_set { - if let Some(handle) = socket_map.peer_to_socket(&msg.handle) { - match sockets.socket_type(*handle) { - Some(SocketType::Tcp) => { - if let Ok(mut tcp) = - sockets.get::>(*handle) - { - tcp.closed_by_remote(); - } - } - Some(SocketType::Udp) => { - if let Ok(mut udp) = - sockets.get::>(*handle) - { - udp.close(); - } - sockets.remove(*handle).ok(); - } - _ => {} - } - socket_map.remove_peer(&msg.handle).unwrap(); - } - } - true - } - Urc::WifiLinkConnected(msg) => { - debug!("[URC] WifiLinkConnected"); - if let Some(ref mut con) = wifi_connection { - con.wifi_state = WiFiState::Connected; - con.network.bssid = msg.bssid; - con.network.channel = msg.channel; - } else { - debug!("[URC] Active network config discovered"); - wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: msg.bssid, - op_mode: crate::command::wifi::types::OperationMode::Infrastructure, - ssid: heapless::String::new(), - channel: msg.channel, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::Station, - }, - WiFiState::Connected, - 255, - ).activate() - ); - } - true - } - Urc::WifiLinkDisconnected(msg) => { - debug!("[URC] WifiLinkDisconnected"); - if let Some(con) = wifi_connection { - match msg.reason { - DisconnectReason::NetworkDisabled => { - con.wifi_state = WiFiState::Inactive; - } - DisconnectReason::SecurityProblems => { - error!("Wifi Security Problems"); - } - _ => { - con.wifi_state = WiFiState::NotConnected; - } - } - } - true - } - Urc::WifiAPUp(_) => { - debug!("[URC] WifiAPUp"); - true - } - Urc::WifiAPDown(_) => { - debug!("[URC] WifiAPDown"); - true - } - Urc::WifiAPStationConnected(client) => { - debug!( - "[URC] WifiAPStationConnected {=[u8]:a}", - client.mac_addr.into_inner() - ); - true - } - Urc::WifiAPStationDisconnected(_) => { - debug!("[URC] WifiAPStationDisconnected"); - true - } - Urc::EthernetLinkUp(_) => { - debug!("[URC] EthernetLinkUp"); - true - } - Urc::EthernetLinkDown(_) => { - debug!("[URC] EthernetLinkDown"); - true - } - Urc::NetworkUp(_) => { - debug!("[URC] NetworkUp"); - if let Some(con) = wifi_connection { - if self.config.network_up_bug { - match con.network_state { - NetworkState::Attached => (), - NetworkState::AlmostAttached => { - con.network_state = NetworkState::Attached - } - NetworkState::Unattached => { - con.network_state = NetworkState::AlmostAttached - } - } - } else { - con.network_state = NetworkState::Attached; - } - } - true - } - Urc::NetworkDown(_) => { - debug!("[URC] NetworkDown"); - if let Some(con) = wifi_connection { - con.network_state = NetworkState::Unattached; - } - true - } - Urc::NetworkError(_) => { - debug!("[URC] NetworkError"); - true - } - Urc::PingResponse(resp) => { - debug!("[URC] PingResponse"); - if *dns_state == DNSState::Resolving { - *dns_state = DNSState::Resolved(resp.ip) - } - true - } - Urc::PingErrorResponse(resp) => { - debug!("[URC] PingErrorResponse: {:?}", resp.error); - if *dns_state == DNSState::Resolving { - *dns_state = DNSState::Error(resp.error) - } - true - } - } - } // end match urc - EdmEvent::StartUp => { - debug!("[EDM_URC] STARTUP"); - self.module_started = true; - self.serial_mode = SerialMode::ExtendedData; - true - } - EdmEvent::IPv4ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv4ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = socket_set { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _ )) = queue.into_iter().find(|(_, remote)| remote == &endpoint) { - socket_map.insert_channel(event.channel_id, *socket_handle).is_ok() - } else { - false - } - } else { - sockets - .iter_mut() - .find_map(|(h, s)| { - match event.protocol { - Protocol::TCP => { - let mut tcp = TcpSocket::downcast(s).ok()?; - if tcp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - tcp.set_state(TcpState::Connected(endpoint)); - return Some(true); - } - } - Protocol::UDP => { - let mut udp = UdpSocket::downcast(s).ok()?; - if udp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - udp.set_state(UdpState::Established); - return Some(true); - } - } - _ => {} - } - None - }) - .is_some() - } - } else { - true - } - } - EdmEvent::IPv6ConnectEvent(event) => { - debug!( - "[EDM_URC] IPv6ConnectEvent! Channel_id: {:?}", - event.channel_id - ); - - if let Some(sockets) = socket_set { - let endpoint = SocketAddr::new(event.remote_ip.into(), event.remote_port); - - // This depends upon Connected AT-URC to arrive first. - if let Some(queue) = udp_listener.incoming(event.local_port) { - if let Some((socket_handle, _ )) = queue.into_iter().find(|(_, remote)| remote == &endpoint) { - socket_map.insert_channel(event.channel_id, *socket_handle).is_ok() - } else { - false - } - } else { - sockets - .iter_mut() - .find_map(|(h, s)| { - match event.protocol { - Protocol::TCP => { - let mut tcp = TcpSocket::downcast(s).ok()?; - if tcp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - tcp.set_state(TcpState::Connected(endpoint)); - return Some(true); - } - } - Protocol::UDP => { - let mut udp = UdpSocket::downcast(s).ok()?; - if udp.endpoint() == Some(endpoint) { - socket_map.insert_channel(event.channel_id, h).unwrap(); - udp.set_state(UdpState::Established); - return Some(true); - } - } - _ => {} - } - None - }) - .is_some() - } - } else { - true - } - } - EdmEvent::BluetoothConnectEvent(_) => { - debug!("[EDM_URC] BluetoothConnectEvent"); - true - } - EdmEvent::DisconnectEvent(channel_id) => { - debug!("[EDM_URC] DisconnectEvent! Channel_id: {:?}", channel_id); - socket_map.remove_channel(&channel_id).ok(); - true - } - EdmEvent::DataEvent(event) => { - debug!("[EDM_URC] DataEvent! Channel_id: {:?}", event.channel_id); - if let Some(sockets) = socket_set { - if !event.data.is_empty() { - if let Some(socket_handle) = - socket_map.channel_to_socket(&event.channel_id) - { - match sockets.socket_type(*socket_handle) { - Some(SocketType::Tcp) => { - // Handle tcp socket - let mut tcp = sockets - .get::>(*socket_handle) - .unwrap(); - if tcp.can_recv() { - tcp.rx_enqueue_slice(&event.data); - true - } else { - false - } - } - Some(SocketType::Udp) => { - // Handle udp socket - let mut udp = sockets - .get::>(*socket_handle) - .unwrap(); - - if udp.can_recv() { - udp.rx_enqueue_slice(&event.data); - true - } else { - false - } - } - _ => { - error!("SocketNotFound {:?}", socket_handle); - false - } - } - } else { - false - } - } else { - false - } - } else { - true - } - } - }; // end match edm-urc - if !res { - if a < max { - error!("[EDM_URC] URC handeling failed"); - a += 1; - return false; - } - error!("[EDM_URC] URC thrown away"); - } - a = 0; - true - }); - self.urc_attempts = a; - Ok(ran) - } - - /// Send AT command - /// Automaticaly waraps commands in EDM context - pub fn send_at(&mut self, cmd: A) -> Result - where - A: atat::AtatCmd, - { - if !self.initialized { - self.init()?; - } - match self.serial_mode { - SerialMode::ExtendedData => self.send_internal(&EdmAtCmdWrapper(cmd), true), - SerialMode::Cmd => self.send_internal(&cmd, true), - } - } - - pub fn supplicant(&mut self) -> Result, Error> { - // TODO: better solution - if !self.initialized { - return Err(Error::Uninitialized); - } - - Ok(Supplicant { - client: &mut self.client, - wifi_connection: &mut self.wifi_connection, - active_on_startup: &mut self.wifi_config_active_on_startup, - }) - } - /// Is the module attached to a WiFi and ready to open sockets - pub fn connected_to_network(&self) -> Result<(), Error> { - if let Some(ref con) = self.wifi_connection { - if !self.initialized { - Err(Error::Uninitialized) - } else if !con.is_connected() { - Err(Error::WifiState(con.wifi_state)) - } else if self.sockets.is_none() { - Err(Error::MissingSocketSet) - } else { - Ok(()) - } - } else { - Err(Error::NoWifiSetup) - } - } - - /// Is the module attached to a WiFi - /// - // TODO: handle this case for better stability - // WiFi connection can disconnect momentarily, but if the network state does not change - // the current context is safe. - pub fn attached_to_wifi(&self) -> Result<(), Error> { - if let Some(ref con) = self.wifi_connection { - if !self.initialized { - Err(Error::Uninitialized) - // } else if !(con.network_state == NetworkState::Attached) { - } else if !con.is_connected() { - if con.wifi_state == WiFiState::Connected { - Err(Error::NetworkState(con.network_state)) - } else { - Err(Error::WifiState(con.wifi_state)) - } - } else { - Ok(()) - } - } else { - Err(Error::NoWifiSetup) - } - } -} diff --git a/src/blocking/dns.rs b/src/blocking/dns.rs deleted file mode 100644 index 48b9904..0000000 --- a/src/blocking/dns.rs +++ /dev/null @@ -1,52 +0,0 @@ -use super::client::{DNSState, UbloxClient}; -use super::timer; -use embassy_time::Duration; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, AddrType, Dns, IpAddr}; -use heapless::String; -use ublox_sockets::Error; - -use crate::{blocking::timer::Timer, command::ping::*}; - -impl Dns for UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - type Error = Error; - - fn get_host_by_address(&mut self, _ip_addr: IpAddr) -> nb::Result, Self::Error> { - unimplemented!() - } - - fn get_host_by_name( - &mut self, - hostname: &str, - _addr_type: AddrType, - ) -> nb::Result { - debug!("Lookup hostname: {}", hostname); - self.send_at(Ping { - hostname, - retry_num: 1, - }) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - - self.dns_state = DNSState::Resolving; - - match Timer::with_timeout(Duration::from_secs(8), || { - if self.spin().is_err() { - return Some(Err(Error::Illegal)); - } - - match self.dns_state { - DNSState::Resolving => None, - DNSState::Resolved(ip) => Some(Ok(ip)), - _ => Some(Err(Error::Illegal)), - } - }) { - Ok(ip) => Ok(ip), - Err(timer::Error::Timeout) => Err(nb::Error::Other(Error::Timeout)), - Err(timer::Error::Other(e)) => Err(nb::Error::Other(e)), - } - } -} diff --git a/src/blocking/mod.rs b/src/blocking/mod.rs deleted file mode 100644 index 208e4a5..0000000 --- a/src/blocking/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -pub(crate) mod client; -mod dns; -pub mod timer; -pub mod tls; - -#[cfg(feature = "socket-udp")] -pub mod udp_stack; - -#[cfg(feature = "socket-tcp")] -pub mod tcp_stack; - -pub use client::UbloxClient; diff --git a/src/blocking/tcp_stack.rs b/src/blocking/tcp_stack.rs deleted file mode 100644 index fb72ce3..0000000 --- a/src/blocking/tcp_stack.rs +++ /dev/null @@ -1,227 +0,0 @@ -use super::{client::new_socket_num, UbloxClient}; -use crate::{ - command::data_mode::*, - command::edm::{EdmAtCmdWrapper, EdmDataCommand}, - wifi::peer_builder::PeerUrlBuilder, -}; -use embedded_hal::digital::OutputPin; -/// Handles receiving data from sockets -/// implements TCP and UDP for WiFi client -use embedded_nal::{nb, SocketAddr, TcpClientStack}; - -use ublox_sockets::{Error, SocketHandle, TcpSocket, TcpState}; - -use crate::wifi::EGRESS_CHUNK_SIZE; - -impl TcpClientStack for UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the UbloxClient, - // as the Socket object itself provides no value without accessing it though the client. - type TcpSocket = SocketHandle; - - /// Open a new TCP socket to the given address and port. The socket starts in the unconnected state. - fn socket(&mut self) -> Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - // Check if there are any unused sockets available - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - if sockets.recycle() { - return Err(Error::SocketSetFull); - } - } - - debug!("[TCP] Opening socket"); - - let socket_id = new_socket_num(sockets).unwrap(); - sockets.add(TcpSocket::new(socket_id)).map_err(|e| { - error!("[TCP] Opening socket Error: {:?}", e); - e - }) - } else { - Err(Error::Illegal) - } - } - - /// Connect to the given remote host and port. - fn connect( - &mut self, - socket: &mut Self::TcpSocket, - remote: SocketAddr, - ) -> nb::Result<(), Self::Error> { - if self.sockets.is_none() { - return Err(Error::Illegal.into()); - } - - debug!("[TCP] Connect socket"); - self.connected_to_network().map_err(|_| Error::Illegal)?; - - let url = PeerUrlBuilder::new() - .address(&remote) - .creds(self.security_credentials.clone()) - .tcp() - .map_err(|_| Error::Unaddressable)?; - - // If no socket is found we stop here - let mut tcp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)?; - - tcp.set_state(TcpState::WaitingForConnect(remote)); - - match self - .send_internal(&EdmAtCmdWrapper(ConnectPeer { url: &url }), false) - .map_err(|_| Error::Unaddressable) - { - Ok(resp) => self - .socket_map - .insert_peer(resp.peer_handle, *socket) - .map_err(|_| Error::InvalidSocket)?, - Err(e) => { - let mut tcp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)?; - tcp.set_state(TcpState::Created); - return Err(nb::Error::Other(e)); - } - } - - trace!("[TCP] Connecting socket: {:?} to url: {=str}", socket, url); - - // TODO: Timeout? - // TODO: Fix the fact that it doesen't wait for both connect messages - while { - matches!( - self.sockets - .as_mut() - .unwrap() - .get::>(*socket) - .map_err(Self::Error::from)? - .state(), - TcpState::WaitingForConnect(_) - ) - } { - self.spin().map_err(|_| Error::Illegal)?; - } - Ok(()) - } - - /// Check if this socket is still connected - fn is_connected(&mut self, socket: &Self::TcpSocket) -> Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - let tcp = sockets.get::>(*socket)?; - Ok(tcp.is_connected()) - } else { - Err(Error::Illegal) - } - } - - /// Write to the stream. Returns the number of bytes written is returned - /// (which may be less than `buffer.len()`), or an error. - fn send( - &mut self, - socket: &mut Self::TcpSocket, - buffer: &[u8], - ) -> nb::Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - let tcp = sockets - .get::>(*socket) - .map_err(|e| nb::Error::Other(e.into()))?; - - if !tcp.is_connected() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(socket) - .ok_or(nb::Error::Other(Error::SocketClosed))?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - true, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - Ok(buffer.len()) - } else { - Err(Error::Illegal.into()) - } - } - - fn receive( - &mut self, - socket: &mut Self::TcpSocket, - buffer: &mut [u8], - ) -> nb::Result { - // TODO: Handle error states - self.spin().map_err(|_| nb::Error::Other(Error::Illegal))?; - if let Some(ref mut sockets) = self.sockets { - // Enable detecting closed socket from receive function - sockets.recycle(); - - let mut tcp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - Ok(tcp.recv_slice(buffer).map_err(Self::Error::from)?) - } else { - Err(Error::Illegal.into()) - } - } - - /// Close an existing TCP socket. - fn close(&mut self, socket: Self::TcpSocket) -> Result<(), Self::Error> { - if let Some(ref mut sockets) = self.sockets { - debug!("[TCP] Closing socket: {:?}", socket); - // If the socket is not found it is already removed - if let Ok(ref tcp) = sockets.get::>(socket) { - // If socket is not closed that means a connection excists which has to be closed - if !matches!( - tcp.state(), - TcpState::ShutdownForWrite(_) | TcpState::Created - ) { - if let Some(peer_handle) = self.socket_map.socket_to_peer(&tcp.handle()) { - let peer_handle = *peer_handle; - match self.send_at(ClosePeerConnection { peer_handle }) { - Err(crate::error::Error::AT(atat::Error::InvalidResponse)) | Ok(_) => { - () - } - Err(_) => return Err(Error::Unaddressable), - } - } else { - error!( - "Illigal state! Socket connected but not in socket map: {:?}", - tcp.handle() - ); - return Err(Error::Illegal); - } - } else { - // No connection exists the socket should be removed from the set here - sockets.remove(socket)?; - } - } - Ok(()) - } else { - Err(Error::Illegal) - } - } -} diff --git a/src/blocking/timer.rs b/src/blocking/timer.rs deleted file mode 100644 index 7ddae99..0000000 --- a/src/blocking/timer.rs +++ /dev/null @@ -1,42 +0,0 @@ -use embassy_time::{Duration, Instant}; - -pub struct Timer { - expires_at: Instant, -} - -pub enum Error { - Timeout, - Other(E), -} - -impl Timer { - pub fn after(duration: Duration) -> Self { - Self { - expires_at: Instant::now() + duration, - } - } - - pub fn with_timeout(timeout: Duration, mut e: F) -> Result> - where - F: FnMut() -> Option>, - { - let timer = Timer::after(timeout); - - loop { - if let Some(res) = e() { - return res.map_err(Error::Other); - } - if timer.expires_at <= Instant::now() { - return Err(Error::Timeout); - } - } - } - - pub fn wait(self) { - loop { - if self.expires_at <= Instant::now() { - break; - } - } - } -} diff --git a/src/blocking/tls.rs b/src/blocking/tls.rs deleted file mode 100644 index ed5687f..0000000 --- a/src/blocking/tls.rs +++ /dev/null @@ -1,105 +0,0 @@ -use super::UbloxClient; -use crate::{ - command::edm::BigEdmAtCmdWrapper, - command::security::{types::*, *}, - error::Error, -}; -use embedded_hal::digital::OutputPin; -use heapless::String; - -pub trait TLS { - fn import_certificate(&mut self, name: &str, certificate: &[u8]) -> Result<(), Error>; - fn import_root_ca(&mut self, name: &str, root_ca: &[u8]) -> Result<(), Error>; - fn import_private_key( - &mut self, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error>; -} - -impl TLS for UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - /// Importing credentials enabeles their use for all further TCP connections - fn import_certificate(&mut self, name: &str, certificate: &[u8]) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientCertificate, - data_size: certificate.len(), - internal_name: name, - password: None, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(certificate), - }), - false, - )?; - - self.security_credentials - .c_cert_name - .replace(String::from(name)); - - Ok(()) - } - - /// Importing credentials enabeles their use for all further TCP connections - fn import_root_ca(&mut self, name: &str, root_ca: &[u8]) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::TrustedRootCA, - data_size: root_ca.len(), - internal_name: name, - password: None, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(root_ca), - }), - false, - )?; - - self.security_credentials - .ca_cert_name - .replace(String::from(name)); - - Ok(()) - } - - /// Importing credentials enabeles their use for all further TCP connections - fn import_private_key( - &mut self, - name: &str, - private_key: &[u8], - password: Option<&str>, - ) -> Result<(), Error> { - assert!(name.len() < 16); - - self.send_at(PrepareSecurityDataImport { - data_type: SecurityDataType::ClientPrivateKey, - data_size: private_key.len(), - internal_name: name, - password, - })?; - - self.send_internal( - &BigEdmAtCmdWrapper(SendSecurityDataImport { - data: atat::serde_bytes::Bytes::new(private_key), - }), - false, - )?; - - self.security_credentials - .c_key_name - .replace(String::from(name)); - - Ok(()) - } -} diff --git a/src/blocking/udp_stack.rs b/src/blocking/udp_stack.rs deleted file mode 100644 index 795bc93..0000000 --- a/src/blocking/udp_stack.rs +++ /dev/null @@ -1,393 +0,0 @@ -use super::client::new_socket_num; -use super::UbloxClient; -use crate::{ - command::data_mode::*, - command::{ - data_mode::types::{IPVersion, ServerType, UDPBehaviour}, - edm::{EdmAtCmdWrapper, EdmDataCommand}, - }, - wifi::peer_builder::PeerUrlBuilder, -}; -use embedded_hal::digital::OutputPin; -use embedded_nal::{nb, SocketAddr, UdpFullStack}; - -use embedded_nal::UdpClientStack; -use ublox_sockets::{Error, SocketHandle, UdpSocket, UdpState}; - -use crate::wifi::EGRESS_CHUNK_SIZE; - -impl UdpClientStack for UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - type Error = Error; - - // Only return a SocketHandle to reference into the SocketSet owned by the UbloxClient, - // as the Socket object itself provides no value without accessing it though the client. - type UdpSocket = SocketHandle; - - fn socket(&mut self) -> Result { - self.connected_to_network().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - // Check if there are any unused sockets available - if sockets.len() >= sockets.capacity() { - // Check if there are any sockets closed by remote, and close it - // if it has exceeded its timeout, in order to recycle it. - if sockets.recycle() { - return Err(Error::SocketSetFull); - } - } - - let socket_id = new_socket_num(sockets).unwrap(); - debug!("[UDP] Opening socket"); - sockets.add(UdpSocket::new(socket_id)).map_err(|_| { - error!("[UDP] Opening socket Error: Socket set full"); - Error::SocketSetFull - }) - } else { - error!("[UDP] Opening socket Error: Missing socket set"); - Err(Error::Illegal) - } - } - - /// Connect a UDP socket with a peer using a dynamically selected port. - /// Selects a port number automatically and initializes for read/writing. - fn connect( - &mut self, - socket: &mut Self::UdpSocket, - remote: SocketAddr, - ) -> Result<(), Self::Error> { - if self.sockets.is_none() { - error!("[UDP] Connecting socket Error: Missing socket set"); - return Err(Error::Illegal); - } - let url = PeerUrlBuilder::new() - .address(&remote) - .udp() - .map_err(|_| Error::Unaddressable)?; - debug!("[UDP] Connecting Socket: {:?} to URL: {=str}", socket, url); - - self.connected_to_network().map_err(|_| Error::Illegal)?; - - // First look to see if socket is valid - let mut udp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)?; - udp.bind(remote)?; - - // Then connect modem - match self - .send_internal(&EdmAtCmdWrapper(ConnectPeer { url: &url }), true) - .map_err(|_| Error::Unaddressable) - { - Ok(resp) => self - .socket_map - .insert_peer(resp.peer_handle.into(), *socket) - .map_err(|_| Error::InvalidSocket)?, - - Err(e) => { - let mut udp = self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)?; - udp.close(); - return Err(e); - } - } - while self - .sockets - .as_mut() - .unwrap() - .get::>(*socket)? - .state() - == UdpState::Closed - { - self.spin().map_err(|_| Error::Illegal)?; - } - Ok(()) - } - - /// Send a datagram to the remote host. - fn send(&mut self, socket: &mut Self::UdpSocket, buffer: &[u8]) -> nb::Result<(), Self::Error> { - self.spin().map_err(|_| Error::Illegal)?; - if let Some(ref mut sockets) = self.sockets { - // No send for server sockets! - if self.udp_listener.is_bound(*socket) { - return Err(nb::Error::Other(Error::Illegal)); - } - - let udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - if !udp.is_open() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(socket) - .ok_or(nb::Error::Other(Error::SocketClosed))?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - true, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - Ok(()) - } else { - Err(Error::Illegal.into()) - } - } - - /// Read a datagram the remote host has sent to us. Returns `Ok(n)`, which - /// means a datagram of size `n` has been received and it has been placed - /// in `&buffer[0..n]`, or an error. - fn receive( - &mut self, - socket: &mut Self::UdpSocket, - buffer: &mut [u8], - ) -> nb::Result<(usize, SocketAddr), Self::Error> { - self.spin().ok(); - let udp_listener = &mut self.udp_listener; - // Handle server sockets - if udp_listener.is_bound(*socket) { - // Nothing available, would block - if !udp_listener.available(*socket).unwrap_or(false) { - return Err(nb::Error::WouldBlock); - } - - let (connection_handle, remote) = self - .udp_listener - .peek_remote(*socket) - .map_err(|_| Error::NotBound)?; - - if let Some(ref mut sockets) = self.sockets { - let mut udp = sockets - .get::>(*connection_handle) - .map_err(|_| Self::Error::InvalidSocket)?; - - let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; - Ok((bytes, remote.clone())) - } else { - Err(Error::Illegal.into()) - } - - // Handle reciving for udp normal sockets - } else if let Some(ref mut sockets) = self.sockets { - let mut udp = sockets - .get::>(*socket) - .map_err(Self::Error::from)?; - - let bytes = udp.recv_slice(buffer).map_err(Self::Error::from)?; - - let endpoint = udp.endpoint().ok_or(Error::SocketClosed)?; - Ok((bytes, endpoint)) - } else { - Err(Error::Illegal.into()) - } - } - - /// Close an existing UDP socket. - fn close(&mut self, socket: Self::UdpSocket) -> Result<(), Self::Error> { - self.spin().ok(); - // Close server socket - if self.udp_listener.is_bound(socket) { - debug!("[UDP] Closing Server socket: {:?}", socket); - - // ID 2 used by UDP server - self.send_internal( - &EdmAtCmdWrapper(ServerConfiguration { - id: 2, - server_config: ServerType::Disabled, - }), - true, - ) - .map_err(|_| Error::Unaddressable)?; - - // Borrow socket set to close server socket - if let Some(ref mut sockets) = self.sockets { - // If socket in socket set close - if sockets.remove(socket).is_err() { - error!( - "[UDP] Closing server socket error: No socket matching: {:?}", - socket - ); - return Err(Error::InvalidSocket); - } - } else { - return Err(Error::Illegal); - } - - // Close incomming connections - while self.udp_listener.available(socket).unwrap_or(false) { - if let Ok((connection_handle, _)) = self.udp_listener.get_remote(socket) { - debug!( - "[UDP] Closing incomming socket for Server: {:?}", - connection_handle - ); - self.close(connection_handle)?; - } else { - error!("[UDP] Incomming socket for server error - Listener says available, while nothing present"); - } - } - - // Unbind server socket in listener - self.udp_listener.unbind(socket).map_err(|_| { - error!( - "[UDP] Closing socket error: No server socket matching: {:?}", - socket - ); - Error::Illegal - }) - // Handle normal sockets - } else if let Some(ref mut sockets) = self.sockets { - debug!("[UDP] Closing socket: {:?}", socket); - // If no sockets exists, nothing to close. - if let Ok(ref mut udp) = sockets.get::>(socket) { - trace!("[UDP] Closing socket state: {:?}", udp.state()); - match udp.state() { - UdpState::Closed => { - sockets.remove(socket).ok(); - } - UdpState::Established => { - // FIXME:udp.close(); - if let Some(peer_handle) = self.socket_map.socket_to_peer(&udp.handle()) { - let peer_handle = *peer_handle; - self.send_at(ClosePeerConnection { peer_handle }) - .map_err(|_| Error::Unaddressable)?; - } - } - } - } else { - error!( - "[UDP] Closing socket error: No socket matching: {:?}", - socket - ); - return Err(Error::InvalidSocket); - } - Ok(()) - } else { - Err(Error::Illegal) - } - } -} - -/// UDP Full Stack -/// -/// This fullstack is build for request-response type servers due to HW/SW limitations -/// Limitations: -/// - The driver can only send to Socket addresses that have send data first. -/// - The driver can only call send_to once after reciving data once. -/// - The driver has to call send_to after reciving data, to release the socket bound by remote host, -/// even if just sending no bytes. Else these sockets will be held open until closure of server socket. -/// -impl UdpFullStack for UbloxClient -where - C: atat::blocking::AtatClient, - RST: OutputPin, -{ - fn bind(&mut self, socket: &mut Self::UdpSocket, local_port: u16) -> Result<(), Self::Error> { - if self.connected_to_network().is_err() || self.udp_listener.is_port_bound(local_port) { - return Err(Error::Illegal); - } - - debug!( - "[UDP] binding socket: {:?} to port: {:?}", - socket, local_port - ); - - // ID 2 used by UDP server - self.send_internal( - &EdmAtCmdWrapper(ServerConfiguration { - id: 2, - server_config: ServerType::UDP( - local_port, - UDPBehaviour::AutoConnect, - IPVersion::IPv4, - ), - }), - true, - ) - .map_err(|_| Error::Unaddressable)?; - - self.udp_listener - .bind(*socket, local_port) - .map_err(|_| Error::Illegal)?; - - Ok(()) - } - - fn send_to( - &mut self, - socket: &mut Self::UdpSocket, - remote: SocketAddr, - buffer: &[u8], - ) -> nb::Result<(), Self::Error> { - self.spin().map_err(|_| Error::Illegal)?; - // Protect against non server sockets - if !self.udp_listener.is_bound(*socket) { - return Err(Error::Illegal.into()); - } - // Check incomming sockets for the socket address - if let Some(connection_socket) = self.udp_listener.get_outgoing(socket, remote) { - if let Some(ref mut sockets) = self.sockets { - if buffer.len() == 0 { - self.close(connection_socket)?; - return Ok(()); - } - - let udp = sockets - .get::>(connection_socket) - .map_err(Self::Error::from)?; - - if !udp.is_open() { - return Err(Error::SocketClosed.into()); - } - - let channel = *self - .socket_map - .socket_to_channel_id(&connection_socket) - .ok_or(nb::Error::WouldBlock)?; - - for chunk in buffer.chunks(EGRESS_CHUNK_SIZE) { - self.send_internal( - &EdmDataCommand { - channel, - data: chunk, - }, - false, - ) - .map_err(|_| nb::Error::Other(Error::Unaddressable))?; - } - self.close(connection_socket).unwrap(); - Ok(()) - } else { - Err(Error::Illegal.into()) - } - } else { - Err(Error::Illegal.into()) - } - - ////// Do with URC - // Crate a new SocketBuffer allocation for the incoming connection - // let mut tcp = self - // .sockets - // .as_mut() - // .ok_or(Error::Illegal)? - // .get::>(data_socket) - // .map_err(Self::Error::from)?; - - // tcp.update_handle(handle); - // tcp.set_state(TcpState::Connected(remote.clone())); - } -} diff --git a/src/command/custom_digest.rs b/src/command/custom_digest.rs index d591a08..2b4b537 100644 --- a/src/command/custom_digest.rs +++ b/src/command/custom_digest.rs @@ -10,6 +10,12 @@ use super::edm::types::{AUTOCONNECTMESSAGE, STARTUPMESSAGE}; #[derive(Debug, Default)] pub struct EdmDigester; +impl EdmDigester { + pub fn new() -> Self { + Self + } +} + impl Digester for EdmDigester { fn digest<'a>(&mut self, buf: &'a [u8]) -> (DigestResult<'a>, usize) { // TODO: Handle module restart, tests and set default startupmessage in client, and optimize this! @@ -112,20 +118,6 @@ impl Digester for EdmDigester { // struct MockWriter; -// impl embedded_io::Io for MockWriter { -// type Error = (); -// } - -// impl embedded_io::blocking::Write for MockWriter { -// fn write(&mut self, buf: &[u8]) -> Result { -// Ok(buf.len()) -// } - -// fn flush(&mut self) -> Result<(), Self::Error> { -// Ok(()) -// } -// } - // /// Removed functionality used to change OK responses to empty responses. // #[test] // fn ok_response<'a>() { @@ -229,7 +221,7 @@ impl Digester for EdmDigester { // assert_eq!(urc_c.read(), None); // } -// /// Regular response with traling regular response.. +// /// Regular response with trailing regular response.. // #[test] // fn at_urc() { // let mut at_pars: Ingress< diff --git a/src/command/data_mode/mod.rs b/src/command/data_mode/mod.rs index fa9da8d..ca9cdfb 100644 --- a/src/command/data_mode/mod.rs +++ b/src/command/data_mode/mod.rs @@ -7,7 +7,6 @@ use atat::atat_derive::AtatCmd; use heapless::String; use responses::*; use types::*; -use ublox_sockets::PeerHandle; use super::NoResponse; @@ -28,6 +27,7 @@ pub struct ChangeMode { /// Connects to an enabled service on a remote device. When the host connects to a /// service on a remote device, it implicitly registers to receive the "Connection Closed" /// event. +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCP", ConnectPeerResponse, timeout_ms = 5000)] pub struct ConnectPeer<'a> { @@ -38,11 +38,12 @@ pub struct ConnectPeer<'a> { /// 5.3 Close peer connection +UDCPC /// /// Closes an existing peer connection. +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDCPC", NoResponse, timeout_ms = 1000)] pub struct ClosePeerConnection { #[at_arg(position = 0, len = 1)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, } /// 5.4 Default remote peer +UDDRP @@ -65,6 +66,7 @@ pub struct SetDefaultRemotePeer<'a> { /// 5.5 Peer list +UDLP /// /// This command reads the connected peers (peer handle). +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatCmd)] #[at_cmd("+UDLP?", PeerListResponse, timeout_ms = 1000)] pub struct PeerList; @@ -127,7 +129,7 @@ pub struct SetWatchdogSettings { /// /// Writes peer configuration. /// -/// Suported parameter tags | Software Version +/// Supported parameter tags | Software Version /// ------------------------|------------------ /// 0,1 | All versions /// 2 | >= 4.0.0 diff --git a/src/command/data_mode/responses.rs b/src/command/data_mode/responses.rs index 0c8832c..fddac87 100644 --- a/src/command/data_mode/responses.rs +++ b/src/command/data_mode/responses.rs @@ -1,26 +1,26 @@ //! Responses for Data Mode use atat::atat_derive::AtatResp; -use heapless::String; -use ublox_sockets::PeerHandle; /// 5.2 Connect peer +UDCP +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct ConnectPeerResponse { #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, } /// 5.5 Peer list +UDLP +#[cfg(feature = "internal-network-stack")] #[derive(Clone, AtatResp)] pub struct PeerListResponse { #[at_arg(position = 0)] - pub peer_handle: PeerHandle, + pub peer_handle: ublox_sockets::PeerHandle, #[at_arg(position = 1)] - pub protocol: String<64>, + pub protocol: heapless::String<64>, #[at_arg(position = 2)] - pub local_address: String<64>, + pub local_address: heapless::String<64>, #[at_arg(position = 3)] - pub remote_address: String<64>, + pub remote_address: heapless::String<64>, } /// 5.12 Bind +UDBIND diff --git a/src/command/data_mode/urc.rs b/src/command/data_mode/urc.rs index 8eb2441..b768654 100644 --- a/src/command/data_mode/urc.rs +++ b/src/command/data_mode/urc.rs @@ -1,15 +1,14 @@ //! Unsolicited responses for Data mode Commands +#[allow(unused_imports)] use super::types::*; -use atat::atat_derive::AtatResp; -use atat::heapless_bytes::Bytes; -use ublox_sockets::PeerHandle; /// 5.10 Peer connected +UUDPC -#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg(feature = "internal-network-stack")] +#[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct PeerConnected { #[at_arg(position = 0)] - pub handle: PeerHandle, + pub handle: ublox_sockets::PeerHandle, #[at_arg(position = 1)] pub connection_type: ConnectionType, #[at_arg(position = 2)] @@ -18,21 +17,22 @@ pub struct PeerConnected { // pub local_address: IpAddr, #[at_arg(position = 3)] #[cfg_attr(feature = "defmt", defmt(Debug2Format))] - pub local_address: Bytes<40>, + pub local_address: atat::heapless_bytes::Bytes<40>, #[at_arg(position = 4)] pub local_port: u16, // #[at_arg(position = 5)] // pub remote_address: IpAddr, #[at_arg(position = 5)] #[cfg_attr(feature = "defmt", defmt(Debug2Format))] - pub remote_address: Bytes<40>, + pub remote_address: atat::heapless_bytes::Bytes<40>, #[at_arg(position = 6)] pub remote_port: u16, } /// 5.11 Peer disconnected +UUDPD -#[derive(Debug, PartialEq, Clone, AtatResp)] +#[cfg(feature = "internal-network-stack")] +#[derive(Debug, PartialEq, Clone, atat::atat_derive::AtatResp)] pub struct PeerDisconnected { #[at_arg(position = 0)] - pub handle: PeerHandle, + pub handle: ublox_sockets::PeerHandle, } diff --git a/src/command/edm/mod.rs b/src/command/edm/mod.rs index 691c47b..61694fb 100644 --- a/src/command/edm/mod.rs +++ b/src/command/edm/mod.rs @@ -69,7 +69,7 @@ impl atat::AtatCmd for EdmAtCmdWrapper { return Err(atat::InternalError::InvalidResponse); } - // Recieved OK response code in EDM response? + // Received OK response code in EDM response? match resp .windows(b"\r\nOK".len()) .position(|window| window == b"\r\nOK") diff --git a/src/command/edm/urc.rs b/src/command/edm/urc.rs index 7d382d8..ef629e6 100644 --- a/src/command/edm/urc.rs +++ b/src/command/edm/urc.rs @@ -20,6 +20,15 @@ pub enum EdmEvent { StartUp, } +impl EdmEvent { + pub fn extract_urc(self) -> Option { + match self { + EdmEvent::ATEvent(urc) => Some(urc), + _ => None, + } + } +} + impl AtatUrc for EdmEvent { /// The type of the response. Usually the enum this trait is implemented on. type Response = Self; @@ -64,7 +73,7 @@ impl AtatUrc for EdmEvent { }; let payload_len = calc_payload_len(resp); if resp.len() != payload_len + EDM_OVERHEAD { - error!("[Parse URC lenght Error] {:?}", LossyStr(resp)); + error!("[Parse URC length Error] {:?}", LossyStr(resp)); return None; } diff --git a/src/command/general/mod.rs b/src/command/general/mod.rs index 66e1b65..08ce8d8 100644 --- a/src/command/general/mod.rs +++ b/src/command/general/mod.rs @@ -61,8 +61,8 @@ pub struct SerialNumber2; /// /// Identificationinformation. #[derive(Clone, AtatCmd)] -#[at_cmd("I0", IdentificationInfomationTypeCodeResponse, timeout_ms = 1000)] -pub struct IdentificationInfomationTypeCode; +#[at_cmd("I0", IdentificationInformationTypeCodeResponse, timeout_ms = 1000)] +pub struct IdentificationInformationTypeCode; /// 3.9 Identification information I9 /// @@ -70,17 +70,17 @@ pub struct IdentificationInfomationTypeCode; #[derive(Clone, AtatCmd)] #[at_cmd( "I9", - IdentificationInfomationSoftwareVersionResponse, + IdentificationInformationSoftwareVersionResponse, timeout_ms = 1000 )] -pub struct IdentificationInfomationSoftwareVersion; +pub struct IdentificationInformationSoftwareVersion; /// 3.9 Identification information I10 /// /// Identificationinformation. #[derive(Clone, AtatCmd)] -#[at_cmd("I10", IdentificationInfomationMCUIDResponse, timeout_ms = 1000)] -pub struct IdentificationInfomationMCUID; +#[at_cmd("I10", IdentificationInformationMCUIDResponse, timeout_ms = 1000)] +pub struct IdentificationInformationMCUID; /// 3.11 Set greeting text +CSGT /// diff --git a/src/command/general/responses.rs b/src/command/general/responses.rs index aea6181..155c8d3 100644 --- a/src/command/general/responses.rs +++ b/src/command/general/responses.rs @@ -37,7 +37,7 @@ pub struct SerialNumberResponse { /// 3.10 Identification information I0 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationTypeCodeResponse { +pub struct IdentificationInformationTypeCodeResponse { /// Text string that identifies the serial number. #[at_arg(position = 0)] pub serial_number: String<64>, @@ -45,7 +45,7 @@ pub struct IdentificationInfomationTypeCodeResponse { /// 3.10 Identification information I9 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationSoftwareVersionResponse { +pub struct IdentificationInformationSoftwareVersionResponse { /// Text string that identifies the firmware version. #[at_arg(position = 0)] pub version: String<64>, @@ -53,7 +53,7 @@ pub struct IdentificationInfomationSoftwareVersionResponse { /// 3.10 Identification information I10 #[derive(Clone, AtatResp)] -pub struct IdentificationInfomationMCUIDResponse { +pub struct IdentificationInformationMCUIDResponse { /// Text string that identifies the serial number. #[at_arg(position = 0)] pub serial_number: String<64>, diff --git a/src/command/gpio/responses.rs b/src/command/gpio/responses.rs index e51e308..9a4bbac 100644 --- a/src/command/gpio/responses.rs +++ b/src/command/gpio/responses.rs @@ -6,7 +6,7 @@ use atat::atat_derive::AtatResp; #[derive(Clone, PartialEq, AtatResp)] pub struct ReadGPIOResponse { #[at_arg(position = 0)] - id: GPIOId, + pub id: GPIOId, #[at_arg(position = 1)] - value: GPIOValue, + pub value: GPIOValue, } diff --git a/src/command/mod.rs b/src/command/mod.rs index 083b282..864dec2 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -1,8 +1,10 @@ //! AT Commands for U-Blox short range module family\ //! Following the [u-connect ATCommands Manual](https://www.u-blox.com/sites/default/files/u-connect-ATCommands-Manual_(UBX-14044127).pdf) +#[cfg(feature = "edm")] pub mod custom_digest; pub mod data_mode; +#[cfg(feature = "edm")] pub mod edm; pub mod ethernet; pub mod general; @@ -19,7 +21,7 @@ use atat::atat_derive::{AtatCmd, AtatEnum, AtatResp, AtatUrc}; pub struct NoResponse; #[derive(Debug, Clone, AtatCmd)] -#[at_cmd("", NoResponse, timeout_ms = 1000)] +#[at_cmd("", NoResponse, attempts = 3, timeout_ms = 1000)] pub struct AT; #[derive(Debug, PartialEq, Clone, AtatUrc)] @@ -28,9 +30,11 @@ pub enum Urc { #[at_urc("+STARTUP")] StartUp, /// 5.10 Peer connected +UUDPC + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPC")] PeerConnected(data_mode::urc::PeerConnected), /// 5.11 Peer disconnected +UUDPD + #[cfg(feature = "internal-network-stack")] #[at_urc("+UUDPD")] PeerDisconnected(data_mode::urc::PeerDisconnected), /// 7.15 Wi-Fi Link connected +UUWLE diff --git a/src/command/network/mod.rs b/src/command/network/mod.rs index c80b06e..81d4c8a 100644 --- a/src/command/network/mod.rs +++ b/src/command/network/mod.rs @@ -23,7 +23,7 @@ pub struct SetNetworkHostName<'a> { /// /// Shows current status of the network interface id. #[derive(Clone, AtatCmd)] -#[at_cmd("+UNSTAT", NetworkStatusResponse, timeout_ms = 1000)] +#[at_cmd("+UNSTAT", NetworkStatusResponse, attempts = 3, timeout_ms = 1000)] pub struct GetNetworkStatus { #[at_arg(position = 0)] pub interface_id: u8, diff --git a/src/command/ping/types.rs b/src/command/ping/types.rs index 16fdaa8..3999593 100644 --- a/src/command/ping/types.rs +++ b/src/command/ping/types.rs @@ -18,12 +18,12 @@ use atat::atat_derive::AtatEnum; /// provides the TTL value received in the incoming packet. /// - Range: 1-255 /// - Default value: 32 -// pub type TTL = (u8, Option); +// pub type TTL = (u8, Option); /// The time in milliseconds to wait after an echo reply response before sending the next /// echo request. /// - Range: 0-60000 /// - Default value: 1000 -// pub type Inteval = u16; +// pub type Interval = u16; #[derive(Debug, PartialEq, Clone, Copy, AtatEnum)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/command/system/mod.rs b/src/command/system/mod.rs index ec1ac13..9ff5be2 100644 --- a/src/command/system/mod.rs +++ b/src/command/system/mod.rs @@ -29,7 +29,7 @@ pub struct SetToDefaultConfig; /// Reset to factory defined defaults. A reboot is required before using the new settings. #[derive(Debug, PartialEq, Clone, AtatCmd)] #[at_cmd("+UFACTORY", NoResponse, timeout_ms = 1000)] -pub struct ResetToFacroryDefaults; +pub struct ResetToFactoryDefaults; /// 4.4 Circuit 108/2 (DTR) behavior &D /// @@ -175,7 +175,7 @@ pub struct ModuleStart { #[at_cmd("+UMLA", NoResponse, timeout_ms = 1000)] pub struct SetLocalAddress<'a> { #[at_arg(position = 0)] - pub interface_id: InserfaceID, + pub interface_id: InterfaceID, /// MAC address of the interface id. If the address is set to 000000000000, the local /// address will be restored to factory-programmed value. /// The least significant bit of the first octet of the

must be 0; that is, the @@ -188,10 +188,10 @@ pub struct SetLocalAddress<'a> { /// /// Reads the local address of the interface id. #[derive(Debug, PartialEq, Clone, AtatCmd)] -#[at_cmd("+UMSM", LocalAddressResponse, timeout_ms = 1000)] +#[at_cmd("+UMLA", LocalAddressResponse, timeout_ms = 1000)] pub struct GetLocalAddress { #[at_arg(position = 0)] - pub interface_id: InserfaceID, + pub interface_id: InterfaceID, } /// 4.15 System status +UMSTAT @@ -215,14 +215,6 @@ pub struct SystemStatus { pub struct SetRS232Settings { #[at_arg(position = 0)] pub baud_rate: BaudRate, - // #[at_arg(position = 1)] - // pub settings: Option<( - // FlowControl, - // Option<( - // u8, - // Option<(StopBits, Option<(Parity, Option)>)>, - // )>, - // )>, #[at_arg(position = 1)] pub flow_control: FlowControl, #[at_arg(position = 2)] diff --git a/src/command/system/responses.rs b/src/command/system/responses.rs index da05fd9..fcd5de8 100644 --- a/src/command/system/responses.rs +++ b/src/command/system/responses.rs @@ -1,6 +1,6 @@ //! Responses for System Commands use super::types::*; -use atat::atat_derive::AtatResp; +use atat::{atat_derive::AtatResp, serde_at::HexStr}; use heapless::String; /// 4.11 Software update +UFWUPD @@ -17,7 +17,7 @@ pub struct LocalAddressResponse { /// MAC address of the interface id. If the address is set to 000000000000, the local /// address will be restored to factory-programmed value. #[at_arg(position = 0)] - pub mac: String<64>, + pub mac: HexStr, } /// 4.15 System status +UMSTAT diff --git a/src/command/system/types.rs b/src/command/system/types.rs index dbec961..b00b2a7 100644 --- a/src/command/system/types.rs +++ b/src/command/system/types.rs @@ -40,7 +40,7 @@ pub enum DSRAssertMode { /// DSR line when no remote peers are connected. See Connect Peer +UDCP and Default /// remote peer +UDDRP for definition of the remote peer. This applies to both incoming /// and outgoing connections. - WhenPeersConected = 2, + WhenPeersConnected = 2, } /// Echo on @@ -81,10 +81,11 @@ pub enum ModuleStartMode { #[derive(Debug, Clone, PartialEq, AtatEnum)] #[repr(u8)] -pub enum InserfaceID { - Bluetooth = 0, - WiFi = 1, - Ethernet = 2, +pub enum InterfaceID { + Bluetooth = 1, + WiFi = 2, + Ethernet = 3, + WiFiAP = 4, } #[derive(Debug, Clone, PartialEq, AtatEnum)] diff --git a/src/command/wifi/types.rs b/src/command/wifi/types.rs index 5772825..ccad35d 100644 --- a/src/command/wifi/types.rs +++ b/src/command/wifi/types.rs @@ -106,7 +106,7 @@ pub enum WifiStationConfigParameter { /// is the Wi-Fi beacon listen interval in units of beacon /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 - WiFiBeaconListenInteval = 300, + WiFiBeaconListenInterval = 300, /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -244,7 +244,7 @@ pub enum WifiStationConfig { /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 #[at_arg(value = 300)] - WiFiBeaconListenInteval(u8), + WiFiBeaconListenInterval(u8), /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon @@ -384,7 +384,7 @@ pub enum WifiStationConfigR { /// interval. The factory default value is 0, listen on all beacons. /// - Valid values 0-16 #[at_arg(value = 300)] - WiFiBeaconListenInteval(u8), + WiFiBeaconListenInterval(u8), /// Enables DTIM in power save. If the DTIM is enabled and the /// module is in power save, the access point sends an indication when new /// data is available. If disabled, the module polls for data every beacon diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..5380f32 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,31 @@ +use embedded_hal::digital::OutputPin; +use embedded_io_async::{Read, Write}; + +use crate::{command::system::types::BaudRate, DEFAULT_BAUD_RATE}; + +pub trait WifiConfig<'a> { + type ResetPin: OutputPin; + + const AT_CONFIG: atat::Config = atat::Config::new(); + + // Transport settings + const FLOW_CONTROL: bool = false; + const BAUD_RATE: BaudRate = DEFAULT_BAUD_RATE; + + #[cfg(feature = "internal-network-stack")] + const TLS_IN_BUFFER_SIZE: Option = None; + #[cfg(feature = "internal-network-stack")] + const TLS_OUT_BUFFER_SIZE: Option = None; + + #[cfg(feature = "ppp")] + const PPP_CONFIG: embassy_net_ppp::Config<'a>; + + fn reset_pin(&mut self) -> Option<&mut Self::ResetPin> { + None + } +} + +pub trait Transport: Write + Read { + fn set_baudrate(&mut self, baudrate: u32); + fn split_ref(&mut self) -> (impl Write, impl Read); +} diff --git a/src/connection.rs b/src/connection.rs index c270887..b5f7292 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,3 +1,5 @@ +use no_std_net::Ipv4Addr; + use crate::network::{WifiMode, WifiNetwork}; #[derive(Debug, Clone, Copy, PartialEq)] @@ -6,46 +8,80 @@ pub enum WiFiState { Inactive, /// Searching for Wifi NotConnected, + SecurityProblems, Connected, } -// Fold into wifi connectivity +/// Static IP address configuration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StaticConfigV4 { + /// IP address and subnet mask. + pub address: Ipv4Addr, + /// Default gateway. + pub gateway: Option, + /// DNS servers. + pub dns_servers: DnsServers, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DnsServers { + pub primary: Option, + pub secondary: Option, +} + pub struct WifiConnection { - /// Keeps track of connection state on module pub wifi_state: WiFiState, - pub network_up: bool, - pub network: WifiNetwork, - /// Number from 0-9. 255 used for unknown - pub config_id: u8, - /// Keeps track of activation of the config by driver - pub activated: bool, + pub ipv6_link_local_up: bool, + pub ipv4_up: bool, + #[cfg(feature = "ipv6")] + pub ipv6_up: bool, + pub network: Option, } impl WifiConnection { - pub(crate) fn new(network: WifiNetwork, wifi_state: WiFiState, config_id: u8) -> Self { + pub(crate) const fn new() -> Self { WifiConnection { - wifi_state, - network_up: false, - network, - config_id, - activated: false, + wifi_state: WiFiState::Inactive, + ipv6_link_local_up: false, + network: None, + ipv4_up: false, + #[cfg(feature = "ipv6")] + ipv6_up: false, } } + #[allow(dead_code)] pub fn is_station(&self) -> bool { - self.network.mode == WifiMode::Station + self.network + .as_ref() + .map(|n| n.mode == WifiMode::Station) + .unwrap_or_default() } + #[allow(dead_code)] pub fn is_access_point(&self) -> bool { !self.is_station() } - pub(crate) fn activate(mut self) -> Self { - self.activated = true; - self + /// Get whether the network stack has a valid IP configuration. + /// This is true if the network stack has a static IP configuration or if DHCP has completed + pub fn is_config_up(&self) -> bool { + let v6_up; + let v4_up = self.ipv4_up; + + #[cfg(feature = "ipv6")] + { + v6_up = self.ipv6_up; + } + #[cfg(not(feature = "ipv6"))] + { + v6_up = false; + } + + (v4_up || v6_up) && self.ipv6_link_local_up } - pub(crate) fn deactivate(&mut self) { - self.activated = false; + pub fn is_connected(&self) -> bool { + self.is_config_up() && self.wifi_state == WiFiState::Connected } } diff --git a/src/error.rs b/src/error.rs index 5c4d426..fd38a1b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "internal-network-stack")] pub use ublox_sockets::Error as SocketError; #[derive(Debug)] @@ -6,6 +7,7 @@ pub enum Error { Overflow, SetState, BadLength, + SecurityProblems, Network, Pin, BaudDetection, @@ -17,6 +19,7 @@ pub enum Error { // NetworkState(crate::wifi::connection::NetworkState), NoWifiSetup, // WifiState(crate::wifi::connection::WiFiState), + #[cfg(feature = "internal-network-stack")] Socket(ublox_sockets::Error), AT(atat::Error), Busy, @@ -40,6 +43,13 @@ impl From for Error { } } +impl From for Error { + fn from(_: embassy_time::TimeoutError) -> Self { + Error::Timeout + } +} + +#[cfg(feature = "internal-network-stack")] impl From for Error { fn from(e: ublox_sockets::Error) -> Self { Error::Socket(e) @@ -94,7 +104,7 @@ pub enum WifiError { #[derive(Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum WifiHotspotError { - /// Failed to ceate wireless hotspot. + /// Failed to create wireless hotspot. CreationFailed, /// Failed to stop wireless hotspot service. Try turning off /// the wireless interface via ```wifi.turn_off()```. diff --git a/src/fmt.rs b/src/fmt.rs index 81c9940..35b929f 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -1,11 +1,12 @@ #![macro_use] -#![allow(unused_macros)] +#![allow(unused)] -use core::fmt::Debug; +use core::fmt::{Debug, Display, LowerHex}; #[cfg(all(feature = "defmt", feature = "log"))] compile_error!("You may not enable both `defmt` and `log` features."); +#[collapse_debuginfo(yes)] macro_rules! assert { ($($x:tt)*) => { { @@ -17,6 +18,7 @@ macro_rules! assert { }; } +#[collapse_debuginfo(yes)] macro_rules! assert_eq { ($($x:tt)*) => { { @@ -28,6 +30,7 @@ macro_rules! assert_eq { }; } +#[collapse_debuginfo(yes)] macro_rules! assert_ne { ($($x:tt)*) => { { @@ -39,39 +42,43 @@ macro_rules! assert_ne { }; } +#[collapse_debuginfo(yes)] macro_rules! debug_assert { ($($x:tt)*) => { { #[cfg(not(feature = "defmt"))] ::core::debug_assert!($($x)*); #[cfg(feature = "defmt")] - ::debug_assert!($($x)*); + ::defmt::debug_assert!($($x)*); } }; } +#[collapse_debuginfo(yes)] macro_rules! debug_assert_eq { ($($x:tt)*) => { { #[cfg(not(feature = "defmt"))] ::core::debug_assert_eq!($($x)*); #[cfg(feature = "defmt")] - ::debug_assert_eq!($($x)*); + ::defmt::debug_assert_eq!($($x)*); } }; } +#[collapse_debuginfo(yes)] macro_rules! debug_assert_ne { ($($x:tt)*) => { { #[cfg(not(feature = "defmt"))] ::core::debug_assert_ne!($($x)*); #[cfg(feature = "defmt")] - ::debug_assert_ne!($($x)*); + ::defmt::debug_assert_ne!($($x)*); } }; } +#[collapse_debuginfo(yes)] macro_rules! todo { ($($x:tt)*) => { { @@ -84,6 +91,7 @@ macro_rules! todo { } #[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] macro_rules! unreachable { ($($x:tt)*) => { ::core::unreachable!($($x)*) @@ -91,12 +99,14 @@ macro_rules! unreachable { } #[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] macro_rules! unreachable { ($($x:tt)*) => { ::defmt::unreachable!($($x)*) }; } +#[collapse_debuginfo(yes)] macro_rules! panic { ($($x:tt)*) => { { @@ -108,6 +118,7 @@ macro_rules! panic { }; } +#[collapse_debuginfo(yes)] macro_rules! trace { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -121,6 +132,7 @@ macro_rules! trace { }; } +#[collapse_debuginfo(yes)] macro_rules! debug { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -134,6 +146,7 @@ macro_rules! debug { }; } +#[collapse_debuginfo(yes)] macro_rules! info { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -147,6 +160,7 @@ macro_rules! info { }; } +#[collapse_debuginfo(yes)] macro_rules! warn { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -160,6 +174,7 @@ macro_rules! warn { }; } +#[collapse_debuginfo(yes)] macro_rules! error { ($s:literal $(, $x:expr)* $(,)?) => { { @@ -174,6 +189,7 @@ macro_rules! error { } #[cfg(feature = "defmt")] +#[collapse_debuginfo(yes)] macro_rules! unwrap { ($($x:tt)*) => { ::defmt::unwrap!($($x)*) @@ -181,6 +197,7 @@ macro_rules! unwrap { } #[cfg(not(feature = "defmt"))] +#[collapse_debuginfo(yes)] macro_rules! unwrap { ($arg:expr) => { match $crate::fmt::Try::into_result($arg) { @@ -228,3 +245,30 @@ impl Try for Result { self } } + +pub(crate) struct Bytes<'a>(pub &'a [u8]); + +impl<'a> Debug for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> Display for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +impl<'a> LowerHex for Bytes<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:#02x?}", self.0) + } +} + +#[cfg(feature = "defmt")] +impl<'a> defmt::Format for Bytes<'a> { + fn format(&self, fmt: defmt::Formatter) { + defmt::write!(fmt, "{:02x}", self.0) + } +} diff --git a/src/lib.rs b/src/lib.rs index c1adaa5..283875a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,37 +1,34 @@ -#![cfg_attr(all(not(test), not(feature = "std")), no_std)] +#![cfg_attr(not(test), no_std)] #![allow(async_fn_in_trait)] -mod fmt; +#[cfg(all(feature = "ppp", feature = "internal-network-stack"))] +compile_error!("You may not enable both `ppp` and `internal-network-stack` features."); -pub mod asynch; +#[cfg(not(any( + feature = "odin-w2xx", + feature = "nina-w1xx", + feature = "nina-b1xx", + feature = "anna-b1xx", + feature = "nina-b2xx", + feature = "nina-b3xx" +)))] +compile_error!("No module feature activated. You must activate exactly one of the following features: odin-w2xx, nina-w1xx, nina-b1xx, anna-b1xx, nina-b2xx, nina-b3xx"); -pub use embedded_nal_async; +mod fmt; -pub use ublox_sockets; +pub mod asynch; +mod config; mod connection; mod network; -mod peer_builder; -// mod blocking; mod hex; pub use atat; pub mod command; pub mod error; -// pub mod wifi; -pub use peer_builder::SecurityCredentials; - -// TODO: -// - UDP stack -// - Secure sockets -// - Network scan -// - AP Mode (control) -// - TCP listener stack -// - (Blocking client?) -// - -// -// FIXME: -// - PWR/Restart stuff doesn't fully work -// - +pub use config::{Transport, WifiConfig}; + +use command::system::types::BaudRate; +pub const DEFAULT_BAUD_RATE: BaudRate = BaudRate::B115200; diff --git a/src/network.rs b/src/network.rs index e980dc9..fc0a593 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use crate::command::wifi::types::{OperationMode, ScannedWifiNetwork}; use crate::error::WifiError; use crate::hex::from_hex; @@ -6,7 +8,7 @@ use heapless::String; use core::convert::TryFrom; -#[derive(PartialEq, Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WifiMode { Station, AccessPoint, diff --git a/src/wifi/ap.rs b/src/wifi/ap.rs deleted file mode 100644 index 5995140..0000000 --- a/src/wifi/ap.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::{ - blocking::UbloxClient, - command::{ - edm::EdmAtCmdWrapper, - wifi::{ - self, - types::{ - AccessPointAction, AccessPointConfig, AccessPointId, IPv4Mode, PasskeyR, - SecurityMode, SecurityModePSK, - }, - SetWifiAPConfig, WifiAPAction, - }, - }, - error::WifiHotspotError, - wifi::{ - network::{WifiMode, WifiNetwork}, - options::{ConnectionOptions, HotspotOptions}, - }, -}; -use atat::blocking::AtatClient; -use atat::heapless_bytes::Bytes; -use embedded_hal::digital::OutputPin; - -use super::connection::{WiFiState, WifiConnection}; - -impl UbloxClient -where - C: AtatClient, - RST: OutputPin, -{ - /// Creates wireless hotspot service for host machine. - pub fn create_hotspot( - &mut self, - options: ConnectionOptions, - configuration: HotspotOptions, - ) -> Result<(), WifiHotspotError> { - let ap_config_id = AccessPointId::Id0; - - // Network part - // Deactivate network id 0 - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Deactivate, - }), - true, - )?; - - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Reset, - }), - true, - )?; - - if let Some(ref con) = self.wifi_connection { - if con.activated { - return Err(WifiHotspotError::CreationFailed); - } - } - - // Disable DHCP Server (static IP address will be used) - if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::IPv4Mode(IPv4Mode::Static), - }), - true, - )?; - } - - // Network IP address - if let Some(ip) = options.ip { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::IPv4Address(ip), - }), - true, - )?; - } - // Network Subnet mask - if let Some(subnet) = options.subnet { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SubnetMask(subnet), - }), - true, - )?; - } - // Network Default gateway - if let Some(gateway) = options.gateway { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::DefaultGateway(gateway), - }), - true, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::DHCPServer(true.into()), - }), - true, - )?; - - // Active on startup - // self.send_internal(&SetWifiAPConfig{ - // ap_config_id, - // ap_config_param: AccessPointConfig::ActiveOnStartup(true), - // }, true)?; - - // Wifi part - // Set the Network SSID to connect to - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SSID(options.ssid.clone()), - }), - true, - )?; - - if let Some(pass) = options.password.clone() { - // Use WPA2 as authentication type - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SecurityMode( - SecurityMode::Wpa2AesCcmp, - SecurityModePSK::PSK, - ), - }), - true, - )?; - - // Input passphrase - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::PSKPassphrase(PasskeyR::Passphrase(pass)), - }), - true, - )?; - } else { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::SecurityMode( - SecurityMode::Open, - SecurityModePSK::Open, - ), - }), - true, - )?; - } - - if let Some(channel) = configuration.channel { - self.send_internal( - &EdmAtCmdWrapper(SetWifiAPConfig { - ap_config_id, - ap_config_param: AccessPointConfig::Channel(channel as u8), - }), - true, - )?; - } - - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Activate, - }), - true, - )?; - - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: Bytes::new(), - op_mode: wifi::types::OperationMode::AdHoc, - ssid: options.ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: WifiMode::AccessPoint, - }, - WiFiState::NotConnected, - ap_config_id as u8, - ) - .activate(), - ); - Ok(()) - } - - /// Stop serving a wireless network. - /// - /// **NOTE: All users connected will automatically be disconnected.** - pub fn stop_hotspot(&mut self) -> Result<(), WifiHotspotError> { - let ap_config_id = AccessPointId::Id0; - - if let Some(ref con) = self.wifi_connection { - if con.activated { - self.send_internal( - &EdmAtCmdWrapper(WifiAPAction { - ap_config_id, - ap_action: AccessPointAction::Deactivate, - }), - true, - )?; - } - } else { - return Err(WifiHotspotError::FailedToStop); - } - if let Some(ref mut con) = self.wifi_connection { - con.deactivate() - } - - Ok(()) - } -} diff --git a/src/wifi/mod.rs b/src/wifi/mod.rs deleted file mode 100644 index 15f6b48..0000000 --- a/src/wifi/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -pub use ublox_sockets::{PeerHandle, SocketHandle}; - -use crate::command::edm::types::ChannelId; - -pub mod ap; -pub mod connection; -pub mod network; -pub mod options; -pub mod supplicant; - -pub mod peer_builder; - -pub(crate) const EGRESS_CHUNK_SIZE: usize = 512; -/// The socket map, keeps mappings between `ublox::sockets`s `SocketHandle`, -/// and the modems `PeerHandle` and `ChannelId`. The peer handle is used -/// for controlling the connection, while the channel id is used for sending -/// data over the connection in EDM mode. -pub struct SocketMap { - channel_map: heapless::FnvIndexMap, - peer_map: heapless::FnvIndexMap, -} - -impl Default for SocketMap { - fn default() -> Self { - Self::new() - } -} - -impl SocketMap { - fn new() -> Self { - Self { - channel_map: heapless::FnvIndexMap::new(), - peer_map: heapless::FnvIndexMap::new(), - } - } - - pub fn insert_channel( - &mut self, - channel_id: ChannelId, - socket_handle: SocketHandle, - ) -> Result<(), ()> { - trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, channel_id); - self.channel_map - .insert(channel_id, socket_handle) - .map_err(drop)?; - Ok(()) - } - - pub fn remove_channel(&mut self, channel_id: &ChannelId) -> Result<(), ()> { - trace!("[SOCK_MAP] {:?} removed", channel_id); - self.channel_map.remove(channel_id).ok_or(())?; - Ok(()) - } - - pub fn channel_to_socket(&self, channel_id: &ChannelId) -> Option<&SocketHandle> { - self.channel_map.get(channel_id) - } - - pub fn socket_to_channel_id(&self, socket_handle: &SocketHandle) -> Option<&ChannelId> { - self.channel_map - .iter() - .find_map(|(c, s)| if s == socket_handle { Some(c) } else { None }) - } - - pub fn insert_peer(&mut self, peer: PeerHandle, socket_handle: SocketHandle) -> Result<(), ()> { - trace!("[SOCK_MAP] {:?} tied to {:?}", socket_handle, peer); - self.peer_map.insert(peer, socket_handle).map_err(drop)?; - Ok(()) - } - - pub fn remove_peer(&mut self, peer: &PeerHandle) -> Result<(), ()> { - trace!("[SOCK_MAP] {:?} removed", peer); - self.peer_map.remove(peer).ok_or(())?; - Ok(()) - } - - pub fn peer_to_socket(&self, peer: &PeerHandle) -> Option<&SocketHandle> { - self.peer_map.get(peer) - } - - pub fn socket_to_peer(&self, socket_handle: &SocketHandle) -> Option<&PeerHandle> { - self.peer_map - .iter() - .find_map(|(c, s)| if s == socket_handle { Some(c) } else { None }) - } -} diff --git a/src/wifi/options.rs b/src/wifi/options.rs deleted file mode 100644 index aac9dd3..0000000 --- a/src/wifi/options.rs +++ /dev/null @@ -1,137 +0,0 @@ -use embedded_nal::Ipv4Addr; -use heapless::String; -use serde::{Deserialize, Serialize}; - -#[allow(dead_code)] -#[derive(Debug, Clone, Copy)] -/// Channel to broadcast wireless hotspot on. -pub enum Channel { - /// Channel 1 - One = 1, - /// Channel 2 - Two = 2, - /// Channel 3 - Three = 3, - /// Channel 4 - Four = 4, - /// Channel 5 - Five = 5, - /// Channel 6 - Six = 6, -} - -#[allow(dead_code)] -#[derive(Debug)] -/// Band type of wireless hotspot. -pub enum Band { - /// Band `A` - A, - /// Band `BG` - Bg, -} - -#[derive(Debug, Default)] -pub struct HotspotOptions { - pub(crate) channel: Option, - pub(crate) band: Option, -} - -impl HotspotOptions { - pub fn new() -> Self { - Self { - channel: Some(Channel::One), - band: Some(Band::Bg), - } - } - - pub fn channel(mut self, channel: Channel) -> Self { - self.channel = Some(channel); - self - } - - pub fn band(mut self, band: Band) -> Self { - self.band = Some(band); - self - } -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct ConnectionOptions { - pub ssid: String<64>, - pub password: Option>, - - #[defmt(Debug2Format)] - pub ip: Option, - #[defmt(Debug2Format)] - pub subnet: Option, - #[defmt(Debug2Format)] - pub gateway: Option, -} - -impl ConnectionOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn ssid(mut self, ssid: String<64>) -> Self { - self.ssid = ssid; - self - } - - pub fn password(mut self, password: String<64>) -> Self { - self.password = Some(password); - self - } - - pub fn ip_address(mut self, ip_addr: Ipv4Addr) -> Self { - self.ip = Some(ip_addr); - self.subnet = if let Some(subnet) = self.subnet { - Some(subnet) - } else { - Some(Ipv4Addr::new(255, 255, 255, 0)) - }; - - self.gateway = if let Some(gateway) = self.gateway { - Some(gateway) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - self - } - - pub fn subnet_address(mut self, subnet_addr: Ipv4Addr) -> Self { - self.subnet = Some(subnet_addr); - - self.ip = if let Some(ip) = self.ip { - Some(ip) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - - self.gateway = if let Some(gateway) = self.gateway { - Some(gateway) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - - self - } - - pub fn gateway_address(mut self, gateway_addr: Ipv4Addr) -> Self { - self.gateway = Some(gateway_addr); - - self.subnet = if let Some(subnet) = self.subnet { - Some(subnet) - } else { - Some(Ipv4Addr::new(255, 255, 255, 0)) - }; - - self.ip = if let Some(ip) = self.ip { - Some(ip) - } else { - Some(Ipv4Addr::new(192, 168, 2, 1)) - }; - self - } -} diff --git a/src/wifi/supplicant.rs b/src/wifi/supplicant.rs deleted file mode 100644 index dabd5bf..0000000 --- a/src/wifi/supplicant.rs +++ /dev/null @@ -1,603 +0,0 @@ -use heapless::Vec; - -use crate::{ - command::{ - edm::EdmAtCmdWrapper, - system::RebootDCE, - wifi::{ - responses::GetWifiStationConfigResponse, - types::{ - Authentication, IPv4Mode, WifiStationAction, WifiStationConfig, - WifiStationConfigParameter, WifiStationConfigR, - }, - ExecWifiStationAction, GetWifiStationConfig, SetWifiStationConfig, WifiScan, - }, - }, - error::{Error, WifiConnectionError, WifiError}, -}; - -use super::{ - connection::{WiFiState, WifiConnection}, - network::WifiNetwork, - options::ConnectionOptions, -}; - -use debug; - -/// Supplicant is used to -/// -/// -/// ``` -/// // Add, activate and remove network -/// let network = ConnectionOptions::new().ssid("my-ssid").password("hunter2"); -/// let config_id: u8 = 0; -/// let mut supplicant = ublox.supplicant::().unwrap(); -/// -/// supplicant.upsert_connection(config_id, network).unwrap(); -/// supplicant.activate(config_id).unwrap(); -/// while ublox.connected_to_network().is_err() { -/// ublox.spin().ok(); -/// } -/// // Now connected to a wifi network. -/// -/// let mut supplicant = ublox.supplicant::().unwrap(); -/// supplicant.deactivate(0).unwrap(); -/// // Connection has to be down before removal -/// while ublox.supplicant::().unwrap().get_active_config().is_some() { -/// ublox.spin().ok(); -/// } -/// let mut supplicant = ublox.supplicant::().unwrap(); -/// supplicant.remove_connection(0) -/// -/// -pub struct Supplicant<'a, C, const N: usize> { - pub(crate) client: &'a mut C, - pub(crate) wifi_connection: &'a mut Option, - pub(crate) active_on_startup: &'a mut Option, -} - -impl<'a, C, const N: usize> Supplicant<'a, C, N> -where - C: atat::blocking::AtatClient, -{ - fn send_at(&mut self, req: &A) -> Result { - self.client.send(req).map_err(|e| { - error!("{:?}: {=[u8]:a}", e, req.as_bytes()); - e.into() - }) - } - - // pub fn load(&mut self) { - // for config_id in 0..N as u8 { - // self.client - // .send(&EdmAtCmdWrapper(ExecWifiStationAction { - // config_id, - // action: WifiStationAction::Load, - // })) - // .ok(); - // } - // } - pub(crate) fn init(&mut self) -> Result<(), Error> { - debug!("[SUP] init"); - for config_id in 0..N as u8 { - let load = self.client.send(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Load, - })); - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::ActiveOnStartup), - }))?; - - if parameter == WifiStationConfigR::ActiveOnStartup(true.into()) { - debug!("[SUP] Config {:?} is active on startup", config_id); - if *self.active_on_startup == None || *self.active_on_startup == Some(config_id) { - *self.active_on_startup = Some(config_id); - // Update wifi connection - if self.wifi_connection.is_none() { - let con = self.get_connection(config_id)?.unwrap_or_default(); - - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: atat::heapless_bytes::Bytes::new(), - op_mode: - crate::command::wifi::types::OperationMode::Infrastructure, - ssid: con.ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: super::network::WifiMode::Station, - }, - WiFiState::NotConnected, - config_id, - ) - .activate(), - ); - } else if let Some(ref mut con) = self.wifi_connection { - if con.config_id == 255 { - con.config_id = config_id; - } - } - // One could argue that an excisting connection should be verified, - // but should this be the case, the module is already having unexpected behaviour - } else { - // This causes unexpected behaviour - error!("Two configs are active on startup!"); - return Err(Error::Supplicant); - } - } else if load.is_err() { - //Handle shadow store bug - //TODO: Check if the ssid is set, if so the credential has to be cleared, as it is not there actually. - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::SSID), - }))?; - - if let WifiStationConfigR::SSID(ssid) = parameter { - if !ssid.is_empty() { - error!("Shadow store bug!"); - // This should fix the issue - self.remove_connection(config_id) - .map_err(|_| Error::Supplicant)?; - self.send_at(&EdmAtCmdWrapper(RebootDCE)).ok(); - return Err(Error::ShadowStoreBug); - } - } - } - } - Ok(()) - } - - pub fn get_connection(&mut self, config_id: u8) -> Result, Error> { - debug!("[SUP] Get connection: {:?}", config_id); - let GetWifiStationConfigResponse { - parameter: ip_mode, .. - } = self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::IPv4Mode), - }))?; - - let mut options = ConnectionOptions { - ssid: heapless::String::new(), - password: None, - ip: None, - subnet: None, - gateway: None, - }; - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::SSID), - }))?; - - if let WifiStationConfigR::SSID(ssid) = parameter { - if ssid.is_empty() { - return Ok(None); - } - options.ssid = ssid; - } - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::Authentication), - }))?; - - if let WifiStationConfigR::Authentication(auth) = parameter { - if !matches!(auth, Authentication::Open) { - options.password = Some(heapless::String::from("***")); - } - } - - if let WifiStationConfigR::IPv4Mode(IPv4Mode::Static) = ip_mode { - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::IPv4Address), - }))?; - - if let WifiStationConfigR::IPv4Address(ip) = parameter { - options.ip = Some(ip); - } - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::SubnetMask), - }))?; - - if let WifiStationConfigR::SubnetMask(subnet) = parameter { - options.subnet = Some(subnet); - } - - let GetWifiStationConfigResponse { parameter, .. } = - self.send_at(&EdmAtCmdWrapper(GetWifiStationConfig { - config_id, - parameter: Some(WifiStationConfigParameter::DefaultGateway), - }))?; - - if let WifiStationConfigR::DefaultGateway(gateway) = parameter { - options.gateway = Some(gateway); - } - } - - Ok(Some(options)) - } - - /// Get id of active config - pub fn get_active_config_id(&self) -> Option { - debug!("[SUP] Get active config id"); - if let Some(ref wifi) = self.wifi_connection { - if wifi.wifi_state != WiFiState::Inactive { - debug!("[SUP] Active: {:?}", wifi.config_id); - return Some(wifi.config_id); - } - } - None - } - - /// Get id of active config - pub fn has_active_config_id(&self) -> bool { - if let Some(ref wifi) = self.wifi_connection { - if wifi.wifi_state != WiFiState::Inactive { - return true; - } - } - false - } - - /// List connections stored in module - /// - /// Sorted by config ID - pub fn list_connections(&mut self) -> Result, Error> { - debug!("[SUP] list connections"); - Ok((0..N as u8) - .filter_map(|config_id| { - self.get_connection(config_id) - .unwrap() - .map(|c| (config_id, c)) - }) - .collect()) - } - - /// Attempts to remove a stored wireless network - /// - /// Removing the active connection is not possible. Deactivate the network first. - pub fn remove_connection(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - // self.deactivate(config_id)?; - debug!("[SUP] Remove connection: {:?}", config_id); - // check for active - if self.is_config_in_use(config_id) { - error!("Config id is active!"); - return Err(WifiConnectionError::Illegal); - } - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Reset, - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Store, - }))?; - - if Some(config_id) == self.get_active_on_startup() { - self.unset_active_on_startup()?; - } - // debug!("[SUP] Remove config: {:?}", config_id); - - Ok(()) - } - - /// Attempts to store a wireless network with the given connection options. - /// - /// Replacing the currently active network is not possible. - pub fn insert_connection( - &mut self, - config_id: u8, - options: &ConnectionOptions, - ) -> Result<(), WifiConnectionError> { - debug!("[SUP] Insert config: {:?}", config_id); - // Network part - // Reset network config slot - - // check for active - if self.is_config_in_use(config_id) { - error!("Config id is active!"); - return Err(WifiConnectionError::Illegal); - } - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Reset, - }))?; - if Some(config_id) == self.get_active_on_startup() { - self.unset_active_on_startup()?; - } - - // Disable DHCP Client (static IP address will be used) - if options.ip.is_some() || options.subnet.is_some() || options.gateway.is_some() { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::IPv4Mode(IPv4Mode::Static), - }))?; - } - - // Network IP address - if let Some(ip) = options.ip { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::IPv4Address(ip), - }))?; - } - // Network Subnet mask - if let Some(subnet) = options.subnet { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::SubnetMask(subnet), - }))?; - } - // Network Default gateway - if let Some(gateway) = options.gateway { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::DefaultGateway(gateway), - }))?; - } - - // Wifi part - // Set the Network SSID to connect to - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::SSID(options.ssid.clone()), - }))?; - - if let Some(pass) = options.password.clone() { - // Use WPA2 as authentication type - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }))?; - - // Input passphrase - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::WpaPskOrPassphrase(pass), - }))?; - } else { - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::Authentication(Authentication::Open), - }))?; - } - - // Store config - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Store, - }))?; - - Ok(()) - } - - /// Activate a given network config - /// Only one config can be active at any time. - /// - /// The driver has two modes of active for ID's. Active in driver and active on module. - /// These are differentiated by the driver mode being called activated and modules - /// mode called active. The driver activates a config, and then the driver reacts - /// asyncronous to the request and sets a config as active. - /// Driver activation is seen as `wificonnection.active()`. - /// Module is seen in `wificonnection.state`, where `inactive` is inactive and all others - /// are activated. - /// - /// The activation flow is as follows: - /// - /// driver.activate() driver.deactivate() - /// ┼─────────────────────────┼ - /// ┼─────────────────────────┼ - /// module is active module inactive - pub fn activate(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - debug!("[SUP] Activate connection: {:?}", config_id); - if let Some(w) = self.wifi_connection { - if w.activated { - return Err(WifiConnectionError::Illegal); - } - } - - let con = self.get_connection(config_id)?.unwrap_or_default(); - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Activate, - }))?; - - self.wifi_connection.replace( - WifiConnection::new( - WifiNetwork { - bssid: atat::heapless_bytes::Bytes::new(), - op_mode: crate::command::wifi::types::OperationMode::Infrastructure, - ssid: con.ssid, - channel: 0, - rssi: 1, - authentication_suites: 0, - unicast_ciphers: 0, - group_ciphers: 0, - mode: super::network::WifiMode::Station, - }, - WiFiState::Inactive, - config_id, - ) - .activate(), - ); - debug!("[SUP] Activated: {:?}", config_id); - - Ok(()) - } - - /// Deactivates a given network config - /// - /// Operation not done until network conneciton is lost - pub fn deactivate(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - debug!("[SUP] Deactivate connection: {:?}", config_id); - let mut active = false; - - if let Some(con) = self.wifi_connection { - if con.activated && con.config_id == config_id { - active = true; - } - } - - if active { - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Deactivate, - }))?; - - if let Some(ref mut con) = self.wifi_connection { - con.deactivate(); - } - debug!("[SUP] Deactivated: {:?}", config_id); - } - Ok(()) - } - - pub fn scan(&mut self) -> Result, WifiError> { - match self.send_at(&EdmAtCmdWrapper(WifiScan { ssid: None })) { - Ok(resp) => resp - .network_list - .into_iter() - .map(WifiNetwork::try_from) - .collect(), - Err(_) => Err(WifiError::UnexpectedResponse), - } - } - - pub fn is_connected(&self) -> bool { - self.wifi_connection - .as_ref() - .map(WifiConnection::is_connected) - .unwrap_or_default() - } - - pub fn flush(&mut self) -> Result<(), WifiConnectionError> { - todo!() - } - - /// Returns Active on startup config ID if any - pub fn get_active_on_startup(&self) -> Option { - debug!( - "[SUP] Get active on startup: {:?}", - self.active_on_startup.clone() - ); - return self.active_on_startup.clone(); - } - - /// Returns Active on startup config ID if any - pub fn has_active_on_startup(&self) -> bool { - return self.active_on_startup.is_some(); - } - - /// Sets a config as active on startup, replacing the current. - /// - /// This is not possible if any of the two are currently active. - pub fn set_active_on_startup(&mut self, config_id: u8) -> Result<(), WifiConnectionError> { - debug!("[SUP] Set active on startup connection: {:?}", config_id); - // check end condition true - if let Some(active_on_startup) = *self.active_on_startup { - if active_on_startup == config_id { - return Ok(()); - } - // check for active connection - if self.is_config_in_use(active_on_startup) { - error!("Active on startup is active!"); - return Err(WifiConnectionError::Illegal); - } - } - if self.is_config_in_use(config_id) { - error!("Config id is active!"); - return Err(WifiConnectionError::Illegal); - } - - // disable current active on startup - if let Some(active_on_startup) = *self.active_on_startup { - // if any active on startup remove this parameter. - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id: active_on_startup, - config_param: WifiStationConfig::ActiveOnStartup(false.into()), - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id: active_on_startup, - action: WifiStationAction::Store, - }))?; - } - - // Insert the new one as active on startup. - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id, - config_param: WifiStationConfig::ActiveOnStartup(true.into()), - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id, - action: WifiStationAction::Store, - }))?; - - *self.active_on_startup = Some(config_id); - - Ok(()) - } - - /// Unsets a config as active on startup, replacing the current. - /// - /// This is not possible if any of the two are currently active. - pub fn unset_active_on_startup(&mut self) -> Result<(), WifiConnectionError> { - debug!("[SUP] Unset active on startup connection"); - // check for any of them as active - if let Some(active_on_startup) = self.active_on_startup.clone() { - // check for active connection - if self.is_config_in_use(active_on_startup) { - error!("Active on startup is active!"); - return Err(WifiConnectionError::Illegal); - } - // if any active remove this asset. - self.send_at(&EdmAtCmdWrapper(SetWifiStationConfig { - config_id: active_on_startup, - config_param: WifiStationConfig::ActiveOnStartup(false.into()), - }))?; - - self.send_at(&EdmAtCmdWrapper(ExecWifiStationAction { - config_id: active_on_startup, - action: WifiStationAction::Store, - }))?; - *self.active_on_startup = None; - } - Ok(()) - } - - /// Checks for active and activated. - /// See self.activate for explanation. - fn is_config_in_use(&self, config_id: u8) -> bool { - if let Some(active_id) = self.get_active_config_id() { - if active_id == config_id { - return true; - } - } else if let Some(ref con) = self.wifi_connection { - if con.activated && con.config_id == config_id { - error!("One of the IDs being changed is activated!"); - return true; - } - } - false - } -} From 0b88cc0b78c949b397ede30d23051bfa39ea0632 Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 18 Jul 2024 09:12:32 +0200 Subject: [PATCH 16/16] Fix defmt feature gating --- src/asynch/control.rs | 52 ++++++++++++++++++++------------------- src/command/wifi/mod.rs | 33 ++++++++++++++----------- src/command/wifi/types.rs | 4 ++- src/network.rs | 5 ++-- 4 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/asynch/control.rs b/src/asynch/control.rs index 8f03d79..67d648b 100644 --- a/src/asynch/control.rs +++ b/src/asynch/control.rs @@ -307,7 +307,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> Ok(()) } - async fn start_ap(&self, ssid: &str) -> Result<(), Error> { + async fn start_ap(&self, ssid: &str, _channel: u8) -> Result<(), Error> { self.state_ch.wait_for_initialized().await; // Deactivate network id 0 @@ -432,13 +432,18 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> } /// Start open access point. - pub async fn start_ap_open(&mut self, ssid: &str, channel: u8) { - todo!() + pub async fn start_ap_open(&mut self, ssid: &str, channel: u8) -> Result<(), Error> { + self.start_ap(ssid, channel).await } /// Start WPA2 protected access point. - pub async fn start_ap_wpa2(&mut self, ssid: &str, passphrase: &str, channel: u8) { - todo!() + pub async fn start_ap_wpa2( + &mut self, + ssid: &str, + _passphrase: &str, + channel: u8, + ) -> Result<(), Error> { + self.start_ap(ssid, channel).await } /// Closes access point. @@ -477,9 +482,7 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> (&self.at_client) .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, - config_param: WifiStationConfig::SSID( - heapless::String::try_from(ssid).map_err(|_| Error::Overflow)?, - ), + config_param: WifiStationConfig::SSID(ssid), }) .await?; @@ -503,26 +506,25 @@ impl<'a, const INGRESS_BUF_SIZE: usize, const URC_CAPACITY: usize> (&self.at_client) .send_retry(&SetWifiStationConfig { config_id: CONFIG_ID, - config_param: WifiStationConfig::WpaPskOrPassphrase( - heapless::String::try_from(passphrase).map_err(|_| Error::Overflow)?, - ), + config_param: WifiStationConfig::WpaPskOrPassphrase(passphrase), }) .await?; } - WifiAuthentication::Wpa2Psk(psk) => { - (&self.at_client) - .send_retry(&SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), - }) - .await?; - - (&self.at_client) - .send_retry(&SetWifiStationConfig { - config_id: CONFIG_ID, - config_param: WifiStationConfig::WpaPskOrPassphrase(todo!("hex values?!")), - }) - .await?; + WifiAuthentication::Wpa2Psk(_psk) => { + unimplemented!() + // (&self.at_client) + // .send_retry(&SetWifiStationConfig { + // config_id: CONFIG_ID, + // config_param: WifiStationConfig::Authentication(Authentication::WpaWpa2Psk), + // }) + // .await?; + + // (&self.at_client) + // .send_retry(&SetWifiStationConfig { + // config_id: CONFIG_ID, + // config_param: WifiStationConfig::WpaPskOrPassphrase(todo!("hex values?!")), + // }) + // .await?; } } diff --git a/src/command/wifi/mod.rs b/src/command/wifi/mod.rs index f0c9223..9dd319a 100644 --- a/src/command/wifi/mod.rs +++ b/src/command/wifi/mod.rs @@ -34,14 +34,29 @@ impl<'a> atat::AtatLen for SetWifiStationConfig<'a> { const ATAT_SETWIFISTATIONCONFIG_LEN: usize = as atat::AtatLen>::LEN + ::LEN + 1usize; #[automatically_derived] -impl<'a> atat::AtatCmd<{ ATAT_SETWIFISTATIONCONFIG_LEN + 12usize }> for SetWifiStationConfig<'a> { +impl<'a> atat::AtatCmd for SetWifiStationConfig<'a> { type Response = NoResponse; const MAX_TIMEOUT_MS: u32 = 1000u32; #[inline] - fn as_bytes(&self) -> atat::heapless::Vec { - match atat::serde_at::to_vec( + fn parse( + &self, + res: Result<&[u8], atat::InternalError>, + ) -> core::result::Result { + match res { + Ok(resp) => { + atat::serde_at::from_slice::(resp).map_err(|_e| atat::Error::Parse) + } + Err(e) => Err(e.into()), + } + } + + const MAX_LEN: usize = ATAT_SETWIFISTATIONCONFIG_LEN + 12usize; + + fn write(&self, buf: &mut [u8]) -> usize { + match atat::serde_at::to_slice( self, "+UWSC", + buf, atat::serde_at::SerializeOptions { value_sep: true, cmd_prefix: "AT", @@ -53,18 +68,6 @@ impl<'a> atat::AtatCmd<{ ATAT_SETWIFISTATIONCONFIG_LEN + 12usize }> for SetWifiS Err(_) => panic!("Failed to serialize command"), } } - #[inline] - fn parse( - &self, - res: Result<&[u8], atat::InternalError>, - ) -> core::result::Result { - match res { - Ok(resp) => { - atat::serde_at::from_slice::(resp).map_err(|e| atat::Error::Parse) - } - Err(e) => Err(e.into()), - } - } } #[automatically_derived] impl<'a> atat::serde_at::serde::Serialize for SetWifiStationConfig<'a> { diff --git a/src/command/wifi/types.rs b/src/command/wifi/types.rs index e2ee94e..25097a5 100644 --- a/src/command/wifi/types.rs +++ b/src/command/wifi/types.rs @@ -426,7 +426,8 @@ pub enum WifiStationAction { Deactivate = 4, } -#[derive(Debug, Clone, PartialEq, AtatEnum, defmt::Format)] +#[derive(Debug, Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum OperationMode { Infrastructure = 1, @@ -434,6 +435,7 @@ pub enum OperationMode { } #[derive(Clone, PartialEq, AtatEnum)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum StatusId { SSID = 0, diff --git a/src/network.rs b/src/network.rs index a319288..0d9204c 100644 --- a/src/network.rs +++ b/src/network.rs @@ -14,9 +14,10 @@ pub enum WifiMode { AccessPoint, } -#[derive(Debug, defmt::Format)] +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct WifiNetwork { - #[defmt(Debug2Format)] + #[cfg_attr(feature = "defmt", defmt(Debug2Format))] pub bssid: Bytes<20>, pub op_mode: OperationMode, pub ssid: String<64>,

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