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 differential;
8mod dnu;
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 => {
813                SbfBlock::RtcmDatum(RtcmDatumBlock::parse(&header, block_data)?)
814            }
815            block_ids::LBAND_BEAMS => {
816                SbfBlock::LBandBeams(LBandBeamsBlock::parse(&header, block_data)?)
817            }
818            block_ids::DYN_DNS_STATUS => {
819                SbfBlock::DynDnsStatus(DynDnsStatusBlock::parse(&header, block_data)?)
820            }
821            block_ids::DISK_STATUS => {
822                SbfBlock::DiskStatus(DiskStatusBlock::parse(&header, block_data)?)
823            }
824            block_ids::P2PP_STATUS => {
825                SbfBlock::P2ppStatus(P2ppStatusBlock::parse(&header, block_data)?)
826            }
827            block_ids::COSMOS_STATUS => {
828                SbfBlock::CosmosStatus(CosmosStatusBlock::parse(&header, block_data)?)
829            }
830            block_ids::RX_MESSAGE => {
831                SbfBlock::RxMessage(RxMessageBlock::parse(&header, block_data)?)
832            }
833            block_ids::ENCAPSULATED_OUTPUT => {
834                SbfBlock::EncapsulatedOutput(EncapsulatedOutputBlock::parse(&header, block_data)?)
835            }
836            block_ids::GIS_ACTION => {
837                SbfBlock::GisAction(GisActionBlock::parse(&header, block_data)?)
838            }
839            block_ids::GIS_STATUS => {
840                SbfBlock::GisStatus(GisStatusBlock::parse(&header, block_data)?)
841            }
842            block_ids::RECEIVER_TIME => {
843                SbfBlock::ReceiverTime(ReceiverTimeBlock::parse(&header, block_data)?)
844            }
845            block_ids::PPS_OFFSET => {
846                SbfBlock::PpsOffset(PpsOffsetBlock::parse(&header, block_data)?)
847            }
848            block_ids::EXT_EVENT => SbfBlock::ExtEvent(ExtEventBlock::parse(&header, block_data)?),
849            block_ids::EXT_EVENT_PVT_CARTESIAN => SbfBlock::ExtEventPvtCartesian(
850                ExtEventPvtCartesianBlock::parse(&header, block_data)?,
851            ),
852            block_ids::EXT_EVENT_PVT_GEODETIC => {
853                SbfBlock::ExtEventPvtGeodetic(ExtEventPvtGeodeticBlock::parse(&header, block_data)?)
854            }
855            block_ids::EXT_EVENT_BASE_VECT_GEOD => SbfBlock::ExtEventBaseVectGeod(
856                ExtEventBaseVectGeodBlock::parse(&header, block_data)?,
857            ),
858            block_ids::EXT_EVENT_ATT_EULER => {
859                SbfBlock::ExtEventAttEuler(ExtEventAttEulerBlock::parse(&header, block_data)?)
860            }
861            block_ids::END_OF_PVT => SbfBlock::EndOfPvt(EndOfPvtBlock::parse(&header, block_data)?),
862            block_ids::INT_PV_CART => {
863                SbfBlock::IntPvCart(IntPvCartBlock::parse(&header, block_data)?)
864            }
865            block_ids::INT_PV_GEOD => {
866                SbfBlock::IntPvGeod(IntPvGeodBlock::parse(&header, block_data)?)
867            }
868            block_ids::INT_ATT_EULER => {
869                SbfBlock::IntAttEuler(IntAttEulerBlock::parse(&header, block_data)?)
870            }
871            block_ids::INT_POS_COV_CART => {
872                SbfBlock::IntPosCovCart(IntPosCovCartBlock::parse(&header, block_data)?)
873            }
874            block_ids::INT_VEL_COV_CART => {
875                SbfBlock::IntVelCovCart(IntVelCovCartBlock::parse(&header, block_data)?)
876            }
877            block_ids::INT_POS_COV_GEOD => {
878                SbfBlock::IntPosCovGeod(IntPosCovGeodBlock::parse(&header, block_data)?)
879            }
880            block_ids::INT_VEL_COV_GEOD => {
881                SbfBlock::IntVelCovGeod(IntVelCovGeodBlock::parse(&header, block_data)?)
882            }
883            block_ids::INT_ATT_COV_EULER => {
884                SbfBlock::IntAttCovEuler(IntAttCovEulerBlock::parse(&header, block_data)?)
885            }
886            block_ids::IP_STATUS => SbfBlock::IpStatus(IpStatusBlock::parse(&header, block_data)?),
887            block_ids::IQ_CORR => SbfBlock::IqCorr(IqCorrBlock::parse(&header, block_data)?),
888            block_ids::EXT_SENSOR_MEAS => {
889                SbfBlock::ExtSensorMeas(ExtSensorMeasBlock::parse(&header, block_data)?)
890            }
891            block_ids::EXT_SENSOR_STATUS => {
892                SbfBlock::ExtSensorStatus(ExtSensorStatusBlock::parse(&header, block_data)?)
893            }
894            block_ids::EXT_SENSOR_SETUP => {
895                SbfBlock::ExtSensorSetup(ExtSensorSetupBlock::parse(&header, block_data)?)
896            }
897            id if is_known_opaque_id(id) => SbfBlock::KnownOpaque {
898                id,
899                rev: header.block_rev,
900                data: data[8..total_len].to_vec(),
901            },
902            _ => SbfBlock::Unknown {
903                id: header.block_id,
904                rev: header.block_rev,
905                data: data[8..total_len].to_vec(),
906            },
907        };
908
909        Ok((block, total_len))
910    }
911
912    /// Get the block ID
913    pub fn block_id(&self) -> u16 {
914        match self {
915            SbfBlock::MeasEpoch(_) => block_ids::MEAS_EPOCH,
916            SbfBlock::MeasExtra(_) => block_ids::MEAS_EXTRA,
917            SbfBlock::EndOfMeas(_) => block_ids::END_OF_MEAS,
918            SbfBlock::Meas3Ranges(_) => block_ids::MEAS3_RANGES,
919            SbfBlock::Meas3Cn0HiRes(_) => block_ids::MEAS3_CN0_HI_RES,
920            SbfBlock::Meas3Doppler(_) => block_ids::MEAS3_DOPPLER,
921            SbfBlock::Meas3Pp(_) => block_ids::MEAS3_PP,
922            SbfBlock::Meas3Mp(_) => block_ids::MEAS3_MP,
923            SbfBlock::PvtGeodetic(_) => block_ids::PVT_GEODETIC,
924            SbfBlock::PvtCartesian(_) => block_ids::PVT_CARTESIAN,
925            SbfBlock::Dop(_) => block_ids::DOP,
926            SbfBlock::PosCart(_) => block_ids::POS_CART,
927            SbfBlock::PvtSatCartesian(_) => block_ids::PVT_SAT_CARTESIAN,
928            SbfBlock::PvtResidualsV2(_) => block_ids::PVT_RESIDUALS_V2,
929            SbfBlock::RaimStatisticsV2(_) => block_ids::RAIM_STATISTICS_V2,
930            SbfBlock::BaseVectorCart(_) => block_ids::BASE_VECTOR_CART,
931            SbfBlock::BaseVectorGeod(_) => block_ids::BASE_VECTOR_GEOD,
932            SbfBlock::PosCovCartesian(_) => block_ids::POS_COV_CARTESIAN,
933            SbfBlock::PosCovGeodetic(_) => block_ids::POS_COV_GEODETIC,
934            SbfBlock::VelCovCartesian(_) => block_ids::VEL_COV_CARTESIAN,
935            SbfBlock::VelCovGeodetic(_) => block_ids::VEL_COV_GEODETIC,
936            SbfBlock::GeoCorrections(_) => block_ids::GEO_CORRECTIONS,
937            SbfBlock::BaseStation(_) => block_ids::BASE_STATION,
938            SbfBlock::DiffCorrIn(_) => block_ids::DIFF_CORR_IN,
939            SbfBlock::PvtSupport(_) => block_ids::PVT_SUPPORT,
940            SbfBlock::PvtSupportA(_) => block_ids::PVT_SUPPORT_A,
941            SbfBlock::PosLocal(_) => block_ids::POS_LOCAL,
942            SbfBlock::PosProjected(_) => block_ids::POS_PROJECTED,
943            SbfBlock::IntPvaaGeod(_) => block_ids::INT_PVA_AGEOD,
944            SbfBlock::AttEuler(_) => block_ids::ATT_EULER,
945            SbfBlock::AttCovEuler(_) => block_ids::ATT_COV_EULER,
946            SbfBlock::AuxAntPositions(_) => block_ids::AUX_ANT_POSITIONS,
947            SbfBlock::EndOfAtt(_) => block_ids::END_OF_ATT,
948            SbfBlock::GpsNav(_) => block_ids::GPS_NAV,
949            SbfBlock::GpsAlm(_) => block_ids::GPS_ALM,
950            SbfBlock::GpsIon(_) => block_ids::GPS_ION,
951            SbfBlock::GpsUtc(_) => block_ids::GPS_UTC,
952            SbfBlock::GpsCNav(_) => block_ids::GPS_CNAV,
953            SbfBlock::GalNav(_) => block_ids::GAL_NAV,
954            SbfBlock::GalAlm(_) => block_ids::GAL_ALM,
955            SbfBlock::GalIon(_) => block_ids::GAL_ION,
956            SbfBlock::GalUtc(_) => block_ids::GAL_UTC,
957            SbfBlock::GalGstGps(_) => block_ids::GAL_GST_GPS,
958            SbfBlock::GalAuthStatus(_) => block_ids::GAL_AUTH_STATUS,
959            SbfBlock::GalSarRlm(_) => block_ids::GAL_SAR_RLM,
960            SbfBlock::GloNav(_) => block_ids::GLO_NAV,
961            SbfBlock::GloAlm(_) => block_ids::GLO_ALM,
962            SbfBlock::GloTime(_) => block_ids::GLO_TIME,
963            SbfBlock::BdsIon(_) => block_ids::BDS_ION,
964            SbfBlock::BdsNav(_) => block_ids::BDS_NAV,
965            SbfBlock::BdsAlm(_) => block_ids::BDS_ALM,
966            SbfBlock::BdsUtc(_) => block_ids::BDS_UTC,
967            SbfBlock::BdsCNav1(_) => block_ids::BDS_CNAV1,
968            SbfBlock::BdsCNav2(_) => block_ids::BDS_CNAV2,
969            SbfBlock::BdsCNav3(_) => block_ids::BDS_CNAV3,
970            SbfBlock::QzsNav(_) => block_ids::QZS_NAV,
971            SbfBlock::QzsAlm(_) => block_ids::QZS_ALM,
972            SbfBlock::GeoIonoDelay(_) => block_ids::GEO_IONO_DELAY,
973            SbfBlock::GpsRawCa(_) => block_ids::GPS_RAW_CA,
974            SbfBlock::GpsRawL2C(_) => block_ids::GPS_RAW_L2C,
975            SbfBlock::GpsRawL5(_) => block_ids::GPS_RAW_L5,
976            SbfBlock::GalRawFnav(_) => block_ids::GAL_RAW_FNAV,
977            SbfBlock::GalRawInav(_) => block_ids::GAL_RAW_INAV,
978            SbfBlock::GalRawCnav(_) => block_ids::GAL_RAW_CNAV,
979            SbfBlock::GeoRawL1(_) => block_ids::GEO_RAW_L1,
980            SbfBlock::GeoRawL5(_) => block_ids::GEO_RAW_L5,
981            SbfBlock::GloRawCa(_) => block_ids::GLO_RAW_CA,
982            SbfBlock::CmpRaw(_) => block_ids::CMP_RAW,
983            SbfBlock::BdsRawB1c(_) => block_ids::BDS_RAW_B1C,
984            SbfBlock::BdsRawB2a(_) => block_ids::BDS_RAW_B2A,
985            SbfBlock::BdsRawB2b(_) => block_ids::BDS_RAW_B2B,
986            SbfBlock::IrnssRaw(_) => block_ids::NAVIC_RAW,
987            SbfBlock::QzsRawL1Ca(_) => block_ids::QZS_RAW_L1CA,
988            SbfBlock::QzsRawL2C(_) => block_ids::QZS_RAW_L2C,
989            SbfBlock::QzsRawL5(_) => block_ids::QZS_RAW_L5,
990            SbfBlock::GeoMt00(_) => block_ids::GEO_MT00,
991            SbfBlock::GeoPrnMask(_) => block_ids::GEO_PRN_MASK,
992            SbfBlock::GeoFastCorr(_) => block_ids::GEO_FAST_CORR,
993            SbfBlock::GeoFastCorrDegr(_) => block_ids::GEO_FAST_CORR_DEGR,
994            SbfBlock::GeoDegrFactors(_) => block_ids::GEO_DEGR_FACTORS,
995            SbfBlock::GeoServiceLevel(_) => block_ids::GEO_SERVICE_LEVEL,
996            SbfBlock::GeoNav(_) => block_ids::GEO_NAV,
997            SbfBlock::GeoIntegrity(_) => block_ids::GEO_INTEGRITY,
998            SbfBlock::GeoAlm(_) => block_ids::GEO_ALM,
999            SbfBlock::GeoNetworkTime(_) => block_ids::GEO_NETWORK_TIME,
1000            SbfBlock::GeoIgpMask(_) => block_ids::GEO_IGP_MASK,
1001            SbfBlock::GeoLongTermCorr(_) => block_ids::GEO_LONG_TERM_CORR,
1002            SbfBlock::GeoClockEphCovMatrix(_) => block_ids::GEO_CLOCK_EPH_COV_MATRIX,
1003            SbfBlock::ReceiverStatus(_) => block_ids::RECEIVER_STATUS,
1004            SbfBlock::TrackingStatus(_) => block_ids::TRACKING_STATUS,
1005            SbfBlock::ChannelStatus(_) => block_ids::CHANNEL_STATUS,
1006            SbfBlock::SatVisibility(_) => block_ids::SAT_VISIBILITY,
1007            SbfBlock::QualityInd(_) => block_ids::QUALITY_IND,
1008            SbfBlock::InputLink(_) => block_ids::INPUT_LINK,
1009            SbfBlock::OutputLink(_) => block_ids::OUTPUT_LINK,
1010            SbfBlock::LBandTrackerStatus(_) => block_ids::LBAND_TRACKER_STATUS,
1011            SbfBlock::Commands(_) => block_ids::COMMANDS,
1012            SbfBlock::Comment(_) => block_ids::COMMENT,
1013            SbfBlock::ReceiverSetup(_) => block_ids::RECEIVER_SETUP,
1014            SbfBlock::BBSamples(_) => block_ids::BB_SAMPLES,
1015            SbfBlock::ASCIIIn(_) => block_ids::ASCII_IN,
1016            SbfBlock::NtripClientStatus(_) => block_ids::NTRIP_CLIENT_STATUS,
1017            SbfBlock::NtripServerStatus(_) => block_ids::NTRIP_SERVER_STATUS,
1018            SbfBlock::RfStatus(_) => block_ids::RF_STATUS,
1019            SbfBlock::RtcmDatum(_) => block_ids::RTCM_DATUM,
1020            SbfBlock::LBandBeams(_) => block_ids::LBAND_BEAMS,
1021            SbfBlock::DynDnsStatus(_) => block_ids::DYN_DNS_STATUS,
1022            SbfBlock::DiskStatus(_) => block_ids::DISK_STATUS,
1023            SbfBlock::P2ppStatus(_) => block_ids::P2PP_STATUS,
1024            SbfBlock::CosmosStatus(_) => block_ids::COSMOS_STATUS,
1025            SbfBlock::RxMessage(_) => block_ids::RX_MESSAGE,
1026            SbfBlock::EncapsulatedOutput(_) => block_ids::ENCAPSULATED_OUTPUT,
1027            SbfBlock::GisAction(_) => block_ids::GIS_ACTION,
1028            SbfBlock::GisStatus(_) => block_ids::GIS_STATUS,
1029            SbfBlock::ReceiverTime(_) => block_ids::RECEIVER_TIME,
1030            SbfBlock::PpsOffset(_) => block_ids::PPS_OFFSET,
1031            SbfBlock::ExtEvent(_) => block_ids::EXT_EVENT,
1032            SbfBlock::ExtEventPvtCartesian(_) => block_ids::EXT_EVENT_PVT_CARTESIAN,
1033            SbfBlock::ExtEventPvtGeodetic(_) => block_ids::EXT_EVENT_PVT_GEODETIC,
1034            SbfBlock::ExtEventBaseVectGeod(_) => block_ids::EXT_EVENT_BASE_VECT_GEOD,
1035            SbfBlock::ExtEventAttEuler(_) => block_ids::EXT_EVENT_ATT_EULER,
1036            SbfBlock::EndOfPvt(_) => block_ids::END_OF_PVT,
1037            SbfBlock::IntPvCart(_) => block_ids::INT_PV_CART,
1038            SbfBlock::IntPvGeod(_) => block_ids::INT_PV_GEOD,
1039            SbfBlock::IntAttEuler(_) => block_ids::INT_ATT_EULER,
1040            SbfBlock::IntPosCovCart(_) => block_ids::INT_POS_COV_CART,
1041            SbfBlock::IntVelCovCart(_) => block_ids::INT_VEL_COV_CART,
1042            SbfBlock::IntPosCovGeod(_) => block_ids::INT_POS_COV_GEOD,
1043            SbfBlock::IntVelCovGeod(_) => block_ids::INT_VEL_COV_GEOD,
1044            SbfBlock::IntAttCovEuler(_) => block_ids::INT_ATT_COV_EULER,
1045            SbfBlock::IpStatus(_) => block_ids::IP_STATUS,
1046            SbfBlock::IqCorr(_) => block_ids::IQ_CORR,
1047            SbfBlock::ExtSensorMeas(_) => block_ids::EXT_SENSOR_MEAS,
1048            SbfBlock::ExtSensorStatus(_) => block_ids::EXT_SENSOR_STATUS,
1049            SbfBlock::ExtSensorSetup(_) => block_ids::EXT_SENSOR_SETUP,
1050            SbfBlock::KnownOpaque { id, .. } => *id,
1051            SbfBlock::Unknown { id, .. } => *id,
1052        }
1053    }
1054
1055    /// Get the block name
1056    pub fn name(&self) -> &'static str {
1057        block_name(self.block_id())
1058    }
1059
1060    /// Get raw payload bytes for unsupported blocks.
1061    ///
1062    /// Returns `Some(&[u8])` for `KnownOpaque` and `Unknown` variants, otherwise `None`.
1063    pub fn unsupported_payload(&self) -> Option<&[u8]> {
1064        match self {
1065            SbfBlock::KnownOpaque { data, .. } | SbfBlock::Unknown { data, .. } => Some(data),
1066            _ => None,
1067        }
1068    }
1069}
1070
1071#[cfg(test)]
1072mod tests {
1073    use super::*;
1074
1075    fn build_min_block(block_id: u16, block_rev: u8) -> Vec<u8> {
1076        let total_len = 16u16;
1077        let mut data = vec![0u8; total_len as usize];
1078        data[0] = 0x24;
1079        data[1] = 0x40;
1080
1081        let id_rev = block_id | ((block_rev as u16 & 0x07) << 13);
1082        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1083        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1084        data[8..12].copy_from_slice(&123_456u32.to_le_bytes());
1085        data[12..14].copy_from_slice(&2150u16.to_le_bytes());
1086        data[14] = 0xAA;
1087        data[15] = 0x55;
1088        data
1089    }
1090
1091    #[test]
1092    fn test_meas3_ranges_min_block_parse() {
1093        // Minimum valid Meas3Ranges: block_data length 18 (fixed header through Reserved, empty Data).
1094        let total_len = 20u16;
1095        let mut data = vec![0u8; total_len as usize];
1096        data[0] = 0x24;
1097        data[1] = 0x40;
1098        let id_rev = block_ids::MEAS3_RANGES | ((2u16 & 0x07) << 13);
1099        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1100        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1101        data[8..12].copy_from_slice(&123_456u32.to_le_bytes());
1102        data[12..14].copy_from_slice(&2150u16.to_le_bytes());
1103
1104        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1105        assert_eq!(consumed, usize::from(total_len));
1106        match block {
1107            SbfBlock::Meas3Ranges(m) => {
1108                assert_eq!(m.tow_ms(), 123_456);
1109                assert_eq!(m.wnc(), 2150);
1110                assert_eq!(m.data, Vec::<u8>::new());
1111            }
1112            other => panic!("expected Meas3Ranges, got {:?}", other),
1113        }
1114    }
1115
1116    #[test]
1117    fn test_pvt_support_parse() {
1118        let data = build_min_block(block_ids::PVT_SUPPORT, 1);
1119        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1120
1121        assert_eq!(consumed, 16);
1122        assert_eq!(block.block_id(), block_ids::PVT_SUPPORT);
1123        match block {
1124            SbfBlock::PvtSupport(pvt) => {
1125                assert_eq!(pvt.tow_ms(), 123_456);
1126                assert_eq!(pvt.wnc(), 2150);
1127                assert!((pvt.tow_seconds() - 123.456).abs() < 1e-6);
1128            }
1129            other => panic!("expected PvtSupport, got {:?}", other),
1130        }
1131    }
1132
1133    #[test]
1134    fn test_bds_raw_b2b_min_block_parse() {
1135        let total_len: u16 = 144; // 2 sync + 142 block bytes (B2b min)
1136        let mut data = vec![0u8; total_len as usize];
1137        data[0] = 0x24;
1138        data[1] = 0x40;
1139        let id_rev = block_ids::BDS_RAW_B2B;
1140        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1141        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1142        data[8..12].copy_from_slice(&1_000u32.to_le_bytes());
1143        data[12..14].copy_from_slice(&200u16.to_le_bytes());
1144        data[14] = 5;
1145        data[15] = 1;
1146
1147        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1148        assert_eq!(consumed, total_len as usize);
1149        match block {
1150            SbfBlock::BdsRawB2b(b) => {
1151                assert_eq!(b.tow_ms(), 1_000);
1152                assert_eq!(b.wnc(), 200);
1153                assert_eq!(b.svid, 5);
1154            }
1155            other => panic!("expected BdsRawB2b, got {:?}", other),
1156        }
1157    }
1158
1159    #[test]
1160    fn test_gal_auth_status_min_block_parse() {
1161        let total_len: u16 = 52;
1162        let mut data = vec![0u8; total_len as usize];
1163        data[0] = 0x24;
1164        data[1] = 0x40;
1165        let id_rev = block_ids::GAL_AUTH_STATUS;
1166        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
1167        data[6..8].copy_from_slice(&total_len.to_le_bytes());
1168        data[8..12].copy_from_slice(&2_000u32.to_le_bytes());
1169        data[12..14].copy_from_slice(&201u16.to_le_bytes());
1170        data[14..16].copy_from_slice(&0xABCDu16.to_le_bytes());
1171        data[16..20].copy_from_slice(&1.5f32.to_le_bytes());
1172        for i in 0..8usize {
1173            data[20 + i] = 0x10 + i as u8;
1174            data[28 + i] = 0x20 + i as u8;
1175            data[36 + i] = 0x30 + i as u8;
1176            data[44 + i] = 0x40 + i as u8;
1177        }
1178
1179        let (block, consumed) = SbfBlock::parse(&data).unwrap();
1180        assert_eq!(consumed, total_len as usize);
1181        match block {
1182            SbfBlock::GalAuthStatus(a) => {
1183                assert_eq!(a.tow_ms(), 2_000);
1184                assert_eq!(a.wnc(), 201);
1185                assert_eq!(a.osnma_status, 0xABCD);
1186                assert!((a.trusted_time_delta - 1.5).abs() < 1e-6);
1187            }
1188            other => panic!("expected GalAuthStatus, got {:?}", other),
1189        }
1190    }
1191
1192    #[test]
1193    fn test_unknown_id_stays_unknown() {
1194        let data = build_min_block(4999, 0);
1195        let (block, _) = SbfBlock::parse(&data).unwrap();
1196
1197        match block {
1198            SbfBlock::Unknown { id, rev, data } => {
1199                assert_eq!(id, 4999);
1200                assert_eq!(rev, 0);
1201                assert_eq!(data, vec![0x40, 0xE2, 0x01, 0x00, 0x66, 0x08, 0xAA, 0x55]);
1202            }
1203            other => panic!("expected Unknown, got {:?}", other),
1204        }
1205    }
1206
1207    #[test]
1208    fn test_unsupported_payload_helper() {
1209        let known_data = build_min_block(block_ids::FUGRO_DDS, 0);
1210        let (known, _) = SbfBlock::parse(&known_data).unwrap();
1211        assert_eq!(
1212            known.unsupported_payload(),
1213            Some([0x40, 0xE2, 0x01, 0x00, 0x66, 0x08, 0xAA, 0x55].as_slice())
1214        );
1215
1216        let unknown_data = build_min_block(4998, 1);
1217        let (unknown, _) = SbfBlock::parse(&unknown_data).unwrap();
1218        assert_eq!(
1219            unknown.unsupported_payload(),
1220            Some([0x40, 0xE2, 0x01, 0x00, 0x66, 0x08, 0xAA, 0x55].as_slice())
1221        );
1222    }
1223}