diff --git a/Cargo.toml b/Cargo.toml index 33ea700..787cac2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,7 +90,7 @@ radar = ["TTM"] ## Water water = ["DBK", "DBS", "DPT", "MTW", "VHW"] ## Vendor-specific messages -vendor-specific = ["RMZ"] +vendor-specific = ["RMZ", "RMCE", "GGAE"] ## Other other = ["HDT", "MDA", "MWV", "TXT", "ZDA"] @@ -180,6 +180,14 @@ MWV = [] ## (feature: `GNSS`) RMC = [] +## RMCE - Enhanced Recommended Minimum Navigation Information (u-blox vendor specific) +## (feature: `vendor-specific`) +RMCE = [] + +## GGAE - Enhanced Global Positioning System Fix Data (u-blox vendor specific) +## (feature: `vendor-specific`) +GGAE = [] + ## PGRMZ - Garmin Altitude (Vendor specific) ## (feature: `vendor-specific`) RMZ = [] diff --git a/README.md b/README.md index 8d6b671..d9eeb5a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Supported sentences (alphabetically ordered): - `DPT` - Depth of Water (feature: `water`) - `GBS` - GPS Satellite Fault Detection (feature: `GNSS`) - `GGA` - * Global Positioning System Fix Data (feature: `GNSS`) +- `GGAE` - Enhanced Global Positioning System Fix Data (feature: `vendor-specific`) - `GLL` - * Geographic Position - Latitude/Longitude (feature: `GNSS`) - `GNS` - * Fix data (feature: `GNSS`) - `GSA` - * GPS DOP and active satellites (feature: `GNSS`) @@ -32,6 +33,7 @@ Supported sentences (alphabetically ordered): - `MTW` - Mean Temperature of Water (feature: `water`) - `MWV` - Wind Speed and Angle (feature: `other`) - `RMC` - * Recommended Minimum Navigation Information (feature: `GNSS`) +- `RMCE` - Enhanced Recommended Minimum Navigation Information (feature: `vendor-specific`) - `RMZ` - PGRMZ - Garmin Altitude (feature: `vendor-specific`) - `TTM` - Tracked target message (feature: `radar`) - `TXT` - * Text message (feature: `other`) diff --git a/src/parse.rs b/src/parse.rs index de2a31b..47130bb 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -70,6 +70,13 @@ fn parse_checksum(i: &str) -> IResult<&str, u8> { } fn parse_sentence_type(i: &str) -> IResult<&str, SentenceType> { + // Try 4-character sentence types first (vendor-specific extensions like RMCE, GGAE) + if i.len() >= 4 { + if let Ok(sentence_type) = SentenceType::try_from(&i[..4]) { + return Ok((&i[4..], sentence_type)); + } + } + // Fall back to standard 3-character sentence types map_res(take(3usize), |sentence_type: &str| { SentenceType::try_from(sentence_type).map_err(|_| "Unknown sentence type") }) @@ -128,6 +135,8 @@ pub enum ParseResult { MTW(MtwData), MWV(MwvData), RMC(RmcData), + RMCE(RmcData), + GGAE(GgaData), TTM(TtmData), TXT(TxtData), VHW(VhwData), @@ -164,6 +173,8 @@ impl From<&ParseResult> for SentenceType { ParseResult::MTW(_) => SentenceType::MTW, ParseResult::MWV(_) => SentenceType::MWV, ParseResult::RMC(_) => SentenceType::RMC, + ParseResult::RMCE(_) => SentenceType::RMCE, + ParseResult::GGAE(_) => SentenceType::GGAE, ParseResult::TTM(_) => SentenceType::TTM, ParseResult::TXT(_) => SentenceType::TXT, ParseResult::VHW(_) => SentenceType::VHW, @@ -380,6 +391,24 @@ pub fn parse_str(sentence_input: &str) -> Result> { } } } + SentenceType::RMCE => { + cfg_if! { + if #[cfg(feature = "RMCE")] { + parse_rmce(nmea_sentence).map(ParseResult::RMCE) + } else { + return Err(Error::DisabledSentence); + } + } + } + SentenceType::GGAE => { + cfg_if! { + if #[cfg(feature = "GGAE")] { + parse_ggae(nmea_sentence).map(ParseResult::GGAE) + } else { + return Err(Error::DisabledSentence); + } + } + } SentenceType::RMZ => { cfg_if! { if #[cfg(feature = "RMZ")] { diff --git a/src/parser.rs b/src/parser.rs index bee6738..77b155d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -254,6 +254,10 @@ impl<'a> Nmea { self.merge_gga_data(gga); Ok(SentenceType::GGA) } + ParseResult::GGAE(gga) => { + self.merge_gga_data(gga); + Ok(SentenceType::GGAE) + } ParseResult::GSV(gsv) => { self.merge_gsv_data(gsv)?; Ok(SentenceType::GSV) @@ -262,6 +266,10 @@ impl<'a> Nmea { self.merge_rmc_data(rmc); Ok(SentenceType::RMC) } + ParseResult::RMCE(rmc) => { + self.merge_rmc_data(rmc); + Ok(SentenceType::RMCE) + } ParseResult::GNS(gns) => { self.merge_gns_data(gns); Ok(SentenceType::GNS) @@ -331,6 +339,17 @@ impl<'a> Nmea { self.merge_rmc_data(rmc_data); self.sentences_for_this_time.insert(SentenceType::RMC); } + ParseResult::RMCE(rmc_data) => { + if rmc_data.status_of_fix == RmcStatusOfFix::Invalid { + self.clear_position_info(); + return Ok(FixType::Invalid); + } + if !self.update_fix_time(rmc_data.fix_time) { + return Ok(FixType::Invalid); + } + self.merge_rmc_data(rmc_data); + self.sentences_for_this_time.insert(SentenceType::RMCE); + } ParseResult::GNS(gns_data) => { let fix_type: FixType = gns_data.faa_modes.into(); if !fix_type.is_valid() { @@ -357,6 +376,20 @@ impl<'a> Nmea { self.merge_gga_data(gga_data); self.sentences_for_this_time.insert(SentenceType::GGA); } + ParseResult::GGAE(gga_data) => { + match gga_data.fix_type { + Some(FixType::Invalid) | None => { + self.clear_position_info(); + return Ok(FixType::Invalid); + } + _ => { /*nothing*/ } + } + if !self.update_fix_time(gga_data.fix_time) { + return Ok(FixType::Invalid); + } + self.merge_gga_data(gga_data); + self.sentences_for_this_time.insert(SentenceType::GGAE); + } ParseResult::GLL(gll_data) => { if !self.update_fix_time(gll_data.fix_time) { return Ok(FixType::Invalid); @@ -895,6 +928,12 @@ define_sentence_type_enum! { /// /// Type: `GPS` GGA, + /// GGAE - Enhanced Global Positioning System Fix Data + /// + /// u-blox proprietary extension with higher precision coordinates + /// + /// Type: `Vendor extensions` + GGAE, /// GLC - Geographic Position, Loran-C /// /// @@ -1065,6 +1104,12 @@ define_sentence_type_enum! { /// /// Type: `Navigation` RMC, + /// RMCE - Enhanced Recommended Minimum Navigation Information + /// + /// u-blox proprietary extension with higher precision coordinates + /// + /// Type: `Vendor extensions` + RMCE, /// PGRMZ - Garmin Altitude /// /// diff --git a/src/sentences.rs b/src/sentences.rs index ed95446..b27db68 100644 --- a/src/sentences.rs +++ b/src/sentences.rs @@ -50,7 +50,7 @@ pub use { faa_mode::{FaaMode, FaaModes}, fix_type::FixType, gbs::{GbsData, parse_gbs}, - gga::{GgaData, parse_gga}, + gga::{GgaData, parse_gga, parse_ggae}, gll::{GllData, parse_gll}, gns::{GnsData, parse_gns}, gnss_type::GnssType, @@ -61,7 +61,7 @@ pub use { mda::{MdaData, parse_mda}, mtw::{MtwData, parse_mtw}, mwv::{MwvData, parse_mwv}, - rmc::{RmcData, parse_rmc}, + rmc::{RmcData, parse_rmc, parse_rmce}, rmz::{PgrmzData, parse_pgrmz}, ttm::{ TtmAngle, TtmData, TtmDistanceUnit, TtmReference, TtmStatus, TtmTypeOfAcquisition, diff --git a/src/sentences/gga.rs b/src/sentences/gga.rs index 52e1543..46ae55c 100644 --- a/src/sentences/gga.rs +++ b/src/sentences/gga.rs @@ -109,6 +109,21 @@ pub fn parse_gga(sentence: NmeaSentence<'_>) -> Result> { } } +/// Parse GGAE message (Enhanced GGA with higher precision) +/// +/// Enhanced version of GGA with higher precision coordinates. +/// Uses the same parsing logic as standard GGA. +pub fn parse_ggae(sentence: NmeaSentence<'_>) -> Result> { + if sentence.message_id != SentenceType::GGAE { + Err(Error::WrongSentenceHeader { + expected: SentenceType::GGAE, + found: sentence.message_id, + }) + } else { + Ok(do_parse_gga(sentence.data)?.1) + } +} + #[cfg(not(feature = "std"))] #[cfg(feature = "serde")] mod serde_naive_time { @@ -328,4 +343,22 @@ mod tests { assert_eq!(data.fix_time, gga.fix_time); } + + #[test] + fn test_parse_ggae_enhanced() { + // Checksum calculated: GPGGAE,092750.000,5321.6802,N,00630.3372,W,1,8,1.03,61.7,M,55.2,M,, = 0x33 + let ggae = "$GPGGAE,092750.000,5321.6802,N,00630.3372,W,1,8,1.03,61.7,M,55.2,M,,*33"; + let s = parse_nmea_sentence(ggae).unwrap(); + assert_eq!(s.checksum, s.calc_checksum()); + let data = parse_ggae(s).unwrap(); + assert_eq!( + data.fix_time, + Some(NaiveTime::from_hms_milli_opt(9, 27, 50, 0).expect("invalid time")) + ); + assert_eq!(data.fix_type.unwrap(), FixType::Gps); + assert_relative_eq!(data.latitude.unwrap(), 53. + 21.6802 / 60.); + assert_relative_eq!(data.longitude.unwrap(), -(6. + 30.3372 / 60.)); + assert_eq!(data.fix_satellites.unwrap(), 8); + assert_relative_eq!(data.hdop.unwrap(), 1.03); + } } diff --git a/src/sentences/rmc.rs b/src/sentences/rmc.rs index 08ad256..c7789d1 100644 --- a/src/sentences/rmc.rs +++ b/src/sentences/rmc.rs @@ -202,6 +202,21 @@ pub fn parse_rmc(sentence: NmeaSentence<'_>) -> Result> { } } +/// Parse RMCE message (Enhanced RMC with higher precision) +/// +/// Enhanced version of RMC with higher precision coordinates. +/// Uses the same parsing logic as standard RMC. +pub fn parse_rmce(sentence: NmeaSentence<'_>) -> Result> { + if sentence.message_id != SentenceType::RMCE { + Err(Error::WrongSentenceHeader { + expected: SentenceType::RMCE, + found: sentence.message_id, + }) + } else { + Ok(do_parse_rmc(sentence.data)?.1) + } +} + #[cfg(test)] mod tests { use approx::assert_relative_eq; @@ -387,4 +402,24 @@ mod tests { assert_eq!(faa_mode, Some(FaaMode::Manual)); assert_eq!(nav_status, Some(RmcNavigationStatus::Estimated)); } + + #[test] + fn parse_rmce_enhanced() { + // Checksum calculated: GPRMCE,225446.33,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E,A = 0x6E + let rmce = "$GPRMCE,225446.33,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E,A*6E"; + let s = parse_nmea_sentence(rmce).unwrap(); + assert_eq!(s.checksum, s.calc_checksum()); + let rmc_data = parse_rmce(s).unwrap(); + assert_eq!( + rmc_data.fix_time, + Some(NaiveTime::from_hms_milli_opt(22, 54, 46, 330).expect("invalid time")) + ); + assert_eq!( + rmc_data.fix_date, + Some(NaiveDate::from_ymd_opt(1994, 11, 19).expect("invalid time")) + ); + assert_relative_eq!(rmc_data.lat.unwrap(), 49.0 + 16.45 / 60.); + assert_relative_eq!(rmc_data.lon.unwrap(), -(123.0 + 11.12 / 60.)); + assert_eq!(rmc_data.faa_mode, Some(FaaMode::Autonomous)); + } }