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