Repository: ublox-rs/ublox
Pull request #137: RXM-SFRBX: GPS L1 C/A message 1, 2 and 3 interpretation
Author: login: gwbres
Created: 2025-05-09T16:14:44Z | Merged: 2025-05-22T18:45:49Z

This PR adds a first solution to `UBX-RXM-SFRBX` interpretation, as required by manual navigation.
This is the orbital equivalent of `RXM-RAWX`.

The decoding is unlocked by a new crate option, although this is fairly small, and still does not require `std`.
The decoding logic is `GNSS` dependent. This only handles GPS L1 C/A messages


Diff:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 06a65fbe..7b33d0dc 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -22,7 +22,7 @@ jobs:
       matrix:
         feature-args:
           - --features "alloc std ubx_proto23" 
-          - --no-default-features --features "alloc ubx_proto23" 
+          - --no-default-features --features "alloc ubx_proto23 sfrbx-gps"
           - --no-default-features --features ubx_proto14
           - --no-default-features --features ubx_proto23
           - --no-default-features --features ubx_proto27
@@ -138,6 +138,8 @@ jobs:
             feature: ubx_proto27 
           - feature-group: only-explicit-features
             feature: ubx_proto31 
+          - feature-group: only-explicit-features
+            feature: ubx_proto31 sfrbx-gps
     steps:
       - uses: actions/checkout@v4
       - name: SemVer Check 
diff --git a/ublox/Cargo.toml b/ublox/Cargo.toml
index 7a03e660..e8dd09db 100644
--- a/ublox/Cargo.toml
+++ b/ublox/Cargo.toml
@@ -16,6 +16,12 @@ ubx_proto27 = []
 ubx_proto31 = []
 default = ["std", "serde", "ubx_proto23"]
 
+# Unlock RXM-SFRBX GPS and QZSS interpretation
+sfrbx-gps = [
+    "gnss-protos",
+    "gnss-protos/gps",
+]
+
 alloc = []
 std = []
 
@@ -32,6 +38,8 @@ serde = { version = "1.0", optional = true, default-features = false, features =
 ] }
 ublox_derive = { path = "../ublox_derive", version = "=0.2.0" }
 
+gnss-protos = { version = "0.0.2", optional = true }
+
 [dev-dependencies]
 cpu-time = "1.0"
 cpuprofiler = "0.0"
