use crate::{
encoder::NmeaEncode,
macros::{write_byte, write_str},
message::NmeaMessageError,
parser::NmeaParse,
};
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct GsvSatelliteInfo<'a> {
pub prn: &'a str,
pub elevation: &'a str,
pub azimuth: &'a str,
pub snr: &'a str,
}
impl GsvSatelliteInfo<'_> {
#[must_use]
pub fn prn(&self) -> Option<u8> {
self.prn.parse().ok()
}
#[must_use]
pub fn elevation(&self) -> Option<u8> {
self.elevation.parse().ok()
}
#[must_use]
pub fn azimuth(&self) -> Option<u16> {
self.azimuth.parse().ok()
}
#[must_use]
pub fn snr(&self) -> Option<u8> {
self.snr.parse().ok()
}
}
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Gsv<'a> {
pub total_messages: &'a str,
pub message_number: &'a str,
pub satellites_in_view: &'a str,
pub satellites: [Option<GsvSatelliteInfo<'a>>; 4],
}
impl<'a> NmeaParse<'a> for Gsv<'a> {
fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
let mut f = fields.splitn(19, ',');
let total_messages = f.next().ok_or(NmeaMessageError::MissingField)?;
let message_number = f.next().ok_or(NmeaMessageError::MissingField)?;
let satellites_in_view = f.next().ok_or(NmeaMessageError::MissingField)?;
let mut satellites = [None, None, None, None];
for slot in &mut satellites {
match (f.next(), f.next(), f.next(), f.next()) {
(Some(prn), Some(elevation), Some(azimuth), Some(snr)) if !prn.is_empty() => {
*slot = Some(GsvSatelliteInfo {
prn,
elevation,
azimuth,
snr,
});
}
_ => break,
}
}
Ok(Self {
total_messages,
message_number,
satellites_in_view,
satellites,
})
}
}
impl NmeaEncode for Gsv<'_> {
fn encoded_len(&self) -> usize {
let sat_len: usize = self
.satellites
.iter()
.flatten()
.map(|s| s.prn.len() + s.elevation.len() + s.azimuth.len() + s.snr.len() + 4)
.sum();
self.total_messages.len()
+ self.message_number.len()
+ self.satellites_in_view.len()
+ sat_len
+ 2
}
fn encode(&self, buf: &mut [u8]) -> usize {
let mut pos = 0;
write_str!(buf, pos, self.total_messages);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.message_number);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.satellites_in_view);
for slot in self.satellites.iter().flatten() {
write_byte!(buf, pos, b',');
write_str!(buf, pos, slot.prn);
write_byte!(buf, pos, b',');
write_str!(buf, pos, slot.elevation);
write_byte!(buf, pos, b',');
write_str!(buf, pos, slot.azimuth);
write_byte!(buf, pos, b',');
write_str!(buf, pos, slot.snr);
}
pos
}
}
impl<'a> Gsv<'a> {
#[must_use]
pub fn total_messages(&self) -> Option<u8> {
self.total_messages.parse().ok()
}
#[must_use]
pub fn message_number(&self) -> Option<u8> {
self.message_number.parse().ok()
}
#[must_use]
pub fn satellites_in_view(&self) -> Option<u8> {
self.satellites_in_view.parse().ok()
}
#[must_use]
pub fn satellite(&self, index: usize) -> Option<&GsvSatelliteInfo<'a>> {
self.satellites.get(index)?.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{encoder::NmeaEncode, parser::NmeaParse};
fn gsv(fields: &str) -> Gsv<'_> {
Gsv::parse(fields).expect("valid parse")
}
#[test]
fn parse_single_satellite() {
let s = gsv("1,1,4,05,45,180,42");
assert_eq!(s.total_messages, "1");
assert_eq!(s.message_number, "1");
assert_eq!(s.satellites_in_view, "4");
let sat = s.satellites[0].as_ref().unwrap();
assert_eq!(sat.prn, "05");
assert_eq!(sat.elevation, "45");
assert_eq!(sat.azimuth, "180");
assert_eq!(sat.snr, "42");
assert!(s.satellites[1].is_none());
}
#[test]
fn parse_four_satellites() {
let s = gsv("2,1,8,05,45,180,42,10,30,090,38,15,20,270,35,20,10,045,30");
for i in 0..4 {
assert!(s.satellites[i].is_some());
}
assert_eq!(s.satellites[0].as_ref().unwrap().prn, "05");
assert_eq!(s.satellites[3].as_ref().unwrap().prn, "20");
}
#[test]
fn parse_no_satellites() {
let s = gsv("1,1,0");
assert_eq!(s.satellites_in_view, "0");
for i in 0..4 {
assert!(s.satellites[i].is_none());
}
}
#[test]
fn parse_missing_fields_returns_error() {
assert!(Gsv::parse("1").is_err());
assert!(Gsv::parse("1,1").is_err());
assert!(Gsv::parse("").is_err());
}
#[test]
fn parse_empty_prn_stops_satellite_parsing() {
let s = gsv("1,1,4,,45,180,42");
assert!(s.satellites[0].is_none());
}
fn roundtrip(input: &str) {
let s = gsv(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_no_satellites() {
roundtrip("1,1,0");
}
#[test]
fn encode_roundtrip_one_satellite() {
roundtrip("1,1,4,05,45,180,42");
}
#[test]
fn encode_roundtrip_four_satellites() {
roundtrip("2,1,8,05,45,180,42,10,30,090,38,15,20,270,35,20,10,045,30");
}
#[test]
fn total_messages_valid() {
assert_eq!(gsv("3,1,12,05,45,180,42").total_messages(), Some(3));
}
#[test]
fn total_messages_invalid_returns_none() {
assert!(gsv("X,1,0").total_messages().is_none());
assert!(gsv(",1,0").total_messages().is_none());
}
#[test]
fn message_number_valid() {
assert_eq!(gsv("3,2,12,05,45,180,42").message_number(), Some(2));
}
#[test]
fn satellites_in_view_valid() {
assert_eq!(gsv("1,1,7,05,45,180,42").satellites_in_view(), Some(7));
}
#[test]
fn satellites_in_view_invalid_returns_none() {
assert!(gsv("1,1,X").satellites_in_view().is_none());
assert!(gsv("1,1,").satellites_in_view().is_none());
}
#[test]
fn satellite_accessor_valid() {
let s = gsv("1,1,4,05,45,180,42");
assert!(s.satellite(0).is_some());
assert!(s.satellite(1).is_none());
}
#[test]
fn satellite_accessor_out_of_bounds_returns_none() {
let s = gsv("1,1,4,05,45,180,42");
assert!(s.satellite(4).is_none());
assert!(s.satellite(99).is_none());
}
#[test]
fn satellite_info_prn_valid() {
let s = gsv("1,1,4,05,45,180,42");
assert_eq!(s.satellite(0).unwrap().prn(), Some(5));
}
#[test]
fn satellite_info_elevation_valid() {
let s = gsv("1,1,4,05,45,180,42");
assert_eq!(s.satellite(0).unwrap().elevation(), Some(45));
}
#[test]
fn satellite_info_azimuth_valid() {
let s = gsv("1,1,4,05,45,180,42");
assert_eq!(s.satellite(0).unwrap().azimuth(), Some(180));
}
#[test]
fn satellite_info_snr_valid() {
let s = gsv("1,1,4,05,45,180,42");
assert_eq!(s.satellite(0).unwrap().snr(), Some(42));
}
#[test]
fn satellite_info_snr_empty_returns_none() {
let s = gsv("1,1,4,05,45,180,");
assert_eq!(s.satellite(0).unwrap().snr(), None);
}
#[test]
fn satellite_info_invalid_fields_return_none() {
let s = Gsv {
total_messages: "1",
message_number: "1",
satellites_in_view: "1",
satellites: [
Some(GsvSatelliteInfo {
prn: "XX",
elevation: "YY",
azimuth: "ZZZ",
snr: "WW",
}),
None,
None,
None,
],
};
assert_eq!(s.satellite(0).unwrap().prn(), None);
assert_eq!(s.satellite(0).unwrap().elevation(), None);
assert_eq!(s.satellite(0).unwrap().azimuth(), None);
assert_eq!(s.satellite(0).unwrap().snr(), None);
}
}