Created
March 14, 2020 08:24
-
-
Save timokroeger/bcd96b601e9d2749afdcf7dc018db821 to your computer and use it in GitHub Desktop.
esp-at
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #![no_std] | |
| mod message; | |
| use core::{fmt, task::Poll}; | |
| use embedded_hal::blocking::serial::Write; | |
| pub use message::*; | |
| #[derive(Copy, Clone, PartialEq, Debug)] | |
| enum Command { | |
| CwMode, | |
| CwJap, | |
| CipStart, | |
| CipSend, | |
| CipSendData, | |
| CipClose, | |
| } | |
| pub struct Esp<TX: Write<u8> + fmt::Write> { | |
| tx: TX, | |
| cmd: Option<(Command, Poll<Result<(), ()>>)>, | |
| connected: bool, | |
| } | |
| impl<TX: Write<u8> + fmt::Write> Esp<TX> { | |
| pub fn new(tx: TX) -> Self { | |
| Self { | |
| tx, | |
| cmd: None, | |
| connected: false, | |
| } | |
| } | |
| pub fn is_connected(&self) -> bool { | |
| self.connected | |
| } | |
| fn command<F>(&mut self, cmd: Command, write_fn: F) -> Poll<Result<(), ()>> | |
| where | |
| F: FnOnce(&mut TX), | |
| { | |
| match self.cmd { | |
| None => { | |
| write_fn(&mut self.tx); | |
| self.cmd = Some((cmd, Poll::Pending)); | |
| Poll::Pending | |
| } | |
| Some((c, state)) if c == cmd => { | |
| if let Poll::Ready(result) = state { | |
| self.cmd = None; | |
| Poll::Ready(result) | |
| } else { | |
| Poll::Pending | |
| } | |
| } | |
| _ => panic!("Command handling bug!"), | |
| } | |
| } | |
| fn command2<F1, F2>( | |
| &mut self, | |
| cmd1: Command, | |
| write_fn1: F1, | |
| cmd2: Command, | |
| write_fn2: F2, | |
| ) -> Poll<Result<(), ()>> | |
| where | |
| F1: FnOnce(&mut TX), | |
| F2: FnOnce(&mut TX), | |
| { | |
| match self.cmd { | |
| None => { | |
| write_fn1(&mut self.tx); | |
| self.cmd = Some((cmd1, Poll::Pending)); | |
| Poll::Pending | |
| } | |
| Some((c, state)) if c == cmd1 => { | |
| if let Poll::Ready(result) = state { | |
| if result.is_ok() { | |
| write_fn2(&mut self.tx); | |
| self.cmd = Some((cmd2, Poll::Pending)); | |
| Poll::Pending | |
| } else { | |
| Poll::Ready(result) | |
| } | |
| } else { | |
| Poll::Pending | |
| } | |
| } | |
| Some((c, state)) if c == cmd2 => { | |
| if let Poll::Ready(result) = state { | |
| self.cmd = None; | |
| Poll::Ready(result) | |
| } else { | |
| Poll::Pending | |
| } | |
| } | |
| _ => panic!("Command handling bug!"), | |
| } | |
| } | |
| pub fn connect_ap(&mut self, ssid: &str, password: &str) -> Poll<Result<(), ()>> { | |
| self.command2( | |
| Command::CwMode, | |
| |tx| { | |
| write!(tx, "AT+CWMODE=1\r\n").unwrap(); | |
| }, | |
| Command::CwJap, | |
| |tx| write!(tx, "AT+CWJAP=\"{}\",\"{}\"\r\n", ssid, password).unwrap(), | |
| ) | |
| } | |
| pub fn open_tcp(&mut self, host: &str, port: u16) -> Poll<Result<(), ()>> { | |
| self.command(Command::CipStart, |tx| { | |
| write!(tx, "AT+CIPSTART=\"TCP\",\"{}\",{}\r\n", host, port).unwrap() | |
| }) | |
| } | |
| pub fn send_data(&mut self, data: &[u8]) -> Poll<Result<(), ()>> { | |
| self.command2( | |
| Command::CipSend, | |
| |tx| { | |
| write!(tx, "AT+CIPSEND={}\r\n", data.len()).unwrap(); | |
| }, | |
| Command::CipSendData, | |
| |tx| { | |
| tx.bwrite_all(data).ok(); | |
| }, | |
| ) | |
| } | |
| pub fn close(&mut self) -> Poll<Result<(), ()>> { | |
| self.command(Command::CipClose, |tx| { | |
| write!(tx, "AT+CIPCLOSE\r\n").unwrap() | |
| }) | |
| } | |
| pub fn handle_message(&mut self, msg: &Message) { | |
| match (self.cmd, msg) { | |
| (Some((cmd, Poll::Pending)), msg) => { | |
| let next_state = match (msg, cmd) { | |
| (Message::Ok, _) | (Message::SendOk, Command::CipSendData) => { | |
| Poll::Ready(Ok(())) | |
| } | |
| (Message::Error, _) | (Message::SendFail, Command::CipSendData) => { | |
| Poll::Ready(Err(())) | |
| } | |
| (_, _) => Poll::Pending, | |
| }; | |
| self.cmd = Some((cmd, next_state)); | |
| } | |
| (_, Message::WifiConnected) => self.connected = true, | |
| (_, Message::WifiDisconnected) => self.connected = false, | |
| (_, _) => (), | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use core::str; | |
| use nom::{ | |
| branch::alt, | |
| bytes::streaming::{tag, take, take_until}, | |
| character::streaming::digit1, | |
| combinator::{map_res, value}, | |
| sequence::{delimited, pair, terminated}, | |
| IResult, | |
| }; | |
| #[derive(Copy, Clone, PartialEq, Debug)] | |
| pub enum Message { | |
| Ok, | |
| Error, | |
| Ready, | |
| Connect, | |
| SendReady, | |
| SendOk, | |
| SendFail, | |
| Closed, | |
| ReceiveData(usize), | |
| WifiConnected, | |
| WifiGotIP, | |
| WifiDisconnected, | |
| UnkownLine, | |
| } | |
| impl Message { | |
| pub fn parse(mut buf: &[u8]) -> IResult<&[u8], Self> { | |
| // Skip empty lines | |
| while buf.len() > 2 && buf[..2] == b"\r\n"[..] { | |
| buf = &buf[2..]; | |
| } | |
| let line = |p| terminated(p, tag(b"\r\n")); | |
| alt(( | |
| value(Message::SendReady, tag(b">")), | |
| recv_data, | |
| line(alt(( | |
| value(Message::Ok, tag(b"OK")), | |
| value(Message::Error, tag(b"ERROR")), | |
| value(Message::Ready, tag(b"ready")), | |
| value(Message::Connect, tag(b"CONNECT")), | |
| value(Message::SendOk, tag(b"SEND OK")), | |
| value(Message::SendFail, tag(b"SEND FAIL")), | |
| value(Message::Closed, tag(b"CLOSED")), | |
| value(Message::WifiConnected, tag(b"WIFI CONNECTED")), | |
| value(Message::WifiGotIP, tag(b"WIFI GOT IP")), | |
| value(Message::WifiDisconnected, tag(b"WIFI DISCONNECTED")), | |
| ))), | |
| value(Message::UnkownLine, pair(take_until("\r\n"), take(2usize))), | |
| ))(buf) | |
| } | |
| } | |
| #[derive(Copy, Clone, PartialEq, Debug)] | |
| pub enum ParseError { | |
| Incomplete(usize), | |
| Error, | |
| } | |
| // There is a bug in the `nom` crate that makes this parser return the wrong | |
| // number of required bytes when it is incomplete. | |
| fn recv_data(input: &[u8]) -> IResult<&[u8], Message> { | |
| map_res( | |
| delimited(tag(b"+IPD,"), digit1, tag(b":")), | |
| |len_bytes: &[u8]| { | |
| let len_str = unsafe { str::from_utf8_unchecked(len_bytes) }; | |
| len_str | |
| .parse::<usize>() | |
| .map(|len| Message::ReceiveData(len)) | |
| }, | |
| )(input) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use nom::{Err as NomErr, Needed}; | |
| #[test] | |
| fn test_message_try_parse() { | |
| assert_eq!(Message::parse(b"OK\r\n"), Ok((&b""[..], Message::Ok))); | |
| assert_eq!( | |
| Message::parse(b"\r\n\r\nOK\r\n"), | |
| Ok((&b""[..], Message::Ok)) | |
| ); | |
| assert_eq!( | |
| Message::parse(b"OK"), | |
| Err(NomErr::Incomplete(Needed::Size(2))) | |
| ); | |
| assert_eq!(Message::parse(b"ready\r\n"), Ok((&b""[..], Message::Ready))); | |
| assert_eq!(Message::parse(b">"), Ok((&b""[..], Message::SendReady))); | |
| assert_eq!( | |
| Message::parse(b"+IPD,3:xxx"), | |
| Ok((&b"xxx"[..], Message::ReceiveData(3))) | |
| ); | |
| assert_eq!( | |
| Message::parse(b"unknown msg\r\n"), | |
| Ok((&b""[..], Message::UnkownLine)) | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment