quectel_bg77/
atst_flows.rs

1// Copyright Open Logistics Foundation
2//
3// Licensed under the Open Logistics Foundation License 1.3.
4// For details on the licensing terms, see the LICENSE file.
5// SPDX-License-Identifier: OLFL-1.3
6
7use core::fmt::Write;
8use core::net;
9use core::time::Duration;
10use embedded_timers::{clock::Clock, timer::Timer};
11use enumflags2::{bitflags, BitFlags};
12use heapless::String;
13
14use crate::atst_commands;
15use crate::error::Bg77Error;
16use crate::types::Protocol;
17use crate::util::{wait, wait_ms};
18use crate::Bg77Hal;
19
20/// heapless version of macro format!(...), requires a type annotation so the size of the
21/// String can be determined.
22/// let text: String<64> = hl_format!("{} {}", 1, "heapless::String").expect("Could not format heapless::String");
23macro_rules! hl_format {
24    ($dst:expr, $($arg : tt) *) => {{
25        let mut formatted = String::new();
26        match write!(formatted, $dst, $($arg)*) {
27            Ok(_) => Ok(formatted),
28            Err(_) => Err(Bg77Error::BufferOverflow),
29        }
30    }}
31}
32
33/// Configures which radio technologies should be used and how they should be prioritized and which
34/// LTE radio bands should be used
35#[derive(Copy, Clone, Debug, PartialEq, Eq)]
36pub enum RadioConfig {
37    /// Uses both radio technologies with the given radio bands without specifying which one to
38    /// prefer
39    Both {
40        emtc_bands: BitFlags<EmtcBand>,
41        nbiot_bands: BitFlags<NbiotBand>,
42    },
43    /// Only tries e-MTC with the given radio bands
44    OnlyEmtc { bands: BitFlags<EmtcBand> },
45    /// Only tries NB-IoT with the given radio bands
46    OnlyNbiot { bands: BitFlags<NbiotBand> },
47    /// Uses both radio technologies with the given radio bands and prefers the given technology
48    Prioritized {
49        preference: RadioTechnology,
50        emtc_bands: BitFlags<EmtcBand>,
51        nbiot_bands: BitFlags<NbiotBand>,
52    },
53}
54
55/// An LPWAN radio technology, eMTC or NB-IoT
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57#[allow(non_camel_case_types)]
58pub enum RadioTechnology {
59    NB_IoT,
60    eMTC,
61}
62
63/// An e-MTC radio band
64///
65/// Can be used as a [`BitFlags<EmtcBand>`] from the [`enumflags2`] crate to specify a list of
66/// enabled radio bands, e.g. `let bands = make_bitflags!(EmtcBand::{B1 | B2 | B8});` or
67/// `let bands = EmtcBand::all();`
68// See below for implementation notes
69#[bitflags]
70#[repr(u64)]
71#[derive(Copy, Clone, Debug, PartialEq, Eq)]
72pub enum EmtcBand {
73    B1 = 0x1,
74    B2 = 0x2,
75    B3 = 0x4,
76    B4 = 0x8,
77    B5 = 0x10,
78    B8 = 0x80,
79    B12 = 0x800,
80    B13 = 0x1000,
81    B18 = 0x20000,
82    B19 = 0x40000,
83    B20 = 0x80000,
84    B25 = 0x1000000,
85    B26 = 0x2000000, // no nbiot
86    B27 = 0x4000000, // no nbiot
87    B28 = 0x8000000,
88    B66 = 0x200000000,      //= 0x20000000000000000,
89    B85 = 0x10000000000000, //= 0x1000000000000000000000,
90}
91
92/// An NB-IoT radio band
93///
94/// Can be used as a [`BitFlags<NbiotBand>`] from the [`enumflags2`] crate to specify a list of
95/// enabled radio bands, e.g. `let bands = make_bitflags!(NbiotBand::{B1 | B2 | B8});` or
96/// `let bands = NbiotBand::all();`
97// See below for implementation notes
98#[bitflags]
99#[repr(u64)]
100#[derive(Copy, Clone, Debug, PartialEq, Eq)]
101pub enum NbiotBand {
102    B1 = 0x1,
103    B2 = 0x2,
104    B3 = 0x4,
105    B4 = 0x8,
106    B5 = 0x10,
107    B8 = 0x80,
108    B12 = 0x800,
109    B13 = 0x1000,
110    B18 = 0x20000,
111    B19 = 0x40000,
112    B20 = 0x80000,
113    B25 = 0x1000000,
114    B28 = 0x8000000,
115    B66 = 0x200000000,      //= 0x20000000000000000,
116    B71 = 0x4000000000,     //= 0x400000000000000000, // no eMTC
117    B85 = 0x10000000000000, //= 0x1000000000000000000000,
118}
119
120/// An LTE radio band
121///
122/// Can be used as a [`BitFlags<Band>`] from the [`enumflags2`] crate to specify a list of enabled
123/// radio bands, e.g. `let bands = make_bitflags!(Band::{B1 | B2 | B8});` or `let bands =
124/// Band::all();`
125// As of 2021-10-05, repr(u128) is an unstable feature, see tracking issue at
126// https://github.com/rust-lang/rust/issues/56071. Therefore, we have to considerably hack here :(
127#[bitflags]
128#[repr(u64)]
129#[derive(Copy, Clone, Debug, PartialEq)]
130enum Band {
131    B1 = 0x1,
132    B2 = 0x2,
133    B3 = 0x4,
134    B4 = 0x8,
135    B5 = 0x10,
136    B8 = 0x80,
137    B12 = 0x800,
138    B13 = 0x1000,
139    B14 = 0x2000, // no nbiot or emtc
140    B18 = 0x20000,
141    B19 = 0x40000,
142    B20 = 0x80000,
143    B25 = 0x1000000,
144    B26 = 0x2000000, // no nbiot
145    B27 = 0x4000000, // no nbiot
146    B28 = 0x8000000,
147    B31 = 0x40000000, // no nbiot or emtc
148    // bits 33 .. 64 are unused, so we shift B66 .. B85 down by 32 bits. When formatting, they have
149    // to be shifted up again.
150    B66 = 0x200000000,      //= 0x20000000000000000,
151    B71 = 0x4000000000,     //= 0x400000000000000000, // no emtc
152    B72 = 0x8000000000,     //= 0x800000000000000000, // no nbiot or emtc
153    B73 = 0x10000000000,    //= 0x1000000000000000000, // no nbiot or emtc
154    B85 = 0x10000000000000, //= 0x1000000000000000000000,
155}
156
157fn from_emtc(band: BitFlags<EmtcBand>) -> BitFlags<Band> {
158    BitFlags::from_bits(band.bits())
159        .expect("EmtcBand should only contain a subset of a generic Band")
160}
161fn from_nbiot(band: BitFlags<NbiotBand>) -> BitFlags<Band> {
162    BitFlags::from_bits(band.bits())
163        .expect("NbiotBand should only contain a subset of a generic Band")
164}
165
166/// Formats the given band info to be usable with the AT+QCFG="band" command
167fn format_bands(bands: BitFlags<Band>) -> String<22> {
168    let upper_bands_shifted = (bands.bits() & 0xFFFFFFFF00000000) as u128;
169    let upper_bands = upper_bands_shifted << 32;
170    let lower_bands = (bands.bits() & 0xFFFFFFFF) as u128;
171    let all_bands = upper_bands | lower_bands;
172    hl_format!("{:X}", all_bands).expect("Could not format radio bands")
173}
174
175/// Implements AT command "flows", i.e. a sequence of AT commands achieving a specific purpose
176///
177/// The methods should only be called if the modem is in the right state, i.e. this should be
178/// wrapped by a type taking care of the internal modem state.
179pub struct Bg77CmdFlows<'a, P0, P1, P2, TX, RX, UE, CLOCK>
180where
181    P0: embedded_hal::digital::OutputPin,
182    P1: embedded_hal::digital::OutputPin,
183    P2: embedded_hal::digital::OutputPin,
184    UE: core::fmt::Debug,
185    TX: embedded_hal_nb::serial::Write<u8, Error = UE>,
186    RX: embedded_hal_nb::serial::Read<u8, Error = UE>,
187    CLOCK: Clock,
188{
189    pin_enable: P0,
190    pin_reset_n: P1,
191    pin_pwrkey: P2,
192    timer: Timer<'a, CLOCK>,
193    at: atst_commands::AtHelper<'a, TX, RX, UE, CLOCK>,
194    radio_config: RadioConfig,
195    apn: Option<String<32>>,
196    operator: Option<String<8>>,
197    connection_timeout: Duration,
198    delay_before_attach: Duration,
199}
200
201impl<'a, P0, P1, P2, TX, RX, UE, CLOCK> Bg77CmdFlows<'a, P0, P1, P2, TX, RX, UE, CLOCK>
202where
203    P0: embedded_hal::digital::OutputPin,
204    P1: embedded_hal::digital::OutputPin,
205    P2: embedded_hal::digital::OutputPin,
206    UE: core::fmt::Debug,
207    TX: embedded_hal_nb::serial::Write<u8, Error = UE>,
208    RX: embedded_hal_nb::serial::Read<u8, Error = UE>,
209    CLOCK: Clock,
210{
211    /// Creates a new Bg77CmdFlows instance
212    ///
213    /// `delay_before_attach` is a `Duration` that will be waited before the `AT+COPS` command
214    /// is run to connect/attach to the network. This has been introduced because we have
215    /// experienced connection issues on a low-power device when doing this too fast. This delay
216    /// helped but could/should be avoided on devices where it is not necessary.
217    pub fn new(
218        hal: Bg77Hal<'a, P0, P1, P2, TX, RX, CLOCK>,
219        radio_config: RadioConfig,
220        apn: Option<&str>,
221        operator: Option<&str>,
222        connection_timeout: Duration,
223        delay_before_attach: Duration,
224        tx_rx_cooldown: Duration,
225    ) -> Self {
226        Bg77CmdFlows {
227            pin_enable: hal.pin_enable,
228            pin_reset_n: hal.pin_reset_n,
229            pin_pwrkey: hal.pin_pwrkey,
230            timer: Timer::new(hal.clock),
231            at: atst_commands::AtHelper::new(hal.tx, hal.rx, hal.clock, tx_rx_cooldown),
232            radio_config,
233            apn: apn.map(|a| a.into()),
234            operator: operator.map(|o| o.into()),
235            connection_timeout,
236            delay_before_attach,
237        }
238    }
239
240    #[cfg(feature = "direct-serial-access")]
241    pub fn serial(&mut self) -> (&mut TX, &mut RX) {
242        self.at.serial()
243    }
244
245    /// Power-off, wait, power-on, wait, reset (+pwrkey_n), wait, un-reset
246    pub fn power_on(&mut self) -> Result<(), Bg77Error> {
247        log::info!("Turn BG77 modem on");
248
249        // Here, we want to do a reset / power cycle to ensure that the
250        // modem boots up even if it was powered before.
251
252        #[cfg(feature = "bg77")]
253        {
254            // From Quectel_BG77_Hardware_Design_V1.0.pdf:
255            // "RESET_N is used to reset the module. Due to platform limitations,
256            // the chipset has integrated the reset function into PWRKEY, and
257            // RESET_N is connected directly to PWRKEY inside the module.
258            // The module can be reset by driving RESET_N low for 2-3.8 s."
259            //
260            // Due to these limitations, a reset using the RESET_N pin takes
261            // a huge amount of time. Thus, we rather power cycle the modem
262            // completely which is much faster.
263            self.pin_enable.set_low().map_err(|_| Bg77Error::Hardware)?;
264            wait_ms(&mut self.timer, 100);
265            self.pin_enable
266                .set_high()
267                .map_err(|_| Bg77Error::Hardware)?;
268            wait_ms(&mut self.timer, 100);
269            self.pin_reset_n
270                .set_low()
271                .map_err(|_| Bg77Error::Hardware)?;
272            self.pin_pwrkey.set_low().map_err(|_| Bg77Error::Hardware)?;
273            wait_ms(&mut self.timer, 550);
274            self.pin_reset_n
275                .set_high()
276                .map_err(|_| Bg77Error::Hardware)?;
277            self.pin_pwrkey
278                .set_high()
279                .map_err(|_| Bg77Error::Hardware)?;
280        }
281
282        #[cfg(feature = "bg770")]
283        {
284            // In our tests, VDD_EXT (which powered our uart level shifters) went low after a few
285            // hundred ms. We could workaround this with a subsequent modem reset.
286            self.pin_enable.set_low().map_err(|_| Bg77Error::Hardware)?;
287            wait_ms(&mut self.timer, 100);
288            self.pin_enable
289                .set_high()
290                .map_err(|_| Bg77Error::Hardware)?;
291            wait_ms(&mut self.timer, 500);
292
293            self.pin_reset_n.set_low().ok();
294            wait_ms(&mut self.timer, 100);
295            self.pin_reset_n.set_high().ok();
296            wait_ms(&mut self.timer, 100);
297
298            self.pin_pwrkey.set_low().ok();
299            wait_ms(&mut self.timer, 550);
300            self.pin_pwrkey.set_high().ok();
301            wait_ms(&mut self.timer, 100);
302        }
303
304        log::info!("BG77 modem turned on");
305        Ok(())
306    }
307
308    pub fn power_off(&mut self) {
309        let _ = self.pin_enable.set_low();
310    }
311
312    /// Configure the modem as requested and try to attach
313    pub fn connect_network(&mut self) -> Result<(), Bg77Error> {
314        log::info!("Connect to network");
315        self.uart_ready_at_command(10)?;
316
317        log::debug!("DisableSleep");
318        self.at.simple_command(b"AT+QSCLK=0\r")?;
319
320        log::debug!("EnableEcho");
321        self.at.simple_command(b"AT+CMEE=1\r")?;
322
323        log::debug!("Check SIM & GetPinState");
324        let res = self
325            .at
326            .command_with_response(b"AT+CPIN?\r", &[b"+CPIN: ", b"\r\n\r\nOK\r\n"])?;
327        if res[0] != b"READY" {
328            return Err(Bg77Error::PinRequired);
329        }
330        drop(res);
331
332        log::debug!("Turn modem functionality on");
333        #[cfg(feature = "bg77")]
334        self.at.simple_command(b"AT+CFUN=1,0\r")?; // fast on BG77
335        #[cfg(feature = "bg770")]
336        self.at
337            .command_with_timeout(b"AT+CFUN=1,0\r", Duration::from_secs(5))?; // slower on BG770
338
339        // Ensure that the modem does not auto-connect
340        log::debug!("SelectOperator (detach)");
341        self.at
342            .command_with_timeout(b"AT+COPS=2\r", Duration::from_secs(10))?;
343
344        let (iotopmode, scanseq, emtc_bands, nbiot_bands, cops_act);
345        match self.radio_config {
346            RadioConfig::Both {
347                emtc_bands: emtc_bnds,
348                nbiot_bands: nbiot_bnds,
349            } => {
350                iotopmode = 2;
351                scanseq = "00";
352                emtc_bands = format_bands(from_emtc(emtc_bnds));
353                nbiot_bands = format_bands(from_nbiot(nbiot_bnds));
354                cops_act = None;
355            }
356            RadioConfig::OnlyEmtc { bands } => {
357                iotopmode = 0;
358                // Prevent "error[E0658]: attributes on expressions are experimental" by { ... }
359                #[cfg(feature = "bg77")]
360                {
361                    scanseq = "02"; // ok on BG77
362                }
363                #[cfg(feature = "bg770")]
364                {
365                    scanseq = "0203"; // both required on BG770
366                }
367                emtc_bands = format_bands(from_emtc(bands));
368                nbiot_bands = String::from("0");
369                cops_act = Some(8);
370            }
371            RadioConfig::OnlyNbiot { bands } => {
372                iotopmode = 1;
373                #[cfg(feature = "bg77")]
374                {
375                    scanseq = "03"; // ok on BG77
376                }
377                #[cfg(feature = "bg770")]
378                {
379                    scanseq = "0302"; // both required on BG770
380                }
381                emtc_bands = String::from("0");
382                nbiot_bands = format_bands(from_nbiot(bands));
383                cops_act = Some(9);
384            }
385            RadioConfig::Prioritized {
386                preference: RadioTechnology::eMTC,
387                emtc_bands: emtc_bnds,
388                nbiot_bands: nbiot_bnds,
389            } => {
390                iotopmode = 2;
391                scanseq = "0203";
392                emtc_bands = format_bands(from_emtc(emtc_bnds));
393                nbiot_bands = format_bands(from_nbiot(nbiot_bnds));
394                cops_act = None;
395            }
396            RadioConfig::Prioritized {
397                preference: RadioTechnology::NB_IoT,
398                emtc_bands: emtc_bnds,
399                nbiot_bands: nbiot_bnds,
400            } => {
401                iotopmode = 2;
402                scanseq = "0302";
403                emtc_bands = format_bands(from_emtc(emtc_bnds));
404                nbiot_bands = format_bands(from_nbiot(nbiot_bnds));
405                cops_act = None;
406            }
407        }
408
409        // For the QT+QCFG=... commands: The last parameter is <effect>: 0 = after reboot, 1 = now
410
411        // Servicedomain 1 = PS only = only packet switched transmissions, no voice connection
412        log::debug!("Configure servicedomain");
413        #[cfg(feature = "bg77")]
414        self.at.simple_command(b"AT+QCFG=\"servicedomain\",1,1\r")?; // BG77
415        #[cfg(feature = "bg770")]
416        self.at.simple_command(b"AT+QCFG=\"servicedomain\",1\r")?; // effect parameter missing on BG770
417
418        // Iotopmode: 0 = eMTC, 1 = NB-IoT, 2 = both
419        log::debug!("Configure iotopmode");
420        let cmd: String<64> = hl_format!("AT+QCFG=\"iotopmode\",{},1\r", iotopmode)?;
421        #[cfg(feature = "bg77")]
422        self.at.simple_command(cmd.as_ref())?; // fast on BG77
423        #[cfg(feature = "bg770")]
424        self.at
425            .command_with_timeout(cmd.as_ref(), Duration::from_secs(6))?; // takes 4s on BG770
426
427        // Scansequence: 00 = automatic, 01 = GSM, 02 = eMTC, 03 = NB-IoT
428        // e.g. 0203 = eMTC first, NB-IoT second
429        log::debug!("Configure scansequence");
430        let cmd: String<64> = hl_format!("AT+QCFG=\"nwscanseq\",{},1\r", scanseq)?;
431        self.at.simple_command(cmd.as_ref())?;
432
433        // AT+QCFG="band", <gsm>, <eMTC>, <NB-IoT>, <effect>
434        // For each protocol: 0 = no change
435        // Otherwise, it is a hexadecimal bitmask with the following meanings. It has to be
436        // transmitted without the "0x" prepended, e.g. 'AT+QCFG="band",0,0,15' is
437        // 0x01 | 0x04 | 0x10 and chooses B1, B3, B5 for NB-IoT
438        //
439        // The following table is taken from the QCFG AT Commands Manuals for BG77 and BG770. It
440        // has been created by merging the eMTC and NB-IoT listings for both modems. The additional
441        // notes on the right designate if a specific band was only found in specific places.
442        // In general, band N is activated by setting bit N to 1.
443        //
444        // 0x1 (BAND_PREF_LTE_BAND1)                        LTE B1
445        // 0x2 (BAND_PREF_LTE_BAND2)                        LTE B2
446        // 0x4 (BAND_PREF_LTE_BAND3)                        LTE B3
447        // 0x8 (BAND_PREF_LTE_BAND4)                        LTE B4
448        // 0x10 (BAND_PREF_LTE_BAND5)                       LTE B5
449        // 0x80 (BAND_PREF_LTE_BAND8)                       LTE B8
450        // 0x800 (BAND_PREF_LTE_BAND12)                     LTE B12
451        // 0x1000 (BAND_PREF_LTE_BAND13)                    LTE B13
452        // 0x10000 (BAND_PREF_LTE_BAND17)                   LTE B17 <-- only BG770 NB-IoT?
453        // 0x20000 (BAND_PREF_LTE_BAND18)                   LTE B18
454        // 0x40000 (BAND_PREF_LTE_BAND19)                   LTE B19
455        // 0x80000 (BAND_PREF_LTE_BAND20)                   LTE B20
456        // 0x1000000 (BAND_PREF_LTE_BAND25)                 LTE B25
457        // 0x2000000 (BAND_PREF_LTE_BAND26)                 LTE B26 <-- not BG770 NB-IoT?
458        // 0x4000000 (BAND_PREF_LTE_BAND27)                 LTE B27 <-- not BG770 NB-IoT?
459        // 0x8000000 (BAND_PREF_LTE_BAND28)                 LTE B28
460        // 0x40000000 (BAND_PREF_LTE_BAND31)                LTE B31 <-- only BG77?
461        // 0x20000000000000000 (BAND_PREF_LTE_BAND66)       LTE B66
462        // 0x400000000000000000 (BAND_PREF_LTE_BAND71)      LTE B71 <-- only BG77 NB-IoT?
463        // 0x800000000000000000 (BAND_PREF_LTE_BAND72       LTE B72 <-- only BG77?
464        // 0x1000000000000000000 (BAND_PREF_LTE_BAND73)     LTE B73 <-- only BG77?
465        // 0x1000000000000000000000 (BAND_PREF_LTE_BAND85)  LTE B85 <-- only BG77?
466        log::debug!("Configure band");
467        let cmd: String<65> = hl_format!("AT+QCFG=\"band\",0,{},{},1\r", emtc_bands, nbiot_bands)?;
468        #[cfg(feature = "bg77")]
469        self.at.simple_command(cmd.as_ref())?; // fast on BG77
470        #[cfg(feature = "bg770")]
471        self.at
472            .command_with_timeout(cmd.as_ref(), Duration::from_secs(4))?; // takes up to 2s on BG770
473
474        // AT+CGDCONT=<context-id>,<PDP-type>,<APN>
475        // context-id: minimum 1, can be read with the test form of the command
476        // pdp-type: IP = IPv4, other options are PPP, IPV6, IPV4V6
477        // apn: "if the value is null or omitted, then the subscription value will be requested"
478        log::debug!("Set pdp context");
479        let cmd: String<64> = match &self.apn {
480            Some(apn) => hl_format!("AT+CGDCONT=1,\"IP\",\"{}\"\r", apn)?,
481            None => String::from("AT+CGDCONT=1,\"IP\"\r"),
482        };
483        self.at.simple_command(cmd.as_ref())?;
484
485        if self.delay_before_attach != Duration::new(0, 0) {
486            log::debug!("Wait before attach");
487            wait(&mut self.timer, self.delay_before_attach);
488        }
489
490        // AT+COPS=<mode>,[<format>,<operator>,[<access-technology>]]
491        // mode: 0 = auto, 1 = manual, 2 = deregister, 3 = only config, 4 = try manual, then auto
492        // format: 2 = numeric format = GSM location are id number, e.g. 26201 German Telekom
493        // access-technology: 0 = GSM, 8 = eMTC, 9 = NB-IoT
494        // examples: AT+COPS=0    AT+COPS=1,2,"26201"    AT+COPS=1,2,"26201",9
495        log::debug!("Select operator");
496        let cmd: String<64> = match (&self.operator, cops_act) {
497            (Some(operator), Some(act)) => hl_format!("AT+COPS=1,2,\"{}\",{}\r", operator, act)?,
498            (Some(operator), None) => hl_format!("AT+COPS=1,2,\"{}\"\r", operator)?,
499            (None, _) => String::from("AT+COPS=0\r"),
500        };
501        self.at
502            .command_with_timeout(cmd.as_ref(), self.connection_timeout)?;
503
504        log::debug!("Check attachment state");
505        // TODO Instead of looping timeout.as_secs() times, use a proper timeout for the whole
506        // flow. In the current setup, this would require another embedded_hal::timer::CountDown so
507        // it was not done in the beginning. Maybe, switch to a clock abstraction instead of
508        // CountDown?
509        for _ in 0..=self.connection_timeout.as_secs() {
510            let res = self
511                .at
512                .command_with_response(b"AT+CGATT?\r", &[b"+CGATT: ", b"\r\n\r\nOK\r\n"])?;
513            if res[0] == b"1" {
514                drop(res);
515                log::debug!("Disable data echo");
516                self.at.simple_command(b"AT+QISDE=0\r")?;
517
518                return Ok(());
519            }
520            wait_ms(&mut self.timer, 1000);
521        }
522
523        Err(Bg77Error::CgattFailure)
524    }
525
526    /// Tries `num_tries` times to communicate with the modem ("AT\r")
527    fn uart_ready_at_command(&mut self, num_tries: usize) -> Result<(), Bg77Error> {
528        for _ in 0..num_tries {
529            log::debug!("Modem ready? Sending AT!");
530            if self.at.simple_command(b"AT\r").is_ok() {
531                return Ok(());
532            }
533        }
534        Err(Bg77Error::AtCmdTimeout)
535    }
536
537    /// Detach from the network
538    pub fn disconnect_network(&mut self) -> Result<(), Bg77Error> {
539        log::info!("Disconnect from network / Power off");
540        log::debug!("SelectOperator (detach)");
541        self.at
542            .command_with_timeout(b"AT+COPS=2\r", Duration::from_secs(10))
543    }
544
545    pub fn open_socket(
546        &mut self,
547        socket_index: usize,
548        protocol: Protocol,
549        remote: net::SocketAddr,
550    ) -> Result<(), Bg77Error> {
551        let protocol = match protocol {
552            Protocol::Tcp => "TCP",
553            Protocol::Udp => "UDP",
554        };
555        let ip_address = match remote {
556            net::SocketAddr::V6(_) => {
557                log::warn!("IPv6 addresses are not supported (yet?)");
558                return Err(Bg77Error::IPv6Unsupported);
559            }
560            net::SocketAddr::V4(v4) => {
561                let ip_address: String<16> = hl_format!("{}", v4.ip())?;
562                ip_address
563            }
564        };
565        let local_port = 0; // = automatic
566        log::info!("Open {} socket", protocol);
567        let cmd: String<64> = hl_format!(
568            "AT+QIOPEN=1,{},\"{}\",\"{}\",{},{}\r",
569            socket_index,
570            protocol,
571            ip_address,
572            remote.port(),
573            local_port
574        )?;
575        // Bg77 TCP/IP Application Note:
576        // "3. It is suggested to wait for 150 seconds for “+QIOPEN: <connectID>,<err>” to be
577        // outputted after executing the Write Command. If the URC cannot be received in 150
578        // seconds, AT+QICLOSE should be used to close the socket."
579        if let Ok(res) = self.at.command(
580            cmd.as_ref(),
581            &[b"\r\nOK\r\n\r\n+QIOPEN: ", b",", b"\r\n"],
582            Duration::from_secs(10), // no, we are not that patient ...
583        ) {
584            if let Ok(Ok(socket_id)) = core::str::from_utf8(res[0]).map(str::parse::<usize>) {
585                if socket_id == socket_index && res[1] == b"0" {
586                    // socket_id , err
587                    return Ok(());
588                }
589            }
590        }
591        log::warn!("Could not open socket, calling 'close_socket' to clean up.");
592        let _ = self.close_socket(socket_index);
593        Err(Bg77Error::OpenSocketError)
594    }
595
596    pub fn close_socket(&mut self, socket_index: usize) -> Result<(), Bg77Error> {
597        log::info!("Closing socket");
598        #[cfg(feature = "bg77")]
599        let cmd: String<64> = hl_format!("AT+QICLOSE={},10\r", socket_index)?; // BG77
600        #[cfg(feature = "bg770")]
601        let cmd: String<64> = hl_format!("AT+QICLOSE={}\r", socket_index)?; // with timeout -> ERROR on BG770
602        self.at
603            .command_with_timeout(cmd.as_ref(), Duration::from_secs(11))
604    }
605
606    pub fn send_data(
607        &mut self,
608        socket_index: usize,
609        protocol: Protocol,
610        tx_data: &[u8],
611    ) -> Result<usize, Bg77Error> {
612        log::info!("Send data");
613        log::debug!("Announce data transmission");
614        // For TCP, we could automatically transmit the data in multiple segments. But the trait
615        // definition actually allows to just transmit part of the data so any user of an
616        // embedded_nal::TcpClientStack should be prepared to handle partial transmits. So if we
617        // did transmit multiple segments here, this should lead to code duplication, so we don't.
618        let txlen = tx_data.len();
619        let txlen = match (txlen > 1460, protocol) {
620            (true, Protocol::Udp) => return Err(Bg77Error::TxSize),
621            (true, Protocol::Tcp) => 1460,
622            (false, _) => txlen,
623        };
624        let cmd: String<64> = hl_format!("AT+QISEND={},{}\r", socket_index, txlen)?;
625        self.at.command_with_response(cmd.as_ref(), &[b"> "])?;
626        log::debug!("Transmit raw data");
627        self.at
628            .command_with_response(&tx_data[..txlen], &[b"\r\nSEND OK\r\n"])?;
629        Ok(txlen)
630    }
631
632    pub fn receive_data(
633        &mut self,
634        socket_index: usize,
635        buffer: &mut [u8],
636    ) -> nb::Result<usize, Bg77Error> {
637        log::info!("Read data");
638        log::debug!("Sending read data command.");
639        let max_data_length = core::cmp::min(1460, buffer.len());
640        let cmd: String<64> = hl_format!("AT+QIRD={},{}\r", socket_index, max_data_length)?;
641        let (res, parsed_len) = self.at.command_with_parsed_len(
642            cmd.as_ref(),
643            &[b"+QIRD: ", b"\r\n"],
644            Duration::from_secs(1),
645        )?;
646        let data_length_str = core::str::from_utf8(res[0]).map_err(|_| Bg77Error::ParseFailed)?;
647        let data_length: usize = data_length_str
648            .parse()
649            .map_err(|_| Bg77Error::ParseFailed)?;
650        drop(res);
651        if data_length > max_data_length {
652            log::error!("Modem responded with more data than we requested!");
653            return Err(nb::Error::Other(Bg77Error::RxSize));
654        }
655        if data_length == 0 {
656            Err(nb::Error::WouldBlock)
657        } else {
658            log::debug!("Read raw data");
659            self.at
660                .read_raw_data(&mut buffer[..data_length], 2500, parsed_len)?;
661            Ok(data_length)
662        }
663    }
664
665    pub fn dns_lookup(&mut self, hostname: &str) -> Result<net::IpAddr, Bg77Error> {
666        log::info!("DNS lookup");
667        let cmd: String<64> = hl_format!("AT+QIDNSGIP=1,\"{}\"\r", hostname)?;
668        let res = self.at.command(
669            cmd.as_ref(),
670            // OK\r\n\r\n+QIURC: "dnsgip",0(error),<ip_count>,<dns_ttl>\r\n+QIURC: "dnsgip","<ip_address>"
671            &[
672                b"\r\nOK\r\n\r\n+QIURC: \"dnsgip\",0,", // IP_count   -> res[0]
673                b",",                                   // DNS_ttl    -> res[1]
674                b"\r\n\r\n+QIURC: \"dnsgip\",\"",       // IP address -> res[2]
675                b"\"\r\n",
676            ],
677            Duration::from_secs(10),
678        )?;
679        let ip_str = core::str::from_utf8(res[2]).map_err(|_| Bg77Error::ParseIpAddressFailed)?;
680        let mut ip_addr_parts = [0; 4];
681        for (i, octet) in ip_str.split('.').enumerate() {
682            if i >= 4 {
683                return Err(Bg77Error::ParseIpAddressFailed);
684            }
685            ip_addr_parts[i] = octet.parse().map_err(|_| Bg77Error::ParseIpAddressFailed)?;
686        }
687        let ap = ip_addr_parts;
688        let ipv4 = net::Ipv4Addr::new(ap[0], ap[1], ap[2], ap[3]);
689        Ok(net::IpAddr::V4(ipv4))
690    }
691}
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696    use enumflags2::BitFlag;
697    #[test]
698    fn band_formatting() {
699        assert_eq!(&format_bands(Band::B1.into()), "1");
700        assert_eq!(&format_bands(Band::B8.into()), "80");
701        assert_eq!(&format_bands(Band::B1 | Band::B8), "81");
702        assert_eq!(&format_bands(Band::B1 | Band::B2), "3");
703        assert_eq!(&format_bands(Band::B28 | Band::B2), "8000002");
704        assert_eq!(&format_bands(Band::B66.into()), "20000000000000000");
705        assert_eq!(&format_bands(Band::B66 | Band::B1), "20000000000000001");
706        assert_eq!(&format_bands(Band::B72.into()), "800000000000000000");
707        assert_eq!(&format_bands(Band::B73.into()), "1000000000000000000");
708        assert_eq!(&format_bands(Band::B85.into()), "1000000000000000000000");
709        assert_eq!(
710            &format_bands(Band::B66 | Band::B71 | Band::B72 | Band::B73 | Band::B85),
711            "1001C20000000000000000"
712        );
713        assert_eq!(&format_bands(Band::all()), "1001C2000000004F0E389F");
714    }
715}