Skip to main content

sbf_tools/blocks/
mod.rs

1//! SBF block definitions and parsing
2//!
3//! This module contains all supported SBF block types and the main `SbfBlock` enum.
4
5mod attitude;
6pub mod catalog;
7mod dnu;
8mod differential;
9mod extended;
10mod external;
11mod ins;
12mod meas3;
13mod meas3_decoder;
14mod measurement;
15mod navigation;
16mod position;
17mod sbas;
18mod status;
19mod time;
20
21pub use attitude::*;
22pub use differential::*;
23pub use extended::*;
24pub use external::*;
25pub use ins::*;
26pub use meas3::*;
27pub use meas3_decoder::*;
28pub use measurement::*;
29pub use navigation::*;
30pub use position::*;
31pub use sbas::*;
32pub use status::*;
33pub use time::*;
34
35pub use catalog::fallback_name;
36
37use crate::error::{SbfError, SbfResult};
38use crate::header::SbfHeader;
39
40// ============================================================================
41// Block IDs
42// ============================================================================
43
44/// Block ID constants for commonly used SBF blocks
45pub mod block_ids {
46    // Measurement blocks
47    pub const MEAS_EPOCH: u16 = 4027;
48    pub const MEAS_EXTRA: u16 = 4000;
49    pub const END_OF_MEAS: u16 = 5922;
50    pub const IQ_CORR: u16 = 4046;
51    pub const MEAS3_RANGES: u16 = 4109;
52    pub const MEAS3_CN0_HI_RES: u16 = 4110;
53    pub const MEAS3_DOPPLER: u16 = 4111;
54    pub const MEAS3_PP: u16 = 4112;
55    pub const MEAS3_MP: u16 = 4113;
56
57    // Position blocks
58    pub const PVT_CARTESIAN: u16 = 4006;
59    pub const PVT_GEODETIC: u16 = 4007;
60    pub const DOP: u16 = 4001;
61    pub const DOP_LEGACY: u16 = 5909;
62    pub const POS_CART: u16 = 4044;
63    pub const PVT_SAT_CARTESIAN: u16 = 4008;
64    pub const PVT_RESIDUALS_V2: u16 = 4009;
65    pub const RAIM_STATISTICS_V2: u16 = 4011;
66    pub const BASE_VECTOR_CART: u16 = 4043;
67    pub const BASE_VECTOR_GEOD: u16 = 4028;
68    pub const POS_COV_CARTESIAN: u16 = 5905;
69    pub const POS_COV_GEODETIC: u16 = 5906;
70    pub const VEL_COV_CARTESIAN: u16 = 5907;
71    pub const VEL_COV_GEODETIC: u16 = 5908;
72    pub const GEO_CORRECTIONS: u16 = 5935;
73    pub const BASE_STATION: u16 = 5949;
74    pub const DIFF_CORR_IN: u16 = 5919;
75    pub const END_OF_PVT: u16 = 5921;
76    pub const PVT_SUPPORT: u16 = 4076;
77    pub const PVT_SUPPORT_A: u16 = 4079;
78    pub const FUGRO_DDS: u16 = 4211;
79    pub const POS_LOCAL: u16 = 4052;
80    pub const POS_PROJECTED: u16 = 4094;
81
82    // INS blocks
83    pub const INT_PV_CART: u16 = 4060;
84    pub const INT_PV_GEOD: u16 = 4061;
85    pub const INT_PVA_AGEOD: u16 = 4045;
86    pub const INT_POS_COV_CART: u16 = 4062;
87    pub const INT_VEL_COV_CART: u16 = 4063;
88    pub const INT_POS_COV_GEOD: u16 = 4064;
89    pub const INT_VEL_COV_GEOD: u16 = 4065;
90    pub const INT_ATT_EULER: u16 = 4070;
91    pub const INT_ATT_COV_EULER: u16 = 4072;
92
93    // Status blocks
94
95    // Attitude blocks
96    pub const ATT_EULER: u16 = 5938;
97    pub const ATT_COV_EULER: u16 = 5939;
98    pub const AUX_ANT_POSITIONS: u16 = 5942;
99    pub const END_OF_ATT: u16 = 5943;
100
101    // Navigation message blocks
102    pub const GPS_NAV: u16 = 5891;
103    pub const GPS_ALM: u16 = 5892;
104    pub const GPS_ION: u16 = 5893;
105    pub const GPS_UTC: u16 = 5894;
106    pub const GPS_CNAV: u16 = 4042;
107    pub const GPS_RAW_CA: u16 = 4017;
108    pub const GPS_RAW_L2C: u16 = 4018;
109    pub const GPS_RAW_L5: u16 = 4019;
110    pub const GEO_RAW_L1: u16 = 4020;
111    pub const GAL_RAW_FNAV: u16 = 4022;
112    pub const GAL_RAW_INAV: u16 = 4023;
113    pub const GAL_RAW_CNAV: u16 = 4024;
114    pub const GLO_RAW_CA: u16 = 4026;
115    pub const CMP_RAW: u16 = 4047;
116    pub const QZS_RAW_L1CA: u16 = 4066;
117    pub const QZS_RAW_L2C: u16 = 4067;
118    pub const QZS_RAW_L5: u16 = 4068;
119    pub const GLO_NAV: u16 = 4004;
120    pub const GLO_ALM: u16 = 4005;
121    pub const GLO_TIME: u16 = 4036;
122    pub const GAL_NAV: u16 = 4002;
123    pub const GAL_ALM: u16 = 4003;
124    pub const GAL_ION: u16 = 4030;
125    pub const GAL_UTC: u16 = 4031;
126    pub const GAL_GST_GPS: u16 = 4032;
127    pub const GAL_SAR_RLM: u16 = 4034;
128    pub const BDS_ION: u16 = 4120;
129    pub const BDS_NAV: u16 = 4081;
130    pub const BDS_ALM: u16 = 4119;
131    pub const BDS_UTC: u16 = 4121;
132    pub const BDS_CNAV1: u16 = 4251;
133    pub const BDS_CNAV2: u16 = 4252;
134    pub const BDS_CNAV3: u16 = 4253;
135    pub const QZS_NAV: u16 = 4095;
136    pub const QZS_ALM: u16 = 4116;
137    pub const GEO_RAW_L5: u16 = 4021;
138    pub const BDS_RAW_B1C: u16 = 4218;
139    pub const BDS_RAW_B2A: u16 = 4219;
140    /// BeiDou B2b navigation frame
141    pub const BDS_RAW_B2B: u16 = 4242;
142    /// Galileo OSNMA authentication status
143    pub const GAL_AUTH_STATUS: u16 = 4245;
144    pub const NAVIC_RAW: u16 = 4093;
145    pub const GEO_IONO_DELAY: u16 = 5933;
146    pub const GEO_MT00: u16 = 5925;
147    pub const GEO_PRN_MASK: u16 = 5926;
148    pub const GEO_FAST_CORR: u16 = 5927;
149    pub const GEO_FAST_CORR_DEGR: u16 = 5929;
150    pub const GEO_DEGR_FACTORS: u16 = 5930;
151    pub const GEO_SERVICE_LEVEL: u16 = 5917;
152    pub const GEO_NAV: u16 = 5896;
153    pub const GEO_INTEGRITY: u16 = 5928;
154    pub const GEO_ALM: u16 = 5897;
155    pub const GEO_NETWORK_TIME: u16 = 5918;
156    pub const GEO_IGP_MASK: u16 = 5931;
157    pub const GEO_LONG_TERM_CORR: u16 = 5932;
158    pub const GEO_CLOCK_EPH_COV_MATRIX: u16 = 5934;
159
160    // Status blocks
161    // TODO(spec-audit): v4.15.1 documents ReceiverStatus as 4014.
162    // Legacy ReceiverStatus IDs (for example 5913 in older references) remain unverified.
163    pub const RECEIVER_STATUS: u16 = 4014;
164    pub const TRACKING_STATUS: u16 = 5912;
165    pub const CHANNEL_STATUS: u16 = 4013;
166    pub const SAT_VISIBILITY: u16 = 4012;
167    pub const QUALITY_IND: u16 = 4082;
168    pub const INPUT_LINK: u16 = 4090;
169    pub const OUTPUT_LINK: u16 = 4091;
170    pub const IP_STATUS: u16 = 4058;
171    pub const LBAND_TRACKER_STATUS: u16 = 4201;
172    pub const LBAND_BEAMS: u16 = 4204;
173    pub const EXT_SENSOR_MEAS: u16 = 4050;
174    pub const EXT_SENSOR_STATUS: u16 = 4056;
175    pub const EXT_SENSOR_SETUP: u16 = 4057;
176    pub const COMMANDS: u16 = 4015;
177    pub const COMMENT: u16 = 5936;
178    pub const RECEIVER_SETUP: u16 = 5902;
179    pub const BB_SAMPLES: u16 = 4040;
180    pub const ASCII_IN: u16 = 4075;
181    pub const RX_MESSAGE: u16 = 4103;
182    pub const ENCAPSULATED_OUTPUT: u16 = 4097;
183    pub const GIS_ACTION: u16 = 4106;
184    pub const GIS_STATUS: u16 = 4107;
185    pub const DYN_DNS_STATUS: u16 = 4105;
186    pub const DISK_STATUS: u16 = 4059;
187    pub const NTRIP_CLIENT_STATUS: u16 = 4053;
188    pub const NTRIP_SERVER_STATUS: u16 = 4122;
189    pub const RF_STATUS: u16 = 4092;
190    pub const P2PP_STATUS: u16 = 4238;
191    pub const COSMOS_STATUS: u16 = 4243;
192    pub const RTCM_DATUM: u16 = 4049;
193
194    // Time blocks
195    pub const RECEIVER_TIME: u16 = 5914;
196    pub const PPS_OFFSET: u16 = 5911;
197    pub const EXT_EVENT: u16 = 5924;
198    pub const EXT_EVENT_PVT_CARTESIAN: u16 = 4037;
199    pub const EXT_EVENT_PVT_GEODETIC: u16 = 4038;
200    pub const EXT_EVENT_BASE_VECT_GEOD: u16 = 4217;
201    pub const EXT_EVENT_ATT_EULER: u16 = 4237;
202}
203
204// ============================================================================
205// Block Name Lookup
206// ============================================================================
207
208/// Get a human-readable name for a block ID
209pub fn block_name(id: u16) -> &'static str {
210    match id {
211        block_ids::MEAS_EPOCH => "MeasEpoch",
212        block_ids::MEAS_EXTRA => "MeasExtra",
213        block_ids::IQ_CORR => "IQCorr",
214        block_ids::END_OF_MEAS => "EndOfMeas",
215        block_ids::MEAS3_RANGES => "Meas3Ranges",
216        block_ids::MEAS3_CN0_HI_RES => "Meas3CN0HiRes",
217        block_ids::MEAS3_DOPPLER => "Meas3Doppler",
218        block_ids::MEAS3_PP => "Meas3PP",
219        block_ids::MEAS3_MP => "Meas3MP",
220        block_ids::PVT_CARTESIAN => "PVTCartesian",
221        block_ids::PVT_GEODETIC => "PVTGeodetic",
222        block_ids::DOP => "DOP",
223        block_ids::DOP_LEGACY => "DOP",
224        block_ids::POS_CART => "PosCart",
225        block_ids::PVT_SAT_CARTESIAN => "PVTSatCartesian",
226        block_ids::PVT_RESIDUALS_V2 => "PVTResiduals_v2",
227        block_ids::RAIM_STATISTICS_V2 => "RAIMStatistics_v2",
228        block_ids::BASE_VECTOR_CART => "BaseVectorCart",
229        block_ids::BASE_VECTOR_GEOD => "BaseVectorGeod",
230        block_ids::POS_COV_CARTESIAN => "PosCovCartesian",
231        block_ids::POS_COV_GEODETIC => "PosCovGeodetic",
232        block_ids::VEL_COV_CARTESIAN => "VelCovCartesian",
233        block_ids::VEL_COV_GEODETIC => "VelCovGeodetic",
234        block_ids::GEO_CORRECTIONS => "GEOCorrections",
235        block_ids::BASE_STATION => "BaseStation",
236        block_ids::DIFF_CORR_IN => "DiffCorrIn",
237        block_ids::END_OF_PVT => "EndOfPVT",
238        block_ids::PVT_SUPPORT => "PVTSupport",
239        block_ids::PVT_SUPPORT_A => "PVTSupportA",
240        block_ids::FUGRO_DDS => "FugroDDS",
241        block_ids::INT_PV_CART => "IntPVCart",
242        block_ids::INT_PV_GEOD => "IntPVGeod",
243        block_ids::INT_PVA_AGEOD => "IntPVAAGeod",
244        block_ids::INT_ATT_EULER => "IntAttEuler",
245        block_ids::INT_POS_COV_CART => "IntPosCovCart",
246        block_ids::INT_VEL_COV_CART => "IntVelCovCart",
247        block_ids::INT_POS_COV_GEOD => "IntPosCovGeod",
248        block_ids::INT_VEL_COV_GEOD => "IntVelCovGeod",
249        block_ids::INT_ATT_COV_EULER => "IntAttCovEuler",
250        block_ids::IP_STATUS => "IPStatus",
251        block_ids::EXT_SENSOR_MEAS => "ExtSensorMeas",
252        block_ids::EXT_SENSOR_STATUS => "ExtSensorStatus",
253        block_ids::EXT_SENSOR_SETUP => "ExtSensorSetup",
254        block_ids::ATT_EULER => "AttEuler",
255        block_ids::ATT_COV_EULER => "AttCovEuler",
256        block_ids::AUX_ANT_POSITIONS => "AuxAntPositions",
257        block_ids::END_OF_ATT => "EndOfAtt",
258        block_ids::GPS_NAV => "GPSNav",
259        block_ids::GPS_ALM => "GPSAlm",
260        block_ids::GPS_ION => "GPSIon",
261        block_ids::GPS_UTC => "GPSUtc",
262        block_ids::GPS_CNAV => "GPSCNav",
263        block_ids::GLO_NAV => "GLONav",
264        block_ids::GLO_ALM => "GLOAlm",
265        block_ids::GLO_TIME => "GLOTime",
266        block_ids::GAL_NAV => "GALNav",
267        block_ids::GAL_ALM => "GALAlm",
268        block_ids::GAL_ION => "GALIon",
269        block_ids::GAL_UTC => "GALUtc",
270        block_ids::GAL_GST_GPS => "GALGstGps",
271        block_ids::GAL_SAR_RLM => "GALSARRLM",
272        block_ids::BDS_ION => "BDSIon",
273        block_ids::BDS_CNAV1 => "BDSCNav1",
274        block_ids::GEO_IONO_DELAY => "GEOIonoDelay",
275        block_ids::GEO_MT00 => "GEOMT00",
276        block_ids::GEO_PRN_MASK => "GEOPRNMask",
277        block_ids::GEO_FAST_CORR => "GEOFastCorr",
278        block_ids::GEO_FAST_CORR_DEGR => "GEOFastCorrDegr",
279        block_ids::GEO_DEGR_FACTORS => "GEODegrFactors",
280        block_ids::GEO_SERVICE_LEVEL => "GEOServiceLevel",
281        block_ids::GEO_NAV => "GEONav",
282        block_ids::GEO_INTEGRITY => "GEOIntegrity",
283        block_ids::GEO_ALM => "GEOAlm",
284        block_ids::GEO_NETWORK_TIME => "GEONetworkTime",
285        block_ids::GEO_IGP_MASK => "GEOIGPMask",
286        block_ids::GEO_LONG_TERM_CORR => "GEOLongTermCorr",
287        block_ids::GEO_CLOCK_EPH_COV_MATRIX => "GEOClockEphCovMatrix",
288        block_ids::GPS_RAW_CA => "GPSRawCA",
289        block_ids::GPS_RAW_L2C => "GPSRawL2C",
290        block_ids::GPS_RAW_L5 => "GPSRawL5",
291        block_ids::GAL_RAW_FNAV => "GALRawFNAV",
292        block_ids::GAL_RAW_INAV => "GALRawINAV",
293        block_ids::GAL_RAW_CNAV => "GALRawCNAV",
294        block_ids::GEO_RAW_L1 => "GEORawL1",
295        block_ids::GLO_RAW_CA => "GLORawCA",
296        block_ids::CMP_RAW => "CMPRaw",
297        block_ids::QZS_RAW_L1CA => "QZSRawL1CA",
298        block_ids::QZS_RAW_L2C => "QZSRawL2C",
299        block_ids::QZS_RAW_L5 => "QZSRawL5",
300        block_ids::RECEIVER_STATUS => "ReceiverStatus",
301        block_ids::TRACKING_STATUS => "TrackingStatus",
302        block_ids::CHANNEL_STATUS => "ChannelStatus",
303        block_ids::SAT_VISIBILITY => "SatVisibility",
304        block_ids::QUALITY_IND => "QualityInd",
305        block_ids::INPUT_LINK => "InputLink",
306        block_ids::OUTPUT_LINK => "OutputLink",
307        block_ids::LBAND_TRACKER_STATUS => "LBandTrackerStatus",
308        block_ids::COMMANDS => "Commands",
309        block_ids::COMMENT => "Comment",
310        block_ids::RECEIVER_SETUP => "ReceiverSetup",
311        block_ids::BB_SAMPLES => "BBSamples",
312        block_ids::ASCII_IN => "ASCIIIn",
313        block_ids::NTRIP_CLIENT_STATUS => "NTRIPClientStatus",
314        block_ids::NTRIP_SERVER_STATUS => "NTRIPServerStatus",
315        block_ids::RF_STATUS => "RFStatus",
316        block_ids::RECEIVER_TIME => "ReceiverTime",
317        block_ids::PPS_OFFSET => "xPPSOffset",
318        block_ids::EXT_EVENT => "ExtEvent",
319        block_ids::EXT_EVENT_PVT_CARTESIAN => "ExtEventPVTCartesian",
320        block_ids::EXT_EVENT_PVT_GEODETIC => "ExtEventPVTGeodetic",
321        block_ids::EXT_EVENT_BASE_VECT_GEOD => "ExtEventBaseVectGeod",
322        block_ids::EXT_EVENT_ATT_EULER => "ExtEventAttEuler",
323        block_ids::POS_LOCAL => "PosLocal",
324        block_ids::POS_PROJECTED => "PosProjected",
325        block_ids::BDS_NAV => "BDSNav",
326        block_ids::BDS_ALM => "BDSAlm",
327        block_ids::BDS_UTC => "BDSUtc",
328        block_ids::BDS_CNAV2 => "BDSCNav2",
329        block_ids::BDS_CNAV3 => "BDSCNav3",
330        block_ids::QZS_NAV => "QZSNav",
331        block_ids::QZS_ALM => "QZSAlm",
332        block_ids::GEO_RAW_L5 => "GEORawL5",
333        block_ids::BDS_RAW_B1C => "BDSRawB1C",
334        block_ids::BDS_RAW_B2A => "BDSRawB2a",
335        block_ids::BDS_RAW_B2B => "BDSRawB2b",
336        block_ids::GAL_AUTH_STATUS => "GALAuthStatus",
337        block_ids::NAVIC_RAW => "NAVICRaw",
338        block_ids::RTCM_DATUM => "RTCMDatum",
339        block_ids::LBAND_BEAMS => "LBandBeams",
340        block_ids::DYN_DNS_STATUS => "DynDNSStatus",
341        block_ids::DISK_STATUS => "DiskStatus",
342        block_ids::P2PP_STATUS => "P2PPStatus",
343        block_ids::COSMOS_STATUS => "CosmosStatus",
344        block_ids::RX_MESSAGE => "RxMessage",
345        block_ids::ENCAPSULATED_OUTPUT => "EncapsulatedOutput",
346        block_ids::GIS_ACTION => "GISAction",
347        block_ids::GIS_STATUS => "GISStatus",
348        _ => catalog::fallback_name(id),
349    }
350}
351
352/// IDs that are known/catalogued but intentionally parsed as opaque payloads.
353pub fn is_known_opaque_id(id: u16) -> bool {
354    matches!(id, block_ids::FUGRO_DDS)
355}
356
357// ============================================================================
358// Block Trait
359// ============================================================================
360
361/// Trait for SBF block parsing
362pub trait SbfBlockParse: Sized {
363    /// The block ID for this block type
364    const BLOCK_ID: u16;
365
366    /// Parse block from data (starting after sync bytes)
367    ///
368    /// # Arguments
369    /// * `header` - Parsed block header
370    /// * `data` - Block data starting from CRC field (after sync bytes)
371    ///
372    /// # Returns
373    /// Parsed block or error
374    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self>;
375}
376
377// ============================================================================
378// Main Block Enum
379// ============================================================================
380
381/// Parsed SBF block variants.
382///
383/// This enum is [`non_exhaustive`]: new block types may appear in any minor release.
384/// Code outside this crate that matches on [`SbfBlock`](Self) should include a wildcard
385/// pattern (for example `_ =>` or a catch-all) so it keeps compiling as variants are added.
386#[non_exhaustive]
387#[derive(Debug, Clone)]
388pub enum SbfBlock {
389    // Measurement blocks
390    MeasEpoch(MeasEpochBlock),
391    MeasExtra(MeasExtraBlock),
392    EndOfMeas(EndOfMeasBlock),
393    Meas3Ranges(Meas3RangesBlock),
394    Meas3Cn0HiRes(Meas3Cn0HiResBlock),
395    Meas3Doppler(Meas3DopplerBlock),
396    Meas3Pp(Meas3PpBlock),
397    Meas3Mp(Meas3MpBlock),
398
399    // Position blocks
400    PvtGeodetic(PvtGeodeticBlock),
401    PvtCartesian(PvtCartesianBlock),
402    Dop(DopBlock),
403    PosCart(PosCartBlock),
404    PvtSatCartesian(PvtSatCartesianBlock),
405    PvtResidualsV2(PvtResidualsV2Block),
406    RaimStatisticsV2(RaimStatisticsV2Block),
407    BaseVectorCart(BaseVectorCartBlock),
408    BaseVectorGeod(BaseVectorGeodBlock),
409    PosCovCartesian(PosCovCartesianBlock),
410    PosCovGeodetic(PosCovGeodeticBlock),
411    VelCovCartesian(VelCovCartesianBlock),
412    VelCovGeodetic(VelCovGeodeticBlock),
413    GeoCorrections(GeoCorrectionsBlock),
414    BaseStation(BaseStationBlock),
415    DiffCorrIn(DiffCorrInBlock),
416    PvtSupport(PvtSupportBlock),
417    PvtSupportA(PvtSupportABlock),
418    PosLocal(PosLocalBlock),
419    PosProjected(PosProjectedBlock),
420
421    // Attitude blocks
422    AttEuler(AttEulerBlock),
423    AttCovEuler(AttCovEulerBlock),
424    AuxAntPositions(AuxAntPositionsBlock),
425    EndOfAtt(EndOfAttBlock),
426
427    // Navigation blocks
428    GpsNav(GpsNavBlock),
429    GpsAlm(GpsAlmBlock),
430    GpsIon(GpsIonBlock),
431    GpsUtc(GpsUtcBlock),
432    GpsCNav(GpsCNavBlock),
433    GalNav(GalNavBlock),
434    GalAlm(GalAlmBlock),
435    GalIon(GalIonBlock),
436    GalUtc(GalUtcBlock),
437    GalGstGps(GalGstGpsBlock),
438    GalAuthStatus(GalAuthStatusBlock),
439    GalSarRlm(GalSarRlmBlock),
440    GloNav(GloNavBlock),
441    GloAlm(GloAlmBlock),
442    GloTime(GloTimeBlock),
443    BdsIon(BdsIonBlock),
444    BdsNav(BdsNavBlock),
445    BdsAlm(BdsAlmBlock),
446    BdsUtc(BdsUtcBlock),
447    BdsCNav1(BdsCNav1Block),
448    BdsCNav2(BdsCNav2Block),
449    BdsCNav3(BdsCNav3Block),
450    QzsNav(QzsNavBlock),
451    QzsAlm(QzsAlmBlock),
452    GeoIonoDelay(GeoIonoDelayBlock),
453    GpsRawCa(GpsRawCaBlock),
454    GpsRawL2C(GpsRawL2CBlock),
455    GpsRawL5(GpsRawL5Block),
456    GalRawFnav(GalRawFnavBlock),
457    GalRawInav(GalRawInavBlock),
458    GalRawCnav(GalRawCnavBlock),
459    GeoRawL1(GeoRawL1Block),
460    GeoRawL5(GeoRawL5Block),
461    GloRawCa(GloRawCaBlock),
462    CmpRaw(CmpRawBlock),
463    BdsRawB1c(BdsRawB1cBlock),
464    BdsRawB2a(BdsRawB2aBlock),
465    BdsRawB2b(BdsRawB2bBlock),
466    IrnssRaw(IrnssRawBlock),
467    QzsRawL1Ca(QzsRawL1CaBlock),
468    QzsRawL2C(QzsRawL2CBlock),
469    QzsRawL5(QzsRawL5Block),
470    GeoMt00(GeoMt00Block),
471    GeoPrnMask(GeoPrnMaskBlock),
472    GeoFastCorr(GeoFastCorrBlock),
473    GeoFastCorrDegr(GeoFastCorrDegrBlock),
474    GeoDegrFactors(GeoDegrFactorsBlock),
475    GeoServiceLevel(GeoServiceLevelBlock),
476    GeoNav(GeoNavBlock),
477    GeoIntegrity(GeoIntegrityBlock),
478    GeoAlm(GeoAlmBlock),
479    GeoNetworkTime(GeoNetworkTimeBlock),
480    GeoIgpMask(GeoIgpMaskBlock),
481    GeoLongTermCorr(GeoLongTermCorrBlock),
482    GeoClockEphCovMatrix(GeoClockEphCovMatrixBlock),
483
484    // Status blocks
485    ReceiverStatus(ReceiverStatusBlock),
486    TrackingStatus(ChannelStatusBlock),
487    ChannelStatus(ChannelStatusBlock),
488    SatVisibility(SatVisibilityBlock),
489    QualityInd(QualityIndBlock),
490    InputLink(InputLinkBlock),
491    OutputLink(OutputLinkBlock),
492    LBandTrackerStatus(LBandTrackerStatusBlock),
493    Commands(CommandsBlock),
494    Comment(CommentBlock),
495    ReceiverSetup(ReceiverSetupBlock),
496    BBSamples(BBSamplesBlock),
497    ASCIIIn(ASCIIInBlock),
498    NtripClientStatus(NtripClientStatusBlock),
499    NtripServerStatus(NtripServerStatusBlock),
500    RfStatus(RfStatusBlock),
501    RtcmDatum(RtcmDatumBlock),
502    LBandBeams(LBandBeamsBlock),
503    DynDnsStatus(DynDnsStatusBlock),
504    DiskStatus(DiskStatusBlock),
505    P2ppStatus(P2ppStatusBlock),
506    CosmosStatus(CosmosStatusBlock),
507    RxMessage(RxMessageBlock),
508    EncapsulatedOutput(EncapsulatedOutputBlock),
509    GisAction(GisActionBlock),
510    GisStatus(GisStatusBlock),
511
512    // Time blocks
513    ReceiverTime(ReceiverTimeBlock),
514    PpsOffset(PpsOffsetBlock),
515    ExtEvent(ExtEventBlock),
516    ExtEventPvtCartesian(ExtEventPvtCartesianBlock),
517    ExtEventPvtGeodetic(ExtEventPvtGeodeticBlock),
518    ExtEventBaseVectGeod(ExtEventBaseVectGeodBlock),
519    ExtEventAttEuler(ExtEventAttEulerBlock),
520    EndOfPvt(EndOfPvtBlock),
521    IntPvCart(IntPvCartBlock),
522    IntPvGeod(IntPvGeodBlock),
523    IntPvaaGeod(IntPvaaGeodBlock),
524    IntAttEuler(IntAttEulerBlock),
525    IntPosCovCart(IntPosCovCartBlock),
526    IntVelCovCart(IntVelCovCartBlock),
527    IntPosCovGeod(IntPosCovGeodBlock),
528    IntVelCovGeod(IntVelCovGeodBlock),
529    IntAttCovEuler(IntAttCovEulerBlock),
530    IpStatus(IpStatusBlock),
531    IqCorr(IqCorrBlock),
532    ExtSensorMeas(ExtSensorMeasBlock),
533    ExtSensorStatus(ExtSensorStatusBlock),
534    ExtSensorSetup(ExtSensorSetupBlock),
535
536    /// Known block ID with intentionally opaque payload bytes.
537    KnownOpaque {
538        id: u16,
539        rev: u8,
540        /// Raw payload bytes (bytes after the 8-byte SBF header).
541        data: Vec<u8>,
542    },
543
544    /// Unknown block (stores raw payload bytes).
545    Unknown {
546        id: u16,
547        rev: u8,
548        /// Raw payload bytes (bytes after the 8-byte SBF header).
549        data: Vec<u8>,
550    },
551}
552
553impl SbfBlock {
554    /// Parse a block from complete block data (including sync bytes)
555    ///
556    /// # Arguments
557    /// * `data` - Complete block data starting with sync bytes (0x24 0x40)
558    ///
559    /// # Returns
560    /// Tuple of (parsed block, bytes consumed)
561    pub fn parse(data: &[u8]) -> SbfResult<(Self, usize)> {
562        // Verify sync bytes
563        if data.len() < 2 || data[0] != 0x24 || data[1] != 0x40 {
564            return Err(SbfError::InvalidSync);
565        }
566
567        // Parse header
568        let header = SbfHeader::parse(&data[2..])?;
569        let total_len = header.length as usize;
570
571        // Verify we have enough data
572        if data.len() < total_len {
573            return Err(SbfError::IncompleteBlock {
574                needed: total_len,
575                have: data.len(),
576            });
577        }
578
579        // Data starting from CRC (after sync)
580        let block_data = &data[2..];
581
582        // Parse based on block ID
583        let block = match header.block_id {
584            block_ids::MEAS_EPOCH => {
585                SbfBlock::MeasEpoch(MeasEpochBlock::parse(&header, block_data)?)
586            }
587            block_ids::MEAS_EXTRA => {
588                SbfBlock::MeasExtra(MeasExtraBlock::parse(&header, block_data)?)
589            }
590            block_ids::END_OF_MEAS => {
591                SbfBlock::EndOfMeas(EndOfMeasBlock::parse(&header, block_data)?)
592            }
593            block_ids::MEAS3_RANGES => {
594                SbfBlock::Meas3Ranges(Meas3RangesBlock::parse(&header, block_data)?)
595            }
596            block_ids::MEAS3_CN0_HI_RES => {
597                SbfBlock::Meas3Cn0HiRes(Meas3Cn0HiResBlock::parse(&header, block_data)?)
598            }
599            block_ids::MEAS3_DOPPLER => {
600                SbfBlock::Meas3Doppler(Meas3DopplerBlock::parse(&header, block_data)?)
601            }
602            block_ids::MEAS3_PP => SbfBlock::Meas3Pp(Meas3PpBlock::parse(&header, block_data)?),
603            block_ids::MEAS3_MP => SbfBlock::Meas3Mp(Meas3MpBlock::parse(&header, block_data)?),
604            block_ids::PVT_GEODETIC => {
605                SbfBlock::PvtGeodetic(PvtGeodeticBlock::parse(&header, block_data)?)
606            }
607            block_ids::PVT_CARTESIAN => {
608                SbfBlock::PvtCartesian(PvtCartesianBlock::parse(&header, block_data)?)
609            }
610            block_ids::DOP | block_ids::DOP_LEGACY => {
611                SbfBlock::Dop(DopBlock::parse(&header, block_data)?)
612            }
613            block_ids::POS_CART => SbfBlock::PosCart(PosCartBlock::parse(&header, block_data)?),
614            block_ids::PVT_SAT_CARTESIAN => {
615                SbfBlock::PvtSatCartesian(PvtSatCartesianBlock::parse(&header, block_data)?)
616            }
617            block_ids::PVT_RESIDUALS_V2 => {
618                SbfBlock::PvtResidualsV2(PvtResidualsV2Block::parse(&header, block_data)?)
619            }
620            block_ids::RAIM_STATISTICS_V2 => {
621                SbfBlock::RaimStatisticsV2(RaimStatisticsV2Block::parse(&header, block_data)?)
622            }
623            block_ids::BASE_VECTOR_CART => {
624                SbfBlock::BaseVectorCart(BaseVectorCartBlock::parse(&header, block_data)?)
625            }
626            block_ids::BASE_VECTOR_GEOD => {
627                SbfBlock::BaseVectorGeod(BaseVectorGeodBlock::parse(&header, block_data)?)
628            }
629            block_ids::POS_COV_CARTESIAN => {
630                SbfBlock::PosCovCartesian(PosCovCartesianBlock::parse(&header, block_data)?)
631            }
632            block_ids::POS_COV_GEODETIC => {
633                SbfBlock::PosCovGeodetic(PosCovGeodeticBlock::parse(&header, block_data)?)
634            }
635            block_ids::VEL_COV_CARTESIAN => {
636                SbfBlock::VelCovCartesian(VelCovCartesianBlock::parse(&header, block_data)?)
637            }
638            block_ids::VEL_COV_GEODETIC => {
639                SbfBlock::VelCovGeodetic(VelCovGeodeticBlock::parse(&header, block_data)?)
640            }
641            block_ids::GEO_CORRECTIONS => {
642                SbfBlock::GeoCorrections(GeoCorrectionsBlock::parse(&header, block_data)?)
643            }
644            block_ids::BASE_STATION => {
645                SbfBlock::BaseStation(BaseStationBlock::parse(&header, block_data)?)
646            }
647            block_ids::DIFF_CORR_IN => {
648                SbfBlock::DiffCorrIn(DiffCorrInBlock::parse(&header, block_data)?)
649            }
650            block_ids::PVT_SUPPORT => {
651                SbfBlock::PvtSupport(PvtSupportBlock::parse(&header, block_data)?)
652            }
653            block_ids::PVT_SUPPORT_A => {
654                SbfBlock::PvtSupportA(PvtSupportABlock::parse(&header, block_data)?)
655            }
656            block_ids::POS_LOCAL => SbfBlock::PosLocal(PosLocalBlock::parse(&header, block_data)?),
657            block_ids::POS_PROJECTED => {
658                SbfBlock::PosProjected(PosProjectedBlock::parse(&header, block_data)?)
659            }
660            block_ids::INT_PVA_AGEOD => {
661                SbfBlock::IntPvaaGeod(IntPvaaGeodBlock::parse(&header, block_data)?)
662            }
663            block_ids::ATT_EULER => SbfBlock::AttEuler(AttEulerBlock::parse(&header, block_data)?),
664            block_ids::ATT_COV_EULER => {
665                SbfBlock::AttCovEuler(AttCovEulerBlock::parse(&header, block_data)?)
666            }
667            block_ids::AUX_ANT_POSITIONS => {
668                SbfBlock::AuxAntPositions(AuxAntPositionsBlock::parse(&header, block_data)?)
669            }
670            block_ids::END_OF_ATT => SbfBlock::EndOfAtt(EndOfAttBlock::parse(&header, block_data)?),
671            block_ids::GPS_NAV => SbfBlock::GpsNav(GpsNavBlock::parse(&header, block_data)?),
672            block_ids::GPS_ALM => SbfBlock::GpsAlm(GpsAlmBlock::parse(&header, block_data)?),
673            block_ids::GPS_ION => SbfBlock::GpsIon(GpsIonBlock::parse(&header, block_data)?),
674            block_ids::GPS_UTC => SbfBlock::GpsUtc(GpsUtcBlock::parse(&header, block_data)?),
675            block_ids::GPS_CNAV => SbfBlock::GpsCNav(GpsCNavBlock::parse(&header, block_data)?),
676            block_ids::GAL_NAV => SbfBlock::GalNav(GalNavBlock::parse(&header, block_data)?),
677            block_ids::GAL_ALM => SbfBlock::GalAlm(GalAlmBlock::parse(&header, block_data)?),
678            block_ids::GAL_ION => SbfBlock::GalIon(GalIonBlock::parse(&header, block_data)?),
679            block_ids::GAL_UTC => SbfBlock::GalUtc(GalUtcBlock::parse(&header, block_data)?),
680            block_ids::GAL_GST_GPS => {
681                SbfBlock::GalGstGps(GalGstGpsBlock::parse(&header, block_data)?)
682            }
683            block_ids::GAL_AUTH_STATUS => {
684                SbfBlock::GalAuthStatus(GalAuthStatusBlock::parse(&header, block_data)?)
685            }
686            block_ids::GAL_SAR_RLM => {
687                SbfBlock::GalSarRlm(GalSarRlmBlock::parse(&header, block_data)?)
688            }
689            block_ids::GLO_NAV => SbfBlock::GloNav(GloNavBlock::parse(&header, block_data)?),
690            block_ids::GLO_ALM => SbfBlock::GloAlm(GloAlmBlock::parse(&header, block_data)?),
691            block_ids::GLO_TIME => SbfBlock::GloTime(GloTimeBlock::parse(&header, block_data)?),
692            block_ids::BDS_ION => SbfBlock::BdsIon(BdsIonBlock::parse(&header, block_data)?),
693            block_ids::BDS_NAV => SbfBlock::BdsNav(BdsNavBlock::parse(&header, block_data)?),
694            block_ids::BDS_ALM => SbfBlock::BdsAlm(BdsAlmBlock::parse(&header, block_data)?),
695            block_ids::BDS_UTC => SbfBlock::BdsUtc(BdsUtcBlock::parse(&header, block_data)?),
696            block_ids::BDS_CNAV1 => SbfBlock::BdsCNav1(BdsCNav1Block::parse(&header, block_data)?),
697            block_ids::BDS_CNAV2 => SbfBlock::BdsCNav2(BdsCNav2Block::parse(&header, block_data)?),
698            block_ids::BDS_CNAV3 => SbfBlock::BdsCNav3(BdsCNav3Block::parse(&header, block_data)?),
699            block_ids::QZS_NAV => SbfBlock::QzsNav(QzsNavBlock::parse(&header, block_data)?),
700            block_ids::QZS_ALM => SbfBlock::QzsAlm(QzsAlmBlock::parse(&header, block_data)?),
701            block_ids::GEO_IONO_DELAY => {
702                SbfBlock::GeoIonoDelay(GeoIonoDelayBlock::parse(&header, block_data)?)
703            }
704            block_ids::GPS_RAW_CA => SbfBlock::GpsRawCa(GpsRawCaBlock::parse(&header, block_data)?),
705            block_ids::GPS_RAW_L2C => {
706                SbfBlock::GpsRawL2C(GpsRawL2CBlock::parse(&header, block_data)?)
707            }
708            block_ids::GPS_RAW_L5 => SbfBlock::GpsRawL5(GpsRawL5Block::parse(&header, block_data)?),
709            block_ids::GAL_RAW_FNAV => {
710                SbfBlock::GalRawFnav(GalRawFnavBlock::parse(&header, block_data)?)
711            }
712            block_ids::GAL_RAW_INAV => {
713                SbfBlock::GalRawInav(GalRawInavBlock::parse(&header, block_data)?)
714            }
715            block_ids::GAL_RAW_CNAV => {
716                SbfBlock::GalRawCnav(GalRawCnavBlock::parse(&header, block_data)?)
717            }
718            block_ids::GEO_RAW_L1 => SbfBlock::GeoRawL1(GeoRawL1Block::parse(&header, block_data)?),
719            block_ids::GEO_RAW_L5 => SbfBlock::GeoRawL5(GeoRawL5Block::parse(&header, block_data)?),
720            block_ids::GLO_RAW_CA => SbfBlock::GloRawCa(GloRawCaBlock::parse(&header, block_data)?),
721            block_ids::CMP_RAW => SbfBlock::CmpRaw(CmpRawBlock::parse(&header, block_data)?),
722            block_ids::BDS_RAW_B1C => {
723                SbfBlock::BdsRawB1c(BdsRawB1cBlock::parse(&header, block_data)?)
724            }
725            block_ids::BDS_RAW_B2A => {
726                SbfBlock::BdsRawB2a(BdsRawB2aBlock::parse(&header, block_data)?)
727            }
728            block_ids::BDS_RAW_B2B => {
729                SbfBlock::BdsRawB2b(BdsRawB2bBlock::parse(&header, block_data)?)
730            }
731            block_ids::NAVIC_RAW => SbfBlock::IrnssRaw(IrnssRawBlock::parse(&header, block_data)?),
732            block_ids::QZS_RAW_L1CA => {
733                SbfBlock::QzsRawL1Ca(QzsRawL1CaBlock::parse(&header, block_data)?)
734            }
735            block_ids::QZS_RAW_L2C => {
736                SbfBlock::QzsRawL2C(QzsRawL2CBlock::parse(&header, block_data)?)
737            }
738            block_ids::QZS_RAW_L5 => SbfBlock::QzsRawL5(QzsRawL5Block::parse(&header, block_data)?),
739            block_ids::GEO_MT00 => SbfBlock::GeoMt00(GeoMt00Block::parse(&header, block_data)?),
740            block_ids::GEO_PRN_MASK => {
741                SbfBlock::GeoPrnMask(GeoPrnMaskBlock::parse(&header, block_data)?)
742            }
743            block_ids::GEO_FAST_CORR => {
744                SbfBlock::GeoFastCorr(GeoFastCorrBlock::parse(&header, block_data)?)
745            }
746            block_ids::GEO_FAST_CORR_DEGR => {
747                SbfBlock::GeoFastCorrDegr(GeoFastCorrDegrBlock::parse(&header, block_data)?)
748            }
749            block_ids::GEO_DEGR_FACTORS => {
750                SbfBlock::GeoDegrFactors(GeoDegrFactorsBlock::parse(&header, block_data)?)
751            }
752            block_ids::GEO_SERVICE_LEVEL => {
753                SbfBlock::GeoServiceLevel(GeoServiceLevelBlock::parse(&header, block_data)?)
754            }
755            block_ids::GEO_NAV => SbfBlock::GeoNav(GeoNavBlock::parse(&header, block_data)?),
756            block_ids::GEO_INTEGRITY => {
757                SbfBlock::GeoIntegrity(GeoIntegrityBlock::parse(&header, block_data)?)
758            }
759            block_ids::GEO_ALM => SbfBlock::GeoAlm(GeoAlmBlock::parse(&header, block_data)?),
760            block_ids::GEO_NETWORK_TIME => {
761                SbfBlock::GeoNetworkTime(GeoNetworkTimeBlock::parse(&header, block_data)?)
762            }
763            block_ids::GEO_IGP_MASK => {
764                SbfBlock::GeoIgpMask(GeoIgpMaskBlock::parse(&header, block_data)?)
765            }
766            block_ids::GEO_LONG_TERM_CORR => {
767                SbfBlock::GeoLongTermCorr(GeoLongTermCorrBlock::parse(&header, block_data)?)
768            }
769            block_ids::GEO_CLOCK_EPH_COV_MATRIX => SbfBlock::GeoClockEphCovMatrix(
770                GeoClockEphCovMatrixBlock::parse(&header, block_data)?,
771            ),
772            block_ids::RECEIVER_STATUS => {
773                SbfBlock::ReceiverStatus(ReceiverStatusBlock::parse(&header, block_data)?)
774            }
775            block_ids::TRACKING_STATUS => {
776                SbfBlock::TrackingStatus(ChannelStatusBlock::parse(&header, block_data)?)
777            }
778            block_ids::CHANNEL_STATUS => {
779                SbfBlock::ChannelStatus(ChannelStatusBlock::parse(&header, block_data)?)
780            }
781            block_ids::SAT_VISIBILITY => {
782                SbfBlock::SatVisibility(SatVisibilityBlock::parse(&header, block_data)?)
783            }
784            block_ids::QUALITY_IND => {
785                SbfBlock::QualityInd(QualityIndBlock::parse(&header, block_data)?)
786            }
787            block_ids::INPUT_LINK => {
788                SbfBlock::InputLink(InputLinkBlock::parse(&header, block_data)?)
789            }
790            block_ids::OUTPUT_LINK => {
791                SbfBlock::OutputLink(OutputLinkBlock::parse(&header, block_data)?)
792            }
793            block_ids::LBAND_TRACKER_STATUS => {
794                SbfBlock::LBandTrackerStatus(LBandTrackerStatusBlock::parse(&header, block_data)?)
795            }
796            block_ids::COMMANDS => SbfBlock::Commands(CommandsBlock::parse(&header, block_data)?),
797            block_ids::COMMENT => SbfBlock::Comment(CommentBlock::parse(&header, block_data)?),
798            block_ids::RECEIVER_SETUP => {
799                SbfBlock::ReceiverSetup(ReceiverSetupBlock::parse(&header, block_data)?)
800            }
801            block_ids::BB_SAMPLES => {
802                SbfBlock::BBSamples(BBSamplesBlock::parse(&header, block_data)?)
803            }
804            block_ids::ASCII_IN => SbfBlock::ASCIIIn(ASCIIInBlock::parse(&header, block_data)?),
805            block_ids::NTRIP_CLIENT_STATUS => {
806                SbfBlock::NtripClientStatus(NtripClientStatusBlock::parse(&header, block_data)?)
807            }
808            block_ids::NTRIP_SERVER_STATUS => {
809                SbfBlock::NtripServerStatus(NtripServerStatusBlock::parse(&header, block_data)?)
810            }
811            block_ids::RF_STATUS => SbfBlock::RfStatus(RfStatusBlock::parse(&header, block_data)?),
812            block_ids::RTCM_DATUM => SbfBlock::RtcmDatum(RtcmDatumBlock::parse(&header, block_data)?),
813            block_ids::LBAND_BEAMS => {
814                SbfBlock::LBandBeams(LBandBeamsBlock::parse(&header, block_data)?)
815            }
816            block_ids::DYN_DNS_STATUS => {
817                SbfBlock::DynDnsStatus(DynDnsStatusBlock::parse(&header, block_data)?)
818            }
819            block_ids::DISK_STATUS => {
820                SbfBlock::DiskStatus(DiskStatusBlock::parse(&header, block_data)?)
821            }
822            block_ids::P2PP_STATUS => {
823                SbfBlock::P2ppStatus(P2ppStatusBlock::parse(&header, block_data)?)
824            }
825            block_ids::COSMOS_STATUS => {
826                SbfBlock::CosmosStatus(CosmosStatusBlock::parse(&header, block_data)?)
827            }
828            block_ids::RX_MESSAGE => {
829                SbfBlock::RxMessage(RxMessageBlock::parse(&header, block_data)?)
830            }
831            block_ids::ENCAPSULATED_OUTPUT => SbfBlock::EncapsulatedOutput(
832                EncapsulatedOutputBlock::parse(&header, block_data)?,
833            ),
834            block_ids::GIS_ACTION => {
835                SbfBlock::GisAction(GisActionBlock::parse(&header, block_data)?)
836            }
837            block_ids::GIS_STATUS => {
838                SbfBlock::GisStatus(GisStatusBlock::parse(&header, block_data)?)
839            }
840            block_ids::RECEIVER_TIME => {
841                SbfBlock::ReceiverTime(ReceiverTimeBlock::parse(&header, block_data)?)
842            }
843            block_ids::PPS_OFFSET => {
844                SbfBlock::PpsOffset(PpsOffsetBlock::parse(&header, block_data)?)
845            }
846            block_ids::EXT_EVENT => SbfBlock::ExtEvent(ExtEventBlock::parse(&header, block_data)?),
847            block_ids::EXT_EVENT_PVT_CARTESIAN => SbfBlock::ExtEventPvtCartesian(
848                ExtEventPvtCartesianBlock::parse(&header, block_data)?,
849            ),
850            block_ids::EXT_EVENT_PVT_GEODETIC => {
851                SbfBlock::ExtEventPvtGeodetic(ExtEventPvtGeodeticBlock::parse(&header, block_data)?)
852            }
853            block_ids::EXT_EVENT_BASE_VECT_GEOD => SbfBlock::ExtEventBaseVectGeod(
854                ExtEventBaseVectGeodBlock::parse(&header, block_data)?,
855            ),
856            block_ids::EXT_EVENT_ATT_EULER => {
857                SbfBlock::ExtEventAttEuler(ExtEventAttEulerBlock::parse(&header, block_data)?)
858            }
859            block_ids::END_OF_PVT => SbfBlock::EndOfPvt(EndOfPvtBlock::parse(&header, block_data)?),
860            block_ids::INT_PV_CART => {
861                SbfBlock::IntPvCart(IntPvCartBlock::parse(&header, block_data)?)
862            }
863            block_ids::INT_PV_GEOD => {
864                SbfBlock::IntPvGeod(IntPvGeodBlock::parse(&header, block_data)?)
865            }
866            block_ids::INT_ATT_EULER => {
867                SbfBlock::IntAttEuler(IntAttEulerBlock::parse(&header, block_data)?)
868            }
869            block_ids::INT_POS_COV_CART => {
870                SbfBlock::IntPosCovCart(IntPosCovCartBlock::parse(&header, block_data)?)
871            }
872            block_ids::INT_VEL_COV_CART => {
873                SbfBlock::IntVelCovCart(IntVelCovCartBlock::parse(&header, block_data)?)
874            }
875            block_ids::INT_POS_COV_GEOD => {
876                SbfBlock::IntPosCovGeod(IntPosCovGeodBlock::parse(&header, block_data)?)
877            }
878            block_ids::INT_VEL_COV_GEOD => {
879                SbfBlock::IntVelCovGeod(IntVelCovGeodBlock::parse(&header, block_data)?)
880            }
881            block_ids::INT_ATT_COV_EULER => {
882                SbfBlock::IntAttCovEuler(IntAttCovEulerBlock::parse(&header, block_data)?)
883            }
884            block_ids::IP_STATUS => SbfBlock::IpStatus(IpStatusBlock::parse(&header, block_data)?),
885            block_ids::IQ_CORR => SbfBlock::IqCorr(IqCorrBlock::parse(&header, block_data)?),
886            block_ids::EXT_SENSOR_MEAS => {
887                SbfBlock::ExtSensorMeas(ExtSensorMeasBlock::parse(&header, block_data)?)
888            }
889            block_ids::EXT_SENSOR_STATUS => {
890                SbfBlock::ExtSensorStatus(ExtSensorStatusBlock::parse(&header, block_data)?)
891            }
892            block_ids::EXT_SENSOR_SETUP => {
893                SbfBlock::ExtSensorSetup(ExtSensorSetupBlock::parse(&header, block_data)?)
894            }
895            id if is_known_opaque_id(id) => SbfBlock::KnownOpaque {
896                id,
897                rev: header.block_rev,
898                data: data[8..total_len].to_vec(),
899            },
900            _ => SbfBlock::Unknown {
901                id: header.block_id,
902                rev: header.block_rev,
903                data: data[8..total_len].to_vec(),
904            },
905        };
906
907        Ok((block, total_len))
908    }
909
910    /// Get the block ID
911    pub fn block_id(&self) -> u16 {
912        match self {
913            SbfBlock::MeasEpoch(_) => block_ids::MEAS_EPOCH,
914            SbfBlock::MeasExtra(_) => block_ids::MEAS_EXTRA,
915            SbfBlock::EndOfMeas(_) => block_ids::END_OF_MEAS,
916            SbfBlock::Meas3Ranges(_) => block_ids::MEAS3_RANGES,
917            SbfBlock::Meas3Cn0HiRes(_) => block_ids::MEAS3_CN0_HI_RES,
918            SbfBlock::Meas3Doppler(_) => block_ids::MEAS3_DOPPLER,
919            SbfBlock::Meas3Pp(_) => block_ids::MEAS3_PP,
920            SbfBlock::Meas3Mp(_) => block_ids::MEAS3_MP,
921            SbfBlock::PvtGeodetic(_) => block_ids::PVT_GEODETIC,
922            SbfBlock::PvtCartesian(_) => block_ids::PVT_CARTESIAN,
923            SbfBlock::Dop(_) => block_ids::DOP,
924            SbfBlock::PosCart(_) => block_ids::POS_CART,
925            SbfBlock::PvtSatCartesian(_) => block_ids::PVT_SAT_CARTESIAN,
926            SbfBlock::PvtResidualsV2(_) => block_ids::PVT_RESIDUALS_V2,
927            SbfBlock::RaimStatisticsV2(_) => block_ids::RAIM_STATISTICS_V2,
928            SbfBlock::BaseVectorCart(_) => block_ids::BASE_VECTOR_CART,
929            SbfBlock::BaseVectorGeod(_) => block_ids::BASE_VECTOR_GEOD,
930            SbfBlock::PosCovCartesian(_) => block_ids::POS_COV_CARTESIAN,
931            SbfBlock::PosCovGeodetic(_) => block_ids::POS_COV_GEODETIC,
932            SbfBlock::VelCovCartesian(_) => block_ids::VEL_COV_CARTESIAN,
933            SbfBlock::VelCovGeodetic(_) => block_ids::VEL_COV_GEODETIC,
934            SbfBlock::GeoCorrections(_) => block_ids::GEO_CORRECTIONS,
935            SbfBlock::BaseStation(_) => block_ids::BASE_STATION,
936            SbfBlock::DiffCorrIn(_) => block_ids::DIFF_CORR_IN,
937            SbfBlock::PvtSupport(_) => block_ids::PVT_SUPPORT,
938            SbfBlock::PvtSupportA(_) => block_ids::PVT_SUPPORT_A,
939            SbfBlock::PosLocal(_) => block_ids::POS_LOCAL,
940            SbfBlock::PosProjected(_) => block_ids::POS_PROJECTED,
941            SbfBlock::IntPvaaGeod(_) => block_ids::INT_PVA_AGEOD,
942            SbfBlock::AttEuler(_) => block_ids::ATT_EULER,
943            SbfBlock::AttCovEuler(_) => block_ids::ATT_COV_EULER,
944            SbfBlock::AuxAntPositions(_) => block_ids::AUX_ANT_POSITIONS,
945            SbfBlock::EndOfAtt(_) => block_ids::END_OF_ATT,
946            SbfBlock::GpsNav(_) => block_ids::GPS_NAV,
947            SbfBlock::GpsAlm(_) => block_ids::GPS_ALM,
948            SbfBlock::GpsIon(_) => block_ids::GPS_ION,
949            SbfBlock::GpsUtc(_) => block_ids::GPS_UTC,
950            SbfBlock::GpsCNav(_) => block_ids::GPS_CNAV,
951            SbfBlock::GalNav(_) => block_ids::GAL_NAV,
952            SbfBlock::GalAlm(_) => block_ids::GAL_ALM,
953            SbfBlock::GalIon(_) => block_ids::GAL_ION,
954            SbfBlock::GalUtc(_) => block_ids::GAL_UTC,
955            SbfBlock::GalGstGps(_) => block_ids::GAL_GST_GPS,
956            SbfBlock::GalAuthStatus(_) => block_ids::GAL_AUTH_STATUS,
957            SbfBlock::GalSarRlm(_) => block_ids::GAL_SAR_RLM,
958            SbfBlock::GloNav(_) => block_ids::GLO_NAV,
959            SbfBlock::GloAlm(_) => block_ids::GLO_ALM,
960            SbfBlock::GloTime(_) => block_ids::GLO_TIME,
961            SbfBlock::BdsIon(_) => block_ids::BDS_ION,
962            SbfBlock::BdsNav(_) => block_ids::BDS_NAV,
963            SbfBlock::BdsAlm(_) => block_ids::BDS_ALM,
964            SbfBlock::BdsUtc(_) => block_ids::BDS_UTC,
965            SbfBlock::BdsCNav1(_) => block_ids::BDS_CNAV1,
966            SbfBlock::BdsCNav2(_) => block_ids::BDS_CNAV2,
967            SbfBlock::BdsCNav3(_) => block_ids::BDS_CNAV3,
968            SbfBlock::QzsNav(_) => block_ids::QZS_NAV,
969            SbfBlock::QzsAlm(_) => block_ids::QZS_ALM,
970            SbfBlock::GeoIonoDelay(_) => block_ids::GEO_IONO_DELAY,
971            SbfBlock::GpsRawCa(_) => block_ids::GPS_RAW_CA,
972            SbfBlock::GpsRawL2C(_) => block_ids::GPS_RAW_L2C,
973            SbfBlock::GpsRawL5(_) => block_ids::GPS_RAW_L5,
974            SbfBlock::GalRawFnav(_) => block_ids::GAL_RAW_FNAV,
975            SbfBlock::GalRawInav(_) => block_ids::GAL_RAW_INAV,
976            SbfBlock::GalRawCnav(_) => block_ids::GAL_RAW_CNAV,
977            SbfBlock::GeoRawL1(_) => block_ids::GEO_RAW_L1,
978            SbfBlock::GeoRawL5(_) => block_ids::GEO_RAW_L5,
979            SbfBlock::GloRawCa(_) => block_ids::GLO_RAW_CA,
980            SbfBlock::CmpRaw(_) => block_ids::CMP_RAW,
981            SbfBlock::BdsRawB1c(_) => block_ids::BDS_RAW_B1C,
982            SbfBlock::BdsRawB2a(_) => block_ids::BDS_RAW_B2A,
983            SbfBlock::BdsRawB2b(_) => block_ids::BDS_RAW_B2B,
984            SbfBlock::IrnssRaw(_) => block_ids::NAVIC_RAW,
985            SbfBlock::QzsRawL1Ca(_) => block_ids::QZS_RAW_L1CA,
986            SbfBlock::QzsRawL2C(_) => block_ids::QZS_RAW_L2C,
987            SbfBlock::QzsRawL5(_) => block_ids::QZS_RAW_L5,
988            SbfBlock::GeoMt00(_) => block_ids::GEO_MT00,
989            SbfBlock::GeoPrnMask(_) => block_ids::GEO_PRN_MASK,
990            SbfBlock::GeoFastCorr(_) => block_ids::GEO_FAST_CORR,
991            SbfBlock::GeoFastCorrDegr(_) => block_ids::GEO_FAST_CORR_DEGR,
992            SbfBlock::GeoDegrFactors(_) => block_ids::GEO_DEGR_FACTORS,
993            SbfBlock::GeoServiceLevel(_) => block_ids::GEO_SERVICE_LEVEL,
994            SbfBlock::GeoNav(_) => block_ids::GEO_NAV,
995            SbfBlock::GeoIntegrity(_) => block_ids::GEO_INTEGRITY,
996            SbfBlock::GeoAlm(_) => block_ids::GEO_ALM,
997            SbfBlock::GeoNetworkTime(_) => block_ids::GEO_NETWORK_TIME,
998            SbfBlock::GeoIgpMask(_) => block_ids::GEO_IGP_MASK,
999            SbfBlock::GeoLongTermCorr(_) => block_ids::GEO_LONG_TERM_CORR,
1000            SbfBlock::GeoClockEphCovMatrix(_) => block_ids::GEO_CLOCK_EPH_COV_MATRIX,
1001            SbfBlock::ReceiverStatus(_) => block_ids::RECEIVER_STATUS,
1002            SbfBlock::TrackingStatus(_) => block_ids::TRACKING_STATUS,
1003            SbfBlock::ChannelStatus(_) => block_ids::CHANNEL_STATUS,
1004            SbfBlock::SatVisibility(_) => block_ids::SAT_VISIBILITY,
1005            SbfBlock::QualityInd(_) => block_ids::QUALITY_IND,
1006            SbfBlock::InputLink(_) => block_ids::INPUT_LINK,
1007            SbfBlock::OutputLink(_) => block_ids::OUTPUT_LINK,
1008            SbfBlock::LBandTrackerStatus(_) => block_ids::LBAND_TRACKER_STATUS,
1009            SbfBlock::Commands(_) => block_ids::COMMANDS,
1010            SbfBlock::Comment(_) => block_ids::COMMENT,
1011            SbfBlock::ReceiverSetup(_) => block_ids::RECEIVER_SETUP,
1012            SbfBlock::BBSamples(_) => block_ids::BB_SAMPLES,
1013            SbfBlock::ASCIIIn(_) => block_ids::ASCII_IN,
1014            SbfBlock::NtripClientStatus(_) => block_ids::NTRIP_CLIENT_STATUS,
1015            SbfBlock::NtripServerStatus(_) => block_ids::NTRIP_SERVER_STATUS,
1016            SbfBlock::RfStatus(_) => block_ids::RF_STATUS,
1017            SbfBlock::RtcmDatum(_) => block_ids::RTCM_DATUM,
1018            SbfBlock::LBandBeams(_) => block_ids::LBAND_BEAMS,
1019            SbfBlock::DynDnsStatus(_) => block_ids::DYN_DNS_STATUS,
1020            SbfBlock::DiskStatus(_) => block_ids::DISK_STATUS,
1021            SbfBlock::P2ppStatus(_) => block_ids::P2PP_STATUS,
1022            SbfBlock::CosmosStatus(_) => block_ids::COSMOS_STATUS,
1023            SbfBlock::RxMessage(_) => block_ids::RX_MESSAGE,
1024            SbfBlock::EncapsulatedOutput(_) => block_ids::ENCAPSULATED_OUTPUT,
1025            SbfBlock::GisAction(_) => block_ids::GIS_ACTION,
1026            SbfBlock::GisStatus(_) => block_ids::GIS_STATUS,
1027            SbfBlock::ReceiverTime(_) => block_ids::RECEIVER_TIME,
1028            SbfBlock::PpsOffset(_) => block_ids::PPS_OFFSET,
1029            SbfBlock::ExtEvent(_) => block_ids::EXT_EVENT,
1030            SbfBlock::ExtEventPvtCartesian(_) => block_ids::EXT_EVENT_PVT_CARTESIAN,
1031            SbfBlock::ExtEventPvtGeodetic(_) => block_ids::EXT_EVENT_PVT_GEODETIC,
1032            SbfBlock::ExtEventBaseVectGeod(_) => block_ids::EXT_EVENT_BASE_VECT_GEOD,
1033            SbfBlock::ExtEventAttEuler(_) => block_ids::EXT_EVENT_ATT_EULER,
1034            SbfBlock::EndOfPvt(_) => block_ids::END_OF_PVT,
1035            SbfBlock::IntPvCart(_) => block_ids::INT_PV_CART,
1036            SbfBlock::IntPvGeod(_) => block_ids::INT_PV_GEOD,
1037            SbfBlock::IntAttEuler(_) => block_ids::INT_ATT_EULER,
1038            SbfBlock::IntPosCovCart(_) => block_ids::INT_POS_COV_CART,
1039            SbfBlock::IntVelCovCart(_) => block_ids::INT_VEL_COV_CART,
1040            SbfBlock::IntPosCovGeod(_) => block_ids::INT_POS_COV_GEOD,
1041            SbfBlock::IntVelCovGeod(_) => block_ids::INT_VEL_COV_GEOD,
1042            SbfBlock::IntAttCovEuler(_) => block_ids::INT_ATT_COV_EULER,
1043            SbfBlock::IpStatus(_) => block_ids::IP_STATUS,
1044            SbfBlock::IqCorr(_) => block_ids::IQ_CORR,
1045            SbfBlock::ExtSensorMeas(_) => block_ids::EXT_SENSOR_MEAS,
1046            SbfBlock::ExtSensorStatus(_) => block_ids::EXT_SENSOR_STATUS,
1047            SbfBlock::ExtSensorSetup(_) => block_ids::EXT_SENSOR_SETUP,
1048            SbfBlock::KnownOpaque { id, .. } => *id,
1049            SbfBlock::Unknown { id, .. } => *id,
1050        }
1051    }
1052
1053    /// Get the block name
1054    pub fn name(&self) -> &'static str {
1055        block_name(self.block_id())
1056    }
1057
1058    /// Get raw payload bytes for unsupported blocks.
1059    ///
1060    /// Returns `Some(&[u8])` for `KnownOpaque` and `Unknown` variants, otherwise `None`.
1061    pub fn unsupported_payload(&self) -> Option<&[u8]> {
1062        match self {
1063            SbfBlock::KnownOpaque { data, .. } | SbfBlock::Unknown { data, .. } => Some(data),
1064            _ => None,
1065        }
1066    }
1067}
1068
1069#[cfg(test)]
1070mod tests {
1071    use super::*;
1072
1073    fn build_min_block(block_id: u16, block_rev: u8) -> Vec<u8> {
1074        let total_len = 16u16;
1075        let mut data = vec![0u8; total_len as usize];
1076        data[0] = 0x24;
1077        data[1] = 0x40;
1078
1079        let id_rev = block_id | ((block_rev as u16 & 0x07) << 13);
1080        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1081        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1082        data[8..12].copy_from_slice(&123_456u32.to_le_bytes());
1083        data[12..14].copy_from_slice(&2150u16.to_le_bytes());
1084        data[14] = 0xAA;
1085        data[15] = 0x55;
1086        data
1087    }
1088
1089    #[test]
1090    fn test_meas3_ranges_min_block_parse() {
1091        // Minimum valid Meas3Ranges: block_data length 18 (fixed header through Reserved, empty Data).
1092        let total_len = 20u16;
1093        let mut data = vec![0u8; total_len as usize];
1094        data[0] = 0x24;
1095        data[1] = 0x40;
1096        let id_rev = block_ids::MEAS3_RANGES | ((2u16 & 0x07) << 13);
1097        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1098        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1099        data[8..12].copy_from_slice(&123_456u32.to_le_bytes());
1100        data[12..14].copy_from_slice(&2150u16.to_le_bytes());
1101
1102        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1103        assert_eq!(consumed, usize::from(total_len));
1104        match block {
1105            SbfBlock::Meas3Ranges(m) => {
1106                assert_eq!(m.tow_ms(), 123_456);
1107                assert_eq!(m.wnc(), 2150);
1108                assert_eq!(m.data, Vec::<u8>::new());
1109            }
1110            other => panic!("expected Meas3Ranges, got {:?}", other),
1111        }
1112    }
1113
1114    #[test]
1115    fn test_pvt_support_parse() {
1116        let data = build_min_block(block_ids::PVT_SUPPORT, 1);
1117        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1118
1119        assert_eq!(consumed, 16);
1120        assert_eq!(block.block_id(), block_ids::PVT_SUPPORT);
1121        match block {
1122            SbfBlock::PvtSupport(pvt) => {
1123                assert_eq!(pvt.tow_ms(), 123_456);
1124                assert_eq!(pvt.wnc(), 2150);
1125                assert!((pvt.tow_seconds() - 123.456).abs() < 1e-6);
1126            }
1127            other => panic!("expected PvtSupport, got {:?}", other),
1128        }
1129    }
1130
1131    #[test]
1132    fn test_bds_raw_b2b_min_block_parse() {
1133        let total_len: u16 = 144; // 2 sync + 142 block bytes (B2b min)
1134        let mut data = vec![0u8; total_len as usize];
1135        data[0] = 0x24;
1136        data[1] = 0x40;
1137        let id_rev = block_ids::BDS_RAW_B2B;
1138        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1139        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1140        data[8..12].copy_from_slice(&1_000u32.to_le_bytes());
1141        data[12..14].copy_from_slice(&200u16.to_le_bytes());
1142        data[14] = 5;
1143        data[15] = 1;
1144
1145        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1146        assert_eq!(consumed, total_len as usize);
1147        match block {
1148            SbfBlock::BdsRawB2b(b) => {
1149                assert_eq!(b.tow_ms(), 1_000);
1150                assert_eq!(b.wnc(), 200);
1151                assert_eq!(b.svid, 5);
1152            }
1153            other => panic!("expected BdsRawB2b, got {:?}", other),
1154        }
1155    }
1156
1157    #[test]
1158    fn test_gal_auth_status_min_block_parse() {
1159        let total_len: u16 = 52;
1160        let mut data = vec![0u8; total_len as usize];
1161        data[0] = 0x24;
1162        data[1] = 0x40;
1163        let id_rev = block_ids::GAL_AUTH_STATUS;
1164        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1165        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1166        data[8..12].copy_from_slice(&2_000u32.to_le_bytes());
1167        data[12..14].copy_from_slice(&201u16.to_le_bytes());
1168        data[14..16].copy_from_slice(&0xABCDu16.to_le_bytes());
1169        data[16..20].copy_from_slice(&1.5f32.to_le_bytes());
1170        for i in 0..8usize {
1171            data[20 + i] = 0x10 + i as u8;
1172            data[28 + i] = 0x20 + i as u8;
1173            data[36 + i] = 0x30 + i as u8;
1174            data[44 + i] = 0x40 + i as u8;
1175        }
1176
1177        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1178        assert_eq!(consumed, total_len as usize);
1179        match block {
1180            SbfBlock::GalAuthStatus(a) => {
1181                assert_eq!(a.tow_ms(), 2_000);
1182                assert_eq!(a.wnc(), 201);
1183                assert_eq!(a.osnma_status, 0xABCD);
1184                assert!((a.trusted_time_delta - 1.5).abs() < 1e-6);
1185            }
1186            other => panic!("expected GalAuthStatus, got {:?}", other),
1187        }
1188    }
1189
1190    #[test]
1191    fn test_unknown_id_stays_unknown() {
1192        let data = build_min_block(4999, 0);
1193        let (block, _) = SbfBlock::parse(&data).unwrap();
1194
1195        match block {
1196            SbfBlock::Unknown { id, rev, data } => {
1197                assert_eq!(id, 4999);
1198                assert_eq!(rev, 0);
1199                assert_eq!(data, vec![0x40, 0xE2, 0x01, 0x00, 0x66, 0x08, 0xAA, 0x55]);
1200            }
1201            other => panic!("expected Unknown, got {:?}", other),
1202        }
1203    }
1204
1205    #[test]
1206    fn test_unsupported_payload_helper() {
1207        let known_data = build_min_block(block_ids::FUGRO_DDS, 0);
1208        let (known, _) = SbfBlock::parse(&known_data).unwrap();
1209        assert_eq!(
1210            known.unsupported_payload(),
1211            Some([0x40, 0xE2, 0x01, 0x00, 0x66, 0x08, 0xAA, 0x55].as_slice())
1212        );
1213
1214        let unknown_data = build_min_block(4998, 1);
1215        let (unknown, _) = SbfBlock::parse(&unknown_data).unwrap();
1216        assert_eq!(
1217            unknown.unsupported_payload(),
1218            Some([0x40, 0xE2, 0x01, 0x00, 0x66, 0x08, 0xAA, 0x55].as_slice())
1219        );
1220    }
1221}