├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src ├── lib.rs ├── serial.rs └── timing.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ## [v0.1.1] - 2018-12-17 11 | 12 | ### Changed 13 | 14 | - Fix readme and metadata 15 | 16 | ## [v0.1.0] - 2018-12-17 17 | 18 | ### Added 19 | 20 | - Initial release 21 | 22 | [Unreleased]: https://github.com/stm32-rs/stm32f1xx-hal/compare/v0.1.1...HEAD 23 | [v0.1.1]: https://github.com/thezoq2/esp01-rs/compare/v0.1.1...v0.1.0 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["TheZoq2 "] 3 | edition = "2018" 4 | name = "esp-01" 5 | version = "0.1.1" 6 | description = "Crate to communicate with the esp01 variant of the esp8266 module" 7 | readme = "README.md" 8 | keywords = ["embedded-hal", "esp8266", "esp01"] 9 | license = "MIT OR Apache-2.0" 10 | repository = "https://github.com/TheZoq2/esp01-rs" 11 | 12 | [dependencies] 13 | embedded-hal = "0.2.3" 14 | arrayvec = {version = "0.4.11", default-features = false} 15 | nb = "0.1.2" 16 | itoa = {version = "0.4.4", default-features = false} 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Esp01 2 | 3 | Crate for communicating with the esp01 variant of the esp8266 wifi module using 4 | the embedded hal traits. 5 | 6 | For now the crate is a bit barebones, for example, only running as a client and 7 | sending TCP/UDP messages is supported. 8 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use embedded_hal as hal; 4 | 5 | use nb::block; 6 | 7 | use core::cmp::min; 8 | use core::fmt::{self}; 9 | use arrayvec::{CapacityError, ArrayString}; 10 | use itoa; 11 | 12 | mod serial; 13 | mod timing; 14 | 15 | pub use timing::{LongTimer, Second, Millisecond}; 16 | 17 | /** 18 | Maximum length of an AT response (Length of message + CRLF) 19 | 20 | longest message: `WIFI GOT IP\r\n` 21 | */ 22 | const AT_RESPONSE_BUFFER_SIZE: usize = 13; 23 | 24 | /** 25 | Possible responses from an esp8266 AT command. 26 | 27 | This does not contain all possible responses but it does contain 28 | ever response that can be received from the commands sent in this crates 29 | */ 30 | #[derive(Debug, PartialEq)] 31 | pub enum ATResponse { 32 | Ok, 33 | Error, 34 | Busy, 35 | WiFiGotIp, 36 | } 37 | 38 | /** 39 | Error type for esp communication. 40 | 41 | `R` and `T` are the error types of the serial module 42 | */ 43 | #[derive(Debug)] 44 | pub enum Error { 45 | /// Serial transmission errors 46 | TxError(T), 47 | /// Serial reception errors 48 | RxError(R), 49 | // Digital pin errors 50 | PinError(P), 51 | /// Invalid or unexpected data received from the device 52 | UnexpectedResponse(ATResponse), 53 | /// Errors from the formating of messages 54 | Fmt(fmt::Error), 55 | /// Error indicating an ArrayString wasn't big enough 56 | Capacity(CapacityError) 57 | } 58 | impl From for Error { 59 | fn from(other: fmt::Error) -> Error { 60 | Error::Fmt(other) 61 | } 62 | } 63 | impl From> for Error { 64 | fn from(other: CapacityError) -> Error { 65 | Error::Capacity(other.simplify()) 66 | } 67 | } 68 | 69 | /** 70 | Indicates what step in the data transmission that the sensor is in. Used 71 | in `TransmissionError` for reporting information about where things went wrong 72 | */ 73 | #[derive(Debug)] 74 | pub enum TransmissionStep { 75 | Connect, 76 | Send, 77 | Close 78 | } 79 | /** 80 | Error indicating failure to transmit a message. 81 | */ 82 | #[derive(Debug)] 83 | pub struct TransmissionError { 84 | step: TransmissionStep, 85 | cause: Error 86 | } 87 | 88 | impl TransmissionError { 89 | pub fn try_step(step: TransmissionStep, cause: Result>) 90 | -> Result 91 | { 92 | cause.map_err(|e| { 93 | Self { 94 | step, 95 | cause: e 96 | } 97 | }) 98 | } 99 | } 100 | 101 | 102 | pub enum ConnectionType { 103 | Tcp, 104 | Udp 105 | } 106 | impl ConnectionType { 107 | pub fn as_str(&self) -> &str { 108 | match *self { 109 | ConnectionType::Tcp => "TCP", 110 | ConnectionType::Udp => "UDP" 111 | } 112 | } 113 | } 114 | 115 | 116 | macro_rules! return_type { 117 | ($ok:ty) => { 118 | Result<$ok, Error, Tx::Error, Rst::Error>> 119 | } 120 | } 121 | 122 | macro_rules! transmission_return_type { 123 | ($ok:ty) => { 124 | Result<$ok, TransmissionError, Tx::Error, Rst::Error>> 125 | } 126 | } 127 | 128 | 129 | //////////////////////////////////////////////////////////////////////////////// 130 | //////////////////////////////////////////////////////////////////////////////// 131 | 132 | const STARTUP_TIMEOUT: Second = Second(10); 133 | const DEFAULT_TIMEOUT: Second = Second(5); 134 | 135 | 136 | /** 137 | Struct for interracting with an esp8266 wifi module over USART 138 | */ 139 | pub struct Esp8266 140 | where Tx: hal::serial::Write, 141 | Rx: hal::serial::Read, 142 | Timer: LongTimer, 143 | Rst: hal::digital::v2::OutputPin 144 | { 145 | tx: Tx, 146 | rx: Rx, 147 | timer: Timer, 148 | chip_enable_pin: Rst 149 | } 150 | 151 | impl Esp8266 152 | where Tx: hal::serial::Write, 153 | Rx: hal::serial::Read, 154 | Timer: LongTimer, 155 | Rst: hal::digital::v2::OutputPin, 156 | { 157 | /** 158 | Sets up the esp8266 struct and configures the device for future use 159 | 160 | `tx` and `rx` are the pins used for serial communication, `timer` is 161 | a hardware timer for dealing with things like serial timeout and 162 | `chip_enable_pin` is a pin which must be connected to the CHIP_EN pin 163 | of the device 164 | */ 165 | pub fn new(tx: Tx, rx: Rx, timer: Timer, chip_enable_pin: Rst) 166 | -> return_type!(Self) 167 | { 168 | let mut result = Self {tx, rx, timer, chip_enable_pin}; 169 | 170 | result.reset()?; 171 | 172 | Ok(result) 173 | } 174 | 175 | pub fn send_data( 176 | &mut self, 177 | connection_type: ConnectionType, 178 | address: &str, 179 | port: u16, 180 | data: &str 181 | ) -> transmission_return_type!(()) 182 | { 183 | // Send a start connection message 184 | let tcp_start_result = self.start_tcp_connection(connection_type, address, port); 185 | TransmissionError::try_step(TransmissionStep::Connect, tcp_start_result)?; 186 | 187 | TransmissionError::try_step(TransmissionStep::Send, self.transmit_data(data))?; 188 | 189 | TransmissionError::try_step(TransmissionStep::Close, self.close_connection()) 190 | } 191 | 192 | pub fn close_connection(&mut self) -> return_type!(()) { 193 | self.send_at_command("+CIPCLOSE")?; 194 | self.wait_for_ok(DEFAULT_TIMEOUT.into()) 195 | } 196 | 197 | /** 198 | Turns off the device by setting chip_enable to 0 199 | */ 200 | pub fn power_down(&mut self) -> return_type!(()) { 201 | self.chip_enable_pin.set_low().map_err(Error::PinError) 202 | } 203 | 204 | /** 205 | Resets the device by setting chip_enable to 0 and then back to 1 206 | */ 207 | pub fn reset(&mut self) -> return_type!(()) { 208 | self.power_down()?; 209 | self.timer.start(Millisecond(10)); 210 | block!(self.timer.wait()).unwrap(); 211 | self.power_up() 212 | } 213 | 214 | /** 215 | Turns the device back on by setting chip_enable to high 216 | */ 217 | pub fn power_up(&mut self) -> return_type!(()) { 218 | self.chip_enable_pin.set_high().map_err(Error::PinError)?; 219 | 220 | // The esp01 sends a bunch of garbage over the serial port before starting properly, 221 | // therefore we need to retry this until we get valid data or time out 222 | let mut error_count = 0; 223 | loop { 224 | match self.wait_for_got_ip(STARTUP_TIMEOUT.into()) { 225 | Ok(()) => break, 226 | e @ Err(Error::RxError(serial::Error::TimedOut)) => return e, 227 | e => { 228 | if error_count < 255 { 229 | error_count += 1; 230 | continue 231 | } 232 | else { 233 | return e 234 | } 235 | } 236 | } 237 | } 238 | 239 | // Turn off echo on the device and wait for it to process that command 240 | self.send_at_command("E0")?; 241 | self.wait_for_ok(DEFAULT_TIMEOUT.into())?; 242 | 243 | Ok(()) 244 | } 245 | 246 | pub fn pull_some_current(&mut self) -> return_type!(()) { 247 | self.chip_enable_pin.set_high().map_err(Error::PinError)?; 248 | 249 | self.timer.start(Millisecond(500)); 250 | block!(self.timer.wait()).unwrap(); 251 | self.chip_enable_pin.set_low().map_err(Error::PinError) 252 | } 253 | 254 | fn transmit_data(&mut self, data: &str) -> return_type!(()) { 255 | self.start_transmission(data.len())?; 256 | self.wait_for_prompt(DEFAULT_TIMEOUT.into())?; 257 | self.send_raw(data.as_bytes())?; 258 | self.wait_for_ok(DEFAULT_TIMEOUT.into()) 259 | } 260 | 261 | fn start_tcp_connection ( 262 | &mut self, 263 | connection_type: ConnectionType, 264 | address: &str, 265 | port: u16 266 | ) -> return_type!(()) 267 | { 268 | // Length of biggest u16: 269 | const PORT_STRING_LENGTH: usize = 5; 270 | let mut port_str = ArrayString::<[_;PORT_STRING_LENGTH]>::new(); 271 | // write!(&mut port_str, "{}", port)?; 272 | itoa::fmt(&mut port_str, port)?; 273 | 274 | self.send_raw("AT+CIPSTART=\"".as_bytes())?; 275 | self.send_raw(connection_type.as_str().as_bytes())?; 276 | self.send_raw("\",\"".as_bytes())?; 277 | self.send_raw(address.as_bytes())?; 278 | self.send_raw("\",".as_bytes())?; 279 | self.send_raw(port_str.as_bytes())?; 280 | self.send_raw("\r\n".as_bytes())?; 281 | self.wait_for_ok(DEFAULT_TIMEOUT.into()) 282 | } 283 | 284 | fn start_transmission(&mut self, message_length: usize) -> return_type!(()) { 285 | // You can only send 2048 bytes per packet 286 | assert!(message_length < 2048); 287 | let mut length_buffer = ArrayString::<[_; 4]>::new(); 288 | // write!(&mut length_buffer, "{}", message_length)?; 289 | itoa::fmt(&mut length_buffer, message_length)?; 290 | 291 | self.send_raw(b"AT+CIPSEND=")?; 292 | self.send_raw(length_buffer.as_bytes())?; 293 | self.send_raw(b"\r\n")?; 294 | Ok(()) 295 | } 296 | 297 | /** 298 | Sends the "AT${command}" to the device 299 | */ 300 | fn send_at_command(&mut self, command: &str) -> return_type!(()) { 301 | self.send_raw(b"AT")?; 302 | self.send_raw(command.as_bytes())?; 303 | self.send_raw(b"\r\n")?; 304 | Ok(()) 305 | } 306 | 307 | fn wait_for_at_response( 308 | &mut self, 309 | expected_response: &ATResponse, 310 | timeout: Millisecond 311 | ) -> return_type!(()) { 312 | let mut buffer = [0; AT_RESPONSE_BUFFER_SIZE]; 313 | let response = serial::read_until_message( 314 | &mut self.rx, 315 | &mut self.timer, 316 | timeout, 317 | &mut buffer, 318 | &parse_at_response 319 | ); 320 | 321 | match response { 322 | Ok(ref resp) if resp == expected_response => { 323 | Ok(()) 324 | }, 325 | Ok(other) => { 326 | Err(Error::UnexpectedResponse(other)) 327 | } 328 | Err(e) => { 329 | Err(Error::RxError(e)) 330 | } 331 | } 332 | } 333 | 334 | fn wait_for_ok(&mut self, timeout: Millisecond) -> return_type!(()) { 335 | self.wait_for_at_response(&ATResponse::Ok, timeout) 336 | } 337 | fn wait_for_got_ip(&mut self, timeout: Millisecond) -> return_type!(()) { 338 | self.wait_for_at_response(&ATResponse::WiFiGotIp, timeout) 339 | } 340 | 341 | fn wait_for_prompt(&mut self, timeout: Millisecond) -> return_type!(()) { 342 | let mut buffer = [0; 1]; 343 | let result = serial::read_until_message( 344 | &mut self.rx, 345 | &mut self.timer, 346 | timeout, 347 | &mut buffer, 348 | &|buf, _ptr| { 349 | if buf[0] == '>' as u8 { 350 | Some(()) 351 | } 352 | else { 353 | None 354 | } 355 | } 356 | ); 357 | match result { 358 | Ok(_) => Ok(()), 359 | Err(e) => Err(Error::RxError(e)) 360 | } 361 | } 362 | 363 | fn send_raw(&mut self, bytes: &[u8]) -> return_type!(()) { 364 | match serial::write_all(&mut self.tx, bytes) { 365 | Ok(_) => Ok(()), 366 | Err(e) => Err(Error::TxError(e)) 367 | } 368 | } 369 | } 370 | 371 | /** 372 | Parses `buffer` as an AT command response returning the type if it 373 | is a valid AT response and `None` otherwise 374 | */ 375 | pub fn parse_at_response(buffer: &[u8], offset: usize) -> Option { 376 | if compare_circular_buffer(buffer, offset, "OK\r\n".as_bytes()) { 377 | Some(ATResponse::Ok) 378 | } 379 | else if compare_circular_buffer(buffer, offset, "ERROR\r\n".as_bytes()) { 380 | Some(ATResponse::Error) 381 | } 382 | else if compare_circular_buffer(buffer, offset, "busy p...\r\n".as_bytes()) { 383 | Some(ATResponse::Busy) 384 | } 385 | else if compare_circular_buffer(buffer, offset, "WIFI GOT IP\r\n".as_bytes()) { 386 | Some(ATResponse::WiFiGotIp) 387 | } 388 | else { 389 | None 390 | } 391 | } 392 | 393 | /** 394 | Compares the content of a circular buffer with another buffer. The comparison 395 | is done 'from the back' and if one buffer is longer than the other, only the 396 | content of the shared bytes is compared. 397 | 398 | `offset` is the index of the first byte of the circular buffer 399 | ``` 400 | [4,5,0,1,2,3] 401 | ^- offset 402 | ``` 403 | */ 404 | pub fn compare_circular_buffer( 405 | circular_buffer: &[u8], 406 | offset: usize, 407 | comparison: &[u8] 408 | ) -> bool 409 | { 410 | let comparison_length = min(circular_buffer.len(), comparison.len()); 411 | for i in 0..comparison_length { 412 | // Addition of circular_buffer.len() because % is remainder, not mathematical modulo 413 | // https://stackoverflow.com/questions/31210357/is-there-a-modulus-not-remainder-function-operation/31210691 414 | let circular_index = (circular_buffer.len() + offset - 1 - i) % circular_buffer.len(); 415 | let comparison_index = comparison.len() - 1 - i; 416 | if circular_buffer[circular_index] != comparison[comparison_index] { 417 | return false; 418 | } 419 | } 420 | true 421 | } 422 | 423 | -------------------------------------------------------------------------------- /src/serial.rs: -------------------------------------------------------------------------------- 1 | use embedded_hal as hal; 2 | use nb::{self, block}; 3 | 4 | use crate::timing::{Millisecond, LongTimer}; 5 | 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | /// Serial interface error 10 | Serial(E), 11 | /// Timeout before receiving reply 12 | TimedOut, 13 | } 14 | 15 | pub fn read_with_timeout( 16 | serial: &mut S, 17 | timer: &mut T, 18 | timeout: Millisecond, 19 | ) -> Result> 20 | where 21 | T: LongTimer, 22 | S: hal::serial::Read, 23 | { 24 | timer.start(timeout); 25 | loop { 26 | match serial.read() { 27 | // raise error 28 | Err(nb::Error::Other(e)) => return Err(Error::Serial(e)), 29 | Err(nb::Error::WouldBlock) => { 30 | // no data available yet, check the timer below 31 | }, 32 | Ok(byte) => return Ok(byte), 33 | } 34 | 35 | match timer.wait() { 36 | Err(nb::Error::Other(_e)) => { 37 | // The error type specified by `timer.wait()` is `!`, which 38 | // means no error can actually occur. The Rust compiler 39 | // still forces us to provide this match arm, though. 40 | unreachable!("Error was !, something has gone horribly wrong") 41 | }, 42 | // no timeout yet, try again 43 | Err(nb::Error::WouldBlock) => continue, 44 | Ok(()) => { 45 | return Err(Error::TimedOut); 46 | } 47 | } 48 | } 49 | } 50 | 51 | /** 52 | */ 53 | pub fn read_until_message( 54 | rx: &mut S, 55 | timer: &mut T, 56 | timeout: Millisecond, 57 | buffer: &mut [u8], 58 | parser: &C 59 | ) -> Result> 60 | where 61 | T: LongTimer, 62 | S: hal::serial::Read, 63 | C: Fn(&[u8], usize) -> Option, 64 | { 65 | let mut ptr = 0; 66 | loop { 67 | match read_with_timeout(rx, timer, timeout) { 68 | Ok(byte) => { 69 | buffer[ptr] = byte; 70 | ptr = (ptr+1) % buffer.len(); 71 | 72 | if let Some(val) = parser(buffer, ptr) { 73 | return Ok(val); 74 | } 75 | }, 76 | Err(Error::TimedOut) => { 77 | // If the remote end has already sent bytes and has now 78 | // stopped, we assume the transmission has ended 79 | return Err(Error::TimedOut); 80 | }, 81 | Err(e) => { 82 | return Err(e) 83 | } 84 | }; 85 | } 86 | } 87 | 88 | pub fn write_all(serial: &mut S, buffer: &[u8]) -> Result<(), S::Error> 89 | where 90 | S: hal::serial::Write 91 | { 92 | for &byte in buffer { 93 | block!(serial.write(byte))?; 94 | } 95 | 96 | Ok(()) 97 | } 98 | 99 | -------------------------------------------------------------------------------- /src/timing.rs: -------------------------------------------------------------------------------- 1 | use nb; 2 | use core::convert::Infallible; 3 | 4 | /** 5 | A countdown timer which nonblockingly waits until the specified countdown 6 | is completed. The countdown is started by calling `start`. 7 | 8 | This trait is needed because `embedded_hal` currently does not have a standardised 9 | measure of time. 10 | 11 | The implementation of this trait will depend on your HAL implementation, but 12 | here is a sample impl for the stm32f1xx_hal 13 | 14 | 15 | ```rust 16 | struct LongTimer { 17 | timer: Timer, 18 | milliseconds_remaining: u32, 19 | } 20 | 21 | impl LongTimer 22 | where Timer: CountDown