use crate::{
encoder::NmeaEncode,
macros::{write_byte, write_str},
message::NmeaMessageError,
number::NmeaNumber,
parser::NmeaParse,
};
#[repr(u8)]
pub enum GsaMode {
Manual = b'M',
Automatic = b'A',
}
impl GsaMode {
#[must_use]
pub fn parse(raw: &str) -> Option<Self> {
match raw.as_bytes().first() {
Some(&b'M') => Some(Self::Manual),
Some(&b'A') => Some(Self::Automatic),
_ => None,
}
}
}
#[repr(u8)]
pub enum GsaFix {
NotAvailable = 1,
TwoDimensional = 2,
ThreeDimensional = 3,
}
impl GsaFix {
#[must_use]
pub fn parse(raw: u8) -> Option<Self> {
match raw {
1 => Some(Self::NotAvailable),
2 => Some(Self::TwoDimensional),
3 => Some(Self::ThreeDimensional),
_ => None,
}
}
}
#[repr(u8)]
pub enum GsaSystemId {
Gps = 1,
Glonass = 2,
Galileo = 3,
BeiDou = 4,
Qzss = 5,
}
impl GsaSystemId {
#[must_use]
pub fn parse(raw: u8) -> Option<Self> {
match raw {
1 => Some(Self::Gps),
2 => Some(Self::Glonass),
3 => Some(Self::Galileo),
4 => Some(Self::BeiDou),
5 => Some(Self::Qzss),
_ => None,
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Gsa<'a> {
pub mode: &'a str,
pub fix: &'a str,
pub prn: [Option<&'a str>; 12],
pub pdop: &'a str,
pub hdop: &'a str,
pub vdop: &'a str,
pub system_id: Option<&'a str>,
}
impl<'a> NmeaParse<'a> for Gsa<'a> {
fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
let mut f = fields.splitn(18, ',');
let mode = f.next().ok_or(NmeaMessageError::MissingField)?;
let fix = f.next().ok_or(NmeaMessageError::MissingField)?;
let prn = core::array::from_fn(|_| f.next().filter(|s| !s.is_empty()));
let pdop = f.next().ok_or(NmeaMessageError::MissingField)?;
let hdop = f.next().ok_or(NmeaMessageError::MissingField)?;
let vdop = f.next().ok_or(NmeaMessageError::MissingField)?;
let system_id = f.next().filter(|s| !s.is_empty());
Ok(Self {
mode,
fix,
prn,
pdop,
hdop,
vdop,
system_id,
})
}
}
impl NmeaEncode for Gsa<'_> {
fn encoded_len(&self) -> usize {
let prn_len: usize = self.prn.iter().flatten().map(|s| s.len()).sum();
self.mode.len()
+ self.fix.len()
+ prn_len
+ 11
+ self.pdop.len()
+ self.hdop.len()
+ self.vdop.len()
+ self.system_id.map_or(0, |s| s.len() + 1)
+ 5
}
fn encode(&self, buf: &mut [u8]) -> usize {
let mut pos = 0;
write_str!(buf, pos, self.mode);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.fix);
write_byte!(buf, pos, b',');
for prn in &self.prn {
match prn {
Some(value) => {
write_str!(buf, pos, *value);
write_byte!(buf, pos, b',');
}
None => {
write_byte!(buf, pos, b',');
}
}
}
write_str!(buf, pos, self.pdop);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.hdop);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.vdop);
if let Some(system_id) = self.system_id {
write_byte!(buf, pos, b',');
write_str!(buf, pos, system_id);
}
pos
}
}
impl Gsa<'_> {
#[must_use]
pub fn mode(&self) -> Option<GsaMode> {
GsaMode::parse(self.mode)
}
#[must_use]
pub fn fix(&self) -> Option<GsaFix> {
GsaFix::parse(self.fix.parse().ok()?)
}
#[must_use]
pub fn prn(&self, index: usize) -> Option<u8> {
self.prn.get(index)?.as_ref().and_then(|p| p.parse().ok())
}
#[must_use]
pub fn pdop(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.pdop)
}
#[must_use]
pub fn hdop(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.hdop)
}
#[must_use]
pub fn vdop(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.vdop)
}
#[must_use]
pub fn system_id(&self) -> Option<GsaSystemId> {
self.system_id
.and_then(|s| GsaSystemId::parse(s.parse().ok()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{encoder::NmeaEncode, parser::NmeaParse};
fn gsa(fields: &str) -> Gsa<'_> {
Gsa::parse(fields).expect("valid parse")
}
#[test]
fn parse_typical_no_system_id() {
let s = gsa("A,3,05,01,17,15,,,,,,,,,1.83,1.09,1.47");
assert_eq!(s.mode, "A");
assert_eq!(s.fix, "3");
assert_eq!(s.prn[0], Some("05"));
assert_eq!(s.prn[1], Some("01"));
assert_eq!(s.prn[2], Some("17"));
assert_eq!(s.prn[3], Some("15"));
assert_eq!(s.prn[4], None);
assert_eq!(s.prn[11], None);
assert_eq!(s.pdop, "1.83");
assert_eq!(s.hdop, "1.09");
assert_eq!(s.vdop, "1.47");
assert_eq!(s.system_id, None);
}
#[test]
fn parse_with_system_id() {
let s = gsa("A,3,05,01,17,15,,,,,,,,,1.83,1.09,1.47,1");
assert_eq!(s.system_id, Some("1"));
}
#[test]
fn parse_all_prn_slots_filled() {
let s = gsa("A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.0,1.0,1.0");
for i in 0..12 {
assert!(s.prn[i].is_some());
}
}
#[test]
fn parse_all_prn_slots_empty() {
let s = gsa("A,3,,,,,,,,,,,,,,1.0,1.0,1.0");
for i in 0..12 {
assert_eq!(s.prn[i], None);
}
}
#[test]
fn parse_missing_fields_returns_error() {
assert!(Gsa::parse("A,3").is_err());
assert!(Gsa::parse("A").is_err());
assert!(Gsa::parse("").is_err());
}
fn roundtrip(input: &str) {
let s = gsa(input);
let len = s.encoded_len();
let mut buf = [0u8; 256];
let written = s.encode(&mut buf);
assert_eq!(
written, len,
"encode() returned {written} but encoded_len() said {len}"
);
assert_eq!(&buf[..written], input.as_bytes());
}
#[test]
fn encode_roundtrip_typical() {
roundtrip("A,3,05,01,17,15,,,,,,,,,1.83,1.09,1.47");
}
#[test]
fn encode_roundtrip_with_system_id() {
roundtrip("A,3,05,01,17,15,,,,,,,,,1.83,1.09,1.47,1");
}
#[test]
fn encode_roundtrip_all_prn_empty() {
roundtrip("A,3,,,,,,,,,,,,,,1.0,1.0,1.0");
}
#[test]
fn encoded_len_no_system_id() {
let s = gsa("A,3,,,,,,,,,,,,,,1.0,1.0,1.0");
let mut buf = [0u8; 256];
let written = s.encode(&mut buf);
assert_eq!(written, s.encoded_len());
}
#[test]
fn mode_automatic() {
assert!(matches!(
gsa("A,3,,,,,,,,,,,,,,1.0,1.0,1.0").mode(),
Some(GsaMode::Automatic)
));
}
#[test]
fn mode_manual() {
assert!(matches!(
gsa("M,3,,,,,,,,,,,,,,1.0,1.0,1.0").mode(),
Some(GsaMode::Manual)
));
}
#[test]
fn mode_invalid_returns_none() {
assert!(gsa("X,3,,,,,,,,,,,,,,1.0,1.0,1.0").mode().is_none());
assert!(gsa(",3,,,,,,,,,,,,,,1.0,1.0,1.0").mode().is_none());
}
#[test]
fn fix_not_available() {
assert!(matches!(
gsa("A,1,,,,,,,,,,,,,,1.0,1.0,1.0").fix(),
Some(GsaFix::NotAvailable)
));
}
#[test]
fn fix_two_dimensional() {
assert!(matches!(
gsa("A,2,,,,,,,,,,,,,,1.0,1.0,1.0").fix(),
Some(GsaFix::TwoDimensional)
));
}
#[test]
fn fix_three_dimensional() {
assert!(matches!(
gsa("A,3,,,,,,,,,,,,,,1.0,1.0,1.0").fix(),
Some(GsaFix::ThreeDimensional)
));
}
#[test]
fn fix_invalid_returns_none() {
assert!(gsa("A,0,,,,,,,,,,,,,,1.0,1.0,1.0").fix().is_none());
assert!(gsa("A,4,,,,,,,,,,,,,,1.0,1.0,1.0").fix().is_none());
assert!(gsa("A,,,,,,,,,,,,,,,1.0,1.0,1.0").fix().is_none());
}
#[test]
fn prn_accessor_valid() {
let s = gsa("A,3,05,12,,,,,,,,,,,1.0,1.0,1.0");
assert_eq!(s.prn(0), Some(5));
assert_eq!(s.prn(1), Some(12));
}
#[test]
fn prn_accessor_empty_slot_returns_none() {
let s = gsa("A,3,05,,,,,,,,,,,,1.0,1.0,1.0");
assert_eq!(s.prn(1), None);
}
#[test]
fn prn_accessor_out_of_bounds_returns_none() {
let s = gsa("A,3,05,,,,,,,,,,,,1.0,1.0,1.0");
assert_eq!(s.prn(12), None);
assert_eq!(s.prn(99), None);
}
#[test]
fn prn_accessor_non_numeric_returns_none() {
let s = Gsa {
mode: "A",
fix: "3",
prn: [
Some("XX"),
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
None,
],
pdop: "1.0",
hdop: "1.0",
vdop: "1.0",
system_id: None,
};
assert_eq!(s.prn(0), None);
}
#[test]
fn pdop_valid() {
let v = gsa("A,3,,,,,,,,,,,,,1.83,1.09,1.47").pdop().unwrap();
assert_eq!(v.value, 183);
assert_eq!(v.scale, 2);
}
#[test]
fn hdop_valid() {
let v = gsa("A,3,,,,,,,,,,,,,1.83,1.09,1.47").hdop().unwrap();
assert_eq!(v.value, 109);
assert_eq!(v.scale, 2);
}
#[test]
fn vdop_valid() {
let v = gsa("A,3,,,,,,,,,,,,,1.83,1.09,1.47").vdop().unwrap();
assert_eq!(v.value, 147);
assert_eq!(v.scale, 2);
}
#[test]
fn dop_empty_returns_none() {
let s = gsa("A,3,,,,,,,,,,,,,,,,");
assert!(s.pdop().is_none());
assert!(s.hdop().is_none());
}
#[test]
fn system_id_none_when_absent() {
assert!(gsa("A,3,,,,,,,,,,,,,,1.0,1.0,1.0").system_id().is_none());
}
#[test]
fn system_id_gps() {
assert!(matches!(
gsa("A,3,,,,,,,,,,,,,1.0,1.0,1.0,1").system_id(),
Some(GsaSystemId::Gps)
));
}
#[test]
fn system_id_invalid_returns_none() {
assert!(gsa("A,3,,,,,,,,,,,,,1.0,1.0,1.0,9").system_id().is_none());
assert!(gsa("A,3,,,,,,,,,,,,,1.0,1.0,1.0,X").system_id().is_none());
}
}