use crate::{
coordinates::NmeaCoordinates,
encoder::NmeaEncode,
faa::FaaMode,
macros::{write_byte, write_str},
message::NmeaMessageError,
number::NmeaNumber,
parser::NmeaParse,
time::{NmeaDate, NmeaTime},
};
#[repr(u8)]
pub enum RmcNavStatus {
Autonomous = b'A',
Differential = b'D',
Estimated = b'E',
Manual = b'M',
NotValid = b'N',
Simulator = b'S',
Valid = b'V',
}
impl RmcNavStatus {
#[must_use]
pub fn parse(raw: &str) -> Option<Self> {
match raw.as_bytes().first() {
Some(&b'A') => Some(Self::Autonomous),
Some(&b'D') => Some(Self::Differential),
Some(&b'E') => Some(Self::Estimated),
Some(&b'M') => Some(Self::Manual),
Some(&b'S') => Some(Self::Simulator),
_ => None,
}
}
}
#[derive(Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Rmc<'a> {
pub time: &'a str,
pub status: &'a str,
pub latitude: &'a str,
pub latitude_dir: &'a str,
pub longitude: &'a str,
pub longitude_dir: &'a str,
pub speed: &'a str,
pub track_made: &'a str,
pub date: &'a str,
pub magnetic_variation: &'a str,
pub magnetic_variation_dir: &'a str,
pub faa_mode: Option<&'a str>,
pub nav_status: Option<&'a str>,
}
impl<'a> NmeaParse<'a> for Rmc<'a> {
fn parse(fields: &'a str) -> Result<Self, NmeaMessageError> {
let mut f = fields.splitn(13, ',');
Ok(Self {
time: f.next().ok_or(NmeaMessageError::MissingField)?,
status: 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)?,
speed: f.next().ok_or(NmeaMessageError::MissingField)?,
track_made: f.next().ok_or(NmeaMessageError::MissingField)?,
date: f.next().ok_or(NmeaMessageError::MissingField)?,
magnetic_variation: f.next().ok_or(NmeaMessageError::MissingField)?,
magnetic_variation_dir: f.next().ok_or(NmeaMessageError::MissingField)?,
faa_mode: f.next().filter(|s| !s.is_empty()),
nav_status: f.next().filter(|s| !s.is_empty()),
})
}
}
impl NmeaEncode for Rmc<'_> {
fn encoded_len(&self) -> usize {
self.time.len()
+ self.status.len()
+ self.latitude.len()
+ self.latitude_dir.len()
+ self.longitude.len()
+ self.longitude_dir.len()
+ self.speed.len()
+ self.track_made.len()
+ self.date.len()
+ self.magnetic_variation.len()
+ self.magnetic_variation_dir.len()
+ self.faa_mode.map_or(0, |s| s.len() + 1)
+ self.nav_status.map_or(0, |s| s.len() + 1)
+ 10
}
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.status);
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.speed);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.track_made);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.date);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.magnetic_variation);
write_byte!(buf, pos, b',');
write_str!(buf, pos, self.magnetic_variation_dir);
if let Some(faa_mode) = self.faa_mode {
write_byte!(buf, pos, b',');
write_str!(buf, pos, faa_mode);
}
if let Some(nav_status) = self.nav_status {
write_byte!(buf, pos, b',');
write_str!(buf, pos, nav_status);
}
pos
}
}
impl Rmc<'_> {
#[must_use]
pub fn time(&self) -> Option<NmeaTime> {
NmeaTime::parse(self.time)
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.status == "A"
}
#[must_use]
pub fn coordinates(&self) -> Option<NmeaCoordinates> {
NmeaCoordinates::parse(
self.latitude,
self.latitude_dir,
self.longitude,
self.longitude_dir,
)
}
#[must_use]
pub fn speed(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.speed)
}
#[must_use]
pub fn track_made(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.track_made)
}
#[must_use]
pub fn date(&self) -> Option<NmeaDate> {
let raw = self.date;
if raw.len() < 6 {
return None;
}
let day: u8 = raw[0..2].parse().ok()?;
if !(1..=31).contains(&day) {
return None;
}
let month: u8 = raw[2..4].parse().ok()?;
if !(1..=12).contains(&month) {
return None;
}
let year: u16 = raw[4..6].parse::<u16>().ok()? + 2000;
Some(NmeaDate { year, month, day })
}
#[must_use]
pub fn magnetic_variation(&self) -> Option<NmeaNumber> {
NmeaNumber::parse(self.magnetic_variation)
}
pub fn faa_mode(&self) -> Option<FaaMode> {
self.faa_mode.and_then(FaaMode::parse)
}
pub fn nav_status(&self) -> Option<RmcNavStatus> {
self.nav_status.and_then(RmcNavStatus::parse)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{encoder::NmeaEncode, parser::NmeaParse};
fn rmc(fields: &str) -> Rmc<'_> {
Rmc::parse(fields).expect("valid parse")
}
const BASE: &str = "123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W";
#[test]
fn parse_typical() {
let s = rmc(BASE);
assert_eq!(s.time, "123519.00");
assert_eq!(s.status, "A");
assert_eq!(s.latitude, "4807.038");
assert_eq!(s.latitude_dir, "N");
assert_eq!(s.longitude, "01131.000");
assert_eq!(s.longitude_dir, "E");
assert_eq!(s.speed, "022.4");
assert_eq!(s.track_made, "084.4");
assert_eq!(s.date, "230394");
assert_eq!(s.magnetic_variation, "003.1");
assert_eq!(s.magnetic_variation_dir, "W");
assert_eq!(s.faa_mode, None);
assert_eq!(s.nav_status, None);
}
#[test]
fn parse_with_faa_mode() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,A");
assert_eq!(s.faa_mode, Some("A"));
assert_eq!(s.nav_status, None);
}
#[test]
fn parse_with_faa_mode_and_nav_status() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,A,V");
assert_eq!(s.faa_mode, Some("A"));
assert_eq!(s.nav_status, Some("V"));
}
#[test]
fn parse_empty_faa_mode_becomes_none() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,");
assert_eq!(s.faa_mode, None);
}
#[test]
fn parse_missing_fields_returns_error() {
assert!(Rmc::parse("123519.00,A").is_err());
assert!(Rmc::parse("123519.00").is_err());
assert!(Rmc::parse("").is_err());
}
fn roundtrip(input: &str) {
let s = rmc(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_optional_fields() {
roundtrip(BASE);
}
#[test]
fn encode_roundtrip_with_faa_mode() {
roundtrip("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,A");
}
#[test]
fn encode_roundtrip_with_faa_and_nav_status() {
roundtrip("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,A,V");
}
#[test]
fn is_valid_true_when_status_a() {
assert!(rmc(BASE).is_valid());
}
#[test]
fn is_valid_false_when_status_v() {
let s = rmc("123519.00,V,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W");
assert!(!s.is_valid());
}
#[test]
fn time_valid() {
let t = rmc(BASE).time().unwrap();
assert_eq!(t.hours, 12);
assert_eq!(t.minutes, 35);
assert_eq!(t.seconds, 19);
}
#[test]
fn time_invalid_returns_none() {
assert!(
rmc("BADTIME,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W")
.time()
.is_none()
);
}
#[test]
fn date_valid() {
let d = rmc(BASE).date().unwrap();
assert_eq!(d.day, 23);
assert_eq!(d.month, 3);
assert_eq!(d.year, 2094);
}
#[test]
fn date_too_short_returns_none() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,2303,003.1,W");
assert!(s.date().is_none());
}
#[test]
fn date_invalid_day_returns_none() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,001294,003.1,W");
assert!(s.date().is_none());
}
#[test]
fn date_invalid_month_returns_none() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,231394,003.1,W");
assert!(s.date().is_none());
}
#[test]
fn speed_valid() {
let v = rmc(BASE).speed().unwrap();
assert_eq!(v.value, 224);
assert_eq!(v.scale, 1);
}
#[test]
fn speed_empty_returns_none() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,,084.4,230394,003.1,W");
assert!(s.speed().is_none());
}
#[test]
fn track_made_valid() {
let v = rmc(BASE).track_made().unwrap();
assert_eq!(v.value, 844);
assert_eq!(v.scale, 1);
}
#[test]
fn magnetic_variation_valid() {
let v = rmc(BASE).magnetic_variation().unwrap();
assert_eq!(v.value, 31);
assert_eq!(v.scale, 1);
}
#[test]
fn magnetic_variation_empty_returns_none() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,,W");
assert!(s.magnetic_variation().is_none());
}
#[test]
fn faa_mode_autonomous() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,A");
assert!(matches!(s.faa_mode(), Some(FaaMode::Autonomous)));
}
#[test]
fn faa_mode_none_when_absent() {
assert!(rmc(BASE).faa_mode().is_none());
}
#[test]
fn nav_status_valid() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,A,S");
assert!(matches!(s.nav_status(), Some(RmcNavStatus::Simulator)));
}
#[test]
fn nav_status_none_when_absent() {
assert!(rmc(BASE).nav_status().is_none());
}
#[test]
fn nav_status_invalid_returns_none() {
let s = rmc("123519.00,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W,A,X");
assert!(s.nav_status().is_none());
}
}