mirror of
https://github.com/embassy-rs/embassy.git
synced 2024-11-22 14:53:03 +00:00
363 lines
12 KiB
Rust
363 lines
12 KiB
Rust
//! 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<at_commands::parser::ParseError> 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<IpAddr>,
|
|
/// Gateway if assigned.
|
|
pub gateway: Option<IpAddr>,
|
|
/// DNS servers if assigned.
|
|
pub dns: Vec<IpAddr, 2>,
|
|
}
|
|
|
|
#[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<bool, Error> {
|
|
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<Status, Error> {
|
|
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<Status, Error> {
|
|
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<F: Fn(&Status)>(&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;
|
|
}
|
|
}
|
|
}
|