mirror of
https://github.com/laanwj/k210-sdk-stuff.git
synced 2024-11-22 01:16:20 +04:00
rust: Add esp8266at library
Add library for communicating with WiFi using the ESP8266 using AT commands. This really should have its own repo at some point.
This commit is contained in:
parent
adc1e73ad1
commit
bfe58fe72b
3
rust/esp8266at/.gitignore
vendored
Normal file
3
rust/esp8266at/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
25
rust/esp8266at/Cargo.toml
Normal file
25
rust/esp8266at/Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "esp8266at"
|
||||
version = "0.1.0"
|
||||
authors = ["Wladimir J. van der Laan <laanwj@gmail.com>"]
|
||||
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"]
|
4
rust/esp8266at/README.md
Normal file
4
rust/esp8266at/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# `esp8266at`
|
||||
|
||||
A crate for communicating with WiFi using the ESP8266 using AT commands.
|
||||
|
53
rust/esp8266at/data/parses.txt
Normal file
53
rust/esp8266at/data/parses.txt
Normal file
@ -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
|
||||
<
|
33
rust/esp8266at/examples/parsetest.rs
Normal file
33
rust/esp8266at/examples/parsetest.rs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
106
rust/esp8266at/examples/serial.rs
Normal file
106
rust/esp8266at/examples/serial.rs
Normal file
@ -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::<u32>() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
325
rust/esp8266at/src/handler.rs
Normal file
325
rust/esp8266at/src/handler.rs
Normal file
@ -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<IPAddress>,
|
||||
/** Current MAC */
|
||||
mac: Option<MACAddress>,
|
||||
/** 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<F>(
|
||||
&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<u32, S::Error> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
}
|
10
rust/esp8266at/src/lib.rs
Normal file
10
rust/esp8266at/src/lib.rs
Normal file
@ -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;
|
81
rust/esp8266at/src/mainloop.rs
Normal file
81
rust/esp8266at/src/mainloop.rs
Normal file
@ -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<P, F, X>(
|
||||
h: &mut SerialNetworkHandler<X>,
|
||||
port: &mut P,
|
||||
mut f: F,
|
||||
debug: &mut fmt::Write,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
P: io::Read,
|
||||
F: FnMut(&mut SerialNetworkHandler<X>, NetworkEvent, &mut fmt::Write) -> bool,
|
||||
X: io::Write,
|
||||
{
|
||||
let mut serial_buf: Vec<u8> = 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(())
|
||||
}
|
305
rust/esp8266at/src/response.rs
Normal file
305
rust/esp8266at/src/response.rs
Normal file
@ -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::<u32>() })
|
||||
);
|
||||
|
||||
/* 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::<i32>() })
|
||||
);
|
||||
|
||||
/* 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::<u8>() })
|
||||
);
|
||||
|
||||
/* 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)))
|
||||
);
|
||||
}
|
||||
}
|
27
rust/esp8266at/src/traits.rs
Normal file
27
rust/esp8266at/src/traits.rs
Normal file
@ -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<X> 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)
|
||||
}
|
||||
}
|
85
rust/esp8266at/src/util.rs
Normal file
85
rust/esp8266at/src/util.rs
Normal file
@ -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>(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>(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<A: Array<Item = u8>> Write for ArrayVec<A> {
|
||||
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");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user