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