Skip to main content

sbf_tools/blocks/
position.rs

1//! Position blocks (PVTGeodetic, PVTCartesian, DOP, covariance)
2
3use crate::error::{SbfError, SbfResult};
4use crate::header::SbfHeader;
5use crate::types::{PvtError, PvtMode};
6
7use super::block_ids;
8use super::dnu::{u16_or_none, u8_or_none, F32_DNU, F64_DNU, I16_DNU, U16_DNU};
9use super::SbfBlockParse;
10
11// ============================================================================
12// Constants
13// ============================================================================
14
15fn num_satellites_or_zero(nr_sv: u8) -> u8 {
16    u8_or_none(nr_sv).unwrap_or(0)
17}
18
19fn dop_or_none(raw: u16) -> Option<f32> {
20    u16_or_none(raw)
21        .filter(|&raw| raw != 0)
22        .map(|raw| raw as f32 * 0.01)
23}
24
25// ============================================================================
26// PVTGeodetic Block
27// ============================================================================
28
29/// PVTGeodetic_v2 block (Block ID 4007)
30///
31/// Position, velocity, and time in geodetic coordinates.
32#[derive(Debug, Clone)]
33#[allow(dead_code)]
34pub struct PvtGeodeticBlock {
35    /// Time of week in milliseconds
36    tow_ms: u32,
37    /// GPS week number
38    wnc: u16,
39    /// PVT mode
40    mode: u8,
41    /// Error code
42    error: u8,
43    /// Latitude in radians
44    latitude_rad: f64,
45    /// Longitude in radians
46    longitude_rad: f64,
47    /// Ellipsoidal height in meters
48    height_m: f64,
49    /// Geoid undulation in meters
50    undulation_m: f32,
51    /// North velocity in m/s
52    vn_mps: f32,
53    /// East velocity in m/s
54    ve_mps: f32,
55    /// Up velocity in m/s
56    vu_mps: f32,
57    /// Course over ground in degrees
58    cog_deg: f32,
59    /// Receiver clock bias in ms
60    rx_clk_bias_ms: f64,
61    /// Receiver clock drift in ppm
62    rx_clk_drift_ppm: f32,
63    /// Time system
64    pub time_system: u8,
65    /// Datum
66    pub datum: u8,
67    /// Number of satellites used
68    nr_sv: u8,
69    /// WAAS correction info
70    pub wa_corr_info: u8,
71    /// Reference station ID
72    pub reference_id: u16,
73    /// Mean correction age (raw, multiply by 0.01 for seconds)
74    mean_corr_age_raw: u16,
75    /// Signal usage info
76    pub signal_info: u32,
77    /// Alert flag
78    pub alert_flag: u8,
79    /// Number of base stations
80    pub nr_bases: u8,
81    /// PPP info
82    pub ppp_info: u16,
83    /// Latency (raw)
84    latency_raw: u16,
85    /// Horizontal accuracy (raw, multiply by 0.01 for meters)
86    h_accuracy_raw: u16,
87    /// Vertical accuracy (raw, multiply by 0.01 for meters)
88    v_accuracy_raw: u16,
89}
90
91impl PvtGeodeticBlock {
92    // Time accessors
93    pub fn tow_seconds(&self) -> f64 {
94        self.tow_ms as f64 * 0.001
95    }
96    pub fn tow_ms(&self) -> u32 {
97        self.tow_ms
98    }
99    pub fn wnc(&self) -> u16 {
100        self.wnc
101    }
102
103    // Mode/error accessors
104    pub fn mode(&self) -> PvtMode {
105        PvtMode::from_mode_byte(self.mode)
106    }
107    pub fn mode_raw(&self) -> u8 {
108        self.mode
109    }
110    pub fn error(&self) -> PvtError {
111        PvtError::from_error_byte(self.error)
112    }
113    pub fn error_raw(&self) -> u8 {
114        self.error
115    }
116    pub fn has_fix(&self) -> bool {
117        self.mode().has_fix() && self.error().is_ok()
118    }
119
120    // Position accessors (scaled)
121    pub fn latitude_deg(&self) -> Option<f64> {
122        if self.latitude_rad == F64_DNU {
123            None
124        } else {
125            Some(self.latitude_rad.to_degrees())
126        }
127    }
128    pub fn longitude_deg(&self) -> Option<f64> {
129        if self.longitude_rad == F64_DNU {
130            None
131        } else {
132            Some(self.longitude_rad.to_degrees())
133        }
134    }
135    pub fn height_m(&self) -> Option<f64> {
136        if self.height_m == F64_DNU {
137            None
138        } else {
139            Some(self.height_m)
140        }
141    }
142    pub fn undulation_m(&self) -> Option<f32> {
143        if self.undulation_m == F32_DNU {
144            None
145        } else {
146            Some(self.undulation_m)
147        }
148    }
149
150    // Position accessors (raw)
151    pub fn latitude_rad(&self) -> f64 {
152        self.latitude_rad
153    }
154    pub fn longitude_rad(&self) -> f64 {
155        self.longitude_rad
156    }
157
158    // Velocity accessors
159    pub fn velocity_north_mps(&self) -> Option<f32> {
160        if self.vn_mps == F32_DNU {
161            None
162        } else {
163            Some(self.vn_mps)
164        }
165    }
166    pub fn velocity_east_mps(&self) -> Option<f32> {
167        if self.ve_mps == F32_DNU {
168            None
169        } else {
170            Some(self.ve_mps)
171        }
172    }
173    pub fn velocity_up_mps(&self) -> Option<f32> {
174        if self.vu_mps == F32_DNU {
175            None
176        } else {
177            Some(self.vu_mps)
178        }
179    }
180    pub fn course_over_ground_deg(&self) -> Option<f32> {
181        if self.cog_deg == F32_DNU {
182            None
183        } else {
184            Some(self.cog_deg)
185        }
186    }
187
188    // Clock accessors
189    pub fn clock_bias_ms(&self) -> Option<f64> {
190        if self.rx_clk_bias_ms == F64_DNU {
191            None
192        } else {
193            Some(self.rx_clk_bias_ms)
194        }
195    }
196    pub fn clock_drift_ppm(&self) -> Option<f32> {
197        if self.rx_clk_drift_ppm == F32_DNU {
198            None
199        } else {
200            Some(self.rx_clk_drift_ppm)
201        }
202    }
203
204    // Satellite count
205    /// Number of satellites used in the PVT computation.
206    ///
207    /// Returns `0` when the SBF `NrSV` field is not available (`255`). Use
208    /// [`Self::num_satellites_opt`] to distinguish unavailable from a real zero.
209    pub fn num_satellites(&self) -> u8 {
210        num_satellites_or_zero(self.nr_sv)
211    }
212    /// Number of satellites used in the PVT computation, or `None` when unavailable.
213    pub fn num_satellites_opt(&self) -> Option<u8> {
214        u8_or_none(self.nr_sv)
215    }
216    /// Raw `NrSV` field from the SBF block.
217    pub fn num_satellites_raw(&self) -> u8 {
218        self.nr_sv
219    }
220
221    // Accuracy (scaled)
222    pub fn h_accuracy_m(&self) -> Option<f32> {
223        if self.h_accuracy_raw == U16_DNU {
224            None
225        } else {
226            Some(self.h_accuracy_raw as f32 * 0.01)
227        }
228    }
229    pub fn v_accuracy_m(&self) -> Option<f32> {
230        if self.v_accuracy_raw == U16_DNU {
231            None
232        } else {
233            Some(self.v_accuracy_raw as f32 * 0.01)
234        }
235    }
236
237    // Accuracy (raw)
238    pub fn h_accuracy_raw(&self) -> u16 {
239        self.h_accuracy_raw
240    }
241    pub fn v_accuracy_raw(&self) -> u16 {
242        self.v_accuracy_raw
243    }
244
245    // Correction age
246    pub fn mean_corr_age_seconds(&self) -> Option<f32> {
247        if self.mean_corr_age_raw == U16_DNU {
248            None
249        } else {
250            Some(self.mean_corr_age_raw as f32 * 0.01)
251        }
252    }
253}
254
255impl SbfBlockParse for PvtGeodeticBlock {
256    const BLOCK_ID: u16 = block_ids::PVT_GEODETIC;
257
258    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
259        if data.len() < 83 {
260            return Err(SbfError::ParseError("PVTGeodetic too short".into()));
261        }
262
263        // Offsets from data start (after sync):
264        // 0-1: CRC, 2-3: ID, 4-5: Length
265        // 6-9: TOW, 10-11: WNc
266        // 12: Mode, 13: Error
267        // 14-21: Latitude (f64)
268        // 22-29: Longitude (f64)
269        // 30-37: Height (f64)
270        // 38-41: Undulation (f32)
271        // 42-45: Vn (f32)
272        // 46-49: Ve (f32)
273        // 50-53: Vu (f32)
274        // 54-57: COG (f32)
275        // 58-65: RxClkBias (f64)
276        // 66-69: RxClkDrift (f32)
277        // 70: TimeSystem
278        // 71: Datum
279        // 72: NrSV
280        // 73: WACorrInfo
281        // 74-75: ReferenceID
282        // 76-77: MeanCorrAge
283        // 78-81: SignalInfo
284        // 82: AlertFlag
285
286        let mode = data[12];
287        let error = data[13];
288
289        let latitude_rad = f64::from_le_bytes(data[14..22].try_into().unwrap());
290        let longitude_rad = f64::from_le_bytes(data[22..30].try_into().unwrap());
291        let height_m = f64::from_le_bytes(data[30..38].try_into().unwrap());
292        let undulation_m = f32::from_le_bytes(data[38..42].try_into().unwrap());
293
294        let vn_mps = f32::from_le_bytes(data[42..46].try_into().unwrap());
295        let ve_mps = f32::from_le_bytes(data[46..50].try_into().unwrap());
296        let vu_mps = f32::from_le_bytes(data[50..54].try_into().unwrap());
297        let cog_deg = f32::from_le_bytes(data[54..58].try_into().unwrap());
298
299        let rx_clk_bias_ms = f64::from_le_bytes(data[58..66].try_into().unwrap());
300        let rx_clk_drift_ppm = f32::from_le_bytes(data[66..70].try_into().unwrap());
301
302        let time_system = data[70];
303        let datum = data[71];
304        let nr_sv = data[72];
305        let wa_corr_info = data[73];
306        let reference_id = u16::from_le_bytes([data[74], data[75]]);
307        let mean_corr_age_raw = u16::from_le_bytes([data[76], data[77]]);
308        let signal_info = u32::from_le_bytes(data[78..82].try_into().unwrap());
309        let alert_flag = data[82];
310
311        // Rev 1+ fields
312        let (nr_bases, ppp_info, latency_raw, h_accuracy_raw, v_accuracy_raw) =
313            if header.block_rev >= 1 && data.len() >= 92 {
314                (
315                    data[83],
316                    u16::from_le_bytes([data[84], data[85]]),
317                    u16::from_le_bytes([data[86], data[87]]),
318                    u16::from_le_bytes([data[88], data[89]]),
319                    u16::from_le_bytes([data[90], data[91]]),
320                )
321            } else {
322                (0, 0, 0, U16_DNU, U16_DNU)
323            };
324
325        Ok(Self {
326            tow_ms: header.tow_ms,
327            wnc: header.wnc,
328            mode,
329            error,
330            latitude_rad,
331            longitude_rad,
332            height_m,
333            undulation_m,
334            vn_mps,
335            ve_mps,
336            vu_mps,
337            cog_deg,
338            rx_clk_bias_ms,
339            rx_clk_drift_ppm,
340            time_system,
341            datum,
342            nr_sv,
343            wa_corr_info,
344            reference_id,
345            mean_corr_age_raw,
346            signal_info,
347            alert_flag,
348            nr_bases,
349            ppp_info,
350            latency_raw,
351            h_accuracy_raw,
352            v_accuracy_raw,
353        })
354    }
355}
356
357// ============================================================================
358// PVTCartesian Block
359// ============================================================================
360
361/// PVTCartesian_v2 block (Block ID 4006)
362///
363/// Position, velocity, and time in ECEF Cartesian coordinates.
364#[derive(Debug, Clone)]
365#[allow(dead_code)]
366pub struct PvtCartesianBlock {
367    tow_ms: u32,
368    wnc: u16,
369    mode: u8,
370    error: u8,
371    x_m: f64,
372    y_m: f64,
373    z_m: f64,
374    undulation_m: f32,
375    vx_mps: f32,
376    vy_mps: f32,
377    vz_mps: f32,
378    cog_deg: f32,
379    rx_clk_bias_ms: f64,
380    rx_clk_drift_ppm: f32,
381    pub time_system: u8,
382    pub datum: u8,
383    nr_sv: u8,
384    pub wa_corr_info: u8,
385    pub reference_id: u16,
386    mean_corr_age_raw: u16,
387    pub signal_info: u32,
388    pub alert_flag: u8,
389    pub nr_bases: u8,
390}
391
392impl PvtCartesianBlock {
393    pub fn tow_seconds(&self) -> f64 {
394        self.tow_ms as f64 * 0.001
395    }
396    pub fn tow_ms(&self) -> u32 {
397        self.tow_ms
398    }
399    pub fn wnc(&self) -> u16 {
400        self.wnc
401    }
402
403    pub fn mode(&self) -> PvtMode {
404        PvtMode::from_mode_byte(self.mode)
405    }
406    pub fn error(&self) -> PvtError {
407        PvtError::from_error_byte(self.error)
408    }
409    pub fn has_fix(&self) -> bool {
410        self.mode().has_fix() && self.error().is_ok()
411    }
412
413    // ECEF position
414    pub fn x_m(&self) -> Option<f64> {
415        if self.x_m == F64_DNU {
416            None
417        } else {
418            Some(self.x_m)
419        }
420    }
421    pub fn y_m(&self) -> Option<f64> {
422        if self.y_m == F64_DNU {
423            None
424        } else {
425            Some(self.y_m)
426        }
427    }
428    pub fn z_m(&self) -> Option<f64> {
429        if self.z_m == F64_DNU {
430            None
431        } else {
432            Some(self.z_m)
433        }
434    }
435
436    // ECEF velocity
437    pub fn vx_mps(&self) -> Option<f32> {
438        if self.vx_mps == F32_DNU {
439            None
440        } else {
441            Some(self.vx_mps)
442        }
443    }
444    pub fn vy_mps(&self) -> Option<f32> {
445        if self.vy_mps == F32_DNU {
446            None
447        } else {
448            Some(self.vy_mps)
449        }
450    }
451    pub fn vz_mps(&self) -> Option<f32> {
452        if self.vz_mps == F32_DNU {
453            None
454        } else {
455            Some(self.vz_mps)
456        }
457    }
458
459    /// Number of satellites used in the PVT computation.
460    ///
461    /// Returns `0` when the SBF `NrSV` field is not available (`255`). Use
462    /// [`Self::num_satellites_opt`] to distinguish unavailable from a real zero.
463    pub fn num_satellites(&self) -> u8 {
464        num_satellites_or_zero(self.nr_sv)
465    }
466    /// Number of satellites used in the PVT computation, or `None` when unavailable.
467    pub fn num_satellites_opt(&self) -> Option<u8> {
468        u8_or_none(self.nr_sv)
469    }
470    /// Raw `NrSV` field from the SBF block.
471    pub fn num_satellites_raw(&self) -> u8 {
472        self.nr_sv
473    }
474}
475
476impl SbfBlockParse for PvtCartesianBlock {
477    const BLOCK_ID: u16 = block_ids::PVT_CARTESIAN;
478
479    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
480        if data.len() < 83 {
481            return Err(SbfError::ParseError("PVTCartesian too short".into()));
482        }
483
484        let mode = data[12];
485        let error = data[13];
486
487        let x_m = f64::from_le_bytes(data[14..22].try_into().unwrap());
488        let y_m = f64::from_le_bytes(data[22..30].try_into().unwrap());
489        let z_m = f64::from_le_bytes(data[30..38].try_into().unwrap());
490        let undulation_m = f32::from_le_bytes(data[38..42].try_into().unwrap());
491
492        let vx_mps = f32::from_le_bytes(data[42..46].try_into().unwrap());
493        let vy_mps = f32::from_le_bytes(data[46..50].try_into().unwrap());
494        let vz_mps = f32::from_le_bytes(data[50..54].try_into().unwrap());
495        let cog_deg = f32::from_le_bytes(data[54..58].try_into().unwrap());
496
497        let rx_clk_bias_ms = f64::from_le_bytes(data[58..66].try_into().unwrap());
498        let rx_clk_drift_ppm = f32::from_le_bytes(data[66..70].try_into().unwrap());
499
500        let time_system = data[70];
501        let datum = data[71];
502        let nr_sv = data[72];
503        let wa_corr_info = data[73];
504        let reference_id = u16::from_le_bytes([data[74], data[75]]);
505        let mean_corr_age_raw = u16::from_le_bytes([data[76], data[77]]);
506        let signal_info = u32::from_le_bytes(data[78..82].try_into().unwrap());
507        let alert_flag = data[82];
508
509        let nr_bases = if header.block_rev >= 1 && data.len() >= 84 {
510            data[83]
511        } else {
512            0
513        };
514
515        Ok(Self {
516            tow_ms: header.tow_ms,
517            wnc: header.wnc,
518            mode,
519            error,
520            x_m,
521            y_m,
522            z_m,
523            undulation_m,
524            vx_mps,
525            vy_mps,
526            vz_mps,
527            cog_deg,
528            rx_clk_bias_ms,
529            rx_clk_drift_ppm,
530            time_system,
531            datum,
532            nr_sv,
533            wa_corr_info,
534            reference_id,
535            mean_corr_age_raw,
536            signal_info,
537            alert_flag,
538            nr_bases,
539        })
540    }
541}
542
543// ============================================================================
544// DOP Block
545// ============================================================================
546
547/// DOP_v2 block (Block ID 4001)
548///
549/// Dilution of Precision values.
550#[derive(Debug, Clone)]
551pub struct DopBlock {
552    tow_ms: u32,
553    wnc: u16,
554    nr_sv: u8,
555    pdop_raw: u16,
556    tdop_raw: u16,
557    hdop_raw: u16,
558    vdop_raw: u16,
559    hpl_m: f32,
560    vpl_m: f32,
561}
562
563impl DopBlock {
564    pub fn tow_seconds(&self) -> f64 {
565        self.tow_ms as f64 * 0.001
566    }
567    pub fn tow_ms(&self) -> u32 {
568        self.tow_ms
569    }
570    pub fn wnc(&self) -> u16 {
571        self.wnc
572    }
573    /// Number of satellites used in the DOP computation.
574    ///
575    /// Returns `0` when DOP information is unavailable. Use
576    /// [`Self::num_satellites_opt`] to distinguish unavailable from a real zero.
577    pub fn num_satellites(&self) -> u8 {
578        if self.nr_sv == 0 {
579            0
580        } else {
581            num_satellites_or_zero(self.nr_sv)
582        }
583    }
584    /// Number of satellites used in the DOP computation, or `None` when unavailable.
585    pub fn num_satellites_opt(&self) -> Option<u8> {
586        if self.nr_sv == 0 {
587            None
588        } else {
589            u8_or_none(self.nr_sv)
590        }
591    }
592    /// Raw `NrSV` field from the SBF block.
593    pub fn num_satellites_raw(&self) -> u8 {
594        self.nr_sv
595    }
596
597    // Scaled DOP values (multiply by 0.01). Unavailable values return 0.0;
598    // use the *_opt accessors to distinguish unavailable from an actual zero.
599    pub fn pdop(&self) -> f32 {
600        self.pdop_opt().unwrap_or(0.0)
601    }
602    pub fn tdop(&self) -> f32 {
603        self.tdop_opt().unwrap_or(0.0)
604    }
605    pub fn hdop(&self) -> f32 {
606        self.hdop_opt().unwrap_or(0.0)
607    }
608    pub fn vdop(&self) -> f32 {
609        self.vdop_opt().unwrap_or(0.0)
610    }
611    /// PDOP, or `None` when unavailable.
612    pub fn pdop_opt(&self) -> Option<f32> {
613        dop_or_none(self.pdop_raw)
614    }
615    /// TDOP, or `None` when unavailable.
616    pub fn tdop_opt(&self) -> Option<f32> {
617        dop_or_none(self.tdop_raw)
618    }
619    /// HDOP, or `None` when unavailable.
620    pub fn hdop_opt(&self) -> Option<f32> {
621        dop_or_none(self.hdop_raw)
622    }
623    /// VDOP, or `None` when unavailable.
624    pub fn vdop_opt(&self) -> Option<f32> {
625        dop_or_none(self.vdop_raw)
626    }
627    /// GDOP computed as sqrt(PDOP^2 + TDOP^2)
628    pub fn gdop(&self) -> f32 {
629        self.gdop_opt().unwrap_or(0.0)
630    }
631    /// GDOP computed as sqrt(PDOP^2 + TDOP^2), or `None` when either input is unavailable.
632    pub fn gdop_opt(&self) -> Option<f32> {
633        let pdop = self.pdop_opt()?;
634        let tdop = self.tdop_opt()?;
635        Some((pdop * pdop + tdop * tdop).sqrt())
636    }
637
638    // Raw DOP values
639    pub fn pdop_raw(&self) -> u16 {
640        self.pdop_raw
641    }
642    pub fn tdop_raw(&self) -> u16 {
643        self.tdop_raw
644    }
645    pub fn hdop_raw(&self) -> u16 {
646        self.hdop_raw
647    }
648    pub fn vdop_raw(&self) -> u16 {
649        self.vdop_raw
650    }
651
652    // Protection levels
653    pub fn hpl_m(&self) -> Option<f32> {
654        if self.hpl_m == F32_DNU {
655            None
656        } else {
657            Some(self.hpl_m)
658        }
659    }
660    pub fn vpl_m(&self) -> Option<f32> {
661        if self.vpl_m == F32_DNU {
662            None
663        } else {
664            Some(self.vpl_m)
665        }
666    }
667}
668
669impl SbfBlockParse for DopBlock {
670    const BLOCK_ID: u16 = block_ids::DOP;
671
672    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
673        if data.len() < 22 {
674            return Err(SbfError::ParseError("DOP block too short".into()));
675        }
676
677        // Offsets:
678        // 12: NrSV
679        // 13: Reserved
680        // 14-15: PDOP
681        // 16-17: TDOP
682        // 18-19: HDOP
683        // 20-21: VDOP
684        // 22-25: HPL (f32)
685        // 26-29: VPL (f32)
686
687        let nr_sv = data[12];
688        let pdop_raw = u16::from_le_bytes([data[14], data[15]]);
689        let tdop_raw = u16::from_le_bytes([data[16], data[17]]);
690        let hdop_raw = u16::from_le_bytes([data[18], data[19]]);
691        let vdop_raw = u16::from_le_bytes([data[20], data[21]]);
692
693        let (hpl_m, vpl_m) = if data.len() >= 30 {
694            (
695                f32::from_le_bytes(data[22..26].try_into().unwrap()),
696                f32::from_le_bytes(data[26..30].try_into().unwrap()),
697            )
698        } else {
699            (F32_DNU, F32_DNU)
700        };
701
702        Ok(Self {
703            tow_ms: header.tow_ms,
704            wnc: header.wnc,
705            nr_sv,
706            pdop_raw,
707            tdop_raw,
708            hdop_raw,
709            vdop_raw,
710            hpl_m,
711            vpl_m,
712        })
713    }
714}
715
716// ============================================================================
717// PosCart Block
718// ============================================================================
719
720/// PosCart block (Block ID 4044)
721///
722/// Position solution in ECEF Cartesian coordinates with base vector and covariance.
723#[derive(Debug, Clone)]
724pub struct PosCartBlock {
725    tow_ms: u32,
726    wnc: u16,
727    mode: u8,
728    error: u8,
729    x_m: f64,
730    y_m: f64,
731    z_m: f64,
732    base_x_m: f64,
733    base_y_m: f64,
734    base_z_m: f64,
735    /// Position covariance (m^2)
736    pub cov_xx: f32,
737    pub cov_yy: f32,
738    pub cov_zz: f32,
739    pub cov_xy: f32,
740    pub cov_xz: f32,
741    pub cov_yz: f32,
742    pdop_raw: u16,
743    hdop_raw: u16,
744    vdop_raw: u16,
745    pub misc: u8,
746    pub alert_flag: u8,
747    pub datum: u8,
748    nr_sv: u8,
749    pub wa_corr_info: u8,
750    pub reference_id: u16,
751    mean_corr_age_raw: u16,
752    pub signal_info: u32,
753}
754
755impl PosCartBlock {
756    pub fn tow_seconds(&self) -> f64 {
757        self.tow_ms as f64 * 0.001
758    }
759    pub fn tow_ms(&self) -> u32 {
760        self.tow_ms
761    }
762    pub fn wnc(&self) -> u16 {
763        self.wnc
764    }
765
766    pub fn mode(&self) -> PvtMode {
767        PvtMode::from_mode_byte(self.mode)
768    }
769    pub fn error(&self) -> PvtError {
770        PvtError::from_error_byte(self.error)
771    }
772
773    pub fn x_m(&self) -> Option<f64> {
774        if self.x_m == F64_DNU {
775            None
776        } else {
777            Some(self.x_m)
778        }
779    }
780    pub fn y_m(&self) -> Option<f64> {
781        if self.y_m == F64_DNU {
782            None
783        } else {
784            Some(self.y_m)
785        }
786    }
787    pub fn z_m(&self) -> Option<f64> {
788        if self.z_m == F64_DNU {
789            None
790        } else {
791            Some(self.z_m)
792        }
793    }
794    pub fn base_to_rover_x_m(&self) -> Option<f64> {
795        if self.base_x_m == F64_DNU {
796            None
797        } else {
798            Some(self.base_x_m)
799        }
800    }
801    pub fn base_to_rover_y_m(&self) -> Option<f64> {
802        if self.base_y_m == F64_DNU {
803            None
804        } else {
805            Some(self.base_y_m)
806        }
807    }
808    pub fn base_to_rover_z_m(&self) -> Option<f64> {
809        if self.base_z_m == F64_DNU {
810            None
811        } else {
812            Some(self.base_z_m)
813        }
814    }
815
816    pub fn x_std_m(&self) -> Option<f32> {
817        if self.cov_xx == F32_DNU || self.cov_xx < 0.0 {
818            None
819        } else {
820            Some(self.cov_xx.sqrt())
821        }
822    }
823    pub fn y_std_m(&self) -> Option<f32> {
824        if self.cov_yy == F32_DNU || self.cov_yy < 0.0 {
825            None
826        } else {
827            Some(self.cov_yy.sqrt())
828        }
829    }
830    pub fn z_std_m(&self) -> Option<f32> {
831        if self.cov_zz == F32_DNU || self.cov_zz < 0.0 {
832            None
833        } else {
834            Some(self.cov_zz.sqrt())
835        }
836    }
837
838    pub fn pdop(&self) -> Option<f32> {
839        dop_or_none(self.pdop_raw)
840    }
841    pub fn hdop(&self) -> Option<f32> {
842        dop_or_none(self.hdop_raw)
843    }
844    pub fn vdop(&self) -> Option<f32> {
845        dop_or_none(self.vdop_raw)
846    }
847
848    pub fn pdop_raw(&self) -> u16 {
849        self.pdop_raw
850    }
851    pub fn hdop_raw(&self) -> u16 {
852        self.hdop_raw
853    }
854    pub fn vdop_raw(&self) -> u16 {
855        self.vdop_raw
856    }
857
858    /// Number of satellites used in the PVT computation.
859    ///
860    /// Returns `0` when the SBF `NrSV` field is not available (`255`). Use
861    /// [`Self::num_satellites_opt`] to distinguish unavailable from a real zero.
862    pub fn num_satellites(&self) -> u8 {
863        num_satellites_or_zero(self.nr_sv)
864    }
865    /// Number of satellites used in the PVT computation, or `None` when unavailable.
866    pub fn num_satellites_opt(&self) -> Option<u8> {
867        u8_or_none(self.nr_sv)
868    }
869    /// Raw `NrSV` field from the SBF block.
870    pub fn num_satellites_raw(&self) -> u8 {
871        self.nr_sv
872    }
873
874    pub fn mean_corr_age_seconds(&self) -> Option<f32> {
875        if self.mean_corr_age_raw == U16_DNU {
876            None
877        } else {
878            Some(self.mean_corr_age_raw as f32 * 0.01)
879        }
880    }
881}
882
883impl SbfBlockParse for PosCartBlock {
884    const BLOCK_ID: u16 = block_ids::POS_CART;
885
886    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
887        if data.len() < 106 {
888            return Err(SbfError::ParseError("PosCart too short".into()));
889        }
890
891        let mode = data[12];
892        let error = data[13];
893        // Byte 14 is reserved.
894
895        let x_m = f64::from_le_bytes(data[15..23].try_into().unwrap());
896        let y_m = f64::from_le_bytes(data[23..31].try_into().unwrap());
897        let z_m = f64::from_le_bytes(data[31..39].try_into().unwrap());
898
899        let base_x_m = f64::from_le_bytes(data[39..47].try_into().unwrap());
900        let base_y_m = f64::from_le_bytes(data[47..55].try_into().unwrap());
901        let base_z_m = f64::from_le_bytes(data[55..63].try_into().unwrap());
902
903        let cov_xx = f32::from_le_bytes(data[63..67].try_into().unwrap());
904        let cov_yy = f32::from_le_bytes(data[67..71].try_into().unwrap());
905        let cov_zz = f32::from_le_bytes(data[71..75].try_into().unwrap());
906        let cov_xy = f32::from_le_bytes(data[75..79].try_into().unwrap());
907        let cov_xz = f32::from_le_bytes(data[79..83].try_into().unwrap());
908        let cov_yz = f32::from_le_bytes(data[83..87].try_into().unwrap());
909
910        let pdop_raw = u16::from_le_bytes([data[87], data[88]]);
911        let hdop_raw = u16::from_le_bytes([data[89], data[90]]);
912        let vdop_raw = u16::from_le_bytes([data[91], data[92]]);
913
914        let misc = data[93];
915        let alert_flag = data[94];
916        let datum = data[95];
917        let nr_sv = data[96];
918        let wa_corr_info = data[97];
919        let reference_id = u16::from_le_bytes([data[98], data[99]]);
920        let mean_corr_age_raw = u16::from_le_bytes([data[100], data[101]]);
921        let signal_info = u32::from_le_bytes(data[102..106].try_into().unwrap());
922
923        Ok(Self {
924            tow_ms: header.tow_ms,
925            wnc: header.wnc,
926            mode,
927            error,
928            x_m,
929            y_m,
930            z_m,
931            base_x_m,
932            base_y_m,
933            base_z_m,
934            cov_xx,
935            cov_yy,
936            cov_zz,
937            cov_xy,
938            cov_xz,
939            cov_yz,
940            pdop_raw,
941            hdop_raw,
942            vdop_raw,
943            misc,
944            alert_flag,
945            datum,
946            nr_sv,
947            wa_corr_info,
948            reference_id,
949            mean_corr_age_raw,
950            signal_info,
951        })
952    }
953}
954
955// ============================================================================
956// PVTSatCartesian Block
957// ============================================================================
958
959/// Per-satellite ECEF position and velocity data.
960#[derive(Debug, Clone)]
961pub struct PvtSatCartesianSatPos {
962    pub svid: u8,
963    pub freq_nr: u8,
964    pub iode: u16,
965    x_m: f64,
966    y_m: f64,
967    z_m: f64,
968    vx_mps: f32,
969    vy_mps: f32,
970    vz_mps: f32,
971}
972
973impl PvtSatCartesianSatPos {
974    pub fn x_m(&self) -> Option<f64> {
975        if self.x_m == F64_DNU {
976            None
977        } else {
978            Some(self.x_m)
979        }
980    }
981
982    pub fn y_m(&self) -> Option<f64> {
983        if self.y_m == F64_DNU {
984            None
985        } else {
986            Some(self.y_m)
987        }
988    }
989
990    pub fn z_m(&self) -> Option<f64> {
991        if self.z_m == F64_DNU {
992            None
993        } else {
994            Some(self.z_m)
995        }
996    }
997
998    pub fn vx_mps(&self) -> Option<f32> {
999        if self.vx_mps == F32_DNU {
1000            None
1001        } else {
1002            Some(self.vx_mps)
1003        }
1004    }
1005
1006    pub fn vy_mps(&self) -> Option<f32> {
1007        if self.vy_mps == F32_DNU {
1008            None
1009        } else {
1010            Some(self.vy_mps)
1011        }
1012    }
1013
1014    pub fn vz_mps(&self) -> Option<f32> {
1015        if self.vz_mps == F32_DNU {
1016            None
1017        } else {
1018            Some(self.vz_mps)
1019        }
1020    }
1021}
1022
1023/// PVTSatCartesian block (Block ID 4008).
1024#[derive(Debug, Clone)]
1025pub struct PvtSatCartesianBlock {
1026    tow_ms: u32,
1027    wnc: u16,
1028    pub satellites: Vec<PvtSatCartesianSatPos>,
1029}
1030
1031impl PvtSatCartesianBlock {
1032    pub fn tow_seconds(&self) -> f64 {
1033        self.tow_ms as f64 * 0.001
1034    }
1035
1036    pub fn tow_ms(&self) -> u32 {
1037        self.tow_ms
1038    }
1039
1040    pub fn wnc(&self) -> u16 {
1041        self.wnc
1042    }
1043
1044    pub fn num_satellites(&self) -> usize {
1045        self.satellites.len()
1046    }
1047}
1048
1049impl SbfBlockParse for PvtSatCartesianBlock {
1050    const BLOCK_ID: u16 = block_ids::PVT_SAT_CARTESIAN;
1051
1052    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1053        if data.len() < 14 {
1054            return Err(SbfError::ParseError("PVTSatCartesian too short".into()));
1055        }
1056
1057        let n = data[12] as usize;
1058        let sb_length = data[13] as usize;
1059        if sb_length < 40 {
1060            return Err(SbfError::ParseError(
1061                "PVTSatCartesian SBLength too small".into(),
1062            ));
1063        }
1064
1065        let mut satellites = Vec::new();
1066        let mut offset = 14;
1067
1068        for _ in 0..n {
1069            if offset + sb_length > data.len() {
1070                break;
1071            }
1072
1073            let svid = data[offset];
1074            let freq_nr = data[offset + 1];
1075            let iode = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
1076            let x_m = f64::from_le_bytes(data[offset + 4..offset + 12].try_into().unwrap());
1077            let y_m = f64::from_le_bytes(data[offset + 12..offset + 20].try_into().unwrap());
1078            let z_m = f64::from_le_bytes(data[offset + 20..offset + 28].try_into().unwrap());
1079            let vx_mps = f32::from_le_bytes(data[offset + 28..offset + 32].try_into().unwrap());
1080            let vy_mps = f32::from_le_bytes(data[offset + 32..offset + 36].try_into().unwrap());
1081            let vz_mps = f32::from_le_bytes(data[offset + 36..offset + 40].try_into().unwrap());
1082
1083            satellites.push(PvtSatCartesianSatPos {
1084                svid,
1085                freq_nr,
1086                iode,
1087                x_m,
1088                y_m,
1089                z_m,
1090                vx_mps,
1091                vy_mps,
1092                vz_mps,
1093            });
1094
1095            offset += sb_length;
1096        }
1097
1098        Ok(Self {
1099            tow_ms: header.tow_ms,
1100            wnc: header.wnc,
1101            satellites,
1102        })
1103    }
1104}
1105
1106// ============================================================================
1107// PVTResiduals_v2 Block
1108// ============================================================================
1109
1110/// Residual entry for a single measurement component.
1111#[derive(Debug, Clone)]
1112pub struct PvtResidualsV2ResidualInfo {
1113    e_i_m: f32,
1114    w_i_raw: u16,
1115    mdb_raw: u16,
1116}
1117
1118impl PvtResidualsV2ResidualInfo {
1119    pub fn residual_m(&self) -> Option<f32> {
1120        if self.e_i_m == F32_DNU {
1121            None
1122        } else {
1123            Some(self.e_i_m)
1124        }
1125    }
1126
1127    pub fn weight(&self) -> Option<u16> {
1128        if self.w_i_raw == U16_DNU {
1129            None
1130        } else {
1131            Some(self.w_i_raw)
1132        }
1133    }
1134
1135    pub fn mdb(&self) -> Option<u16> {
1136        if self.mdb_raw == U16_DNU {
1137            None
1138        } else {
1139            Some(self.mdb_raw)
1140        }
1141    }
1142}
1143
1144/// Per-signal residual metadata and nested residual entries.
1145#[derive(Debug, Clone)]
1146pub struct PvtResidualsV2SatSignalInfo {
1147    pub svid: u8,
1148    pub freq_nr: u8,
1149    pub signal_type: u8,
1150    pub ref_svid: u8,
1151    pub ref_freq_nr: u8,
1152    pub meas_info: u8,
1153    pub iode: u16,
1154    corr_age_raw: u16,
1155    pub reference_id: u16,
1156    pub residuals: Vec<PvtResidualsV2ResidualInfo>,
1157}
1158
1159impl PvtResidualsV2SatSignalInfo {
1160    pub fn corr_age_seconds(&self) -> Option<f32> {
1161        if self.corr_age_raw == U16_DNU {
1162            None
1163        } else {
1164            Some(self.corr_age_raw as f32 * 0.01)
1165        }
1166    }
1167
1168    pub fn expected_residual_count(&self) -> usize {
1169        residual_count_from_meas_info(self.meas_info)
1170    }
1171}
1172
1173/// PVTResiduals_v2 block (Block ID 4009).
1174#[derive(Debug, Clone)]
1175pub struct PvtResidualsV2Block {
1176    tow_ms: u32,
1177    wnc: u16,
1178    pub sat_signal_info: Vec<PvtResidualsV2SatSignalInfo>,
1179}
1180
1181impl PvtResidualsV2Block {
1182    pub fn tow_seconds(&self) -> f64 {
1183        self.tow_ms as f64 * 0.001
1184    }
1185
1186    pub fn tow_ms(&self) -> u32 {
1187        self.tow_ms
1188    }
1189
1190    pub fn wnc(&self) -> u16 {
1191        self.wnc
1192    }
1193
1194    pub fn num_sat_signals(&self) -> usize {
1195        self.sat_signal_info.len()
1196    }
1197}
1198
1199fn residual_count_from_meas_info(meas_info: u8) -> usize {
1200    ((meas_info & (1 << 2) != 0) as usize)
1201        + ((meas_info & (1 << 3) != 0) as usize)
1202        + ((meas_info & (1 << 4) != 0) as usize)
1203}
1204
1205impl SbfBlockParse for PvtResidualsV2Block {
1206    const BLOCK_ID: u16 = block_ids::PVT_RESIDUALS_V2;
1207
1208    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1209        if data.len() < 15 {
1210            return Err(SbfError::ParseError("PVTResiduals_v2 too short".into()));
1211        }
1212
1213        let n = data[12] as usize;
1214        let sb1_length = data[13] as usize;
1215        let sb2_length = data[14] as usize;
1216
1217        if sb1_length < 12 {
1218            return Err(SbfError::ParseError(
1219                "PVTResiduals_v2 SB1Length too small".into(),
1220            ));
1221        }
1222        if sb2_length < 8 {
1223            return Err(SbfError::ParseError(
1224                "PVTResiduals_v2 SB2Length too small".into(),
1225            ));
1226        }
1227
1228        let mut sat_signal_info = Vec::new();
1229        let mut offset = 15;
1230
1231        for _ in 0..n {
1232            if offset + sb1_length > data.len() {
1233                break;
1234            }
1235
1236            let svid = data[offset];
1237            let freq_nr = data[offset + 1];
1238            let signal_type = data[offset + 2];
1239            let ref_svid = data[offset + 3];
1240            let ref_freq_nr = data[offset + 4];
1241            let meas_info = data[offset + 5];
1242            let iode = u16::from_le_bytes([data[offset + 6], data[offset + 7]]);
1243            let corr_age_raw = u16::from_le_bytes([data[offset + 8], data[offset + 9]]);
1244            let reference_id = u16::from_le_bytes([data[offset + 10], data[offset + 11]]);
1245            offset += sb1_length;
1246
1247            let residual_count = residual_count_from_meas_info(meas_info);
1248            let mut residuals = Vec::new();
1249            for _ in 0..residual_count {
1250                if offset + sb2_length > data.len() {
1251                    break;
1252                }
1253
1254                let e_i_m = f32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
1255                let w_i_raw = u16::from_le_bytes([data[offset + 4], data[offset + 5]]);
1256                let mdb_raw = u16::from_le_bytes([data[offset + 6], data[offset + 7]]);
1257                residuals.push(PvtResidualsV2ResidualInfo {
1258                    e_i_m,
1259                    w_i_raw,
1260                    mdb_raw,
1261                });
1262
1263                offset += sb2_length;
1264            }
1265
1266            sat_signal_info.push(PvtResidualsV2SatSignalInfo {
1267                svid,
1268                freq_nr,
1269                signal_type,
1270                ref_svid,
1271                ref_freq_nr,
1272                meas_info,
1273                iode,
1274                corr_age_raw,
1275                reference_id,
1276                residuals,
1277            });
1278        }
1279
1280        Ok(Self {
1281            tow_ms: header.tow_ms,
1282            wnc: header.wnc,
1283            sat_signal_info,
1284        })
1285    }
1286}
1287
1288// ============================================================================
1289// RAIMStatistics_v2 Block
1290// ============================================================================
1291
1292/// RAIM integrity statistics.
1293#[derive(Debug, Clone)]
1294pub struct RaimStatisticsV2Block {
1295    tow_ms: u32,
1296    wnc: u16,
1297    pub integrity_flag: u8,
1298    herl_position_m: f32,
1299    verl_position_m: f32,
1300    herl_velocity_mps: f32,
1301    verl_velocity_mps: f32,
1302    overall_model: u16,
1303}
1304
1305impl RaimStatisticsV2Block {
1306    pub fn tow_seconds(&self) -> f64 {
1307        self.tow_ms as f64 * 0.001
1308    }
1309
1310    pub fn tow_ms(&self) -> u32 {
1311        self.tow_ms
1312    }
1313
1314    pub fn wnc(&self) -> u16 {
1315        self.wnc
1316    }
1317
1318    pub fn herl_position_m(&self) -> Option<f32> {
1319        if self.herl_position_m == F32_DNU {
1320            None
1321        } else {
1322            Some(self.herl_position_m)
1323        }
1324    }
1325
1326    pub fn verl_position_m(&self) -> Option<f32> {
1327        if self.verl_position_m == F32_DNU {
1328            None
1329        } else {
1330            Some(self.verl_position_m)
1331        }
1332    }
1333
1334    pub fn herl_velocity_mps(&self) -> Option<f32> {
1335        if self.herl_velocity_mps == F32_DNU {
1336            None
1337        } else {
1338            Some(self.herl_velocity_mps)
1339        }
1340    }
1341
1342    pub fn verl_velocity_mps(&self) -> Option<f32> {
1343        if self.verl_velocity_mps == F32_DNU {
1344            None
1345        } else {
1346            Some(self.verl_velocity_mps)
1347        }
1348    }
1349
1350    pub fn overall_model(&self) -> Option<u16> {
1351        if self.overall_model == U16_DNU {
1352            None
1353        } else {
1354            Some(self.overall_model)
1355        }
1356    }
1357}
1358
1359impl SbfBlockParse for RaimStatisticsV2Block {
1360    const BLOCK_ID: u16 = block_ids::RAIM_STATISTICS_V2;
1361
1362    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1363        if data.len() < 34 {
1364            return Err(SbfError::ParseError("RAIMStatistics_v2 too short".into()));
1365        }
1366
1367        let integrity_flag = data[12];
1368        let herl_position_m = f32::from_le_bytes(data[14..18].try_into().unwrap());
1369        let verl_position_m = f32::from_le_bytes(data[18..22].try_into().unwrap());
1370        let herl_velocity_mps = f32::from_le_bytes(data[22..26].try_into().unwrap());
1371        let verl_velocity_mps = f32::from_le_bytes(data[26..30].try_into().unwrap());
1372        let overall_model = u16::from_le_bytes([data[30], data[31]]);
1373
1374        Ok(Self {
1375            tow_ms: header.tow_ms,
1376            wnc: header.wnc,
1377            integrity_flag,
1378            herl_position_m,
1379            verl_position_m,
1380            herl_velocity_mps,
1381            verl_velocity_mps,
1382            overall_model,
1383        })
1384    }
1385}
1386
1387// ============================================================================
1388// BaseVectorCart Block
1389// ============================================================================
1390
1391/// Base vector info in ECEF Cartesian coordinates
1392#[derive(Debug, Clone)]
1393pub struct BaseVectorCartInfo {
1394    pub nr_sv: u8,
1395    error: u8,
1396    mode: u8,
1397    pub misc: u8,
1398    dx_m: f64,
1399    dy_m: f64,
1400    dz_m: f64,
1401    dvx_mps: f32,
1402    dvy_mps: f32,
1403    dvz_mps: f32,
1404    azimuth_raw: u16,
1405    elevation_raw: i16,
1406    pub reference_id: u16,
1407    corr_age_raw: u16,
1408    pub signal_info: u32,
1409}
1410
1411impl BaseVectorCartInfo {
1412    pub fn mode(&self) -> PvtMode {
1413        PvtMode::from_mode_byte(self.mode)
1414    }
1415    pub fn error(&self) -> PvtError {
1416        PvtError::from_error_byte(self.error)
1417    }
1418
1419    pub fn dx_m(&self) -> Option<f64> {
1420        if self.dx_m == F64_DNU {
1421            None
1422        } else {
1423            Some(self.dx_m)
1424        }
1425    }
1426    pub fn dy_m(&self) -> Option<f64> {
1427        if self.dy_m == F64_DNU {
1428            None
1429        } else {
1430            Some(self.dy_m)
1431        }
1432    }
1433    pub fn dz_m(&self) -> Option<f64> {
1434        if self.dz_m == F64_DNU {
1435            None
1436        } else {
1437            Some(self.dz_m)
1438        }
1439    }
1440    pub fn dvx_mps(&self) -> Option<f32> {
1441        if self.dvx_mps == F32_DNU {
1442            None
1443        } else {
1444            Some(self.dvx_mps)
1445        }
1446    }
1447    pub fn dvy_mps(&self) -> Option<f32> {
1448        if self.dvy_mps == F32_DNU {
1449            None
1450        } else {
1451            Some(self.dvy_mps)
1452        }
1453    }
1454    pub fn dvz_mps(&self) -> Option<f32> {
1455        if self.dvz_mps == F32_DNU {
1456            None
1457        } else {
1458            Some(self.dvz_mps)
1459        }
1460    }
1461
1462    pub fn azimuth_deg(&self) -> Option<f64> {
1463        if self.azimuth_raw == U16_DNU {
1464            None
1465        } else {
1466            Some(self.azimuth_raw as f64 * 0.01)
1467        }
1468    }
1469    pub fn elevation_deg(&self) -> Option<f64> {
1470        if self.elevation_raw == I16_DNU {
1471            None
1472        } else {
1473            Some(self.elevation_raw as f64 * 0.01)
1474        }
1475    }
1476
1477    pub fn corr_age_seconds(&self) -> Option<f32> {
1478        if self.corr_age_raw == U16_DNU {
1479            None
1480        } else {
1481            Some(self.corr_age_raw as f32 * 0.01)
1482        }
1483    }
1484}
1485
1486/// BaseVectorCart block (Block ID 4043)
1487#[derive(Debug, Clone)]
1488pub struct BaseVectorCartBlock {
1489    tow_ms: u32,
1490    wnc: u16,
1491    pub vectors: Vec<BaseVectorCartInfo>,
1492}
1493
1494impl BaseVectorCartBlock {
1495    pub fn tow_seconds(&self) -> f64 {
1496        self.tow_ms as f64 * 0.001
1497    }
1498    pub fn tow_ms(&self) -> u32 {
1499        self.tow_ms
1500    }
1501    pub fn wnc(&self) -> u16 {
1502        self.wnc
1503    }
1504
1505    pub fn num_vectors(&self) -> usize {
1506        self.vectors.len()
1507    }
1508}
1509
1510impl SbfBlockParse for BaseVectorCartBlock {
1511    const BLOCK_ID: u16 = block_ids::BASE_VECTOR_CART;
1512
1513    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1514        if data.len() < 14 {
1515            return Err(SbfError::ParseError("BaseVectorCart too short".into()));
1516        }
1517
1518        let n = data[12] as usize;
1519        let sb_length = data[13] as usize;
1520
1521        if sb_length < 52 {
1522            return Err(SbfError::ParseError(
1523                "BaseVectorCart SBLength too small".into(),
1524            ));
1525        }
1526
1527        let mut vectors = Vec::new();
1528        let mut offset = 14;
1529
1530        for _ in 0..n {
1531            if offset + sb_length > data.len() {
1532                break;
1533            }
1534
1535            let nr_sv = data[offset];
1536            let error = data[offset + 1];
1537            let mode = data[offset + 2];
1538            let misc = data[offset + 3];
1539
1540            let dx_m = f64::from_le_bytes(data[offset + 4..offset + 12].try_into().unwrap());
1541            let dy_m = f64::from_le_bytes(data[offset + 12..offset + 20].try_into().unwrap());
1542            let dz_m = f64::from_le_bytes(data[offset + 20..offset + 28].try_into().unwrap());
1543
1544            let dvx_mps = f32::from_le_bytes(data[offset + 28..offset + 32].try_into().unwrap());
1545            let dvy_mps = f32::from_le_bytes(data[offset + 32..offset + 36].try_into().unwrap());
1546            let dvz_mps = f32::from_le_bytes(data[offset + 36..offset + 40].try_into().unwrap());
1547
1548            let azimuth_raw = u16::from_le_bytes([data[offset + 40], data[offset + 41]]);
1549            let elevation_raw = i16::from_le_bytes([data[offset + 42], data[offset + 43]]);
1550            let reference_id = u16::from_le_bytes([data[offset + 44], data[offset + 45]]);
1551            let corr_age_raw = u16::from_le_bytes([data[offset + 46], data[offset + 47]]);
1552            let signal_info =
1553                u32::from_le_bytes(data[offset + 48..offset + 52].try_into().unwrap());
1554
1555            vectors.push(BaseVectorCartInfo {
1556                nr_sv,
1557                error,
1558                mode,
1559                misc,
1560                dx_m,
1561                dy_m,
1562                dz_m,
1563                dvx_mps,
1564                dvy_mps,
1565                dvz_mps,
1566                azimuth_raw,
1567                elevation_raw,
1568                reference_id,
1569                corr_age_raw,
1570                signal_info,
1571            });
1572
1573            offset += sb_length;
1574        }
1575
1576        Ok(Self {
1577            tow_ms: header.tow_ms,
1578            wnc: header.wnc,
1579            vectors,
1580        })
1581    }
1582}
1583
1584// ============================================================================
1585// BaseVectorGeod Block
1586// ============================================================================
1587
1588/// Base vector info in local geodetic coordinates
1589#[derive(Debug, Clone)]
1590pub struct BaseVectorGeodInfo {
1591    pub nr_sv: u8,
1592    error: u8,
1593    mode: u8,
1594    pub misc: u8,
1595    de_m: f64,
1596    dn_m: f64,
1597    du_m: f64,
1598    dve_mps: f32,
1599    dvn_mps: f32,
1600    dvu_mps: f32,
1601    azimuth_raw: u16,
1602    elevation_raw: i16,
1603    pub reference_id: u16,
1604    corr_age_raw: u16,
1605    pub signal_info: u32,
1606}
1607
1608impl BaseVectorGeodInfo {
1609    pub fn mode(&self) -> PvtMode {
1610        PvtMode::from_mode_byte(self.mode)
1611    }
1612    pub fn error(&self) -> PvtError {
1613        PvtError::from_error_byte(self.error)
1614    }
1615
1616    pub fn de_m(&self) -> Option<f64> {
1617        if self.de_m == F64_DNU {
1618            None
1619        } else {
1620            Some(self.de_m)
1621        }
1622    }
1623    pub fn dn_m(&self) -> Option<f64> {
1624        if self.dn_m == F64_DNU {
1625            None
1626        } else {
1627            Some(self.dn_m)
1628        }
1629    }
1630    pub fn du_m(&self) -> Option<f64> {
1631        if self.du_m == F64_DNU {
1632            None
1633        } else {
1634            Some(self.du_m)
1635        }
1636    }
1637    pub fn dve_mps(&self) -> Option<f32> {
1638        if self.dve_mps == F32_DNU {
1639            None
1640        } else {
1641            Some(self.dve_mps)
1642        }
1643    }
1644    pub fn dvn_mps(&self) -> Option<f32> {
1645        if self.dvn_mps == F32_DNU {
1646            None
1647        } else {
1648            Some(self.dvn_mps)
1649        }
1650    }
1651    pub fn dvu_mps(&self) -> Option<f32> {
1652        if self.dvu_mps == F32_DNU {
1653            None
1654        } else {
1655            Some(self.dvu_mps)
1656        }
1657    }
1658
1659    pub fn azimuth_deg(&self) -> Option<f64> {
1660        if self.azimuth_raw == U16_DNU {
1661            None
1662        } else {
1663            Some(self.azimuth_raw as f64 * 0.01)
1664        }
1665    }
1666    pub fn elevation_deg(&self) -> Option<f64> {
1667        if self.elevation_raw == I16_DNU {
1668            None
1669        } else {
1670            Some(self.elevation_raw as f64 * 0.01)
1671        }
1672    }
1673
1674    pub fn corr_age_seconds(&self) -> Option<f32> {
1675        if self.corr_age_raw == U16_DNU {
1676            None
1677        } else {
1678            Some(self.corr_age_raw as f32 * 0.01)
1679        }
1680    }
1681}
1682
1683/// BaseVectorGeod block (Block ID 4028)
1684#[derive(Debug, Clone)]
1685pub struct BaseVectorGeodBlock {
1686    tow_ms: u32,
1687    wnc: u16,
1688    pub vectors: Vec<BaseVectorGeodInfo>,
1689}
1690
1691impl BaseVectorGeodBlock {
1692    pub fn tow_seconds(&self) -> f64 {
1693        self.tow_ms as f64 * 0.001
1694    }
1695    pub fn tow_ms(&self) -> u32 {
1696        self.tow_ms
1697    }
1698    pub fn wnc(&self) -> u16 {
1699        self.wnc
1700    }
1701
1702    pub fn num_vectors(&self) -> usize {
1703        self.vectors.len()
1704    }
1705}
1706
1707impl SbfBlockParse for BaseVectorGeodBlock {
1708    const BLOCK_ID: u16 = block_ids::BASE_VECTOR_GEOD;
1709
1710    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1711        if data.len() < 14 {
1712            return Err(SbfError::ParseError("BaseVectorGeod too short".into()));
1713        }
1714
1715        let n = data[12] as usize;
1716        let sb_length = data[13] as usize;
1717
1718        if sb_length < 52 {
1719            return Err(SbfError::ParseError(
1720                "BaseVectorGeod SBLength too small".into(),
1721            ));
1722        }
1723
1724        let mut vectors = Vec::new();
1725        let mut offset = 14;
1726
1727        for _ in 0..n {
1728            if offset + sb_length > data.len() {
1729                break;
1730            }
1731
1732            let nr_sv = data[offset];
1733            let error = data[offset + 1];
1734            let mode = data[offset + 2];
1735            let misc = data[offset + 3];
1736
1737            let de_m = f64::from_le_bytes(data[offset + 4..offset + 12].try_into().unwrap());
1738            let dn_m = f64::from_le_bytes(data[offset + 12..offset + 20].try_into().unwrap());
1739            let du_m = f64::from_le_bytes(data[offset + 20..offset + 28].try_into().unwrap());
1740
1741            let dve_mps = f32::from_le_bytes(data[offset + 28..offset + 32].try_into().unwrap());
1742            let dvn_mps = f32::from_le_bytes(data[offset + 32..offset + 36].try_into().unwrap());
1743            let dvu_mps = f32::from_le_bytes(data[offset + 36..offset + 40].try_into().unwrap());
1744
1745            let azimuth_raw = u16::from_le_bytes([data[offset + 40], data[offset + 41]]);
1746            let elevation_raw = i16::from_le_bytes([data[offset + 42], data[offset + 43]]);
1747            let reference_id = u16::from_le_bytes([data[offset + 44], data[offset + 45]]);
1748            let corr_age_raw = u16::from_le_bytes([data[offset + 46], data[offset + 47]]);
1749            let signal_info =
1750                u32::from_le_bytes(data[offset + 48..offset + 52].try_into().unwrap());
1751
1752            vectors.push(BaseVectorGeodInfo {
1753                nr_sv,
1754                error,
1755                mode,
1756                misc,
1757                de_m,
1758                dn_m,
1759                du_m,
1760                dve_mps,
1761                dvn_mps,
1762                dvu_mps,
1763                azimuth_raw,
1764                elevation_raw,
1765                reference_id,
1766                corr_age_raw,
1767                signal_info,
1768            });
1769
1770            offset += sb_length;
1771        }
1772
1773        Ok(Self {
1774            tow_ms: header.tow_ms,
1775            wnc: header.wnc,
1776            vectors,
1777        })
1778    }
1779}
1780
1781// ============================================================================
1782// GEOCorrections Block
1783// ============================================================================
1784
1785/// SBAS GEO satellite correction sub-block.
1786#[derive(Debug, Clone)]
1787pub struct GeoCorrectionsSatCorr {
1788    pub svid: u8,
1789    pub iode: u8,
1790    prc_m: f32,
1791    corr_age_fc_s: f32,
1792    delta_x_m: f32,
1793    delta_y_m: f32,
1794    delta_z_m: f32,
1795    delta_clock_m: f32,
1796    corr_age_lt_s: f32,
1797    iono_pp_lat_rad: f32,
1798    iono_pp_lon_rad: f32,
1799    slant_iono_m: f32,
1800    corr_age_iono_s: f32,
1801    var_flt_m2: f32,
1802    var_uire_m2: f32,
1803    var_air_m2: f32,
1804    var_tropo_m2: f32,
1805}
1806
1807impl GeoCorrectionsSatCorr {
1808    pub fn prc_m(&self) -> Option<f32> {
1809        if self.prc_m == F32_DNU {
1810            None
1811        } else {
1812            Some(self.prc_m)
1813        }
1814    }
1815    pub fn corr_age_fc_seconds(&self) -> Option<f32> {
1816        if self.corr_age_fc_s == F32_DNU {
1817            None
1818        } else {
1819            Some(self.corr_age_fc_s)
1820        }
1821    }
1822    pub fn delta_x_m(&self) -> Option<f32> {
1823        if self.delta_x_m == F32_DNU {
1824            None
1825        } else {
1826            Some(self.delta_x_m)
1827        }
1828    }
1829    pub fn delta_y_m(&self) -> Option<f32> {
1830        if self.delta_y_m == F32_DNU {
1831            None
1832        } else {
1833            Some(self.delta_y_m)
1834        }
1835    }
1836    pub fn delta_z_m(&self) -> Option<f32> {
1837        if self.delta_z_m == F32_DNU {
1838            None
1839        } else {
1840            Some(self.delta_z_m)
1841        }
1842    }
1843    pub fn delta_clock_m(&self) -> Option<f32> {
1844        if self.delta_clock_m == F32_DNU {
1845            None
1846        } else {
1847            Some(self.delta_clock_m)
1848        }
1849    }
1850    pub fn corr_age_lt_seconds(&self) -> Option<f32> {
1851        if self.corr_age_lt_s == F32_DNU {
1852            None
1853        } else {
1854            Some(self.corr_age_lt_s)
1855        }
1856    }
1857    pub fn iono_pp_lat_rad(&self) -> Option<f32> {
1858        if self.iono_pp_lat_rad == F32_DNU {
1859            None
1860        } else {
1861            Some(self.iono_pp_lat_rad)
1862        }
1863    }
1864    pub fn iono_pp_lon_rad(&self) -> Option<f32> {
1865        if self.iono_pp_lon_rad == F32_DNU {
1866            None
1867        } else {
1868            Some(self.iono_pp_lon_rad)
1869        }
1870    }
1871    pub fn slant_iono_m(&self) -> Option<f32> {
1872        if self.slant_iono_m == F32_DNU {
1873            None
1874        } else {
1875            Some(self.slant_iono_m)
1876        }
1877    }
1878    pub fn corr_age_iono_seconds(&self) -> Option<f32> {
1879        if self.corr_age_iono_s == F32_DNU {
1880            None
1881        } else {
1882            Some(self.corr_age_iono_s)
1883        }
1884    }
1885    pub fn var_flt_m2(&self) -> Option<f32> {
1886        if self.var_flt_m2 == F32_DNU || self.var_flt_m2 < 0.0 {
1887            None
1888        } else {
1889            Some(self.var_flt_m2)
1890        }
1891    }
1892    pub fn var_uire_m2(&self) -> Option<f32> {
1893        if self.var_uire_m2 == F32_DNU || self.var_uire_m2 < 0.0 {
1894            None
1895        } else {
1896            Some(self.var_uire_m2)
1897        }
1898    }
1899    pub fn var_air_m2(&self) -> Option<f32> {
1900        if self.var_air_m2 == F32_DNU || self.var_air_m2 < 0.0 {
1901            None
1902        } else {
1903            Some(self.var_air_m2)
1904        }
1905    }
1906    pub fn var_tropo_m2(&self) -> Option<f32> {
1907        if self.var_tropo_m2 == F32_DNU || self.var_tropo_m2 < 0.0 {
1908            None
1909        } else {
1910            Some(self.var_tropo_m2)
1911        }
1912    }
1913}
1914
1915/// GEOCorrections block (Block ID 5935).
1916///
1917/// SBAS GEO satellite corrections: fast, long-term, ionosphere, variances.
1918#[derive(Debug, Clone)]
1919pub struct GeoCorrectionsBlock {
1920    tow_ms: u32,
1921    wnc: u16,
1922    pub sat_corrections: Vec<GeoCorrectionsSatCorr>,
1923}
1924
1925impl GeoCorrectionsBlock {
1926    pub fn tow_seconds(&self) -> f64 {
1927        self.tow_ms as f64 * 0.001
1928    }
1929    pub fn tow_ms(&self) -> u32 {
1930        self.tow_ms
1931    }
1932    pub fn wnc(&self) -> u16 {
1933        self.wnc
1934    }
1935    pub fn num_satellites(&self) -> usize {
1936        self.sat_corrections.len()
1937    }
1938}
1939
1940impl SbfBlockParse for GeoCorrectionsBlock {
1941    const BLOCK_ID: u16 = block_ids::GEO_CORRECTIONS;
1942
1943    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1944        if data.len() < 14 {
1945            return Err(SbfError::ParseError("GEOCorrections too short".into()));
1946        }
1947
1948        let n = data[12] as usize;
1949        let sb_length = data[13] as usize;
1950        // SatCorr: SVID(1) + IODE(1) + 15×f32(60) = 62 bytes
1951        const SAT_CORR_MIN: usize = 62;
1952        if sb_length < SAT_CORR_MIN {
1953            return Err(SbfError::ParseError(
1954                "GEOCorrections SBLength too small".into(),
1955            ));
1956        }
1957
1958        let mut sat_corrections = Vec::new();
1959        let mut offset = 14;
1960
1961        for _ in 0..n {
1962            if offset + sb_length > data.len() {
1963                break;
1964            }
1965
1966            let svid = data[offset];
1967            let iode = data[offset + 1];
1968            let prc_m = f32::from_le_bytes(data[offset + 2..offset + 6].try_into().unwrap());
1969            let corr_age_fc_s =
1970                f32::from_le_bytes(data[offset + 6..offset + 10].try_into().unwrap());
1971            let delta_x_m = f32::from_le_bytes(data[offset + 10..offset + 14].try_into().unwrap());
1972            let delta_y_m = f32::from_le_bytes(data[offset + 14..offset + 18].try_into().unwrap());
1973            let delta_z_m = f32::from_le_bytes(data[offset + 18..offset + 22].try_into().unwrap());
1974            let delta_clock_m =
1975                f32::from_le_bytes(data[offset + 22..offset + 26].try_into().unwrap());
1976            let corr_age_lt_s =
1977                f32::from_le_bytes(data[offset + 26..offset + 30].try_into().unwrap());
1978            let iono_pp_lat_rad =
1979                f32::from_le_bytes(data[offset + 30..offset + 34].try_into().unwrap());
1980            let iono_pp_lon_rad =
1981                f32::from_le_bytes(data[offset + 34..offset + 38].try_into().unwrap());
1982            let slant_iono_m =
1983                f32::from_le_bytes(data[offset + 38..offset + 42].try_into().unwrap());
1984            let corr_age_iono_s =
1985                f32::from_le_bytes(data[offset + 42..offset + 46].try_into().unwrap());
1986            let var_flt_m2 = f32::from_le_bytes(data[offset + 46..offset + 50].try_into().unwrap());
1987            let var_uire_m2 =
1988                f32::from_le_bytes(data[offset + 50..offset + 54].try_into().unwrap());
1989            let var_air_m2 = f32::from_le_bytes(data[offset + 54..offset + 58].try_into().unwrap());
1990            let var_tropo_m2 =
1991                f32::from_le_bytes(data[offset + 58..offset + 62].try_into().unwrap());
1992
1993            sat_corrections.push(GeoCorrectionsSatCorr {
1994                svid,
1995                iode,
1996                prc_m,
1997                corr_age_fc_s,
1998                delta_x_m,
1999                delta_y_m,
2000                delta_z_m,
2001                delta_clock_m,
2002                corr_age_lt_s,
2003                iono_pp_lat_rad,
2004                iono_pp_lon_rad,
2005                slant_iono_m,
2006                corr_age_iono_s,
2007                var_flt_m2,
2008                var_uire_m2,
2009                var_air_m2,
2010                var_tropo_m2,
2011            });
2012
2013            offset += sb_length;
2014        }
2015
2016        Ok(Self {
2017            tow_ms: header.tow_ms,
2018            wnc: header.wnc,
2019            sat_corrections,
2020        })
2021    }
2022}
2023
2024// ============================================================================
2025// BaseStation Block
2026// ============================================================================
2027
2028/// BaseStation block (Block ID 5949).
2029///
2030/// Base station ECEF coordinates for differential correction.
2031#[derive(Debug, Clone)]
2032pub struct BaseStationBlock {
2033    tow_ms: u32,
2034    wnc: u16,
2035    pub base_station_id: u16,
2036    pub base_type: u8,
2037    pub source: u8,
2038    pub datum: u8,
2039    x_m: f64,
2040    y_m: f64,
2041    z_m: f64,
2042}
2043
2044impl BaseStationBlock {
2045    pub fn tow_seconds(&self) -> f64 {
2046        self.tow_ms as f64 * 0.001
2047    }
2048    pub fn tow_ms(&self) -> u32 {
2049        self.tow_ms
2050    }
2051    pub fn wnc(&self) -> u16 {
2052        self.wnc
2053    }
2054
2055    pub fn x_m(&self) -> Option<f64> {
2056        if self.x_m == F64_DNU {
2057            None
2058        } else {
2059            Some(self.x_m)
2060        }
2061    }
2062    pub fn y_m(&self) -> Option<f64> {
2063        if self.y_m == F64_DNU {
2064            None
2065        } else {
2066            Some(self.y_m)
2067        }
2068    }
2069    pub fn z_m(&self) -> Option<f64> {
2070        if self.z_m == F64_DNU {
2071            None
2072        } else {
2073            Some(self.z_m)
2074        }
2075    }
2076}
2077
2078impl SbfBlockParse for BaseStationBlock {
2079    const BLOCK_ID: u16 = block_ids::BASE_STATION;
2080
2081    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2082        if data.len() < 42 {
2083            return Err(SbfError::ParseError("BaseStation too short".into()));
2084        }
2085
2086        let base_station_id = u16::from_le_bytes([data[12], data[13]]);
2087        let base_type = data[14];
2088        let source = data[15];
2089        let datum = data[16];
2090        // Byte 17 is reserved.
2091        let x_m = f64::from_le_bytes(data[18..26].try_into().unwrap());
2092        let y_m = f64::from_le_bytes(data[26..34].try_into().unwrap());
2093        let z_m = f64::from_le_bytes(data[34..42].try_into().unwrap());
2094
2095        Ok(Self {
2096            tow_ms: header.tow_ms,
2097            wnc: header.wnc,
2098            base_station_id,
2099            base_type,
2100            source,
2101            datum,
2102            x_m,
2103            y_m,
2104            z_m,
2105        })
2106    }
2107}
2108
2109// ============================================================================
2110// PosCovCartesian Block
2111// ============================================================================
2112
2113/// PosCovCartesian block (Block ID 5905)
2114///
2115/// Position covariance matrix in ECEF Cartesian coordinates.
2116#[derive(Debug, Clone)]
2117pub struct PosCovCartesianBlock {
2118    tow_ms: u32,
2119    wnc: u16,
2120    mode: u8,
2121    error: u8,
2122    /// X position variance (m^2)
2123    pub cov_xx: f32,
2124    /// Y position variance (m^2)
2125    pub cov_yy: f32,
2126    /// Z position variance (m^2)
2127    pub cov_zz: f32,
2128    /// Clock bias variance (m^2)
2129    pub cov_bb: f32,
2130    /// X-Y covariance
2131    pub cov_xy: f32,
2132    /// X-Z covariance
2133    pub cov_xz: f32,
2134    /// X-Bias covariance
2135    pub cov_xb: f32,
2136    /// Y-Z covariance
2137    pub cov_yz: f32,
2138    /// Y-Bias covariance
2139    pub cov_yb: f32,
2140    /// Z-Bias covariance
2141    pub cov_zb: f32,
2142}
2143
2144impl PosCovCartesianBlock {
2145    pub fn tow_seconds(&self) -> f64 {
2146        self.tow_ms as f64 * 0.001
2147    }
2148    pub fn tow_ms(&self) -> u32 {
2149        self.tow_ms
2150    }
2151    pub fn wnc(&self) -> u16 {
2152        self.wnc
2153    }
2154    pub fn mode(&self) -> PvtMode {
2155        PvtMode::from_mode_byte(self.mode)
2156    }
2157    pub fn error(&self) -> PvtError {
2158        PvtError::from_error_byte(self.error)
2159    }
2160
2161    /// Get X position standard deviation in meters
2162    pub fn x_std_m(&self) -> Option<f32> {
2163        if self.cov_xx == F32_DNU || self.cov_xx < 0.0 {
2164            None
2165        } else {
2166            Some(self.cov_xx.sqrt())
2167        }
2168    }
2169
2170    /// Get Y position standard deviation in meters
2171    pub fn y_std_m(&self) -> Option<f32> {
2172        if self.cov_yy == F32_DNU || self.cov_yy < 0.0 {
2173            None
2174        } else {
2175            Some(self.cov_yy.sqrt())
2176        }
2177    }
2178
2179    /// Get Z position standard deviation in meters
2180    pub fn z_std_m(&self) -> Option<f32> {
2181        if self.cov_zz == F32_DNU || self.cov_zz < 0.0 {
2182            None
2183        } else {
2184            Some(self.cov_zz.sqrt())
2185        }
2186    }
2187
2188    /// Get clock bias standard deviation in meters
2189    pub fn clock_std_m(&self) -> Option<f32> {
2190        if self.cov_bb == F32_DNU || self.cov_bb < 0.0 {
2191            None
2192        } else {
2193            Some(self.cov_bb.sqrt())
2194        }
2195    }
2196}
2197
2198// ============================================================================
2199// PVTSupport Block
2200// ============================================================================
2201
2202/// PVTSupport block (Block ID 4076)
2203///
2204/// Internal PVT support parameters. Per SBF spec, contains TOW and WNc.
2205/// Full payload layout is not in public domain; this parses the common header.
2206#[derive(Debug, Clone)]
2207pub struct PvtSupportBlock {
2208    tow_ms: u32,
2209    wnc: u16,
2210}
2211
2212impl PvtSupportBlock {
2213    pub fn tow_seconds(&self) -> f64 {
2214        self.tow_ms as f64 * 0.001
2215    }
2216    pub fn tow_ms(&self) -> u32 {
2217        self.tow_ms
2218    }
2219    pub fn wnc(&self) -> u16 {
2220        self.wnc
2221    }
2222}
2223
2224impl SbfBlockParse for PvtSupportBlock {
2225    const BLOCK_ID: u16 = block_ids::PVT_SUPPORT;
2226
2227    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2228        if data.len() < 12 {
2229            return Err(SbfError::ParseError("PVTSupport too short".into()));
2230        }
2231
2232        Ok(Self {
2233            tow_ms: header.tow_ms,
2234            wnc: header.wnc,
2235        })
2236    }
2237}
2238
2239/// PVTSupportA block (Block ID 4079).
2240///
2241/// The public reference guide does not document the payload layout. This preserves the raw
2242/// payload bytes after the time header.
2243#[derive(Debug, Clone)]
2244pub struct PvtSupportABlock {
2245    tow_ms: u32,
2246    wnc: u16,
2247    payload: Vec<u8>,
2248}
2249
2250impl PvtSupportABlock {
2251    pub fn tow_seconds(&self) -> f64 {
2252        self.tow_ms as f64 * 0.001
2253    }
2254    pub fn tow_ms(&self) -> u32 {
2255        self.tow_ms
2256    }
2257    pub fn wnc(&self) -> u16 {
2258        self.wnc
2259    }
2260    pub fn payload(&self) -> &[u8] {
2261        &self.payload
2262    }
2263}
2264
2265impl SbfBlockParse for PvtSupportABlock {
2266    const BLOCK_ID: u16 = block_ids::PVT_SUPPORT_A;
2267
2268    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2269        let block_len = header.length as usize;
2270        let data_len = block_len.saturating_sub(2);
2271        if data_len < 12 || data.len() < data_len {
2272            return Err(SbfError::ParseError("PVTSupportA too short".into()));
2273        }
2274
2275        Ok(Self {
2276            tow_ms: header.tow_ms,
2277            wnc: header.wnc,
2278            payload: data[12..data_len].to_vec(),
2279        })
2280    }
2281}
2282
2283impl SbfBlockParse for PosCovCartesianBlock {
2284    const BLOCK_ID: u16 = block_ids::POS_COV_CARTESIAN;
2285
2286    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2287        if data.len() < 54 {
2288            return Err(SbfError::ParseError("PosCovCartesian too short".into()));
2289        }
2290
2291        let mode = data[12];
2292        let error = data[13];
2293
2294        let cov_xx = f32::from_le_bytes(data[14..18].try_into().unwrap());
2295        let cov_yy = f32::from_le_bytes(data[18..22].try_into().unwrap());
2296        let cov_zz = f32::from_le_bytes(data[22..26].try_into().unwrap());
2297        let cov_bb = f32::from_le_bytes(data[26..30].try_into().unwrap());
2298        let cov_xy = f32::from_le_bytes(data[30..34].try_into().unwrap());
2299        let cov_xz = f32::from_le_bytes(data[34..38].try_into().unwrap());
2300        let cov_xb = f32::from_le_bytes(data[38..42].try_into().unwrap());
2301        let cov_yz = f32::from_le_bytes(data[42..46].try_into().unwrap());
2302        let cov_yb = f32::from_le_bytes(data[46..50].try_into().unwrap());
2303        let cov_zb = f32::from_le_bytes(data[50..54].try_into().unwrap());
2304
2305        Ok(Self {
2306            tow_ms: header.tow_ms,
2307            wnc: header.wnc,
2308            mode,
2309            error,
2310            cov_xx,
2311            cov_yy,
2312            cov_zz,
2313            cov_bb,
2314            cov_xy,
2315            cov_xz,
2316            cov_xb,
2317            cov_yz,
2318            cov_yb,
2319            cov_zb,
2320        })
2321    }
2322}
2323
2324// ============================================================================
2325// PosCovGeodetic Block
2326// ============================================================================
2327
2328/// PosCovGeodetic block (Block ID 5906)
2329///
2330/// Position covariance matrix in geodetic coordinates.
2331#[derive(Debug, Clone)]
2332pub struct PosCovGeodeticBlock {
2333    tow_ms: u32,
2334    wnc: u16,
2335    mode: u8,
2336    error: u8,
2337    /// Latitude variance (m^2)
2338    pub cov_lat_lat: f32,
2339    /// Longitude variance (m^2)
2340    pub cov_lon_lon: f32,
2341    /// Height variance (m^2)
2342    pub cov_h_h: f32,
2343    /// Clock bias variance (m^2)
2344    pub cov_b_b: f32,
2345    /// Lat-Lon covariance
2346    pub cov_lat_lon: f32,
2347    /// Lat-Height covariance
2348    pub cov_lat_h: f32,
2349    /// Lat-Bias covariance
2350    pub cov_lat_b: f32,
2351    /// Lon-Height covariance
2352    pub cov_lon_h: f32,
2353    /// Lon-Bias covariance
2354    pub cov_lon_b: f32,
2355    /// Height-Bias covariance
2356    pub cov_h_b: f32,
2357}
2358
2359impl PosCovGeodeticBlock {
2360    pub fn tow_seconds(&self) -> f64 {
2361        self.tow_ms as f64 * 0.001
2362    }
2363    pub fn tow_ms(&self) -> u32 {
2364        self.tow_ms
2365    }
2366    pub fn wnc(&self) -> u16 {
2367        self.wnc
2368    }
2369    pub fn mode(&self) -> PvtMode {
2370        PvtMode::from_mode_byte(self.mode)
2371    }
2372    pub fn error(&self) -> PvtError {
2373        PvtError::from_error_byte(self.error)
2374    }
2375
2376    /// Get latitude standard deviation in meters
2377    pub fn lat_std_m(&self) -> Option<f32> {
2378        if self.cov_lat_lat == F32_DNU || self.cov_lat_lat < 0.0 {
2379            None
2380        } else {
2381            Some(self.cov_lat_lat.sqrt())
2382        }
2383    }
2384
2385    /// Get longitude standard deviation in meters
2386    pub fn lon_std_m(&self) -> Option<f32> {
2387        if self.cov_lon_lon == F32_DNU || self.cov_lon_lon < 0.0 {
2388            None
2389        } else {
2390            Some(self.cov_lon_lon.sqrt())
2391        }
2392    }
2393
2394    /// Get height standard deviation in meters
2395    pub fn height_std_m(&self) -> Option<f32> {
2396        if self.cov_h_h == F32_DNU || self.cov_h_h < 0.0 {
2397            None
2398        } else {
2399            Some(self.cov_h_h.sqrt())
2400        }
2401    }
2402}
2403
2404impl SbfBlockParse for PosCovGeodeticBlock {
2405    const BLOCK_ID: u16 = block_ids::POS_COV_GEODETIC;
2406
2407    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2408        if data.len() < 54 {
2409            return Err(SbfError::ParseError("PosCovGeodetic too short".into()));
2410        }
2411
2412        let mode = data[12];
2413        let error = data[13];
2414
2415        let cov_lat_lat = f32::from_le_bytes(data[14..18].try_into().unwrap());
2416        let cov_lon_lon = f32::from_le_bytes(data[18..22].try_into().unwrap());
2417        let cov_h_h = f32::from_le_bytes(data[22..26].try_into().unwrap());
2418        let cov_b_b = f32::from_le_bytes(data[26..30].try_into().unwrap());
2419        let cov_lat_lon = f32::from_le_bytes(data[30..34].try_into().unwrap());
2420        let cov_lat_h = f32::from_le_bytes(data[34..38].try_into().unwrap());
2421        let cov_lat_b = f32::from_le_bytes(data[38..42].try_into().unwrap());
2422        let cov_lon_h = f32::from_le_bytes(data[42..46].try_into().unwrap());
2423        let cov_lon_b = f32::from_le_bytes(data[46..50].try_into().unwrap());
2424        let cov_h_b = f32::from_le_bytes(data[50..54].try_into().unwrap());
2425
2426        Ok(Self {
2427            tow_ms: header.tow_ms,
2428            wnc: header.wnc,
2429            mode,
2430            error,
2431            cov_lat_lat,
2432            cov_lon_lon,
2433            cov_h_h,
2434            cov_b_b,
2435            cov_lat_lon,
2436            cov_lat_h,
2437            cov_lat_b,
2438            cov_lon_h,
2439            cov_lon_b,
2440            cov_h_b,
2441        })
2442    }
2443}
2444
2445// ============================================================================
2446// VelCovGeodetic Block
2447// ============================================================================
2448
2449/// VelCovGeodetic block (Block ID 5908)
2450///
2451/// Velocity covariance matrix in geodetic coordinates.
2452#[derive(Debug, Clone)]
2453pub struct VelCovGeodeticBlock {
2454    tow_ms: u32,
2455    wnc: u16,
2456    mode: u8,
2457    error: u8,
2458    /// North velocity variance (m^2/s^2)
2459    pub cov_vn_vn: f32,
2460    /// East velocity variance (m^2/s^2)
2461    pub cov_ve_ve: f32,
2462    /// Up velocity variance (m^2/s^2)
2463    pub cov_vu_vu: f32,
2464    /// Clock drift variance
2465    pub cov_dt_dt: f32,
2466    /// Vn-Ve covariance
2467    pub cov_vn_ve: f32,
2468    /// Vn-Vu covariance
2469    pub cov_vn_vu: f32,
2470    /// Vn-Dt covariance
2471    pub cov_vn_dt: f32,
2472    /// Ve-Vu covariance
2473    pub cov_ve_vu: f32,
2474    /// Ve-Dt covariance
2475    pub cov_ve_dt: f32,
2476    /// Vu-Dt covariance
2477    pub cov_vu_dt: f32,
2478}
2479
2480impl VelCovGeodeticBlock {
2481    pub fn tow_seconds(&self) -> f64 {
2482        self.tow_ms as f64 * 0.001
2483    }
2484    pub fn tow_ms(&self) -> u32 {
2485        self.tow_ms
2486    }
2487    pub fn wnc(&self) -> u16 {
2488        self.wnc
2489    }
2490    pub fn mode(&self) -> PvtMode {
2491        PvtMode::from_mode_byte(self.mode)
2492    }
2493    pub fn error(&self) -> PvtError {
2494        PvtError::from_error_byte(self.error)
2495    }
2496
2497    /// Get north velocity standard deviation in m/s
2498    pub fn vn_std_mps(&self) -> Option<f32> {
2499        if self.cov_vn_vn == F32_DNU || self.cov_vn_vn < 0.0 {
2500            None
2501        } else {
2502            Some(self.cov_vn_vn.sqrt())
2503        }
2504    }
2505
2506    /// Get east velocity standard deviation in m/s
2507    pub fn ve_std_mps(&self) -> Option<f32> {
2508        if self.cov_ve_ve == F32_DNU || self.cov_ve_ve < 0.0 {
2509            None
2510        } else {
2511            Some(self.cov_ve_ve.sqrt())
2512        }
2513    }
2514
2515    /// Get up velocity standard deviation in m/s
2516    pub fn vu_std_mps(&self) -> Option<f32> {
2517        if self.cov_vu_vu == F32_DNU || self.cov_vu_vu < 0.0 {
2518            None
2519        } else {
2520            Some(self.cov_vu_vu.sqrt())
2521        }
2522    }
2523}
2524
2525impl SbfBlockParse for VelCovGeodeticBlock {
2526    const BLOCK_ID: u16 = block_ids::VEL_COV_GEODETIC;
2527
2528    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2529        if data.len() < 54 {
2530            return Err(SbfError::ParseError("VelCovGeodetic too short".into()));
2531        }
2532
2533        let mode = data[12];
2534        let error = data[13];
2535
2536        let cov_vn_vn = f32::from_le_bytes(data[14..18].try_into().unwrap());
2537        let cov_ve_ve = f32::from_le_bytes(data[18..22].try_into().unwrap());
2538        let cov_vu_vu = f32::from_le_bytes(data[22..26].try_into().unwrap());
2539        let cov_dt_dt = f32::from_le_bytes(data[26..30].try_into().unwrap());
2540        let cov_vn_ve = f32::from_le_bytes(data[30..34].try_into().unwrap());
2541        let cov_vn_vu = f32::from_le_bytes(data[34..38].try_into().unwrap());
2542        let cov_vn_dt = f32::from_le_bytes(data[38..42].try_into().unwrap());
2543        let cov_ve_vu = f32::from_le_bytes(data[42..46].try_into().unwrap());
2544        let cov_ve_dt = f32::from_le_bytes(data[46..50].try_into().unwrap());
2545        let cov_vu_dt = f32::from_le_bytes(data[50..54].try_into().unwrap());
2546
2547        Ok(Self {
2548            tow_ms: header.tow_ms,
2549            wnc: header.wnc,
2550            mode,
2551            error,
2552            cov_vn_vn,
2553            cov_ve_ve,
2554            cov_vu_vu,
2555            cov_dt_dt,
2556            cov_vn_ve,
2557            cov_vn_vu,
2558            cov_vn_dt,
2559            cov_ve_vu,
2560            cov_ve_dt,
2561            cov_vu_dt,
2562        })
2563    }
2564}
2565
2566// ============================================================================
2567// VelCovCartesian Block
2568// ============================================================================
2569
2570/// VelCovCartesian block (Block ID 5907)
2571///
2572/// Velocity covariance matrix in ECEF Cartesian coordinates.
2573#[derive(Debug, Clone)]
2574pub struct VelCovCartesianBlock {
2575    tow_ms: u32,
2576    wnc: u16,
2577    mode: u8,
2578    error: u8,
2579    /// X velocity variance (m^2/s^2)
2580    pub cov_vx_vx: f32,
2581    /// Y velocity variance (m^2/s^2)
2582    pub cov_vy_vy: f32,
2583    /// Z velocity variance (m^2/s^2)
2584    pub cov_vz_vz: f32,
2585    /// Clock drift variance
2586    pub cov_dt_dt: f32,
2587    /// Vx-Vy covariance
2588    pub cov_vx_vy: f32,
2589    /// Vx-Vz covariance
2590    pub cov_vx_vz: f32,
2591    /// Vx-Dt covariance
2592    pub cov_vx_dt: f32,
2593    /// Vy-Vz covariance
2594    pub cov_vy_vz: f32,
2595    /// Vy-Dt covariance
2596    pub cov_vy_dt: f32,
2597    /// Vz-Dt covariance
2598    pub cov_vz_dt: f32,
2599}
2600
2601impl VelCovCartesianBlock {
2602    pub fn tow_seconds(&self) -> f64 {
2603        self.tow_ms as f64 * 0.001
2604    }
2605    pub fn tow_ms(&self) -> u32 {
2606        self.tow_ms
2607    }
2608    pub fn wnc(&self) -> u16 {
2609        self.wnc
2610    }
2611    pub fn mode(&self) -> PvtMode {
2612        PvtMode::from_mode_byte(self.mode)
2613    }
2614    pub fn error(&self) -> PvtError {
2615        PvtError::from_error_byte(self.error)
2616    }
2617
2618    /// Get X velocity standard deviation in m/s
2619    pub fn vx_std_mps(&self) -> Option<f32> {
2620        if self.cov_vx_vx == F32_DNU || self.cov_vx_vx < 0.0 {
2621            None
2622        } else {
2623            Some(self.cov_vx_vx.sqrt())
2624        }
2625    }
2626
2627    /// Get Y velocity standard deviation in m/s
2628    pub fn vy_std_mps(&self) -> Option<f32> {
2629        if self.cov_vy_vy == F32_DNU || self.cov_vy_vy < 0.0 {
2630            None
2631        } else {
2632            Some(self.cov_vy_vy.sqrt())
2633        }
2634    }
2635
2636    /// Get Z velocity standard deviation in m/s
2637    pub fn vz_std_mps(&self) -> Option<f32> {
2638        if self.cov_vz_vz == F32_DNU || self.cov_vz_vz < 0.0 {
2639            None
2640        } else {
2641            Some(self.cov_vz_vz.sqrt())
2642        }
2643    }
2644
2645    /// Get clock drift standard deviation
2646    pub fn clock_drift_std(&self) -> Option<f32> {
2647        if self.cov_dt_dt == F32_DNU || self.cov_dt_dt < 0.0 {
2648            None
2649        } else {
2650            Some(self.cov_dt_dt.sqrt())
2651        }
2652    }
2653}
2654
2655impl SbfBlockParse for VelCovCartesianBlock {
2656    const BLOCK_ID: u16 = block_ids::VEL_COV_CARTESIAN;
2657
2658    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2659        if data.len() < 54 {
2660            return Err(SbfError::ParseError("VelCovCartesian too short".into()));
2661        }
2662
2663        let mode = data[12];
2664        let error = data[13];
2665
2666        let cov_vx_vx = f32::from_le_bytes(data[14..18].try_into().unwrap());
2667        let cov_vy_vy = f32::from_le_bytes(data[18..22].try_into().unwrap());
2668        let cov_vz_vz = f32::from_le_bytes(data[22..26].try_into().unwrap());
2669        let cov_dt_dt = f32::from_le_bytes(data[26..30].try_into().unwrap());
2670        let cov_vx_vy = f32::from_le_bytes(data[30..34].try_into().unwrap());
2671        let cov_vx_vz = f32::from_le_bytes(data[34..38].try_into().unwrap());
2672        let cov_vx_dt = f32::from_le_bytes(data[38..42].try_into().unwrap());
2673        let cov_vy_vz = f32::from_le_bytes(data[42..46].try_into().unwrap());
2674        let cov_vy_dt = f32::from_le_bytes(data[46..50].try_into().unwrap());
2675        let cov_vz_dt = f32::from_le_bytes(data[50..54].try_into().unwrap());
2676
2677        Ok(Self {
2678            tow_ms: header.tow_ms,
2679            wnc: header.wnc,
2680            mode,
2681            error,
2682            cov_vx_vx,
2683            cov_vy_vy,
2684            cov_vz_vz,
2685            cov_dt_dt,
2686            cov_vx_vy,
2687            cov_vx_vz,
2688            cov_vx_dt,
2689            cov_vy_vz,
2690            cov_vy_dt,
2691            cov_vz_dt,
2692        })
2693    }
2694}
2695
2696#[cfg(test)]
2697mod tests {
2698    use super::*;
2699    use crate::header::SbfHeader;
2700
2701    fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
2702        SbfHeader {
2703            crc: 0,
2704            block_id,
2705            block_rev: 0,
2706            length: (data_len + 2) as u16,
2707            tow_ms,
2708            wnc,
2709        }
2710    }
2711
2712    #[test]
2713    fn test_pvt_nr_sv_dnu_handling() {
2714        let mut geodetic_data = vec![0u8; 83];
2715        geodetic_data[72] = 255;
2716        let header = header_for(block_ids::PVT_GEODETIC, geodetic_data.len(), 1000, 2200);
2717        let geodetic = PvtGeodeticBlock::parse(&header, &geodetic_data).unwrap();
2718        assert_eq!(geodetic.num_satellites_raw(), 255);
2719        assert_eq!(geodetic.num_satellites_opt(), None);
2720        assert_eq!(geodetic.num_satellites(), 0);
2721
2722        let mut cartesian_data = vec![0u8; 83];
2723        cartesian_data[72] = 255;
2724        let header = header_for(block_ids::PVT_CARTESIAN, cartesian_data.len(), 1000, 2200);
2725        let cartesian = PvtCartesianBlock::parse(&header, &cartesian_data).unwrap();
2726        assert_eq!(cartesian.num_satellites_raw(), 255);
2727        assert_eq!(cartesian.num_satellites_opt(), None);
2728        assert_eq!(cartesian.num_satellites(), 0);
2729    }
2730
2731    #[test]
2732    fn test_pvt_sat_cartesian_accessors() {
2733        let sat = PvtSatCartesianSatPos {
2734            svid: 12,
2735            freq_nr: 1,
2736            iode: 22,
2737            x_m: 10.0,
2738            y_m: 20.0,
2739            z_m: 30.0,
2740            vx_mps: 1.0,
2741            vy_mps: 2.0,
2742            vz_mps: 3.0,
2743        };
2744        let block = PvtSatCartesianBlock {
2745            tow_ms: 2500,
2746            wnc: 2345,
2747            satellites: vec![sat],
2748        };
2749
2750        assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
2751        assert_eq!(block.num_satellites(), 1);
2752        let sat = &block.satellites[0];
2753        assert!((sat.x_m().unwrap() - 10.0).abs() < 1e-6);
2754        assert!((sat.vz_mps().unwrap() - 3.0).abs() < 1e-6);
2755    }
2756
2757    #[test]
2758    fn test_pvt_sat_cartesian_dnu_handling() {
2759        let sat = PvtSatCartesianSatPos {
2760            svid: 1,
2761            freq_nr: 0,
2762            iode: 0,
2763            x_m: F64_DNU,
2764            y_m: 1.0,
2765            z_m: F64_DNU,
2766            vx_mps: F32_DNU,
2767            vy_mps: 0.5,
2768            vz_mps: F32_DNU,
2769        };
2770
2771        assert!(sat.x_m().is_none());
2772        assert!(sat.y_m().is_some());
2773        assert!(sat.z_m().is_none());
2774        assert!(sat.vx_mps().is_none());
2775        assert!(sat.vy_mps().is_some());
2776        assert!(sat.vz_mps().is_none());
2777    }
2778
2779    #[test]
2780    fn test_pvt_sat_cartesian_parse() {
2781        let mut data = vec![0u8; 14 + 40];
2782        data[12] = 1;
2783        data[13] = 40;
2784
2785        let offset = 14;
2786        let iode = 512_u16;
2787        let x = 11.0_f64;
2788        let y = 22.0_f64;
2789        let z = 33.0_f64;
2790        let vx = 0.1_f32;
2791        let vy = 0.2_f32;
2792        let vz = 0.3_f32;
2793
2794        data[offset] = 31;
2795        data[offset + 1] = 2;
2796        data[offset + 2..offset + 4].copy_from_slice(&iode.to_le_bytes());
2797        data[offset + 4..offset + 12].copy_from_slice(&x.to_le_bytes());
2798        data[offset + 12..offset + 20].copy_from_slice(&y.to_le_bytes());
2799        data[offset + 20..offset + 28].copy_from_slice(&z.to_le_bytes());
2800        data[offset + 28..offset + 32].copy_from_slice(&vx.to_le_bytes());
2801        data[offset + 32..offset + 36].copy_from_slice(&vy.to_le_bytes());
2802        data[offset + 36..offset + 40].copy_from_slice(&vz.to_le_bytes());
2803
2804        let header = header_for(block_ids::PVT_SAT_CARTESIAN, data.len(), 123000, 2201);
2805        let block = PvtSatCartesianBlock::parse(&header, &data).unwrap();
2806
2807        assert_eq!(block.tow_ms(), 123000);
2808        assert_eq!(block.wnc(), 2201);
2809        assert_eq!(block.num_satellites(), 1);
2810        let sat = &block.satellites[0];
2811        assert_eq!(sat.svid, 31);
2812        assert_eq!(sat.iode, iode);
2813        assert!((sat.x_m().unwrap() - x).abs() < 1e-6);
2814        assert!((sat.vy_mps().unwrap() - vy).abs() < 1e-6);
2815    }
2816
2817    #[test]
2818    fn test_pvt_residuals_v2_accessors() {
2819        let residual = PvtResidualsV2ResidualInfo {
2820            e_i_m: 0.25,
2821            w_i_raw: 120,
2822            mdb_raw: 42,
2823        };
2824        let sat = PvtResidualsV2SatSignalInfo {
2825            svid: 7,
2826            freq_nr: 1,
2827            signal_type: 17,
2828            ref_svid: 33,
2829            ref_freq_nr: 0,
2830            meas_info: (1 << 2) | (1 << 4),
2831            iode: 300,
2832            corr_age_raw: 150,
2833            reference_id: 12,
2834            residuals: vec![residual],
2835        };
2836        let block = PvtResidualsV2Block {
2837            tow_ms: 2000,
2838            wnc: 100,
2839            sat_signal_info: vec![sat],
2840        };
2841
2842        assert!((block.tow_seconds() - 2.0).abs() < 1e-6);
2843        assert_eq!(block.num_sat_signals(), 1);
2844        let sat = &block.sat_signal_info[0];
2845        assert!((sat.corr_age_seconds().unwrap() - 1.5).abs() < 1e-6);
2846        assert_eq!(sat.expected_residual_count(), 2);
2847        assert!((sat.residuals[0].residual_m().unwrap() - 0.25).abs() < 1e-6);
2848        assert_eq!(sat.residuals[0].weight().unwrap(), 120);
2849    }
2850
2851    #[test]
2852    fn test_pvt_residuals_v2_dnu_handling() {
2853        let residual = PvtResidualsV2ResidualInfo {
2854            e_i_m: F32_DNU,
2855            w_i_raw: U16_DNU,
2856            mdb_raw: U16_DNU,
2857        };
2858        let sat = PvtResidualsV2SatSignalInfo {
2859            svid: 0,
2860            freq_nr: 0,
2861            signal_type: 0,
2862            ref_svid: 0,
2863            ref_freq_nr: 0,
2864            meas_info: 0,
2865            iode: 0,
2866            corr_age_raw: U16_DNU,
2867            reference_id: 0,
2868            residuals: vec![residual],
2869        };
2870
2871        assert!(sat.corr_age_seconds().is_none());
2872        assert!(sat.residuals[0].residual_m().is_none());
2873        assert!(sat.residuals[0].weight().is_none());
2874        assert!(sat.residuals[0].mdb().is_none());
2875    }
2876
2877    #[test]
2878    fn test_pvt_residuals_v2_parse() {
2879        let mut data = vec![0u8; 15 + 12 + (2 * 8)];
2880        data[12] = 1; // N
2881        data[13] = 12; // SB1Length
2882        data[14] = 8; // SB2Length
2883
2884        // One SatSignalInfo with two residual entries (MeasInfo bits 2 and 4).
2885        let mut offset = 15;
2886        data[offset] = 8; // SVID
2887        data[offset + 1] = 1; // FreqNr
2888        data[offset + 2] = 17; // Type
2889        data[offset + 3] = 33; // RefSVID
2890        data[offset + 4] = 2; // RefFreqNr
2891        data[offset + 5] = (1 << 2) | (1 << 4); // MeasInfo
2892        data[offset + 6..offset + 8].copy_from_slice(&0x1234_u16.to_le_bytes()); // IODE
2893        data[offset + 8..offset + 10].copy_from_slice(&250_u16.to_le_bytes()); // CorrAge
2894        data[offset + 10..offset + 12].copy_from_slice(&77_u16.to_le_bytes()); // ReferenceID
2895        offset += 12;
2896
2897        let e1 = 0.5_f32;
2898        let e2 = -0.25_f32;
2899        data[offset..offset + 4].copy_from_slice(&e1.to_le_bytes());
2900        data[offset + 4..offset + 6].copy_from_slice(&100_u16.to_le_bytes());
2901        data[offset + 6..offset + 8].copy_from_slice(&200_u16.to_le_bytes());
2902        offset += 8;
2903
2904        data[offset..offset + 4].copy_from_slice(&e2.to_le_bytes());
2905        data[offset + 4..offset + 6].copy_from_slice(&101_u16.to_le_bytes());
2906        data[offset + 6..offset + 8].copy_from_slice(&201_u16.to_le_bytes());
2907
2908        let header = header_for(block_ids::PVT_RESIDUALS_V2, data.len(), 654321, 2222);
2909        let block = PvtResidualsV2Block::parse(&header, &data).unwrap();
2910
2911        assert_eq!(block.num_sat_signals(), 1);
2912        let sat = &block.sat_signal_info[0];
2913        assert_eq!(sat.svid, 8);
2914        assert_eq!(sat.expected_residual_count(), 2);
2915        assert_eq!(sat.residuals.len(), 2);
2916        assert!((sat.corr_age_seconds().unwrap() - 2.5).abs() < 1e-6);
2917        assert!((sat.residuals[0].residual_m().unwrap() - e1).abs() < 1e-6);
2918        assert!((sat.residuals[1].residual_m().unwrap() - e2).abs() < 1e-6);
2919    }
2920
2921    #[test]
2922    fn test_raim_statistics_v2_accessors() {
2923        let block = RaimStatisticsV2Block {
2924            tow_ms: 3000,
2925            wnc: 123,
2926            integrity_flag: 2,
2927            herl_position_m: 5.0,
2928            verl_position_m: 6.0,
2929            herl_velocity_mps: 0.7,
2930            verl_velocity_mps: 0.8,
2931            overall_model: 42,
2932        };
2933
2934        assert!((block.tow_seconds() - 3.0).abs() < 1e-6);
2935        assert_eq!(block.integrity_flag, 2);
2936        assert!((block.herl_position_m().unwrap() - 5.0).abs() < 1e-6);
2937        assert!((block.verl_velocity_mps().unwrap() - 0.8).abs() < 1e-6);
2938        assert_eq!(block.overall_model().unwrap(), 42);
2939    }
2940
2941    #[test]
2942    fn test_raim_statistics_v2_dnu_handling() {
2943        let block = RaimStatisticsV2Block {
2944            tow_ms: 0,
2945            wnc: 0,
2946            integrity_flag: 0,
2947            herl_position_m: F32_DNU,
2948            verl_position_m: F32_DNU,
2949            herl_velocity_mps: F32_DNU,
2950            verl_velocity_mps: F32_DNU,
2951            overall_model: U16_DNU,
2952        };
2953
2954        assert!(block.herl_position_m().is_none());
2955        assert!(block.verl_position_m().is_none());
2956        assert!(block.herl_velocity_mps().is_none());
2957        assert!(block.verl_velocity_mps().is_none());
2958        assert!(block.overall_model().is_none());
2959    }
2960
2961    #[test]
2962    fn test_raim_statistics_v2_parse() {
2963        let mut data = vec![0u8; 34];
2964        data[12] = 3; // IntegrityFlag
2965        data[13] = 0; // Reserved
2966
2967        let herl_position = 7.5_f32;
2968        let verl_position = 8.5_f32;
2969        let herl_velocity = 0.9_f32;
2970        let verl_velocity = 1.1_f32;
2971        let overall_model = 321_u16;
2972
2973        data[14..18].copy_from_slice(&herl_position.to_le_bytes());
2974        data[18..22].copy_from_slice(&verl_position.to_le_bytes());
2975        data[22..26].copy_from_slice(&herl_velocity.to_le_bytes());
2976        data[26..30].copy_from_slice(&verl_velocity.to_le_bytes());
2977        data[30..32].copy_from_slice(&overall_model.to_le_bytes());
2978
2979        let header = header_for(block_ids::RAIM_STATISTICS_V2, data.len(), 111222, 3333);
2980        let block = RaimStatisticsV2Block::parse(&header, &data).unwrap();
2981
2982        assert_eq!(block.tow_ms(), 111222);
2983        assert_eq!(block.wnc(), 3333);
2984        assert_eq!(block.integrity_flag, 3);
2985        assert!((block.herl_position_m().unwrap() - herl_position).abs() < 1e-6);
2986        assert!((block.verl_position_m().unwrap() - verl_position).abs() < 1e-6);
2987        assert!((block.herl_velocity_mps().unwrap() - herl_velocity).abs() < 1e-6);
2988        assert!((block.verl_velocity_mps().unwrap() - verl_velocity).abs() < 1e-6);
2989        assert_eq!(block.overall_model().unwrap(), overall_model);
2990    }
2991
2992    #[test]
2993    fn test_dop_scaling() {
2994        let dop = DopBlock {
2995            tow_ms: 0,
2996            wnc: 0,
2997            nr_sv: 10,
2998            pdop_raw: 150, // 1.50
2999            tdop_raw: 100, // 1.00
3000            hdop_raw: 120, // 1.20
3001            vdop_raw: 200, // 2.00
3002            hpl_m: F32_DNU,
3003            vpl_m: F32_DNU,
3004        };
3005
3006        assert!((dop.pdop() - 1.50).abs() < 0.001);
3007        assert!((dop.hdop() - 1.20).abs() < 0.001);
3008        assert!((dop.gdop() - (1.50_f32.powi(2) + 1.0_f32.powi(2)).sqrt()).abs() < 0.001);
3009        assert_eq!(dop.pdop_opt(), Some(1.50));
3010        assert_eq!(dop.tdop_opt(), Some(1.00));
3011        assert_eq!(dop.num_satellites_opt(), Some(10));
3012    }
3013
3014    #[test]
3015    fn test_dop_dnu_handling() {
3016        let mut data = vec![0u8; 30];
3017        data[12] = 0; // NrSV is 0 when DOP information is unavailable.
3018        data[14..16].copy_from_slice(&0_u16.to_le_bytes());
3019        data[16..18].copy_from_slice(&0_u16.to_le_bytes());
3020        data[18..20].copy_from_slice(&0_u16.to_le_bytes());
3021        data[20..22].copy_from_slice(&0_u16.to_le_bytes());
3022
3023        let header = header_for(block_ids::DOP, data.len(), 1000, 2200);
3024        let dop = DopBlock::parse(&header, &data).unwrap();
3025        assert_eq!(dop.num_satellites_raw(), 0);
3026        assert_eq!(dop.num_satellites_opt(), None);
3027        assert_eq!(dop.num_satellites(), 0);
3028        assert_eq!(dop.pdop_raw(), 0);
3029        assert_eq!(dop.pdop_opt(), None);
3030        assert_eq!(dop.tdop_opt(), None);
3031        assert_eq!(dop.hdop_opt(), None);
3032        assert_eq!(dop.vdop_opt(), None);
3033        assert_eq!(dop.gdop_opt(), None);
3034        assert_eq!(dop.pdop(), 0.0);
3035        assert_eq!(dop.gdop(), 0.0);
3036    }
3037
3038    #[test]
3039    fn test_dop_generic_u16_dnu_does_not_scale_to_655_35() {
3040        let mut data = vec![0u8; 30];
3041        data[12] = 255;
3042        data[14..16].copy_from_slice(&U16_DNU.to_le_bytes());
3043        data[16..18].copy_from_slice(&U16_DNU.to_le_bytes());
3044        data[18..20].copy_from_slice(&U16_DNU.to_le_bytes());
3045        data[20..22].copy_from_slice(&U16_DNU.to_le_bytes());
3046
3047        let header = header_for(block_ids::DOP, data.len(), 1000, 2200);
3048        let dop = DopBlock::parse(&header, &data).unwrap();
3049        assert_eq!(dop.num_satellites_raw(), 255);
3050        assert_eq!(dop.num_satellites_opt(), None);
3051        assert_eq!(dop.num_satellites(), 0);
3052        assert_eq!(dop.pdop_raw(), U16_DNU);
3053        assert_eq!(dop.pdop_opt(), None);
3054        assert_eq!(dop.pdop(), 0.0);
3055        assert_ne!(dop.pdop(), 655.35);
3056        assert_eq!(dop.gdop_opt(), None);
3057    }
3058
3059    #[test]
3060    fn test_pos_cov_cartesian_std_accessors() {
3061        // Variance of 4.0 m^2 should give std dev of 2.0 m
3062        let block = PosCovCartesianBlock {
3063            tow_ms: 100000,
3064            wnc: 2300,
3065            mode: 4, // RTK Fixed
3066            error: 0,
3067            cov_xx: 4.0,
3068            cov_yy: 9.0,
3069            cov_zz: 16.0,
3070            cov_bb: 25.0,
3071            cov_xy: 1.0,
3072            cov_xz: 2.0,
3073            cov_xb: 3.0,
3074            cov_yz: 4.0,
3075            cov_yb: 5.0,
3076            cov_zb: 6.0,
3077        };
3078
3079        assert!((block.x_std_m().unwrap() - 2.0).abs() < 0.001);
3080        assert!((block.y_std_m().unwrap() - 3.0).abs() < 0.001);
3081        assert!((block.z_std_m().unwrap() - 4.0).abs() < 0.001);
3082        assert!((block.clock_std_m().unwrap() - 5.0).abs() < 0.001);
3083        assert!((block.tow_seconds() - 100.0).abs() < 0.001);
3084    }
3085
3086    #[test]
3087    fn test_pos_cov_cartesian_dnu_handling() {
3088        let block = PosCovCartesianBlock {
3089            tow_ms: 0,
3090            wnc: 0,
3091            mode: 0,
3092            error: 0,
3093            cov_xx: F32_DNU,
3094            cov_yy: -1.0, // negative variance
3095            cov_zz: 4.0,
3096            cov_bb: F32_DNU,
3097            cov_xy: 0.0,
3098            cov_xz: 0.0,
3099            cov_xb: 0.0,
3100            cov_yz: 0.0,
3101            cov_yb: 0.0,
3102            cov_zb: 0.0,
3103        };
3104
3105        assert!(block.x_std_m().is_none()); // DNU
3106        assert!(block.y_std_m().is_none()); // negative
3107        assert!(block.z_std_m().is_some()); // valid
3108        assert!(block.clock_std_m().is_none()); // DNU
3109    }
3110
3111    #[test]
3112    fn test_pos_cov_cartesian_parse() {
3113        // Build synthetic block data
3114        // Format: CRC(2) + ID(2) + Length(2) + TOW(4) + WNc(2) + Mode(1) + Error(1) + 10×f32
3115        let mut data = vec![0u8; 54];
3116
3117        // Skip CRC (0-1), ID (2-3), Length (4-5)
3118        // TOW at offset 6-9 (already handled by header)
3119        // WNc at offset 10-11 (already handled by header)
3120        // Mode at offset 12
3121        data[12] = 4; // RTK Fixed
3122                      // Error at offset 13
3123        data[13] = 0;
3124
3125        // Covariance values starting at offset 14
3126        let cov_xx: f32 = 1.0;
3127        let cov_yy: f32 = 4.0;
3128        let cov_zz: f32 = 9.0;
3129        let cov_bb: f32 = 16.0;
3130        let cov_xy: f32 = 0.5;
3131        let cov_xz: f32 = 0.6;
3132        let cov_xb: f32 = 0.7;
3133        let cov_yz: f32 = 0.8;
3134        let cov_yb: f32 = 0.9;
3135        let cov_zb: f32 = 1.1;
3136
3137        data[14..18].copy_from_slice(&cov_xx.to_le_bytes());
3138        data[18..22].copy_from_slice(&cov_yy.to_le_bytes());
3139        data[22..26].copy_from_slice(&cov_zz.to_le_bytes());
3140        data[26..30].copy_from_slice(&cov_bb.to_le_bytes());
3141        data[30..34].copy_from_slice(&cov_xy.to_le_bytes());
3142        data[34..38].copy_from_slice(&cov_xz.to_le_bytes());
3143        data[38..42].copy_from_slice(&cov_xb.to_le_bytes());
3144        data[42..46].copy_from_slice(&cov_yz.to_le_bytes());
3145        data[46..50].copy_from_slice(&cov_yb.to_le_bytes());
3146        data[50..54].copy_from_slice(&cov_zb.to_le_bytes());
3147
3148        let header = SbfHeader {
3149            crc: 0,
3150            block_id: block_ids::POS_COV_CARTESIAN,
3151            block_rev: 0,
3152            length: 56, // 2 sync + 54 data
3153            tow_ms: 123456,
3154            wnc: 2300,
3155        };
3156
3157        let block = PosCovCartesianBlock::parse(&header, &data).unwrap();
3158
3159        assert_eq!(block.tow_ms(), 123456);
3160        assert_eq!(block.wnc(), 2300);
3161        assert!((block.x_std_m().unwrap() - 1.0).abs() < 0.001);
3162        assert!((block.y_std_m().unwrap() - 2.0).abs() < 0.001);
3163        assert!((block.z_std_m().unwrap() - 3.0).abs() < 0.001);
3164        assert!((block.clock_std_m().unwrap() - 4.0).abs() < 0.001);
3165        assert!((block.cov_xy - 0.5).abs() < 0.001);
3166    }
3167
3168    #[test]
3169    fn test_vel_cov_cartesian_std_accessors() {
3170        // Variance of 0.04 m²/s² should give std dev of 0.2 m/s
3171        let block = VelCovCartesianBlock {
3172            tow_ms: 200000,
3173            wnc: 2300,
3174            mode: 4,
3175            error: 0,
3176            cov_vx_vx: 0.04,
3177            cov_vy_vy: 0.09,
3178            cov_vz_vz: 0.16,
3179            cov_dt_dt: 0.25,
3180            cov_vx_vy: 0.01,
3181            cov_vx_vz: 0.02,
3182            cov_vx_dt: 0.03,
3183            cov_vy_vz: 0.04,
3184            cov_vy_dt: 0.05,
3185            cov_vz_dt: 0.06,
3186        };
3187
3188        assert!((block.vx_std_mps().unwrap() - 0.2).abs() < 0.001);
3189        assert!((block.vy_std_mps().unwrap() - 0.3).abs() < 0.001);
3190        assert!((block.vz_std_mps().unwrap() - 0.4).abs() < 0.001);
3191        assert!((block.clock_drift_std().unwrap() - 0.5).abs() < 0.001);
3192        assert!((block.tow_seconds() - 200.0).abs() < 0.001);
3193    }
3194
3195    #[test]
3196    fn test_vel_cov_cartesian_dnu_handling() {
3197        let block = VelCovCartesianBlock {
3198            tow_ms: 0,
3199            wnc: 0,
3200            mode: 0,
3201            error: 0,
3202            cov_vx_vx: F32_DNU,
3203            cov_vy_vy: -0.01, // negative variance
3204            cov_vz_vz: 0.04,
3205            cov_dt_dt: F32_DNU,
3206            cov_vx_vy: 0.0,
3207            cov_vx_vz: 0.0,
3208            cov_vx_dt: 0.0,
3209            cov_vy_vz: 0.0,
3210            cov_vy_dt: 0.0,
3211            cov_vz_dt: 0.0,
3212        };
3213
3214        assert!(block.vx_std_mps().is_none()); // DNU
3215        assert!(block.vy_std_mps().is_none()); // negative
3216        assert!(block.vz_std_mps().is_some()); // valid
3217        assert!(block.clock_drift_std().is_none()); // DNU
3218    }
3219
3220    #[test]
3221    fn test_vel_cov_cartesian_parse() {
3222        // Build synthetic block data
3223        let mut data = vec![0u8; 54];
3224
3225        data[12] = 4; // Mode: RTK Fixed
3226        data[13] = 0; // Error: none
3227
3228        let cov_vx_vx: f32 = 0.01;
3229        let cov_vy_vy: f32 = 0.04;
3230        let cov_vz_vz: f32 = 0.09;
3231        let cov_dt_dt: f32 = 0.16;
3232        let cov_vx_vy: f32 = 0.001;
3233        let cov_vx_vz: f32 = 0.002;
3234        let cov_vx_dt: f32 = 0.003;
3235        let cov_vy_vz: f32 = 0.004;
3236        let cov_vy_dt: f32 = 0.005;
3237        let cov_vz_dt: f32 = 0.006;
3238
3239        data[14..18].copy_from_slice(&cov_vx_vx.to_le_bytes());
3240        data[18..22].copy_from_slice(&cov_vy_vy.to_le_bytes());
3241        data[22..26].copy_from_slice(&cov_vz_vz.to_le_bytes());
3242        data[26..30].copy_from_slice(&cov_dt_dt.to_le_bytes());
3243        data[30..34].copy_from_slice(&cov_vx_vy.to_le_bytes());
3244        data[34..38].copy_from_slice(&cov_vx_vz.to_le_bytes());
3245        data[38..42].copy_from_slice(&cov_vx_dt.to_le_bytes());
3246        data[42..46].copy_from_slice(&cov_vy_vz.to_le_bytes());
3247        data[46..50].copy_from_slice(&cov_vy_dt.to_le_bytes());
3248        data[50..54].copy_from_slice(&cov_vz_dt.to_le_bytes());
3249
3250        let header = SbfHeader {
3251            crc: 0,
3252            block_id: block_ids::VEL_COV_CARTESIAN,
3253            block_rev: 0,
3254            length: 56,
3255            tow_ms: 345678,
3256            wnc: 2301,
3257        };
3258
3259        let block = VelCovCartesianBlock::parse(&header, &data).unwrap();
3260
3261        assert_eq!(block.tow_ms(), 345678);
3262        assert_eq!(block.wnc(), 2301);
3263        assert!((block.vx_std_mps().unwrap() - 0.1).abs() < 0.001);
3264        assert!((block.vy_std_mps().unwrap() - 0.2).abs() < 0.001);
3265        assert!((block.vz_std_mps().unwrap() - 0.3).abs() < 0.001);
3266        assert!((block.clock_drift_std().unwrap() - 0.4).abs() < 0.001);
3267        assert!((block.cov_vx_vy - 0.001).abs() < 0.0001);
3268    }
3269
3270    #[test]
3271    fn test_pos_cart_scaled_accessors() {
3272        let block = PosCartBlock {
3273            tow_ms: 5000,
3274            wnc: 2000,
3275            mode: 4,
3276            error: 0,
3277            x_m: 10.0,
3278            y_m: 20.0,
3279            z_m: 30.0,
3280            base_x_m: 1.0,
3281            base_y_m: 2.0,
3282            base_z_m: 3.0,
3283            cov_xx: 4.0,
3284            cov_yy: 9.0,
3285            cov_zz: 16.0,
3286            cov_xy: 0.0,
3287            cov_xz: 0.0,
3288            cov_yz: 0.0,
3289            pdop_raw: 200,
3290            hdop_raw: 150,
3291            vdop_raw: 250,
3292            misc: 0,
3293            alert_flag: 0,
3294            datum: 0,
3295            nr_sv: 12,
3296            wa_corr_info: 1,
3297            reference_id: 10,
3298            mean_corr_age_raw: 150,
3299            signal_info: 0,
3300        };
3301
3302        assert!((block.pdop().unwrap() - 2.0).abs() < 1e-6);
3303        assert!((block.hdop().unwrap() - 1.5).abs() < 1e-6);
3304        assert!((block.vdop().unwrap() - 2.5).abs() < 1e-6);
3305        assert!((block.mean_corr_age_seconds().unwrap() - 1.5).abs() < 1e-6);
3306        assert!((block.x_std_m().unwrap() - 2.0).abs() < 1e-6);
3307        assert!((block.tow_seconds() - 5.0).abs() < 1e-6);
3308    }
3309
3310    #[test]
3311    fn test_pos_cart_dnu_handling() {
3312        let block = PosCartBlock {
3313            tow_ms: 0,
3314            wnc: 0,
3315            mode: 0,
3316            error: 0,
3317            x_m: F64_DNU,
3318            y_m: 1.0,
3319            z_m: 1.0,
3320            base_x_m: 1.0,
3321            base_y_m: 1.0,
3322            base_z_m: 1.0,
3323            cov_xx: F32_DNU,
3324            cov_yy: -1.0,
3325            cov_zz: 4.0,
3326            cov_xy: 0.0,
3327            cov_xz: 0.0,
3328            cov_yz: 0.0,
3329            pdop_raw: 0,
3330            hdop_raw: U16_DNU,
3331            vdop_raw: 0,
3332            misc: 0,
3333            alert_flag: 0,
3334            datum: 0,
3335            nr_sv: 255,
3336            wa_corr_info: 0,
3337            reference_id: 0,
3338            mean_corr_age_raw: U16_DNU,
3339            signal_info: 0,
3340        };
3341
3342        assert!(block.x_m().is_none());
3343        assert!(block.x_std_m().is_none());
3344        assert!(block.y_std_m().is_none());
3345        assert!(block.pdop().is_none());
3346        assert!(block.hdop().is_none());
3347        assert!(block.vdop().is_none());
3348        assert_eq!(block.num_satellites_raw(), 255);
3349        assert_eq!(block.num_satellites_opt(), None);
3350        assert_eq!(block.num_satellites(), 0);
3351        assert!(block.mean_corr_age_seconds().is_none());
3352    }
3353
3354    #[test]
3355    fn test_pos_cart_parse() {
3356        let mut data = vec![0u8; 106];
3357        data[12] = 8;
3358        data[13] = 0;
3359        data[14] = 0; // Reserved
3360
3361        let x_m = 123.0_f64;
3362        let y_m = 456.0_f64;
3363        let z_m = 789.0_f64;
3364        let base_x = 10.0_f64;
3365        let base_y = 20.0_f64;
3366        let base_z = 30.0_f64;
3367        let cov_xx = 1.0_f32;
3368        let cov_yy = 4.0_f32;
3369        let cov_zz = 9.0_f32;
3370        let cov_xy = 0.1_f32;
3371        let cov_xz = 0.2_f32;
3372        let cov_yz = 0.3_f32;
3373        let pdop_raw = 250_u16;
3374        let hdop_raw = 150_u16;
3375        let vdop_raw = 350_u16;
3376        let mean_corr_age_raw = 120_u16;
3377        let signal_info = 0x12345678_u32;
3378
3379        data[15..23].copy_from_slice(&x_m.to_le_bytes());
3380        data[23..31].copy_from_slice(&y_m.to_le_bytes());
3381        data[31..39].copy_from_slice(&z_m.to_le_bytes());
3382        data[39..47].copy_from_slice(&base_x.to_le_bytes());
3383        data[47..55].copy_from_slice(&base_y.to_le_bytes());
3384        data[55..63].copy_from_slice(&base_z.to_le_bytes());
3385        data[63..67].copy_from_slice(&cov_xx.to_le_bytes());
3386        data[67..71].copy_from_slice(&cov_yy.to_le_bytes());
3387        data[71..75].copy_from_slice(&cov_zz.to_le_bytes());
3388        data[75..79].copy_from_slice(&cov_xy.to_le_bytes());
3389        data[79..83].copy_from_slice(&cov_xz.to_le_bytes());
3390        data[83..87].copy_from_slice(&cov_yz.to_le_bytes());
3391        data[87..89].copy_from_slice(&pdop_raw.to_le_bytes());
3392        data[89..91].copy_from_slice(&hdop_raw.to_le_bytes());
3393        data[91..93].copy_from_slice(&vdop_raw.to_le_bytes());
3394        data[93] = 0;
3395        data[94] = 0;
3396        data[95] = 1;
3397        data[96] = 8;
3398        data[97] = 2;
3399        data[98..100].copy_from_slice(&55_u16.to_le_bytes());
3400        data[100..102].copy_from_slice(&mean_corr_age_raw.to_le_bytes());
3401        data[102..106].copy_from_slice(&signal_info.to_le_bytes());
3402
3403        let header = header_for(block_ids::POS_CART, data.len(), 123456, 2222);
3404        let block = PosCartBlock::parse(&header, &data).unwrap();
3405
3406        assert_eq!(block.tow_ms(), 123456);
3407        assert_eq!(block.wnc(), 2222);
3408        assert!((block.x_m().unwrap() - x_m).abs() < 1e-6);
3409        assert!((block.pdop().unwrap() - 2.5).abs() < 1e-6);
3410        assert_eq!(block.reference_id, 55);
3411        assert_eq!(block.signal_info, signal_info);
3412    }
3413
3414    #[test]
3415    fn test_base_vector_cart_scaled_accessors() {
3416        let info = BaseVectorCartInfo {
3417            nr_sv: 12,
3418            error: 0,
3419            mode: 8,
3420            misc: 0,
3421            dx_m: 1.0,
3422            dy_m: 2.0,
3423            dz_m: 3.0,
3424            dvx_mps: 0.1,
3425            dvy_mps: 0.2,
3426            dvz_mps: 0.3,
3427            azimuth_raw: 12345,
3428            elevation_raw: 250,
3429            reference_id: 7,
3430            corr_age_raw: 150,
3431            signal_info: 0,
3432        };
3433
3434        assert!((info.azimuth_deg().unwrap() - 123.45).abs() < 1e-2);
3435        assert!((info.elevation_deg().unwrap() - 2.5).abs() < 1e-6);
3436        assert!((info.corr_age_seconds().unwrap() - 1.5).abs() < 1e-6);
3437        assert!((info.dvx_mps().unwrap() - 0.1).abs() < 1e-6);
3438    }
3439
3440    #[test]
3441    fn test_base_vector_cart_dnu_handling() {
3442        let info = BaseVectorCartInfo {
3443            nr_sv: 0,
3444            error: 0,
3445            mode: 0,
3446            misc: 0,
3447            dx_m: F64_DNU,
3448            dy_m: 1.0,
3449            dz_m: 1.0,
3450            dvx_mps: F32_DNU,
3451            dvy_mps: 0.0,
3452            dvz_mps: 0.0,
3453            azimuth_raw: U16_DNU,
3454            elevation_raw: I16_DNU,
3455            reference_id: 0,
3456            corr_age_raw: U16_DNU,
3457            signal_info: 0,
3458        };
3459
3460        assert!(info.dx_m().is_none());
3461        assert!(info.dvx_mps().is_none());
3462        assert!(info.azimuth_deg().is_none());
3463        assert!(info.elevation_deg().is_none());
3464        assert!(info.corr_age_seconds().is_none());
3465    }
3466
3467    #[test]
3468    fn test_base_vector_cart_parse() {
3469        let mut data = vec![0u8; 14 + 52];
3470        data[12] = 1;
3471        data[13] = 52;
3472
3473        let offset = 14;
3474        data[offset] = 10;
3475        data[offset + 1] = 0;
3476        data[offset + 2] = 8;
3477        data[offset + 3] = 0;
3478
3479        let dx_m = 1.5_f64;
3480        let dy_m = 2.5_f64;
3481        let dz_m = 3.5_f64;
3482        let dvx_mps = 0.25_f32;
3483        let dvy_mps = 0.5_f32;
3484        let dvz_mps = 0.75_f32;
3485        let azimuth_raw = 9000_u16;
3486        let elevation_raw = 450_i16;
3487        let reference_id = 22_u16;
3488        let corr_age_raw = 80_u16;
3489        let signal_info = 0x87654321_u32;
3490
3491        data[offset + 4..offset + 12].copy_from_slice(&dx_m.to_le_bytes());
3492        data[offset + 12..offset + 20].copy_from_slice(&dy_m.to_le_bytes());
3493        data[offset + 20..offset + 28].copy_from_slice(&dz_m.to_le_bytes());
3494        data[offset + 28..offset + 32].copy_from_slice(&dvx_mps.to_le_bytes());
3495        data[offset + 32..offset + 36].copy_from_slice(&dvy_mps.to_le_bytes());
3496        data[offset + 36..offset + 40].copy_from_slice(&dvz_mps.to_le_bytes());
3497        data[offset + 40..offset + 42].copy_from_slice(&azimuth_raw.to_le_bytes());
3498        data[offset + 42..offset + 44].copy_from_slice(&elevation_raw.to_le_bytes());
3499        data[offset + 44..offset + 46].copy_from_slice(&reference_id.to_le_bytes());
3500        data[offset + 46..offset + 48].copy_from_slice(&corr_age_raw.to_le_bytes());
3501        data[offset + 48..offset + 52].copy_from_slice(&signal_info.to_le_bytes());
3502
3503        let header = header_for(block_ids::BASE_VECTOR_CART, data.len(), 999, 7);
3504        let block = BaseVectorCartBlock::parse(&header, &data).unwrap();
3505
3506        assert_eq!(block.num_vectors(), 1);
3507        let info = &block.vectors[0];
3508        assert_eq!(info.nr_sv, 10);
3509        assert_eq!(info.reference_id, reference_id);
3510        assert!((info.dx_m().unwrap() - dx_m).abs() < 1e-6);
3511        assert!((info.azimuth_deg().unwrap() - 90.0).abs() < 1e-6);
3512        assert!((info.corr_age_seconds().unwrap() - 0.8).abs() < 1e-6);
3513    }
3514
3515    #[test]
3516    fn test_base_vector_geod_scaled_accessors() {
3517        let info = BaseVectorGeodInfo {
3518            nr_sv: 8,
3519            error: 0,
3520            mode: 9,
3521            misc: 0,
3522            de_m: 1.0,
3523            dn_m: 2.0,
3524            du_m: 3.0,
3525            dve_mps: 0.1,
3526            dvn_mps: 0.2,
3527            dvu_mps: 0.3,
3528            azimuth_raw: 18000,
3529            elevation_raw: 100,
3530            reference_id: 9,
3531            corr_age_raw: 200,
3532            signal_info: 0,
3533        };
3534
3535        assert!((info.azimuth_deg().unwrap() - 180.0).abs() < 1e-6);
3536        assert!((info.elevation_deg().unwrap() - 1.0).abs() < 1e-6);
3537        assert!((info.corr_age_seconds().unwrap() - 2.0).abs() < 1e-6);
3538        assert!((info.dvn_mps().unwrap() - 0.2).abs() < 1e-6);
3539    }
3540
3541    #[test]
3542    fn test_base_vector_geod_dnu_handling() {
3543        let info = BaseVectorGeodInfo {
3544            nr_sv: 0,
3545            error: 0,
3546            mode: 0,
3547            misc: 0,
3548            de_m: F64_DNU,
3549            dn_m: 1.0,
3550            du_m: 1.0,
3551            dve_mps: F32_DNU,
3552            dvn_mps: 0.0,
3553            dvu_mps: 0.0,
3554            azimuth_raw: U16_DNU,
3555            elevation_raw: I16_DNU,
3556            reference_id: 0,
3557            corr_age_raw: U16_DNU,
3558            signal_info: 0,
3559        };
3560
3561        assert!(info.de_m().is_none());
3562        assert!(info.dve_mps().is_none());
3563        assert!(info.azimuth_deg().is_none());
3564        assert!(info.elevation_deg().is_none());
3565        assert!(info.corr_age_seconds().is_none());
3566    }
3567
3568    #[test]
3569    fn test_base_vector_geod_parse() {
3570        let mut data = vec![0u8; 14 + 52];
3571        data[12] = 1;
3572        data[13] = 52;
3573
3574        let offset = 14;
3575        data[offset] = 6;
3576        data[offset + 1] = 0;
3577        data[offset + 2] = 8;
3578        data[offset + 3] = 0;
3579
3580        let de_m = 4.5_f64;
3581        let dn_m = 5.5_f64;
3582        let du_m = 6.5_f64;
3583        let dve_mps = 0.15_f32;
3584        let dvn_mps = 0.25_f32;
3585        let dvu_mps = 0.35_f32;
3586        let azimuth_raw = 27000_u16;
3587        let elevation_raw = 300_i16;
3588        let reference_id = 33_u16;
3589        let corr_age_raw = 90_u16;
3590        let signal_info = 0x12340000_u32;
3591
3592        data[offset + 4..offset + 12].copy_from_slice(&de_m.to_le_bytes());
3593        data[offset + 12..offset + 20].copy_from_slice(&dn_m.to_le_bytes());
3594        data[offset + 20..offset + 28].copy_from_slice(&du_m.to_le_bytes());
3595        data[offset + 28..offset + 32].copy_from_slice(&dve_mps.to_le_bytes());
3596        data[offset + 32..offset + 36].copy_from_slice(&dvn_mps.to_le_bytes());
3597        data[offset + 36..offset + 40].copy_from_slice(&dvu_mps.to_le_bytes());
3598        data[offset + 40..offset + 42].copy_from_slice(&azimuth_raw.to_le_bytes());
3599        data[offset + 42..offset + 44].copy_from_slice(&elevation_raw.to_le_bytes());
3600        data[offset + 44..offset + 46].copy_from_slice(&reference_id.to_le_bytes());
3601        data[offset + 46..offset + 48].copy_from_slice(&corr_age_raw.to_le_bytes());
3602        data[offset + 48..offset + 52].copy_from_slice(&signal_info.to_le_bytes());
3603
3604        let header = header_for(block_ids::BASE_VECTOR_GEOD, data.len(), 777, 5);
3605        let block = BaseVectorGeodBlock::parse(&header, &data).unwrap();
3606
3607        assert_eq!(block.num_vectors(), 1);
3608        let info = &block.vectors[0];
3609        assert_eq!(info.nr_sv, 6);
3610        assert_eq!(info.reference_id, reference_id);
3611        assert!((info.de_m().unwrap() - de_m).abs() < 1e-6);
3612        assert!((info.azimuth_deg().unwrap() - 270.0).abs() < 1e-6);
3613        assert!((info.corr_age_seconds().unwrap() - 0.9).abs() < 1e-6);
3614    }
3615
3616    #[test]
3617    fn test_geo_corrections_accessors() {
3618        let corr = GeoCorrectionsSatCorr {
3619            svid: 131,
3620            iode: 5,
3621            prc_m: 2.5,
3622            corr_age_fc_s: 1.2,
3623            delta_x_m: 0.1,
3624            delta_y_m: 0.2,
3625            delta_z_m: 0.3,
3626            delta_clock_m: 0.01,
3627            corr_age_lt_s: 120.0,
3628            iono_pp_lat_rad: 0.5,
3629            iono_pp_lon_rad: -0.3,
3630            slant_iono_m: 0.8,
3631            corr_age_iono_s: 60.0,
3632            var_flt_m2: 0.25,
3633            var_uire_m2: 0.5,
3634            var_air_m2: 1.0,
3635            var_tropo_m2: 0.1,
3636        };
3637        let block = GeoCorrectionsBlock {
3638            tow_ms: 5000,
3639            wnc: 2100,
3640            sat_corrections: vec![corr],
3641        };
3642
3643        assert!((block.tow_seconds() - 5.0).abs() < 1e-6);
3644        assert_eq!(block.num_satellites(), 1);
3645        let c = &block.sat_corrections[0];
3646        assert_eq!(c.svid, 131);
3647        assert!((c.prc_m().unwrap() - 2.5).abs() < 1e-6);
3648        assert!((c.corr_age_fc_seconds().unwrap() - 1.2).abs() < 1e-6);
3649        assert!((c.delta_x_m().unwrap() - 0.1).abs() < 1e-6);
3650        assert!((c.slant_iono_m().unwrap() - 0.8).abs() < 1e-6);
3651        assert!((c.var_flt_m2().unwrap() - 0.25).abs() < 1e-6);
3652    }
3653
3654    #[test]
3655    fn test_geo_corrections_dnu_handling() {
3656        let corr = GeoCorrectionsSatCorr {
3657            svid: 0,
3658            iode: 0,
3659            prc_m: F32_DNU,
3660            corr_age_fc_s: F32_DNU,
3661            delta_x_m: 0.0,
3662            delta_y_m: F32_DNU,
3663            delta_z_m: 0.0,
3664            delta_clock_m: F32_DNU,
3665            corr_age_lt_s: F32_DNU,
3666            iono_pp_lat_rad: F32_DNU,
3667            iono_pp_lon_rad: 0.0,
3668            slant_iono_m: F32_DNU,
3669            corr_age_iono_s: F32_DNU,
3670            var_flt_m2: F32_DNU,
3671            var_uire_m2: -1.0,
3672            var_air_m2: 0.0,
3673            var_tropo_m2: F32_DNU,
3674        };
3675
3676        assert!(corr.prc_m().is_none());
3677        assert!(corr.corr_age_fc_seconds().is_none());
3678        assert!(corr.delta_y_m().is_none());
3679        assert!(corr.var_flt_m2().is_none());
3680        assert!(corr.var_uire_m2().is_none());
3681    }
3682
3683    #[test]
3684    fn test_geo_corrections_parse() {
3685        let sb_len = 62usize;
3686        let mut data = vec![0u8; 14 + sb_len];
3687        data[12] = 1;
3688        data[13] = sb_len as u8;
3689
3690        let offset = 14;
3691        data[offset] = 132;
3692        data[offset + 1] = 7;
3693        let prc = 3.1_f32;
3694        let delta_x = 0.5_f32;
3695        data[offset + 2..offset + 6].copy_from_slice(&prc.to_le_bytes());
3696        data[offset + 10..offset + 14].copy_from_slice(&delta_x.to_le_bytes());
3697
3698        let header = header_for(block_ids::GEO_CORRECTIONS, data.len(), 88888, 2200);
3699        let block = GeoCorrectionsBlock::parse(&header, &data).unwrap();
3700
3701        assert_eq!(block.tow_ms(), 88888);
3702        assert_eq!(block.wnc(), 2200);
3703        assert_eq!(block.num_satellites(), 1);
3704        let c = &block.sat_corrections[0];
3705        assert_eq!(c.svid, 132);
3706        assert_eq!(c.iode, 7);
3707        assert!((c.prc_m().unwrap() - prc).abs() < 1e-6);
3708        assert!((c.delta_x_m().unwrap() - delta_x).abs() < 1e-6);
3709    }
3710
3711    #[test]
3712    fn test_base_station_accessors() {
3713        let block = BaseStationBlock {
3714            tow_ms: 10000,
3715            wnc: 2300,
3716            base_station_id: 42,
3717            base_type: 1,
3718            source: 2,
3719            datum: 0,
3720            x_m: 4e6,
3721            y_m: 3e6,
3722            z_m: -5e6,
3723        };
3724
3725        assert!((block.tow_seconds() - 10.0).abs() < 1e-6);
3726        assert_eq!(block.base_station_id, 42);
3727        assert!((block.x_m().unwrap() - 4e6).abs() < 1.0);
3728        assert!((block.y_m().unwrap() - 3e6).abs() < 1.0);
3729        assert!((block.z_m().unwrap() - (-5e6)).abs() < 1.0);
3730    }
3731
3732    #[test]
3733    fn test_base_station_dnu_handling() {
3734        let block = BaseStationBlock {
3735            tow_ms: 0,
3736            wnc: 0,
3737            base_station_id: 0,
3738            base_type: 0,
3739            source: 0,
3740            datum: 0,
3741            x_m: F64_DNU,
3742            y_m: 1.0,
3743            z_m: F64_DNU,
3744        };
3745
3746        assert!(block.x_m().is_none());
3747        assert!(block.y_m().is_some());
3748        assert!(block.z_m().is_none());
3749    }
3750
3751    #[test]
3752    fn test_base_station_parse() {
3753        let mut data = vec![0u8; 42];
3754        let base_id = 100_u16;
3755        let x_m = 4.0e6_f64;
3756        let y_m = 3.0e6_f64;
3757        let z_m = -5.5e6_f64;
3758
3759        data[12..14].copy_from_slice(&base_id.to_le_bytes());
3760        data[14] = 2;
3761        data[15] = 1;
3762        data[16] = 0;
3763        data[17] = 0; // Reserved
3764        data[18..26].copy_from_slice(&x_m.to_le_bytes());
3765        data[26..34].copy_from_slice(&y_m.to_le_bytes());
3766        data[34..42].copy_from_slice(&z_m.to_le_bytes());
3767
3768        let header = header_for(block_ids::BASE_STATION, data.len(), 123456, 2345);
3769        let block = BaseStationBlock::parse(&header, &data).unwrap();
3770
3771        assert_eq!(block.tow_ms(), 123456);
3772        assert_eq!(block.wnc(), 2345);
3773        assert_eq!(block.base_station_id, base_id);
3774        assert_eq!(block.base_type, 2);
3775        assert!((block.x_m().unwrap() - x_m).abs() < 0.01);
3776        assert!((block.y_m().unwrap() - y_m).abs() < 0.01);
3777        assert!((block.z_m().unwrap() - z_m).abs() < 0.01);
3778    }
3779
3780    #[test]
3781    fn test_pvt_support_parse_and_accessors() {
3782        let data = vec![0u8; 12];
3783        let header = header_for(block_ids::PVT_SUPPORT, data.len(), 5000, 2100);
3784        let block = PvtSupportBlock::parse(&header, &data).unwrap();
3785
3786        assert_eq!(block.tow_ms(), 5000);
3787        assert_eq!(block.wnc(), 2100);
3788        assert!((block.tow_seconds() - 5.0).abs() < 1e-6);
3789    }
3790
3791    #[test]
3792    fn test_pvt_support_too_short() {
3793        let data = vec![0u8; 8];
3794        let header = header_for(block_ids::PVT_SUPPORT, data.len(), 0, 0);
3795        let result = PvtSupportBlock::parse(&header, &data);
3796        assert!(result.is_err());
3797    }
3798}