Skip to main content

ados_positioning/
nmea.rs

1//! NMEA GGA / RMC decode, the fallback when a receiver sends no UBX.
2
3use crate::receiver::Fix;
4
5/// A decoded GGA sentence.
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct Gga {
8    /// Fix quality from the GGA quality field.
9    pub fix: Fix,
10    /// Satellites used.
11    pub sats: u8,
12    /// Horizontal dilution of precision.
13    pub hdop: Option<f64>,
14    /// Latitude, degrees.
15    pub lat_deg: Option<f64>,
16    /// Longitude, degrees.
17    pub lon_deg: Option<f64>,
18    /// Altitude above mean sea level, metres.
19    pub h_msl_m: Option<f64>,
20    /// Height above the ellipsoid (MSL plus geoid separation), metres.
21    pub h_ellipsoid_m: Option<f64>,
22}
23
24/// A decoded RMC sentence.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Rmc {
27    /// Fix from the RMC status and mode fields.
28    pub fix: Fix,
29    /// Latitude, degrees.
30    pub lat_deg: Option<f64>,
31    /// Longitude, degrees.
32    pub lon_deg: Option<f64>,
33}
34
35/// A sentence type this crate decodes.
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub enum Sentence {
38    /// GGA: fix data.
39    Gga(Gga),
40    /// RMC: recommended minimum data.
41    Rmc(Rmc),
42}
43
44/// Parse a checksum-verified sentence (`$GNGGA,...*hh`).
45pub fn parse(line: &str) -> Option<Sentence> {
46    let body = line.strip_prefix('$')?;
47    let body = body.rsplit_once('*').map_or(body, |(b, _)| b);
48    let f: Vec<&str> = body.split(',').collect();
49    let kind = f.first()?.get(2..)?;
50    match kind {
51        "GGA" if f.len() >= 12 => Some(Sentence::Gga(gga(&f))),
52        "RMC" if f.len() >= 7 => Some(Sentence::Rmc(rmc(&f))),
53        _ => None,
54    }
55}
56
57fn num(s: &str) -> Option<f64> {
58    s.parse().ok().filter(|v: &f64| v.is_finite())
59}
60
61/// `ddmm.mmmm` / `dddmm.mmmm` with a hemisphere letter.
62fn coord(v: &str, hemi: &str) -> Option<f64> {
63    let raw = num(v)?;
64    let deg = (raw / 100.0).trunc();
65    let val = deg + (raw - deg * 100.0) / 60.0;
66    match hemi {
67        "N" | "E" => Some(val),
68        "S" | "W" => Some(-val),
69        _ => None,
70    }
71}
72
73fn gga(f: &[&str]) -> Gga {
74    let fix = match f[6] {
75        "1" => Fix::ThreeD,
76        "2" => Fix::Dgps,
77        "4" => Fix::RtkFixed,
78        "5" => Fix::RtkFloat,
79        _ => Fix::None,
80    };
81    let h_msl = num(f[9]);
82    let sep = num(f[11]);
83    let has_fix = fix != Fix::None;
84    Gga {
85        fix,
86        sats: f[7].parse().unwrap_or(0),
87        hdop: num(f[8]),
88        lat_deg: coord(f[2], f[3]).filter(|_| has_fix),
89        lon_deg: coord(f[4], f[5]).filter(|_| has_fix),
90        h_msl_m: h_msl.filter(|_| has_fix),
91        h_ellipsoid_m: h_msl.zip(sep).map(|(h, s)| h + s).filter(|_| has_fix),
92    }
93}
94
95fn rmc(f: &[&str]) -> Rmc {
96    // RMC cannot tell 2D from 3D; a valid autonomous fix reads as 2d.
97    let mode = f.get(12).copied().unwrap_or("A");
98    let fix = match (f[2], mode) {
99        ("A", "D") => Fix::Dgps,
100        ("A", "R") => Fix::RtkFixed,
101        ("A", "F") => Fix::RtkFloat,
102        ("A", "N") => Fix::None,
103        ("A", _) => Fix::TwoD,
104        _ => Fix::None,
105    };
106    let has_fix = fix != Fix::None;
107    Rmc {
108        fix,
109        lat_deg: coord(f[3], f[4]).filter(|_| has_fix),
110        lon_deg: coord(f[5], f[6]).filter(|_| has_fix),
111    }
112}