Skip to content

Instantly share code, notes, and snippets.

@timokroeger
Created March 14, 2020 08:24
Show Gist options
  • Select an option

  • Save timokroeger/bcd96b601e9d2749afdcf7dc018db821 to your computer and use it in GitHub Desktop.

Select an option

Save timokroeger/bcd96b601e9d2749afdcf7dc018db821 to your computer and use it in GitHub Desktop.
esp-at
#![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,
(_, _) => (),
}
}
}
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