diff --git a/Cargo.toml b/Cargo.toml index ce76b61..7ee8934 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,8 @@ all-sentences = ["GNSS", "waypoint", "maritime", "water", "vendor-specific", "ot GNSS = ["APA", "ALM", "GBS", "GGA", "GLL", "GNS", "GSA", "GST", "GSV", "RMC", "VTG"] waypoint = ["AAM", "BOD", "BWC", "BWW", "WNC", "ZFO", "ZTG"] -maritime = ["waypoint", "water"] +maritime = ["waypoint", "water", "radar"] +radar = ["TTM"] water = ["DBK", "MTW", "VHW"] vendor-specific = ["RMZ"] other = ["HDT", "MDA", "MWV", "TXT", "ZDA"] @@ -152,6 +153,9 @@ RMC = [] # feature: vendor-specific RMZ = [] +# TTM - Tracked target message +TTM = [] + # TXT - Text message TXT = [] diff --git a/README.md b/README.md index 80e38a0..3d33fd0 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ NMEA Standard Sentences - MTW - MWV - RMC * +- TTM - VHW - VTG * - WNC diff --git a/src/lib.rs b/src/lib.rs index 57f84cd..dbc9636 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ //! - MTW //! - MWV //! - RMC * +//! - TTM //! - VHW //! - VTG * //! - WNC diff --git a/src/parse.rs b/src/parse.rs index 6a009cc..741102a 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -126,6 +126,7 @@ pub enum ParseResult { MTW(MtwData), MWV(MwvData), RMC(RmcData), + TTM(TtmData), TXT(TxtData), VHW(VhwData), VTG(VtgData), @@ -160,6 +161,7 @@ impl From<&ParseResult> for SentenceType { ParseResult::MTW(_) => SentenceType::MTW, ParseResult::MWV(_) => SentenceType::MWV, ParseResult::RMC(_) => SentenceType::RMC, + ParseResult::TTM(_) => SentenceType::TTM, ParseResult::TXT(_) => SentenceType::TXT, ParseResult::VHW(_) => SentenceType::VHW, ParseResult::VTG(_) => SentenceType::VTG, @@ -383,6 +385,15 @@ pub fn parse_str(sentence_input: &str) -> Result { } } } + SentenceType::TTM => { + cfg_if! { + if #[cfg(feature = "TTM")] { + parse_ttm(nmea_sentence).map(ParseResult::TTM) + } else { + return Err(Error::DisabledSentence); + } + } + } SentenceType::TXT => { cfg_if! { if #[cfg(feature = "TXT")] { diff --git a/src/parser.rs b/src/parser.rs index 3571e9c..4350f3b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -384,6 +384,7 @@ impl<'a> Nmea { | ParseResult::MWV(_) | ParseResult::MDA(_) | ParseResult::VHW(_) + | ParseResult::TTM(_) | ParseResult::ZDA(_) | ParseResult::ZFO(_) | ParseResult::WNC(_) diff --git a/src/sentences/mod.rs b/src/sentences/mod.rs index f0ae50b..ac8246a 100644 --- a/src/sentences/mod.rs +++ b/src/sentences/mod.rs @@ -20,6 +20,7 @@ pub mod mtw; pub mod mwv; pub mod rmc; pub mod rmz; +pub mod ttm; pub mod txt; pub mod utils; pub mod vhw; @@ -58,6 +59,10 @@ pub use { mwv::{parse_mwv, MwvData}, rmc::{parse_rmc, RmcData}, rmz::{parse_pgrmz, PgrmzData}, + ttm::{ + parse_ttm, TtmAngle, TtmData, TtmDistanceUnit, TtmReference, TtmStatus, + TtmTypeOfAcquisition, + }, txt::{parse_txt, TxtData}, vhw::{parse_vhw, VhwData}, vtg::{parse_vtg, VtgData}, diff --git a/src/sentences/ttm.rs b/src/sentences/ttm.rs new file mode 100644 index 0000000..b61a6f0 --- /dev/null +++ b/src/sentences/ttm.rs @@ -0,0 +1,321 @@ +use chrono::NaiveTime; +use nom::{ + bytes::complete::take_until, + character::complete::{char, one_of}, + combinator::{map_res, opt}, + error::ErrorKind, + IResult, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use super::utils::{parse_float_num, parse_hms, parse_number_in_range}; +use crate::{Error, NmeaSentence, SentenceType}; + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TtmReference { + Relative, + Theoretical, +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct TtmAngle { + angle: f32, + reference: TtmReference, +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TtmDistanceUnit { + Kilometer, + NauticalMile, + StatuteMile, +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TtmStatus { + /// Tracked target has been lost + Lost, + /// Target in the process of acquisition + Query, + /// Target is being tracked + Tracking, +} + +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TtmTypeOfAcquisition { + Automatic, + Manual, + Reported, +} + +/// TTM - Tracked Target Message +/// +/// +/// +/// ```text +/// 11 13 16 +/// 1 2 3 4 5 6 7 8 9 10| 12| 14 15 | +/// | | | | | | | | | | | | | | | | +/// $--TTM,xx,x.x,x.x,a,x.x,x.x,a,x.x,x.x,a,c--c,a,a,hhmmss.ss,a*hh +/// ``` +/// 1. Target Number (0-99) +/// 2. Target Distance +/// 3. Bearing from own ship +/// 4. T = True, R = Relative +/// 5. Target Speed +/// 6. Target Course +/// 7. T = True, R = Relative +/// 8. Distance of closest-point-of-approach +/// 9. Time until closest-point-of-approach "-" means increasing +/// 10. Speed/distance units, K/N +/// 11. Target name +/// 12. Target Status +/// 13. Reference Target +/// 14. UTC of data (NMEA 3 and above) hh is hours, mm is minutes, ss.ss is seconds. +/// 15. Type, A = Auto, M = Manual, R = Reported (NMEA 3 and above) +/// 16. Checksum +/// +/// +/// Example: +/// ```text +/// $RATTM,01,0.2,190.8,T,12.1,109.7,T,0.1,0.5,N,TGT01,T,,100021.00,A*79 +/// ``` +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "defmt-03", derive(defmt::Format))] +#[derive(Debug, PartialEq)] +pub struct TtmData { + /// Target number + pub target_number: Option, + /// Target distance + pub target_distance: Option, + /// Bearing from own ship + pub bearing_from_own_ship: Option, + /// Target speed + pub target_speed: Option, + /// Target course + pub target_course: Option, + /// Distance of closest-point-of-approach + pub distance_of_cpa: Option, + /// Time to closest-point-of-approach + pub time_to_cpa: Option, + /// Unit used for speed and distance + pub speed_or_distance_unit: Option, + /// Target name + pub target_name: Option>, + /// Target status + pub target_status: Option, + /// Set to true if target is a reference used to determine own-ship position or velocity + pub is_target_reference: bool, + /// Time of data + pub time_of_data: Option, + /// Type of acquisition + pub type_of_acquisition: Option, +} + +/// # Parse TTM message +pub fn parse_ttm(sentence: NmeaSentence) -> Result { + if sentence.message_id != SentenceType::TTM { + Err(Error::WrongSentenceHeader { + expected: SentenceType::TTM, + found: sentence.message_id, + }) + } else { + Ok(do_parse_ttm(sentence.data)?.1) + } +} + +fn do_parse_ttm(i: &str) -> IResult<&str, TtmData> { + let (i, target_number) = opt(|i| parse_number_in_range::(i, 0, 99))(i)?; + let (i, _) = char(',')(i)?; + + let (i, target_distance) = opt(map_res(take_until(","), parse_float_num::))(i)?; + let (i, _) = char(',')(i)?; + + let (i, bearing_from_own_ship) = parse_ttm_angle(i)?; + let (i, _) = char(',')(i)?; + + let (i, target_speed) = opt(map_res(take_until(","), parse_float_num::))(i)?; + let (i, _) = char(',')(i)?; + + let (i, target_course) = parse_ttm_angle(i)?; + let (i, _) = char(',')(i)?; + + let (i, distance_of_cpa) = opt(map_res(take_until(","), parse_float_num::))(i)?; + let (i, _) = char(',')(i)?; + + let (i, time_to_cpa) = opt(map_res(take_until(","), parse_float_num::))(i)?; + let (i, _) = char(',')(i)?; + + let (i, unit_char) = opt(one_of("KNS"))(i)?; + let (i, _) = char(',')(i)?; + let unit = unit_char.map(|unit| match unit { + 'K' => TtmDistanceUnit::Kilometer, + 'N' => TtmDistanceUnit::NauticalMile, + 'S' => TtmDistanceUnit::StatuteMile, + _ => unreachable!(), + }); + + let (i, target_name) = take_until(",")(i)?; + let (i, _) = char(',')(i)?; + let target_name = if target_name.is_empty() { + None + } else { + Some(heapless::String::try_from(target_name).map_err(|_| { + nom::Err::Failure(nom::error::Error { + input: i, + code: ErrorKind::Fail, + }) + })?) + }; + + let (i, target_status_char) = opt(one_of("LQT"))(i)?; + let (i, _) = char(',')(i)?; + let target_status = target_status_char.map(|char| match char { + 'L' => TtmStatus::Lost, + 'Q' => TtmStatus::Query, + 'T' => TtmStatus::Tracking, + _ => unreachable!(), + }); + + let (i, is_target_reference_char) = opt(one_of("R"))(i)?; + let (i, _) = char(',')(i)?; + let is_target_reference = is_target_reference_char.is_some(); + + let (i, time_of_data) = opt(parse_hms)(i)?; + let (i, _) = char(',')(i)?; + + let (i, type_of_acquisition_char) = opt(one_of("AMR"))(i)?; + let type_of_acquisition = type_of_acquisition_char.map(|char| match char { + 'A' => TtmTypeOfAcquisition::Automatic, + 'M' => TtmTypeOfAcquisition::Manual, + 'R' => TtmTypeOfAcquisition::Reported, + _ => unreachable!(), + }); + + Ok(( + i, + TtmData { + target_number, + target_distance, + bearing_from_own_ship, + target_speed, + target_course, + distance_of_cpa, + time_to_cpa, + speed_or_distance_unit: unit, + target_name, + target_status, + is_target_reference, + time_of_data, + type_of_acquisition, + }, + )) +} + +fn parse_ttm_angle(i: &str) -> IResult<&str, Option> { + let (i, angle) = opt(map_res(take_until(","), parse_float_num::))(i)?; + let (i, _) = char(',')(i)?; + + let (i, reference) = opt(one_of("RT"))(i)?; + + Ok(( + i, + angle.and_then(|angle| { + reference.map(|reference_char| { + let reference = match reference_char { + 'R' => TtmReference::Relative, + 'T' => TtmReference::Theoretical, + _ => unreachable!(), + }; + + TtmAngle { angle, reference } + }) + }), + )) +} + +#[cfg(test)] +mod tests { + use approx::assert_relative_eq; + + use super::*; + use crate::parse::parse_nmea_sentence; + + #[test] + fn test_parse_ttm_full() { + let data = parse_ttm(NmeaSentence { + talker_id: "RA", + message_id: SentenceType::TTM, + data: "00,0.5,187.5,T,12.0,17.6,T,0.0,1.2,N,TGT00,T,,100023.00,A", + checksum: 0x4e, + }) + .unwrap(); + assert_eq!(data.target_number.unwrap(), 0); + assert_relative_eq!(data.target_distance.unwrap(), 0.5); + + let bearing_from_own_ship = data.bearing_from_own_ship.unwrap(); + assert_relative_eq!(bearing_from_own_ship.angle, 187.5,); + assert_eq!(bearing_from_own_ship.reference, TtmReference::Theoretical); + + assert_relative_eq!(data.target_speed.unwrap(), 12.0); + + let target_course = data.target_course.unwrap(); + assert_relative_eq!(target_course.angle, 17.6); + assert_eq!(target_course.reference, TtmReference::Theoretical); + + assert_relative_eq!(data.distance_of_cpa.unwrap(), 0.0); + assert_relative_eq!(data.time_to_cpa.unwrap(), 1.2); + assert_eq!( + data.speed_or_distance_unit.unwrap(), + TtmDistanceUnit::NauticalMile + ); + assert_eq!(data.target_name.unwrap(), "TGT00"); + assert_eq!(data.target_status.unwrap(), TtmStatus::Tracking); + assert!(!data.is_target_reference); + assert_eq!( + data.time_of_data.unwrap(), + NaiveTime::from_hms_opt(10, 0, 23).unwrap() + ); + assert_eq!( + data.type_of_acquisition.unwrap(), + TtmTypeOfAcquisition::Automatic + ); + } + + #[test] + fn test_parse_ttm_all_optional() { + let s = parse_nmea_sentence("$RATTM,,,,,,,,,,,,,,,*72").unwrap(); + assert_eq!(s.checksum, s.calc_checksum()); + + let data = parse_ttm(s); + assert_eq!( + data, + Ok(TtmData { + target_number: None, + target_distance: None, + bearing_from_own_ship: None, + target_speed: None, + target_course: None, + distance_of_cpa: None, + time_to_cpa: None, + speed_or_distance_unit: None, + target_name: None, + target_status: None, + is_target_reference: false, + time_of_data: None, + type_of_acquisition: None, + }) + ); + } +} diff --git a/tests/all_supported_messages.rs b/tests/all_supported_messages.rs index 7b1ede4..b96f24c 100644 --- a/tests/all_supported_messages.rs +++ b/tests/all_supported_messages.rs @@ -37,6 +37,8 @@ fn test_all_supported_messages() { (SentenceType::RMC, "$GPRMC,225446.33,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E,A*2B"), // RMZ (SentenceType::RMZ, "$PGRMZ,2282,f,3*21"), + // TTM + (SentenceType::TTM, "$RATTM,01,0.2,190.8,T,12.1,109.7,T,0.1,0.5,N,TGT01,T,,100021.00,A*79"), // TXT (SentenceType::TXT, "$GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E"), // VHW