Skip to content

Commit

Permalink
Merge pull request #84 from ekuinox/feature/vhw-parser
Browse files Browse the repository at this point in the history
Add VHW sentence parser
  • Loading branch information
elpiel authored Apr 10, 2023
2 parents 750e0a9 + c0139e5 commit b203556
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ NMEA Standard Sentences
- MTW
- MWV
- RMC *
- VHW
- VTG *
- ZDA

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
//! - MDA
//! - MWV
//! - RMC *
//! - VHW
//! - VTG *
//! - ZDA
//!
Expand Down
3 changes: 3 additions & 0 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub enum ParseResult {
MWV(MwvData),
RMC(RmcData),
TXT(TxtData),
VHW(VhwData),
VTG(VtgData),
ZDA(ZdaData),
PGRMZ(PgrmzData),
Expand Down Expand Up @@ -141,6 +142,7 @@ impl From<&ParseResult> for SentenceType {
ParseResult::MWV(_) => SentenceType::MWV,
ParseResult::RMC(_) => SentenceType::RMC,
ParseResult::TXT(_) => SentenceType::TXT,
ParseResult::VHW(_) => SentenceType::VHW,
ParseResult::VTG(_) => SentenceType::VTG,
ParseResult::PGRMZ(_) => SentenceType::RMZ,
ParseResult::ZDA(_) => SentenceType::ZDA,
Expand Down Expand Up @@ -188,6 +190,7 @@ pub fn parse_str(sentence_input: &str) -> Result<ParseResult, Error> {
SentenceType::RMC => parse_rmc(nmea_sentence).map(ParseResult::RMC),
SentenceType::GSA => parse_gsa(nmea_sentence).map(ParseResult::GSA),
SentenceType::VTG => parse_vtg(nmea_sentence).map(ParseResult::VTG),
SentenceType::VHW => parse_vhw(nmea_sentence).map(ParseResult::VHW),
SentenceType::GLL => parse_gll(nmea_sentence).map(ParseResult::GLL),
SentenceType::TXT => parse_txt(nmea_sentence).map(ParseResult::TXT),
SentenceType::GNS => parse_gns(nmea_sentence).map(ParseResult::GNS),
Expand Down
1 change: 1 addition & 0 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ impl<'a> Nmea {
| ParseResult::MTW(_)
| ParseResult::MWV(_)
| ParseResult::MDA(_)
| ParseResult::VHW(_)
| ParseResult::ZDA(_) => return Ok(FixType::Invalid),

ParseResult::Unsupported(_) => {
Expand Down
2 changes: 2 additions & 0 deletions src/sentences/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod rmc;
mod rmz;
mod txt;
mod utils;
mod vhw;
mod vtg;
mod zda;

Expand Down Expand Up @@ -48,6 +49,7 @@ pub use {
rmc::{parse_rmc, RmcData, RmcStatusOfFix},
rmz::{parse_pgrmz, PgrmzData},
txt::{parse_txt, TxtData},
vhw::{parse_vhw, VhwData},
vtg::{parse_vtg, VtgData},
zda::{parse_zda, ZdaData},
};
Expand Down
217 changes: 217 additions & 0 deletions src/sentences/vhw.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
use nom::{
bytes::complete::take_until,
character::complete::char,
combinator::{map_res, opt},
IResult,
};

use crate::{Error, NmeaSentence, SentenceType};

use super::utils::parse_float_num;

/// VHW - Water speed and heading
///
/// <https://gpsd.gitlab.io/gpsd/NMEA.html#_vhw_water_speed_and_heading>
///
/// ```text
/// 1 2 3 4 5 6 7 8 9
/// | | | | | | | | |
/// $--VHW,x.x,T,x.x,M,x.x,N,x.x,K*hh<CR><LF>
/// ```
/// 1. Heading degrees, True
/// 2. T = True
/// 3. Heading degrees, Magnetic
/// 4. M = Magnetic
/// 5. Speed of vessel relative to the water, knots
/// 6. N = Knots
/// 7. Speed of vessel relative to the water, km/hr
/// 8. K = Kilometers
/// 9. Checksum
///
/// Note that this implementation follows the documentation published by `gpsd`, but the GLOBALSAT documentation may have conflicting definitions.
/// > [[GLOBALSAT](https://gpsd.gitlab.io/gpsd/NMEA.html#GLOBALSAT)] describes a different format in which the first three fields are water-temperature measurements.
/// > It’s not clear which is correct.
#[derive(Clone, PartialEq, Debug)]
pub struct VhwData {
/// Heading degrees, True
pub heading_true: Option<f64>,
/// Heading degrees, Magnetic
pub heading_magnetic: Option<f64>,
/// Speed of vessel relative to the water, knots
pub relative_speed_knots: Option<f64>,
/// Speed of vessel relative to the water, km/hr
pub relative_speed_kmph: Option<f64>,
}

/// # Parse VHW message
///
/// ```text
/// $GPVHW,100.5,T,105.5,M,10.5,N,19.4,K*4F
/// ```
/// 1. 100.5 Heading True
/// 2. T
/// 3. 105.5 Heading Magnetic
/// 4. M
/// 5. 10.5 Speed relative to water, knots
/// 6. N
/// 7. 19.4 Speed relative to water, km/hr
/// 8. K
///
/// Each is considered as a pair of a float value and a single character,
/// and if the float value exists but the single character is not correct, it is treated as `None`.
/// For example, if 1 is "100.5" and 2 is not "T", then heading_true is `None`.
pub fn parse_vhw(sentence: NmeaSentence) -> Result<VhwData, Error> {
if sentence.message_id == SentenceType::VHW {
Ok(do_parse_vhw(sentence.data)?.1)
} else {
Err(Error::WrongSentenceHeader {
expected: SentenceType::VHW,
found: sentence.message_id,
})
}
}

/// Parses a float value
/// and returns `None` if the float value can be parsed but the next field does not match the specified character.
fn do_parse_float_with_char(c: char, i: &str) -> IResult<&str, Option<f64>> {
let (i, value) = opt(map_res(take_until(","), parse_float_num::<f64>))(i)?;
let (i, _) = char(',')(i)?;
let (i, tag) = opt(char(c))(i)?;
Ok((i, tag.and(value)))
}

fn do_parse_vhw(i: &str) -> IResult<&str, VhwData> {
let comma = char(',');

let (i, heading_true) = do_parse_float_with_char('T', i)?;
let (i, _) = comma(i)?;

let (i, heading_magnetic) = do_parse_float_with_char('M', i)?;
let (i, _) = comma(i)?;

let (i, relative_speed_knots) = do_parse_float_with_char('N', i)?;
let (i, _) = comma(i)?;

let (i, relative_speed_kmph) = do_parse_float_with_char('K', i)?;

Ok((
i,
VhwData {
heading_true,
heading_magnetic,
relative_speed_knots,
relative_speed_kmph,
},
))
}

#[cfg(test)]
mod tests {
use approx::assert_relative_eq;

use super::*;
use crate::parse::parse_nmea_sentence;

#[test]
fn test_do_parse_float_with_char() {
assert_eq!(do_parse_float_with_char('T', "1.5,T"), Ok(("", Some(1.5))));
assert_eq!(do_parse_float_with_char('T', "1.5,"), Ok(("", None)));
assert_eq!(do_parse_float_with_char('T', ","), Ok(("", None)));
}

#[test]
fn test_wrong_sentence() {
let invalid_aam_sentence = NmeaSentence {
message_id: SentenceType::AAM,
data: "",
talker_id: "GP",
checksum: 0,
};
assert_eq!(
Err(Error::WrongSentenceHeader {
expected: SentenceType::VHW,
found: SentenceType::AAM
}),
parse_vhw(invalid_aam_sentence)
);
}

#[test]
fn test_parse_vhw() {
let s = NmeaSentence {
message_id: SentenceType::VHW,
talker_id: "GP",
data: "100.5,T,105.5,M,10.5,N,19.4,K",
checksum: 0x4f,
};
let vhw_data = parse_vhw(s).unwrap();
assert_relative_eq!(vhw_data.heading_true.unwrap(), 100.5);
assert_relative_eq!(vhw_data.heading_magnetic.unwrap(), 105.5);
assert_relative_eq!(vhw_data.relative_speed_knots.unwrap(), 10.5);
assert_relative_eq!(vhw_data.relative_speed_kmph.unwrap(), 19.4);

let s = parse_nmea_sentence("$GPVHW,100.5,T,105.5,M,10.5,N,19.4,K*4F").unwrap();
assert_eq!(s.checksum, s.calc_checksum());
assert_eq!(s.checksum, 0x4F);

let vhw_data = parse_vhw(s).unwrap();
assert_relative_eq!(vhw_data.heading_true.unwrap(), 100.5);
assert_relative_eq!(vhw_data.heading_magnetic.unwrap(), 105.5);
assert_relative_eq!(vhw_data.relative_speed_knots.unwrap(), 10.5);
assert_relative_eq!(vhw_data.relative_speed_kmph.unwrap(), 19.4);
}

#[test]
fn test_parse_incomplete_vhw() {
// Pattern with all single letter alphabetical fields filled, but all numeric fields blank.
let s = NmeaSentence {
message_id: SentenceType::VHW,
talker_id: "GP",
data: ",T,,M,,N,,K",
checksum: 0,
};
assert_eq!(
parse_vhw(s),
Ok(VhwData {
heading_true: None,
heading_magnetic: None,
relative_speed_knots: None,
relative_speed_kmph: None,
})
);

// Pattern with all single letter alphabetical fields filled and some numerical fields filled.
let s = NmeaSentence {
message_id: SentenceType::VHW,
talker_id: "GP",
data: ",T,,M,10.5,N,20.0,K",
checksum: 0,
};
assert_eq!(
parse_vhw(s),
Ok(VhwData {
heading_true: None,
heading_magnetic: None,
relative_speed_knots: Some(10.5),
relative_speed_kmph: Some(20.0),
})
);

// Pattern with all fields missing
let s = NmeaSentence {
message_id: SentenceType::VHW,
talker_id: "GP",
data: ",,,,,,,",
checksum: 0,
};
assert_eq!(
parse_vhw(s),
Ok(VhwData {
heading_true: None,
heading_magnetic: None,
relative_speed_knots: None,
relative_speed_kmph: None
})
);
}
}
2 changes: 2 additions & 0 deletions tests/all_supported_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ fn test_all_supported_messages() {
(SentenceType::RMZ, "$PGRMZ,2282,f,3*21"),
// TXT
(SentenceType::TXT, "$GNTXT,01,01,02,u-blox AG - www.u-blox.com*4E"),
// VHW
(SentenceType::VHW, "$GPVHW,100.5,T,105.5,M,10.5,N,19.4,K*4F"),
// VTG
(SentenceType::VTG, "$GPVTG,360.0,T,348.7,M,000.0,N,000.0,K*43"),
// ZDA
Expand Down

0 comments on commit b203556

Please sign in to comment.