use crate::{
coordinates::NmeaCoordinates,
encoder::NmeaEncode,
faa::FaaMode,
macros::{write_byte, write_str},
message::NmeaMessageError,
number::NmeaNumber,
parser::NmeaParse,
time::NmeaTime,
};
#[repr(u8)]
pub enum GnsModeIdentifier {
Gps = 0,
Glonass = 1,
Galileo = 2,
BeiDou = 3,
Qzss = 4,
}
impl GnsModeIdentifier {
#[must_use]
pub fn parse(raw: u8) -> Option<Self> {
match raw {
0 => Some(Self::Gps),
1 => Some(Self::Glonass),
2 => Some(Self::Galileo),
3 => Some(Self::BeiDou),
4 => Some(Self::Qzss),
_ => None,
}
}
}
impl From<GnsModeIdentifier> for usize {
fn from(value: GnsModeIdentifier) -> Self {
value as usize
}
}
#[repr(u8)]
pub enum GnsNavStatus {
Safe = b'S',
Caution = b'C',
Unsafe = b'U',
Invalid = b'V',
}
impl GnsNavStatus {
#[must_use]
pub fn parse(raw: &str) -> Option<Self> {
match raw.as_bytes().first() {
Some(&b'S') => Some(Self::Safe),
Some(&b'C') => Some(Self::Caution),
Some(&b'U') => Some(Self::Unsafe),
Some(&b'V') => Some(Self::Invalid),
_ => None,
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Gns<'a> {
pub time: &'a str,
pub latitude: &'a str,
pub latitude_dir: &'a str,
pub longitude: &'a str,
pub longitude_dir: &'a str,
pub modes: &'a str,
pub satellite_count: &'a str,
pub hdop: &'a str,
pub altitude: &'a str,
pub geoidal_seperation: &'a str,
pub dgps_age: Option<&'a str>,
pub dgps_station_id: Option<&'a str>,
pub nav_status: Option<&'a str>,
}
impl<'a> NmeaParse<'a> for Gns<'a> {
fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
let mut f = fields.splitn(13, ',');
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)?,
modes: f.next().ok_or(NmeaMessageError::MissingField)?,
satellite_count: f.next().ok_or(NmeaMessageError::MissingField)?,
hdop: f.next().ok_or(NmeaMessageError::MissingField)?,
altitude: f.next().ok_or(NmeaMessageError::MissingField)?,
geoidal_seperation: 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()),
nav_status: f.next().filter(|s| !s.is_empty()),
})
}
}
impl NmeaEncode for Gns<'_> {
fn encoded_len(&self) -> usize {
self.time.len()
+ self.latitude.len()
+ self.latitude_dir.len()
+ self.longitude.len()
+ self.longitude_dir.len()
+ self.modes.len()
+ self.satellite_count.len()
+ self.hdop.len()
+ self.altitude.len()
+ self.geoidal_seperation.len()
+ self.dgps_age.map_or(0, str::len)
+ self.dgps_station_id.map_or(0, str::len)
+ self.nav_status.map_or(0, |s| s.len() + 1)
+ 11
}
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.modes);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.satellite_count);
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.geoidal_seperation);
write_byte!(buf, pos, b',');
if let Some(dgps_age) = self.dgps_age {
write_str!(buf, pos, dgps_age);
}
write_byte!(buf, pos, b',');
if let Some(dgps_station_id) = self.dgps_station_id {
write_str!(buf, pos, dgps_station_id);
}
if let Some(nav_status) = self.nav_status {
write_byte!(buf, pos, b',');
write_str!(buf, pos, nav_status);
}
pos
}
}
impl Gns<'_> {
#[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 mode(&self, id: GnsModeIdentifier) -> Option<FaaMode> {
let index: usize = id.into();
self.modes.get(index..=index).and_then(FaaMode::parse)
}
#[must_use]
pub fn satellite_count(&self) -> Option<u8> {
self.satellite_count.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 geoidal_seperation(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.geoidal_seperation)
}
#[must_use]
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(|d| d.parse::<u16>().ok())
}
#[must_use]
pub fn nav_status(&self) -> Option<GnsNavStatus> {
self.nav_status.and_then(GnsNavStatus::parse)
}
}
#[cfg(test)]
mod tests {
use super::*;
const FIELDS: &str = "092750.000,5321.6802,N,00630.3372,W,AAN,08,1.03,61.7,55.2,2.5,0023,S";
const FIELDS_MINIMAL: &str = "092750.000,5321.6802,N,00630.3372,W,AAN,08,1.03,61.7,55.2,,";
#[test]
fn parses_raw_fields() {
let gns = Gns::parse(FIELDS).unwrap();
assert_eq!(gns.time, "092750.000");
assert_eq!(gns.latitude, "5321.6802");
assert_eq!(gns.latitude_dir, "N");
assert_eq!(gns.longitude, "00630.3372");
assert_eq!(gns.longitude_dir, "W");
assert_eq!(gns.modes, "AAN");
assert_eq!(gns.satellite_count, "08");
assert_eq!(gns.hdop, "1.03");
assert_eq!(gns.altitude, "61.7");
assert_eq!(gns.geoidal_seperation, "55.2");
assert_eq!(gns.dgps_age, Some("2.5"));
assert_eq!(gns.dgps_station_id, Some("0023"));
assert_eq!(gns.nav_status, Some("S"));
}
#[test]
fn time_parsed() {
let gns = Gns::parse(FIELDS).unwrap();
assert_eq!(
gns.time(),
Some(NmeaTime {
hours: 9,
minutes: 27,
seconds: 50,
subseconds: 0,
})
);
}
#[test]
fn coordinates_parsed() {
let gns = Gns::parse(FIELDS).unwrap();
assert!(gns.coordinates().is_some());
}
#[test]
fn satellite_count_parsed() {
let gns = Gns::parse(FIELDS).unwrap();
assert_eq!(gns.satellite_count(), Some(8));
}
#[test]
fn hdop_parsed() {
let gns = Gns::parse(FIELDS).unwrap();
assert!(gns.hdop().is_some());
}
#[test]
fn altitude_parsed() {
let gns = Gns::parse(FIELDS).unwrap();
assert!(gns.altitude().is_some());
}
#[test]
fn geoidal_separation_parsed() {
let gns = Gns::parse(FIELDS).unwrap();
assert!(gns.geoidal_seperation().is_some());
}
#[test]
fn dgps_fields_present() {
let gns = Gns::parse(FIELDS).unwrap();
assert!(gns.dgps_age().is_some());
assert_eq!(gns.dgps_station_id(), Some(23));
}
#[test]
fn optional_dgps_fields_empty() {
let gns = Gns::parse(FIELDS_MINIMAL).unwrap();
assert!(gns.dgps_age().is_none());
assert!(gns.dgps_station_id().is_none());
}
#[test]
fn nav_status_safe() {
let gns = Gns::parse(FIELDS).unwrap();
assert!(matches!(gns.nav_status(), Some(GnsNavStatus::Safe)));
}
#[test]
fn nav_status_caution() {
let gns =
Gns::parse("092750.000,5321.6802,N,00630.3372,W,AAN,08,1.03,61.7,55.2,,,C").unwrap();
assert!(matches!(gns.nav_status(), Some(GnsNavStatus::Caution)));
}
#[test]
fn nav_status_unsafe() {
let gns =
Gns::parse("092750.000,5321.6802,N,00630.3372,W,AAN,08,1.03,61.7,55.2,,,U").unwrap();
assert!(matches!(gns.nav_status(), Some(GnsNavStatus::Unsafe)));
}
#[test]
fn nav_status_invalid() {
let gns =
Gns::parse("092750.000,5321.6802,N,00630.3372,W,AAN,08,1.03,61.7,55.2,,,V").unwrap();
assert!(matches!(gns.nav_status(), Some(GnsNavStatus::Invalid)));
}
#[test]
fn nav_status_absent_when_pre_4_10() {
let gns = Gns::parse(FIELDS_MINIMAL).unwrap();
assert!(gns.nav_status().is_none());
}
#[test]
fn mode_gps_parsed() {
let gns = Gns::parse(FIELDS).unwrap(); assert!(gns.mode(GnsModeIdentifier::Gps).is_some());
}
#[test]
fn mode_out_of_range_returns_none() {
let gns = Gns::parse("092750.000,5321.6802,N,00630.3372,W,A,08,1.03,61.7,55.2,,,").unwrap();
assert!(gns.mode(GnsModeIdentifier::Glonass).is_none());
}
#[test]
fn missing_required_field_returns_error() {
assert!(Gns::parse("092750.000,5321.6802,N").is_err());
}
#[test]
fn empty_optional_strings_become_none() {
let gns = Gns::parse(FIELDS_MINIMAL).unwrap();
assert!(gns.dgps_age.is_none());
assert!(gns.dgps_station_id.is_none());
assert!(gns.nav_status.is_none());
}
}