diff --git a/Cargo.toml b/Cargo.toml index 175ddf6..2f0403a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,25 +5,28 @@ keywords = ["ESP8266", "network", "no_std", "at"] categories = ["embedded", "network-programming", "no-std"] authors = ["PEGASUS GmbH "] license = "MIT OR Apache-2.0" -version = "0.1.2" +version = "0.2.1" edition = "2021" repository = "https://github.com/pegasus-aero/rt-esp-at-nal" readme = "README.md" documentation = "https://docs.rs/esp-at-nal" [dependencies] -atat = "0.17.0" +atat = "0.18.0" embedded-nal = "0.8.0" nb = "1.0.0" fugit = "0.3.6" fugit-timer = "0.1.3" heapless = "0.7.16" -bbqueue = { version = "0.5.0", optional=true } +bbqueue = { version = "0.5.0", optional = true } numtoa = "0.2" base16 = { version = "0.2", default-features = false } [dev-dependencies] +env_logger = "0.6" +log = "0.4" mockall = "0.11.2" +serialport = { git = "https://github.com/dbrgn/serialport-rs", branch = "embedded-hal", features = ["embedded"], default_features = false } [features] default = ["examples"] diff --git a/README.md b/README.md index d053c9a..c9b683f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # no_std ESP-AT network layer + [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Crates.io](https://img.shields.io/crates/v/esp-at-nal.svg)](https://crates.io/crates/esp-at-nal) @@ -11,6 +12,9 @@ Currently, this crates offers the following features * TCP client stack (multi socket), s. [stack module](https://docs.rs/esp-at-nal/latest/esp_at_nal/stack/index.html) ## Example + +Here's a simple example using a mocked AtClient: + ````rust use std::str::FromStr; use embedded_nal::{SocketAddr, TcpClientStack}; @@ -34,6 +38,13 @@ adapter.connect(&mut socket, SocketAddr::from_str("10.0.0.1:21").unwrap()).unwra adapter.send(&mut socket, b"hallo!").unwrap(); ```` +To see a real-world example that runs on Linux, check out `examples/linux.rs`: + + # For logging + export RUST_LOG=trace + + cargo run --example linux --features "atat/log" -- \ + /dev/ttyUSB0 115200 mywifi hellopasswd123 ## Development @@ -41,10 +52,11 @@ Any form of support is greatly appreciated. Feel free to create issues and PRs. See [DEVELOPMENT](DEVELOPMENT.md) for more details. ## License + Licensed under either of * Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) * MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) at your option. -Each contributor agrees that his/her contribution covers both licenses. \ No newline at end of file +Each contributor agrees that his/her contribution covers both licenses. diff --git a/examples/linux.rs b/examples/linux.rs new file mode 100644 index 0000000..23fa4bc --- /dev/null +++ b/examples/linux.rs @@ -0,0 +1,274 @@ +//! Example that runs on Linux using a serial-USB-adapter. +use std::{ + env, io, + net::{SocketAddr as StdSocketAddr, ToSocketAddrs}, + thread, + time::Duration, +}; + +use atat::bbqueue::BBBuffer; +use embedded_nal::{SocketAddr as NalSocketAddr, TcpClientStack}; +use esp_at_nal::{ + urc::URCMessages, + wifi::{Adapter, WifiAdapter}, +}; +use serialport::{DataBits, FlowControl, Parity, SerialPort, StopBits}; + +// Chunk size in bytes when sending data. Higher value results in better +// performance, but introduces also higher stack memory footprint. Max value: 8192. +const TX_SIZE: usize = 1024; +// Chunk size in bytes when receiving data. Value should be matched to buffer +// size of receive() calls. +const RX_SIZE: usize = 2048; + +// Constants derived from TX_SIZE and RX_SIZE +const ESP_TX_SIZE: usize = TX_SIZE; +const ESP_RX_SIZE: usize = RX_SIZE; +const ATAT_RX_SIZE: usize = RX_SIZE; +const URC_RX_SIZE: usize = RX_SIZE; +const RES_CAPACITY: usize = RX_SIZE; +const URC_CAPACITY: usize = RX_SIZE * 3; + +// Timer frequency in Hz +const TIMER_HZ: u32 = 1000; + +fn main() { + env_logger::init(); + + // Parse args + let args: Vec = env::args().collect(); + if args.len() != 5 { + println!("Usage: {} ", args[0]); + println!("Example: {} /dev/ttyUSB0 115200 mywifi hellopasswd123", args[0]); + println!("\nNote: To run the example with debug logging, run it like this:"); + println!("\n RUST_LOG=trace cargo run --example linux --features \"atat/log\" -- /dev/ttyUSB0 115200 mywifi hellopasswd123"); + std::process::exit(1); + } + let dev = &args[1]; + let baud_rate: u32 = args[2].parse().unwrap(); + let ssid = &args[3]; + let psk = &args[4]; + + println!("Starting (dev={}, baud={:?})...", dev, baud_rate); + + // Open serial port + let serial_tx = serialport::new(dev, baud_rate) + .data_bits(DataBits::Eight) + .flow_control(FlowControl::None) + .parity(Parity::None) + .stop_bits(StopBits::One) + .timeout(Duration::from_millis(500)) + .open() + .expect("Could not open serial port"); + let mut serial_rx = serial_tx.try_clone().expect("Could not clone serial port"); + + // Atat queues + static mut RES_QUEUE: BBBuffer = BBBuffer::new(); + static mut URC_QUEUE: BBBuffer = BBBuffer::new(); + let queues = atat::Queues { + res_queue: unsafe { RES_QUEUE.try_split_framed().unwrap() }, + urc_queue: unsafe { URC_QUEUE.try_split_framed().unwrap() }, + }; + + // Two timer instances + let atat_timer = timer::SysTimer::new(); + let esp_timer = timer::SysTimer::new(); + + // Atat client + let config = atat::Config::new(atat::Mode::Timeout); + let digester = atat::AtDigester::>::new(); + let (client, mut ingress) = + atat::ClientBuilder::<_, _, _, TIMER_HZ, ATAT_RX_SIZE, RES_CAPACITY, URC_CAPACITY>::new( + serial_tx, atat_timer, digester, config, + ) + .build(queues); + + // Flush serial RX buffer, to ensure that there isn't any remaining left + // form previous sessions. + flush_serial(&mut serial_rx); + + // Launch reading thread, to pass incoming data from serial to the atat ingress + thread::Builder::new() + .name("serial_read".to_string()) + .spawn(move || loop { + let mut buffer = [0; 32]; + match serial_rx.read(&mut buffer[..]) { + Ok(0) => {} + Ok(bytes_read) => { + ingress.write(&buffer[0..bytes_read]); + ingress.digest(); + ingress.digest(); + } + Err(e) => match e.kind() { + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut | io::ErrorKind::Interrupted => { + // Ignore + } + _ => { + log::error!("Serial reading thread error while reading: {}", e); + } + }, + } + }) + .unwrap(); + + // ESP AT adapter + let mut adapter: Adapter<_, _, TIMER_HZ, ESP_TX_SIZE, ESP_RX_SIZE> = Adapter::new(client, esp_timer); + + // Join WIFI access point + println!("Join WiFi \"{}\"...", ssid); + let state = adapter.join(ssid, psk).unwrap(); + assert!(state.connected); + + // Resolve IPv4 for ifconfig.net + let remote_host = "ifconfig.net"; + let ipv4_and_port = (remote_host, 80) + .to_socket_addrs() + .unwrap() + .find_map(|addr| match addr { + StdSocketAddr::V4(v4) => Some((v4.ip().octets(), v4.port())), + _ => None, + }) + .unwrap(); + let socket_addr = NalSocketAddr::from(ipv4_and_port); + + // Create TCP connection + let mut socket = adapter.socket().expect("Failed to create socket"); + println!("Connecting to {}...", remote_host); + adapter + .connect(&mut socket, socket_addr) + .unwrap_or_else(|_| panic!("Failed to connect to {}", remote_host)); + println!("Connected!"); + + // Send HTTP request + println!("Sending HTTP request..."); + let request = b"GET / HTTP/1.1\r\nAccept: text/plain\r\nHost: ifconfig.net\r\n\r\n"; + adapter.send(&mut socket, request).expect("Could not send HTTP request"); + + // Read response + let mut rx_buf = [0; RX_SIZE]; + let bytes_read = nb::block!(adapter.receive(&mut socket, &mut rx_buf)).expect("Error while receiving data"); + println!("Read {} bytes", bytes_read); + assert!(bytes_read < rx_buf.len(), "HTTP response did not fit in rx_buffer"); + let response = std::str::from_utf8(&rx_buf[..bytes_read]).expect("HTTP response is not valid UTF8"); + + // Very primitive HTTP response parsing + let (headers, body) = response.split_once("\r\n\r\n").unwrap_or_else(|| { + println!("Response:\n---\n{}\n---", response); + panic!("Could not parse HTTP response"); + }); + if !headers.starts_with("HTTP/1.1 ") { + panic!( + "Unsupported HTTP response, expected HTTP/1.1 but found {}", + &headers[..8] + ); + } + if !headers.starts_with("HTTP/1.1 200 ") { + panic!("Bad HTTP response code, expected 200 but found {}", &headers[9..12]); + } + println!("Your public IP, as returned by {}: {}", remote_host, body.trim()); +} + +/// Flush the serial port receive buffer. +fn flush_serial(serial_rx: &mut Box) { + let mut buf = [0; 32]; + loop { + match serial_rx.read(&mut buf[..]) { + Ok(0) => break, + Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => break, + Ok(_) => continue, + Err(e) => panic!("Error while flushing serial: {}", e), + } + } +} + +mod timer { + use std::{convert::TryInto, time::Instant as StdInstant}; + + use atat::clock::Clock; + use fugit::Instant; + + /// A timer with millisecond precision. + pub struct SysTimer { + start: StdInstant, + duration_ms: u32, + started: bool, + } + + impl SysTimer { + pub fn new() -> SysTimer { + SysTimer { + start: StdInstant::now(), + duration_ms: 0, + started: false, + } + } + } + + impl Clock<1000> for SysTimer { + type Error = &'static str; + + /// Return current time `Instant` + fn now(&mut self) -> fugit::TimerInstantU32<1000> { + let milliseconds = (StdInstant::now() - self.start).as_millis(); + let ticks: u32 = milliseconds.try_into().expect("u32 timer overflow"); + Instant::::from_ticks(ticks) + } + + /// Start timer with a `duration` + fn start(&mut self, duration: fugit::TimerDurationU32<1000>) -> Result<(), Self::Error> { + // (Re)set start and duration + self.start = StdInstant::now(); + self.duration_ms = duration.ticks(); + + // Set started flag + self.started = true; + + Ok(()) + } + + /// Tries to stop this timer. + /// + /// An error will be returned if the timer has already been canceled or was never started. + /// An error is also returned if the timer is not `Periodic` and has already expired. + fn cancel(&mut self) -> Result<(), Self::Error> { + if !self.started { + Err("cannot cancel stopped timer") + } else { + self.started = false; + Ok(()) + } + } + + /// Wait until timer `duration` has expired. + /// Must return `nb::Error::WouldBlock` if timer `duration` is not yet over. + /// Must return `OK(())` as soon as timer `duration` has expired. + fn wait(&mut self) -> nb::Result<(), Self::Error> { + let now = StdInstant::now(); + if (now - self.start).as_millis() > self.duration_ms.into() { + Ok(()) + } else { + Err(nb::Error::WouldBlock) + } + } + } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn test_delay() { + let mut timer = SysTimer::new(); + + // Wait 500 ms + let before = StdInstant::now(); + timer.start(fugit::Duration::::from_ticks(500)).unwrap(); + nb::block!(timer.wait()).unwrap(); + let after = StdInstant::now(); + + let duration_ms = (after - before).as_millis(); + assert!(duration_ms >= 500); + assert!(duration_ms < 1000); + } + } +} diff --git a/src/commands.rs b/src/commands.rs index 4d2f7bd..c8adf3b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -3,7 +3,7 @@ use core::fmt::Write; use crate::responses::LocalAddressResponse; use crate::responses::NoResponse; use crate::stack::Error as StackError; -use crate::wifi::{AddressErrors, JoinError}; +use crate::wifi::{AddressErrors, CommandError, JoinError}; use atat::atat_derive::AtatCmd; use atat::heapless::{String, Vec}; use atat::{AtatCmd, Error as AtError, InternalError}; @@ -51,6 +51,32 @@ impl CommandErrorHandler for WifiModeCommand { } } +/// Enables/Disables auto connect, so that ESP-AT automatically connects to the stored AP when powered on. +#[derive(Clone, AtatCmd)] +#[at_cmd("+CWAUTOCONN", NoResponse, timeout_ms = 1_000)] +pub struct AutoConnectCommand { + /// 1: Enables auto connect, 0: Disables auto connect + mode: usize, +} + +impl AutoConnectCommand { + pub fn new(enabled: bool) -> Self { + Self { + mode: usize::from(enabled), + } + } +} + +impl CommandErrorHandler for AutoConnectCommand { + type Error = CommandError; + + const WOULD_BLOCK_ERROR: Self::Error = CommandError::UnexpectedWouldBlock; + + fn command_error(&self, error: AtError) -> Self::Error { + CommandError::CommandFailed(error) + } +} + /// Command for setting the target WIFI access point parameters #[derive(Clone, Default, AtatCmd)] #[at_cmd("+CWJAP", NoResponse, timeout_ms = 20_000)] @@ -81,8 +107,7 @@ impl CommandErrorHandler for AccessPointConnectCommand { } /// Command for receiving local address information including IP and MAC -#[derive(Clone, AtatCmd)] -#[at_cmd("+CIFSR", Vec, timeout_ms = 5_000)] +#[derive(Clone)] pub struct ObtainLocalAddressCommand {} impl ObtainLocalAddressCommand { @@ -91,6 +116,23 @@ impl ObtainLocalAddressCommand { } } +impl AtatCmd<10> for ObtainLocalAddressCommand { + type Response = Vec; + const MAX_TIMEOUT_MS: u32 = 5_000; + + fn as_bytes(&self) -> Vec { + Vec::from_slice("AT+CIFSR\r\n".as_bytes()).unwrap() + } + + fn parse(&self, resp: Result<&[u8], InternalError>) -> Result { + if resp.is_err() { + return Err(AtError::InvalidResponse); + } + + atat::serde_at::from_slice::>(resp.unwrap()).map_err(|_| AtError::Parse) + } +} + impl CommandErrorHandler for ObtainLocalAddressCommand { type Error = AddressErrors; const WOULD_BLOCK_ERROR: Self::Error = AddressErrors::UnexpectedWouldBlock; @@ -346,6 +388,20 @@ impl CommandErrorHandler for CloseSocketCommand { } } +/// Restarts the module +#[derive(Clone, Default, AtatCmd)] +#[at_cmd("+RST", NoResponse, timeout_ms = 1_000)] +pub struct RestartCommand {} + +impl CommandErrorHandler for RestartCommand { + type Error = CommandError; + const WOULD_BLOCK_ERROR: Self::Error = CommandError::UnexpectedWouldBlock; + + fn command_error(&self, error: AtError) -> Self::Error { + CommandError::CommandFailed(error) + } +} + #[cfg(test)] mod tests { use embedded_nal::{Ipv4Addr, Ipv6Addr}; diff --git a/src/stack.rs b/src/stack.rs index 3305f6e..52290c0 100644 --- a/src/stack.rs +++ b/src/stack.rs @@ -36,7 +36,7 @@ use crate::commands::{ CloseSocketCommand, ConnectCommand, ReceiveDataCommand, SetMultipleConnectionsCommand, SetSocketReceivingModeCommand, TransmissionCommand, TransmissionPrepareCommand, }; -use crate::wifi::Adapter; +use crate::wifi::{Adapter, Session}; use atat::AtatClient; use atat::Error as AtError; use embedded_nal::{SocketAddr, TcpClientStack, TcpError, TcpErrorKind}; @@ -57,9 +57,19 @@ impl Socket { } } +/// Internal state of a single socket +#[derive(Copy, Clone, Default)] +pub(crate) struct SocketState { + /// Connection state + pub(crate) state: ConnectionState, + + /// Data length in bytes available to receive which is buffered by ESP-AT + pub(crate) data_available: usize, +} + /// Internal connection state #[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub(crate) enum SocketState { +pub(crate) enum ConnectionState { /// Socket is closed an may be (re)used Closed, /// Socket was returned by socket() but is not connected yet @@ -70,6 +80,12 @@ pub(crate) enum SocketState { Closing, } +impl Default for ConnectionState { + fn default() -> Self { + Self::Closed + } +} + /// Network related errors #[derive(Clone, Debug, PartialEq)] pub enum Error { @@ -156,12 +172,12 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz fn connect(&mut self, socket: &mut Socket, remote: SocketAddr) -> nb::Result<(), Self::Error> { self.process_urc_messages(); - if self.sockets[socket.link_id] == SocketState::Connected { + if self.session.is_socket_connected(socket) { return nb::Result::Err(nb::Error::Other(Error::AlreadyConnected)); } self.enable_passive_receiving_mode()?; - self.already_connected = false; + self.session.already_connected = false; let command = match remote { SocketAddr::V4(address) => ConnectCommand::tcp_v4(socket.link_id, address), @@ -171,17 +187,17 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz self.process_urc_messages(); // ESP-AT returned that given socket is already connected. This indicates that a URC Connect message was missed. - if self.already_connected { - self.sockets[socket.link_id] = SocketState::Connected; + if self.session.already_connected { + self.session.sockets[socket.link_id].state = ConnectionState::Connected; return nb::Result::Ok(()); } result?; - if self.sockets[socket.link_id] != SocketState::Connected { + if !self.session.is_socket_connected(socket) { return nb::Result::Err(nb::Error::Other(Error::UnconfirmedSocketState)); } - self.data_available[socket.link_id] = 0; + self.session.reset_available_data(socket); nb::Result::Ok(()) } @@ -206,23 +222,23 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz fn receive(&mut self, socket: &mut Self::TcpSocket, buffer: &mut [u8]) -> nb::Result { self.process_urc_messages(); - if self.data_available[socket.link_id] == 0 { + if !self.session.is_data_available(socket) { return nb::Result::Err(nb::Error::WouldBlock); } let mut buffer: Buffer = Buffer::new(buffer); - while self.data_available[socket.link_id] > 0 && !buffer.is_full() { + while self.session.is_data_available(socket) && !buffer.is_full() { let command = ReceiveDataCommand::::new(socket.link_id, buffer.get_next_length()); self.send_command(command)?; self.process_urc_messages(); - if self.data.is_none() { + if self.session.data.is_none() { return nb::Result::Err(nb::Error::Other(Error::ReceiveFailed(AtError::InvalidResponse))); } - let data = self.data.take().unwrap(); - self.reduce_data_available(socket.link_id, data.len()); + let data = self.session.data.take().unwrap(); + self.session.reduce_available_data(socket, data.len()); buffer.append(data)?; } @@ -237,21 +253,26 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz fn close(&mut self, socket: Self::TcpSocket) -> Result<(), Self::Error> { self.process_urc_messages(); + // Socket already closed during restart + if self.session.is_socket_closed(&socket) { + return Ok(()); + } + // Socket is not connected yet or was already closed remotely - if self.sockets[socket.link_id] == SocketState::Closing || self.sockets[socket.link_id] == SocketState::Open { - self.sockets[socket.link_id] = SocketState::Closed; + if self.session.is_socket_closing(&socket) || self.session.is_socket_open(&socket) { + self.session.sockets[socket.link_id].state = ConnectionState::Closed; return Ok(()); } let mut result = self.send_command(CloseSocketCommand::new(socket.link_id)); self.process_urc_messages(); - if self.sockets[socket.link_id] != SocketState::Closing && result.is_ok() { + if !self.session.is_socket_closing(&socket) && result.is_ok() { result = Err(Error::UnconfirmedSocketState); } // Setting to Closed even on error. Otherwise socket can not be reused in future, as its consumed. - self.sockets[socket.link_id] = SocketState::Closed; + self.session.sockets[socket.link_id].state = ConnectionState::Closed; result?; Ok(()) @@ -265,21 +286,21 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz /// The current implementation never returns a Error. pub fn is_connected(&mut self, socket: &Socket) -> Result { self.process_urc_messages(); - Ok(self.sockets[socket.link_id] == SocketState::Connected) + Ok(self.session.is_socket_connected(socket)) } /// Sends a chunk of max. 256 bytes fn send_chunk(&mut self, data: &[u8]) -> Result<(), Error> { - self.send_confirmed = None; - self.recv_byte_count = None; + self.session.send_confirmed = None; + self.session.recv_byte_count = None; self.send_command::, TX_SIZE>(TransmissionCommand::new(data))?; self.timer.start(self.send_timeout).map_err(|_| Error::TimerError)?; - while self.send_confirmed.is_none() { + while self.session.send_confirmed.is_none() { self.process_urc_messages(); - if let Some(send_success) = self.send_confirmed { + if let Some(send_success) = self.session.send_confirmed { // Transmission failed if !send_success { // Reset prompt status. Otherwise client does not match any command responses. @@ -288,7 +309,7 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz } // Byte count does not match - if self.recv_byte_count.is_some() && *self.recv_byte_count.as_ref().unwrap() != data.len() { + if self.session.is_received_byte_count_incorrect(data.len()) { return Err(Error::PartialSend); } @@ -314,31 +335,31 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz /// Enables multiple connections. /// Stores internal state, so command is just sent once for saving bandwidth fn enable_multiple_connections(&mut self) -> Result<(), Error> { - if self.multi_connections_enabled { + if self.session.multi_connections_enabled { return Ok(()); } self.send_command(SetMultipleConnectionsCommand::multiple())?; - self.multi_connections_enabled = true; + self.session.multi_connections_enabled = true; Ok(()) } /// Enables the passive socket receiving mode /// Stores internal state, so command is just sent once for saving bandwidth fn enable_passive_receiving_mode(&mut self) -> Result<(), Error> { - if self.passive_mode_enabled { + if self.session.passive_mode_enabled { return Ok(()); } self.send_command(SetSocketReceivingModeCommand::passive_mode())?; - self.passive_mode_enabled = true; + self.session.passive_mode_enabled = true; Ok(()) } /// Assigns a free link_id. Returns an error in case no more free sockets are available fn open_socket(&mut self) -> Result { - if let Some(link_id) = self.sockets.iter().position(|state| state == &SocketState::Closed) { - self.sockets[link_id] = SocketState::Open; + if let Some(link_id) = self.session.get_next_open() { + self.session.sockets[link_id].state = ConnectionState::Open; return Ok(Socket::new(link_id)); } @@ -347,25 +368,68 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz /// Asserts that the given socket is connected and returns otherwise the appropriate error fn assert_socket_connected(&self, socket: &Socket) -> nb::Result<(), Error> { - if self.sockets[socket.link_id] == SocketState::Closing { + if self.session.is_socket_closing(socket) { return nb::Result::Err(nb::Error::Other(Error::ClosingSocket)); } - if self.sockets[socket.link_id] != SocketState::Connected { + if !self.session.is_socket_connected(socket) { return nb::Result::Err(nb::Error::Other(Error::SocketUnconnected)); } nb::Result::Ok(()) } +} + +impl Session { + /// Fetches the next open socket ID and returns None in case no socket is available + fn get_next_open(&self) -> Option { + self.sockets.iter().position(|state| state.state == ConnectionState::Closed) + } + + /// Returns true if data is available for the given socket + fn is_data_available(&self, socket: &Socket) -> bool { + self.sockets[socket.link_id].data_available > 0 + } - /// Reduces the available data length mark by the given length - fn reduce_data_available(&mut self, link_id: usize, length: usize) { - if self.data_available[link_id] < length { - self.data_available[link_id] = 0; + /// Reduces the available data length mark by the given length of the given socket ID + fn reduce_available_data(&mut self, socket: &Socket, length: usize) { + if self.sockets[socket.link_id].data_available < length { + self.sockets[socket.link_id].data_available = 0; return; } - self.data_available[link_id] -= length; + self.sockets[socket.link_id].data_available -= length; + } + + /// Returns true if the reported received byte length does NOT match the actual data length + /// Returns false if received byte count was not reported by ESP-AT (older firmware version) + fn is_received_byte_count_incorrect(&self, actual_data_length: usize) -> bool { + self.recv_byte_count.is_some() && *self.recv_byte_count.as_ref().unwrap() != actual_data_length + } + + /// Sets the available data of the given socket to zero + fn reset_available_data(&mut self, socket: &Socket) { + self.sockets[socket.link_id].data_available = 0; + } + + /// Returns true if the given socket is in OPEN state + fn is_socket_open(&self, socket: &Socket) -> bool { + self.sockets[socket.link_id].state == ConnectionState::Open + } + + /// Returns true if the given socket is in CLOSED state + fn is_socket_closed(&self, socket: &Socket) -> bool { + self.sockets[socket.link_id].state == ConnectionState::Closed + } + + /// Returns true if the given socket is in CLOSING state + fn is_socket_closing(&self, socket: &Socket) -> bool { + self.sockets[socket.link_id].state == ConnectionState::Closing + } + + /// Returns true if the given socket is in CONNECTED state + fn is_socket_connected(&self, socket: &Socket) -> bool { + self.sockets[socket.link_id].state == ConnectionState::Connected } } diff --git a/src/tests/stack.rs b/src/tests/stack.rs index b6ddab5..8e2a053 100644 --- a/src/tests/stack.rs +++ b/src/tests/stack.rs @@ -1,6 +1,6 @@ use crate::stack::{Error, Socket}; use crate::tests::mock::{MockAtatClient, MockTimer}; -use crate::wifi::Adapter; +use crate::wifi::{Adapter, WifiAdapter}; use alloc::string::{String, ToString}; use alloc::vec; use atat::Error as AtError; @@ -152,6 +152,63 @@ fn test_connect_correct_commands_ipv4() { assert_eq!("AT+CIPSTART=0,\"TCP\",\"127.0.0.1\",5000\r\n".to_string(), commands[2]); } +#[test] +fn test_connect_after_restart() { + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(|_| Ok(())); + timer + .expect_wait() + .times(1) + .returning(|| nb::Result::Err(nb::Error::WouldBlock)); + + let mut client = MockAtatClient::new(); + + // Responses to CIPMUX, CIPRECVMODE and CIPSTART + client.add_ok_response(); + client.add_ok_response(); + client.add_ok_response(); + + client.skip_urc(1); + client.add_urc_first_socket_connected(); + + let mut adapter: AdapterType = Adapter::new(client, timer); + + let mut socket = adapter.socket().unwrap(); + adapter + .connect(&mut socket, SocketAddr::from_str("127.0.0.1:5000").unwrap()) + .unwrap(); + + // Response to RST command + adapter.client.add_ok_response(); + adapter.client.add_urc_ready(); + adapter.restart().unwrap(); + + // Responses to CIPMUX, CIPRECVMODE and CIPSTART + adapter.client.add_ok_response(); + adapter.client.add_ok_response(); + adapter.client.add_ok_response(); + + adapter.client.skip_urc(1); + adapter.client.add_urc_first_socket_connected(); + + adapter.client.reset_captured_commands(); + + socket = adapter.socket().unwrap(); + adapter + .connect(&mut socket, SocketAddr::from_str("127.0.0.1:5000").unwrap()) + .unwrap(); + + // Assert that socket state gets reset on restart + assert_eq!(0, socket.link_id); + + // Assert that internal state issued commands gets reset + let commands = adapter.client.get_commands_as_strings(); + assert_eq!(3, commands.len()); + assert_eq!("AT+CIPMUX=1\r\n".to_string(), commands[0]); + assert_eq!("AT+CIPRECVMODE=1\r\n".to_string(), commands[1]); + assert_eq!("AT+CIPSTART=0,\"TCP\",\"127.0.0.1\",5000\r\n".to_string(), commands[2]); +} + #[test] fn test_connect_correct_commands_ipv6() { let timer = MockTimer::new(); @@ -773,6 +830,35 @@ fn test_receive_no_data_available() { assert_eq!(nb::Error::WouldBlock, error); } +#[test] +fn test_receive_after_restart() { + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(|_| Ok(())); + timer + .expect_wait() + .times(1) + .returning(|| nb::Result::Err(nb::Error::WouldBlock)); + + let client = MockAtatClient::new(); + let mut adapter: AdapterType = Adapter::new(client, timer); + let mut socket = connect_socket(&mut adapter); + + // Fake available data + adapter.client.add_urc_message(b"+IPD,0,256\r\n"); + adapter.process_urc_messages(); + + adapter.client.add_ok_response(); + adapter.client.add_urc_ready(); + adapter.restart().unwrap(); + + let mut buffer = [0x0; 32]; + let error = adapter.receive(&mut socket, &mut buffer).unwrap_err(); + + // Assert that available data is reset on restart + assert_eq!([0x0; 32], buffer); + assert_eq!(nb::Error::WouldBlock, error); +} + #[test] fn test_receive_receive_command_failed() { let timer = MockTimer::new(); @@ -965,7 +1051,7 @@ fn test_receive_data_received_buffer_overflow() { } #[test] -fn test_closed_socket_not_connected_yet() { +fn test_close_socket_not_connected_yet() { let timer = MockTimer::new(); let client = MockAtatClient::new(); let mut adapter: AdapterType = Adapter::new(client, timer); @@ -988,7 +1074,7 @@ fn test_closed_socket_not_connected_yet() { } #[test] -fn test_closed_socket_already_closed_by_remote() { +fn test_close_socket_already_closed_by_remote() { let timer = MockTimer::new(); let client = MockAtatClient::new(); let mut adapter: AdapterType = Adapter::new(client, timer); @@ -1009,7 +1095,59 @@ fn test_closed_socket_already_closed_by_remote() { } #[test] -fn test_closed_socket_command_error() { +fn test_close_open_socket() { + let timer = MockTimer::new(); + let client = MockAtatClient::new(); + let mut adapter: AdapterType = Adapter::new(client, timer); + + // Receiving socket + adapter.client.add_ok_response(); + let socket = adapter.socket().unwrap(); + + adapter.client.reset_captured_commands(); + adapter.close(socket).unwrap(); + + // Socket is available for reuse + let socket = adapter.socket().unwrap(); + assert_eq!(0, socket.link_id); + + // Asserts that no close command is sent + let commands = adapter.client.get_commands_as_strings(); + assert!(commands.is_empty()); +} + +#[test] +fn test_close_after_restart() { + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(|_| Ok(())); + timer + .expect_wait() + .times(1) + .returning(|| nb::Result::Err(nb::Error::WouldBlock)); + + let client = MockAtatClient::new(); + let mut adapter: AdapterType = Adapter::new(client, timer); + let socket = connect_socket(&mut adapter); + + // Response to RST command + adapter.client.add_ok_response(); + adapter.client.add_urc_ready(); + adapter.restart().unwrap(); + + adapter.client.reset_captured_commands(); + adapter.close(socket).unwrap(); + + // Asserts that no close command is sent + assert!(adapter.client.get_commands_as_strings().is_empty()); + + // Socket is available for reuse + adapter.client.add_ok_response(); + let socket = adapter.socket().unwrap(); + assert_eq!(0, socket.link_id); +} + +#[test] +fn test_close_socket_command_error() { let timer = MockTimer::new(); let client = MockAtatClient::new(); let mut adapter: AdapterType = Adapter::new(client, timer); @@ -1025,7 +1163,7 @@ fn test_closed_socket_command_error() { } #[test] -fn test_closed_socket_command_would_block() { +fn test_close_socket_command_would_block() { let timer = MockTimer::new(); let client = MockAtatClient::new(); let mut adapter: AdapterType = Adapter::new(client, timer); @@ -1041,7 +1179,7 @@ fn test_closed_socket_command_would_block() { } #[test] -fn test_closed_socket_unconfirmed() { +fn test_close_socket_unconfirmed() { let timer = MockTimer::new(); let client = MockAtatClient::new(); let mut adapter: AdapterType = Adapter::new(client, timer); @@ -1057,7 +1195,7 @@ fn test_closed_socket_unconfirmed() { } #[test] -fn test_closed_socket_closed_successfully() { +fn test_close_socket_closed_successfully() { let timer = MockTimer::new(); let client = MockAtatClient::new(); let mut adapter: AdapterType = Adapter::new(client, timer); diff --git a/src/tests/urc.rs b/src/tests/urc.rs index 5313c3d..cc035f0 100644 --- a/src/tests/urc.rs +++ b/src/tests/urc.rs @@ -1,12 +1,5 @@ -use crate::commands::{ - AccessPointConnectCommand, ConnectCommand, ObtainLocalAddressCommand, SetMultipleConnectionsCommand, - SetSocketReceivingModeCommand, TransmissionPrepareCommand, WifiModeCommand, -}; use crate::urc::URCMessages; -use atat::heapless::String; -use atat::{AtatCmd, AtatUrc, Parser}; -use core::str::FromStr; -use embedded_nal::SocketAddrV4; +use atat::{AtatUrc, Parser}; use heapless::Vec; #[test] @@ -139,6 +132,18 @@ fn test_first_parse_data_serial_data_incomplete() { assert!( as Parser>::parse(b"\r\n+CIPRECVDATA,5:abcd").is_err()); } +#[test] +fn test_first_parse_boot_incomplete() { + assert!( as Parser>::parse(b"ets Jan 8 2013,rst cause:1, boot mode:(3,7)").is_err()); + assert!( as Parser>::parse(b"ets Jan 8 2013,rst cause:1, boot mode:(3,7)\r\n\n\r\n").is_err()); + assert!( as Parser>::parse(b"tail 0\r\nready\r\n").is_err()); +} + +#[test] +fn test_first_parse_boot_matches() { + assert_result(b"ready\r\n", 95, b"\r\nets Jan 8 2013,rst cause:1, boot mode:(3,7)\r\n\r\nload 0x40100000, len 2592, room 16\r\n\r\nready\r\nWIFI GOT IP\r\n"); +} + #[test] fn test_first_parse_data_fully_received() { assert_result(b"+CIPRECVDATA,5:abcde", 20, b"+CIPRECVDATA,5:abcde\r\n\r\nOK\r\n"); @@ -295,23 +300,6 @@ fn test_second_parse_longer_then_block_size() { assert!( as AtatUrc>::parse(b"+CIPRECVDATA,5:abcde").is_none()) } -#[test] -fn test_matching_cmd_echo() { - assert_cmd_echo_matching(WifiModeCommand::station_mode()); - assert_cmd_echo_matching(SetSocketReceivingModeCommand::passive_mode()); - assert_cmd_echo_matching(SetMultipleConnectionsCommand::multiple()); - assert_cmd_echo_matching(AccessPointConnectCommand::new( - String::from_str("test_network").unwrap(), - String::from_str("secret").unwrap(), - )); - assert_cmd_echo_matching(TransmissionPrepareCommand::new(0, 8)); - assert_cmd_echo_matching(ConnectCommand::tcp_v4( - 0, - SocketAddrV4::from_str("10.0.0.1:5000").unwrap(), - )); - assert_cmd_echo_matching(ObtainLocalAddressCommand::new()); -} - fn assert_result(string: &[u8], size: usize, data: &[u8]) { match as Parser>::parse(data) { Ok(result) => { @@ -323,19 +311,3 @@ fn assert_result(string: &[u8], size: usize, data: &[u8]) { } } } - -/// Asserts that command echo is matched -fn assert_cmd_echo_matching, const LEN: usize>(command: Cmd) { - let encoded = command.as_bytes(); - - // Assert that first parser ist matching - assert_result(encoded.as_slice(), encoded.len(), encoded.as_slice()); - - // Assert that echo gets converted to Unknown URC - assert_eq!( - URCMessages::Echo, - as AtatUrc>::parse(encoded.as_slice()).unwrap(), - "Echo of command {} did not return URCMessages::Echo on second parser.", - core::str::from_utf8(encoded.as_slice()).unwrap() - ); -} diff --git a/src/tests/wifi.rs b/src/tests/wifi.rs index ef94f5e..2a8a6a2 100644 --- a/src/tests/wifi.rs +++ b/src/tests/wifi.rs @@ -1,6 +1,6 @@ use crate::tests::mock::{MockAtatClient, MockTimer}; -use crate::wifi::WifiAdapter; use crate::wifi::{Adapter, JoinError}; +use crate::wifi::{CommandError, WifiAdapter}; use alloc::string::ToString; use atat::Error; @@ -207,3 +207,203 @@ fn test_get_join_state_connected_without_ip() { assert!(result.connected); assert!(!result.ip_assigned); } + +#[test] +fn test_restart_command_failed() { + let mut client = MockAtatClient::new(); + let timer = MockTimer::new(); + client.add_error_response(); + + let mut adapter: AdapterType = Adapter::new(client, timer); + let error = adapter.restart().unwrap_err(); + + assert_eq!(CommandError::CommandFailed(Error::Parse), error); +} + +#[test] +fn test_restart_command_would_block() { + let mut client = MockAtatClient::new(); + let timer = MockTimer::new(); + client.send_would_block(0); + + let mut adapter: AdapterType = Adapter::new(client, timer); + let error = adapter.restart().unwrap_err(); + + assert_eq!(CommandError::UnexpectedWouldBlock, error); +} + +#[test] +fn test_restart_upstream_timer_start_error() { + let mut client = MockAtatClient::new(); + client.add_ok_response(); + + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(move |_| Err(31)); + + let mut adapter: AdapterType = Adapter::new(client, timer); + let error = adapter.restart().unwrap_err(); + + assert_eq!(CommandError::TimerError, error); +} + +#[test] +fn test_restart_upstream_timer_wait_error() { + let mut client = MockAtatClient::new(); + client.add_ok_response(); + + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(move |_| Ok(())); + timer + .expect_wait() + .times(1) + .returning(move || nb::Result::Err(nb::Error::Other(1))); + + let mut adapter: AdapterType = Adapter::new(client, timer); + let error = adapter.restart().unwrap_err(); + + assert_eq!(CommandError::TimerError, error); +} + +#[test] +fn test_restart_ready_received() { + let mut client = MockAtatClient::new(); + client.add_ok_response(); + client.add_urc_ready(); + + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(|duration| { + assert_eq!(duration, MockTimer::duration_ms(5_000)); + Ok(()) + }); + timer + .expect_wait() + .times(1) + .returning(|| nb::Result::Err(nb::Error::WouldBlock)); + + let mut adapter: AdapterType = Adapter::new(client, timer); + adapter.restart().unwrap(); + + let commands = adapter.client.get_commands_as_strings(); + assert_eq!(1, commands.len()); + assert_eq!("AT+RST\r\n".to_string(), commands[0]); +} + +#[test] +fn test_restart_double() { + let mut client = MockAtatClient::new(); + client.add_ok_response(); + client.add_urc_ready(); + + let mut timer = MockTimer::new(); + timer.expect_start().times(2).returning(|duration| { + assert_eq!(duration, MockTimer::duration_ms(5_000)); + Ok(()) + }); + timer + .expect_wait() + .times(2) + .returning(|| nb::Result::Err(nb::Error::WouldBlock)); + + let mut adapter: AdapterType = Adapter::new(client, timer); + adapter.restart().unwrap(); + + adapter.client.add_ok_response(); + adapter.client.add_urc_ready(); + + // Assert that ready state is reset and a second restart is possible + adapter.restart().unwrap(); + + assert_eq!(2, adapter.client.get_commands_as_strings().len()); +} + +#[test] +fn test_restart_ready_timeout() { + let mut client = MockAtatClient::new(); + client.add_ok_response(); + + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(|duration| { + assert_eq!(duration, MockTimer::duration_ms(5_000)); + Ok(()) + }); + timer + .expect_wait() + .times(1) + .returning(|| nb::Result::Err(nb::Error::WouldBlock)); + timer.expect_wait().times(1).returning(|| nb::Result::Ok(())); + + let mut adapter: AdapterType = Adapter::new(client, timer); + let error = adapter.restart().unwrap_err(); + + assert_eq!(CommandError::ReadyTimeout, error); +} + +#[test] +fn test_restart_wifi_state_reset() { + let mut client = MockAtatClient::new(); + client.add_urc_wifi_connected(); + client.add_urc_wifi_got_ip(); + + let mut timer = MockTimer::new(); + timer.expect_start().times(1).returning(|duration| { + assert_eq!(duration, MockTimer::duration_ms(5_000)); + Ok(()) + }); + timer + .expect_wait() + .times(1) + .returning(|| nb::Result::Err(nb::Error::WouldBlock)); + + let mut adapter: AdapterType = Adapter::new(client, timer); + // Faking WIFI connection state + adapter.process_urc_messages(); + + adapter.client.add_ok_response(); + adapter.client.add_urc_ready(); + adapter.restart().unwrap(); + + assert!(!adapter.get_join_status().connected); + assert!(!adapter.get_join_status().ip_assigned); +} + +#[test] +fn test_set_auto_connect_error() { + let mut client = MockAtatClient::new(); + let timer = MockTimer::new(); + client.add_error_response(); + + let mut adapter: AdapterType = Adapter::new(client, timer); + let result = adapter.set_auto_connect(true).unwrap_err(); + + assert_eq!(CommandError::CommandFailed(Error::Parse), result); +} + +#[test] +fn test_enable_auto_connect_would_block() { + let mut client = MockAtatClient::new(); + let timer = MockTimer::new(); + client.send_would_block(0); + + let mut adapter: AdapterType = Adapter::new(client, timer); + let error = adapter.set_auto_connect(true).unwrap_err(); + + assert_eq!(CommandError::UnexpectedWouldBlock, error); +} + +#[test] +fn test_set_auto_connect_correct_command() { + let mut client = MockAtatClient::new(); + let timer = MockTimer::new(); + + client.add_ok_response(); + client.add_ok_response(); + + let mut adapter: AdapterType = Adapter::new(client, timer); + adapter.set_auto_connect(true).unwrap(); + adapter.set_auto_connect(false).unwrap(); + + let commands = adapter.client.get_commands_as_strings(); + assert_eq!(2, commands.len()); + assert_eq!("AT+CWAUTOCONN=1\r\n".to_string(), commands[0]); + assert_eq!("AT+CWAUTOCONN=0\r\n".to_string(), commands[1]); +} diff --git a/src/urc.rs b/src/urc.rs index e6293b8..49574be 100644 --- a/src/urc.rs +++ b/src/urc.rs @@ -34,8 +34,6 @@ pub enum URCMessages { DataAvailable(usize, usize), /// Received the following data requested by CIPRECVDATA command. Data(Vec), - /// Echo of a command - Echo, /// Unknown URC message Unknown, } @@ -44,11 +42,6 @@ impl AtatUrc for URCMessages { type Response = Self; fn parse(resp: &[u8]) -> Option { - // Command echo - if &resp[..3] == b"AT+" { - return Some(Self::Echo); - } - if &resp[..4] == b"+IPD" { return URCMessages::parse_data_available(resp); } @@ -130,7 +123,11 @@ impl Parser for URCMessages { return matcher.handle(); } - LineBasedMatcher::new(buf).handle() + if let Ok(result) = LineBasedMatcher::new(buf).handle() { + return Ok(result); + } + + BootMessageParser::new(buf).handle() } } @@ -216,7 +213,6 @@ impl<'a> LineBasedMatcher<'a> { /// True if a regular CRLF terminated URC message was matched fn matches_lines_based_urc(&self, line: &str) -> bool { line == "ready" - || &line[..3] == "AT+" || &line[..4] == "+IPD" || line == "SEND OK" || line == "SEND FAIL" @@ -298,3 +294,44 @@ impl<'a> DataMessage<'a> { Some(vec) } } + +/// Parser for boot messages +struct BootMessageParser<'a> { + buffer: &'a [u8], +} + +impl<'a> BootMessageParser<'a> { + pub fn new(buffer: &'a [u8]) -> Self { + Self { buffer } + } + + /// Matches if a boot sequence is detected an a ready message is found + pub fn handle(self) -> Result<(&'a [u8], usize), ParseError> { + let mut is_boot_seq = false; + let mut size = 0; + + for line in self.buffer.split(|b| b == &b'\n') { + size += line.len() + 1; + + if !is_boot_seq && self.is_boot_line(line) { + is_boot_seq = true; + continue; + } + + if is_boot_seq && line == b"ready\r" { + return Ok((b"ready\r\n", size)); + } + } + + Err(ParseError::NoMatch) + } + + /// Returns true if a boot line like "ets Jan 8 2013,rst cause:1, boot mode:(3,7)" is found + fn is_boot_line(&self, line: &[u8]) -> bool { + if let Ok(decoded) = core::str::from_utf8(line) { + return decoded.contains("rst cause:"); + } + + false + } +} diff --git a/src/wifi.rs b/src/wifi.rs index 838bcc4..f7d3d64 100644 --- a/src/wifi.rs +++ b/src/wifi.rs @@ -29,11 +29,11 @@ //! assert_eq!("10.0.0.181", address.ipv4.unwrap().to_string()); //! ```` use crate::commands::{ - AccessPointConnectCommand, CommandErrorHandler, ObtainLocalAddressCommand, SetSocketReceivingModeCommand, - WifiModeCommand, + AccessPointConnectCommand, AutoConnectCommand, CommandErrorHandler, ObtainLocalAddressCommand, RestartCommand, + SetSocketReceivingModeCommand, WifiModeCommand, }; use crate::responses::LocalAddressResponse; -use crate::stack::SocketState; +use crate::stack::{ConnectionState, SocketState}; use crate::urc::URCMessages; use atat::heapless::Vec; use atat::{AtatClient, AtatCmd, Error as AtError}; @@ -43,6 +43,7 @@ use embedded_nal::{Ipv4Addr, Ipv6Addr}; use fugit::{ExtU32, TimerDurationU32}; use fugit_timer::Timer; use heapless::String; +use nb::Error; /// Wifi network adapter trait pub trait WifiAdapter { @@ -52,6 +53,12 @@ pub trait WifiAdapter { /// Error when receiving local address information type AddressError: Debug; + /// Errors for configuration commands + type ConfigurationErrors: Debug; + + /// Errors when restarting the module + type RestartError: Debug; + /// Connects to an WIFI access point and returns the connection state fn join(&mut self, ssid: &str, key: &str) -> Result; @@ -60,6 +67,12 @@ pub trait WifiAdapter { /// Returns local address information fn get_address(&mut self) -> Result; + + /// Enables/Disables auto connect, so that ESP-AT whether automatically joins to the stored AP when powered on. + fn set_auto_connect(&mut self, enabled: bool) -> Result<(), Self::ConfigurationErrors>; + + /// Restarts the module and blocks until ready + fn restart(&mut self) -> Result<(), Self::RestartError>; } /// Central client for network communication @@ -78,12 +91,22 @@ pub struct Adapter, const TIMER_HZ: u32, const /// Timeout for data transmission pub(crate) send_timeout: TimerDurationU32, + /// Network state + pub(crate) session: Session, +} + +/// Collection of network state +#[derive(Default)] +pub(crate) struct Session { /// Currently joined to WIFI network? Gets updated by URC messages. joined: bool, /// True if an IP was assigned by access point. Get updated by URC message. ip_assigned: bool, + /// True if a URC ready message arrived. + ready: bool, + /// True if multiple connections have been enabled pub(crate) multi_connections_enabled: bool, @@ -93,9 +116,6 @@ pub struct Adapter, const TIMER_HZ: u32, const /// Current socket states, array index = link_id pub(crate) sockets: [SocketState; 5], - /// Data length available to receive which is buffered by ESP-AT. Array index = link_id - pub(crate) data_available: [usize; 5], - /// Received byte count confirmed by URC message. Gets reset to NONE by 'send()' method pub(crate) recv_byte_count: Option, @@ -111,6 +131,34 @@ pub struct Adapter, const TIMER_HZ: u32, const pub(crate) data: Option>, } +impl Session { + /// Handles a single URC message + pub(crate) fn handle_urc(&mut self, message: URCMessages) { + match message { + URCMessages::WifiDisconnected => { + self.joined = false; + self.ip_assigned = false; + } + URCMessages::ReceivedIP => self.ip_assigned = true, + URCMessages::WifiConnected => self.joined = true, + URCMessages::Ready => self.ready = true, + URCMessages::SocketConnected(link_id) => self.sockets[link_id].state = ConnectionState::Connected, + URCMessages::SocketClosed(link_id) => self.sockets[link_id].state = ConnectionState::Closing, + URCMessages::AlreadyConnected => self.already_connected = true, + URCMessages::ReceivedBytes(count) => self.recv_byte_count = Some(count), + URCMessages::SendConfirmation => self.send_confirmed = Some(true), + URCMessages::SendFail => self.send_confirmed = Some(false), + URCMessages::DataAvailable(link_id, length) => { + if link_id < self.sockets.len() { + self.sockets[link_id].data_available = length; + } + } + URCMessages::Data(data) => self.data = Some(data), + URCMessages::Unknown => {} + } + } +} + /// Possible errors when joining an access point #[derive(Clone, Debug, PartialEq)] pub enum JoinError { @@ -148,6 +196,23 @@ pub enum AddressErrors { UnexpectedWouldBlock, } +/// General errors for simple commands (e.g. enabling a configuration flag) +#[derive(Clone, Debug, PartialEq)] +pub enum CommandError { + /// Command failed with the given upstream error + CommandFailed(AtError), + + /// No ready message received within timout (5 seconds) + ReadyTimeout, + + /// Upstream timer error + TimerError, + + /// Received an unexpected WouldBlock. The most common cause of errors is an incorrect mode of the client. + /// This must be either timeout or blocking. + UnexpectedWouldBlock, +} + /// Current WIFI connection state #[derive(Copy, Clone, Debug)] pub struct JoinState { @@ -163,6 +228,8 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz { type JoinError = JoinError; type AddressError = AddressErrors; + type ConfigurationErrors = CommandError; + type RestartError = CommandError; /// Connects to an WIFI access point and returns the connection state /// @@ -176,8 +243,8 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz self.process_urc_messages(); Ok(JoinState { - connected: self.joined, - ip_assigned: self.ip_assigned, + connected: self.session.joined, + ip_assigned: self.session.ip_assigned, }) } @@ -185,16 +252,47 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz fn get_join_status(&mut self) -> JoinState { self.process_urc_messages(); JoinState { - connected: self.joined, - ip_assigned: self.ip_assigned, + connected: self.session.joined, + ip_assigned: self.session.ip_assigned, } } /// Returns local address information fn get_address(&mut self) -> Result { - let responses = self.send_command(ObtainLocalAddressCommand::new())?; + let responses = self.send_command::<_, 10>(ObtainLocalAddressCommand::new())?; LocalAddress::from_responses(responses) } + + /// Enables auto connect, so that ESP-AT automatically connects to the stored AP when powered on. + fn set_auto_connect(&mut self, enabled: bool) -> Result<(), CommandError> { + self.send_command(AutoConnectCommand::new(enabled))?; + Ok(()) + } + + /// Restarts the module and blocks until the module is ready. + /// If module is not ready within five seconds, [CommandError::ReadyTimeout] is returned + fn restart(&mut self) -> Result<(), CommandError> { + self.session.ready = false; + self.send_command(RestartCommand::default())?; + + self.session = Session::default(); + + self.timer.start(5.secs()).map_err(|_| CommandError::TimerError)?; + while !self.session.ready { + if let nb::Result::Err(error) = self.timer.wait() { + match error { + Error::Other(_) => return Err(CommandError::TimerError), + Error::WouldBlock => {} + } + } else { + return Err(CommandError::ReadyTimeout); + } + + self.process_urc_messages(); + } + + Ok(()) + } } impl, const TIMER_HZ: u32, const TX_SIZE: usize, const RX_SIZE: usize> @@ -206,56 +304,20 @@ impl, const TIMER_HZ: u32, const TX_SIZE: usiz client, timer, send_timeout: 5_000.millis(), - joined: false, - ip_assigned: false, - multi_connections_enabled: false, - passive_mode_enabled: false, - sockets: [SocketState::Closed; 5], - data_available: [0; 5], - recv_byte_count: None, - send_confirmed: None, - already_connected: false, - data: None, + session: Session::default(), } } /// Processes all pending messages in the queue pub(crate) fn process_urc_messages(&mut self) { while let Some(message) = self.client.check_urc::>() { - self.handle_urc(message) + self.session.handle_urc(message) } // Avoid full response queue, which gets full for a unknown reason let _ = self.client.check_response(&SetSocketReceivingModeCommand::passive_mode()); } - /// Handles a single URC message - pub(crate) fn handle_urc(&mut self, message: URCMessages) { - match message { - URCMessages::WifiDisconnected => { - self.joined = false; - self.ip_assigned = false; - } - URCMessages::ReceivedIP => self.ip_assigned = true, - URCMessages::WifiConnected => self.joined = true, - URCMessages::Ready => {} - URCMessages::SocketConnected(link_id) => self.sockets[link_id] = SocketState::Connected, - URCMessages::SocketClosed(link_id) => self.sockets[link_id] = SocketState::Closing, - URCMessages::AlreadyConnected => self.already_connected = true, - URCMessages::ReceivedBytes(count) => self.recv_byte_count = Some(count), - URCMessages::SendConfirmation => self.send_confirmed = Some(true), - URCMessages::SendFail => self.send_confirmed = Some(false), - URCMessages::DataAvailable(link_id, length) => { - if link_id < self.sockets.len() { - self.data_available[link_id] = length; - } - } - URCMessages::Data(data) => self.data = Some(data), - URCMessages::Echo => {} - URCMessages::Unknown => {} - } - } - /// Sends the command for switching to station mode fn set_station_mode(&mut self) -> Result<(), JoinError> { let command = WifiModeCommand::station_mode();