use crate::nmea::field::{FieldReader, FieldWriter, NmeaEncodable};
#[derive(Debug, Clone, PartialEq)]
pub struct SatInfo {
pub prn: Option<u32>,
pub elevation: Option<i8>,
pub azimuth: Option<u32>,
pub snr: Option<u8>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Gsv {
pub total_msgs: Option<u8>,
pub msg_num: Option<u8>,
pub sats_in_view: Option<u8>,
pub sats: Vec<SatInfo>,
pub signal_id: Option<u8>,
}
impl Gsv {
pub fn parse(fields: &[&str]) -> Option<Self> {
let mut r = FieldReader::new(fields);
let total_msgs = r.u8();
let msg_num = r.u8();
let sats_in_view = r.u8();
let remaining = fields.len().saturating_sub(3);
let num_groups = remaining / 4;
let has_signal_id = remaining % 4 == 1;
let mut sats = Vec::with_capacity(num_groups);
for _ in 0..num_groups {
sats.push(SatInfo {
prn: r.u32(),
elevation: r.i8(),
azimuth: r.u32(),
snr: r.u8(),
});
}
let signal_id = if has_signal_id { r.u8() } else { None };
Some(Self {
total_msgs,
msg_num,
sats_in_view,
sats,
signal_id,
})
}
}
impl NmeaEncodable for Gsv {
const SENTENCE_TYPE: &str = "GSV";
fn encode(&self) -> Vec<String> {
let mut w = FieldWriter::new();
w.u8(self.total_msgs);
w.u8(self.msg_num);
w.u8(self.sats_in_view);
for sat in &self.sats {
w.u32(sat.prn);
w.i8(sat.elevation);
w.u32(sat.azimuth);
w.u8(sat.snr);
}
if self.signal_id.is_some() {
w.u8(self.signal_id);
}
w.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_frame;
#[test]
fn gsv_empty() {
let f = Gsv {
total_msgs: None,
msg_num: None,
sats_in_view: None,
sats: vec![],
signal_id: None,
}
.to_sentence("GP");
let frame = parse_frame(f.trim()).expect("valid");
let g = Gsv::parse(&frame.fields).expect("parse");
assert!(g.total_msgs.is_none());
assert!(g.msg_num.is_none());
assert!(g.sats_in_view.is_none());
assert!(g.sats.is_empty());
}
#[test]
fn gsv_encode_roundtrip() {
let original = Gsv {
total_msgs: Some(3),
msg_num: Some(1),
sats_in_view: Some(9),
sats: vec![
SatInfo {
prn: Some(9),
elevation: Some(73),
azimuth: Some(246),
snr: Some(35),
},
SatInfo {
prn: Some(2),
elevation: Some(51),
azimuth: Some(60),
snr: Some(40),
},
SatInfo {
prn: Some(6),
elevation: Some(16),
azimuth: Some(58),
snr: Some(37),
},
SatInfo {
prn: Some(7),
elevation: Some(16),
azimuth: Some(35),
snr: Some(25),
},
],
signal_id: None,
};
let sentence = original.to_sentence("GP");
let frame = parse_frame(sentence.trim()).expect("re-parse");
let parsed = Gsv::parse(&frame.fields).expect("re-parse GSV");
assert_eq!(original, parsed);
}
#[test]
fn gsv_glonass_gonmea() {
let frame =
parse_frame("$GLGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,12,13,06,292,00*6B")
.expect("valid go-nmea GLONASS GSV frame");
let gsv = Gsv::parse(&frame.fields).expect("parse GSV");
assert_eq!(gsv.total_msgs, Some(3));
assert_eq!(gsv.msg_num, Some(1));
assert_eq!(gsv.sats_in_view, Some(11));
assert_eq!(gsv.sats.len(), 4);
assert_eq!(gsv.sats[0].prn, Some(3));
assert_eq!(gsv.sats[0].elevation, Some(3));
assert_eq!(gsv.sats[0].azimuth, Some(111));
assert_eq!(gsv.sats[0].snr, Some(0));
assert_eq!(gsv.sats[1].prn, Some(4));
assert_eq!(gsv.sats[3].prn, Some(13));
assert!(gsv.signal_id.is_none());
}
#[test]
fn gsv_gpsd() {
let frame =
parse_frame("$GPGSV,3,1,09,09,73,246,35,02,51,060,40,06,16,058,37,07,16,291,25*78")
.expect("valid gpsd GSV frame");
let gsv = Gsv::parse(&frame.fields).expect("parse GSV");
assert_eq!(gsv.total_msgs, Some(3));
assert_eq!(gsv.msg_num, Some(1));
assert_eq!(gsv.sats_in_view, Some(9));
assert_eq!(gsv.sats.len(), 4);
assert_eq!(gsv.sats[0].prn, Some(9));
assert_eq!(gsv.sats[0].elevation, Some(73));
assert_eq!(gsv.sats[0].azimuth, Some(246));
assert_eq!(gsv.sats[0].snr, Some(35));
assert_eq!(gsv.sats[1].prn, Some(2));
assert_eq!(gsv.sats[2].prn, Some(6));
assert_eq!(gsv.sats[3].prn, Some(7));
assert!(gsv.signal_id.is_none());
}
}