Skip to main content

sidereon_core/ntrip/
gga.rs

1use crate::nmea::{self, Gga, GgaQuality, NmeaTalker, NmeaTime};
2use crate::{Error, Result, Wgs84Geodetic};
3
4#[derive(Clone, Debug, PartialEq)]
5pub struct GgaPosition {
6    pub lat_deg: f64,
7    pub lon_deg: f64,
8    pub height_m: f64,
9    pub fix_quality: u8,
10    pub num_satellites: u8,
11    pub hdop: f64,
12}
13
14impl Default for GgaPosition {
15    fn default() -> Self {
16        Self {
17            lat_deg: 0.0,
18            lon_deg: 0.0,
19            height_m: 0.0,
20            fix_quality: 1,
21            num_satellites: 10,
22            hdop: 1.0,
23        }
24    }
25}
26
27pub fn format_gga(position: &GgaPosition, utc_seconds_of_day: f64) -> Result<Vec<u8>> {
28    validate(position, utc_seconds_of_day)?;
29    let geodetic = Wgs84Geodetic::new(
30        position.lat_deg.to_radians(),
31        position.lon_deg.to_radians(),
32        position.height_m,
33    )
34    .map_err(|err| Error::InvalidInput(err.to_string()))?;
35    let mut gga = Gga::vrs_position(
36        geodetic,
37        format_time(utc_seconds_of_day)?,
38        quality(position.fix_quality),
39        position.num_satellites,
40        position.hdop,
41        7,
42    )
43    .map_err(|err| Error::InvalidInput(err.to_string()))?;
44    gga.geoid_separation_m = None;
45    nmea::write_gga(NmeaTalker::System(crate::GnssSystem::Gps), &gga)
46        .map(|sentence| sentence.into_bytes())
47        .map_err(|err| Error::InvalidInput(err.to_string()))
48}
49
50fn validate(position: &GgaPosition, utc_seconds_of_day: f64) -> Result<()> {
51    if !position.lat_deg.is_finite()
52        || !position.lon_deg.is_finite()
53        || !position.height_m.is_finite()
54        || !position.hdop.is_finite()
55        || !utc_seconds_of_day.is_finite()
56    {
57        return Err(Error::InvalidInput("GGA inputs must be finite".into()));
58    }
59    if !(-90.0..=90.0).contains(&position.lat_deg) {
60        return Err(Error::InvalidInput("GGA latitude outside [-90, 90]".into()));
61    }
62    if !(-180.0..=180.0).contains(&position.lon_deg) {
63        return Err(Error::InvalidInput(
64            "GGA longitude outside [-180, 180]".into(),
65        ));
66    }
67    if position.hdop < 0.0 {
68        return Err(Error::InvalidInput("GGA HDOP must be non-negative".into()));
69    }
70    if !(0.0..86400.0).contains(&utc_seconds_of_day) {
71        return Err(Error::InvalidInput("GGA time must be in [0, 86400)".into()));
72    }
73    Ok(())
74}
75
76fn format_time(seconds: f64) -> Result<NmeaTime> {
77    NmeaTime::from_seconds_of_day_floor_centis(seconds)
78        .map_err(|err| Error::InvalidInput(err.to_string()))
79}
80
81fn quality(value: u8) -> GgaQuality {
82    match value {
83        0 => GgaQuality::Invalid,
84        1 => GgaQuality::GpsSps,
85        2 => GgaQuality::Differential,
86        3 => GgaQuality::Pps,
87        4 => GgaQuality::RtkFixed,
88        5 => GgaQuality::RtkFloat,
89        6 => GgaQuality::Estimated,
90        7 => GgaQuality::Manual,
91        8 => GgaQuality::Simulator,
92        other => GgaQuality::Other(other),
93    }
94}