diff --git a/rust/esp8266at/.gitignore b/rust/esp8266at/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/rust/esp8266at/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/rust/esp8266at/Cargo.toml b/rust/esp8266at/Cargo.toml new file mode 100644 index 0000000..1120051 --- /dev/null +++ b/rust/esp8266at/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "esp8266at" +version = "0.1.0" +authors = ["Wladimir J. van der Laan "] +edition = "2018" + +[features] +std = [] +default = ["std"] + +[dependencies] +nom = { version = "4", default-features = false } + +[dev-dependencies] +clap = "2" +serialport = { version = "3", default-features = false } +arrayvec = "0.4" + +[[example]] +name = "parsetest" +required-features = ["std"] + +[[example]] +name = "serial" +required-features = ["std"] diff --git a/rust/esp8266at/README.md b/rust/esp8266at/README.md new file mode 100644 index 0000000..80ad3d8 --- /dev/null +++ b/rust/esp8266at/README.md @@ -0,0 +1,4 @@ +# `esp8266at` + +A crate for communicating with WiFi using the ESP8266 using AT commands. + diff --git a/rust/esp8266at/data/parses.txt b/rust/esp8266at/data/parses.txt new file mode 100644 index 0000000..2a86b43 --- /dev/null +++ b/rust/esp8266at/data/parses.txt @@ -0,0 +1,53 @@ +> AT +< OK +> AT+CWJAP_CUR? +< No AP +< OK +> AT+CWQAP +< OK +> AT+CWMODE? +< +CWMODE:1 +< OK +> AT+CWJAP_CUR="x","x" +< WIFI DISCONNECT +> AT+CWJAP_CUR="x","x" +< busy p... +> AT+CWJAP_CUR="x","x" +< busy p... +< +CWJAP:3 +< FAIL +> AT +< OK +> AT+CWJAP_CUR? +< No AP +< OK +> AT+CWQAP +< OK +> AT+CWMODE? +< +CWMODE:1 +< OK +> AT+CWJAP_CUR="x","x" +< WIFI CONNECTED +< WIFI GOT IP +< OK +> AT+CIFSR +< +CIFSR:STAIP,"192.168.1.1" +< +CIFSR:STAMAC,"12:34:56:78:9a:bc" +< OK +> AT+CIPSTATUS +< STATUS:2 +< OK +> AT+CIPMUX=0 +< OK +> AT+CIPSTART="TCP","wttr.in",80 +< CONNECT +< OK +> AT+CIPSEND=81 +< OK +< > +< Recv 81 bytes +< SEND OK +> AT+CWJAP_CUR? +< +CWJAP_CUR:"xxxxxxxx","12:34:56:78:9a:bc",10,-58 +< +IPD:8:012345 +< diff --git a/rust/esp8266at/examples/parsetest.rs b/rust/esp8266at/examples/parsetest.rs new file mode 100644 index 0000000..73ea3bf --- /dev/null +++ b/rust/esp8266at/examples/parsetest.rs @@ -0,0 +1,33 @@ +use std::fs::File; +use std::io::BufRead; +use std::io::BufReader; + +use esp8266at::response::parse_response; + +fn main() { + let f = File::open("data/parses.txt").unwrap(); + let file = BufReader::new(&f); + for line in file.lines() { + let l = line.unwrap(); + if l.len() >= 2 { + let mut lb = l[2..].as_bytes().to_vec(); + lb.push(13); + lb.push(10); + let res = parse_response(&lb); + match res { + Err(x) => { + println!("failed command was: {}", l); + println!("{:?}", x); + } + Ok((res, x)) => { + if res.is_empty() { + println!("{:?}", x); + } else { + println!("non-empty residue command was: {}", l); + println!("{:?} {:?}", res, x); + } + } + } + } + } +} diff --git a/rust/esp8266at/examples/serial.rs b/rust/esp8266at/examples/serial.rs new file mode 100644 index 0000000..392a8f3 --- /dev/null +++ b/rust/esp8266at/examples/serial.rs @@ -0,0 +1,106 @@ +use std::fmt; +use std::str; +use std::time::Duration; + +use clap::{App, AppSettings, Arg}; +use serialport::prelude::*; + +use esp8266at::handler::{NetworkEvent, SerialNetworkHandler}; +use esp8266at::mainloop::mainloop; +use esp8266at::response::ConnectionType; +use esp8266at::traits::Write; + +struct StdoutDebug {} +impl fmt::Write for StdoutDebug { + fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { + print!("{}", s); + Ok(()) + } +} + +fn main() { + let matches = App::new("Serialport Example - Receive Data") + .about("Reads data from a serial port and echoes it to stdout") + .setting(AppSettings::DisableVersion) + .arg( + Arg::with_name("port") + .help("The device path to a serial port") + .use_delimiter(false) + .required(true), + ) + .arg( + Arg::with_name("baud") + .help("The baud rate to connect at") + .use_delimiter(false) + .required(true), + ) + .arg( + Arg::with_name("apname") + .help("Access point name") + .use_delimiter(false) + .required(true), + ) + .arg( + Arg::with_name("appass") + .help("Access point password") + .use_delimiter(false) + .required(true), + ) + .get_matches(); + let port_name = matches.value_of("port").unwrap(); + let baud_rate = matches.value_of("baud").unwrap(); + let apname = matches.value_of("apname").unwrap(); + let appass = matches.value_of("appass").unwrap(); + + let mut settings: SerialPortSettings = Default::default(); + settings.timeout = Duration::from_millis(10); + if let Ok(rate) = baud_rate.parse::() { + settings.baud_rate = rate.into(); + } else { + eprintln!("Error: Invalid baud rate '{}' specified", baud_rate); + ::std::process::exit(1); + } + + match serialport::open_with_settings(&port_name, &settings) { + Ok(mut tx) => { + // Split into TX and RX halves + let mut rx = tx.try_clone().unwrap(); + println!("Receiving data on {} at {} baud:", &port_name, &baud_rate); + let mut sh = SerialNetworkHandler::new(&mut tx, apname.as_bytes(), appass.as_bytes()); + + sh.start(true).unwrap(); + + mainloop(&mut sh, &mut rx, |port, ev, debug| { + match ev { + NetworkEvent::Ready => { + writeln!(debug, "--Ready--").unwrap(); + port.connect(ConnectionType::TCP, b"wttr.in", 80).unwrap(); + //port.connect(0, ConnectionType::SSL, b"wttr.in", 443); + true + } + NetworkEvent::Error => { + writeln!(debug, "--Could not connect to AP--").unwrap(); + false + } + NetworkEvent::ConnectionEstablished(_) => { + port.write_all(b"GET /?0qA HTTP/1.1\r\nHost: wttr.in\r\nConnection: close\r\nUser-Agent: Weather-Spy\r\n\r\n").unwrap(); + port.send(0).unwrap(); + true + } + NetworkEvent::Data(_, data) => { + write!(debug, "{}", str::from_utf8(data).unwrap()).unwrap(); + true + } + NetworkEvent::ConnectionClosed(_) => { + false + } + _ => { true } + } + }, &mut StdoutDebug {}).unwrap(); + } + Err(e) => { + eprintln!("Failed to open \"{}\". Error: {}", port_name, e); + ::std::process::exit(1); + } + } +} diff --git a/rust/esp8266at/src/handler.rs b/rust/esp8266at/src/handler.rs new file mode 100644 index 0000000..769c653 --- /dev/null +++ b/rust/esp8266at/src/handler.rs @@ -0,0 +1,325 @@ +/** ESP8285 serial WiFi network handler, for connecting to AP and making connections */ +use core::{fmt, str}; + +use crate::response::{ + CmdResponse, ConnectionType, GenResponse, IPAddress, MACAddress, Response, Status, +}; +use crate::traits::Write; +use crate::util::{write_num_u32, write_qstr}; + +/** Handler state */ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum State { + Initial, + SetStationMode, + // QueryCurrentAP, + ConnectingToAP, + QueryIP, + SetMux, + MakeConnection(u32), + Error, + Idle, + Sending(u32), +} + +/** Wifi network state */ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum WifiState { + Unknown, + Disconnected, + Connected, + GotIP, +} + +/** Event type for callback */ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NetworkEvent<'a> { + /** Network handler became idle */ + Ready, + /** Error connecting to AP */ + Error, + ConnectionEstablished(u32), + ConnectionFailed(u32), + Data(u32, &'a [u8]), + ConnectionClosed(u32), + SendComplete(u32), +} + +/** Max CIPSEND buffer size */ +const TX_BUFFER_SIZE: usize = 2048; +/** Max link_id */ +const MAX_NUM_LINKS: usize = 5; + +/** ESP8285 serial WiFi network handler */ +pub struct SerialNetworkHandler<'a, S> +where + S: Write, +{ + /** Serial port */ + port: &'a mut S, + /** Handler state */ + state: State, + /** Current AP connction state */ + wifistate: WifiState, + /** Current IP */ + ip: Option, + /** Current MAC */ + mac: Option, + /** Access point name to connect to */ + apname: &'a [u8], + /** Access point password */ + appass: &'a [u8], + /** Send buffer */ + txbuf: [u8; TX_BUFFER_SIZE], + /** Send buffer size */ + txn: usize, + /** Connection slots (in use) */ + links: [bool; MAX_NUM_LINKS], +} + +impl<'a, S> SerialNetworkHandler<'a, S> +where + S: Write, +{ + pub fn new(port: &'a mut S, apname: &'a [u8], appass: &'a [u8]) -> Self { + Self { + port, + state: State::Initial, + wifistate: WifiState::Unknown, + ip: None, + mac: None, + apname, + appass, + txbuf: [0; TX_BUFFER_SIZE], + txn: 0, + links: [false; MAX_NUM_LINKS], + } + } + + /** Start off network handling by checking liveness of the link and ESP device. */ + pub fn start(&mut self, echo: bool) -> Result<(), S::Error> { + assert!(self.state == State::Initial || self.state == State::Idle); + if echo { + self.port.write_all(b"AT\r\n")?; + } else { + // Disable echo as very first thing to avoid excess serial traffic + self.port.write_all(b"ATE0\r\n")?; + } + Ok(()) + } + + /** Handle an incoming message */ + pub fn message( + &mut self, + resp: &Response, + on_event: &mut F, + debug: &mut fmt::Write, + ) -> Result<(), S::Error> + where + F: FnMut(&mut Self, NetworkEvent, &mut fmt::Write), + { + match resp { + Response::Echo(data) => { + writeln!(debug, "→ {}", str::from_utf8(data).unwrap_or("???")).unwrap(); + } + Response::Data(link, _) => { + writeln!(debug, "← Data({}, [...])", link).unwrap(); + } + _ => { + writeln!(debug, "← {:?}", resp).unwrap(); + } + } + match self.state { + State::Initial => { + if let Response::Gen(GenResponse::OK) = resp { + writeln!(debug, "Initial AT confirmed - configuring station mode").unwrap(); + // Set station mode so that we're sure we can connect to an AP + self.port.write_all(b"AT+CWMODE_CUR=1\r\n").unwrap(); + self.state = State::SetStationMode; + } + // TODO: retry if ERROR + } + State::SetStationMode => { + if let Response::Gen(GenResponse::OK) = resp { + writeln!(debug, "Station mode set - connecting to AP").unwrap(); + self.port.write_all(b"AT+CWJAP_CUR=").unwrap(); + write_qstr(self.port, self.apname)?; + self.port.write_all(b",")?; + write_qstr(self.port, self.appass)?; + self.port.write_all(b"\r\n")?; + self.state = State::ConnectingToAP; + } + } + State::ConnectingToAP => match resp { + Response::Gen(GenResponse::FAIL) | Response::Gen(GenResponse::ERROR) => { + writeln!(debug, "Fatal: failed to connect to AP").unwrap(); + self.state = State::Error; + on_event(self, NetworkEvent::Error, debug); + } + Response::Gen(GenResponse::OK) => { + if self.wifistate != WifiState::GotIP { + writeln!(debug, "Warning: succesful but did not get IP yet").unwrap(); + } + writeln!(debug, "Succesfully connected to AP").unwrap(); + self.port.write_all(b"AT+CIFSR\r\n")?; + self.state = State::QueryIP; + } + _ => {} + }, + State::QueryIP => { + match resp { + Response::Gen(GenResponse::OK) => { + writeln!(debug, "Succesfully queried IP").unwrap(); + + // Multi-connection mode + self.port.write_all(b"AT+CIPMUX=1\r\n")?; + self.state = State::SetMux; + } + _ => {} + } + } + State::SetMux => match resp { + Response::Gen(GenResponse::OK) => { + writeln!(debug, "Succesfully set multi-connection mode").unwrap(); + + self.state = State::Idle; + on_event(self, NetworkEvent::Ready, debug); + } + _ => {} + }, + State::MakeConnection(link) => match resp { + Response::Gen(GenResponse::OK) => { + self.state = State::Idle; + on_event(self, NetworkEvent::ConnectionEstablished(link), debug); + } + Response::Gen(GenResponse::ERROR) => { + self.state = State::Idle; + on_event(self, NetworkEvent::ConnectionFailed(link), debug); + } + _ => {} + }, + State::Sending(link) => { + match resp { + Response::Gen(GenResponse::OK) => {} + Response::Gen(GenResponse::ERROR) => {} + Response::RecvPrompt => { + // Send queued data + self.port.write_all(&self.txbuf[0..self.txn])?; + self.txn = 0; + } + Response::Status(Status::SEND_OK) => { + self.state = State::Idle; + on_event(self, NetworkEvent::SendComplete(link), debug); + } + _ => {} + } + } + _ => {} + } + match resp { + Response::Status(Status::WIFI_DISCONNECT) => { + writeln!(debug, "Disconnected from AP").unwrap(); + self.wifistate = WifiState::Disconnected; + self.ip = None; + self.mac = None; + } + Response::Status(Status::WIFI_CONNECTED) => { + writeln!(debug, "Connected to AP").unwrap(); + self.wifistate = WifiState::Connected; + } + Response::Status(Status::WIFI_GOT_IP) => { + writeln!(debug, "Have IP").unwrap(); + self.wifistate = WifiState::GotIP; + } + Response::Status(Status::CONNECT(link)) => { + // Mark connection slot id as connected + self.links[*link as usize] = true; + } + Response::Status(Status::CLOSED(link)) => { + // Mark connection slot id as closed + self.links[*link as usize] = false; + on_event(self, NetworkEvent::ConnectionClosed(*link), debug); + } + Response::Cmd(CmdResponse::CIFSR_STAIP(ip)) => { + self.ip = Some(*ip); + writeln!(debug, "Queried IP: {}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]).unwrap(); + } + Response::Cmd(CmdResponse::CIFSR_STAMAC(mac)) => { + self.mac = Some(*mac); + writeln!( + debug, + "Queried MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] + ) + .unwrap(); + } + Response::Data(link, data) => { + on_event(self, NetworkEvent::Data(*link, data), debug); + } + _ => {} + } + Ok(()) + } + + /** Initiate a connection */ + pub fn connect( + &mut self, + ctype: ConnectionType, + addr: &[u8], + port: u32, + ) -> Result { + assert!(self.state == State::Idle); + // pick out a free link slot automatically + let link = self.links.iter().position(|used| !used).unwrap() as u32; + assert!(!self.links[link as usize]); + self.port.write_all(b"AT+CIPSTART=")?; + write_num_u32(self.port, link)?; + self.port.write_all(b",")?; + write_qstr( + self.port, + match ctype { + ConnectionType::TCP => b"TCP", + ConnectionType::UDP => b"UDP", + ConnectionType::SSL => b"SSL", + }, + )?; + self.port.write_all(b",")?; + write_qstr(self.port, addr)?; + self.port.write_all(b",")?; + write_num_u32(self.port, port)?; + self.port.write_all(b"\r\n")?; + self.state = State::MakeConnection(link); + Ok(link) + } + + /** Send contents of send buffer to a connection */ + pub fn send(&mut self, link: u32) -> Result<(), S::Error> { + assert!(self.state == State::Idle); + self.port.write_all(b"AT+CIPSEND=")?; + write_num_u32(self.port, link)?; + self.port.write_all(b",")?; + write_num_u32(self.port, self.txn as u32)?; + self.port.write_all(b"\r\n")?; + self.state = State::Sending(link); + Ok(()) + } +} + +/** Write trait for writing to send buffer */ +impl<'a, S> Write for SerialNetworkHandler<'a, S> +where + S: Write, +{ + type Error = (); + + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + assert!(self.state == State::Idle); + if (self.txn + buf.len()) <= TX_BUFFER_SIZE { + self.txbuf[self.txn..self.txn + buf.len()].copy_from_slice(buf); + self.txn += buf.len(); + Ok(()) + } else { + Err(()) + } + } +} diff --git a/rust/esp8266at/src/lib.rs b/rust/esp8266at/src/lib.rs new file mode 100644 index 0000000..e76dce1 --- /dev/null +++ b/rust/esp8266at/src/lib.rs @@ -0,0 +1,10 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#[macro_use] +extern crate nom; + +pub mod handler; +#[cfg(feature = "std")] +pub mod mainloop; +pub mod response; +pub mod traits; +mod util; diff --git a/rust/esp8266at/src/mainloop.rs b/rust/esp8266at/src/mainloop.rs new file mode 100644 index 0000000..ddbf1b1 --- /dev/null +++ b/rust/esp8266at/src/mainloop.rs @@ -0,0 +1,81 @@ +/** Example synchronous serial receive event loop (for std) */ +use nom::Offset; +use std::fmt; +use std::io; + +use crate::handler::{NetworkEvent, SerialNetworkHandler}; +use crate::response::parse_response; + +/** Mainloop handling serial input and dispatching network events */ +pub fn mainloop( + h: &mut SerialNetworkHandler, + port: &mut P, + mut f: F, + debug: &mut fmt::Write, +) -> io::Result<()> +where + P: io::Read, + F: FnMut(&mut SerialNetworkHandler, NetworkEvent, &mut fmt::Write) -> bool, + X: io::Write, +{ + let mut serial_buf: Vec = vec![0; 2560]; // 2048 + some + let mut ofs: usize = 0; + let mut running: bool = true; + while running { + // Receive bytes into buffer + match port.read(&mut serial_buf[ofs..]) { + Ok(t) => { + // io::stdout().write_all(&serial_buf[ofs..ofs+t]).unwrap(); + ofs += t; + + // Loop as long as there's something in the buffer to parse, starting at the + // beginning + let mut start = 0; + while start < ofs { + // try parsing + let tail = &serial_buf[start..ofs]; + let erase = match parse_response(tail) { + Ok((residue, resp)) => { + h.message( + &resp, + &mut |a, b, debug| { + running = f(a, b, debug); + }, + debug, + )?; + + tail.offset(residue) + } + Err(nom::Err::Incomplete(_)) => { + // Incomplete, ignored, just retry after a new receive + 0 + } + Err(err) => { + writeln!(debug, "err: {:?}", err).unwrap(); + // Erase unparseable data to next line, if line is complete + if let Some(ofs) = tail.iter().position(|&x| x == b'\n') { + ofs + 1 + } else { + // If not, retry next time + 0 + } + } + }; + if erase == 0 { + // End of input or remainder unparseable + break; + } + start += erase; + } + // Erase everything before new starting offset + for i in start..ofs { + serial_buf[i - start] = serial_buf[i]; + } + ofs -= start; + } + Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (), + Err(e) => return Err(e), + } + } + Ok(()) +} diff --git a/rust/esp8266at/src/response.rs b/rust/esp8266at/src/response.rs new file mode 100644 index 0000000..e3264ca --- /dev/null +++ b/rust/esp8266at/src/response.rs @@ -0,0 +1,305 @@ +use core::str; +/** Parser for ESP8266 AT responses */ +use nom::{digit, hex_digit}; + +/** Connection type for CIPSTATUS etc */ +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ConnectionType { + TCP, + UDP, + SSL, +} + +/** General command responses/statuses */ +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GenResponse { + /** Command finished with OK response */ + OK, + /** Command finished with ERROR response */ + ERROR, + /** Command finished with FAIL response */ + FAIL, + /** Command could not be executed because device is busy sending */ + BUSY_S, + /** Command could not be executed because device is busy handling previous command */ + BUSY_P, +} + +/** Async status messages */ +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Status { + READY, + WIFI_DISCONNECT, + WIFI_CONNECTED, + WIFI_GOT_IP, + RECV_BYTES(u32), + SEND_OK, + /** TCP/UDP connection connected */ + CONNECT(u32), + /** TCP/UDP connection closed */ + CLOSED(u32), +} + +pub type IPAddress = [u8; 4]; +pub type MACAddress = [u8; 6]; + +/** Specific command responses */ +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CmdResponse<'a> { + NO_AP, + CWMODE(u32), + CWJAP(u32), + CWJAP_CUR(&'a [u8], &'a [u8], i32, i32), + CIFSR_STAIP(IPAddress), + CIFSR_STAMAC(MACAddress), + STATUS(u32), + ALREADY_CONNECTED, +} + +/** Parsed response */ +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Response<'a> { + Empty, + Gen(GenResponse), + Status(Status), + Cmd(CmdResponse<'a>), + Data(u32, &'a [u8]), + Echo(&'a [u8]), + RecvPrompt, +} + +/* Decimal unsigned integer */ +named!(num_u32<&[u8], u32>, + // The unwrap() here is safe because digit will never return non-UTF8 + map_res!(digit, |s| { str::from_utf8(s).unwrap().parse::() }) +); + +/* Decimal signed integer. + * May start with unary minus. + */ +named!(num_i32<&[u8], i32>, + // The unwrap() here is safe because digit will never return non-UTF8 + map_res!( + recognize!( + tuple!( + opt!(one_of!(b"-")), + digit + ) + ), + |s| { str::from_utf8(s).unwrap().parse::() }) +); + +/* Decimal byte */ +named!(num_u8<&[u8], u8>, + // The unwrap() here is safe because digit will never return non-UTF8 + map_res!(digit, |s| { str::from_utf8(s).unwrap().parse::() }) +); + +/* Hex byte */ +named!(hex_u8<&[u8], u8>, + // The unwrap() here is safe because digit will never return non-UTF8 + map_res!(hex_digit, |s| { u8::from_str_radix(str::from_utf8(s).unwrap(), 16) }) +); + +/* Quoted string */ +named!(qstr<&[u8], &[u8]>, + do_parse!( + tag!(b"\"") >> + a: escaped!(is_not!("\\\""), b'\\', one_of!(b"\"\\")) >> + //a: is_not!("\"") >> + tag!(b"\"") >> + ( a ) + ) +); + +/* Quoted IP */ +named!(qip<&[u8], IPAddress>, + do_parse!( + tag!(b"\"") >> + a: num_u8 >> + tag!(b".") >> + b: num_u8 >> + tag!(b".") >> + c: num_u8 >> + tag!(b".") >> + d: num_u8 >> + tag!(b"\"") >> + ( [a, b, c, d] ) + ) +); + +/* Quoted MAC address */ +named!(qmac<&[u8], MACAddress>, + do_parse!( + tag!(b"\"") >> + a: hex_u8 >> + tag!(b":") >> + b: hex_u8 >> + tag!(b":") >> + c: hex_u8 >> + tag!(b":") >> + d: hex_u8 >> + tag!(b":") >> + e: hex_u8 >> + tag!(b":") >> + f: hex_u8 >> + tag!(b"\"") >> + ( [a, b, c, d, e, f] ) + ) +); + +/* Parse general responses */ +named!(genresponse<&[u8],GenResponse>, + alt!( + tag!(b"OK") => { |_| GenResponse::OK } + | tag!(b"ERROR") => { |_| GenResponse::ERROR } + | tag!(b"FAIL") => { |_| GenResponse::FAIL } + | tag!(b"busy s...") => { |_| GenResponse::BUSY_S } + | tag!(b"busy p...") => { |_| GenResponse::BUSY_P } + ) +); + +/* Parse status messages */ +named!(status<&[u8],Status>, + alt!( + //tag!(b"") => { |_| Status::EMPTY } + tag!(b"ready") => { |_| Status::READY } + | tag!(b"WIFI DISCONNECT") => { |_| Status::WIFI_DISCONNECT } + | tag!(b"WIFI CONNECTED") => { |_| Status::WIFI_CONNECTED } + | tag!(b"WIFI GOT IP") => { |_| Status::WIFI_GOT_IP } + | tag!(b"SEND OK") => { |_| Status::SEND_OK } + | do_parse!( + tag!(b"Recv ") >> + a: num_u32 >> + tag!(b" bytes") >> + ( Status::RECV_BYTES(a) ) + ) + | do_parse!( + id: num_u32 >> + tag!(b",") >> + r: alt!( + tag!(b"CONNECT") => { |_| Status::CONNECT(id) } + | tag!(b"CLOSED") => { |_| Status::CLOSED(id) } + ) >> + ( r ) + ) + ) +); + +/* Parse command-response messages */ +named!(cmdresponse<&[u8],CmdResponse>, + alt!( + /* AT+CWJAP_CUR? */ + tag!(b"No AP") => { |_| CmdResponse::NO_AP } + | do_parse!( + tag!(b"+CWJAP_CUR:") >> + a: qstr >> + tag!(",") >> + b: qstr >> + tag!(",") >> + c: num_i32 >> + tag!(",") >> + d: num_i32 >> + (CmdResponse::CWJAP_CUR(a,b,c,d)) + ) + /* AT+CWMODE? */ + | do_parse!( + tag!(b"+CWMODE:") >> + a: num_u32 >> + (CmdResponse::CWMODE(a)) + ) + | do_parse!( + tag!(b"+CWJAP:") >> + a: num_u32 >> + (CmdResponse::CWJAP(a)) + ) + | do_parse!( + tag!(b"+CIFSR:STAIP,") >> + a: qip >> + (CmdResponse::CIFSR_STAIP(a)) + ) + | do_parse!( + tag!(b"+CIFSR:STAMAC,") >> + a: qmac >> + (CmdResponse::CIFSR_STAMAC(a)) + ) + /* AT+CIPSTATUS */ + | do_parse!( + tag!(b"STATUS:") >> + a: num_u32 >> + (CmdResponse::STATUS(a)) + ) + /* AT+CIPSTART */ + | tag!(b"ALREADY CONNECTED") => { |_| CmdResponse::ALREADY_CONNECTED } + // DNS Fail + ) +); + +/* Parse command-echo messages */ +named!(cmdecho<&[u8],&[u8]>, + recognize!(tuple!( + tag!(b"AT"), + take_until!("\r") + )) +); + +/* Newline-terminated response */ +named!(nl_terminated<&[u8],Response>, + do_parse!( + x: alt!( + genresponse => { |x| Response::Gen(x) } + | status => { |x| Response::Status(x) } + | cmdresponse => { |x| Response::Cmd(x) } + | cmdecho => { |x| Response::Echo(x) } + ) >> + tag!(b"\r\n") >> + (x) + ) +); + +/* Data response */ +named!(ipd_data<&[u8],Response>, + do_parse!( + tag!(b"+IPD") >> + tag!(b",") >> + id: num_u32 >> + tag!(b",") >> + a: num_u32 >> + tag!(b":") >> + b: take!(a) >> + ( Response::Data(id, b) ) + ) +); + +/* Parse response from line */ +named!(pub parse_response<&[u8],Response>, + alt!( + nl_terminated + | ipd_data + | tag!(b"> ") => { |_| Response::RecvPrompt } + | tag!(b"\r\n") => { |_| Response::Empty } + ) +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + assert_eq!( + parse_response(b"AT\r\n"), + Ok((&b""[..], Response::Echo(b"AT"))) + ); + assert_eq!(parse_response(b"\r\n"), Ok((&b""[..], Response::Empty))); + assert_eq!(parse_response(b"> "), Ok((&b""[..], Response::RecvPrompt))); + assert_eq!( + parse_response(b"OK\r\n"), + Ok((&b""[..], Response::Gen(GenResponse::OK))) + ); + } +} diff --git a/rust/esp8266at/src/traits.rs b/rust/esp8266at/src/traits.rs new file mode 100644 index 0000000..87961d9 --- /dev/null +++ b/rust/esp8266at/src/traits.rs @@ -0,0 +1,27 @@ +use core::fmt; +#[cfg(feature = "std")] +use std::io; + +/** The trait that's required of anything acting as serial port writer. + * It is much simpler than io::Write. The reason for implementing our own trait here + * is that this is compatible with no_std. + */ +pub trait Write { + type Error: fmt::Debug; + + /** Write all bytes from the buffer, or fail */ + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error>; +} + +/** Implement our write trait for everything that implements io::Write */ +#[cfg(feature = "std")] +impl Write for X +where + X: io::Write, +{ + type Error = io::Error; + + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + (self as &mut io::Write).write_all(buf) + } +} diff --git a/rust/esp8266at/src/util.rs b/rust/esp8266at/src/util.rs new file mode 100644 index 0000000..acaf7f5 --- /dev/null +++ b/rust/esp8266at/src/util.rs @@ -0,0 +1,85 @@ +use core::slice; + +use crate::traits::Write; + +/** Write quoted string. `\` and `"` are escaped, and the string + * is automatically surrounded with double-quotes. + */ +pub fn write_qstr(w: &mut W, s: &[u8]) -> Result<(), W::Error> +where + W: Write, +{ + w.write_all(b"\"")?; + for ch in s { + w.write_all(match ch { + b'\"' => &[b'\\', b'"'], + b'\\' => &[b'\\', b'\\'], + _ => slice::from_ref(ch), + })?; + } + w.write_all(b"\"")?; + Ok(()) +} + +/** Write decimal unsigned number */ +pub fn write_num_u32(w: &mut W, mut val: u32) -> Result<(), W::Error> +where + W: Write, +{ + let mut buf = [0u8; 10]; + let mut curr = buf.len(); + for byte in buf.iter_mut().rev() { + *byte = b'0' + (val % 10) as u8; + val = val / 10; + curr -= 1; + if val == 0 { + break; + } + } + w.write_all(&buf[curr..]) +} + +#[cfg(test)] +mod tests { + use super::*; + use arrayvec::ArrayVec; + + #[cfg(not(feature = "std"))] + use arrayvec::Array; + #[cfg(not(feature = "std"))] + impl> Write for ArrayVec { + type Error = (); + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + for byte in buf { + self.push(*byte); // Panics if the vector is already full. + } + Ok(()) + } + } + + #[test] + fn test_qstr() { + let mut o = ArrayVec::<[_; 16]>::new(); + write_qstr(&mut o, b"123").unwrap(); + assert_eq!(o.as_slice(), b"\"123\""); + + o.clear(); + write_qstr(&mut o, b"\"\\").unwrap(); + assert_eq!(o.as_slice(), b"\"\\\"\\\\\""); + } + + #[test] + fn test_num() { + let mut o = ArrayVec::<[_; 16]>::new(); + write_num_u32(&mut o, 123).unwrap(); + assert_eq!(o.as_slice(), b"123"); + + o.clear(); + write_num_u32(&mut o, 0).unwrap(); + assert_eq!(o.as_slice(), b"0"); + + o.clear(); + write_num_u32(&mut o, 4294967295).unwrap(); + assert_eq!(o.as_slice(), b"4294967295"); + } +}