//! Helper utility to configure a specific modem context. use core::net::IpAddr; use core::str::FromStr; use at_commands::builder::CommandBuilder; use at_commands::parser::CommandParser; use embassy_time::{Duration, Timer}; use heapless::Vec; /// Provides a higher level API for controlling a given context. pub struct Control<'a> { control: crate::Control<'a>, cid: u8, } /// Configuration for a given context pub struct Config<'a> { /// Desired APN address. pub apn: &'a [u8], /// Desired authentication protocol. pub auth_prot: AuthProt, /// Credentials. pub auth: Option<(&'a [u8], &'a [u8])>, /// SIM pin pub pin: Option<&'a [u8]>, } /// Authentication protocol. #[derive(Clone, Copy, PartialEq, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum AuthProt { /// No authentication. None = 0, /// PAP authentication. Pap = 1, /// CHAP authentication. Chap = 2, } /// Error returned by control. #[derive(Clone, Copy, PartialEq, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum Error { /// Not enough space for command. BufferTooSmall, /// Error parsing response from modem. AtParseError, /// Error parsing IP addresses. AddrParseError, } impl From for Error { fn from(_: at_commands::parser::ParseError) -> Self { Self::AtParseError } } /// Status of a given context. #[derive(PartialEq, Debug)] pub struct Status { /// Attached to APN or not. pub attached: bool, /// IP if assigned. pub ip: Option, /// Gateway if assigned. pub gateway: Option, /// DNS servers if assigned. pub dns: Vec, } #[cfg(feature = "defmt")] impl defmt::Format for Status { fn format(&self, f: defmt::Formatter<'_>) { defmt::write!(f, "attached: {}", self.attached); if let Some(ip) = &self.ip { defmt::write!(f, ", ip: {}", defmt::Debug2Format(&ip)); } } } impl<'a> Control<'a> { /// Create a new instance of a control handle for a given context. /// /// Will wait for the modem to be initialized if not. pub async fn new(control: crate::Control<'a>, cid: u8) -> Self { control.wait_init().await; Self { control, cid } } /// Perform a raw AT command pub async fn at_command(&self, req: &[u8], resp: &mut [u8]) -> usize { self.control.at_command(req, resp).await } /// Configures the modem with the provided config. /// /// NOTE: This will disconnect the modem from any current APN and should not /// be called if the configuration has not been changed. /// /// After configuring, invoke [`enable()`] to activate the configuration. pub async fn configure(&self, config: &Config<'_>) -> Result<(), Error> { let mut cmd: [u8; 256] = [0; 256]; let mut buf: [u8; 256] = [0; 256]; let op = CommandBuilder::create_set(&mut cmd, true) .named("+CFUN") .with_int_parameter(0) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; let op = CommandBuilder::create_set(&mut cmd, true) .named("+CGDCONT") .with_int_parameter(self.cid) .with_string_parameter("IP") .with_string_parameter(config.apn) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; // info!("RES1: {}", unsafe { core::str::from_utf8_unchecked(&buf[..n]) }); CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; let mut op = CommandBuilder::create_set(&mut cmd, true) .named("+CGAUTH") .with_int_parameter(self.cid) .with_int_parameter(config.auth_prot as u8); if let Some((username, password)) = config.auth { op = op.with_string_parameter(username).with_string_parameter(password); } let op = op.finish().map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; // info!("RES2: {}", unsafe { core::str::from_utf8_unchecked(&buf[..n]) }); CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; if let Some(pin) = config.pin { let op = CommandBuilder::create_set(&mut cmd, true) .named("+CPIN") .with_string_parameter(pin) .finish() .map_err(|_| Error::BufferTooSmall)?; let _ = self.control.at_command(op, &mut buf).await; // Ignore ERROR which means no pin required } Ok(()) } /// Attach to the PDN pub async fn attach(&self) -> Result<(), Error> { let mut cmd: [u8; 256] = [0; 256]; let mut buf: [u8; 256] = [0; 256]; let op = CommandBuilder::create_set(&mut cmd, true) .named("+CGATT") .with_int_parameter(1) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; Ok(()) } /// Read current connectivity status for modem. pub async fn detach(&self) -> Result<(), Error> { let mut cmd: [u8; 256] = [0; 256]; let mut buf: [u8; 256] = [0; 256]; let op = CommandBuilder::create_set(&mut cmd, true) .named("+CGATT") .with_int_parameter(0) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; Ok(()) } async fn attached(&self) -> Result { let mut cmd: [u8; 256] = [0; 256]; let mut buf: [u8; 256] = [0; 256]; let op = CommandBuilder::create_query(&mut cmd, true) .named("+CGATT") .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; let (res,) = CommandParser::parse(&buf[..n]) .expect_identifier(b"+CGATT: ") .expect_int_parameter() .expect_identifier(b"\r\nOK") .finish()?; Ok(res == 1) } /// Read current connectivity status for modem. pub async fn status(&self) -> Result { let mut cmd: [u8; 256] = [0; 256]; let mut buf: [u8; 256] = [0; 256]; let op = CommandBuilder::create_query(&mut cmd, true) .named("+CGATT") .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; let (res,) = CommandParser::parse(&buf[..n]) .expect_identifier(b"+CGATT: ") .expect_int_parameter() .expect_identifier(b"\r\nOK") .finish()?; let attached = res == 1; if !attached { return Ok(Status { attached, ip: None, gateway: None, dns: Vec::new(), }); } let op = CommandBuilder::create_set(&mut cmd, true) .named("+CGPADDR") .with_int_parameter(self.cid) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; let (_, ip1, _ip2) = CommandParser::parse(&buf[..n]) .expect_identifier(b"+CGPADDR: ") .expect_int_parameter() .expect_optional_string_parameter() .expect_optional_string_parameter() .expect_identifier(b"\r\nOK") .finish()?; let ip = if let Some(ip) = ip1 { let ip = IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?; Some(ip) } else { None }; let op = CommandBuilder::create_set(&mut cmd, true) .named("+CGCONTRDP") .with_int_parameter(self.cid) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; let (_cid, _bid, _apn, _mask, gateway, dns1, dns2, _, _, _, _, _mtu) = CommandParser::parse(&buf[..n]) .expect_identifier(b"+CGCONTRDP: ") .expect_int_parameter() .expect_optional_int_parameter() .expect_optional_string_parameter() .expect_optional_string_parameter() .expect_optional_string_parameter() .expect_optional_string_parameter() .expect_optional_string_parameter() .expect_optional_int_parameter() .expect_optional_int_parameter() .expect_optional_int_parameter() .expect_optional_int_parameter() .expect_optional_int_parameter() .expect_identifier(b"\r\nOK") .finish()?; let gateway = if let Some(ip) = gateway { if ip.is_empty() { None } else { Some(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?) } } else { None }; let mut dns = Vec::new(); if let Some(ip) = dns1 { dns.push(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?) .unwrap(); } if let Some(ip) = dns2 { dns.push(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?) .unwrap(); } Ok(Status { attached, ip, gateway, dns, }) } async fn wait_attached(&self) -> Result { while !self.attached().await? { Timer::after(Duration::from_secs(1)).await; } let status = self.status().await?; Ok(status) } /// Disable modem pub async fn disable(&self) -> Result<(), Error> { let mut cmd: [u8; 256] = [0; 256]; let mut buf: [u8; 256] = [0; 256]; let op = CommandBuilder::create_set(&mut cmd, true) .named("+CFUN") .with_int_parameter(0) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; Ok(()) } /// Enable modem pub async fn enable(&self) -> Result<(), Error> { let mut cmd: [u8; 256] = [0; 256]; let mut buf: [u8; 256] = [0; 256]; let op = CommandBuilder::create_set(&mut cmd, true) .named("+CFUN") .with_int_parameter(1) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; // Make modem survive PDN detaches let op = CommandBuilder::create_set(&mut cmd, true) .named("%XPDNCFG") .with_int_parameter(1) .finish() .map_err(|_| Error::BufferTooSmall)?; let n = self.control.at_command(op, &mut buf).await; CommandParser::parse(&buf[..n]).expect_identifier(b"OK").finish()?; Ok(()) } /// Run a control loop for this context, ensuring that reaattach is handled. pub async fn run(&self, reattach: F) -> Result<(), Error> { self.enable().await?; let status = self.wait_attached().await?; let mut fd = self.control.open_raw_socket().await; reattach(&status); loop { if !self.attached().await? { trace!("detached"); self.control.close_raw_socket(fd).await; let status = self.wait_attached().await?; trace!("attached"); fd = self.control.open_raw_socket().await; reattach(&status); } Timer::after(Duration::from_secs(10)).await; } } }