mirror of
https://github.com/laanwj/k210-sdk-stuff.git
synced 2024-11-22 09:26:21 +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