use crate::{
coordinates::NmeaCoordinates,
encoder::NmeaEncode,
macros::{write_byte, write_str},
message::NmeaMessageError,
number::NmeaNumber,
parser::NmeaParse,
time::NmeaTime,
};
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Gga<'a> {
pub time: &'a str,
pub latitude: &'a str,
pub latitude_dir: &'a str,
pub longitude: &'a str,
pub longitude_dir: &'a str,
pub fix_quality: &'a str,
pub satellites: &'a str,
pub hdop: &'a str,
pub altitude: &'a str,
pub altitude_units: &'a str,
pub geoid_separation: &'a str,
pub geoid_separation_units: &'a str,
pub dgps_age: Option<&'a str>,
pub dgps_station_id: Option<&'a str>,
}
impl<'a> NmeaParse<'a> for Gga<'a> {
fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
let mut f = fields.splitn(15, ',');
Ok(Self {
time: f.next().ok_or(NmeaMessageError::MissingField)?,
latitude: f.next().ok_or(NmeaMessageError::MissingField)?,
latitude_dir: f.next().ok_or(NmeaMessageError::MissingField)?,
longitude: f.next().ok_or(NmeaMessageError::MissingField)?,
longitude_dir: f.next().ok_or(NmeaMessageError::MissingField)?,
fix_quality: f.next().ok_or(NmeaMessageError::MissingField)?,
satellites: f.next().ok_or(NmeaMessageError::MissingField)?,
hdop: f.next().ok_or(NmeaMessageError::MissingField)?,
altitude: f.next().ok_or(NmeaMessageError::MissingField)?,
altitude_units: f.next().ok_or(NmeaMessageError::MissingField)?,
geoid_separation: f.next().ok_or(NmeaMessageError::MissingField)?,
geoid_separation_units: f.next().ok_or(NmeaMessageError::MissingField)?,
dgps_age: f.next().filter(|s| !s.is_empty()),
dgps_station_id: f.next().filter(|s| !s.is_empty()),
})
}
}
impl NmeaEncode for Gga<'_> {
fn encoded_len(&self) -> usize {
self.time.len()
+ self.latitude.len()
+ self.latitude_dir.len()
+ self.longitude.len()
+ self.longitude_dir.len()
+ self.fix_quality.len()
+ self.satellites.len()
+ self.hdop.len()
+ self.altitude.len()
+ self.altitude_units.len()
+ self.geoid_separation.len()
+ self.geoid_separation_units.len()
+ self.dgps_age.map_or(0, str::len)
+ self.dgps_station_id.map_or(0, str::len)
+ 13
}
fn encode(&self, buf: &mut [u8]) -> usize {
let mut pos = 0;
write_str!(buf, pos, self.time);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.latitude);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.latitude_dir);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.longitude);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.longitude_dir);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.fix_quality);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.satellites);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.hdop);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.altitude);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.altitude_units);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.geoid_separation);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.geoid_separation_units);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.dgps_age.unwrap_or(""));
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.dgps_station_id.unwrap_or(""));
pos
}
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GgaFixQuality {
Invalid = 0,
GpsFix = 1,
DifferentialGpsFix = 2,
PpsFix = 3,
RtkFixed = 4,
RtkFloat = 5,
DeadReckoning = 6,
ManualInput = 7,
Simulation = 8,
}
impl GgaFixQuality {
#[must_use]
pub fn parse(raw: u8) -> Option<Self> {
match raw {
0 => Some(GgaFixQuality::Invalid),
1 => Some(GgaFixQuality::GpsFix),
2 => Some(GgaFixQuality::DifferentialGpsFix),
3 => Some(GgaFixQuality::PpsFix),
4 => Some(GgaFixQuality::RtkFixed),
5 => Some(GgaFixQuality::RtkFloat),
6 => Some(GgaFixQuality::DeadReckoning),
7 => Some(GgaFixQuality::ManualInput),
8 => Some(GgaFixQuality::Simulation),
_ => None,
}
}
}
impl Gga<'_> {
#[must_use]
pub fn time(&self) -> Option<NmeaTime> {
NmeaTime::parse(self.time)
}
#[must_use]
pub fn coordinates(&self) -> Option<NmeaCoordinates> {
NmeaCoordinates::parse(
self.latitude,
self.latitude_dir,
self.longitude,
self.longitude_dir,
)
}
#[must_use]
pub fn fix_quality(&self) -> Option<GgaFixQuality> {
GgaFixQuality::parse(self.fix_quality.parse().ok()?)
}
#[must_use]
pub fn has_fix(&self) -> bool {
self.fix_quality()
.is_some_and(|fq| fq != GgaFixQuality::Invalid)
}
#[must_use]
pub fn satellites(&self) -> Option<u8> {
self.satellites.parse().ok()
}
#[must_use]
pub fn hdop(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.hdop)
}
#[must_use]
pub fn altitude(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.altitude)
}
#[must_use]
pub fn geoid_separation(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.geoid_separation)
}
pub fn dgps_age(&self) -> Option<NmeaNumber> {
self.dgps_age.and_then(NmeaNumber::parse)
}
#[must_use]
pub fn dgps_station_id(&self) -> Option<u16> {
self.dgps_station_id.and_then(|s| s.parse().ok())
}
}
#[cfg(test)]
mod tests {
use super::*;
const FIELDS: &str = "092750.000,5321.6802,N,00630.3372,W,1,8,1.03,61.7,M,55.2,M,,";
#[test]
fn parses_raw_fields() {
let gga = Gga::parse(FIELDS).unwrap();
assert_eq!(gga.latitude, "5321.6802");
assert_eq!(gga.latitude_dir, "N");
assert_eq!(gga.longitude, "00630.3372");
assert_eq!(gga.longitude_dir, "W");
assert_eq!(gga.satellites, "8");
assert_eq!(gga.altitude, "61.7");
assert_eq!(gga.geoid_separation, "55.2");
}
#[test]
fn time_parsed() {
let gga = Gga::parse(FIELDS).unwrap();
assert_eq!(
gga.time(),
Some(NmeaTime {
hours: 9,
minutes: 27,
seconds: 50,
subseconds: 0
})
);
}
#[test]
fn gps_fix_quality() {
let gga = Gga::parse(FIELDS).unwrap();
assert_eq!(gga.fix_quality().unwrap(), GgaFixQuality::GpsFix);
assert!(gga.has_fix());
}
#[test]
fn no_fix() {
let gga = Gga::parse("092750.000,,,,,0,0,,,M,,M,,").unwrap();
assert_eq!(gga.fix_quality().unwrap(), GgaFixQuality::Invalid);
assert!(!gga.has_fix());
}
#[test]
fn rtk_fixed() {
let gga =
Gga::parse("092750.000,5321.6802,N,00630.3372,W,4,12,0.8,61.7,M,55.2,M,,").unwrap();
assert_eq!(gga.fix_quality().unwrap(), GgaFixQuality::RtkFixed);
}
#[test]
fn satellites_parsed() {
let gga = Gga::parse(FIELDS).unwrap();
assert_eq!(gga.satellites(), Some(8));
}
#[test]
fn missing_required_field() {
assert!(Gga::parse("092750.000,5321.6802,N").is_err());
}
#[test]
fn unknown_fix_quality_treated_as_none() {
let gga =
Gga::parse("092750.000,5321.6802,N,00630.3372,W,9,8,1.03,61.7,M,55.2,M,,").unwrap();
assert!(gga.fix_quality().is_none());
}
#[test]
fn optional_dgps_fields_empty() {
let gga = Gga::parse(FIELDS).unwrap();
assert!(gga.dgps_age().is_none());
assert!(gga.dgps_station_id().is_none());
}
#[test]
fn dgps_fields_present() {
let gga = Gga::parse("092750.000,5321.6802,N,00630.3372,W,2,8,1.03,61.7,M,55.2,M,2.5,0023")
.unwrap();
assert!(gga.dgps_age().is_some());
assert_eq!(gga.dgps_station_id(), Some(23));
}
}