use crate::nmea::field::{FieldReader, FieldWriter};
#[derive(Debug, Clone, PartialEq)]
pub struct Rmb {
pub status: Option<char>,
pub ctrkerr: Option<f32>,
pub dirs: Option<char>,
pub wpt_origin: Option<String>,
pub wpt_dest: Option<String>,
pub dest_lat: Option<f32>,
pub ns: Option<char>,
pub dest_lon: Option<f32>,
pub ew: Option<char>,
pub range: Option<f32>,
pub bearing: Option<f32>,
pub velclos: Option<f32>,
pub arrstatus: Option<char>,
pub valstatus: Option<char>,
}
impl Rmb {
pub const SENTENCE_TYPE: &str = "RMB";
pub fn parse(fields: &[&str]) -> Option<Self> {
let mut r = FieldReader::new(fields);
Some(Self {
status: r.char(),
ctrkerr: r.f32(),
dirs: r.char(),
wpt_origin: r.string(),
wpt_dest: r.string(),
dest_lat: r.f32(),
ns: r.char(),
dest_lon: r.f32(),
ew: r.char(),
range: r.f32(),
bearing: r.f32(),
velclos: r.f32(),
arrstatus: r.char(),
valstatus: r.char(),
})
}
pub fn encode(&self) -> Vec<String> {
let mut w = FieldWriter::new();
w.char(self.status);
w.f32(self.ctrkerr);
w.char(self.dirs);
w.string(self.wpt_origin.as_deref());
w.string(self.wpt_dest.as_deref());
w.f32(self.dest_lat);
w.char(self.ns);
w.f32(self.dest_lon);
w.char(self.ew);
w.f32(self.range);
w.f32(self.bearing);
w.f32(self.velclos);
w.char(self.arrstatus);
w.char(self.valstatus);
w.finish()
}
pub fn to_sentence(&self, talker: &str) -> String {
let fields = self.encode();
let field_refs: Vec<&str> = fields.iter().map(|s| s.as_str()).collect();
crate::encode_frame('$', talker, Self::SENTENCE_TYPE, &field_refs)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_frame;
#[test]
fn rmb_empty() {
let f = parse_frame("$GPRMB,,,,,,,,,,,,,,*4A").expect("valid");
let r = Rmb::parse(&f.fields).expect("parse");
assert!(r.status.is_none());
assert!(r.ctrkerr.is_none());
assert!(r.dirs.is_none());
assert!(r.wpt_origin.is_none());
assert!(r.wpt_dest.is_none());
assert!(r.dest_lat.is_none());
assert!(r.range.is_none());
assert!(r.bearing.is_none());
assert!(r.velclos.is_none());
assert!(r.arrstatus.is_none());
assert!(r.valstatus.is_none());
}
#[test]
fn rmb_encode_roundtrip() {
let rmb = Rmb {
status: Some('A'),
ctrkerr: Some(0.5),
dirs: Some('L'),
wpt_origin: Some("001".to_string()),
wpt_dest: Some("002".to_string()),
dest_lat: Some(4653.55),
ns: Some('N'),
dest_lon: Some(7115.984),
ew: Some('W'),
range: Some(2.505),
bearing: Some(334.205),
velclos: Some(0.0),
arrstatus: Some('V'),
valstatus: Some('A'),
};
let sentence = rmb.to_sentence("EC");
assert!(sentence.starts_with("$ECRMB,"));
let frame = parse_frame(sentence.trim()).expect("re-parse");
let rmb2 = Rmb::parse(&frame.fields).expect("re-parse RMB");
assert_eq!(rmb.status, rmb2.status);
assert_eq!(rmb.wpt_dest, rmb2.wpt_dest);
assert_eq!(rmb.valstatus, rmb2.valstatus);
}
#[test]
fn rmb_full_signalk() {
let frame =
parse_frame("$ECRMB,A,0.000,L,001,002,4653.550,N,07115.984,W,2.505,334.205,0.000,V*04")
.expect("valid");
let rmb = Rmb::parse(&frame.fields).expect("parse RMB");
assert_eq!(rmb.status, Some('A'));
assert!((rmb.ctrkerr.expect("ctrkerr") - 0.0).abs() < 0.001);
assert_eq!(rmb.dirs, Some('L'));
assert_eq!(rmb.wpt_origin, Some("001".to_string()));
assert_eq!(rmb.wpt_dest, Some("002".to_string()));
assert!((rmb.dest_lat.expect("lat") - 4653.55).abs() < 0.01);
assert_eq!(rmb.ns, Some('N'));
assert!((rmb.dest_lon.expect("lon") - 7115.984).abs() < 0.01);
assert_eq!(rmb.ew, Some('W'));
assert!((rmb.range.expect("range") - 2.505).abs() < 0.01);
assert!((rmb.bearing.expect("bearing") - 334.205).abs() < 0.01);
assert!((rmb.velclos.expect("velclos") - 0.0).abs() < 0.001);
assert_eq!(rmb.arrstatus, Some('V'));
assert!(rmb.valstatus.is_none());
}
#[test]
fn rmb_pynmeagps() {
let frame =
parse_frame("$GPRMB,A,0.66,L,003,004,4917.24,N,12309.57,W,001.3,052.5,000.5,V*20")
.expect("valid pynmeagps RMB frame");
let rmb = Rmb::parse(&frame.fields).expect("parse RMB");
assert_eq!(rmb.status, Some('A'));
assert!((rmb.ctrkerr.expect("ctrkerr") - 0.66).abs() < 0.01);
assert_eq!(rmb.wpt_dest, Some("004".to_string()));
}
}