From 4783ca79b5bd68de5b24f3242425d86fa14e36b4 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:49:37 -0700 Subject: [PATCH 01/17] clippy fixes --- Cargo.toml | 2 +- swiftnav/src/coords/ecef.rs | 1 + swiftnav/src/coords/mod.rs | 13 +-- swiftnav/src/edc.rs | 11 ++- swiftnav/src/reference_frame/mod.rs | 3 +- swiftnav/src/signal/code.rs | 1 + swiftnav/src/signal/mod.rs | 1 + swiftnav/src/time/gnss.rs | 28 +++--- swiftnav/src/time/mod.rs | 10 +- swiftnav/src/time/utc.rs | 137 ++++++++++++++-------------- swiftnav/tests/reference_frames.rs | 2 +- 11 files changed, 110 insertions(+), 99 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13039ef..f5c4cc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] members = [ "swiftnav", - "swiftnav-sys" + # "swiftnav-sys" ] diff --git a/swiftnav/src/coords/ecef.rs b/swiftnav/src/coords/ecef.rs index 2f71387..9774214 100644 --- a/swiftnav/src/coords/ecef.rs +++ b/swiftnav/src/coords/ecef.rs @@ -359,6 +359,7 @@ impl MulAssign<&f64> for ECEF { mod tests { use super::*; + #[expect(clippy::float_cmp)] #[test] fn ecef_ops() { let a = ECEF::new(1.0, 2.0, 3.0); diff --git a/swiftnav/src/coords/mod.rs b/swiftnav/src/coords/mod.rs index 3ba4a72..198f035 100644 --- a/swiftnav/src/coords/mod.rs +++ b/swiftnav/src/coords/mod.rs @@ -303,6 +303,7 @@ mod tests { const MAX_ANGLE_ERROR_SECS: f64 = 1e-7; const MAX_ANGLE_ERROR_RAD: f64 = (MAX_ANGLE_ERROR_SECS / 3600.0).to_radians(); + #[expect(clippy::float_cmp)] #[test] fn llhrad2deg() { let zeros = LLHRadians::default(); @@ -312,11 +313,11 @@ mod tests { assert_eq!(0.0, deg.longitude()); assert_eq!(0.0, deg.height()); - let swift_home: LLHDegrees = [37.779804, -122.391751, 60.0].into(); + let swift_home: LLHDegrees = [37.779_804, -122.391_751, 60.0].into(); let rads = swift_home.to_radians(); - assert!((rads.latitude() - 0.659381970558).abs() < MAX_ANGLE_ERROR_RAD); - assert!((rads.longitude() + 2.136139032231).abs() < MAX_ANGLE_ERROR_RAD); + assert!((rads.latitude() - 0.659_381_970_558).abs() < MAX_ANGLE_ERROR_RAD); + assert!((rads.longitude() + 2.136_139_032_231).abs() < MAX_ANGLE_ERROR_RAD); assert!( rads.height() == swift_home.height(), "rads.height() = {}, swift_home.height() = {}", @@ -339,7 +340,7 @@ mod tests { ]; /* Semi-major axis. */ - const EARTH_A: f64 = 6378137.0; + const EARTH_A: f64 = 6_378_137.0; /* Semi-minor axis. */ const EARTH_B: f64 = 6_356_752.314_245_179; @@ -404,7 +405,7 @@ mod tests { #[test] fn llh2ecef2llh() { - for llh_input in LLH_VALUES.iter() { + for llh_input in &LLH_VALUES { let llh_input: LLHRadians = llh_input.into(); let llh_output = llh_input.to_ecef().to_llh(); @@ -425,7 +426,7 @@ mod tests { #[test] fn ecef2llh2ecef() { - for ecef_input in ECEF_VALUES.iter() { + for ecef_input in &ECEF_VALUES { let ecef_input: ECEF = ecef_input.into(); let ecef_output = ecef_input.to_llh().to_ecef(); diff --git a/swiftnav/src/edc.rs b/swiftnav/src/edc.rs index 1171760..c593e3b 100644 --- a/swiftnav/src/edc.rs +++ b/swiftnav/src/edc.rs @@ -332,6 +332,7 @@ mod tests { const TEST_DATA: &[u8] = "123456789".as_bytes(); /// Helper function to append a CRC-24Q value as 3 bytes (big-endian) to a buffer + #[expect(clippy::cast_possible_truncation)] fn append_crc24q(data: &mut Vec, crc: u32) { data.push((crc >> 16) as u8); data.push((crc >> 8) as u8); @@ -365,9 +366,9 @@ mod tests { /* Test value taken from python crcmod package tests, see: * http://crcmod.sourceforge.net/crcmod.predefined.html */ - let crc = compute_crc24q(TEST_DATA, 0xB704CE); + let crc = compute_crc24q(TEST_DATA, 0x00B7_04CE); assert!( - crc == 0x21CF02, + crc == 0x0021_CF02, "CRC of \"123456789\" with init value 0xB704CE should be 0x21CF02, not {}", crc ); @@ -403,7 +404,7 @@ mod tests { #[test] fn prop_crc_stays_within_24_bits(data in prop::collection::vec(any::(), 0..1000), init in any::()) { let crc = compute_crc24q(&data, init); - prop_assert!(crc <= 0xFFFFFF, "CRC result 0x{:08X} exceeds 24-bit maximum", crc); + prop_assert!(crc <= 0x00FF_FFFF, "CRC result 0x{:08X} exceeds 24-bit maximum", crc); } /// Property: Incremental CRC calculation equals full calculation. @@ -432,10 +433,10 @@ mod tests { #[test] fn prop_crc_initial_value_masked(data in prop::collection::vec(any::(), 0..100), init in any::()) { let crc1 = compute_crc24q(&data, init); - let crc2 = compute_crc24q(&data, init & 0xFFFFFF); + let crc2 = compute_crc24q(&data, init & 0x00FF_FFFF); prop_assert_eq!(crc1, crc2, "CRC with init 0x{:08X} should equal CRC with masked init 0x{:06X}", - init, init & 0xFFFFFF); + init, init & 0x00FF_FFFF); } /// Property: Single bit errors are detected (CRC changes). diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 9b2e95f..208e11a 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -579,6 +579,7 @@ mod tests { use params::TRANSFORMATIONS; use std::str::FromStr; + #[expect(clippy::too_many_lines)] #[test] fn reference_frame_strings() { assert_eq!(ReferenceFrame::ITRF88.to_string(), "ITRF88"); @@ -996,7 +997,7 @@ mod tests { }, }; - let params = transformation.params.clone(); + let params = transformation.params; repo.add_transformation(transformation); assert_eq!(repo.count(), 2); diff --git a/swiftnav/src/signal/code.rs b/swiftnav/src/signal/code.rs index 9b461d4..2d90814 100644 --- a/swiftnav/src/signal/code.rs +++ b/swiftnav/src/signal/code.rs @@ -455,6 +455,7 @@ mod tests { assert!(Code::AuxBds.is_bds()); } + #[expect(clippy::too_many_lines)] #[test] fn code_strings() { use std::str::FromStr; diff --git a/swiftnav/src/signal/mod.rs b/swiftnav/src/signal/mod.rs index 83c324d..e1a3c0d 100644 --- a/swiftnav/src/signal/mod.rs +++ b/swiftnav/src/signal/mod.rs @@ -133,6 +133,7 @@ impl fmt::Display for GnssSignal { mod tests { use super::*; + #[expect(clippy::too_many_lines)] #[test] fn signal_to_constellation() { assert_eq!( diff --git a/swiftnav/src/time/gnss.rs b/swiftnav/src/time/gnss.rs index 7bb6aad..4b9b9eb 100644 --- a/swiftnav/src/time/gnss.rs +++ b/swiftnav/src/time/gnss.rs @@ -742,7 +742,7 @@ mod tests { fn add_duration() { let mut t = GpsTime::new(0, 0.0).unwrap(); let t_expected = GpsTime::new(0, 1.001).unwrap(); - let d = Duration::new(1, 1000000); + let d = Duration::new(1, 1_000_000); t.add_duration(&d); assert_eq!(t, t_expected); @@ -760,7 +760,7 @@ mod tests { fn subtract_duration() { let mut t = GpsTime::new(0, 1.001).unwrap(); let t_expected = GpsTime::new(0, 0.0).unwrap(); - let d = Duration::new(1, 1000000); + let d = Duration::new(1, 1_000_000); t.subtract_duration(&d); assert_eq!(t, t_expected); @@ -783,14 +783,14 @@ mod tests { let epsilon = 1e-5; let test_cases = [ - GpsTime::new_unchecked(1234, 567890.01), - GpsTime::new_unchecked(1234, 567890.0501), - GpsTime::new_unchecked(1234, 604800.06), + GpsTime::new_unchecked(1234, 567_890.01), + GpsTime::new_unchecked(1234, 567_890.050_1), + GpsTime::new_unchecked(1234, 604_800.06), ]; let expectations = [ - GpsTime::new_unchecked(1234, 567890.00), - GpsTime::new_unchecked(1234, 567890.10), + GpsTime::new_unchecked(1234, 567_890.00), + GpsTime::new_unchecked(1234, 567_890.10), GpsTime::new_unchecked(1235, 0.1), ]; @@ -812,14 +812,14 @@ mod tests { let epsilon = 1e-6; let test_cases = [ - GpsTime::new_unchecked(1234, 567890.01), - GpsTime::new_unchecked(1234, 567890.0501), - GpsTime::new_unchecked(1234, 604800.06), + GpsTime::new_unchecked(1234, 567_890.01), + GpsTime::new_unchecked(1234, 567_890.050_1), + GpsTime::new_unchecked(1234, 604_800.06), ]; let expectations = [ - GpsTime::new_unchecked(1234, 567890.00), - GpsTime::new_unchecked(1234, 567890.00), + GpsTime::new_unchecked(1234, 567_890.00), + GpsTime::new_unchecked(1234, 567_890.00), GpsTime::new_unchecked(1235, 0.0), ]; @@ -840,7 +840,7 @@ mod tests { assert!(GalTime::new(-1, 0.0).is_err()); assert!(GalTime::new(0, -1.0).is_err()); - assert!(GalTime::new(0, consts::WEEK_SECS as f64 + 1.0).is_err()); + assert!(GalTime::new(0, f64::from(consts::WEEK_SECS) + 1.0).is_err()); } #[test] @@ -854,6 +854,6 @@ mod tests { assert!(BdsTime::new(-1, 0.0).is_err()); assert!(BdsTime::new(0, -1.0).is_err()); - assert!(BdsTime::new(0, consts::WEEK_SECS as f64 + 1.0).is_err()); + assert!(BdsTime::new(0, f64::from(consts::WEEK_SECS) + 1.0).is_err()); } } diff --git a/swiftnav/src/time/mod.rs b/swiftnav/src/time/mod.rs index f6db360..0dc0788 100644 --- a/swiftnav/src/time/mod.rs +++ b/swiftnav/src/time/mod.rs @@ -98,13 +98,13 @@ mod tests { #[test] fn conversions() { const TEST_CASES: [GpsTime; 10] = [ - GpsTime::new_unchecked(1234, 567890.0), - GpsTime::new_unchecked(1234, 567890.5), - GpsTime::new_unchecked(1234, 567890.0), + GpsTime::new_unchecked(1234, 567_890.0), + GpsTime::new_unchecked(1234, 567_890.5), + GpsTime::new_unchecked(1234, 567_890.0), GpsTime::new_unchecked(1234, 0.0), - GpsTime::new_unchecked(1000, 604578.0), + GpsTime::new_unchecked(1000, 604_578.0), GpsTime::new_unchecked(1001, 222.222), - GpsTime::new_unchecked(1001, 604578.0), + GpsTime::new_unchecked(1001, 604_578.0), GpsTime::new_unchecked(1939, 222.222), GpsTime::new_unchecked(1930, 16.0), GpsTime::new_unchecked(1930, 18.0), /* around Jan 2017 leap second */ diff --git a/swiftnav/src/time/utc.rs b/swiftnav/src/time/utc.rs index 6336a97..3547a3c 100644 --- a/swiftnav/src/time/utc.rs +++ b/swiftnav/src/time/utc.rs @@ -485,32 +485,32 @@ mod tests { let test_cases: &[UtcOffsetTestdata] = &[ /* July 1 1981 */ UtcOffsetTestdata { - t: GpsTime::new_unchecked(77, 259199.0), + t: GpsTime::new_unchecked(77, 259_199.0), d_utc: 0.0, is_lse: false, }, UtcOffsetTestdata { - t: GpsTime::new_unchecked(77, 259199.5), + t: GpsTime::new_unchecked(77, 259_199.5), d_utc: 0.0, is_lse: false, }, UtcOffsetTestdata { - t: GpsTime::new_unchecked(77, 259200.0), + t: GpsTime::new_unchecked(77, 259_200.0), d_utc: 0.0, is_lse: true, }, UtcOffsetTestdata { - t: GpsTime::new_unchecked(77, 259200.5), + t: GpsTime::new_unchecked(77, 259_200.5), d_utc: 0.0, is_lse: true, }, UtcOffsetTestdata { - t: GpsTime::new_unchecked(77, 259201.0), + t: GpsTime::new_unchecked(77, 259_201.0), d_utc: 1.0, is_lse: false, }, UtcOffsetTestdata { - t: GpsTime::new_unchecked(77, 259202.0), + t: GpsTime::new_unchecked(77, 259_202.0), d_utc: 1.0, is_lse: false, }, @@ -555,7 +555,10 @@ mod tests { let d_utc = test_case.t.gps_utc_offset_hardcoded(); let is_lse = test_case.t.is_leap_second_event_hardcoded(); - assert!(d_utc == test_case.d_utc && is_lse == test_case.is_lse, "test_case.t: {:?}, test_case.d_utc: {}, test_case.is_lse: {}, d_utc: {}, is_lse: {}", test_case.t, test_case.d_utc, test_case.is_lse, d_utc, is_lse); + #[expect(clippy::float_cmp)] + { + assert!(d_utc == test_case.d_utc && is_lse == test_case.is_lse, "test_case.t: {:?}, test_case.d_utc: {}, test_case.is_lse: {}, d_utc: {}, is_lse: {}", test_case.t, test_case.d_utc, test_case.is_lse, d_utc, is_lse); + } } } @@ -567,7 +570,7 @@ mod tests { 0.0, 0.0, &GpsTime::new_unchecked(2080, 0.0), - &GpsTime::new_unchecked(2086, 259218.0 - 0.125), + &GpsTime::new_unchecked(2086, 259_218.0 - 0.125), 18, 19, ) @@ -579,7 +582,7 @@ mod tests { 0.0, 0.0, &GpsTime::new_unchecked(2080, 0.0), - &GpsTime::new_unchecked(2086, 259218.125), + &GpsTime::new_unchecked(2086, 259_218.125), 18, 19, ) @@ -593,7 +596,7 @@ mod tests { &GpsTime::new_unchecked(2080, 0.0), &GpsTime::new_unchecked( 2086, - 259218.0 + 1e-12 * (6.0 * consts::WEEK_SECS as f64 + 259218.0), + 259_218.0 + 1e-12 * (6.0 * f64::from(consts::WEEK_SECS) + 259_218.00), ), 18, 19, @@ -608,13 +611,14 @@ mod tests { &GpsTime::new_unchecked(2080, 0.0), &GpsTime::new_unchecked( 2086, - 259218.0 - 1e-12 * (6.0 * consts::WEEK_SECS as f64 + 259218.0), + 259_218.0 - 1e-12 * (6.0 * f64::from(consts::WEEK_SECS) + 259_218.0), ), 18, 19, ) } + #[expect(clippy::too_many_lines)] #[test] fn utc_params() { struct TestCase { @@ -627,148 +631,148 @@ mod tests { let test_cases = [ /* Jan 1 2020 (constant negative UTC offset) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.0 - 0.125), + t: GpsTime::new_unchecked(2086, 259_217.0 - 0.125), d_utc: 18.0 - 0.125, is_lse: false, params: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5 - 0.125), + t: GpsTime::new_unchecked(2086, 259_217.5 - 0.125), d_utc: 18.0 - 0.125, is_lse: false, params: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.0 - 0.125), + t: GpsTime::new_unchecked(2086, 259_218.0 - 0.125), d_utc: 18.0 - 0.125, is_lse: true, params: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5 - 0.125), + t: GpsTime::new_unchecked(2086, 259_218.5 - 0.125), d_utc: 18.0 - 0.125, is_lse: true, params: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.0 - 0.125), + t: GpsTime::new_unchecked(2086, 259_219.0 - 0.125), d_utc: 19.0 - 0.125, is_lse: false, params: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.5 - 0.125), + t: GpsTime::new_unchecked(2086, 259_219.5 - 0.125), d_utc: 19.0 - 0.125, is_lse: false, params: Some(make_p_neg_offset()), }, /* Jan 1 2020 (constant positive UTC offset) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.125), + t: GpsTime::new_unchecked(2086, 259_217.125), d_utc: 18.125, is_lse: false, params: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5 + 0.125), + t: GpsTime::new_unchecked(2086, 259_217.5 + 0.125), d_utc: 18.125, is_lse: false, params: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.125), + t: GpsTime::new_unchecked(2086, 259_218.125), d_utc: 18.125, is_lse: true, params: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5 + 0.125), + t: GpsTime::new_unchecked(2086, 259_218.5 + 0.125), d_utc: 18.125, is_lse: true, params: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.125), + t: GpsTime::new_unchecked(2086, 259_219.125), d_utc: 19.125, is_lse: false, params: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.5 + 0.125), + t: GpsTime::new_unchecked(2086, 259_219.5 + 0.125), d_utc: 19.125, is_lse: false, params: Some(make_p_pos_offset()), }, /* Jan 1 2020 (positive UTC linear correction) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.0), + t: GpsTime::new_unchecked(2086, 259_217.0), d_utc: 18.0, is_lse: false, params: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5), + t: GpsTime::new_unchecked(2086, 259_217.5), d_utc: 18.0, is_lse: false, params: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.0001), + t: GpsTime::new_unchecked(2086, 259_218.000_1), d_utc: 18.0, is_lse: true, params: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5), + t: GpsTime::new_unchecked(2086, 259_218.5), d_utc: 18.0, is_lse: true, params: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.0001), + t: GpsTime::new_unchecked(2086, 259_219.000_1), d_utc: 19.0, is_lse: false, params: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.5), + t: GpsTime::new_unchecked(2086, 259_219.5), d_utc: 19.0, is_lse: false, params: Some(make_p_pos_trend()), }, /* Jan 1 2020 (negative UTC linear correction) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.0), + t: GpsTime::new_unchecked(2086, 259_217.0), d_utc: 18.0, is_lse: false, params: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5), + t: GpsTime::new_unchecked(2086, 259_217.5), d_utc: 18.0, is_lse: false, params: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.0), + t: GpsTime::new_unchecked(2086, 259_218.0), d_utc: 18.0, is_lse: true, params: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5), + t: GpsTime::new_unchecked(2086, 259_218.5), d_utc: 18.0, is_lse: true, params: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.0), + t: GpsTime::new_unchecked(2086, 259_219.0), d_utc: 19.0, is_lse: false, params: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.5), + t: GpsTime::new_unchecked(2086, 259_219.5), d_utc: 19.0, is_lse: false, params: Some(make_p_neg_trend()), @@ -798,6 +802,7 @@ mod tests { } } + #[expect(clippy::too_many_lines)] #[test] fn gps2utc() { /* test leap second on 1st Jan 2020 */ @@ -841,27 +846,27 @@ mod tests { let test_cases = [ /* July 1 1981 */ TestCase { - t: GpsTime::new_unchecked(77, 259199.0), + t: GpsTime::new_unchecked(77, 259_199.0), u: UtcExpectation::new(1981, 6, 30, 23, 59, 59.0), p: None, }, TestCase { - t: GpsTime::new_unchecked(77, 259199.5), + t: GpsTime::new_unchecked(77, 259_199.5), u: UtcExpectation::new(1981, 6, 30, 23, 59, 59.5), p: None, }, TestCase { - t: GpsTime::new_unchecked(77, 259200.0), + t: GpsTime::new_unchecked(77, 259_200.0), u: UtcExpectation::new(1981, 6, 30, 23, 59, 60.0), p: None, }, TestCase { - t: GpsTime::new_unchecked(77, 259200.5), + t: GpsTime::new_unchecked(77, 259_200.5), u: UtcExpectation::new(1981, 6, 30, 23, 59, 60.5), p: None, }, TestCase { - t: GpsTime::new_unchecked(77, 259201.0), + t: GpsTime::new_unchecked(77, 259_201.0), u: UtcExpectation::new(1981, 7, 1, 00, 00, 00.0), p: None, }, @@ -920,108 +925,108 @@ mod tests { /* Jan 1 2020 (leap second announced in utc_params_t above, constant negative offset) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.0 - 0.125), + t: GpsTime::new_unchecked(2086, 259_217.0 - 0.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.0), p: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5 - 0.125), + t: GpsTime::new_unchecked(2086, 259_217.5 - 0.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.5), p: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.0 - 0.125), + t: GpsTime::new_unchecked(2086, 259_218.0 - 0.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.0), p: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5 - 0.125), + t: GpsTime::new_unchecked(2086, 259_218.5 - 0.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.5), p: Some(make_p_neg_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.0 - 0.125), + t: GpsTime::new_unchecked(2086, 259_219.0 - 0.125), u: UtcExpectation::new(2020, 1, 1, 00, 00, 00.0), p: Some(make_p_neg_offset()), }, /* Jan 1 2020 (leap second announced in utc_params_t above, constant positive offset) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.125), + t: GpsTime::new_unchecked(2086, 259_217.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.0), p: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5 + 0.125), + t: GpsTime::new_unchecked(2086, 259_217.5 + 0.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.5), p: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.125), + t: GpsTime::new_unchecked(2086, 259_218.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.0), p: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5 + 0.125), + t: GpsTime::new_unchecked(2086, 259_218.5 + 0.125), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.5), p: Some(make_p_pos_offset()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.125), + t: GpsTime::new_unchecked(2086, 259_219.125), u: UtcExpectation::new(2020, 1, 1, 00, 00, 00.0), p: Some(make_p_pos_offset()), }, /* Jan 1 2020 (leap second announced in utc_params_t above, positive UTC linear correction) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.0), + t: GpsTime::new_unchecked(2086, 259_217.0), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.0), p: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5), + t: GpsTime::new_unchecked(2086, 259_217.5), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.5), p: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.0), + t: GpsTime::new_unchecked(2086, 259_218.0), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.0), p: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5), + t: GpsTime::new_unchecked(2086, 259_218.5), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.5), p: Some(make_p_pos_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.00001), + t: GpsTime::new_unchecked(2086, 259_219.000_01), u: UtcExpectation::new(2020, 1, 1, 00, 00, 00.0), p: Some(make_p_pos_trend()), }, /* Jan 1 2020 (leap second announced in utc_params_t above, negative UTC linear correction) */ TestCase { - t: GpsTime::new_unchecked(2086, 259217.0), + t: GpsTime::new_unchecked(2086, 259_217.0), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.0), p: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259217.5), + t: GpsTime::new_unchecked(2086, 259_217.5), u: UtcExpectation::new(2019, 12, 31, 23, 59, 59.5), p: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.0), + t: GpsTime::new_unchecked(2086, 259_218.0), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.0), p: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259218.5), + t: GpsTime::new_unchecked(2086, 259_218.5), u: UtcExpectation::new(2019, 12, 31, 23, 59, 60.5), p: Some(make_p_neg_trend()), }, TestCase { - t: GpsTime::new_unchecked(2086, 259219.0), + t: GpsTime::new_unchecked(2086, 259_219.0), u: UtcExpectation::new(2020, 1, 1, 00, 00, 00.0), p: Some(make_p_neg_trend()), }, @@ -1099,13 +1104,13 @@ mod tests { Utc, ); - let converted: DateTime = swift_date.clone().into(); + let converted: DateTime = swift_date.into(); assert!((converted - expected_utc).to_std().unwrap() < epsilon); - assert_eq!(converted.year(), swift_date.year() as i32); - assert_eq!(converted.month(), swift_date.month() as u32); - assert_eq!(converted.day(), swift_date.day_of_month() as u32); - assert_eq!(converted.hour(), swift_date.hour() as u32); - assert_eq!(converted.minute(), swift_date.minute() as u32); + assert_eq!(converted.year(), i32::from(swift_date.year())); + assert_eq!(converted.month(), u32::from(swift_date.month())); + assert_eq!(converted.day(), u32::from(swift_date.day_of_month())); + assert_eq!(converted.hour(), u32::from(swift_date.hour())); + assert_eq!(converted.minute(), u32::from(swift_date.minute())); assert_eq!(converted.second(), swift_date.seconds() as u32); } } diff --git a/swiftnav/tests/reference_frames.rs b/swiftnav/tests/reference_frames.rs index 0bcbf3d..ace7d8a 100644 --- a/swiftnav/tests/reference_frames.rs +++ b/swiftnav/tests/reference_frames.rs @@ -889,7 +889,7 @@ fn dref91_r2016() { ReferenceFrame::ITRF2020, ECEF::new(3842152.805, 563402.164, 5042888.600), None, - UtcTime::from_parts(2023, 02, 22, 0, 0, 0.).to_gps_hardcoded(), + UtcTime::from_parts(2023, 2, 22, 0, 0, 0.).to_gps_hardcoded(), ); let transformations = TransformationRepository::from_builtin(); From df779c0814798e850719d8fb78332aa4fd210b54 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:19:42 -0700 Subject: [PATCH 02/17] add a few nmea types --- swiftnav/Cargo.toml | 16 ++++++++-------- swiftnav/src/coords/llh.rs | 22 ++++++++++++++++++++++ swiftnav/src/coords/mod.rs | 2 ++ swiftnav/src/lib.rs | 1 + 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/swiftnav/Cargo.toml b/swiftnav/Cargo.toml index 5d27706..921349e 100644 --- a/swiftnav/Cargo.toml +++ b/swiftnav/Cargo.toml @@ -10,18 +10,18 @@ license = "LGPL-3.0" rust-version = "1.85.0" [dependencies] -rustversion = "1.0" -chrono = { version = "0.4", optional = true } -strum = { version = "0.27", features = ["derive"] } -nalgebra = "0.33" -thiserror = "2.0" +bon = "3.8.1" +rustversion = "1.0.22" +chrono = { version = "0.4.42", optional = true } +strum = { version = "0.27.2", features = ["derive"] } +nalgebra = "0.34.1" +thiserror = "2.0.17" [dev-dependencies] float_eq = "1.0.1" -proptest = "1.5" +proptest = "1.8.0" # This tells docs.rs to include the katex header for math formatting # To do this locally [package.metadata.docs.rs] -rustdoc-args = [ "--html-in-header", "katex-header.html" ] - +rustdoc-args = ["--html-in-header", "katex-header.html"] diff --git a/swiftnav/src/coords/llh.rs b/swiftnav/src/coords/llh.rs index 174fbb2..58b03b3 100644 --- a/swiftnav/src/coords/llh.rs +++ b/swiftnav/src/coords/llh.rs @@ -1,3 +1,5 @@ +use crate::coords::{LatitudinalHemisphere, LongitudinalHemisphere}; + use super::{Ellipsoid, ECEF, WGS84}; use nalgebra::Vector3; @@ -44,12 +46,32 @@ impl LLHDegrees { self.0.x } + /// Get the latitudinal hemisphere + #[must_use] + pub fn latitudinal_hemisphere(&self) -> LatitudinalHemisphere { + if self.latitude() >= 0.0 { + LatitudinalHemisphere::North + } else { + LatitudinalHemisphere::South + } + } + /// Get the longitude component #[must_use] pub fn longitude(&self) -> f64 { self.0.y } + /// Get the longitudinal hemisphere + #[must_use] + pub fn longitudinal_hemisphere(&self) -> LongitudinalHemisphere { + if self.longitude() >= 0.0 { + LongitudinalHemisphere::East + } else { + LongitudinalHemisphere::West + } + } + /// Get the height component #[must_use] pub fn height(&self) -> f64 { diff --git a/swiftnav/src/coords/mod.rs b/swiftnav/src/coords/mod.rs index 198f035..fb3d202 100644 --- a/swiftnav/src/coords/mod.rs +++ b/swiftnav/src/coords/mod.rs @@ -76,11 +76,13 @@ mod ecef; mod ellipsoid; +mod hemisphere; mod llh; mod ned; pub use ecef::*; pub use ellipsoid::*; +pub use hemisphere::*; pub use llh::*; pub use ned::*; diff --git a/swiftnav/src/lib.rs b/swiftnav/src/lib.rs index 2dd39f4..5ae6d3d 100644 --- a/swiftnav/src/lib.rs +++ b/swiftnav/src/lib.rs @@ -58,6 +58,7 @@ pub mod coords; pub mod edc; mod math; +pub mod nmea; pub mod reference_frame; pub mod signal; pub mod time; From 83ba99057eaf96b0cbc4560fc4587bd7b663d36a Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:20:48 -0700 Subject: [PATCH 03/17] clean up test --- swiftnav/src/coords/hemisphere.rs | 43 +++++++ swiftnav/src/nmea/gga.rs | 191 ++++++++++++++++++++++++++++++ swiftnav/src/nmea/mod.rs | 5 + swiftnav/src/nmea/source.rs | 16 +++ 4 files changed, 255 insertions(+) create mode 100644 swiftnav/src/coords/hemisphere.rs create mode 100644 swiftnav/src/nmea/gga.rs create mode 100644 swiftnav/src/nmea/mod.rs create mode 100644 swiftnav/src/nmea/source.rs diff --git a/swiftnav/src/coords/hemisphere.rs b/swiftnav/src/coords/hemisphere.rs new file mode 100644 index 0000000..fd49bd3 --- /dev/null +++ b/swiftnav/src/coords/hemisphere.rs @@ -0,0 +1,43 @@ +use core::fmt; + +pub enum LatitudinalHemisphere { + North, + South, +} + +impl fmt::Display for LatitudinalHemisphere { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LatitudinalHemisphere::North => write!(f, "N"), + LatitudinalHemisphere::South => write!(f, "S"), + } + } +} + +pub enum LongitudinalHemisphere { + East, + West, +} + +impl fmt::Display for LongitudinalHemisphere { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LongitudinalHemisphere::East => write!(f, "E"), + LongitudinalHemisphere::West => write!(f, "W"), + } + } +} + +pub enum Hemisphere { + Latitudinal(LatitudinalHemisphere), + Longitudinal(LongitudinalHemisphere), +} + +impl fmt::Display for Hemisphere { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Hemisphere::Latitudinal(hemisphere) => write!(f, "{hemisphere}"), + Hemisphere::Longitudinal(hemisphere) => write!(f, "{hemisphere}"), + } + } +} diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs new file mode 100644 index 0000000..2983c22 --- /dev/null +++ b/swiftnav/src/nmea/gga.rs @@ -0,0 +1,191 @@ +use std::{ + fmt::{self}, + time::Duration, +}; + +use bon::Builder; +use chrono::{DateTime, Timelike, Utc}; + +use crate::{coords::LLHDegrees, nmea::Source}; + +/// Quality of GPS solution +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum GPSQuality { + /// Fix not available or invalid + NoFix, + /// GPS SPS Mode, fix valid + GPS, + /// Differential GPS, SPS Mode, fix valid + DGPS, + /// GPS PPS (pulse per second), fix valid + PPS, + /// RTK (real time kinematic). System used in RTK mode with fixed integers + RTK, + /// Float RTK, satelite system used in RTK mode, floating integers + FRTK, + /// Estimated (dead reckoning) mode. + Estimated, + /// Manual input mode + Manual, + /// Simulated mode + Simulated, +} + +impl fmt::Display for GPSQuality { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GPSQuality::NoFix => write!(f, "0"), + GPSQuality::GPS => write!(f, "1"), + GPSQuality::DGPS => write!(f, "2"), + GPSQuality::PPS => write!(f, "3"), + GPSQuality::RTK => write!(f, "4"), + GPSQuality::FRTK => write!(f, "5"), + GPSQuality::Estimated => write!(f, "6"), + GPSQuality::Manual => write!(f, "7"), + GPSQuality::Simulated => write!(f, "8"), + } + } +} + +/// Geographic coordinates including altitude, GPS solution quality, DGPS usage information. +#[derive(Debug, PartialEq, Clone, Builder)] +pub struct GGA { + /// Navigational system. + #[builder(default = Source::GPS)] + pub source: Source, + /// Time of fix in UTC. + #[builder(default = Utc::now())] + pub time: DateTime, + /// Latitude, longitude and height in degrees. + pub llh: LLHDegrees, + /// Quality of GPS solution. + #[builder(default = GPSQuality::GPS)] + pub gps_quality: GPSQuality, + /// Sattelites in use + pub sat_in_use: u8, + /// Horizontal dilusion of presicion + pub hdop: f32, + /// The difference between reference ellipsoid surface and mean-sea-level. + pub geoidal_separation: Option, + /// DGPS data age. None if DGPS not in use. + pub age_dgps: Option, + /// ID of reference DGPS station used for fix. None if DGPS not in use. + pub dgps_station_id: Option, +} + +impl GGA { + // converts the GGA struct into an NMEA sentence + #[must_use] + pub fn to_sentence(&self) -> String { + // NOTE(ted): We are formatting here a bit strange because for some ungodly reason, + // chrono chose not to allow for abitrary fractional seconds precision when formatting + // Construct timestamp in HHMMSS.SS format + let hour = self.time.hour(); + let minute = self.time.minute(); + let second = f64::from(self.time.second()); + let second_fracs = f64::from(self.time.nanosecond()) / 1_000_000_000.0; + + let timestamp = format!("{hour}{minute}{:.2}", second + second_fracs); + + let latitude = self.llh.latitude(); + let latitudinal_hemisphere = self.llh.latitudinal_hemisphere(); + + let longitude = self.llh.longitude(); + let longitudinal_hemisphere = self.llh.longitudinal_hemisphere(); + + let gps_quality = self.gps_quality; + + let sat_in_use = self.sat_in_use; + + let hdop = self.hdop; + + // NOTE(ted): This is actually not the right value to use, however, we don't really use height for finding information like nearest station so it's ok to use for now + let height = "0.0"; + + // if DGPS is not used, this should be a null field + let age_dgps = if matches!(gps_quality, GPSQuality::DGPS) { + let age = self.age_dgps.map_or(0.0, |age| age.as_secs_f64()); + + format!("{age:.1}") + } else { + String::new() + }; + + let geoidal_separation = self + .geoidal_separation + .map_or(String::new(), |sep| format!("{sep:.2}")); + + let dgps_station_id = self + .dgps_station_id + .map_or(String::new(), |id| id.to_string()); + + let sentence = format!( + "$GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop:.1},{height:.6},M,{geoidal_separation},{age_dgps:.1},{dgps_station_id}", + ); + + // NOTE(ted): We should skip the first character in the sentence (the '$') + // https://forum.arduino.cc/t/nmea-checksums-explained/1046083 + let checksum = nmea_checksum(&sentence[1..]); + + format!("{sentence}*{checksum}\r\n") + } +} + +fn u8_to_nibbles(byte: u8) -> (u8, u8) { + // The high nibble is obtained by shifting the byte 4 bits to the right. + // This discards the lower 4 bits and moves the upper 4 bits into the lower 4 bit positions. + let high_nibble = byte >> 4; + + // The low nibble is obtained by masking the byte with 0x0F (binary 0000_1111). + // This keeps only the lower 4 bits and sets the upper 4 bits to zero. + let low_nibble = byte & 0x0F; + + (high_nibble, low_nibble) +} + +/// Convert a nibble (4 bits) to its ASCII character representation +fn u8_to_ascii_char(nibble: u8) -> char { + // if we are between 0 and 9, we map to '0' to '9' + if nibble <= 0x9 { + (nibble + b'0') as char + // else, we map to 'A' to 'F' + } else { + (nibble - 10 + b'A') as char + } +} + +fn nmea_checksum(sentence: &str) -> String { + let mut checksum = 0; + for byte in sentence.bytes() { + checksum ^= byte; + } + + let (nibble1, nibble2) = u8_to_nibbles(checksum); + + let char1 = u8_to_ascii_char(nibble1); + let char2 = u8_to_ascii_char(nibble2); + + format!("{char1}{char2}") +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn gga_can_be_turned_into_an_nmea_sentence() { + let gga = GGA::builder() + .sat_in_use(12) + .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) + .hdop(0.9) + .llh(super::LLHDegrees::new(37.7749, -122.4194, 10.0)) + .build(); + + let sentence = gga.to_sentence(); + + assert_eq!( + sentence, + "$GPGGA,0189.00,37.774900,N,-122.419400,W,1,12,0.9,0.0,M,,,*26\r\n" + ); + } +} diff --git a/swiftnav/src/nmea/mod.rs b/swiftnav/src/nmea/mod.rs new file mode 100644 index 0000000..c0898f5 --- /dev/null +++ b/swiftnav/src/nmea/mod.rs @@ -0,0 +1,5 @@ +mod gga; +mod source; + +pub use gga::*; +pub use source::*; diff --git a/swiftnav/src/nmea/source.rs b/swiftnav/src/nmea/source.rs new file mode 100644 index 0000000..1462bad --- /dev/null +++ b/swiftnav/src/nmea/source.rs @@ -0,0 +1,16 @@ +/// Source of NMEA sentence like GPS, GLONASS or other. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Source { + /// USA Global Positioning System + GPS = 0b1, + /// Russian Federation GLONASS + GLONASS = 0b10, + /// European Union Gallileo + Gallileo = 0b100, + /// China's Beidou + Beidou = 0b1000, + /// Global Navigation Sattelite System. Some combination of other systems. Depends on receiver model, receiver settings, etc.. + GNSS = 0b10000, + /// `MediaTek` NMEA packet protocol + MTK = 0b10_0000, +} From ed88d7df02f1b2aefeee32e2acfb2b965e2237fe Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:23:42 -0700 Subject: [PATCH 04/17] update rust dep --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 371ddcb..cd18201 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,7 +65,7 @@ jobs: # MSRV - os: ubuntu-latest binary_target: x86_64-unknown-linux-gnu - toolchain: 1.85.0 + toolchain: 1.90.0 steps: - uses: actions/checkout@v5 with: From e1421123f15a32fbf757a756571b93db67f199a7 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:19:36 -0700 Subject: [PATCH 05/17] little refactor --- swiftnav/src/nmea/checksum.rs | 43 ++++++++++++++++++++++++++++++++ swiftnav/src/nmea/gga.rs | 46 ++++------------------------------- swiftnav/src/nmea/mod.rs | 2 ++ 3 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 swiftnav/src/nmea/checksum.rs diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs new file mode 100644 index 0000000..b0a21b5 --- /dev/null +++ b/swiftnav/src/nmea/checksum.rs @@ -0,0 +1,43 @@ +fn u8_to_nibbles(byte: u8) -> (u8, u8) { + // The high nibble is obtained by shifting the byte 4 bits to the right. + // This discards the lower 4 bits and moves the upper 4 bits into the lower 4 bit positions. + let high_nibble = byte >> 4; + + // The low nibble is obtained by masking the byte with 0x0F (binary 0000_1111). + // This keeps only the lower 4 bits and sets the upper 4 bits to zero. + let low_nibble = byte & 0x0F; + + (high_nibble, low_nibble) +} + +/// Convert a nibble (4 bits) to its ASCII character representation +fn u8_to_ascii_char(nibble: u8) -> char { + if nibble <= 0x9 { + (nibble + b'0') as char + } else { + (nibble - 10 + b'A') as char + } +} + +// Calculate the NMEA checksum for a given sentence +// https://forum.arduino.cc/t/nmea-checksums-explained/1046083 +#[must_use] +pub fn calculate_checksum(sentence: &str) -> String { + let mut checksum = 0; + + for (i, byte) in sentence.bytes().enumerate() { + // Skip the starting '$' and the '*' before the checksum + if (i == 0 && byte == b'$') || byte == b'*' { + continue; + } + + checksum ^= byte; + } + + let (nibble1, nibble2) = u8_to_nibbles(checksum); + + let char1 = u8_to_ascii_char(nibble1); + let char2 = u8_to_ascii_char(nibble2); + + format!("{char1}{char2}") +} diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index 2983c22..133f160 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -6,7 +6,10 @@ use std::{ use bon::Builder; use chrono::{DateTime, Timelike, Utc}; -use crate::{coords::LLHDegrees, nmea::Source}; +use crate::{ + coords::LLHDegrees, + nmea::{self, Source}, +}; /// Quality of GPS solution #[derive(Debug, PartialEq, Clone, Copy)] @@ -123,51 +126,12 @@ impl GGA { "$GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop:.1},{height:.6},M,{geoidal_separation},{age_dgps:.1},{dgps_station_id}", ); - // NOTE(ted): We should skip the first character in the sentence (the '$') - // https://forum.arduino.cc/t/nmea-checksums-explained/1046083 - let checksum = nmea_checksum(&sentence[1..]); + let checksum = nmea::calculate_checksum(&sentence); format!("{sentence}*{checksum}\r\n") } } -fn u8_to_nibbles(byte: u8) -> (u8, u8) { - // The high nibble is obtained by shifting the byte 4 bits to the right. - // This discards the lower 4 bits and moves the upper 4 bits into the lower 4 bit positions. - let high_nibble = byte >> 4; - - // The low nibble is obtained by masking the byte with 0x0F (binary 0000_1111). - // This keeps only the lower 4 bits and sets the upper 4 bits to zero. - let low_nibble = byte & 0x0F; - - (high_nibble, low_nibble) -} - -/// Convert a nibble (4 bits) to its ASCII character representation -fn u8_to_ascii_char(nibble: u8) -> char { - // if we are between 0 and 9, we map to '0' to '9' - if nibble <= 0x9 { - (nibble + b'0') as char - // else, we map to 'A' to 'F' - } else { - (nibble - 10 + b'A') as char - } -} - -fn nmea_checksum(sentence: &str) -> String { - let mut checksum = 0; - for byte in sentence.bytes() { - checksum ^= byte; - } - - let (nibble1, nibble2) = u8_to_nibbles(checksum); - - let char1 = u8_to_ascii_char(nibble1); - let char2 = u8_to_ascii_char(nibble2); - - format!("{char1}{char2}") -} - #[cfg(test)] mod test { use super::*; diff --git a/swiftnav/src/nmea/mod.rs b/swiftnav/src/nmea/mod.rs index c0898f5..8cdfef7 100644 --- a/swiftnav/src/nmea/mod.rs +++ b/swiftnav/src/nmea/mod.rs @@ -1,5 +1,7 @@ +mod checksum; mod gga; mod source; +pub use checksum::*; pub use gga::*; pub use source::*; From 46ff14b106b15f664efcf9d3a920d9e62c3a00ff Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:42:50 -0700 Subject: [PATCH 06/17] add more tests --- swiftnav/src/nmea/checksum.rs | 49 ++++++++++++++++++++++++++++++++- swiftnav/src/nmea/gga.rs | 51 ++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index b0a21b5..ee3d00e 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -25,9 +25,16 @@ fn u8_to_ascii_char(nibble: u8) -> char { pub fn calculate_checksum(sentence: &str) -> String { let mut checksum = 0; + let mut at_checksum_validation_value = false; + for (i, byte) in sentence.bytes().enumerate() { // Skip the starting '$' and the '*' before the checksum - if (i == 0 && byte == b'$') || byte == b'*' { + if (i == 0 && byte == b'$') || at_checksum_validation_value { + continue; + } + + if byte == b'*' { + at_checksum_validation_value = true; continue; } @@ -41,3 +48,43 @@ pub fn calculate_checksum(sentence: &str) -> String { format!("{char1}{char2}") } + +#[cfg(test)] +mod tests { + #[test] + fn test_calculate_checksum() { + let sentence = "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "47"); + } + + #[test] + fn calculate_checksum_ignores_dollar_and_asterisk_tails() { + // NOTE(ted): All of these examples should produce the same checksum + + // check with '$' + let sentence = "$GPGGA,0189.00,34.052200,N,-118.243700,W,2,8,1.2,0.0,M,1.00,2,42"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "37"); + + //check with '$' and '*' + let sentence = "$GPGGA,0189.00,34.052200,N,-118.243700,W,2,8,1.2,0.0,M,1.00,2,42*"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "37"); + + //check with '$' and '*' and fake checksum + let sentence = "$GPGGA,0189.00,34.052200,N,-118.243700,W,2,8,1.2,0.0,M,1.00,2,42*00"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "37"); + + //check '*' + let sentence = "GPGGA,0189.00,34.052200,N,-118.243700,W,2,8,1.2,0.0,M,1.00,2,42*"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "37"); + + //check '*' and fake checksum + let sentence = "GPGGA,0189.00,34.052200,N,-118.243700,W,2,8,1.2,0.0,M,1.00,2,42*00"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "37"); + } +} diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index 133f160..bebaea6 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -118,9 +118,12 @@ impl GGA { .geoidal_separation .map_or(String::new(), |sep| format!("{sep:.2}")); - let dgps_station_id = self - .dgps_station_id - .map_or(String::new(), |id| id.to_string()); + let dgps_station_id = if matches!(gps_quality, GPSQuality::DGPS) { + self.dgps_station_id + .map_or(String::new(), |id| id.to_string()) + } else { + String::new() + }; let sentence = format!( "$GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop:.1},{height:.6},M,{geoidal_separation},{age_dgps:.1},{dgps_station_id}", @@ -152,4 +155,46 @@ mod test { "$GPGGA,0189.00,37.774900,N,-122.419400,W,1,12,0.9,0.0,M,,,*26\r\n" ); } + + #[test] + fn gga_with_dgps_can_be_turned_into_an_nmea_sentence() { + let gga = GGA::builder() + .sat_in_use(8) + .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) + .hdop(1.2) + .llh(super::LLHDegrees::new(34.0522, -118.2437, 15.0)) + .gps_quality(GPSQuality::DGPS) + .age_dgps(Duration::from_secs_f64(2.5)) + .geoidal_separation(1.0) + .dgps_station_id(42) + .build(); + + let sentence = gga.to_sentence(); + + assert_eq!( + sentence, + "$GPGGA,0189.00,34.052200,N,-118.243700,W,2,8,1.2,0.0,M,1.00,2,42*37\r\n" + ); + } + + #[test] + fn gga_with_dgps_fields_that_is_not_dgps_is_ignored() { + let gga = GGA::builder() + .sat_in_use(8) + .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) + .hdop(1.2) + .llh(super::LLHDegrees::new(34.0522, -118.2437, 15.0)) + .gps_quality(GPSQuality::GPS) + .age_dgps(Duration::from_secs_f64(2.5)) + .geoidal_separation(1.0) + .dgps_station_id(42) + .build(); + + let sentence = gga.to_sentence(); + + assert_eq!( + sentence, + "$GPGGA,0189.00,34.052200,N,-118.243700,W,1,8,1.2,0.0,M,1.00,,*00\r\n" + ); + } } From 9be6411ef4e389a6d9a80b365e151b69ceec73e3 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:49:39 -0700 Subject: [PATCH 07/17] add comment --- swiftnav/src/nmea/checksum.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index ee3d00e..dca8298 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -25,16 +25,17 @@ fn u8_to_ascii_char(nibble: u8) -> char { pub fn calculate_checksum(sentence: &str) -> String { let mut checksum = 0; - let mut at_checksum_validation_value = false; + // this flag indicates if we have reached the '*' character or anything beyond that such as the actual checksum value or the end sequence () + let mut at_or_past_checksum_field = false; for (i, byte) in sentence.bytes().enumerate() { // Skip the starting '$' and the '*' before the checksum - if (i == 0 && byte == b'$') || at_checksum_validation_value { + if (i == 0 && byte == b'$') || at_or_past_checksum_field { continue; } if byte == b'*' { - at_checksum_validation_value = true; + at_or_past_checksum_field = true; continue; } From 9980a32f7c86312787fc6b04d0084bf328772ed0 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:10:06 -0700 Subject: [PATCH 08/17] make sure to zero out top 4 bits in high nibble --- swiftnav/src/nmea/checksum.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index dca8298..6d4a3bc 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -1,7 +1,7 @@ fn u8_to_nibbles(byte: u8) -> (u8, u8) { // The high nibble is obtained by shifting the byte 4 bits to the right. // This discards the lower 4 bits and moves the upper 4 bits into the lower 4 bit positions. - let high_nibble = byte >> 4; + let high_nibble = (byte >> 4) & 0x0F; // The low nibble is obtained by masking the byte with 0x0F (binary 0000_1111). // This keeps only the lower 4 bits and sets the upper 4 bits to zero. From d6f49f65e85caef69dbb7dded71965a05f8c2e0f Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:27:53 -0700 Subject: [PATCH 09/17] add test for gga sentence len --- swiftnav/src/nmea/gga.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index bebaea6..851f936 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -126,12 +126,14 @@ impl GGA { }; let sentence = format!( - "$GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop:.1},{height:.6},M,{geoidal_separation},{age_dgps:.1},{dgps_station_id}", + "GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop:.1},{height:.6},M,{geoidal_separation},{age_dgps:.1},{dgps_station_id}", ); let checksum = nmea::calculate_checksum(&sentence); - format!("{sentence}*{checksum}\r\n") + let sentence = format!("${sentence}*{checksum}\r\n"); + + sentence } } @@ -197,4 +199,27 @@ mod test { "$GPGGA,0189.00,34.052200,N,-118.243700,W,1,8,1.2,0.0,M,1.00,,*00\r\n" ); } + + #[test] + fn gga_sentence_is_always_less_than_82_characters() { + // we are going to set some very large decimal places and the highest possible values in terms of character count to ensure our sentence is always below 82 characters + let gga = GGA::builder() + .sat_in_use(12) + .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) + .hdop(1.210_123_1) + .llh(super::LLHDegrees::new( + -90.000_000_001, + -180.000_000_000_1, + 1_000.000_000_000, + )) + .gps_quality(GPSQuality::DGPS) + .age_dgps(Duration::from_secs_f64(2.500_000_000_001)) + .geoidal_separation(1.00) + .dgps_station_id(1023) // 1023 is the max value for a 4 digit station ID + .build(); + + let sentence = gga.to_sentence(); + + assert!(sentence.len() < 82); + } } From e12b6db7cba515f878bdd623a54bdad3026a24f1 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:46:04 -0700 Subject: [PATCH 10/17] update option for gga --- swiftnav/src/nmea/gga.rs | 42 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index 851f936..8adcf69 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -53,27 +53,24 @@ impl fmt::Display for GPSQuality { /// Geographic coordinates including altitude, GPS solution quality, DGPS usage information. #[derive(Debug, PartialEq, Clone, Builder)] pub struct GGA { - /// Navigational system. - #[builder(default = Source::GPS)] - pub source: Source, /// Time of fix in UTC. #[builder(default = Utc::now())] pub time: DateTime, /// Latitude, longitude and height in degrees. pub llh: LLHDegrees, /// Quality of GPS solution. - #[builder(default = GPSQuality::GPS)] + #[builder(default = GPSQuality::NoFix)] pub gps_quality: GPSQuality, /// Sattelites in use - pub sat_in_use: u8, + pub sat_in_use: Option, /// Horizontal dilusion of presicion - pub hdop: f32, + pub hdop: Option, /// The difference between reference ellipsoid surface and mean-sea-level. pub geoidal_separation: Option, - /// DGPS data age. None if DGPS not in use. + /// DGPS data age pub age_dgps: Option, - /// ID of reference DGPS station used for fix. None if DGPS not in use. - pub dgps_station_id: Option, + /// ID of reference DGPS station used for fix + pub reference_station_id: Option, } impl GGA { @@ -98,35 +95,24 @@ impl GGA { let gps_quality = self.gps_quality; - let sat_in_use = self.sat_in_use; + let sat_in_use = self.sat_in_use.map_or(String::new(), |sat| sat.to_string()); - let hdop = self.hdop; + let hdop = self.hdop.map_or(String::new(), |hdop| format!("{hdop:.1}")); // NOTE(ted): This is actually not the right value to use, however, we don't really use height for finding information like nearest station so it's ok to use for now let height = "0.0"; - // if DGPS is not used, this should be a null field - let age_dgps = if matches!(gps_quality, GPSQuality::DGPS) { - let age = self.age_dgps.map_or(0.0, |age| age.as_secs_f64()); - - format!("{age:.1}") - } else { - String::new() - }; + let age_dgps = self.age_dgps.map_or(0.0, |age| age.as_secs_f64()); let geoidal_separation = self .geoidal_separation .map_or(String::new(), |sep| format!("{sep:.2}")); - let dgps_station_id = if matches!(gps_quality, GPSQuality::DGPS) { - self.dgps_station_id + let reference_station_id = self.reference_station_id .map_or(String::new(), |id| id.to_string()) - } else { - String::new() - }; let sentence = format!( - "GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop:.1},{height:.6},M,{geoidal_separation},{age_dgps:.1},{dgps_station_id}", + "GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop},{height:.6},M,{geoidal_separation},{age_dgps:.1},{reference_station_id}", ); let checksum = nmea::calculate_checksum(&sentence); @@ -168,7 +154,7 @@ mod test { .gps_quality(GPSQuality::DGPS) .age_dgps(Duration::from_secs_f64(2.5)) .geoidal_separation(1.0) - .dgps_station_id(42) + .reference_station_id(42) .build(); let sentence = gga.to_sentence(); @@ -189,7 +175,7 @@ mod test { .gps_quality(GPSQuality::GPS) .age_dgps(Duration::from_secs_f64(2.5)) .geoidal_separation(1.0) - .dgps_station_id(42) + .reference_station_id(42) .build(); let sentence = gga.to_sentence(); @@ -215,7 +201,7 @@ mod test { .gps_quality(GPSQuality::DGPS) .age_dgps(Duration::from_secs_f64(2.500_000_000_001)) .geoidal_separation(1.00) - .dgps_station_id(1023) // 1023 is the max value for a 4 digit station ID + .reference_station_id(1023) // 1023 is the max value for a 4 digit station ID .build(); let sentence = gga.to_sentence(); From d68e21b8ae9c5b0755a348dd4db1c5a459b4dbe3 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:46:34 -0700 Subject: [PATCH 11/17] fix typo --- swiftnav/src/nmea/gga.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index 8adcf69..b0e2d52 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -8,7 +8,7 @@ use chrono::{DateTime, Timelike, Utc}; use crate::{ coords::LLHDegrees, - nmea::{self, Source}, + nmea::{self}, }; /// Quality of GPS solution @@ -108,8 +108,9 @@ impl GGA { .geoidal_separation .map_or(String::new(), |sep| format!("{sep:.2}")); - let reference_station_id = self.reference_station_id - .map_or(String::new(), |id| id.to_string()) + let reference_station_id = self + .reference_station_id + .map_or(String::new(), |id| id.to_string()); let sentence = format!( "GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop},{height:.6},M,{geoidal_separation},{age_dgps:.1},{reference_station_id}", From 204de5096b1f8d074115fea9f329a091ae7b6cd5 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 18:59:57 +0000 Subject: [PATCH 12/17] rustfmt (#138) --- rustfmt.toml | 8 +++++++ swiftnav/Cargo.toml | 4 ++-- swiftnav/src/coords/ecef.rs | 3 ++- swiftnav/src/coords/llh.rs | 12 ++++++---- swiftnav/src/coords/mod.rs | 25 ++++++++++---------- swiftnav/src/edc.rs | 3 ++- swiftnav/src/math.rs | 15 ++++++------ swiftnav/src/nmea/checksum.rs | 3 ++- swiftnav/src/nmea/gga.rs | 36 +++++++++-------------------- swiftnav/src/nmea/source.rs | 3 ++- swiftnav/src/reference_frame/mod.rs | 25 ++++++++++++-------- swiftnav/src/signal/code.rs | 2 +- swiftnav/src/signal/mod.rs | 6 +++-- swiftnav/src/time/gnss.rs | 5 ++-- swiftnav/src/time/mjd.rs | 17 ++++++++++++-- swiftnav/src/time/utc.rs | 17 ++++++++++---- 16 files changed, 106 insertions(+), 78 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c9a1eda --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,8 @@ +max_width = 100 +wrap_comments = true +comment_width = 100 +format_strings = true +edition = "2024" +imports_granularity = "Crate" +group_imports = "StdExternalCrate" +style_edition = "2024" diff --git a/swiftnav/Cargo.toml b/swiftnav/Cargo.toml index 921349e..094d805 100644 --- a/swiftnav/Cargo.toml +++ b/swiftnav/Cargo.toml @@ -12,14 +12,14 @@ rust-version = "1.85.0" [dependencies] bon = "3.8.1" rustversion = "1.0.22" -chrono = { version = "0.4.42", optional = true } +chrono = { version = "0.4.42" } strum = { version = "0.27.2", features = ["derive"] } nalgebra = "0.34.1" thiserror = "2.0.17" [dev-dependencies] float_eq = "1.0.1" -proptest = "1.8.0" +proptest = "1.9.0" # This tells docs.rs to include the katex header for math formatting # To do this locally diff --git a/swiftnav/src/coords/ecef.rs b/swiftnav/src/coords/ecef.rs index 9774214..8ef2038 100644 --- a/swiftnav/src/coords/ecef.rs +++ b/swiftnav/src/coords/ecef.rs @@ -1,6 +1,7 @@ -use nalgebra::Vector3; use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; +use nalgebra::Vector3; + use crate::{ coords::{AzimuthElevation, Ellipsoid, LLHDegrees, LLHRadians, NED, WGS84}, math, diff --git a/swiftnav/src/coords/llh.rs b/swiftnav/src/coords/llh.rs index 58b03b3..beec5e7 100644 --- a/swiftnav/src/coords/llh.rs +++ b/swiftnav/src/coords/llh.rs @@ -1,11 +1,12 @@ -use crate::coords::{LatitudinalHemisphere, LongitudinalHemisphere}; - -use super::{Ellipsoid, ECEF, WGS84}; use nalgebra::Vector3; +use super::{ECEF, Ellipsoid, WGS84}; +use crate::coords::{LatitudinalHemisphere, LongitudinalHemisphere}; + /// WGS84 geodetic coordinates (Latitude, Longitude, Height), with angles in degrees. /// -/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above the ellipsoid in meters +/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above +/// the ellipsoid in meters #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)] pub struct LLHDegrees(Vector3); @@ -157,7 +158,8 @@ impl AsMut> for LLHDegrees { /// WGS84 geodetic coordinates (Latitude, Longitude, Height), with angles in radians. /// -/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above the ellipsoid in meters +/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above +/// the ellipsoid in meters #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)] pub struct LLHRadians(Vector3); diff --git a/swiftnav/src/coords/mod.rs b/swiftnav/src/coords/mod.rs index fb3d202..804bae7 100644 --- a/swiftnav/src/coords/mod.rs +++ b/swiftnav/src/coords/mod.rs @@ -44,11 +44,10 @@ //! the more common algortihms based on the Newton-Raphson method. //! //! ## References -//! * "A comparison of methods used in rectangular to Geodetic Coordinates -//! Transformations", Burtch R. R. (2006), American Congress for Surveying -//! and Mapping Annual Conference. Orlando, Florida. -//! * "Transformation from Cartesian to Geodetic Coordinates Accelerated by -//! Halley’s Method", T. Fukushima (2006), Journal of Geodesy. +//! * "A comparison of methods used in rectangular to Geodetic Coordinates Transformations", Burtch +//! R. R. (2006), American Congress for Surveying and Mapping Annual Conference. Orlando, Florida. +//! * "Transformation from Cartesian to Geodetic Coordinates Accelerated by Halley’s Method", T. +//! Fukushima (2006), Journal of Geodesy. //! //! # Examples //! @@ -84,16 +83,19 @@ pub use ecef::*; pub use ellipsoid::*; pub use hemisphere::*; pub use llh::*; +use nalgebra::Vector2; pub use ned::*; use crate::{reference_frame::ReferenceFrame, time::GpsTime}; -use nalgebra::Vector2; -/// WGS84 local horizontal coordinates consisting of an Azimuth and Elevation, with angles stored as radians +/// WGS84 local horizontal coordinates consisting of an Azimuth and Elevation, with angles stored as +/// radians /// -/// Azimuth can range from $0$ to $2\pi$. North has an azimuth of $0$, east has an azimuth of $\frac{\pi}{2}$ +/// Azimuth can range from $0$ to $2\pi$. North has an azimuth of $0$, east has an azimuth of +/// $\frac{\pi}{2}$ /// -/// Elevation can range from $-\frac{\pi}{2}$ to $\frac{\pi}{2}$. Up has an elevation of $\frac{\pi}{2}$, down an elevation of $-\frac{\pi}{2}$ +/// Elevation can range from $-\frac{\pi}{2}$ to $\frac{\pi}{2}$. Up has an elevation of +/// $\frac{\pi}{2}$, down an elevation of $-\frac{\pi}{2}$ #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)] pub struct AzimuthElevation(Vector2); @@ -292,11 +294,10 @@ impl Coordinate { #[cfg(test)] mod tests { use float_eq::assert_float_eq; - - use crate::time::UtcTime; + use proptest::prelude::*; use super::*; - use proptest::prelude::*; + use crate::time::UtcTime; /* Maximum allowable error in quantities with units of length (in meters). */ const MAX_DIST_ERROR_M: f64 = 1e-6; diff --git a/swiftnav/src/edc.rs b/swiftnav/src/edc.rs index c593e3b..402f0d4 100644 --- a/swiftnav/src/edc.rs +++ b/swiftnav/src/edc.rs @@ -326,9 +326,10 @@ pub fn compute_crc24q(buf: &[u8], initial_value: u32) -> u32 { #[cfg(test)] mod tests { - use super::*; use proptest::prelude::*; + use super::*; + const TEST_DATA: &[u8] = "123456789".as_bytes(); /// Helper function to append a CRC-24Q value as 3 bytes (big-endian) to a buffer diff --git a/swiftnav/src/math.rs b/swiftnav/src/math.rs index 47977fb..38026d8 100644 --- a/swiftnav/src/math.rs +++ b/swiftnav/src/math.rs @@ -10,11 +10,7 @@ /// We define a `const` max function since [`std::cmp::max`] isn't `const` pub(crate) const fn compile_time_max_u16(a: u16, b: u16) -> u16 { - if b < a { - a - } else { - b - } + if b < a { a } else { b } } /// Computes the square root of a given number at compile time using the Newton-Raphson method. @@ -35,7 +31,8 @@ pub(crate) const fn compile_time_max_u16(a: u16, b: u16) -> u16 { /// # Notes /// /// - This function is marked as `const`, allowing it to be evaluated at compile time. -/// - The algorithm iteratively refines the approximation of the square root until the result stabilizes. +/// - The algorithm iteratively refines the approximation of the square root until the result +/// stabilizes. #[expect(clippy::many_single_char_names, reason = "It's math, whatyagonnado?")] pub(crate) const fn compile_time_sqrt(s: f64) -> f64 { assert!( @@ -57,7 +54,8 @@ pub(crate) const fn compile_time_sqrt(s: f64) -> f64 { x } -/// Calculate the rotation matrix for rotating between an [`crate::coords::ECEF`] and [`crate::coords::NED`] frames +/// Calculate the rotation matrix for rotating between an [`crate::coords::ECEF`] and +/// [`crate::coords::NED`] frames #[must_use] pub(crate) fn ecef2ned_matrix(llh: crate::coords::LLHRadians) -> nalgebra::Matrix3 { let sin_lat = llh.latitude().sin(); @@ -80,10 +78,11 @@ pub(crate) fn ecef2ned_matrix(llh: crate::coords::LLHRadians) -> nalgebra::Matri #[cfg(test)] mod tests { - use super::*; use float_eq::assert_float_eq; use proptest::prelude::*; + use super::*; + proptest! { #![proptest_config(ProptestConfig::with_cases(1000))] diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index 6d4a3bc..1a6e5e1 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -25,7 +25,8 @@ fn u8_to_ascii_char(nibble: u8) -> char { pub fn calculate_checksum(sentence: &str) -> String { let mut checksum = 0; - // this flag indicates if we have reached the '*' character or anything beyond that such as the actual checksum value or the end sequence () + // this flag indicates if we have reached the '*' character or anything beyond that such as the + // actual checksum value or the end sequence () let mut at_or_past_checksum_field = false; for (i, byte) in sentence.bytes().enumerate() { diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index b0e2d52..292ff8b 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -99,10 +99,13 @@ impl GGA { let hdop = self.hdop.map_or(String::new(), |hdop| format!("{hdop:.1}")); - // NOTE(ted): This is actually not the right value to use, however, we don't really use height for finding information like nearest station so it's ok to use for now + // NOTE(ted): This is actually not the right value to use, however, we don't really use + // height for finding information like nearest station so it's ok to use for now let height = "0.0"; - let age_dgps = self.age_dgps.map_or(0.0, |age| age.as_secs_f64()); + let age_dgps = self + .age_dgps + .map_or(String::new(), |age| format!("{:.1}", age.as_secs_f64())); let geoidal_separation = self .geoidal_separation @@ -113,7 +116,9 @@ impl GGA { .map_or(String::new(), |id| id.to_string()); let sentence = format!( - "GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},{longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop},{height:.6},M,{geoidal_separation},{age_dgps:.1},{reference_station_id}", + "GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},\ + {longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop},{height:.6},M,\ + {geoidal_separation},{age_dgps:.1},{reference_station_id}", ); let checksum = nmea::calculate_checksum(&sentence); @@ -133,6 +138,7 @@ mod test { let gga = GGA::builder() .sat_in_use(12) .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) + .gps_quality(GPSQuality::GPS) .hdop(0.9) .llh(super::LLHDegrees::new(37.7749, -122.4194, 10.0)) .build(); @@ -166,30 +172,10 @@ mod test { ); } - #[test] - fn gga_with_dgps_fields_that_is_not_dgps_is_ignored() { - let gga = GGA::builder() - .sat_in_use(8) - .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) - .hdop(1.2) - .llh(super::LLHDegrees::new(34.0522, -118.2437, 15.0)) - .gps_quality(GPSQuality::GPS) - .age_dgps(Duration::from_secs_f64(2.5)) - .geoidal_separation(1.0) - .reference_station_id(42) - .build(); - - let sentence = gga.to_sentence(); - - assert_eq!( - sentence, - "$GPGGA,0189.00,34.052200,N,-118.243700,W,1,8,1.2,0.0,M,1.00,,*00\r\n" - ); - } - #[test] fn gga_sentence_is_always_less_than_82_characters() { - // we are going to set some very large decimal places and the highest possible values in terms of character count to ensure our sentence is always below 82 characters + // we are going to set some very large decimal places and the highest possible values in + // terms of character count to ensure our sentence is always below 82 characters let gga = GGA::builder() .sat_in_use(12) .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) diff --git a/swiftnav/src/nmea/source.rs b/swiftnav/src/nmea/source.rs index 1462bad..ea1f037 100644 --- a/swiftnav/src/nmea/source.rs +++ b/swiftnav/src/nmea/source.rs @@ -9,7 +9,8 @@ pub enum Source { Gallileo = 0b100, /// China's Beidou Beidou = 0b1000, - /// Global Navigation Sattelite System. Some combination of other systems. Depends on receiver model, receiver settings, etc.. + /// Global Navigation Sattelite System. Some combination of other systems. Depends on receiver + /// model, receiver settings, etc.. GNSS = 0b10000, /// `MediaTek` NMEA packet protocol MTK = 0b10_0000, diff --git a/swiftnav/src/reference_frame/mod.rs b/swiftnav/src/reference_frame/mod.rs index 208e11a..9a5f08b 100644 --- a/swiftnav/src/reference_frame/mod.rs +++ b/swiftnav/src/reference_frame/mod.rs @@ -9,9 +9,9 @@ // WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. //! Geodetic reference frame transformations //! -//! Transform coordinates between geodetic reference frames using time-dependent Helmert transformations. -//! Supports both global frames (ITRF series) and regional frames (ETRF, NAD83, etc.) with runtime -//! parameter loading. +//! Transform coordinates between geodetic reference frames using time-dependent Helmert +//! transformations. Supports both global frames (ITRF series) and regional frames (ETRF, NAD83, +//! etc.) with runtime parameter loading. //! //! # Key Concepts //! @@ -68,14 +68,16 @@ //! repo.add_transformation(custom_transform); //! ``` -use crate::coords::{Coordinate, ECEF}; -use nalgebra::{Matrix3, Vector3}; use std::{ collections::{HashMap, HashSet, VecDeque}, fmt, }; + +use nalgebra::{Matrix3, Vector3}; use strum::{Display, EnumIter, EnumString}; +use crate::coords::{Coordinate, ECEF}; + mod params; /// Geodetic reference frame identifiers @@ -195,10 +197,10 @@ impl PartialEq for &ReferenceFrame { /// # Parameter Units /// /// Input parameters are stored in standard geodetic units: -/// - **Translation** (`tx`, `ty`, `tz`): millimeters (mm) +/// - **Translation** (`tx`, `ty`, `tz`): millimeters (mm) /// - **Translation rates** (`tx_dot`, `ty_dot`, `tz_dot`): mm/year /// - **Scale** (`s`): parts per billion (ppb) -/// - **Scale rate** (`s_dot`): ppb/year +/// - **Scale rate** (`s_dot`): ppb/year /// - **Rotation** (`rx`, `ry`, `rz`): milliarcseconds (mas) /// - **Rotation rates** (`rx_dot`, `ry_dot`, `rz_dot`): mas/year /// - **Reference epoch** (`epoch`): decimal years @@ -258,7 +260,8 @@ impl TimeDependentHelmertParams { } } - /// Reverses the transformation. Since this is a linear transformation we simply negate all terms + /// Reverses the transformation. Since this is a linear transformation we simply negate all + /// terms #[must_use] pub fn invert(mut self) -> Self { self.t *= -1.0; @@ -574,10 +577,12 @@ fn builtin_transformations() -> Vec { #[cfg(test)] mod tests { - use super::*; + use std::str::FromStr; + use float_eq::assert_float_eq; use params::TRANSFORMATIONS; - use std::str::FromStr; + + use super::*; #[expect(clippy::too_many_lines)] #[test] diff --git a/swiftnav/src/signal/code.rs b/swiftnav/src/signal/code.rs index 2d90814..1339dc1 100644 --- a/swiftnav/src/signal/code.rs +++ b/swiftnav/src/signal/code.rs @@ -8,7 +8,7 @@ // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. -use super::{consts, Constellation}; +use super::{Constellation, consts}; /// Code identifiers #[derive( diff --git a/swiftnav/src/signal/mod.rs b/swiftnav/src/signal/mod.rs index e1a3c0d..3722519 100644 --- a/swiftnav/src/signal/mod.rs +++ b/swiftnav/src/signal/mod.rs @@ -16,7 +16,8 @@ //! This module provides: //! - [`Constellation`] - Representing the supporting GNSS constellations //! - [`Code`] - Representing the codes broadcast from the GNSS satellites -//! - [`GnssSignal`] - Represents a [`Code`] broadcast by a specific satellite, using the satellite PRN as the identifier +//! - [`GnssSignal`] - Represents a [`Code`] broadcast by a specific satellite, using the satellite +//! PRN as the identifier //! //! # Examples //! @@ -37,9 +38,10 @@ mod code; mod constellation; pub mod consts; +use std::fmt; + pub use code::*; pub use constellation::*; -use std::fmt; /// GNSS Signal identifier #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] diff --git a/swiftnav/src/time/gnss.rs b/swiftnav/src/time/gnss.rs index 4b9b9eb..ddb4f6c 100644 --- a/swiftnav/src/time/gnss.rs +++ b/swiftnav/src/time/gnss.rs @@ -7,12 +7,13 @@ // THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, // EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED // WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. -use crate::time::{consts, UtcParams, UtcTime, MJD, UTC_LEAPS, WEEK}; use std::{ ops::{Add, AddAssign, Sub, SubAssign}, time::Duration, }; +use crate::time::{MJD, UTC_LEAPS, UtcParams, UtcTime, WEEK, consts}; + /// Representation of GPS Time #[derive(Debug, Copy, Clone)] pub struct GpsTime { @@ -189,7 +190,7 @@ impl GpsTime { assert!(utc_time.hour() == 23); assert!(utc_time.minute() == 59); assert!(utc_time.seconds_int() == 59); - /* add the extra second back in*/ + /* add the extra second back in */ utc_time.add_second(); } diff --git a/swiftnav/src/time/mjd.rs b/swiftnav/src/time/mjd.rs index 197b34e..21a7156 100644 --- a/swiftnav/src/time/mjd.rs +++ b/swiftnav/src/time/mjd.rs @@ -15,9 +15,10 @@ reason = "We need to review the math for overflows" )] -use crate::time::{consts, GpsTime, UtcParams, UtcTime}; use std::time::Duration; +use crate::time::{GpsTime, UtcParams, UtcTime, consts}; + /// Representation of modified julian dates (MJD) #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] pub struct MJD(f64); @@ -51,7 +52,19 @@ impl MJD { #[must_use] pub fn from_parts(year: u16, month: u8, day: u8, hour: u8, minute: u8, seconds: f64) -> MJD { // Enforce our assumption that the date is just the MJD period - assert!(year > 1858 || (year == 1858 && month > 11) || (year == 1858 && month == 11 && day >= 17), "Attempting to convert a date prior to the start of the Modified Julian Date system ({}-{}-{}T{}:{}:{}Z", year, month, day, hour, minute, seconds); + assert!( + year > 1858 + || (year == 1858 && month > 11) + || (year == 1858 && month == 11 && day >= 17), + "Attempting to convert a date prior to the start of the Modified Julian Date system \ + ({}-{}-{}T{}:{}:{}Z", + year, + month, + day, + hour, + minute, + seconds + ); let full_days = 367 * i64::from(year) - 7 * (i64::from(year) + (i64::from(month) + 9) / 12) / 4 diff --git a/swiftnav/src/time/utc.rs b/swiftnav/src/time/utc.rs index 3547a3c..1ea9f6f 100644 --- a/swiftnav/src/time/utc.rs +++ b/swiftnav/src/time/utc.rs @@ -10,9 +10,10 @@ #![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] -use crate::time::{consts, is_leap_year, GpsTime, MJD}; use std::time::Duration; +use crate::time::{GpsTime, MJD, consts, is_leap_year}; + /// GPS UTC correction parameters #[derive(Clone)] pub struct UtcParams { @@ -399,7 +400,6 @@ impl From for UtcTime { } } -#[cfg(feature = "chrono")] impl From for chrono::DateTime { fn from(utc: UtcTime) -> chrono::DateTime { use chrono::prelude::*; @@ -425,7 +425,6 @@ impl From for chrono::DateTime { } } -#[cfg(feature = "chrono")] impl From> for UtcTime { fn from(chrono: chrono::DateTime) -> UtcTime { use chrono::prelude::*; @@ -557,7 +556,16 @@ mod tests { #[expect(clippy::float_cmp)] { - assert!(d_utc == test_case.d_utc && is_lse == test_case.is_lse, "test_case.t: {:?}, test_case.d_utc: {}, test_case.is_lse: {}, d_utc: {}, is_lse: {}", test_case.t, test_case.d_utc, test_case.is_lse, d_utc, is_lse); + assert!( + d_utc == test_case.d_utc && is_lse == test_case.is_lse, + "test_case.t: {:?}, test_case.d_utc: {}, test_case.is_lse: {}, d_utc: {}, \ + is_lse: {}", + test_case.t, + test_case.d_utc, + test_case.is_lse, + d_utc, + is_lse + ); } } } @@ -1090,7 +1098,6 @@ mod tests { } } - #[cfg(feature = "chrono")] #[test] fn chrono_conversions() { use chrono::prelude::*; From 39b27c7a98ac983830077c3ad620e4ab39351443 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:01:18 -0700 Subject: [PATCH 13/17] add more checksum examples --- swiftnav/src/nmea/checksum.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index 1a6e5e1..669abe9 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -58,6 +58,14 @@ mod tests { let sentence = "GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,"; let checksum = super::calculate_checksum(sentence); assert_eq!(checksum, "47"); + + let sentence = "$GPGLL,5057.970,N,00146.110,E,142451,A"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "27"); + + let sentence = "$GPVTG,089.0,T,,,15.2,N,,"; + let checksum = super::calculate_checksum(sentence); + assert_eq!(checksum, "7F"); } #[test] From 3a2e70aacc561ba1382f50ec08d41b6bf57602a6 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:27:26 -0700 Subject: [PATCH 14/17] fix gga sentence --- .github/workflows/ci.yaml | 2 +- Cargo.toml | 6 ++---- swiftnav/Cargo.toml | 4 ++-- swiftnav/src/coords/llh.rs | 32 ++++++++++++++++++++++++++++++ swiftnav/src/coords/mod.rs | 4 ++-- swiftnav/src/edc.rs | 9 +++------ swiftnav/src/nmea/checksum.rs | 9 ++------- swiftnav/src/nmea/gga.rs | 31 +++++++++++++++-------------- swiftnav/src/nmea/mod.rs | 2 -- swiftnav/src/nmea/source.rs | 17 ---------------- swiftnav/src/time/mjd.rs | 8 +------- swiftnav/src/time/mod.rs | 37 +++++++++++------------------------ 12 files changed, 72 insertions(+), 89 deletions(-) delete mode 100644 swiftnav/src/nmea/source.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cd18201..1cd5352 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -65,7 +65,7 @@ jobs: # MSRV - os: ubuntu-latest binary_target: x86_64-unknown-linux-gnu - toolchain: 1.90.0 + toolchain: 1.87.0 steps: - uses: actions/checkout@v5 with: diff --git a/Cargo.toml b/Cargo.toml index f5c4cc1..dc3fda2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,3 @@ [workspace] -members = [ - "swiftnav", - # "swiftnav-sys" -] +resolver = "3" +members = ["swiftnav"] diff --git a/swiftnav/Cargo.toml b/swiftnav/Cargo.toml index 094d805..7880145 100644 --- a/swiftnav/Cargo.toml +++ b/swiftnav/Cargo.toml @@ -2,12 +2,12 @@ name = "swiftnav" version = "0.10.0" authors = ["Swift Navigation "] -edition = "2018" +edition = "2024" description = "GNSS positioning and related utilities" readme = "README.md" repository = "https://github.com/swift-nav/swiftnav-rs" license = "LGPL-3.0" -rust-version = "1.85.0" +rust-version = "1.87.0" [dependencies] bon = "3.8.1" diff --git a/swiftnav/src/coords/llh.rs b/swiftnav/src/coords/llh.rs index beec5e7..6e8e61d 100644 --- a/swiftnav/src/coords/llh.rs +++ b/swiftnav/src/coords/llh.rs @@ -47,6 +47,22 @@ impl LLHDegrees { self.0.x } + /// Get the latitude in degrees and decimal minutes + #[must_use] + pub fn latitude_degree_decimal_minutes(&self) -> (u16, f64) { + let lat = self.latitude(); + + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "We are using trunc() and abs() already to remove the fractional part" + )] + let degrees = lat.trunc().abs() as u16; + let minutes = lat.fract().abs() * 60.0; + + (degrees, minutes) + } + /// Get the latitudinal hemisphere #[must_use] pub fn latitudinal_hemisphere(&self) -> LatitudinalHemisphere { @@ -63,6 +79,22 @@ impl LLHDegrees { self.0.y } + /// Get the latitude in degrees and decimal minutes + #[must_use] + pub fn longitude_degree_decimal_minutes(&self) -> (u16, f64) { + let lon = self.longitude(); + + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + reason = "We are using trunc() and abs() already to remove the fractional part" + )] + let degrees = lon.trunc().abs() as u16; + let minutes = lon.fract().abs() * 60.0; + + (degrees, minutes) + } + /// Get the longitudinal hemisphere #[must_use] pub fn longitudinal_hemisphere(&self) -> LongitudinalHemisphere { diff --git a/swiftnav/src/coords/mod.rs b/swiftnav/src/coords/mod.rs index 804bae7..f8bab7c 100644 --- a/swiftnav/src/coords/mod.rs +++ b/swiftnav/src/coords/mod.rs @@ -538,11 +538,11 @@ mod tests { let lat_err = llh_input.latitude() - llh_output.latitude(); assert!(lat_err.abs() < MAX_ANGLE_ERROR_RAD, - "Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {:?}, ECEF: {:?}, Final: {:?}, Lat error (rad): {}", llh_input, ecef, llh_output, lat_err); + "Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {llh_input:?}, ECEF: {ecef:?}, Final: {llh_output:?}, Lat error (rad): {lat_err}"); let lon_err = llh_input.longitude() - llh_output.longitude(); assert!(lon_err.abs() < MAX_ANGLE_ERROR_RAD, - "Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {:?}, ECEF: {:?}, Final: {:?}, Lon error (rad): {}", llh_input, ecef, llh_output, lon_err); + "Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {llh_input:?}, ECEF: {ecef:?}, Final: {llh_output:?}, Lon error (rad): {lon_err}"); let hgt_err = llh_input.height() - llh_output.height(); assert!(hgt_err.abs() < MAX_DIST_ERROR_M, diff --git a/swiftnav/src/edc.rs b/swiftnav/src/edc.rs index 402f0d4..5d8f841 100644 --- a/swiftnav/src/edc.rs +++ b/swiftnav/src/edc.rs @@ -354,15 +354,13 @@ mod tests { let crc = compute_crc24q(&TEST_DATA[0..0], 0); assert!( crc == 0, - "CRC of empty buffer with starting value 0 should be 0, not {}", - crc + "CRC of empty buffer with starting value 0 should be 0, not {crc}", ); let crc = compute_crc24q(&TEST_DATA[0..0], 22); assert!( crc == 22, - "CRC of empty buffer with starting value 22 should be 22, not {}", - crc + "CRC of empty buffer with starting value 22 should be 22, not {crc}", ); /* Test value taken from python crcmod package tests, see: @@ -370,8 +368,7 @@ mod tests { let crc = compute_crc24q(TEST_DATA, 0x00B7_04CE); assert!( crc == 0x0021_CF02, - "CRC of \"123456789\" with init value 0xB704CE should be 0x21CF02, not {}", - crc + "CRC of \"123456789\" with init value 0xB704CE should be 0x21CF02, not {crc}", ); } diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index 669abe9..ec8e64b 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -25,19 +25,14 @@ fn u8_to_ascii_char(nibble: u8) -> char { pub fn calculate_checksum(sentence: &str) -> String { let mut checksum = 0; - // this flag indicates if we have reached the '*' character or anything beyond that such as the - // actual checksum value or the end sequence () - let mut at_or_past_checksum_field = false; - for (i, byte) in sentence.bytes().enumerate() { // Skip the starting '$' and the '*' before the checksum - if (i == 0 && byte == b'$') || at_or_past_checksum_field { + if i == 0 && byte == b'$' { continue; } if byte == b'*' { - at_or_past_checksum_field = true; - continue; + break; } checksum ^= byte; diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index 292ff8b..f445da9 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -12,12 +12,13 @@ use crate::{ }; /// Quality of GPS solution -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy, Default)] pub enum GPSQuality { /// Fix not available or invalid + #[default] NoFix, /// GPS SPS Mode, fix valid - GPS, + SPS, /// Differential GPS, SPS Mode, fix valid DGPS, /// GPS PPS (pulse per second), fix valid @@ -27,7 +28,7 @@ pub enum GPSQuality { /// Float RTK, satelite system used in RTK mode, floating integers FRTK, /// Estimated (dead reckoning) mode. - Estimated, + DeadReckoning, /// Manual input mode Manual, /// Simulated mode @@ -38,12 +39,12 @@ impl fmt::Display for GPSQuality { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { GPSQuality::NoFix => write!(f, "0"), - GPSQuality::GPS => write!(f, "1"), + GPSQuality::SPS => write!(f, "1"), GPSQuality::DGPS => write!(f, "2"), GPSQuality::PPS => write!(f, "3"), GPSQuality::RTK => write!(f, "4"), GPSQuality::FRTK => write!(f, "5"), - GPSQuality::Estimated => write!(f, "6"), + GPSQuality::DeadReckoning => write!(f, "6"), GPSQuality::Manual => write!(f, "7"), GPSQuality::Simulated => write!(f, "8"), } @@ -59,7 +60,7 @@ pub struct GGA { /// Latitude, longitude and height in degrees. pub llh: LLHDegrees, /// Quality of GPS solution. - #[builder(default = GPSQuality::NoFix)] + #[builder(default = GPSQuality::default())] pub gps_quality: GPSQuality, /// Sattelites in use pub sat_in_use: Option, @@ -87,11 +88,11 @@ impl GGA { let timestamp = format!("{hour}{minute}{:.2}", second + second_fracs); - let latitude = self.llh.latitude(); - let latitudinal_hemisphere = self.llh.latitudinal_hemisphere(); + let (lat_deg, lat_mins) = self.llh.latitude_degree_decimal_minutes(); + let lat_hemisphere = self.llh.latitudinal_hemisphere(); - let longitude = self.llh.longitude(); - let longitudinal_hemisphere = self.llh.longitudinal_hemisphere(); + let (lon_deg, lon_mins) = self.llh.longitude_degree_decimal_minutes(); + let lon_hemisphere = self.llh.longitudinal_hemisphere(); let gps_quality = self.gps_quality; @@ -116,8 +117,8 @@ impl GGA { .map_or(String::new(), |id| id.to_string()); let sentence = format!( - "GPGGA,{timestamp},{latitude:.6},{latitudinal_hemisphere},{longitude:.6},\ - {longitudinal_hemisphere},{gps_quality},{sat_in_use},{hdop},{height:.6},M,\ + "GPGGA,{timestamp},{lat_deg:02}{lat_mins:010.7},{lat_hemisphere},{lon_deg:\ + 03}{lon_mins:010.7},{lon_hemisphere},{gps_quality},{sat_in_use},{hdop},{height:.6},M,\ {geoidal_separation},{age_dgps:.1},{reference_station_id}", ); @@ -138,7 +139,7 @@ mod test { let gga = GGA::builder() .sat_in_use(12) .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) - .gps_quality(GPSQuality::GPS) + .gps_quality(GPSQuality::SPS) .hdop(0.9) .llh(super::LLHDegrees::new(37.7749, -122.4194, 10.0)) .build(); @@ -147,7 +148,7 @@ mod test { assert_eq!( sentence, - "$GPGGA,0189.00,37.774900,N,-122.419400,W,1,12,0.9,0.0,M,,,*26\r\n" + "$GPGGA,0189.00,3746.4940000,N,12225.1640000,W,1,12,0.9,0.0,M,,,*01\r\n" ); } @@ -168,7 +169,7 @@ mod test { assert_eq!( sentence, - "$GPGGA,0189.00,34.052200,N,-118.243700,W,2,8,1.2,0.0,M,1.00,2,42*37\r\n" + "$GPGGA,0189.00,3403.1320000,N,11814.6220000,W,2,8,1.2,0.0,M,1.00,2,42*1D\r\n" ); } diff --git a/swiftnav/src/nmea/mod.rs b/swiftnav/src/nmea/mod.rs index 8cdfef7..de97cdc 100644 --- a/swiftnav/src/nmea/mod.rs +++ b/swiftnav/src/nmea/mod.rs @@ -1,7 +1,5 @@ mod checksum; mod gga; -mod source; pub use checksum::*; pub use gga::*; -pub use source::*; diff --git a/swiftnav/src/nmea/source.rs b/swiftnav/src/nmea/source.rs deleted file mode 100644 index ea1f037..0000000 --- a/swiftnav/src/nmea/source.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Source of NMEA sentence like GPS, GLONASS or other. -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum Source { - /// USA Global Positioning System - GPS = 0b1, - /// Russian Federation GLONASS - GLONASS = 0b10, - /// European Union Gallileo - Gallileo = 0b100, - /// China's Beidou - Beidou = 0b1000, - /// Global Navigation Sattelite System. Some combination of other systems. Depends on receiver - /// model, receiver settings, etc.. - GNSS = 0b10000, - /// `MediaTek` NMEA packet protocol - MTK = 0b10_0000, -} diff --git a/swiftnav/src/time/mjd.rs b/swiftnav/src/time/mjd.rs index 21a7156..c2565dd 100644 --- a/swiftnav/src/time/mjd.rs +++ b/swiftnav/src/time/mjd.rs @@ -57,13 +57,7 @@ impl MJD { || (year == 1858 && month > 11) || (year == 1858 && month == 11 && day >= 17), "Attempting to convert a date prior to the start of the Modified Julian Date system \ - ({}-{}-{}T{}:{}:{}Z", - year, - month, - day, - hour, - minute, - seconds + ({year}-{month}-{day}T{hour}:{minute}:{seconds}Z" ); let full_days = 367 * i64::from(year) diff --git a/swiftnav/src/time/mod.rs b/swiftnav/src/time/mod.rs index 0dc0788..6022986 100644 --- a/swiftnav/src/time/mod.rs +++ b/swiftnav/src/time/mod.rs @@ -79,7 +79,7 @@ pub const WEEK: Duration = Duration::from_secs(consts::WEEK_SECS as u64); /// All year values are treated as if they are in the Gregorian calendar #[must_use] pub fn is_leap_year(year: u16) -> bool { - ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) + (year.is_multiple_of(4) && !year.is_multiple_of(100)) || year.is_multiple_of(400) } #[cfg(test)] @@ -119,11 +119,8 @@ mod tests { let diff = test_case.diff(&round_trip).abs(); assert!( diff < TOW_TOL, - "gps2mjd2gps failure. original: {:?}, round trip: {:?}, diff: {}, TOW_TOL: {}", - test_case, - round_trip, - diff, - TOW_TOL + "gps2mjd2gps failure. original: {test_case:?}, round trip: {round_trip:?}, diff: \ + {diff}, TOW_TOL: {TOW_TOL}" ); // test mjd -> date -> mjd @@ -132,11 +129,8 @@ mod tests { let diff = (mjd.as_f64() - round_trip.as_f64()).abs(); assert!( diff < TOW_TOL, - "mjd2date2mjd failure. original: {:?}, round trip: {:?}, diff: {}, TOW_TOL: {}", - mjd, - round_trip, - diff, - TOW_TOL + "mjd2date2mjd failure. original: {mjd:?}, round trip: {round_trip:?}, diff: \ + {diff}, TOW_TOL: {TOW_TOL}" ); // test mjd -> utc -> mjd @@ -145,11 +139,8 @@ mod tests { let diff = (mjd.as_f64() - round_trip.as_f64()).abs(); assert!( diff < TOW_TOL, - "mjd2utc2mjd failure. original: {:?}, round trip: {:?}, diff: {}, TOW_TOL: {}", - mjd, - round_trip, - diff, - TOW_TOL + "mjd2utc2mjd failure. original: {mjd:?}, round trip: {round_trip:?}, diff: \ + {diff}, TOW_TOL: {TOW_TOL}" ); // test gps -> date -> gps @@ -158,11 +149,8 @@ mod tests { let diff = test_case.diff(&round_trip).abs(); assert!( diff < TOW_TOL, - "gps2date2gps failure. original: {:?}, round trip: {:?}, diff: {}, TOW_TOL: {}", - test_case, - round_trip, - diff, - TOW_TOL + "gps2date2gps failure. original: {test_case:?}, round trip: {round_trip:?}, diff: \ + {diff}, TOW_TOL: {TOW_TOL}" ); // test utc -> date -> utc @@ -171,11 +159,8 @@ mod tests { let diff = utc.to_mjd().as_f64() - mjd.as_f64(); assert!( diff < TOW_TOL, - "utc2date2utc failure. original: {:?}, round trip: {:?}, diff: {}, TOW_TOL: {}", - mjd, - round_trip, - diff, - TOW_TOL + "utc2date2utc failure. original: {mjd:?}, round trip: {round_trip:?}, diff: \ + {diff}, TOW_TOL: {TOW_TOL}" ); } } From c33c12db25d274c76c710d56b56b137cfb5b287a Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:30:44 -0700 Subject: [PATCH 15/17] ensure we are formatting correctly by using a smaller lon --- swiftnav/src/nmea/gga.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index f445da9..2945a44 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -76,6 +76,7 @@ pub struct GGA { impl GGA { // converts the GGA struct into an NMEA sentence + // http://lefebure.com/articles/nmea-gga/ #[must_use] pub fn to_sentence(&self) -> String { // NOTE(ted): We are formatting here a bit strange because for some ungodly reason, @@ -158,7 +159,7 @@ mod test { .sat_in_use(8) .time(DateTime::from_timestamp(1_761_351_489, 0).unwrap()) .hdop(1.2) - .llh(super::LLHDegrees::new(34.0522, -118.2437, 15.0)) + .llh(super::LLHDegrees::new(34.0522, -18.2437, 15.0)) .gps_quality(GPSQuality::DGPS) .age_dgps(Duration::from_secs_f64(2.5)) .geoidal_separation(1.0) @@ -169,7 +170,7 @@ mod test { assert_eq!( sentence, - "$GPGGA,0189.00,3403.1320000,N,11814.6220000,W,2,8,1.2,0.0,M,1.00,2,42*1D\r\n" + "$GPGGA,0189.00,3403.1320000,N,01814.6220000,W,2,8,1.2,0.0,M,1.00,2,42*1C\r\n" ); } From 305bef4385587419202bcf198edd27f97067f9f0 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:37:11 -0700 Subject: [PATCH 16/17] add docs --- swiftnav/src/nmea/checksum.rs | 10 ++++++++++ swiftnav/src/nmea/gga.rs | 13 ++++++++++++- swiftnav/src/nmea/mod.rs | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index ec8e64b..0925867 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -1,3 +1,13 @@ +// Copyright (c) 2025 Swift Navigation Inc. +// Contact: Swift Navigation +// +// This source is subject to the license found in the file 'LICENSE' which must +// be be distributed together with this source. All other rights reserved. +// +// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + fn u8_to_nibbles(byte: u8) -> (u8, u8) { // The high nibble is obtained by shifting the byte 4 bits to the right. // This discards the lower 4 bits and moves the upper 4 bits into the lower 4 bit positions. diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index 2945a44..9f88849 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -1,3 +1,13 @@ +// Copyright (c) 2025 Swift Navigation Inc. +// Contact: Swift Navigation +// +// This source is subject to the license found in the file 'LICENSE' which must +// be be distributed together with this source. All other rights reserved. +// +// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + use std::{ fmt::{self}, time::Duration, @@ -51,7 +61,8 @@ impl fmt::Display for GPSQuality { } } -/// Geographic coordinates including altitude, GPS solution quality, DGPS usage information. +/// Global Positioning System Fix Data including time, position and fix related data for a GNSS +/// receiver #[derive(Debug, PartialEq, Clone, Builder)] pub struct GGA { /// Time of fix in UTC. diff --git a/swiftnav/src/nmea/mod.rs b/swiftnav/src/nmea/mod.rs index de97cdc..e1b1173 100644 --- a/swiftnav/src/nmea/mod.rs +++ b/swiftnav/src/nmea/mod.rs @@ -1,3 +1,17 @@ +// Copyright (c) 2025 Swift Navigation Inc. +// Contact: Swift Navigation +// +// This source is subject to the license found in the file 'LICENSE' which must +// be be distributed together with this source. All other rights reserved. +// +// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +// EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. + +//! This module contains National Marine Electronics Association (NMEA) related structures and +//! formatting utilities. Notably, it contains (or eventually will contain) structures related to +//! NMEA sentences and parsing/serialization of those sentences. + mod checksum; mod gga; From 2e125ab1ad371ecfa0c23b357daa03bb57981071 Mon Sep 17 00:00:00 2001 From: Theodore Zilist <14153237+tzilist@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:04:41 -0700 Subject: [PATCH 17/17] add source back in --- swiftnav/src/nmea/checksum.rs | 2 +- swiftnav/src/nmea/gga.rs | 31 +++++++++++++++++++++++++------ swiftnav/src/nmea/mod.rs | 2 ++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/swiftnav/src/nmea/checksum.rs b/swiftnav/src/nmea/checksum.rs index 0925867..5595eba 100644 --- a/swiftnav/src/nmea/checksum.rs +++ b/swiftnav/src/nmea/checksum.rs @@ -36,7 +36,7 @@ pub fn calculate_checksum(sentence: &str) -> String { let mut checksum = 0; for (i, byte) in sentence.bytes().enumerate() { - // Skip the starting '$' and the '*' before the checksum + // Skip the starting '$' if i == 0 && byte == b'$' { continue; } diff --git a/swiftnav/src/nmea/gga.rs b/swiftnav/src/nmea/gga.rs index 9f88849..cd710c8 100644 --- a/swiftnav/src/nmea/gga.rs +++ b/swiftnav/src/nmea/gga.rs @@ -18,7 +18,7 @@ use chrono::{DateTime, Timelike, Utc}; use crate::{ coords::LLHDegrees, - nmea::{self}, + nmea::{self, Source}, }; /// Quality of GPS solution @@ -31,7 +31,7 @@ pub enum GPSQuality { SPS, /// Differential GPS, SPS Mode, fix valid DGPS, - /// GPS PPS (pulse per second), fix valid + /// GPS PPS (precise positioning service, military encrypted signals), fix valid PPS, /// RTK (real time kinematic). System used in RTK mode with fixed integers RTK, @@ -61,17 +61,28 @@ impl fmt::Display for GPSQuality { } } +#[derive(thiserror::Error, Debug)] +pub enum GGAParseError { + #[error("Invalid time format")] + InvalidTimeFormat, + + #[error("Invalid or missing GPS quality")] + InvalidGPSQuality, +} + /// Global Positioning System Fix Data including time, position and fix related data for a GNSS /// receiver #[derive(Debug, PartialEq, Clone, Builder)] pub struct GGA { + #[builder(default)] + pub source: Source, /// Time of fix in UTC. #[builder(default = Utc::now())] pub time: DateTime, /// Latitude, longitude and height in degrees. pub llh: LLHDegrees, /// Quality of GPS solution. - #[builder(default = GPSQuality::default())] + #[builder(default)] pub gps_quality: GPSQuality, /// Sattelites in use pub sat_in_use: Option, @@ -86,10 +97,18 @@ pub struct GGA { } impl GGA { - // converts the GGA struct into an NMEA sentence - // http://lefebure.com/articles/nmea-gga/ + /// converts the GGA struct into an NMEA sentence + /// + /// + /// + /// ```text + /// 1 2 3 4 5 6 7 8 9 10 | 12 13 14 15 + /// | | | | | | | | | | | | | | | + /// $--GGA,hhmmss.ss,ddmm.mm,a,ddmm.mm,a,x,xx,x.x,x.x,M,x.x,M,x.x,xxxx*hh + /// ``` #[must_use] pub fn to_sentence(&self) -> String { + let talker_id = self.source.to_nmea_talker_id(); // NOTE(ted): We are formatting here a bit strange because for some ungodly reason, // chrono chose not to allow for abitrary fractional seconds precision when formatting // Construct timestamp in HHMMSS.SS format @@ -129,7 +148,7 @@ impl GGA { .map_or(String::new(), |id| id.to_string()); let sentence = format!( - "GPGGA,{timestamp},{lat_deg:02}{lat_mins:010.7},{lat_hemisphere},{lon_deg:\ + "{talker_id}GGA,{timestamp},{lat_deg:02}{lat_mins:010.7},{lat_hemisphere},{lon_deg:\ 03}{lon_mins:010.7},{lon_hemisphere},{gps_quality},{sat_in_use},{hdop},{height:.6},M,\ {geoidal_separation},{age_dgps:.1},{reference_station_id}", ); diff --git a/swiftnav/src/nmea/mod.rs b/swiftnav/src/nmea/mod.rs index e1b1173..dfdce89 100644 --- a/swiftnav/src/nmea/mod.rs +++ b/swiftnav/src/nmea/mod.rs @@ -14,6 +14,8 @@ mod checksum; mod gga; +mod source; pub use checksum::*; pub use gga::*; +pub use source::*;