nrf_modem/embassy_net_modem/
context.rs

1//! Helper utility to configure a specific modem context.
2
3// Modified from embassy-rs:
4// Licence: https://github.com/embassy-rs/embassy/blob/main/LICENSE-APACHE
5// Source file: https://github.com/embassy-rs/embassy/blob/a8cb8a7fe1f594b765dee4cfc6ff3065842c7c6e/embassy-net-nrf91/src/context.rs
6
7use core::net::IpAddr;
8use core::str::FromStr;
9
10use at_commands::builder::CommandBuilder;
11use at_commands::parser::CommandParser;
12use embassy_time::{Duration, Timer};
13use heapless::Vec;
14
15use crate::embassy_net_modem::CAP_SIZE;
16
17/// Provides a higher level API for controlling a given context.
18pub struct Control<'a> {
19    control: super::Control<'a>,
20    cid: u8,
21}
22
23/// Configuration for a given context
24pub struct Config<'a> {
25    /// Desired APN address.
26    pub apn: &'a [u8],
27    /// Desired authentication protocol.
28    pub auth_prot: AuthProt,
29    /// Credentials.
30    pub auth: Option<(&'a [u8], &'a [u8])>,
31    /// SIM pin
32    pub pin: Option<&'a [u8]>,
33}
34
35/// Authentication protocol.
36#[derive(Clone, Copy, PartialEq, Debug)]
37#[cfg_attr(feature = "defmt", derive(defmt::Format))]
38#[repr(u8)]
39pub enum AuthProt {
40    /// No authentication.
41    None = 0,
42    /// PAP authentication.
43    Pap = 1,
44    /// CHAP authentication.
45    Chap = 2,
46}
47
48/// Error returned by control.
49#[derive(Clone, Copy, PartialEq, Debug)]
50#[cfg_attr(feature = "defmt", derive(defmt::Format))]
51pub enum Error {
52    /// Not enough space for command.
53    BufferTooSmall,
54    /// Error parsing response from modem.
55    AtParseError,
56    /// Error parsing IP addresses.
57    AddrParseError,
58}
59
60impl From<at_commands::parser::ParseError> for Error {
61    fn from(_: at_commands::parser::ParseError) -> Self {
62        Self::AtParseError
63    }
64}
65
66/// Status of a given context.
67#[derive(PartialEq, Debug)]
68pub struct Status {
69    /// Attached to APN or not.
70    pub attached: bool,
71    /// IP if assigned.
72    pub ip: Option<IpAddr>,
73    /// Gateway if assigned.
74    pub gateway: Option<IpAddr>,
75    /// DNS servers if assigned.
76    pub dns: Vec<IpAddr, 2>,
77}
78
79#[cfg(feature = "defmt")]
80impl defmt::Format for Status {
81    fn format(&self, f: defmt::Formatter<'_>) {
82        defmt::write!(f, "attached: {}", self.attached);
83        if let Some(ip) = &self.ip {
84            defmt::write!(f, ", ip: {}", defmt::Debug2Format(&ip));
85        }
86    }
87}
88
89impl<'a> Control<'a> {
90    /// Create a new instance of a control handle for a given context.
91    ///
92    /// Will wait for the modem to be initialized if not.
93    pub async fn new(control: super::Control<'a>, cid: u8) -> Self {
94        Self { control, cid }
95    }
96
97    /// Perform a raw AT command
98    pub async fn at_command(self, req: &[u8]) -> arrayvec::ArrayString<CAP_SIZE> {
99        self.control.at_command(req).await
100    }
101
102    /// Configures the modem with the provided config.
103    ///
104    /// NOTE: This will disconnect the modem from any current APN and should not
105    /// be called if the configuration has not been changed.
106    ///
107    /// After configuring, invoke [Self::enable] to activate the configuration.
108    pub async fn configure(&self, config: &Config<'_>) -> Result<(), Error> {
109        let mut cmd: [u8; 256] = [0; 256];
110
111        let op = CommandBuilder::create_set(&mut cmd, true)
112            .named("+CFUN")
113            .with_int_parameter(0)
114            .finish()
115            .map_err(|_| Error::BufferTooSmall)?;
116        let n = self.control.at_command(op).await;
117        CommandParser::parse(n.as_bytes())
118            .expect_identifier(b"OK")
119            .finish()?;
120
121        let op = CommandBuilder::create_set(&mut cmd, true)
122            .named("+CGDCONT")
123            .with_int_parameter(self.cid)
124            .with_string_parameter("IP")
125            .with_string_parameter(config.apn)
126            .finish()
127            .map_err(|_| Error::BufferTooSmall)?;
128        let n = self.control.at_command(op).await;
129        // info!("RES1: {}", unsafe { core::str::from_utf8_unchecked(&buf[..n]) });
130        CommandParser::parse(n.as_bytes())
131            .expect_identifier(b"OK")
132            .finish()?;
133
134        let mut op = CommandBuilder::create_set(&mut cmd, true)
135            .named("+CGAUTH")
136            .with_int_parameter(self.cid)
137            .with_int_parameter(config.auth_prot as u8);
138        if let Some((username, password)) = config.auth {
139            op = op
140                .with_string_parameter(username)
141                .with_string_parameter(password);
142        }
143        let op = op.finish().map_err(|_| Error::BufferTooSmall)?;
144
145        let n = self.control.at_command(op).await;
146        // info!("RES2: {}", unsafe { core::str::from_utf8_unchecked(&buf[..n]) });
147        CommandParser::parse(n.as_bytes())
148            .expect_identifier(b"OK")
149            .finish()?;
150
151        if let Some(pin) = config.pin {
152            let op = CommandBuilder::create_set(&mut cmd, true)
153                .named("+CPIN")
154                .with_string_parameter(pin)
155                .finish()
156                .map_err(|_| Error::BufferTooSmall)?;
157            let _ = self.control.at_command(op).await;
158            // Ignore ERROR which means no pin required
159        }
160
161        Ok(())
162    }
163
164    /// Attach to the PDN
165    pub async fn attach(&self) -> Result<(), Error> {
166        let mut cmd: [u8; 256] = [0; 256];
167        let op = CommandBuilder::create_set(&mut cmd, true)
168            .named("+CGATT")
169            .with_int_parameter(1)
170            .finish()
171            .map_err(|_| Error::BufferTooSmall)?;
172        let n = self.control.at_command(op).await;
173        CommandParser::parse(n.as_bytes())
174            .expect_identifier(b"OK")
175            .finish()?;
176        Ok(())
177    }
178
179    /// Read current connectivity status for modem.
180    pub async fn detach(&self) -> Result<(), Error> {
181        let mut cmd: [u8; 256] = [0; 256];
182        let op = CommandBuilder::create_set(&mut cmd, true)
183            .named("+CGATT")
184            .with_int_parameter(0)
185            .finish()
186            .map_err(|_| Error::BufferTooSmall)?;
187        let n = self.control.at_command(op).await;
188        CommandParser::parse(n.as_bytes())
189            .expect_identifier(b"OK")
190            .finish()?;
191        Ok(())
192    }
193
194    async fn attached(&self) -> Result<bool, Error> {
195        let mut cmd: [u8; 256] = [0; 256];
196
197        let op = CommandBuilder::create_query(&mut cmd, true)
198            .named("+CGATT")
199            .finish()
200            .map_err(|_| Error::BufferTooSmall)?;
201        let n = self.control.at_command(op).await;
202        let (res,) = CommandParser::parse(n.as_bytes())
203            .expect_identifier(b"+CGATT: ")
204            .expect_int_parameter()
205            .expect_identifier(b"\r\nOK")
206            .finish()?;
207        Ok(res == 1)
208    }
209
210    /// Read current connectivity status for modem.
211    pub async fn status(&self) -> Result<Status, Error> {
212        let mut cmd: [u8; 256] = [0; 256];
213
214        let op = CommandBuilder::create_query(&mut cmd, true)
215            .named("+CGATT")
216            .finish()
217            .map_err(|_| Error::BufferTooSmall)?;
218        let n = self.control.at_command(op).await;
219        let (res,) = CommandParser::parse(n.as_bytes())
220            .expect_identifier(b"+CGATT: ")
221            .expect_int_parameter()
222            .expect_identifier(b"\r\nOK")
223            .finish()?;
224        let attached = res == 1;
225        if !attached {
226            return Ok(Status {
227                attached,
228                ip: None,
229                gateway: None,
230                dns: Vec::new(),
231            });
232        }
233
234        let op = CommandBuilder::create_set(&mut cmd, true)
235            .named("+CGPADDR")
236            .with_int_parameter(self.cid)
237            .finish()
238            .map_err(|_| Error::BufferTooSmall)?;
239        let n = self.control.at_command(op).await;
240        let (_, ip1, _ip2) = CommandParser::parse(n.as_bytes())
241            .expect_identifier(b"+CGPADDR: ")
242            .expect_int_parameter()
243            .expect_optional_string_parameter()
244            .expect_optional_string_parameter()
245            .expect_identifier(b"\r\nOK")
246            .finish()?;
247
248        let ip = if let Some(ip) = ip1 {
249            let ip = IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?;
250            Some(ip)
251        } else {
252            None
253        };
254
255        let op = CommandBuilder::create_set(&mut cmd, true)
256            .named("+CGCONTRDP")
257            .with_int_parameter(self.cid)
258            .finish()
259            .map_err(|_| Error::BufferTooSmall)?;
260        let n = self.control.at_command(op).await;
261        let (_cid, _bid, _apn, _mask, gateway, dns1, dns2, _, _, _, _, _mtu) =
262            CommandParser::parse(n.as_bytes())
263                .expect_identifier(b"+CGCONTRDP: ")
264                .expect_int_parameter()
265                .expect_optional_int_parameter()
266                .expect_optional_string_parameter()
267                .expect_optional_string_parameter()
268                .expect_optional_string_parameter()
269                .expect_optional_string_parameter()
270                .expect_optional_string_parameter()
271                .expect_optional_int_parameter()
272                .expect_optional_int_parameter()
273                .expect_optional_int_parameter()
274                .expect_optional_int_parameter()
275                .expect_optional_int_parameter()
276                .expect_identifier(b"\r\nOK")
277                .finish()?;
278
279        let gateway = if let Some(ip) = gateway {
280            if ip.is_empty() {
281                None
282            } else {
283                Some(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?)
284            }
285        } else {
286            None
287        };
288
289        let mut dns = Vec::new();
290        if let Some(ip) = dns1 {
291            dns.push(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?)
292                .unwrap();
293        }
294
295        if let Some(ip) = dns2 {
296            dns.push(IpAddr::from_str(ip).map_err(|_| Error::AddrParseError)?)
297                .unwrap();
298        }
299
300        Ok(Status {
301            attached,
302            ip,
303            gateway,
304            dns,
305        })
306    }
307
308    async fn wait_attached(&self) -> Result<Status, Error> {
309        while !self.attached().await? {
310            Timer::after(Duration::from_secs(1)).await;
311        }
312        let status = self.status().await?;
313        Ok(status)
314    }
315
316    /// Disable modem
317    pub async fn disable(&self) -> Result<(), Error> {
318        let mut cmd: [u8; 256] = [0; 256];
319
320        let op = CommandBuilder::create_set(&mut cmd, true)
321            .named("+CFUN")
322            .with_int_parameter(0)
323            .finish()
324            .map_err(|_| Error::BufferTooSmall)?;
325        let n = self.control.at_command(op).await;
326        CommandParser::parse(n.as_bytes())
327            .expect_identifier(b"OK")
328            .finish()?;
329
330        Ok(())
331    }
332
333    /// Enable modem
334    pub async fn enable(&self) -> Result<(), Error> {
335        let mut cmd: [u8; 256] = [0; 256];
336
337        let op = CommandBuilder::create_set(&mut cmd, true)
338            .named("+CFUN")
339            .with_int_parameter(1)
340            .finish()
341            .map_err(|_| Error::BufferTooSmall)?;
342        let n = self.control.at_command(op).await;
343        CommandParser::parse(n.as_bytes())
344            .expect_identifier(b"OK")
345            .finish()?;
346
347        // Make modem survive PDN detaches
348        let op = CommandBuilder::create_set(&mut cmd, true)
349            .named("%XPDNCFG")
350            .with_int_parameter(1)
351            .finish()
352            .map_err(|_| Error::BufferTooSmall)?;
353        let n = self.control.at_command(op).await;
354        CommandParser::parse(n.as_bytes())
355            .expect_identifier(b"OK")
356            .finish()?;
357        Ok(())
358    }
359
360    /// Run a control loop for this context, ensuring that reaattach is handled.
361    pub async fn run<F: Fn(&Status)>(&self, reattach: F) -> Result<(), Error> {
362        self.enable().await?;
363        let status = self.wait_attached().await?;
364        self.control.open_raw_socket().await;
365        reattach(&status);
366
367        loop {
368            if !self.attached().await? {
369                self.control.close_raw_socket().await;
370                let status = self.wait_attached().await?;
371                self.control.open_raw_socket().await;
372                reattach(&status);
373            }
374            Timer::after(Duration::from_secs(10)).await;
375        }
376    }
377}