diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..440cf8b --- /dev/null +++ b/src/command.rs @@ -0,0 +1,219 @@ +#[allow(dead_code)] + +use std::time::Duration; + +pub struct Command { + pub text: String, + pub timeout: Duration, + pub ends_with: Option, +} + +impl Command { + pub fn initgprs() -> Command { + Command { + text: "AT+SAPBR=3,1,\"Contype\",\"GPRS\"".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn modeminfo() -> Command { + Command { + text: "ATI".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn fwrevision() -> Command { + Command { + text: "AT+CGMR".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn battery() -> Command { + Command { + text: "AT+CBC".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn scan() -> Command { + Command { + text: "AT+COPS=?".to_owned(), + timeout: Duration::from_millis(60), + ends_with: Some("OK".to_owned()), + } + } + + pub fn network() -> Command { + Command { + text: "AT+COPS?".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn signal() -> Command { + Command { + text: "AT+CSQ".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn checkreg() -> Command { + Command { + text: "AT+CREG?".to_owned(), + timeout: Duration::from_millis(3), + ends_with: None, + } + } + + pub fn opengprs() -> Command { + Command { + text: "AT+SAPBR=1,1".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn getbear() -> Command { + Command { + text: "AT+SAPBR=2,1".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn inithttp() -> Command { + Command { + text: "AT+HTTPINIT".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn sethttp() -> Command { + Command { + text: "AT+HTTPPARA=\"CID\",1".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn checkssl() -> Command { + Command { + text: "AT+CIPSSL=?".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn enablessl() -> Command { + Command { + text: "AT+HTTPSSL=1".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn disablessl() -> Command { + Command { + text: "AT+HTTPSSL=0".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn initurl() -> Command { + Command { + text: "AT+HTTPPARA=\"URL\",\"{}\"".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn doget() -> Command { + Command { + text: "AT+HTTPACTION=0".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("+HTTPACTION".to_owned()), + } + } + + pub fn setcontent() -> Command { + Command { + text: "AT+HTTPPARA=\"CONTENT\",\"{}\"".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn postlen() -> Command { + Command { + text: "AT+HTTPDATA={}5000".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("DOWNLOAD".to_owned()), + } + } + + pub fn dopost() -> Command { + Command { + text: "AT+HTTPACTION=1".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("+HTTPACTION".to_owned()), + } + } + + pub fn getdata() -> Command { + Command { + text: "AT+HTTPREAD".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn closehttp() -> Command { + Command { + text: "AT+HTTPTERM".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn closebear() -> Command { + Command { + text: "AT+SAPBR=0,1".to_owned(), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn setapn(apn: &str) -> Command { + Command { + text: format!("AT+SAPBR=3,1,\"APN\",\"{}\"", apn), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn setuser(user: &str) -> Command { + Command { + text: format!("AT+SAPBR=3,1,\"USER\",\"{}\"", user), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } + + pub fn setpwd(password: &str) -> Command { + Command { + text: format!("AT+SAPBR=3,1,\"PWD\",\"{}\"", password), + timeout: Duration::from_millis(3), + ends_with: Some("OK".to_owned()), + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..8b73836 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,12 @@ +pub struct GprsAp<'a> { + pub apn: &'a str, + pub username: &'a str, + pub password: &'a str, +} + +pub const A1_GPRS_AP: GprsAp = GprsAp { + apn: "internet", + username: "internet", + password: "internet", +}; + diff --git a/src/main.rs b/src/main.rs index 6f9055a..f00db07 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,59 +1,50 @@ +mod config; mod modem; +#[allow(dead_code)] +mod command; use esp_idf_hal::prelude::*; use esp_idf_hal::peripherals::Peripherals; use esp_idf_hal::serial; -fn start_loop(_m: &mut modem::Modem) -> () { - println!("Starting rx/tx loop (not implemented yet) ..."); -} - -struct Ap<'a> { - apn: &'a str, - username: &'a str, - password: &'a str, -} - -const A1: Ap = Ap { - apn: "internet", - username: "internet", - password: "internet", -}; - fn main() { esp_idf_sys::link_patches(); let dp = Peripherals::take().unwrap(); + // LilyGo TTGO T-Call sim800l board serial pins. + let serial_rx = dp.pins.gpio26; + let serial_tx = dp.pins.gpio27; + + let serial_pins = serial::Pins { + tx: serial_tx, + rx: serial_rx, + cts: None, + rts: None, + }; + + // Create the serial and panic with a message ... if we can't create the serial port, then we + // can't communicate with the sim800l module, hence we don't run anymore. + let serial: serial::Serial = serial::Serial::new( + dp.uart1, + serial_pins, + serial::config::Config::default().baudrate(Hertz(115200)), + ).expect("Failed to create serial ... :("); + + // 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).unwrap(); - let rx_pin = dp.pins.gpio26; - let tx_pin = dp.pins.gpio27; - - let serial_pins = serial::Pins { - tx: tx_pin, - rx: rx_pin, - cts: None, - rts: None, - }; - - println!("Setting up serial ..."); - let serial: serial::Serial = serial::Serial::new( - dp.uart1, - serial_pins, - serial::config::Config::default().baudrate(Hertz(9600)), - ).expect("Failed to create serial ... bailing"); - println!("Serial done ..."); - - println!("Writing to serial TX ..."); - let (tx, rx) = serial.split(); + let mut mdm = modem::Modem::new(tx, rx); - let mut modem = modem::Modem::new(tx, rx); - let _ = modem.connect_to_gprs_ap(A1.apn, A1.username, A1.password).unwrap(); - let _ = modem.test_modem().unwrap(); + let _ = mdm.modem_info().unwrap(); + let _ = mdm.connect_to_gprs_ap( + config::A1_GPRS_AP.apn, + config::A1_GPRS_AP.username, + config::A1_GPRS_AP.password, + ).unwrap(); } diff --git a/src/modem.rs b/src/modem.rs index aaadd35..d0f31f7 100644 --- a/src/modem.rs +++ b/src/modem.rs @@ -1,25 +1,13 @@ -use std::fmt::Write; -use std::thread; -use std::time::Duration; +use crate::command::Command; +use std::iter::FromIterator; +use std::thread; +use std::time::{Duration, Instant}; + +use embedded_hal::serial::{Read, Write}; use embedded_hal::digital::v2::OutputPin; -use embedded_hal::serial::Read; use esp_idf_hal::serial::{self, Rx, Tx}; -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_owned()))?; - rst.set_high().map_err(|_| ModemError::SetupError("Error setting RST to high.".to_owned()))?; - // 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_owned()))?; - thread::sleep(Duration::from_millis(100)); - pwrkey.set_low().map_err(|_| ModemError::SetupError("Error setting PWRKEY to low.".to_owned()))?; - thread::sleep(Duration::from_millis(1000)); - pwrkey.set_high().map_err(|_| ModemError::SetupError("Error setting PWRKEY to high.".to_owned()))?; - println!("Waiting 5s for sim module to come online ..."); - thread::sleep(Duration::from_millis(5000)); - Ok(()) -} pub struct Modem { is_connected: bool, @@ -29,8 +17,6 @@ pub struct Modem { #[derive(Debug)] pub enum ModemError { - GprsAPConnectionError(String), - ATCommandError(String), CommandError(String), SetupError(String), } @@ -52,59 +38,107 @@ impl Modem { } } - fn at_command(&mut self, cmd: &str) -> Result { - let mut msg = "AT+".to_owned(); - msg.push_str(cmd); - self.send_command(&msg) - .map_err(|err| ModemError::ATCommandError(format!("{}", err))) - } - - fn read_response(&mut self) -> Result { + /// 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. + /// + /// 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, expected: Option, timeout: Duration) -> Result { let len = self.rx.count() .map_err(|_| ModemError::CommandError("Error getting RX fifo length".to_owned()))?; - if cfg!(debug_assertions) { - println!("Reading {} bytes from serial RX ...", len); - } + println!("Reading {} bytes from serial RX ...", len); let mut response = String::new(); - for _ in 0..len { - nb::block!(self.rx.read()) - .map(|b| response.push(b as char)) - .map_err(|_| ModemError::CommandError("Error reading from RX".to_owned()))?; - } - response = response.trim().to_owned(); + let now = Instant::now(); - if cfg!(debug_assertions) { - println!("Received: {}", response); + loop { + let len = self.rx.count().unwrap(); + if len == 0 { + if now + timeout > Instant::now() { + break; + } + thread::sleep(Duration::from_secs(1)); + continue; + } + println!("Reading {} bytes from serial RX ...", len); + + for _ in 0..len { + nb::block!(self.rx.read()) + .map(|b| response.push(b as char)) + .map_err(|_| ModemError::CommandError("Error reading from RX".to_owned()))?; + } } - Ok(response) + let response = response.trim(); + + println!("Received: {}", response); + + expected.map(|expected_end| { + if response.ends_with(&expected_end) { + Ok(response.to_owned()) + } else { + let res = String::from_iter(response.chars().rev().take(expected_end.len())); + Err(ModemError::CommandError(format!("Got invalid response end {} instead of expected {}", res, expected_end))) + } + }).unwrap_or(Ok(response.to_owned())) } - fn send_command(&mut self, cmd: &str) -> Result { - writeln!(self.tx, "{}", cmd) - .map_err(|_| ModemError::CommandError("error writing to serial".to_owned()))?; - self.read_response() + 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_owned()))?; + } + self.read_response(cmd.ends_with, cmd.timeout) } - pub fn connect_to_gprs_ap(&mut self, apn: &str, username: &str, password: &str)-> Result { + pub fn connect_to_gprs_ap(&mut self, apn: &str, username: &str, password: &str)-> Result<()> { println!("connecting to {} with {}:{}", apn, username, password); - self.at_command("CGATTCGATT=1") - .map(|response| { - println!("connected to {} with response: {}", apn, response); - self.is_connected = true; - response - }) - .map_err(|err| ModemError::GprsAPConnectionError(format!("{}", err))) + + 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::opengprs())?; + + self.is_connected = true; + Ok(()) } - pub fn test_modem(&mut self)-> Result { + pub fn modem_info(&mut self)-> Result { println!("testing modem with AP command"); - self.at_command("AT") - .map(|response| { - println!("modem responded with: {}", response); - response - }) - .map_err(|err| ModemError::GprsAPConnectionError(format!("{}", err))) + self.send_command(Command::modeminfo()) } } + +/// 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 mut modem_pwrkey = dp.pins.gpio4.into_output().unwrap(); +/// let mut modem_rst = dp.pins.gpio5.into_output().unwrap(); +/// let mut 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_owned()))?; + rst.set_high().map_err(|_| ModemError::SetupError("Error setting RST to high.".to_owned()))?; + // 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_owned()))?; + thread::sleep(Duration::from_millis(100)); + pwrkey.set_low().map_err(|_| ModemError::SetupError("Error setting PWRKEY to low.".to_owned()))?; + thread::sleep(Duration::from_millis(1000)); + pwrkey.set_high().map_err(|_| ModemError::SetupError("Error setting PWRKEY to high.".to_owned()))?; + println!("Waiting 3s for sim module to come online ..."); + thread::sleep(Duration::from_millis(3000)); + Ok(()) +}