From 792eef13ba65c17b52aa091be84ab054a33f4a32 Mon Sep 17 00:00:00 2001 From: Vladan Popovic Date: Sun, 19 Jun 2022 00:33:32 +0200 Subject: [PATCH] implement non-blocking read with timeout much better than before :) --- src/command.rs | 172 +++++++++++++++++++++++----------- src/config.rs | 1 - src/main.rs | 29 ++++-- src/modem.rs | 249 +++++++++++++++++++++++++++---------------------- 4 files changed, 276 insertions(+), 175 deletions(-) diff --git a/src/command.rs b/src/command.rs index 0a64681..bd97e9d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -7,15 +7,7 @@ pub struct Command { } impl Command { - pub fn initgprs() -> Command { - Command { - text: "AT+SAPBR=3,1,\"Contype\",\"GPRS\"".to_string(), - timeout: Duration::from_millis(3000), - contains: Some("OK".to_string()), - } - } - - pub fn modeminfo() -> Command { + pub fn modem_info() -> Command { Command { text: "ATI".to_string(), timeout: Duration::from_millis(6000), @@ -23,7 +15,7 @@ impl Command { } } - pub fn fwrevision() -> Command { + pub fn fw_revision() -> Command { Command { text: "AT+CGMR".to_string(), timeout: Duration::from_millis(3000), @@ -63,7 +55,7 @@ impl Command { } } - pub fn checkreg() -> Command { + pub fn check_reg() -> Command { Command { text: "AT+CREG?".to_string(), timeout: Duration::from_millis(3000), @@ -71,7 +63,15 @@ impl Command { } } - pub fn opengprs() -> Command { + pub fn gprs_init() -> Command { + Command { + text: "AT+SAPBR=3,1,\"Contype\",\"GPRS\"".to_string(), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn gprs_open() -> Command { Command { text: "AT+SAPBR=1,1".to_string(), timeout: Duration::from_millis(3000), @@ -79,6 +79,30 @@ impl Command { } } + pub fn gprs_set_apn(apn: &str) -> Command { + Command { + text: format!("AT+SAPBR=3,1,\"APN\",\"{}\"", apn), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn gprs_set_user(user: &str) -> Command { + Command { + text: format!("AT+SAPBR=3,1,\"USER\",\"{}\"", user), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn gprs_set_pwd(password: &str) -> Command { + Command { + text: format!("AT+SAPBR=3,1,\"PWD\",\"{}\"", password), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + pub fn getbear() -> Command { Command { text: "AT+SAPBR=2,1".to_string(), @@ -87,7 +111,23 @@ impl Command { } } - pub fn inithttp() -> Command { + pub fn get_local_ip_addr() -> Command { + Command { + text: "AT+CIFSR".to_string(), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn ping(domain: &str) -> Command { + Command { + text: format!("AT+CIPPING=\"{}\"", domain), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn http_init() -> Command { Command { text: "AT+HTTPINIT".to_string(), timeout: Duration::from_millis(3000), @@ -95,7 +135,7 @@ impl Command { } } - pub fn sethttp() -> Command { + pub fn http_set() -> Command { Command { text: "AT+HTTPPARA=\"CID\",1".to_string(), timeout: Duration::from_millis(3000), @@ -103,15 +143,7 @@ impl Command { } } - pub fn checkssl() -> Command { - Command { - text: "AT+CIPSSL=?".to_string(), - timeout: Duration::from_millis(3000), - contains: Some("OK".to_string()), - } - } - - pub fn enablessl() -> Command { + pub fn http_enable_ssl() -> Command { Command { text: "AT+HTTPSSL=1".to_string(), timeout: Duration::from_millis(3000), @@ -119,7 +151,7 @@ impl Command { } } - pub fn disablessl() -> Command { + pub fn http_disable_ssl() -> Command { Command { text: "AT+HTTPSSL=0".to_string(), timeout: Duration::from_millis(3000), @@ -127,7 +159,7 @@ impl Command { } } - pub fn initurl() -> Command { + pub fn http_init_url() -> Command { Command { text: "AT+HTTPPARA=\"URL\",\"{}\"".to_string(), timeout: Duration::from_millis(3000), @@ -135,7 +167,7 @@ impl Command { } } - pub fn doget() -> Command { + pub fn http_get() -> Command { Command { text: "AT+HTTPACTION=0".to_string(), timeout: Duration::from_millis(3000), @@ -143,7 +175,7 @@ impl Command { } } - pub fn setcontent() -> Command { + pub fn http_set_content() -> Command { Command { text: "AT+HTTPPARA=\"CONTENT\",\"{}\"".to_string(), timeout: Duration::from_millis(3000), @@ -151,7 +183,7 @@ impl Command { } } - pub fn postlen() -> Command { + pub fn http_post_len() -> Command { Command { text: "AT+HTTPDATA={}5000".to_string(), timeout: Duration::from_millis(3000), @@ -159,7 +191,7 @@ impl Command { } } - pub fn dopost() -> Command { + pub fn http_post() -> Command { Command { text: "AT+HTTPACTION=1".to_string(), timeout: Duration::from_millis(3000), @@ -167,7 +199,7 @@ impl Command { } } - pub fn getdata() -> Command { + pub fn http_get_data() -> Command { Command { text: "AT+HTTPREAD".to_string(), timeout: Duration::from_millis(3000), @@ -191,30 +223,6 @@ impl Command { } } - pub fn setapn(apn: &str) -> Command { - Command { - text: format!("AT+SAPBR=3,1,\"APN\",\"{}\"", apn), - timeout: Duration::from_millis(3000), - contains: Some("OK".to_string()), - } - } - - pub fn setuser(user: &str) -> Command { - Command { - text: format!("AT+SAPBR=3,1,\"USER\",\"{}\"", user), - timeout: Duration::from_millis(3000), - contains: Some("OK".to_string()), - } - } - - pub fn setpwd(password: &str) -> Command { - Command { - text: format!("AT+SAPBR=3,1,\"PWD\",\"{}\"", password), - timeout: Duration::from_millis(3000), - contains: Some("OK".to_string()), - } - } - pub fn probe() -> Command { Command { text: "AT".to_string(), @@ -230,4 +238,60 @@ impl Command { contains: Some("OK".to_string()), } } + + pub fn tcp_ssl_disable() -> Command { + Command { + text: "AT+CIPSSL=0".to_string(), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn tcp_ssl_enable() -> Command { + Command { + text: "AT+CIPSSL=1".to_string(), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn tcp_ssl_check() -> Command { + Command { + text: "AT+CIPSSL=?".to_string(), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn tcp_connect(addr: &str, port: u16) -> Command { + Command { + text: format!("AT+CIPSTART=\"TCP\",\"{}\",\"{}\"", addr, port), + timeout: Duration::from_millis(5000), + contains: Some("CONNECT OK".to_string()), + } + } + + pub fn tcp_send(size: usize) -> Command { + Command { + text: format!("AT+CIPSEND={}", size), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } + + pub fn tcp_write(payload: &str) -> Command { + Command { + text: payload.to_string(), + timeout: Duration::from_millis(2000), + contains: Some("> ".to_string()), + } + } + + pub fn tcp_set_quick_mode(quick_mode: bool) -> Command { + Command { + text: format!("AT+CIPQSEND={}", quick_mode as u8), + timeout: Duration::from_millis(3000), + contains: Some("OK".to_string()), + } + } } diff --git a/src/config.rs b/src/config.rs index 8b73836..62f368f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,4 +9,3 @@ pub const A1_GPRS_AP: GprsAp = GprsAp { username: "internet", password: "internet", }; - diff --git a/src/main.rs b/src/main.rs index 7393a33..8159e4b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,6 @@ use anyhow; use esp_idf_hal::prelude::*; use esp_idf_hal::peripherals::Peripherals; use esp_idf_hal::serial; -use std::time::Duration; -use std::thread; - -use std::sync::mpsc::{channel, Receiver, Sender}; fn main() -> anyhow::Result<()> { esp_idf_sys::link_patches(); @@ -36,23 +32,36 @@ fn main() -> anyhow::Result<()> { serial::config::Config::default().baudrate(Hertz(115200)), ).expect("Failed to create serial ... :("); + let (tx, rx) = serial.split(); + let mut mdm = modem::Modem::new(tx, rx); + // Unwrap all of these, without them we can't do anything anyways. let modem_pwrkey = dp.pins.gpio4.into_output().unwrap(); let modem_rst = dp.pins.gpio5.into_output().unwrap(); let modem_power = dp.pins.gpio23.into_output().unwrap(); - modem::init(modem_pwrkey, modem_rst, modem_power)?; - let (tx, rx) = serial.split(); - let mut mdm = modem::Modem::new(tx, rx); + mdm.init(modem_pwrkey, modem_rst, modem_power)?; - let _ = mdm.probe()?; let _ = mdm.connect_to_gprs_ap( config::A1_GPRS_AP.apn, config::A1_GPRS_AP.username, config::A1_GPRS_AP.password, )?; - if mdm.is_gprs_attached()? { - println!("GPRS ATTACHEEEDDDDDDDDD!!!!!!!!!!!!!!!1"); + + loop { + if mdm.is_gprs_attached()? { + if mdm.tcp_is_ssl_enabled()? { + mdm.tcp_ssl_disable()?; + } + let _ = mdm.get_ip_addr()?; + + println!("connecting to server!"); + let _ = mdm.tcp_set_quick_mode(true); + let _ = mdm.tcp_connect("51.158.66.64", 9988)?; + let _ = mdm.tcp_send("aaaaa")?; + break; + } + println!("!!!!!!!!!!!!!!!! GPRS NOT ATTACHED !!!!!!!!!!!!!!!!"); } Ok(()) diff --git a/src/modem.rs b/src/modem.rs index 65f2db2..357deca 100644 --- a/src/modem.rs +++ b/src/modem.rs @@ -1,19 +1,17 @@ use crate::command::Command; use std::thread; -use std::io::{Error as IoError, ErrorKind, BufRead}; use std::error::Error; use std::time::{Duration, Instant}; use embedded_hal::serial::{Read, Write}; use embedded_hal::digital::v2::OutputPin; -use esp_idf_hal::serial::{self, Rx, Tx, SerialError}; +use esp_idf_hal::serial::{self, Rx, Tx}; pub type Result = std::result::Result; pub struct Modem { - is_connected: bool, - rx: MyRx, + rx: Rx, tx: Tx, } @@ -21,6 +19,7 @@ pub struct Modem { pub enum ModemError { CommandError(String), SetupError(String), + ReadError, TimeoutError, } @@ -32,161 +31,191 @@ impl std::fmt::Display for ModemError { } } -struct MyRx(Rx); - -impl MyRx { - fn read(&mut self) -> nb::Result { - self.0.read() - } - - fn read_blocking(&mut self) -> std::result::Result { - nb::block!(self.read()) - } -} - -impl std::io::Read for MyRx { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let mut read_bytes: usize = 0; - loop { - let b = self.read_blocking() - .map_err(|err| IoError::new(ErrorKind::Other, format!("Serial error {:?}", err)))?; - buf[read_bytes] = b; - read_bytes += 1; - if b == '\0' as u8 || b == '\n' as u8 { - break; - } - }; - Ok(read_bytes) - } -} - impl Modem { pub fn new(tx: Tx, rx: Rx) -> Self { Self { - is_connected: false, - rx: MyRx(rx), + rx, tx, } } - /// Reads the serial RX until it has bytes, or until a timeout is reached. The timeout is - /// provided on input via the `timeout` argument. The first argument `expected` is the expected - /// end of the buffer. If it's `None`, the whole response is returned as is. If it's - /// `Some(expected_end)`, then the end of the response is matched against `expected_end`. If - /// they match, great! The response is returned from the function, but if not, then a - /// [ModemError::CommandError](crate::modem::ModemError::CommandError) is returned. + /// Initialize the modem (sim800l in this case). The initialization process sets all pins in the + /// required state so that the modem is turned on, then resets it a couple of times (beats me) and + /// sleeps for 3 seconds, which is enough for the modem to come online. /// - /// It's ok to use this function like this for now because the write/read cycle is blocking, - /// hence the case where multiple writes happen asynchronously isn't handled. See - /// [send_command](crate::modem::Modem::send_command) for more info on the blocking part. - fn read_response(&mut self, contains: Option, timeout: Duration) -> Result { - let mut response = String::new(); - let now = Instant::now(); - let match_text: String = contains.unwrap_or("".to_string()); + /// Below is an example for sim800l pins on a LilyGo TTGO T-Call. + /// + /// # Examples + /// + /// ``` + /// let modem_pwrkey = dp.pins.gpio4.into_output().unwrap(); + /// let modem_rst = dp.pins.gpio5.into_output().unwrap(); + /// let modem_power = dp.pins.gpio23.into_output().unwrap(); + /// + /// modem::init(modem_pwrkey, modem_rst, modem_power); + /// ``` + pub fn init(&mut self, mut pwrkey: impl OutputPin, mut rst: impl OutputPin, mut power: impl OutputPin) -> Result<()> { + println!("Turning SIM800L on ..."); + power.set_high().map_err(|_| ModemError::SetupError("Error setting POWER to high.".to_string()))?; + rst.set_high().map_err(|_| ModemError::SetupError("Error setting RST to high.".to_string()))?; + // Pull down PWRKEY for more than 1 second according to manual requirements + pwrkey.set_high().map_err(|_| ModemError::SetupError("Error setting PWRKEY to high.".to_string()))?; + thread::sleep(Duration::from_millis(100)); + pwrkey.set_low().map_err(|_| ModemError::SetupError("Error setting PWRKEY to low.".to_string()))?; + thread::sleep(Duration::from_millis(1000)); + pwrkey.set_high().map_err(|_| ModemError::SetupError("Error setting PWRKEY to high.".to_string()))?; + println!("Waiting 3s for sim module to come online ..."); + thread::sleep(Duration::from_millis(3000)); + Ok(()) + } - let mut reader = std::io::BufReader::new(&mut self.rx); + /// Reads a whole line (that ends with \\n) within the given `timeout` passed on input. + /// `nb` returns Ok(byte), or one of Err(WouldBlock) and Err(Other) which isn't of anyone's + /// interest, so the retry mechanism is triggered on _any_ error every 200ms until a byte is + /// received, or the timeout is reached. + /// + fn read_line(&mut self, timeout: Duration) -> Result { + let mut buf = String::new(); + let start = Instant::now(); loop { - let mut line = String::new(); - reader.read_line(&mut line) - .map_err(|err| ModemError::CommandError(format!("read line failed: {}", err)))?; - println!("Read {} from serial ...", line.trim()); + match self.rx.read() { + Ok(b) => { + print!("{}, ", b); + buf.push(b as char); + if b == '\n' as u8 { + return Ok(buf); + } + }, + _ => { + if Instant::now() > start + timeout { + return Err(ModemError::ReadError); + } + else { + thread::sleep(Duration::from_millis(200)); + } + }, + } + } + } - // check if empty first because it's much simpler and more frequent - if line == "" { - if now + timeout > Instant::now() { - return Err(ModemError::TimeoutError); - } - thread::sleep(Duration::from_millis(1000)); - continue; - } else { + /// Reads the serial RX until a \\n char is encoutered, or a timeout is reached. The timeout is + /// provided on input via the `timeout` argument. The first argument `contains` is checked + /// against a line in the response, if it's there the reading stops. + /// + /// If `contains` is `None`, the first line only is returned in the response. If it's + /// `Some(match_txt)`, then the end of the response is matched against `match_txt`. + fn read_response(&mut self, contains: Option, timeout: Duration) -> Result { + let mut response = String::new(); + let start = Instant::now(); + let match_text: String = contains.unwrap_or("".to_string()); + + loop { + if let Ok(line) = self.read_line(start + timeout - Instant::now()) { response.push_str(&line); - if line.contains(&match_text) { + if line.contains("ERROR") || line.contains(&match_text) { println!("Found match {} for line {} ... exiting response reader now!", match_text, line); println!("-----------------------------------------------------------"); break; } + } else { + if Instant::now() > start + timeout { + return Err(ModemError::TimeoutError); + } + println!("got into retry loop in read_response"); + thread::sleep(Duration::from_millis(200)); } } Ok(response.to_string()) } - fn send_command(&mut self, cmd: Command) -> Result { - for b in cmd.text.as_bytes().iter() { - nb::block!(self.tx.write(*b)).map_err(|_| ModemError::CommandError("error writing to serial".to_string()))?; + fn send(&mut self, b: u8) -> Result<()> { + nb::block!(self.tx.write(b)) + .map_err(|_| ModemError::CommandError(format!("error writing {} to serial", b)))?; + Ok(()) + } + + fn send_bytes(&mut self, payload: &[u8]) -> Result<()> { + for b in payload.iter() { + self.send(*b)?; } - nb::block!(self.tx.write('\r' as u8)).map_err(|_| ModemError::CommandError("error writing to serial".to_string()))?; + self.send('\r' as u8)?; + Ok(()) + } + + fn send_command(&mut self, cmd: Command) -> Result { + println!("-----------------------------------------------------------"); + println!("Sending {} ...", cmd.text); + let _ = self.send_bytes(cmd.text.as_bytes())?; self.read_response(cmd.contains.clone(), cmd.timeout.clone()) } pub fn get_ip_addr(&mut self) -> Result { - println!("getting ip addres ..."); - let res = self.send_command(Command::getbear())?; - println!("{}", res); - Ok(res) + self.send_command(Command::getbear()) + } + + pub fn get_local_ip_addr(&mut self) -> Result<()> { + let _ = self.send_command(Command::get_local_ip_addr())?; + Ok(()) } pub fn connect_to_gprs_ap(&mut self, apn: &str, username: &str, password: &str)-> Result<()> { println!("init gprs ..."); - let _ = self.send_command(Command::initgprs())?; + let _ = self.send_command(Command::gprs_init())?; - println!("connecting to {} with {}:{}", apn, username, password); + println!("setting up gprs credentials for apn {}, {}:{})", apn, username, password); - let _ = self.send_command(Command::setapn(apn))?; - let _ = self.send_command(Command::setuser(username))?; - let _ = self.send_command(Command::setpwd(password))?; + let _ = self.send_command(Command::gprs_set_apn(apn))?; + let _ = self.send_command(Command::gprs_set_user(username))?; + let _ = self.send_command(Command::gprs_set_pwd(password))?; println!("open gprs ..."); - let _ = self.send_command(Command::initgprs())?; + let _ = self.send_command(Command::gprs_open())?; - self.is_connected = true; Ok(()) } pub fn info(&mut self)-> Result { - println!("getting modem info with API command"); - self.send_command(Command::modeminfo()) + self.send_command(Command::modem_info()) } pub fn probe(&mut self)-> Result { - println!("probing modem with AP command"); self.send_command(Command::probe()) } pub fn is_gprs_attached(&mut self)-> Result { - println!("testing gprs connection ..."); let res = self.send_command(Command::is_gprs_attached())?; - Ok(res.contains("+CGATT: 1")) } -} -/// Initialize the modem (sim800l in this case). The initialization process sets all pins in the -/// required state so that the modem is turned on, then resets it a couple of times (beats me) and -/// sleeps for 3 seconds, which is enough for the modem to come online. -/// -/// Below is an example for sim800l pins on a LilyGo TTGO T-Call. -/// -/// # Examples -/// -/// ``` -/// let modem_pwrkey = dp.pins.gpio4.into_output().unwrap(); -/// let modem_rst = dp.pins.gpio5.into_output().unwrap(); -/// let modem_power = dp.pins.gpio23.into_output().unwrap(); -/// -/// modem::init(modem_pwrkey, modem_rst, modem_power); -/// ``` -pub fn init(mut pwrkey: impl OutputPin, mut rst: impl OutputPin, mut power: impl OutputPin) -> Result<()> { - println!("Turning SIM800L on ..."); - power.set_high().map_err(|_| ModemError::SetupError("Error setting POWER to high.".to_string()))?; - rst.set_high().map_err(|_| ModemError::SetupError("Error setting RST to high.".to_string()))?; - // Pull down PWRKEY for more than 1 second according to manual requirements - pwrkey.set_high().map_err(|_| ModemError::SetupError("Error setting PWRKEY to high.".to_string()))?; - thread::sleep(Duration::from_millis(100)); - pwrkey.set_low().map_err(|_| ModemError::SetupError("Error setting PWRKEY to low.".to_string()))?; - thread::sleep(Duration::from_millis(1000)); - pwrkey.set_high().map_err(|_| ModemError::SetupError("Error setting PWRKEY to high.".to_string()))?; - println!("Waiting 3s for sim module to come online ..."); - thread::sleep(Duration::from_millis(3000)); - Ok(()) + pub fn ping(&mut self, domain: &str)-> Result<()> { + self.send_command(Command::ping(domain))?; + Ok(()) + } + + pub fn tcp_is_ssl_enabled(&mut self) -> Result { + let res = self.send_command(Command::tcp_ssl_check())?; + Ok(res.contains("+CIPSSL: (1)")) + } + + pub fn tcp_ssl_disable(&mut self) -> Result<()> { + let _ = self.send_command(Command::tcp_ssl_disable())?; + Ok(()) + } + + pub fn tcp_connect(&mut self, addr: &str, port: u16) -> Result<()> { + self.send_command(Command::tcp_connect(addr, port))?; + Ok(()) + } + + pub fn tcp_set_quick_mode(&mut self, mode: bool) -> Result<()> { + self.send_command(Command::tcp_set_quick_mode(mode))?; + Ok(()) + } + + pub fn tcp_send(&mut self, payload: &str) -> Result<()> { + self.send_command(Command::tcp_send(payload.len()))?; + self.send_command(Command::tcp_write(payload))?; + Ok(()) + } }