monarch2/
modem.rs

1use core::cell::RefCell;
2
3use atat::{AtatCmd, UrcChannel, UrcSubscription, asynch::AtatClient};
4use embassy_sync::{
5    blocking_mutex::{
6        Mutex,
7        raw::{CriticalSectionRawMutex, NoopRawMutex},
8    },
9    signal::Signal,
10};
11use heapless::String;
12use static_cell::StaticCell;
13
14use crate::{
15    Bool,
16    command::{
17        self, Urc,
18        device::{GetOperatingMode, SetOperatingMode, types::RAT},
19        mobile_equipment::{SetFunctionality, types::FunctionalMode},
20        mqtt::{self, types::MQTTStatusCode},
21        network::{PLMNSelection, types::NetworkRegistrationState},
22        pdp::DefinePDPContext,
23        system_features::{ConfigureCEREGReports, ConfigureCMEErrorReports},
24    },
25    error::Error,
26    nvm, ssl_tls,
27};
28#[cfg(feature = "gm02sp")]
29use crate::{
30    Reserved,
31    command::{
32        device::GetClock,
33        gnss::{
34            GetGnssAssitance, ProgramGnss, SetGnssConfig, UpdateGnssAssitance,
35            types::FixSensitivity, urc::GnssFixReady,
36        },
37    },
38};
39use embassy_time::{Duration, Timer, with_timeout};
40
41/// Represents the state of the modem.
42///
43/// The state is designed to be shared across multiple components of the modem stack,
44/// such as the URC (unsolicited result code) handler and any control interface.
45struct ModemState {
46    reg_state: Mutex<CriticalSectionRawMutex, RefCell<NetworkRegistrationState>>,
47    mqtt_connected: Signal<NoopRawMutex, mqtt::urc::Connected>,
48
49    #[cfg(feature = "gm02sp")]
50    fix_subscriber: Signal<NoopRawMutex, GnssFixReady>,
51}
52
53impl ModemState {
54    /// Creates a new `ModemState`.
55    const fn new() -> Self {
56        Self {
57            reg_state: Mutex::new(RefCell::new(NetworkRegistrationState::NotSearching)),
58            mqtt_connected: Signal::new(),
59            #[cfg(feature = "gm02sp")]
60            fix_subscriber: Signal::new(),
61        }
62    }
63}
64
65/// A handle to the modem, providing access to AT command operations and URC subscription handling.
66pub struct Modem<'a, AtCl, const N: usize, const L: usize> {
67    client: AtCl,
68    state: &'a ModemState,
69    urc_chan: &'a UrcChannel<Urc, N, L>,
70    initialized: bool,
71    #[cfg(feature = "gm02sp")]
72    update_almanac: bool,
73    #[cfg(feature = "gm02sp")]
74    update_ephemeris: bool,
75}
76
77/// Handles unsolicited result codes (URCs) received from the modem.
78///
79/// This handler is intended to run as a long-lived task that continuously polls for URC messages
80/// and processes them. It is typically launched by calling [`Modem::urc_handler`] followed by
81/// `.run().await`.
82pub struct UrcHandler<'a, const N: usize, const L: usize> {
83    urc_subscription: UrcSubscription<'a, Urc, N, L>,
84    state: &'a ModemState,
85}
86
87impl<'a, const N: usize, const L: usize> UrcHandler<'a, N, L> {
88    /// Runs the URC handler task indefinitely.
89    ///
90    /// This method should be spawned as a background task alongside other modem activities.
91    pub async fn run(&mut self) -> ! {
92        loop {
93            let msg = self.urc_subscription.next_message_pure().await;
94            match msg {
95                #[cfg(feature = "gm02sp")]
96                command::Urc::GnssFixReady(fix_ready) => {
97                    debug!("GNSS fix ready: {:?}", fix_ready);
98                    self.state.fix_subscriber.signal(fix_ready);
99                }
100                command::Urc::MqttConnected(connected) => {
101                    debug!("MQTT connected: {:?}", connected);
102                    self.state.mqtt_connected.signal(connected);
103                }
104                command::Urc::MqttDisconnected(disconnected) => {
105                    debug!("MQTT disconnected: {:?}", disconnected);
106                    // self.state.mqtt_connected.signal(connected);
107                }
108                command::Urc::MqttMessagePublished(published) => {
109                    debug!("MQTT message published: {:?}", published);
110                }
111                command::Urc::MqttMessageReceived(received) => {
112                    debug!("MQTT message received: {:?}", received);
113                }
114                command::Urc::MqttSubscribed(subscribed) => {
115                    debug!("MQTT subscribed: {:?}", subscribed);
116                }
117                command::Urc::MqttPromptToPublish(prompt) => {
118                    debug!("MQTT prompt to publish: {:?}", prompt);
119                }
120                command::Urc::Shutdown => {
121                    debug!("Device shutdown");
122                }
123                command::Urc::Start => {
124                    debug!("Device started");
125                }
126                command::Urc::CoapConnected(conn) => {
127                    debug!("COAP connected: {:?}", conn);
128                }
129                command::Urc::NetworkRegistrationStatus(status) => {
130                    debug!("Network registration status: {:?}", status);
131                    self.state.reg_state.lock(|v| {
132                        v.replace(status.stat);
133                    });
134                }
135            };
136        }
137    }
138}
139
140impl<'a, AtCl, const N: usize, const L: usize> Modem<'a, AtCl, N, L>
141where
142    AtCl: AtatClient,
143{
144    /// Constructs a new `Modem` instance with a client, URC channel, and shared state.
145    ///
146    /// # Arguments
147    ///
148    /// - `client`: An AT command client for communicating with the modem.
149    /// - `urc_chan`: A reference to the URC channel used to receive asynchronous modem messages.
150    ///
151    /// This method does not initialize the modem; call [`begin`](Self::begin) to do so.
152    pub fn new(client: AtCl, urc_chan: &'a UrcChannel<Urc, N, L>) -> Self {
153        static MODEM_STATE_CELL: StaticCell<ModemState> = StaticCell::new();
154        let modem_state: &'static ModemState = MODEM_STATE_CELL.init(ModemState::new());
155        Self {
156            client,
157            urc_chan,
158            state: modem_state,
159            initialized: false,
160            #[cfg(feature = "gm02sp")]
161            update_almanac: false,
162            #[cfg(feature = "gm02sp")]
163            update_ephemeris: false,
164        }
165    }
166
167    /// Creates a new URC handler associated with this modem.
168    ///
169    /// The URC handler will subscribe to unsolicited messages from the modem and process them,
170    /// updating shared state where necessary. The user must run the [`UrcHandler`](UrcHandler) to begin handling messages.
171    ///
172    /// # Panics
173    ///
174    /// Panics if the subscription to the URC channel fails (e.g., buffer full or uninitialized).
175    pub fn urc_handler(&self) -> UrcHandler<'a, N, L> {
176        UrcHandler {
177            urc_subscription: self.urc_chan.subscribe().unwrap(),
178            state: self.state,
179        }
180    }
181
182    pub async fn send<Cmd: AtatCmd>(&mut self, cmd: &Cmd) -> Result<Cmd::Response, Error> {
183        self.client.send(cmd).await.map_err(|e| e.into())
184    }
185
186    /// Initializes the modem by sending basic configuration commands.
187    ///
188    /// This method must be called once before other modem operations are invoked.
189    /// It is safe to call multiple times; subsequent calls will be no-ops.
190    ///
191    /// - Enables numeric CME error reporting.
192    /// - Enables network registration URC reporting.
193    pub async fn begin(&mut self) -> Result<(), Error> {
194        if self.initialized {
195            return Ok(());
196        }
197
198        self.send(&ConfigureCMEErrorReports {
199            typ: crate::command::system_features::types::CMEErrorReports::Numeric,
200        })
201        .await?;
202
203        self.send(&ConfigureCEREGReports {
204            typ: crate::command::system_features::types::CEREGReports::Enabled,
205        })
206        .await?;
207
208        self.initialized = true;
209
210        Ok(())
211    }
212
213    pub async fn get_operation_mode(&mut self) -> Result<RAT, Error> {
214        let res = self.send(&GetOperatingMode).await?;
215        Ok(res.rat)
216    }
217
218    pub async fn set_opeartion_mode(&mut self, mode: RAT) -> Result<(), Error> {
219        self.send(&SetOperatingMode { mode }).await?;
220        Ok(())
221    }
222
223    pub async fn ping(&mut self) -> Result<(), Error> {
224        self.send(&command::AT).await?;
225        Ok(())
226    }
227
228    pub async fn define_pdp_context(&mut self) -> Result<(), Error> {
229        self.send(&DefinePDPContext {
230            cid: 1,
231            pdp_type: command::pdp::types::PDPType::IP,
232            apn: String::try_from("").unwrap(),
233            pdp_addr: String::try_from("").unwrap(),
234            d_comp: command::pdp::types::PDPDComp::default(),
235            h_comp: command::pdp::types::PDPHComp::default(),
236            ipv4_alloc: command::pdp::types::PDPIPv4Alloc::NAS,
237            request_type: command::pdp::types::PDPRequestType::NewOrHandover,
238            pdp_pcscf_discovery_method: command::pdp::types::PDPPCSCF::Auto,
239            for_imcn: Bool::False,
240            nslpi: Bool::False,
241            secure_pco: Bool::False,
242            ipv4_mtu_discovery: Bool::False,
243            local_addr_ind: Bool::False,
244            non_ip_mtu_discovery: Bool::False,
245        })
246        .await?;
247        Ok(())
248    }
249
250    pub async fn set_op_state(&mut self, mode: FunctionalMode) -> Result<(), Error> {
251        self.send(&SetFunctionality {
252            fun: mode,
253            rst: None,
254        })
255        .await?;
256        Ok(())
257    }
258
259    pub fn get_network_registration_state(&self) -> NetworkRegistrationState {
260        self.state.reg_state.lock(|v| v.borrow().clone())
261    }
262}
263
264impl<'sub, AtCl, const N: usize, const L: usize> Modem<'sub, AtCl, N, L>
265where
266    AtCl: AtatClient,
267{
268    /// Connect to the LTE network.
269    ///
270    /// This function will connect the modem to the LTE network. This function will
271    /// block until the modem is attached.
272    pub async fn lte_connect(&mut self) -> Result<(), Error> {
273        self.set_op_state(FunctionalMode::Full).await?;
274
275        //  Set the network operator selection to automatic
276        self.send(&PLMNSelection {
277            mode: command::network::types::NetworkSelectionMode::Automatic,
278            ..Default::default()
279        })
280        .await?;
281
282        loop {
283            match self.get_network_registration_state() {
284                NetworkRegistrationState::RegisteredHome => break,
285                NetworkRegistrationState::RegisteredRoaming => break,
286                _ => {
287                    Timer::after(Duration::from_millis(1000)).await;
288                    // let signal = self.send(&GetSignalQuality).await?;
289                    // debug!("rssi: {:?}", signal);
290                }
291            }
292        }
293
294        Ok(())
295    }
296
297    /// Disconnect from the LTE network.
298    ///
299    /// This function will disconnect the modem from the LTE network and block until
300    /// the network is actually disconnected. After the network is disconnected the
301    /// GNSS subsystem can be used.
302    pub async fn lte_disconnect(&mut self) -> Result<(), Error> {
303        self.set_op_state(command::mobile_equipment::types::FunctionalMode::Minimum)
304            .await?;
305
306        while self.get_network_registration_state() != NetworkRegistrationState::NotSearching {
307            Timer::after(Duration::from_millis(100)).await;
308        }
309
310        Ok(())
311    }
312}
313
314#[cfg(feature = "gm02sp")]
315impl<'sub, AtCl, const N: usize, const L: usize> Modem<'sub, AtCl, N, L>
316where
317    AtCl: AtatClient,
318{
319    pub async fn set_gnss_config(&mut self, sensitivity: FixSensitivity) -> Result<(), Error> {
320        self.send(&SetGnssConfig {
321            location_mode: command::gnss::types::LocationMode::OnDeviceLocation,
322            fix_sensitivity: sensitivity,
323            urc_settings: command::gnss::types::UrcNotificationSetting::Full,
324            reserved: Reserved,
325            metrics: false.into(),
326            acquisition_mode: command::gnss::types::AcquisitionMode::ColdWarmStart,
327            early_abort: false.into(),
328        })
329        .await?;
330
331        Ok(())
332    }
333
334    // Check the assistance data in the modem response.
335    //
336    // This function checks the availability of assistance data in the modem's
337    // response. This function also sets a flag if any of the assistance databases
338    // should be updated.
339    async fn check_assistance_data(&mut self) -> Result<(), Error> {
340        use crate::gnss::responses::GnssAsssitance;
341
342        let data = self.send(&GetGnssAssitance).await?;
343
344        self.update_almanac = false;
345        self.update_ephemeris = false;
346
347        for GnssAsssitance {
348            typ,
349            available,
350            time_to_update,
351            ..
352        } in data
353        {
354            match typ {
355                crate::gnss::types::GnssAssitanceType::Almanac => match available {
356                    Bool::True => {
357                        debug!(
358                            "almanace data is available and should be updated within {}",
359                            time_to_update
360                        );
361                        self.update_almanac = time_to_update <= 0;
362                    }
363                    Bool::False => {
364                        debug!("almanace data is not available",);
365                        self.update_almanac = true;
366                    }
367                },
368                crate::gnss::types::GnssAssitanceType::RealTimeEphemeris => match available {
369                    Bool::True => {
370                        debug!(
371                            "real-time ephemeris data is available and should be updated within {}",
372                            time_to_update
373                        );
374                        self.update_ephemeris = time_to_update <= 0;
375                    }
376                    Bool::False => {
377                        debug!("real-time ephemerise data is not available",);
378                        self.update_ephemeris = true;
379                    }
380                },
381                crate::gnss::types::GnssAssitanceType::PredictedEphemeris => {}
382            }
383        }
384
385        Ok(())
386    }
387
388    /// Update GNSS assistance data when needed.
389    ///
390    /// This funtion will check if the current real-time ephemeris data is good
391    /// enough to get a fast GNSS fix. If not the function will attach to the LTE
392    /// network to download newer assistance data.
393    pub async fn update_gnss_asistance(&mut self) -> Result<(), Error> {
394        self.lte_disconnect().await?;
395
396        // Even with valid assistance data the system clock could be invalid
397        let mut clock = self.send(&GetClock).await?;
398
399        if clock.time.0.timestamp().is_zero() {
400            debug!("Clock time out of sync, synchronizing");
401
402            // The system clock is invalid, connect to LTE network to sync time
403            self.lte_connect().await?;
404
405            // Wait for the modem to synchronize time with the LTE network, try 5 times
406            // with a delay of 500ms.
407            for _ in 0..5 {
408                Timer::after(Duration::from_millis(500)).await;
409                clock = self.send(&GetClock).await?;
410                if !clock.time.0.timestamp().is_zero() {
411                    break;
412                }
413            }
414
415            self.lte_disconnect().await?;
416
417            if clock.time.0.timestamp().is_zero() {
418                return Err(Error::ClockSynchronization);
419            }
420        };
421
422        // Check the availability of assistance data
423        self.check_assistance_data().await?;
424
425        if !self.update_almanac && !self.update_ephemeris {
426            return Ok(());
427        }
428
429        self.lte_connect().await?;
430
431        if self.update_almanac {
432            self.send(&UpdateGnssAssitance {
433                typ: command::gnss::types::GnssAssitanceType::Almanac,
434            })
435            .await?;
436        }
437
438        if self.update_ephemeris {
439            self.send(&UpdateGnssAssitance {
440                typ: command::gnss::types::GnssAssitanceType::RealTimeEphemeris,
441            })
442            .await?;
443        }
444
445        for _ in 0..10 {
446            Timer::after(Duration::from_secs(10)).await;
447            self.check_assistance_data().await?;
448            if !self.update_almanac && !self.update_ephemeris {
449                break;
450            }
451        }
452
453        self.lte_disconnect().await?;
454
455        Ok(())
456    }
457
458    pub async fn get_gnss_fix(&mut self) -> Result<GnssFixReady, Error> {
459        use embassy_time::TimeoutError;
460
461        self.state.fix_subscriber.reset();
462
463        self.send(&ProgramGnss {
464            action: command::gnss::types::ProgramGnssAction::Single,
465        })
466        .await?;
467
468        match with_timeout(Duration::from_secs(180), self.state.fix_subscriber.wait()).await {
469            Ok(fix) => {
470                debug!("GNSS fix received: {:?}", fix);
471                Ok(fix)
472            }
473            Err(TimeoutError) => {
474                debug!("GNSS fix timed out");
475
476                self.send(&ProgramGnss {
477                    action: command::gnss::types::ProgramGnssAction::Stop,
478                })
479                .await?;
480
481                Err(TimeoutError.into())
482            }
483        }
484    }
485}
486
487#[derive(Clone, Debug, PartialEq)]
488pub struct UsernamePassword {
489    /// Username for broker authentication.
490    pub username: String<256>,
491
492    /// Password for broker authentication.
493    pub password: String<256>,
494}
495
496#[derive(Clone, Debug, PartialEq)]
497pub struct SecurityProfile {
498    /// The index of the secure profile previously set with the SSL / TLS Security Profile Configuration.
499    pub id: u8,
500}
501
502// TODO: replace enum with dedicated methods.
503#[derive(Clone, Debug, PartialEq)]
504#[allow(clippy::large_enum_variant)]
505pub enum MqttAuth {
506    UsernamePassword(UsernamePassword),
507    SecurityProfile(SecurityProfile),
508}
509
510impl<'sub, AtCl, const N: usize, const L: usize> Modem<'sub, AtCl, N, L>
511where
512    AtCl: AtatClient,
513{
514    pub async fn mqtt_configure(
515        &mut self,
516        client_id: &str,
517        auth: Option<MqttAuth>,
518    ) -> Result<(), Error> {
519        let msg = match auth {
520            Some(MqttAuth::UsernamePassword(UsernamePassword { username, password })) => {
521                &mqtt::Configure {
522                    id: 0,
523                    client_id,
524                    username: Some(username),
525                    password: Some(password),
526                    sp_id: None,
527                }
528            }
529            Some(MqttAuth::SecurityProfile(SecurityProfile { id })) => &mqtt::Configure {
530                id: 0,
531                client_id,
532                username: None,
533                password: None,
534                sp_id: Some(id),
535            },
536            None => &mqtt::Configure {
537                id: 0,
538                client_id,
539                username: None,
540                password: None,
541                sp_id: None,
542            },
543        };
544
545        self.send(msg).await?;
546
547        Ok(())
548    }
549
550    pub async fn mqtt_connect(&mut self, host: &str) -> Result<(), Error> {
551        self.lte_connect().await?;
552
553        self.send(&mqtt::Connect {
554            id: 0,
555            host,
556            port: None,
557            keepalive: None,
558        })
559        .await?;
560
561        let connected =
562            with_timeout(Duration::from_secs(30), self.state.mqtt_connected.wait()).await?;
563
564        match connected.rc {
565            MQTTStatusCode::Success => Ok(()),
566            status => {
567                error!("MQTT connect error: {:?}", connected.rc);
568                Err(Error::MQTT(status))
569            }
570        }
571    }
572
573    pub async fn mqtt_send(
574        &mut self,
575        topic: &str,
576        qos: mqtt::types::Qos,
577        data: &[u8],
578    ) -> Result<(), Error> {
579        debug!("Sending MQTT message");
580
581        self.send(&mqtt::PreparePublish {
582            id: 0,
583            topic,
584            qos: Some(qos),
585            length: data.len(),
586        })
587        .await?;
588
589        debug!("MQTT publish prepared");
590
591        self.send(&mqtt::Publish {
592            payload: atat::serde_bytes::Bytes::new(data),
593        })
594        .await?;
595
596        debug!("MQTT publish Sent");
597
598        Ok(())
599    }
600
601    pub async fn mqtt_disconnect(&mut self) -> Result<(), Error> {
602        self.send(&mqtt::Disconnect { id: 0 }).await?;
603        self.lte_disconnect().await?;
604        Ok(())
605    }
606}
607
608impl<'sub, AtCl, const N: usize, const L: usize> Modem<'sub, AtCl, N, L>
609where
610    AtCl: AtatClient,
611{
612    pub async fn nvm_write(
613        &mut self,
614        data_type: nvm::types::DataType,
615        index: u8,
616        data: &[u8],
617    ) -> Result<(), Error> {
618        debug!("Writing to nvm");
619
620        assert!(
621            !(0..=4).contains(&index) && !(7..=10).contains(&index),
622            "Indexes O to 4 and 7 to 10 are reserved for Sequans's internal use."
623        );
624
625        self.send(&nvm::PrepareWrite {
626            data_type,
627            index,
628            size: data.len(),
629        })
630        .await?;
631
632        debug!("NVM write ready");
633
634        self.send(&nvm::Write {
635            data: atat::serde_bytes::Bytes::new(data),
636        })
637        .await?;
638
639        debug!("NVM written");
640
641        Ok(())
642    }
643}
644
645impl<'sub, AtCl, const N: usize, const L: usize> Modem<'sub, AtCl, N, L>
646where
647    AtCl: AtatClient,
648{
649    /// Configures TLS/SSL security profile for use with e.g. MQTT.
650    ///
651    /// Certificates first need to be written to NVM (boot persistent).
652    pub async fn configure_tls_profile(
653        &mut self,
654        sp_id: u8,
655        ca_cert_id: u8,
656        client_cert_id: u8,
657        client_private_key_id: u8,
658    ) -> Result<(), Error> {
659        self.send(&ssl_tls::Configure {
660            sp_id,
661            version: ssl_tls::types::SslTlsVersion::Tls12,
662            cipher_specs: String::new(),
663            cert_valid_level: 0,
664            ca_cert_id,
665            client_cert_id,
666            client_private_key_id,
667            psk: String::new(),
668            psk_identity: String::new(),
669            storage_id: ssl_tls::types::StorageId::NVM,
670            resume: ssl_tls::types::Resume::Disabled,
671            lifetime: 0,
672        })
673        .await?;
674
675        Ok(())
676    }
677}