nrf_modem/
gnss.rs

1use crate::error::{Error, ErrorSource};
2use arrayvec::{ArrayString, ArrayVec};
3use core::{
4    cell::RefCell,
5    mem::{size_of, MaybeUninit},
6    pin::Pin,
7    sync::atomic::{AtomicU32, Ordering},
8    task::{Context, Poll},
9};
10use critical_section::Mutex;
11use futures::{task::AtomicWaker, Stream};
12use num_enum::{FromPrimitive, IntoPrimitive, TryFromPrimitive};
13
14const MAX_NMEA_BURST_SIZE: usize = 5;
15
16static GNSS_WAKER: AtomicWaker = AtomicWaker::new();
17static GNSS_NOTICED_EVENTS: AtomicU32 = AtomicU32::new(0);
18static GNSS_NMEA_STRINGS: Mutex<RefCell<ArrayVec<Result<GnssData, Error>, MAX_NMEA_BURST_SIZE>>> =
19    Mutex::new(RefCell::new(ArrayVec::new_const()));
20
21unsafe extern "C" fn gnss_callback(event: i32) {
22    let event_type = GnssEventType::from(event as u32);
23
24    #[cfg(feature = "defmt")]
25    defmt::trace!("Gnss -> {}", event_type);
26
27    if matches!(event_type, GnssEventType::Nmea) {
28        critical_section::with(|cs| {
29            GNSS_NMEA_STRINGS
30                .borrow_ref_mut(cs)
31                .try_push(GnssData::read_from_modem(GnssDataType::Nmea))
32                .ok()
33        });
34    }
35
36    GNSS_NOTICED_EVENTS.fetch_or(1 << event as u32, Ordering::SeqCst);
37
38    GNSS_WAKER.wake();
39}
40
41/// A GNSS objects that controls the GPS of the modem.
42///
43/// There can only be one instance at a time.
44pub struct Gnss {}
45
46impl Gnss {
47    /// Activate the GPS
48    pub async fn new() -> Result<Self, Error> {
49        if unsafe { !nrfxlib_sys::nrf_modem_is_initialized() } {
50            return Err(Error::ModemNotInitialized);
51        }
52
53        crate::MODEM_RUNTIME_STATE.activate_gps().await?;
54
55        unsafe {
56            nrfxlib_sys::nrf_modem_gnss_event_handler_set(Some(gnss_callback));
57        }
58
59        Ok(Gnss {})
60    }
61
62    /// Do a single GPS fix until a valid Position Velocity Time (PVT) is found.
63    ///
64    /// The `timeout_seconds` parameter controls the maximum time the GNSS receiver is allowed to run while trying to produce a valid PVT estimate.
65    /// If the value is non-zero, the GNSS receiver is turned off after the time is up regardless of whether a valid PVT estimate was produced or not.
66    /// If the value is set to zero, the GNSS receiver is allowed to run indefinitely until a valid PVT estimate is produced.
67    /// A sane default value: 60s.
68    pub fn start_single_fix(
69        mut self,
70        config: GnssConfig,
71        timeout_seconds: u16,
72    ) -> Result<GnssStream, Error> {
73        #[cfg(feature = "defmt")]
74        defmt::trace!("Setting single fix");
75
76        unsafe {
77            nrfxlib_sys::nrf_modem_gnss_fix_interval_set(0).into_result()?;
78            nrfxlib_sys::nrf_modem_gnss_fix_retry_set(timeout_seconds).into_result()?;
79        }
80
81        #[cfg(feature = "defmt")]
82        defmt::trace!("Apply config");
83
84        self.apply_config(config)?;
85
86        #[cfg(feature = "defmt")]
87        defmt::debug!("Starting gnss");
88
89        let gnss_stream = GnssStream::new(true, self);
90        unsafe {
91            nrfxlib_sys::nrf_modem_gnss_start();
92        }
93
94        Ok(gnss_stream)
95    }
96
97    pub fn start_continuous_fix(mut self, config: GnssConfig) -> Result<GnssStream, Error> {
98        #[cfg(feature = "defmt")]
99        defmt::trace!("Setting single fix");
100
101        unsafe {
102            nrfxlib_sys::nrf_modem_gnss_fix_interval_set(1)
103                .into_result()
104                .unwrap();
105        }
106
107        #[cfg(feature = "defmt")]
108        defmt::trace!("Apply config");
109
110        self.apply_config(config)?;
111
112        #[cfg(feature = "defmt")]
113        defmt::debug!("Starting gnss");
114
115        let gnss_stream = GnssStream::new(false, self);
116        unsafe {
117            nrfxlib_sys::nrf_modem_gnss_start();
118        }
119
120        Ok(gnss_stream)
121    }
122
123    pub fn start_periodic_fix(
124        mut self,
125        config: GnssConfig,
126        period_seconds: u16,
127    ) -> Result<GnssStream, Error> {
128        #[cfg(feature = "defmt")]
129        defmt::trace!("Setting single fix");
130
131        unsafe {
132            nrfxlib_sys::nrf_modem_gnss_fix_interval_set(period_seconds.max(10))
133                .into_result()
134                .unwrap();
135        }
136
137        #[cfg(feature = "defmt")]
138        defmt::trace!("Apply config");
139
140        self.apply_config(config)?;
141
142        #[cfg(feature = "defmt")]
143        defmt::debug!("Starting gnss");
144
145        let gnss_stream = GnssStream::new(false, self);
146        unsafe {
147            nrfxlib_sys::nrf_modem_gnss_start();
148        }
149
150        Ok(gnss_stream)
151    }
152
153    fn apply_config(&mut self, config: GnssConfig) -> Result<(), Error> {
154        unsafe {
155            nrfxlib_sys::nrf_modem_gnss_elevation_threshold_set(config.elevation_threshold_angle)
156                .into_result()?;
157            nrfxlib_sys::nrf_modem_gnss_use_case_set(config.use_case.into()).into_result()?;
158            nrfxlib_sys::nrf_modem_gnss_nmea_mask_set(config.nmea_mask.into()).into_result()?;
159            nrfxlib_sys::nrf_modem_gnss_power_mode_set(u32::from(config.power_mode) as _)
160                .into_result()?;
161            nrfxlib_sys::nrf_modem_gnss_timing_source_set(u32::from(config.timing_source) as _)
162                .into_result()?;
163        }
164        Ok(())
165    }
166
167    pub async fn deactivate(self) -> Result<(), Error> {
168        core::mem::forget(self);
169        let result = crate::MODEM_RUNTIME_STATE.deactivate_gps().await;
170
171        if result.is_err() {
172            crate::MODEM_RUNTIME_STATE.set_error_active();
173        }
174
175        result
176    }
177}
178
179impl Drop for Gnss {
180    fn drop(&mut self) {
181        #[cfg(feature = "defmt")]
182        defmt::warn!(
183            "Turning off GNSS synchronously. Use async function `deactivate` to avoid blocking and to get more guarantees that the modem is actually shut off."
184        );
185
186        if let Err(_e) = crate::MODEM_RUNTIME_STATE.deactivate_gps_blocking() {
187            #[cfg(feature = "defmt")]
188            defmt::error!("Could not turn off the gnss: {}", _e);
189            crate::MODEM_RUNTIME_STATE.set_error_active();
190        }
191    }
192}
193
194#[cfg_attr(feature = "defmt", derive(defmt::Format))]
195pub struct NmeaMask {
196    /// Enables Global Positioning System Fix Data.
197    pub gga: bool,
198    /// Enables Geographic Position Latitude/Longitude and time.
199    pub gll: bool,
200    /// Enables DOP and active satellites.
201    pub gsa: bool,
202    /// Enables Satellites in view.
203    pub gsv: bool,
204    /// Enables Recommended minimum specific GPS/Transit data.
205    pub rmc: bool,
206}
207
208impl Default for NmeaMask {
209    fn default() -> Self {
210        Self {
211            gga: true,
212            gll: true,
213            gsa: true,
214            gsv: true,
215            rmc: true,
216        }
217    }
218}
219
220impl From<NmeaMask> for u16 {
221    fn from(mask: NmeaMask) -> Self {
222        (mask.gga as u16 * nrfxlib_sys::NRF_MODEM_GNSS_NMEA_GGA_MASK as u16)
223            | (mask.gll as u16 * nrfxlib_sys::NRF_MODEM_GNSS_NMEA_GLL_MASK as u16)
224            | (mask.gsa as u16 * nrfxlib_sys::NRF_MODEM_GNSS_NMEA_GSA_MASK as u16)
225            | (mask.gsv as u16 * nrfxlib_sys::NRF_MODEM_GNSS_NMEA_GSV_MASK as u16)
226            | (mask.rmc as u16 * nrfxlib_sys::NRF_MODEM_GNSS_NMEA_RMC_MASK as u16)
227    }
228}
229
230#[derive(Copy, Clone, IntoPrimitive, FromPrimitive, Debug, Default)]
231#[cfg_attr(feature = "defmt", derive(defmt::Format))]
232#[repr(u32)]
233enum GnssEventType {
234    #[default]
235    None = 0,
236    /// PVT event.
237    Pvt = nrfxlib_sys::NRF_MODEM_GNSS_EVT_PVT,
238    /// GNSS fix event.
239    GnssFix = nrfxlib_sys::NRF_MODEM_GNSS_EVT_FIX,
240    /// NMEA event.
241    Nmea = nrfxlib_sys::NRF_MODEM_GNSS_EVT_NMEA,
242    /// Need new APGS data event.
243    AgpsRequest = nrfxlib_sys::NRF_MODEM_GNSS_EVT_AGNSS_REQ,
244    /// GNSS is blocked by LTE event.
245    BlockedByLte = nrfxlib_sys::NRF_MODEM_GNSS_EVT_BLOCKED,
246    /// GNSS is unblocked by LTE event.
247    UnblockedByLte = nrfxlib_sys::NRF_MODEM_GNSS_EVT_UNBLOCKED,
248    /// GNSS woke up in periodic mode.
249    ///
250    /// This event is sent when GNSS receiver is turned on in periodic mode. This happens when GNSS starts acquiring the next periodic fix but also when a scheduled download starts.
251    PeriodicWakeup = nrfxlib_sys::NRF_MODEM_GNSS_EVT_PERIODIC_WAKEUP,
252    /// GNSS enters sleep because fix retry timeout was reached in periodic or single fix mode.
253    RetryTimeoutReached = nrfxlib_sys::NRF_MODEM_GNSS_EVT_SLEEP_AFTER_TIMEOUT,
254    /// GNSS enters sleep because fix was achieved in periodic mode.
255    SleepAfterFix = nrfxlib_sys::NRF_MODEM_GNSS_EVT_SLEEP_AFTER_FIX,
256    /// Reference altitude for 3-satellite fix expired.
257    ReferenceAltitudeExpired = nrfxlib_sys::NRF_MODEM_GNSS_EVT_REF_ALT_EXPIRED,
258}
259
260impl GnssEventType {
261    pub fn get_from_bit_packed(container: u32) -> Self {
262        let variants = [
263            Self::ReferenceAltitudeExpired,
264            Self::SleepAfterFix,
265            Self::RetryTimeoutReached,
266            Self::PeriodicWakeup,
267            Self::UnblockedByLte,
268            Self::BlockedByLte,
269            Self::AgpsRequest,
270            Self::Nmea,
271            Self::GnssFix,
272            Self::Pvt,
273        ];
274
275        for variant in variants {
276            if container & (1 << variant as u32) != 0 {
277                return variant;
278            }
279        }
280
281        Self::None
282    }
283}
284
285#[cfg_attr(feature = "defmt", derive(defmt::Format))]
286pub struct GnssConfig {
287    /// Set below which elevation angle GNSS should stop tracking a satellite.
288    ///
289    /// Satellites with elevation angle less than the threshold are excluded from the estimation.
290    ///
291    /// Default value: 5 deg
292    pub elevation_threshold_angle: u8,
293    pub use_case: GnssUsecase,
294    pub nmea_mask: NmeaMask,
295    pub timing_source: GnssTimingSource,
296    pub power_mode: GnssPowerSaveMode,
297}
298
299impl Default for GnssConfig {
300    fn default() -> Self {
301        Self {
302            elevation_threshold_angle: 5,
303            use_case: Default::default(),
304            nmea_mask: Default::default(),
305            timing_source: Default::default(),
306            power_mode: Default::default(),
307        }
308    }
309}
310
311#[cfg_attr(feature = "defmt", derive(defmt::Format))]
312#[derive(Debug, Clone, Default)]
313pub struct GnssUsecase {
314    /// Low accuracy fixes allowed.
315    ///
316    /// The error in position calculation can be larger than in normal accuracy mode.
317    /// In addition, GNSS might only use three satellites to determine a fix,
318    /// while in normal accuracy mode at least four satellites are used.
319    pub low_accuracy: bool,
320    /// Disable scheduled downloads.
321    ///
322    /// By default, in periodic navigation mode, when GNSS determines it needs to download ephemerides or almanacs from the broadcast,
323    /// the fix interval and fix retry parameters are temporarily ignored. GNSS will perform scheduled downloads until it has downloaded the data it needs,
324    /// after which normal operation is resumed.
325    ///
326    /// When this bit is set, scheduled downloads are disabled.
327    /// This is recommended when A-GPS is used to supply assistance data to the GNSS.
328    /// It is also possible to use this option without A-GPS, but it should be noted that in that case GNSS will never get some data (for example ionospheric corrections),
329    /// which may affect the accuracy.
330    pub scheduled_downloads_disable: bool,
331}
332
333impl From<GnssUsecase> for u8 {
334    fn from(usecase: GnssUsecase) -> Self {
335        nrfxlib_sys::NRF_MODEM_GNSS_USE_CASE_MULTIPLE_HOT_START as u8
336            | (usecase.low_accuracy as u8 * nrfxlib_sys::NRF_MODEM_GNSS_USE_CASE_LOW_ACCURACY as u8)
337            | (usecase.scheduled_downloads_disable as u8
338                * nrfxlib_sys::NRF_MODEM_GNSS_USE_CASE_SCHED_DOWNLOAD_DISABLE as u8)
339    }
340}
341
342/// Used to select which sleep timing source GNSS uses.
343///
344/// Using TCXO instead of RTC during GNSS sleep periods might be beneficial when used with 1PPS.
345/// When GNSS is not running all the time (periodic navigation or duty-cycling is used), 1PPS accuracy can be improved by using TCXO.
346/// It may also improve sensitivity for periodic navigation when the fix interval is short.
347///
348/// *Note*: Use of TCXO significantly raises the idle current consumption.
349#[derive(Debug, Clone, Default, IntoPrimitive, TryFromPrimitive)]
350#[cfg_attr(feature = "defmt", derive(defmt::Format))]
351#[repr(u32)]
352pub enum GnssTimingSource {
353    #[default]
354    Rtc = nrfxlib_sys::NRF_MODEM_GNSS_TIMING_SOURCE_RTC,
355    Tcxo = nrfxlib_sys::NRF_MODEM_GNSS_TIMING_SOURCE_TCXO,
356}
357
358/// Use these values to select which power save mode GNSS should use.
359///
360/// This only affects continuous navigation mode.
361///
362/// When GNSS engages duty-cycled tracking, it only tracks for 20% of time and spends the rest of the time in sleep.
363/// The different modes control how aggressively GNSS engages duty-cycled tracking, but the duty-cycling itself is the same with both modes.
364#[derive(Debug, Clone, Default, IntoPrimitive, TryFromPrimitive)]
365#[cfg_attr(feature = "defmt", derive(defmt::Format))]
366#[repr(u32)]
367pub enum GnssPowerSaveMode {
368    #[default]
369    Disabled = nrfxlib_sys::NRF_MODEM_GNSS_PSM_DISABLED,
370    DutyCyclingPerformance = nrfxlib_sys::NRF_MODEM_GNSS_PSM_DUTY_CYCLING_PERFORMANCE,
371    DutyCycling = nrfxlib_sys::NRF_MODEM_GNSS_PSM_DUTY_CYCLING_POWER,
372}
373
374#[derive(Debug, Clone, IntoPrimitive, TryFromPrimitive)]
375#[cfg_attr(feature = "defmt", derive(defmt::Format))]
376#[repr(u32)]
377enum GnssDataType {
378    PositionVelocityTime = nrfxlib_sys::NRF_MODEM_GNSS_DATA_PVT,
379    Nmea = nrfxlib_sys::NRF_MODEM_GNSS_DATA_NMEA,
380    Agps = nrfxlib_sys::NRF_MODEM_GNSS_DATA_AGNSS_REQ,
381}
382
383/// An enum containing all possible GNSS data types
384#[derive(Debug, Clone)]
385pub enum GnssData {
386    /// A PVT value
387    PositionVelocityTime(nrfxlib_sys::nrf_modem_gnss_pvt_data_frame),
388    /// An NMEA string
389    Nmea(ArrayString<83>),
390    /// An assisted gps data frame
391    Agps(nrfxlib_sys::nrf_modem_gnss_agnss_data_frame),
392}
393
394impl GnssData {
395    fn read_from_modem(data_type: GnssDataType) -> Result<Self, Error> {
396        match data_type {
397            GnssDataType::PositionVelocityTime => {
398                let mut data = MaybeUninit::uninit();
399
400                unsafe {
401                    nrfxlib_sys::nrf_modem_gnss_read(
402                        data.as_mut_ptr() as *mut _,
403                        size_of::<nrfxlib_sys::nrf_modem_gnss_pvt_data_frame>() as i32,
404                        data_type as u32 as _,
405                    )
406                    .into_result()?;
407                    Ok(GnssData::PositionVelocityTime(data.assume_init()))
408                }
409            }
410            GnssDataType::Nmea => {
411                let mut data: MaybeUninit<nrfxlib_sys::nrf_modem_gnss_nmea_data_frame> =
412                    MaybeUninit::uninit();
413
414                unsafe {
415                    nrfxlib_sys::nrf_modem_gnss_read(
416                        data.as_mut_ptr() as *mut _,
417                        size_of::<nrfxlib_sys::nrf_modem_gnss_nmea_data_frame>() as i32,
418                        data_type as u32 as _,
419                    )
420                    .into_result()?;
421
422                    // Allow transmute warnings. Sometimes apparently the nmea_str is a `u8` array already
423                    #[allow(clippy::useless_transmute, clippy::missing_transmute_annotations)]
424                    let data = core::mem::transmute::<_, [u8; 83]>(data.assume_init().nmea_str); // Make data be u8
425                    let mut string_data = ArrayString::from_byte_string(&data)?;
426                    string_data.truncate(
427                        string_data
428                            .as_bytes()
429                            .iter()
430                            .take_while(|b| **b != 0)
431                            .count(),
432                    );
433                    Ok(GnssData::Nmea(string_data))
434                }
435            }
436            GnssDataType::Agps => {
437                let mut data = MaybeUninit::uninit();
438
439                unsafe {
440                    nrfxlib_sys::nrf_modem_gnss_read(
441                        data.as_mut_ptr() as *mut _,
442                        size_of::<nrfxlib_sys::nrf_modem_gnss_agnss_data_frame>() as i32,
443                        data_type as u32 as _,
444                    )
445                    .into_result()?;
446                    Ok(GnssData::Agps(data.assume_init()))
447                }
448            }
449        }
450    }
451}
452
453/// An async stream of gnss data.
454///
455/// Implements the [futures::Stream] trait for polling.
456pub struct GnssStream {
457    single_fix: bool,
458    done: bool,
459    gnss: Option<Gnss>,
460}
461
462impl GnssStream {
463    fn new(single_fix: bool, gnss: Gnss) -> Self {
464        GNSS_NOTICED_EVENTS.store(0, Ordering::SeqCst);
465        Self {
466            single_fix,
467            done: false,
468            gnss: Some(gnss),
469        }
470    }
471
472    pub async fn deactivate(self) -> Result<(), Error> {
473        self.free().deactivate().await
474    }
475
476    /// Get back the gnss instance
477    pub fn free(mut self) -> Gnss {
478        self.gnss.take().unwrap()
479    }
480}
481
482impl Stream for GnssStream {
483    type Item = Result<GnssData, Error>;
484
485    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
486        if self.done {
487            #[cfg(feature = "defmt")]
488            defmt::trace!("Gnss is done");
489
490            return Poll::Ready(None);
491        }
492
493        let event_bits = GNSS_NOTICED_EVENTS.load(Ordering::SeqCst);
494        let event = GnssEventType::get_from_bit_packed(event_bits);
495
496        let mut left_over_nmea_strings = false;
497
498        let data = match event {
499            GnssEventType::Pvt => Some(GnssData::read_from_modem(
500                GnssDataType::PositionVelocityTime,
501            )),
502            GnssEventType::GnssFix if self.single_fix => {
503                self.get_mut().done = true;
504                Some(GnssData::read_from_modem(
505                    GnssDataType::PositionVelocityTime,
506                ))
507            }
508            GnssEventType::GnssFix => Some(GnssData::read_from_modem(
509                GnssDataType::PositionVelocityTime,
510            )),
511            GnssEventType::Nmea => critical_section::with(|cs| {
512                let mut strings = GNSS_NMEA_STRINGS.borrow_ref_mut(cs);
513                left_over_nmea_strings = strings.len() > 1;
514                strings.pop_at(0)
515            }),
516            GnssEventType::AgpsRequest => Some(GnssData::read_from_modem(GnssDataType::Agps)),
517            GnssEventType::RetryTimeoutReached | GnssEventType::SleepAfterFix
518                if self.single_fix =>
519            {
520                self.get_mut().done = true;
521                return Poll::Ready(None);
522            }
523            _ => None,
524        };
525
526        let left_over_event_bits = if !left_over_nmea_strings {
527            GNSS_NOTICED_EVENTS.fetch_and(!(1 << event as u32), Ordering::SeqCst) != 0
528        } else {
529            true
530        };
531
532        if left_over_event_bits {
533            cx.waker().wake_by_ref();
534        } else {
535            GNSS_WAKER.register(cx.waker());
536        }
537
538        match data {
539            Some(data) => Poll::Ready(Some(data)),
540            None => Poll::Pending,
541        }
542    }
543}
544
545impl Drop for GnssStream {
546    fn drop(&mut self) {
547        unsafe {
548            #[cfg(feature = "defmt")]
549            defmt::debug!("Stopping gnss");
550
551            nrfxlib_sys::nrf_modem_gnss_stop();
552        }
553    }
554}