diff --git a/ublox/src/error.rs b/ublox/src/error.rs
index 1d1a1257..67c01df2 100644
--- a/ublox/src/error.rs
+++ b/ublox/src/error.rs
@@ -45,7 +45,7 @@ impl fmt::Display for ParserError {
         match self {
             ParserError::InvalidChecksum { expect, got } => write!(
                 f,
-                "Not valid packet's checksum, expect {:x}, got {:x}",
+                "Not valid packet's checksum, expect 0x{:02x}, got 0x{:02x}",
                 expect, got
             ),
             ParserError::InvalidField { packet, field } => {
diff --git a/ublox/src/ubx_packets/packets/rxm_sfrbx.rs b/ublox/src/ubx_packets/packets/rxm_sfrbx.rs
index 9f771837..0ae78650 100644
--- a/ublox/src/ubx_packets/packets/rxm_sfrbx.rs
+++ b/ublox/src/ubx_packets/packets/rxm_sfrbx.rs
@@ -1,7 +1,9 @@
 #[cfg(feature = "serde")]
 use super::SerializeUbxPacketFields;
+
 #[cfg(feature = "serde")]
 use crate::serde::ser::SerializeMap;
+
 #[allow(unused_imports, reason = "It's only unused in some feature sets")]
 use crate::FieldIter;
 
@@ -11,14 +13,31 @@ use ublox_derive::ubx_packet_recv;
 #[ubx_packet_recv]
 #[ubx(class = 0x02, id = 0x13, max_payload_len = 72)]
 struct RxmSfrbx {
+    /// GNSS identifier
     gnss_id: u8,
+
+    /// Satellite identifier
     sv_id: u8,
+
+    /// Reserved
     reserved1: u8,
+
+    /// Only used for GLonass: this is the frequency slot +7
     freq_id: u8,
+
+    /// Number of data words
     num_words: u8,
+
+    /// Reserved
     reserved2: u8,
+
+    /// Message version
     version: u8,
+
+    /// Reserved
     reserved3: u8,
+
+    /// Data words
     #[ubx(
         map_type = DwrdIter,
         from = DwrdIter::new,
@@ -50,3 +69,62 @@ impl core::iter::Iterator for DwrdIter<'_> {
             .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
     }
 }
+
+#[cfg(feature = "sfrbx-gps")]
+use gnss_protos::{GpsDataByte, GpsQzssDecoder, GpsQzssFrame};
+
+#[derive(Debug, Clone)]
+pub enum RxmSfrbxInterpreted {
+    /// [GpsQzssFrame]
+    #[cfg(feature = "sfrbx-gps")]
+    GpsQzss(GpsQzssFrame),
+}
+
+impl RxmSfrbxRef<'_> {
+    /// Try to interpret the RXM-SFRBX inner frame (when supported/known).
+    pub fn interpret(&self) -> Option<RxmSfrbxInterpreted> {
+        // interpretation is GNSS dependent
+        match self.gnss_id() {
+            0 | 5 => {
+                #[cfg(feature = "sfrbx-gps")]
+                {
+                    let decoded = self.gps_qzss_decoding()?;
+                    Some(RxmSfrbxInterpreted::GpsQzss(decoded))
+                }
+                #[cfg(not(feature = "sfrbx-gps"))]
+                {
+                    None
+                }
+            },
+            _ => {
+                // either not supported or not applicable
+                None
+            },
+        }
+    }
+
+    #[cfg(feature = "sfrbx-gps")]
+    /// [GpsQzssFrame] interpretation attempt, from parsed UBX-SFRBX data.
+    fn gps_qzss_decoding(&self) -> Option<GpsQzssFrame> {
+        let mut decoder = GpsQzssDecoder::default().without_parity_verification();
+
+        for byte in self.dwrd() {
+            let bytes = byte.to_be_bytes();
+
+            let gps_bytes = [
+                GpsDataByte::MsbPadded(bytes[0]),
+                GpsDataByte::Byte(bytes[1]),
+                GpsDataByte::Byte(bytes[2]),
+                GpsDataByte::Byte(bytes[3]),
+            ];
+
+            for gps_byte in gps_bytes {
+                if let Some(decoded) = decoder.parse(gps_byte) {
+                    return Some(decoded);
+                }
+            }
+        }
+
+        None
+    }
+}
diff --git a/ublox/tests/mod.rs b/ublox/tests/mod.rs
new file mode 100644
index 00000000..c0774919
--- /dev/null
+++ b/ublox/tests/mod.rs
@@ -0,0 +1,4 @@
+mod generator_test;
+mod parser_binary_dump_test;
+mod parser_tests;
+mod rxm_sfrbx;
diff --git a/ublox/tests/rxm_sfrbx/gps.rs b/ublox/tests/rxm_sfrbx/gps.rs
new file mode 100644
index 00000000..fa327a6a
--- /dev/null
+++ b/ublox/tests/rxm_sfrbx/gps.rs
@@ -0,0 +1,338 @@
+#![cfg(feature = "alloc")]
+
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+/**
+ * UBX-RXM-SFRBX GPS Eph#1 L1/CA parsing and interpretation
+ */
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x73,
+        0xC9,
+        0x27,
+        0x15, // 2
+        0x04,
+        0x00,
+        0xE4,
+        0x13, // 3
+        0x31,
+        0x5D,
+        0x4F,
+        0x10, // 4
+        0xD7,
+        0xE6,
+        0x44,
+        0x97, // 5
+        0x83,
+        0x57,
+        0x75,
+        0x07, // 6
+        0xB5,
+        0x80,
+        0x0C,
+        0x33, // 7
+        0xA1,
+        0x42,
+        0x50,
+        0x92, // 8
+        0x84,
+        0x16,
+        0x00,
+        0x80, // 9
+        0x33,
+        0x30,
+        0x2C,
+        0x31, // 10
+        0xFB, // CK_A
+        0xE6, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris1(subframe) => {
+                            assert_eq!(subframe.af2, 0.0);
+                            assert!((subframe.af1 - 1.023181539495e-011).abs() < 1e-14);
+                            assert!((subframe.af0 - -4.524961113930e-004).abs() < 1.0e-11);
+                            assert_eq!(subframe.week, 318);
+                            assert_eq!(subframe.toc, 266_400);
+                            assert_eq!(subframe.health, 0);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+/**
+ * UBX-RXM-SFRBX GPS Eph#2 L1/CA parsing and interpretation
+ */
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph2() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x1B,
+        0xEA,
+        0x27,
+        0x15, // 2
+        0x65,
+        0xF1,
+        0x7F,
+        0x12, // 3
+        0x7C,
+        0x1F,
+        0x68,
+        0x8C, // 3
+        0x15,
+        0x34,
+        0x49,
+        0x02, // 4
+        0x1E,
+        0x81,
+        0xF8,
+        0xBF, // 5
+        0x14,
+        0x81,
+        0x1B,
+        0x99, // 6
+        0x6E,
+        0x68,
+        0x3E,
+        0x04, // 7
+        0x21,
+        0x72,
+        0x34,
+        0x83, // 8
+        0x7B,
+        0x9F,
+        0x42,
+        0x90, // 9
+        0xD2, // CK_A
+        0x9A, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris2(subframe) => {
+                            assert_eq!(subframe.toe, 266_400);
+                            assert_eq!(subframe.crs, -1.843750000000e+000);
+                            assert!((subframe.sqrt_a - 5.153602432251e+003).abs() < 1e-9);
+                            assert!((subframe.m0 - 9.768415465951e-001).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-9);
+                            assert!((subframe.e - 8.578718174249e-003).abs() < 1e-9);
+                            assert!((subframe.cus - 8.093193173409e-006).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-6);
+                            assert!((subframe.dn - 1.444277586415e-009).abs() < 1e-9);
+                            assert_eq!(subframe.fit_int_flag, false);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+/**
+ * UBX-RXM-SFRBX GPS Eph#3 L1/CA parsing and interpretation
+ */
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph3() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0xDB,
+        0x0B,
+        0x28,
+        0x15, // 2
+        0x34,
+        0xEA,
+        0x0A,
+        0x00, // 3
+        0xEE,
+        0xFF,
+        0x3C,
+        0x03, // 4
+        0xEB,
+        0xC9,
+        0xE5,
+        0xBF, // 5
+        0x4E,
+        0xB6,
+        0x6F,
+        0x13, // 6
+        0x2C,
+        0xAB,
+        0xF4,
+        0x86, // 7
+        0x44,
+        0xEB,
+        0x71,
+        0x06, // 8
+        0x02,
+        0xF6,
+        0xEA,
+        0x3F, // 9
+        0x13,
+        0x52,
+        0x45,
+        0x92, // 10
+        0x43, // CK_A
+        0x6D, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris3(subframe) => {
+                            assert!((subframe.cic - 8.009374141693e-008).abs() < 1e-9);
+                            assert!((subframe.cis - -1.955777406693e-007).abs() < 1e-9);
+                            assert!((subframe.crc - 2.225625000000e+002).abs() < 1e-9);
+                            assert!((subframe.i0 - 3.070601043291e-001).abs() < 1e-9);
+                            assert!((subframe.idot - 1.548414729768e-010).abs() < 1e-9);
+                            assert!((subframe.omega0 - -6.871047024615e-001).abs() < 1e-9);
+                            assert!((subframe.omega_dot - -2.449269231874e-009).abs() < 1e-9);
+                            assert!((subframe.omega - -6.554632573389e-001).abs() < 1e-9);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
diff --git a/ublox/tests/rxm_sfrbx/mod.rs b/ublox/tests/rxm_sfrbx/mod.rs
new file mode 100644
index 00000000..1c806295
--- /dev/null
+++ b/ublox/tests/rxm_sfrbx/mod.rs
@@ -0,0 +1,2 @@
+#[cfg(feature = "sfrbx-gps")]
+mod gps;


Reviews:
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T08:52:20Z
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T08:54:37Z
user: login: CramBL
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:14:24Z
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T15:58:30Z
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T15:42:24Z
user: login: CramBL
body: Just a few wishes for documentation basically
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T08:17:44Z
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T08:25:34Z
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T08:26:02Z
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T08:26:45Z
user: login: gwbres
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-22T08:34:28Z

Review comments:
diff_hunk: @@ -45,7 +45,7 @@ impl fmt::Display for ParserError {
         match self {
             ParserError::InvalidChecksum { expect, got } => write!(
                 f,
-                "Not valid packet's checksum, expect {:x}, got {:x}",
+                "Not valid packet's checksum, expect 0x{:02x}, got 0x{:02x}",
path: ublox/src/error.rs
user: login: gwbres
body: emphasize this is hexa encoding/printing
created_at: 2025-05-18T08:52:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,4 @@
+mod generator_test;
+mod parser_binary_dump_test;
+mod parser_tests;
+mod rxm_sfrbx;
path: ublox/tests/mod.rs
user: login: gwbres
body: I am unable to execute the tests individually without this
created_at: 2025-05-18T08:54:36Z
author_association: COLLABORATOR
diff_hunk: @@ -45,7 +45,7 @@ impl fmt::Display for ParserError {
         match self {
             ParserError::InvalidChecksum { expect, got } => write!(
                 f,
-                "Not valid packet's checksum, expect {:x}, got {:x}",
+                "Not valid packet's checksum, expect 0x{:02x}, got 0x{:02x}",
path: ublox/src/error.rs
user: login: CramBL
body: Do you prefer that way of formatting it? Otherwise `{:#02x}` achieves the same as `0x{:02x}`

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=28034d7258915efabaa04f7964cb117e
created_at: 2025-05-18T11:14:24Z
author_association: COLLABORATOR
diff_hunk: @@ -45,7 +45,7 @@ impl fmt::Display for ParserError {
         match self {
             ParserError::InvalidChecksum { expect, got } => write!(
                 f,
-                "Not valid packet's checksum, expect {:x}, got {:x}",
+                "Not valid packet's checksum, expect 0x{:02x}, got 0x{:02x}",
path: ublox/src/error.rs
user: login: gwbres
body: The 0x prefix is important
created_at: 2025-05-18T15:58:30Z
author_association: COLLABORATOR
diff_hunk: @@ -50,3 +69,62 @@ impl core::iter::Iterator for DwrdIter<'_> {
             .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
     }
 }
+
+#[cfg(feature = "sfrbx-gps")]
+use gnss_protos::{GpsDataByte, GpsQzssDecoder, GpsQzssFrame};
+
+#[derive(Debug, Clone)]
+pub enum RxmSfrbxInterpreted {
+    /// [GpsQzssFrame]
+    #[cfg(feature = "sfrbx-gps")]
+    GpsQzss(GpsQzssFrame),
+}
+
+impl RxmSfrbxRef<'_> {
+    /// Try to interpret the RXM-SFRBX inner frame (when supported/known).
+    pub fn interpret(&self) -> Option<RxmSfrbxInterpreted> {
+        // interpretation is GNSS dependent
+        match self.gnss_id() {
+            0 | 5 => {
+                #[cfg(feature = "sfrbx-gps")]
+                {
+                    let decoded = self.gps_qzss_decoding()?;
+                    Some(RxmSfrbxInterpreted::GpsQzss(decoded))
+                }
+                #[cfg(not(feature = "sfrbx-gps"))]
+                {
+                    None
+                }
+            },
+            _ => {
+                // either not supported or not applicable
+                None
+            },
+        }
+    }
+
+    #[cfg(feature = "sfrbx-gps")]
+    /// [GpsQzssFrame] interpretation attempt, from parsed UBX-SFRBX data.
+    fn gps_qzss_decoding(&self) -> Option<GpsQzssFrame> {
+        let mut decoder = GpsQzssDecoder::default().without_parity_verification();
path: ublox/src/ubx_packets/packets/rxm_sfrbx.rs
user: login: gwbres
body: The decoder does not allocate anything and is `no-std` compliant.
So It's fine to "build" one per interpretation.
This will apply similarly to other cases (when we unlock them), considering GPS frames are tiny streams of bytes
created_at: 2025-05-21T15:42:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: CramBL
body: Can you name it or comment it a bit more, is it a certain kind of packet or when are we expecting this bytestream exactly?
created_at: 2025-05-21T17:48:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x73,
+        0xC9,
+        0x27,
+        0x15, // 2
+        0x04,
+        0x00,
+        0xE4,
+        0x13, // 3
+        0x31,
+        0x5D,
+        0x4F,
+        0x10, // 4
+        0xD7,
+        0xE6,
+        0x44,
+        0x97, // 5
+        0x83,
+        0x57,
+        0x75,
+        0x07, // 6
+        0xB5,
+        0x80,
+        0x0C,
+        0x33, // 7
+        0xA1,
+        0x42,
+        0x50,
+        0x92, // 8
+        0x84,
+        0x16,
+        0x00,
+        0x80, // 9
+        0x33,
+        0x30,
+        0x2C,
+        0x31, // 10
+        0xFB, // CK_A
+        0xE6, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris1(subframe) => {
+                            assert_eq!(subframe.af2, 0.0);
+                            assert!((subframe.af1 - 1.023181539495e-011).abs() < 1e-14);
+                            assert!((subframe.af0 - -4.524961113930e-004).abs() < 1.0e-11);
+                            assert_eq!(subframe.week, 318);
+                            assert_eq!(subframe.toc, 266_400);
+                            assert_eq!(subframe.health, 0);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph2() {
+    let bytes = [
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: CramBL
body: This byte stream is different, can it also be described? What makes it different?
created_at: 2025-05-21T17:48:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: CramBL
body: Most of the tests on master are not named very descriptively, but I would still like to ask for a more descriptive test here, what is being tested and what is expected?
created_at: 2025-05-21T17:51:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x73,
+        0xC9,
+        0x27,
+        0x15, // 2
+        0x04,
+        0x00,
+        0xE4,
+        0x13, // 3
+        0x31,
+        0x5D,
+        0x4F,
+        0x10, // 4
+        0xD7,
+        0xE6,
+        0x44,
+        0x97, // 5
+        0x83,
+        0x57,
+        0x75,
+        0x07, // 6
+        0xB5,
+        0x80,
+        0x0C,
+        0x33, // 7
+        0xA1,
+        0x42,
+        0x50,
+        0x92, // 8
+        0x84,
+        0x16,
+        0x00,
+        0x80, // 9
+        0x33,
+        0x30,
+        0x2C,
+        0x31, // 10
+        0xFB, // CK_A
+        0xE6, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris1(subframe) => {
+                            assert_eq!(subframe.af2, 0.0);
+                            assert!((subframe.af1 - 1.023181539495e-011).abs() < 1e-14);
+                            assert!((subframe.af0 - -4.524961113930e-004).abs() < 1.0e-11);
+                            assert_eq!(subframe.week, 318);
+                            assert_eq!(subframe.toc, 266_400);
+                            assert_eq!(subframe.health, 0);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph2() {
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: CramBL
body: likewise, could this test name be more descriptive?
created_at: 2025-05-21T17:52:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x73,
+        0xC9,
+        0x27,
+        0x15, // 2
+        0x04,
+        0x00,
+        0xE4,
+        0x13, // 3
+        0x31,
+        0x5D,
+        0x4F,
+        0x10, // 4
+        0xD7,
+        0xE6,
+        0x44,
+        0x97, // 5
+        0x83,
+        0x57,
+        0x75,
+        0x07, // 6
+        0xB5,
+        0x80,
+        0x0C,
+        0x33, // 7
+        0xA1,
+        0x42,
+        0x50,
+        0x92, // 8
+        0x84,
+        0x16,
+        0x00,
+        0x80, // 9
+        0x33,
+        0x30,
+        0x2C,
+        0x31, // 10
+        0xFB, // CK_A
+        0xE6, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris1(subframe) => {
+                            assert_eq!(subframe.af2, 0.0);
+                            assert!((subframe.af1 - 1.023181539495e-011).abs() < 1e-14);
+                            assert!((subframe.af0 - -4.524961113930e-004).abs() < 1.0e-11);
+                            assert_eq!(subframe.week, 318);
+                            assert_eq!(subframe.toc, 266_400);
+                            assert_eq!(subframe.health, 0);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph2() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x1B,
+        0xEA,
+        0x27,
+        0x15, // 2
+        0x65,
+        0xF1,
+        0x7F,
+        0x12, // 3
+        0x7C,
+        0x1F,
+        0x68,
+        0x8C, // 3
+        0x15,
+        0x34,
+        0x49,
+        0x02, // 4
+        0x1E,
+        0x81,
+        0xF8,
+        0xBF, // 5
+        0x14,
+        0x81,
+        0x1B,
+        0x99, // 6
+        0x6E,
+        0x68,
+        0x3E,
+        0x04, // 7
+        0x21,
+        0x72,
+        0x34,
+        0x83, // 8
+        0x7B,
+        0x9F,
+        0x42,
+        0x90, // 9
+        0xD2, // CK_A
+        0x9A, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris2(subframe) => {
+                            assert_eq!(subframe.toe, 266_400);
+                            assert_eq!(subframe.crs, -1.843750000000e+000);
+                            assert!((subframe.sqrt_a - 5.153602432251e+003).abs() < 1e-9);
+                            assert!((subframe.m0 - 9.768415465951e-001).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-9);
+                            assert!((subframe.e - 8.578718174249e-003).abs() < 1e-9);
+                            assert!((subframe.cus - 8.093193173409e-006).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-6);
+                            assert!((subframe.dn - 1.444277586415e-009).abs() < 1e-9);
+                            assert_eq!(subframe.fit_int_flag, false);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph3() {
+    let bytes = [
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: CramBL
body: bytes and test name could be more descriptive
created_at: 2025-05-21T17:52:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x73,
+        0xC9,
+        0x27,
+        0x15, // 2
+        0x04,
+        0x00,
+        0xE4,
+        0x13, // 3
+        0x31,
+        0x5D,
+        0x4F,
+        0x10, // 4
+        0xD7,
+        0xE6,
+        0x44,
+        0x97, // 5
+        0x83,
+        0x57,
+        0x75,
+        0x07, // 6
+        0xB5,
+        0x80,
+        0x0C,
+        0x33, // 7
+        0xA1,
+        0x42,
+        0x50,
+        0x92, // 8
+        0x84,
+        0x16,
+        0x00,
+        0x80, // 9
+        0x33,
+        0x30,
+        0x2C,
+        0x31, // 10
+        0xFB, // CK_A
+        0xE6, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris1(subframe) => {
+                            assert_eq!(subframe.af2, 0.0);
+                            assert!((subframe.af1 - 1.023181539495e-011).abs() < 1e-14);
+                            assert!((subframe.af0 - -4.524961113930e-004).abs() < 1.0e-11);
+                            assert_eq!(subframe.week, 318);
+                            assert_eq!(subframe.toc, 266_400);
+                            assert_eq!(subframe.health, 0);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph2() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x1B,
+        0xEA,
+        0x27,
+        0x15, // 2
+        0x65,
+        0xF1,
+        0x7F,
+        0x12, // 3
+        0x7C,
+        0x1F,
+        0x68,
+        0x8C, // 3
+        0x15,
+        0x34,
+        0x49,
+        0x02, // 4
+        0x1E,
+        0x81,
+        0xF8,
+        0xBF, // 5
+        0x14,
+        0x81,
+        0x1B,
+        0x99, // 6
+        0x6E,
+        0x68,
+        0x3E,
+        0x04, // 7
+        0x21,
+        0x72,
+        0x34,
+        0x83, // 8
+        0x7B,
+        0x9F,
+        0x42,
+        0x90, // 9
+        0xD2, // CK_A
+        0x9A, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris2(subframe) => {
+                            assert_eq!(subframe.toe, 266_400);
+                            assert_eq!(subframe.crs, -1.843750000000e+000);
+                            assert!((subframe.sqrt_a - 5.153602432251e+003).abs() < 1e-9);
+                            assert!((subframe.m0 - 9.768415465951e-001).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-9);
+                            assert!((subframe.e - 8.578718174249e-003).abs() < 1e-9);
+                            assert!((subframe.cus - 8.093193173409e-006).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-6);
+                            assert!((subframe.dn - 1.444277586415e-009).abs() < 1e-9);
+                            assert_eq!(subframe.fit_int_flag, false);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph3() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0xDB,
+        0x0B,
+        0x28,
+        0x15, // 2
+        0x34,
+        0xEA,
+        0x0A,
+        0x00, // 3
+        0xEE,
+        0xFF,
+        0x3C,
+        0x03, // 4
+        0xEB,
+        0xC9,
+        0xE5,
+        0xBF, // 5
+        0x4E,
+        0xB6,
+        0x6F,
+        0x13, // 6
+        0x2C,
+        0xAB,
+        0xF4,
+        0x86, // 7
+        0x44,
+        0xEB,
+        0x71,
+        0x06, // 8
+        0x02,
+        0xF6,
+        0xEA,
+        0x3F, // 9
+        0x13,
+        0x52,
+        0x45,
+        0x92, // 10
+        0x43, // CK_A
+        0x6D, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris3(subframe) => {
+                            assert!((subframe.cic - 8.009374141693e-008).abs() < 1e-9);
+                            assert!((subframe.cis - -1.955777406693e-007).abs() < 1E-9);
+                            assert!((subframe.crc - 2.225625000000e+002).abs() < 1E-9);
+                            assert!((subframe.i0 - 3.070601043291e-001).abs() < 1e-9);
+                            assert!((subframe.idot - 1.548414729768e-010).abs() < 1E-9);
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: CramBL
body: ```suggestion
                            assert!((subframe.cis - -1.955777406693e-007).abs() < 1e-9);
                            assert!((subframe.crc - 2.225625000000e+002).abs() < 1e-9);
                            assert!((subframe.i0 - 3.070601043291e-001).abs() < 1e-9);
                            assert!((subframe.idot - 1.548414729768e-010).abs() < 1e-9);
```
created_at: 2025-05-21T17:54:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: gwbres
body: This tests one of the possible frame that may be transported by RXM-SFRBX, GPS L1/CA in this very case, because that is the only frame that I have treated so far. So it tests two things:

* that GPS L1/CA RXM-SFRBX is correctly decoded by Ublox
* that GPS L1/CA inner content is correctly interpreted after Ublox transport 
created_at: 2025-05-22T08:25:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x73,
+        0xC9,
+        0x27,
+        0x15, // 2
+        0x04,
+        0x00,
+        0xE4,
+        0x13, // 3
+        0x31,
+        0x5D,
+        0x4F,
+        0x10, // 4
+        0xD7,
+        0xE6,
+        0x44,
+        0x97, // 5
+        0x83,
+        0x57,
+        0x75,
+        0x07, // 6
+        0xB5,
+        0x80,
+        0x0C,
+        0x33, // 7
+        0xA1,
+        0x42,
+        0x50,
+        0x92, // 8
+        0x84,
+        0x16,
+        0x00,
+        0x80, // 9
+        0x33,
+        0x30,
+        0x2C,
+        0x31, // 10
+        0xFB, // CK_A
+        0xE6, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris1(subframe) => {
+                            assert_eq!(subframe.af2, 0.0);
+                            assert!((subframe.af1 - 1.023181539495e-011).abs() < 1e-14);
+                            assert!((subframe.af0 - -4.524961113930e-004).abs() < 1.0e-11);
+                            assert_eq!(subframe.week, 318);
+                            assert_eq!(subframe.toc, 266_400);
+                            assert_eq!(subframe.health, 0);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph2() {
+    let bytes = [
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: gwbres
body: * second frame type that may be transported by RXM-SFRBX for GPS constellation
created_at: 2025-05-22T08:26:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: gwbres
body: correct UBX decoding of UBX-RXM-SFRBX, and correct GPS Eph#1, Eph#2 and Eph#3 transport and interpretation
created_at: 2025-05-22T08:26:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,327 @@
+use ublox::{PacketRef, Parser, RxmSfrbxInterpreted};
+
+use gnss_protos::GpsQzssSubframe;
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph1() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x73,
+        0xC9,
+        0x27,
+        0x15, // 2
+        0x04,
+        0x00,
+        0xE4,
+        0x13, // 3
+        0x31,
+        0x5D,
+        0x4F,
+        0x10, // 4
+        0xD7,
+        0xE6,
+        0x44,
+        0x97, // 5
+        0x83,
+        0x57,
+        0x75,
+        0x07, // 6
+        0xB5,
+        0x80,
+        0x0C,
+        0x33, // 7
+        0xA1,
+        0x42,
+        0x50,
+        0x92, // 8
+        0x84,
+        0x16,
+        0x00,
+        0x80, // 9
+        0x33,
+        0x30,
+        0x2C,
+        0x31, // 10
+        0xFB, // CK_A
+        0xE6, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris1(subframe) => {
+                            assert_eq!(subframe.af2, 0.0);
+                            assert!((subframe.af1 - 1.023181539495e-011).abs() < 1e-14);
+                            assert!((subframe.af0 - -4.524961113930e-004).abs() < 1.0e-11);
+                            assert_eq!(subframe.week, 318);
+                            assert_eq!(subframe.toc, 266_400);
+                            assert_eq!(subframe.health, 0);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph2() {
+    let bytes = [
+        0xb5,
+        0x62,
+        0x02,
+        0x13, // UBX
+        12 + 36,
+        0, // LENGTH
+        0,
+        1,
+        2,
+        3, // SFRBX (1)
+        1,
+        5,
+        6,
+        7, // SFRBX (2)
+        0x1B,
+        0x3E,
+        0xC1,
+        0x22, // 1
+        0x1B,
+        0xEA,
+        0x27,
+        0x15, // 2
+        0x65,
+        0xF1,
+        0x7F,
+        0x12, // 3
+        0x7C,
+        0x1F,
+        0x68,
+        0x8C, // 3
+        0x15,
+        0x34,
+        0x49,
+        0x02, // 4
+        0x1E,
+        0x81,
+        0xF8,
+        0xBF, // 5
+        0x14,
+        0x81,
+        0x1B,
+        0x99, // 6
+        0x6E,
+        0x68,
+        0x3E,
+        0x04, // 7
+        0x21,
+        0x72,
+        0x34,
+        0x83, // 8
+        0x7B,
+        0x9F,
+        0x42,
+        0x90, // 9
+        0xD2, // CK_A
+        0x9A, // CK_B
+    ];
+
+    let mut test_passed = false;
+    let mut parser = Parser::default();
+    let mut it = parser.consume_ubx(&bytes);
+
+    while let Some(pack) = it.next() {
+        match pack {
+            Ok(PacketRef::RxmSfrbx(packet)) => {
+                assert_eq!(packet.gnss_id(), 0);
+                assert_eq!(packet.sv_id(), 1);
+                assert_eq!(packet.reserved1(), 2);
+                assert_eq!(packet.freq_id(), 3);
+                assert_eq!(packet.num_words(), 1);
+                assert_eq!(packet.reserved2(), 5);
+                assert_eq!(packet.version(), 6);
+                assert_eq!(packet.reserved3(), 7);
+
+                // GPS interpretation
+                let interpreted = packet
+                    .interpret()
+                    .unwrap_or_else(|| panic!("UBX-SFRBX (GPS/QZSS) interpretation failed!"));
+
+                match interpreted {
+                    RxmSfrbxInterpreted::GpsQzss(frame) => match frame.subframe {
+                        GpsQzssSubframe::Ephemeris2(subframe) => {
+                            assert_eq!(subframe.toe, 266_400);
+                            assert_eq!(subframe.crs, -1.843750000000e+000);
+                            assert!((subframe.sqrt_a - 5.153602432251e+003).abs() < 1e-9);
+                            assert!((subframe.m0 - 9.768415465951e-001).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-9);
+                            assert!((subframe.e - 8.578718174249e-003).abs() < 1e-9);
+                            assert!((subframe.cus - 8.093193173409e-006).abs() < 1e-9);
+                            assert!((subframe.cuc - -5.587935447693e-008).abs() < 1e-6);
+                            assert!((subframe.dn - 1.444277586415e-009).abs() < 1e-9);
+                            assert_eq!(subframe.fit_int_flag, false);
+                        },
+                        _ => panic!("UBX-SFRBX (GPS/QZSS) invalid subframe interpretation!"),
+                    },
+                    _ => panic!("UBX-SFRBX (GPS/QZSS) incorrect interpretation"),
+                }
+                test_passed = true;
+            },
+            Ok(_) => panic!("found invalid packet"),
+            Err(e) => panic!("UBX-SFRBX parsing failed with {}", e),
+        }
+    }
+    assert!(test_passed, "UBX-SFRBX test failed");
+}
+
+#[test]
+#[cfg(feature = "ubx_proto23")]
+fn sfrbx_gps_eph3() {
+    let bytes = [
path: ublox/tests/rxm_sfrbx/gps.rs
user: login: gwbres
body: SFRBX GPS Ephemeris frame n°3 is about as accurate as it can get ? 

I added comments on top of that function. 

I will add similar tests when we introduce other interpretation,
but this made me realize the amount of UBX packet tests is very limited. So it's good RXM-SFRBX is now tested, but I think we should push for adding more tests, especially complex UBX packets
created_at: 2025-05-22T08:34:28Z
author_association: COLLABORATOR

PR comments:
user: login: gwbres
created_at: 2025-05-13T19:06:55Z
body: @andrei-ng, @CramBL,

still a few errors here & there (scaling/interpretation issues). I hope to conclude the draft in the next few days.
I think most of the new structs should be renamed (I will rework the proposal) and prefixed with Sfrbx to emphasize what we are talking about here. I will keep you updated and provide my test results when I'm done
author_association: COLLABORATOR
user: login: gwbres
created_at: 2025-05-18T10:32:32Z
body: I'm not a fan of seeing this code base grow while it could be handled externally. Let me see if I can provide a solution
author_association: COLLABORATOR
user: login: CramBL
created_at: 2025-05-18T11:15:29Z
body: > I'm not a fan of seeing this code base grow while it could be handled externally. Let me see if I can provide a solution

You could put it in another crate in the workspace, it's easy to remove it or feature gate it then.
author_association: COLLABORATOR
user: login: gwbres
created_at: 2025-05-18T12:07:12Z
body: > > I'm not a fan of seeing this code base grow while it could be handled externally. Let me see if I can provide a solution
> 
> You could put it in another crate in the workspace, it's easy to remove it or feature gate it then.

Yep, I know. I'm working on it. It will take a while, but that's the right thing to do IMO.
We're not going to have all the GNSS protocols imported here. 
Back to draft, will confirm once stable again
author_association: COLLABORATOR
user: login: gwbres
created_at: 2025-05-21T15:37:38Z
body: The GPS /QZSS decoding logic in now integreted in a separate lib, vastly reducing the code quantity.

`RXM-SFRBX` interpretation is GNSS dependent, and gated on a `sfrbx` compilation option.
The two tests still pass.

It has not been easy, because GPS is not aligned to 32 bit , but 30.
And Ublox obviously aligns to 32 bit. And since I extracted everything in a dedicated lib, might as well cover the real GPS use case, coming from an unpadded stream of raw bytes. Although this use cas is not 100% verified yet, the interface allows it and it should be fairly close. Padded and aligned data, as this use case require, does work.
author_association: COLLABORATOR