Skip to main content

sbf_tools/blocks/
navigation.rs

1//! Navigation message blocks (GPSNav, GALNav, GLONav)
2//!
3//! These blocks contain decoded ephemeris data for satellite orbit computation.
4
5use crate::error::{SbfError, SbfResult};
6use crate::header::SbfHeader;
7
8use super::block_ids;
9use super::dnu::{f32_or_none, f64_or_none};
10use super::SbfBlockParse;
11
12#[cfg(test)]
13use super::dnu::{F32_DNU, F64_DNU};
14
15// ============================================================================
16// GPSNav Block
17// ============================================================================
18
19/// GPSNav block (Block ID 5891)
20///
21/// Decoded GPS navigation message (ephemeris).
22#[derive(Debug, Clone)]
23pub struct GpsNavBlock {
24    tow_ms: u32,
25    wnc: u16,
26    /// PRN number (1-32)
27    pub prn: u8,
28    /// GPS week number from ephemeris
29    pub wn: u16,
30    /// C/A or P code on L2
31    pub ca_or_p_on_l2: u8,
32    /// User Range Accuracy index
33    pub ura: u8,
34    /// Satellite health
35    pub health: u8,
36    /// L2 data flag
37    pub l2_data_flag: u8,
38    /// Issue of Data Clock
39    pub iodc: u16,
40    /// Issue of Data Ephemeris (subframe 2)
41    pub iode2: u8,
42    /// Issue of Data Ephemeris (subframe 3)
43    pub iode3: u8,
44    /// Fit interval flag
45    pub fit_int_flag: u8,
46    /// Group delay (seconds)
47    pub t_gd: f32,
48    /// Clock reference time (seconds)
49    pub t_oc: u32,
50    /// Clock drift rate (s/s^2)
51    pub a_f2: f32,
52    /// Clock drift (s/s)
53    pub a_f1: f32,
54    /// Clock bias (s)
55    pub a_f0: f32,
56    /// Sine harmonic radius correction (m)
57    pub c_rs: f32,
58    /// Mean motion difference (rad/s)
59    pub delta_n: f32,
60    /// Mean anomaly at reference time (rad)
61    pub m_0: f64,
62    /// Cosine harmonic latitude correction (rad)
63    pub c_uc: f32,
64    /// Eccentricity
65    pub e: f64,
66    /// Sine harmonic latitude correction (rad)
67    pub c_us: f32,
68    /// Square root of semi-major axis (m^0.5)
69    pub sqrt_a: f64,
70    /// Ephemeris reference time (seconds)
71    pub t_oe: u32,
72    /// Cosine harmonic inclination correction (rad)
73    pub c_ic: f32,
74    /// Right ascension at reference time (rad)
75    pub omega_0: f64,
76    /// Sine harmonic inclination correction (rad)
77    pub c_is: f32,
78    /// Inclination angle at reference time (rad)
79    pub i_0: f64,
80    /// Cosine harmonic radius correction (m)
81    pub c_rc: f32,
82    /// Argument of perigee (rad)
83    pub omega: f64,
84    /// Rate of right ascension (rad/s)
85    pub omega_dot: f32,
86    /// Rate of inclination (rad/s)
87    pub i_dot: f32,
88    /// Week number of t_oc
89    pub wnt_oc: u16,
90    /// Week number of t_oe
91    pub wnt_oe: u16,
92}
93
94impl GpsNavBlock {
95    pub fn tow_seconds(&self) -> f64 {
96        self.tow_ms as f64 * 0.001
97    }
98    pub fn tow_ms(&self) -> u32 {
99        self.tow_ms
100    }
101    pub fn wnc(&self) -> u16 {
102        self.wnc
103    }
104
105    /// Check if ephemeris is healthy
106    pub fn is_healthy(&self) -> bool {
107        self.health == 0
108    }
109
110    /// Get semi-major axis in meters
111    pub fn semi_major_axis_m(&self) -> f64 {
112        self.sqrt_a * self.sqrt_a
113    }
114
115    /// Check if IODE values are consistent
116    pub fn iode_consistent(&self) -> bool {
117        self.iode2 == self.iode3
118    }
119}
120
121impl SbfBlockParse for GpsNavBlock {
122    const BLOCK_ID: u16 = block_ids::GPS_NAV;
123
124    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
125        if data.len() < 140 {
126            return Err(SbfError::ParseError("GPSNav too short".into()));
127        }
128
129        // Offsets from data start (after sync):
130        // 12: PRN
131        // 13: Reserved
132        // 14-15: WN
133        // 16: CAorPonL2
134        // 17: URA
135        // 18: health
136        // 19: L2DataFlag
137        // 20-21: IODC
138        // 22: IODE2
139        // 23: IODE3
140        // 24: FitIntFlg
141        // 25: Reserved
142        // 26-29: T_gd (f32)
143        // 30-33: T_oc (u32)
144        // 34-37: A_f2 (f32)
145        // 38-41: A_f1 (f32)
146        // 42-45: A_f0 (f32)
147        // 46-49: C_rs (f32)
148        // 50-53: DELTA_N (f32)
149        // 54-61: M_0 (f64)
150        // 62-65: C_uc (f32)
151        // 66-73: E (f64)
152        // 74-77: C_us (f32)
153        // 78-85: SQRT_A (f64)
154        // 86-89: T_oe (u32)
155        // 90-93: C_ic (f32)
156        // 94-101: OMEGA_0 (f64)
157        // 102-105: C_is (f32)
158        // 106-113: I_0 (f64)
159        // 114-117: C_rc (f32)
160        // 118-125: omega (f64)
161        // 126-129: OMEGADOT (f32)
162        // 130-133: IDOT (f32)
163        // 134-135: WNt_oc
164        // 136-137: WNt_oe
165
166        let prn = data[12];
167        let wn = u16::from_le_bytes([data[14], data[15]]);
168        let ca_or_p_on_l2 = data[16];
169        let ura = data[17];
170        let health = data[18];
171        let l2_data_flag = data[19];
172        let iodc = u16::from_le_bytes([data[20], data[21]]);
173        let iode2 = data[22];
174        let iode3 = data[23];
175        let fit_int_flag = data[24];
176
177        let t_gd = f32::from_le_bytes(data[26..30].try_into().unwrap());
178        let t_oc = u32::from_le_bytes(data[30..34].try_into().unwrap());
179        let a_f2 = f32::from_le_bytes(data[34..38].try_into().unwrap());
180        let a_f1 = f32::from_le_bytes(data[38..42].try_into().unwrap());
181        let a_f0 = f32::from_le_bytes(data[42..46].try_into().unwrap());
182        let c_rs = f32::from_le_bytes(data[46..50].try_into().unwrap());
183        let delta_n = f32::from_le_bytes(data[50..54].try_into().unwrap());
184        let m_0 = f64::from_le_bytes(data[54..62].try_into().unwrap());
185        let c_uc = f32::from_le_bytes(data[62..66].try_into().unwrap());
186        let e = f64::from_le_bytes(data[66..74].try_into().unwrap());
187        let c_us = f32::from_le_bytes(data[74..78].try_into().unwrap());
188        let sqrt_a = f64::from_le_bytes(data[78..86].try_into().unwrap());
189        let t_oe = u32::from_le_bytes(data[86..90].try_into().unwrap());
190        let c_ic = f32::from_le_bytes(data[90..94].try_into().unwrap());
191        let omega_0 = f64::from_le_bytes(data[94..102].try_into().unwrap());
192        let c_is = f32::from_le_bytes(data[102..106].try_into().unwrap());
193        let i_0 = f64::from_le_bytes(data[106..114].try_into().unwrap());
194        let c_rc = f32::from_le_bytes(data[114..118].try_into().unwrap());
195        let omega = f64::from_le_bytes(data[118..126].try_into().unwrap());
196        let omega_dot = f32::from_le_bytes(data[126..130].try_into().unwrap());
197        let i_dot = f32::from_le_bytes(data[130..134].try_into().unwrap());
198        let wnt_oc = u16::from_le_bytes([data[134], data[135]]);
199        let wnt_oe = u16::from_le_bytes([data[136], data[137]]);
200
201        Ok(Self {
202            tow_ms: header.tow_ms,
203            wnc: header.wnc,
204            prn,
205            wn,
206            ca_or_p_on_l2,
207            ura,
208            health,
209            l2_data_flag,
210            iodc,
211            iode2,
212            iode3,
213            fit_int_flag,
214            t_gd,
215            t_oc,
216            a_f2,
217            a_f1,
218            a_f0,
219            c_rs,
220            delta_n,
221            m_0,
222            c_uc,
223            e,
224            c_us,
225            sqrt_a,
226            t_oe,
227            c_ic,
228            omega_0,
229            c_is,
230            i_0,
231            c_rc,
232            omega,
233            omega_dot,
234            i_dot,
235            wnt_oc,
236            wnt_oe,
237        })
238    }
239}
240
241// ============================================================================
242// GALNav Block
243// ============================================================================
244
245/// GALNav block (Block ID 4002)
246///
247/// Decoded Galileo navigation message (ephemeris).
248#[derive(Debug, Clone)]
249pub struct GalNavBlock {
250    tow_ms: u32,
251    wnc: u16,
252    /// SVID (71-106 for Galileo)
253    pub svid: u8,
254    /// Signal source (F/NAV or I/NAV)
255    pub source: u8,
256    /// Square root of semi-major axis (m^0.5)
257    pub sqrt_a: f64,
258    /// Mean anomaly at reference time (rad)
259    pub m_0: f64,
260    /// Eccentricity
261    pub e: f64,
262    /// Inclination at reference time (rad)
263    pub i_0: f64,
264    /// Argument of perigee (rad)
265    pub omega: f64,
266    /// Right ascension at reference time (rad)
267    pub omega_0: f64,
268    /// Rate of right ascension (rad/s)
269    pub omega_dot: f32,
270    /// Rate of inclination (rad/s)
271    pub i_dot: f32,
272    /// Mean motion difference (rad/s)
273    pub delta_n: f32,
274    /// Cosine harmonic latitude correction (rad)
275    pub c_uc: f32,
276    /// Sine harmonic latitude correction (rad)
277    pub c_us: f32,
278    /// Cosine harmonic radius correction (m)
279    pub c_rc: f32,
280    /// Sine harmonic radius correction (m)
281    pub c_rs: f32,
282    /// Cosine harmonic inclination correction (rad)
283    pub c_ic: f32,
284    /// Sine harmonic inclination correction (rad)
285    pub c_is: f32,
286    /// Ephemeris reference time (seconds)
287    pub t_oe: u32,
288    /// Clock reference time (seconds)
289    pub t_oc: u32,
290    /// Clock drift rate (s/s^2)
291    pub a_f2: f32,
292    /// Clock drift (s/s)
293    pub a_f1: f32,
294    /// Clock bias (s)
295    pub a_f0: f64,
296    /// Week number of t_oc
297    pub wnt_oc: u16,
298    /// Week number of t_oe
299    pub wnt_oe: u16,
300    /// Issue of Data Navigation
301    pub iod_nav: u16,
302    /// Health/OS/SOL flags
303    pub health_os_sol: u16,
304    /// Signal-In-Space Accuracy (L1/E5a)
305    pub sisa_l1e5a: u8,
306    /// Signal-In-Space Accuracy (L1/E5b)
307    pub sisa_l1e5b: u8,
308    /// Broadcast Group Delay (L1/E5a)
309    pub bgd_l1e5a: f32,
310    /// Broadcast Group Delay (L1/E5b)
311    pub bgd_l1e5b: f32,
312}
313
314impl GalNavBlock {
315    pub fn tow_seconds(&self) -> f64 {
316        self.tow_ms as f64 * 0.001
317    }
318    pub fn tow_ms(&self) -> u32 {
319        self.tow_ms
320    }
321    pub fn wnc(&self) -> u16 {
322        self.wnc
323    }
324
325    /// Get PRN number (1-36)
326    pub fn prn(&self) -> u8 {
327        if self.svid >= 71 && self.svid <= 106 {
328            self.svid - 70
329        } else {
330            self.svid
331        }
332    }
333
334    /// Check if from F/NAV
335    pub fn is_fnav(&self) -> bool {
336        self.source == 16
337    }
338
339    /// Check if from I/NAV
340    pub fn is_inav(&self) -> bool {
341        self.source == 2
342    }
343
344    /// Get semi-major axis in meters
345    pub fn semi_major_axis_m(&self) -> f64 {
346        self.sqrt_a * self.sqrt_a
347    }
348}
349
350impl SbfBlockParse for GalNavBlock {
351    const BLOCK_ID: u16 = block_ids::GAL_NAV;
352
353    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
354        if data.len() < 144 {
355            return Err(SbfError::ParseError("GALNav too short".into()));
356        }
357
358        let svid = data[12];
359        let source = data[13];
360
361        let sqrt_a = f64::from_le_bytes(data[14..22].try_into().unwrap());
362        let m_0 = f64::from_le_bytes(data[22..30].try_into().unwrap());
363        let e = f64::from_le_bytes(data[30..38].try_into().unwrap());
364        let i_0 = f64::from_le_bytes(data[38..46].try_into().unwrap());
365        let omega = f64::from_le_bytes(data[46..54].try_into().unwrap());
366        let omega_0 = f64::from_le_bytes(data[54..62].try_into().unwrap());
367        let omega_dot = f32::from_le_bytes(data[62..66].try_into().unwrap());
368        let i_dot = f32::from_le_bytes(data[66..70].try_into().unwrap());
369        let delta_n = f32::from_le_bytes(data[70..74].try_into().unwrap());
370        let c_uc = f32::from_le_bytes(data[74..78].try_into().unwrap());
371        let c_us = f32::from_le_bytes(data[78..82].try_into().unwrap());
372        let c_rc = f32::from_le_bytes(data[82..86].try_into().unwrap());
373        let c_rs = f32::from_le_bytes(data[86..90].try_into().unwrap());
374        let c_ic = f32::from_le_bytes(data[90..94].try_into().unwrap());
375        let c_is = f32::from_le_bytes(data[94..98].try_into().unwrap());
376        let t_oe = u32::from_le_bytes(data[98..102].try_into().unwrap());
377        let t_oc = u32::from_le_bytes(data[102..106].try_into().unwrap());
378        let a_f2 = f32::from_le_bytes(data[106..110].try_into().unwrap());
379        let a_f1 = f32::from_le_bytes(data[110..114].try_into().unwrap());
380        let a_f0 = f64::from_le_bytes(data[114..122].try_into().unwrap());
381        let wnt_oc = u16::from_le_bytes([data[122], data[123]]);
382        let wnt_oe = u16::from_le_bytes([data[124], data[125]]);
383        let iod_nav = u16::from_le_bytes([data[126], data[127]]);
384        let health_os_sol = u16::from_le_bytes([data[128], data[129]]);
385        let sisa_l1e5a = data[130];
386        let sisa_l1e5b = data[131];
387        let bgd_l1e5a = f32::from_le_bytes(data[132..136].try_into().unwrap());
388        let bgd_l1e5b = f32::from_le_bytes(data[136..140].try_into().unwrap());
389
390        Ok(Self {
391            tow_ms: header.tow_ms,
392            wnc: header.wnc,
393            svid,
394            source,
395            sqrt_a,
396            m_0,
397            e,
398            i_0,
399            omega,
400            omega_0,
401            omega_dot,
402            i_dot,
403            delta_n,
404            c_uc,
405            c_us,
406            c_rc,
407            c_rs,
408            c_ic,
409            c_is,
410            t_oe,
411            t_oc,
412            a_f2,
413            a_f1,
414            a_f0,
415            wnt_oc,
416            wnt_oe,
417            iod_nav,
418            health_os_sol,
419            sisa_l1e5a,
420            sisa_l1e5b,
421            bgd_l1e5a,
422            bgd_l1e5b,
423        })
424    }
425}
426
427// ============================================================================
428// GLONav Block
429// ============================================================================
430
431/// GLONav block (Block ID 4004)
432///
433/// Decoded GLONASS navigation message (ephemeris).
434/// GLONASS uses a different ephemeris model based on position/velocity/acceleration.
435#[derive(Debug, Clone)]
436pub struct GloNavBlock {
437    tow_ms: u32,
438    wnc: u16,
439    /// SVID (38-61 for GLONASS)
440    pub svid: u8,
441    /// Frequency number (-7 to +6)
442    pub freq_nr: i8,
443    /// X position (km)
444    pub x_km: f64,
445    /// Y position (km)
446    pub y_km: f64,
447    /// Z position (km)
448    pub z_km: f64,
449    /// X velocity (km/s)
450    pub dx_kmps: f32,
451    /// Y velocity (km/s)
452    pub dy_kmps: f32,
453    /// Z velocity (km/s)
454    pub dz_kmps: f32,
455    /// X acceleration (km/s^2)
456    pub ddx_kmps2: f32,
457    /// Y acceleration (km/s^2)
458    pub ddy_kmps2: f32,
459    /// Z acceleration (km/s^2)
460    pub ddz_kmps2: f32,
461    /// Frequency bias (gamma)
462    pub gamma: f32,
463    /// Clock bias (tau, seconds)
464    pub tau: f32,
465    /// Time difference L1-L2 (dtau, seconds)
466    pub dtau: f32,
467    /// Ephemeris reference time
468    pub t_oe: u32,
469    /// Ephemeris reference week
470    pub wn_toe: u16,
471    /// Time interval P1
472    pub p1: u8,
473    /// Odd/even flag P2
474    pub p2: u8,
475    /// Age of data E
476    pub e_age: u8,
477    /// Health flag B
478    pub b_health: u8,
479    /// Frame time tb (15-minute intervals)
480    pub tb: u16,
481    /// Satellite type M
482    pub m_type: u8,
483    /// P1/P2 mode P
484    pub p_mode: u8,
485    /// Health flag l
486    pub l_health: u8,
487    /// Data updated flag P4
488    pub p4: u8,
489    /// Day number N_T
490    pub n_t: u16,
491    /// Accuracy F_T
492    pub f_t: u16,
493}
494
495impl GloNavBlock {
496    pub fn tow_seconds(&self) -> f64 {
497        self.tow_ms as f64 * 0.001
498    }
499    pub fn tow_ms(&self) -> u32 {
500        self.tow_ms
501    }
502    pub fn wnc(&self) -> u16 {
503        self.wnc
504    }
505
506    /// Get slot number (1-24)
507    pub fn slot(&self) -> u8 {
508        if self.svid >= 38 && self.svid <= 61 {
509            self.svid - 37
510        } else {
511            self.svid
512        }
513    }
514
515    /// Get position in meters
516    pub fn position_m(&self) -> (f64, f64, f64) {
517        (self.x_km * 1000.0, self.y_km * 1000.0, self.z_km * 1000.0)
518    }
519
520    /// Get velocity in m/s
521    pub fn velocity_mps(&self) -> (f64, f64, f64) {
522        (
523            self.dx_kmps as f64 * 1000.0,
524            self.dy_kmps as f64 * 1000.0,
525            self.dz_kmps as f64 * 1000.0,
526        )
527    }
528
529    /// Get acceleration in m/s^2
530    pub fn acceleration_mps2(&self) -> (f64, f64, f64) {
531        (
532            self.ddx_kmps2 as f64 * 1000.0,
533            self.ddy_kmps2 as f64 * 1000.0,
534            self.ddz_kmps2 as f64 * 1000.0,
535        )
536    }
537
538    /// Check if satellite is healthy
539    pub fn is_healthy(&self) -> bool {
540        self.b_health == 0 && self.l_health == 0
541    }
542}
543
544impl SbfBlockParse for GloNavBlock {
545    const BLOCK_ID: u16 = block_ids::GLO_NAV;
546
547    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
548        if data.len() < 96 {
549            return Err(SbfError::ParseError("GLONav too short".into()));
550        }
551
552        let svid = data[12];
553        let freq_nr = data[13] as i8;
554
555        let x_km = f64::from_le_bytes(data[14..22].try_into().unwrap());
556        let y_km = f64::from_le_bytes(data[22..30].try_into().unwrap());
557        let z_km = f64::from_le_bytes(data[30..38].try_into().unwrap());
558        let dx_kmps = f32::from_le_bytes(data[38..42].try_into().unwrap());
559        let dy_kmps = f32::from_le_bytes(data[42..46].try_into().unwrap());
560        let dz_kmps = f32::from_le_bytes(data[46..50].try_into().unwrap());
561        let ddx_kmps2 = f32::from_le_bytes(data[50..54].try_into().unwrap());
562        let ddy_kmps2 = f32::from_le_bytes(data[54..58].try_into().unwrap());
563        let ddz_kmps2 = f32::from_le_bytes(data[58..62].try_into().unwrap());
564        let gamma = f32::from_le_bytes(data[62..66].try_into().unwrap());
565        let tau = f32::from_le_bytes(data[66..70].try_into().unwrap());
566        let dtau = f32::from_le_bytes(data[70..74].try_into().unwrap());
567        let t_oe = u32::from_le_bytes(data[74..78].try_into().unwrap());
568        let wn_toe = u16::from_le_bytes([data[78], data[79]]);
569        let p1 = data[80];
570        let p2 = data[81];
571        let e_age = data[82];
572        let b_health = data[83];
573        let tb = u16::from_le_bytes([data[84], data[85]]);
574        let m_type = data[86];
575        let p_mode = data[87];
576        let l_health = data[88];
577        let p4 = data[89];
578        let n_t = u16::from_le_bytes([data[90], data[91]]);
579        let f_t = u16::from_le_bytes([data[92], data[93]]);
580
581        Ok(Self {
582            tow_ms: header.tow_ms,
583            wnc: header.wnc,
584            svid,
585            freq_nr,
586            x_km,
587            y_km,
588            z_km,
589            dx_kmps,
590            dy_kmps,
591            dz_kmps,
592            ddx_kmps2,
593            ddy_kmps2,
594            ddz_kmps2,
595            gamma,
596            tau,
597            dtau,
598            t_oe,
599            wn_toe,
600            p1,
601            p2,
602            e_age,
603            b_health,
604            tb,
605            m_type,
606            p_mode,
607            l_health,
608            p4,
609            n_t,
610            f_t,
611        })
612    }
613}
614
615// ============================================================================
616// GPSAlm Block
617// ============================================================================
618
619/// GPSAlm block (Block ID 5892)
620///
621/// Decoded GPS almanac data.
622#[derive(Debug, Clone)]
623pub struct GpsAlmBlock {
624    tow_ms: u32,
625    wnc: u16,
626    /// PRN number (1-32)
627    pub prn: u8,
628    /// Eccentricity
629    pub e: f32,
630    /// Almanac reference time (s)
631    pub t_oa: u32,
632    /// Inclination offset (rad)
633    pub delta_i: f32,
634    /// Rate of right ascension (rad/s)
635    pub omega_dot: f32,
636    /// Square root of semi-major axis (m^0.5)
637    pub sqrt_a: f32,
638    /// Right ascension (rad)
639    pub omega_0: f32,
640    /// Argument of perigee (rad)
641    pub omega: f32,
642    /// Mean anomaly (rad)
643    pub m_0: f32,
644    /// Clock drift (s/s)
645    pub a_f1: f32,
646    /// Clock bias (s)
647    pub a_f0: f32,
648    /// Almanac week
649    pub wn_a: u8,
650    /// Anti-spoofing config
651    pub as_config: u8,
652    /// Health (8-bit)
653    pub health8: u8,
654    /// Health (6-bit)
655    pub health6: u8,
656}
657
658impl GpsAlmBlock {
659    pub fn tow_seconds(&self) -> f64 {
660        self.tow_ms as f64 * 0.001
661    }
662    pub fn tow_ms(&self) -> u32 {
663        self.tow_ms
664    }
665    pub fn wnc(&self) -> u16 {
666        self.wnc
667    }
668
669    /// Eccentricity (None if DNU)
670    pub fn eccentricity(&self) -> Option<f32> {
671        f32_or_none(self.e)
672    }
673
674    /// Semi-major axis in meters (None if DNU)
675    pub fn semi_major_axis_m(&self) -> Option<f32> {
676        f32_or_none(self.sqrt_a).map(|value| value * value)
677    }
678
679    /// Clock bias in seconds (None if DNU)
680    pub fn clock_bias_s(&self) -> Option<f32> {
681        f32_or_none(self.a_f0)
682    }
683}
684
685impl SbfBlockParse for GpsAlmBlock {
686    const BLOCK_ID: u16 = block_ids::GPS_ALM;
687
688    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
689        if data.len() < 57 {
690            return Err(SbfError::ParseError("GPSAlm too short".into()));
691        }
692
693        let prn = data[12];
694        let e = f32::from_le_bytes(data[13..17].try_into().unwrap());
695        let t_oa = u32::from_le_bytes(data[17..21].try_into().unwrap());
696        let delta_i = f32::from_le_bytes(data[21..25].try_into().unwrap());
697        let omega_dot = f32::from_le_bytes(data[25..29].try_into().unwrap());
698        let sqrt_a = f32::from_le_bytes(data[29..33].try_into().unwrap());
699        let omega_0 = f32::from_le_bytes(data[33..37].try_into().unwrap());
700        let omega = f32::from_le_bytes(data[37..41].try_into().unwrap());
701        let m_0 = f32::from_le_bytes(data[41..45].try_into().unwrap());
702        let a_f1 = f32::from_le_bytes(data[45..49].try_into().unwrap());
703        let a_f0 = f32::from_le_bytes(data[49..53].try_into().unwrap());
704        let wn_a = data[53];
705        let as_config = data[54];
706        let health8 = data[55];
707        let health6 = data[56];
708
709        Ok(Self {
710            tow_ms: header.tow_ms,
711            wnc: header.wnc,
712            prn,
713            e,
714            t_oa,
715            delta_i,
716            omega_dot,
717            sqrt_a,
718            omega_0,
719            omega,
720            m_0,
721            a_f1,
722            a_f0,
723            wn_a,
724            as_config,
725            health8,
726            health6,
727        })
728    }
729}
730
731// ============================================================================
732// GPSIon Block
733// ============================================================================
734
735/// GPSIon block (Block ID 5893)
736///
737/// GPS ionosphere parameters.
738#[derive(Debug, Clone)]
739pub struct GpsIonBlock {
740    tow_ms: u32,
741    wnc: u16,
742    /// PRN number (1-32)
743    pub prn: u8,
744    /// Ionosphere alpha parameter (s)
745    pub alpha_0: f32,
746    /// Ionosphere alpha parameter (s/semi-circle)
747    pub alpha_1: f32,
748    /// Ionosphere alpha parameter (s/semi-circle^2)
749    pub alpha_2: f32,
750    /// Ionosphere alpha parameter (s/semi-circle^3)
751    pub alpha_3: f32,
752    /// Ionosphere beta parameter (s)
753    pub beta_0: f32,
754    /// Ionosphere beta parameter (s/semi-circle)
755    pub beta_1: f32,
756    /// Ionosphere beta parameter (s/semi-circle^2)
757    pub beta_2: f32,
758    /// Ionosphere beta parameter (s/semi-circle^3)
759    pub beta_3: f32,
760}
761
762impl GpsIonBlock {
763    pub fn tow_seconds(&self) -> f64 {
764        self.tow_ms as f64 * 0.001
765    }
766    pub fn tow_ms(&self) -> u32 {
767        self.tow_ms
768    }
769    pub fn wnc(&self) -> u16 {
770        self.wnc
771    }
772
773    pub fn alpha_0(&self) -> Option<f32> {
774        f32_or_none(self.alpha_0)
775    }
776
777    pub fn beta_0(&self) -> Option<f32> {
778        f32_or_none(self.beta_0)
779    }
780}
781
782impl SbfBlockParse for GpsIonBlock {
783    const BLOCK_ID: u16 = block_ids::GPS_ION;
784
785    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
786        if data.len() < 45 {
787            return Err(SbfError::ParseError("GPSIon too short".into()));
788        }
789
790        let prn = data[12];
791        let alpha_0 = f32::from_le_bytes(data[13..17].try_into().unwrap());
792        let alpha_1 = f32::from_le_bytes(data[17..21].try_into().unwrap());
793        let alpha_2 = f32::from_le_bytes(data[21..25].try_into().unwrap());
794        let alpha_3 = f32::from_le_bytes(data[25..29].try_into().unwrap());
795        let beta_0 = f32::from_le_bytes(data[29..33].try_into().unwrap());
796        let beta_1 = f32::from_le_bytes(data[33..37].try_into().unwrap());
797        let beta_2 = f32::from_le_bytes(data[37..41].try_into().unwrap());
798        let beta_3 = f32::from_le_bytes(data[41..45].try_into().unwrap());
799
800        Ok(Self {
801            tow_ms: header.tow_ms,
802            wnc: header.wnc,
803            prn,
804            alpha_0,
805            alpha_1,
806            alpha_2,
807            alpha_3,
808            beta_0,
809            beta_1,
810            beta_2,
811            beta_3,
812        })
813    }
814}
815
816// ============================================================================
817// GPSUtc Block
818// ============================================================================
819
820/// GPSUtc block (Block ID 5894)
821///
822/// GPS to UTC conversion parameters.
823#[derive(Debug, Clone)]
824pub struct GpsUtcBlock {
825    tow_ms: u32,
826    wnc: u16,
827    /// PRN number (1-32)
828    pub prn: u8,
829    /// UTC drift (s/s)
830    pub a_1: f32,
831    /// UTC bias (s)
832    pub a_0: f64,
833    /// Reference time (s)
834    pub t_ot: u32,
835    /// Reference week
836    pub wn_t: u8,
837    /// Current leap seconds
838    pub delta_t_ls: i8,
839    /// Week of future leap second
840    pub wn_lsf: u8,
841    /// Day of future leap second
842    pub dn: u8,
843    /// Future leap seconds
844    pub delta_t_lsf: i8,
845}
846
847impl GpsUtcBlock {
848    pub fn tow_seconds(&self) -> f64 {
849        self.tow_ms as f64 * 0.001
850    }
851    pub fn tow_ms(&self) -> u32 {
852        self.tow_ms
853    }
854    pub fn wnc(&self) -> u16 {
855        self.wnc
856    }
857
858    pub fn utc_bias_s(&self) -> Option<f64> {
859        f64_or_none(self.a_0)
860    }
861
862    pub fn utc_drift_s_per_s(&self) -> Option<f32> {
863        f32_or_none(self.a_1)
864    }
865}
866
867impl SbfBlockParse for GpsUtcBlock {
868    const BLOCK_ID: u16 = block_ids::GPS_UTC;
869
870    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
871        if data.len() < 34 {
872            return Err(SbfError::ParseError("GPSUtc too short".into()));
873        }
874
875        let prn = data[12];
876        let a_1 = f32::from_le_bytes(data[13..17].try_into().unwrap());
877        let a_0 = f64::from_le_bytes(data[17..25].try_into().unwrap());
878        let t_ot = u32::from_le_bytes(data[25..29].try_into().unwrap());
879        let wn_t = data[29];
880        let delta_t_ls = data[30] as i8;
881        let wn_lsf = data[31];
882        let dn = data[32];
883        let delta_t_lsf = data[33] as i8;
884
885        Ok(Self {
886            tow_ms: header.tow_ms,
887            wnc: header.wnc,
888            prn,
889            a_1,
890            a_0,
891            t_ot,
892            wn_t,
893            delta_t_ls,
894            wn_lsf,
895            dn,
896            delta_t_lsf,
897        })
898    }
899}
900
901// ============================================================================
902// GLOAlm Block
903// ============================================================================
904
905/// GLOAlm block (Block ID 4005)
906///
907/// GLONASS almanac data.
908#[derive(Debug, Clone)]
909pub struct GloAlmBlock {
910    tow_ms: u32,
911    wnc: u16,
912    /// SVID (38-61 for GLONASS)
913    pub svid: u8,
914    /// Frequency number (-7 to +6)
915    pub freq_nr: i8,
916    /// Eccentricity
917    pub epsilon: f32,
918    /// Almanac reference time
919    pub t_oa: u32,
920    /// Inclination correction (rad)
921    pub delta_i: f32,
922    /// Longitude of ascending node (rad)
923    pub lambda: f32,
924    /// Time of ascending node (s)
925    pub t_ln: f32,
926    /// Argument of perigee (rad)
927    pub omega: f32,
928    /// Orbit period correction (s/orbit)
929    pub delta_t: f32,
930    /// Orbit period rate (s/orbit^2)
931    pub d_delta_t: f32,
932    /// Clock bias (s)
933    pub tau: f32,
934    /// Almanac week
935    pub wn_a: u8,
936    /// Health flag
937    pub c: u8,
938    /// Day number
939    pub n: u16,
940    /// Satellite type
941    pub m_type: u8,
942    /// 4-year interval
943    pub n_4: u8,
944}
945
946impl GloAlmBlock {
947    pub fn tow_seconds(&self) -> f64 {
948        self.tow_ms as f64 * 0.001
949    }
950    pub fn tow_ms(&self) -> u32 {
951        self.tow_ms
952    }
953    pub fn wnc(&self) -> u16 {
954        self.wnc
955    }
956
957    /// Get slot number (1-24)
958    pub fn slot(&self) -> u8 {
959        if self.svid >= 38 && self.svid <= 61 {
960            self.svid - 37
961        } else {
962            self.svid
963        }
964    }
965
966    pub fn eccentricity(&self) -> Option<f32> {
967        f32_or_none(self.epsilon)
968    }
969
970    pub fn clock_bias_s(&self) -> Option<f32> {
971        f32_or_none(self.tau)
972    }
973}
974
975impl SbfBlockParse for GloAlmBlock {
976    const BLOCK_ID: u16 = block_ids::GLO_ALM;
977
978    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
979        if data.len() < 56 {
980            return Err(SbfError::ParseError("GLOAlm too short".into()));
981        }
982
983        let svid = data[12];
984        let freq_nr = data[13] as i8;
985        let epsilon = f32::from_le_bytes(data[14..18].try_into().unwrap());
986        let t_oa = u32::from_le_bytes(data[18..22].try_into().unwrap());
987        let delta_i = f32::from_le_bytes(data[22..26].try_into().unwrap());
988        let lambda = f32::from_le_bytes(data[26..30].try_into().unwrap());
989        let t_ln = f32::from_le_bytes(data[30..34].try_into().unwrap());
990        let omega = f32::from_le_bytes(data[34..38].try_into().unwrap());
991        let delta_t = f32::from_le_bytes(data[38..42].try_into().unwrap());
992        let d_delta_t = f32::from_le_bytes(data[42..46].try_into().unwrap());
993        let tau = f32::from_le_bytes(data[46..50].try_into().unwrap());
994        let wn_a = data[50];
995        let c = data[51];
996        let n = u16::from_le_bytes([data[52], data[53]]);
997        let m_type = data[54];
998        let n_4 = data[55];
999
1000        Ok(Self {
1001            tow_ms: header.tow_ms,
1002            wnc: header.wnc,
1003            svid,
1004            freq_nr,
1005            epsilon,
1006            t_oa,
1007            delta_i,
1008            lambda,
1009            t_ln,
1010            omega,
1011            delta_t,
1012            d_delta_t,
1013            tau,
1014            wn_a,
1015            c,
1016            n,
1017            m_type,
1018            n_4,
1019        })
1020    }
1021}
1022
1023// ============================================================================
1024// GLOTime Block
1025// ============================================================================
1026
1027/// GLOTime block (Block ID 4036)
1028///
1029/// GLONASS time parameters.
1030#[derive(Debug, Clone)]
1031pub struct GloTimeBlock {
1032    tow_ms: u32,
1033    wnc: u16,
1034    /// SVID (38-61 for GLONASS)
1035    pub svid: u8,
1036    /// Frequency number (-7 to +6)
1037    pub freq_nr: i8,
1038    /// 4-year interval number
1039    pub n_4: u8,
1040    /// Notification of leap second
1041    pub kp: u8,
1042    /// Day number
1043    pub n: u16,
1044    /// GPS-GLONASS time offset (s)
1045    pub tau_gps: f32,
1046    /// GLONASS time scale correction (s)
1047    pub tau_c: f64,
1048    /// UT1-UTC coefficient
1049    pub b1: f32,
1050    /// UT1-UTC rate
1051    pub b2: f32,
1052}
1053
1054impl GloTimeBlock {
1055    pub fn tow_seconds(&self) -> f64 {
1056        self.tow_ms as f64 * 0.001
1057    }
1058    pub fn tow_ms(&self) -> u32 {
1059        self.tow_ms
1060    }
1061    pub fn wnc(&self) -> u16 {
1062        self.wnc
1063    }
1064
1065    /// Get slot number (1-24)
1066    pub fn slot(&self) -> u8 {
1067        if self.svid >= 38 && self.svid <= 61 {
1068            self.svid - 37
1069        } else {
1070            self.svid
1071        }
1072    }
1073
1074    pub fn gps_glonass_offset_s(&self) -> Option<f32> {
1075        f32_or_none(self.tau_gps)
1076    }
1077
1078    pub fn time_scale_correction_s(&self) -> Option<f64> {
1079        f64_or_none(self.tau_c)
1080    }
1081}
1082
1083impl SbfBlockParse for GloTimeBlock {
1084    const BLOCK_ID: u16 = block_ids::GLO_TIME;
1085
1086    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1087        if data.len() < 38 {
1088            return Err(SbfError::ParseError("GLOTime too short".into()));
1089        }
1090
1091        let svid = data[12];
1092        let freq_nr = data[13] as i8;
1093        let n_4 = data[14];
1094        let kp = data[15];
1095        let n = u16::from_le_bytes([data[16], data[17]]);
1096        let tau_gps = f32::from_le_bytes(data[18..22].try_into().unwrap());
1097        let tau_c = f64::from_le_bytes(data[22..30].try_into().unwrap());
1098        let b1 = f32::from_le_bytes(data[30..34].try_into().unwrap());
1099        let b2 = f32::from_le_bytes(data[34..38].try_into().unwrap());
1100
1101        Ok(Self {
1102            tow_ms: header.tow_ms,
1103            wnc: header.wnc,
1104            svid,
1105            freq_nr,
1106            n_4,
1107            kp,
1108            n,
1109            tau_gps,
1110            tau_c,
1111            b1,
1112            b2,
1113        })
1114    }
1115}
1116
1117// ============================================================================
1118// GALAlm Block
1119// ============================================================================
1120
1121/// GALAlm block (Block ID 4003)
1122///
1123/// Galileo almanac data.
1124#[derive(Debug, Clone)]
1125pub struct GalAlmBlock {
1126    tow_ms: u32,
1127    wnc: u16,
1128    /// SVID (71-106 for Galileo)
1129    pub svid: u8,
1130    /// Signal source (F/NAV or I/NAV)
1131    pub source: u8,
1132    /// Eccentricity
1133    pub e: f32,
1134    /// Almanac reference time (s)
1135    pub t_oa: u32,
1136    /// Inclination offset (rad)
1137    pub delta_i: f32,
1138    /// Rate of right ascension (rad/s)
1139    pub omega_dot: f32,
1140    /// Semi-major axis delta (m^0.5)
1141    pub delta_sqrt_a: f32,
1142    /// Right ascension (rad)
1143    pub omega_0: f32,
1144    /// Argument of perigee (rad)
1145    pub omega: f32,
1146    /// Mean anomaly (rad)
1147    pub m_0: f32,
1148    /// Clock drift (s/s)
1149    pub a_f1: f32,
1150    /// Clock bias (s)
1151    pub a_f0: f32,
1152    /// Almanac week
1153    pub wn_a: u8,
1154    /// Almanac SVID
1155    pub svid_a: u8,
1156    /// Health flags
1157    pub health: u16,
1158    /// Issue of Data Almanac
1159    pub ioda: u8,
1160}
1161
1162impl GalAlmBlock {
1163    pub fn tow_seconds(&self) -> f64 {
1164        self.tow_ms as f64 * 0.001
1165    }
1166    pub fn tow_ms(&self) -> u32 {
1167        self.tow_ms
1168    }
1169    pub fn wnc(&self) -> u16 {
1170        self.wnc
1171    }
1172
1173    /// Get PRN number (1-36)
1174    pub fn prn(&self) -> u8 {
1175        if self.svid >= 71 && self.svid <= 106 {
1176            self.svid - 70
1177        } else {
1178            self.svid
1179        }
1180    }
1181
1182    pub fn eccentricity(&self) -> Option<f32> {
1183        f32_or_none(self.e)
1184    }
1185
1186    pub fn delta_sqrt_a(&self) -> Option<f32> {
1187        f32_or_none(self.delta_sqrt_a)
1188    }
1189}
1190
1191impl SbfBlockParse for GalAlmBlock {
1192    const BLOCK_ID: u16 = block_ids::GAL_ALM;
1193
1194    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1195        if data.len() < 59 {
1196            return Err(SbfError::ParseError("GALAlm too short".into()));
1197        }
1198
1199        let svid = data[12];
1200        let source = data[13];
1201        let e = f32::from_le_bytes(data[14..18].try_into().unwrap());
1202        let t_oa = u32::from_le_bytes(data[18..22].try_into().unwrap());
1203        let delta_i = f32::from_le_bytes(data[22..26].try_into().unwrap());
1204        let omega_dot = f32::from_le_bytes(data[26..30].try_into().unwrap());
1205        let delta_sqrt_a = f32::from_le_bytes(data[30..34].try_into().unwrap());
1206        let omega_0 = f32::from_le_bytes(data[34..38].try_into().unwrap());
1207        let omega = f32::from_le_bytes(data[38..42].try_into().unwrap());
1208        let m_0 = f32::from_le_bytes(data[42..46].try_into().unwrap());
1209        let a_f1 = f32::from_le_bytes(data[46..50].try_into().unwrap());
1210        let a_f0 = f32::from_le_bytes(data[50..54].try_into().unwrap());
1211        let wn_a = data[54];
1212        let svid_a = data[55];
1213        let health = u16::from_le_bytes([data[56], data[57]]);
1214        let ioda = data[58];
1215
1216        Ok(Self {
1217            tow_ms: header.tow_ms,
1218            wnc: header.wnc,
1219            svid,
1220            source,
1221            e,
1222            t_oa,
1223            delta_i,
1224            omega_dot,
1225            delta_sqrt_a,
1226            omega_0,
1227            omega,
1228            m_0,
1229            a_f1,
1230            a_f0,
1231            wn_a,
1232            svid_a,
1233            health,
1234            ioda,
1235        })
1236    }
1237}
1238
1239// ============================================================================
1240// GALIon Block
1241// ============================================================================
1242
1243/// GALIon block (Block ID 4030)
1244///
1245/// Galileo ionosphere parameters.
1246#[derive(Debug, Clone)]
1247pub struct GalIonBlock {
1248    tow_ms: u32,
1249    wnc: u16,
1250    /// SVID (71-106 for Galileo)
1251    pub svid: u8,
1252    /// Signal source (F/NAV or I/NAV)
1253    pub source: u8,
1254    /// Ionosphere coefficient (sfu)
1255    pub a_i0: f32,
1256    /// Ionosphere coefficient (sfu/degree)
1257    pub a_i1: f32,
1258    /// Ionosphere coefficient (sfu/degree^2)
1259    pub a_i2: f32,
1260    /// Ionosphere storm flags
1261    pub storm_flags: u8,
1262}
1263
1264impl GalIonBlock {
1265    pub fn tow_seconds(&self) -> f64 {
1266        self.tow_ms as f64 * 0.001
1267    }
1268    pub fn tow_ms(&self) -> u32 {
1269        self.tow_ms
1270    }
1271    pub fn wnc(&self) -> u16 {
1272        self.wnc
1273    }
1274
1275    /// Check if from F/NAV
1276    pub fn is_fnav(&self) -> bool {
1277        self.source == 16
1278    }
1279
1280    /// Check if from I/NAV
1281    pub fn is_inav(&self) -> bool {
1282        self.source == 2
1283    }
1284
1285    pub fn a_i0(&self) -> Option<f32> {
1286        f32_or_none(self.a_i0)
1287    }
1288}
1289
1290impl SbfBlockParse for GalIonBlock {
1291    const BLOCK_ID: u16 = block_ids::GAL_ION;
1292
1293    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1294        if data.len() < 27 {
1295            return Err(SbfError::ParseError("GALIon too short".into()));
1296        }
1297
1298        let svid = data[12];
1299        let source = data[13];
1300        let a_i0 = f32::from_le_bytes(data[14..18].try_into().unwrap());
1301        let a_i1 = f32::from_le_bytes(data[18..22].try_into().unwrap());
1302        let a_i2 = f32::from_le_bytes(data[22..26].try_into().unwrap());
1303        let storm_flags = data[26];
1304
1305        Ok(Self {
1306            tow_ms: header.tow_ms,
1307            wnc: header.wnc,
1308            svid,
1309            source,
1310            a_i0,
1311            a_i1,
1312            a_i2,
1313            storm_flags,
1314        })
1315    }
1316}
1317
1318// ============================================================================
1319// GALUtc Block
1320// ============================================================================
1321
1322/// GALUtc block (Block ID 4031)
1323///
1324/// Galileo UTC parameters.
1325#[derive(Debug, Clone)]
1326pub struct GalUtcBlock {
1327    tow_ms: u32,
1328    wnc: u16,
1329    /// SVID (71-106 for Galileo)
1330    pub svid: u8,
1331    /// Signal source (F/NAV or I/NAV)
1332    pub source: u8,
1333    /// UTC drift (s/s)
1334    pub a_1: f32,
1335    /// UTC bias (s)
1336    pub a_0: f64,
1337    /// Reference time (s)
1338    pub t_ot: u32,
1339    /// Reference week
1340    pub wn_ot: u8,
1341    /// Current leap seconds
1342    pub delta_t_ls: i8,
1343    /// Week of future leap second
1344    pub wn_lsf: u8,
1345    /// Day of future leap second
1346    pub dn: u8,
1347    /// Future leap seconds
1348    pub delta_t_lsf: i8,
1349}
1350
1351impl GalUtcBlock {
1352    pub fn tow_seconds(&self) -> f64 {
1353        self.tow_ms as f64 * 0.001
1354    }
1355    pub fn tow_ms(&self) -> u32 {
1356        self.tow_ms
1357    }
1358    pub fn wnc(&self) -> u16 {
1359        self.wnc
1360    }
1361
1362    /// Get PRN number (1-36)
1363    pub fn prn(&self) -> u8 {
1364        if self.svid >= 71 && self.svid <= 106 {
1365            self.svid - 70
1366        } else {
1367            self.svid
1368        }
1369    }
1370
1371    pub fn utc_bias_s(&self) -> Option<f64> {
1372        f64_or_none(self.a_0)
1373    }
1374
1375    pub fn utc_drift_s_per_s(&self) -> Option<f32> {
1376        f32_or_none(self.a_1)
1377    }
1378}
1379
1380impl SbfBlockParse for GalUtcBlock {
1381    const BLOCK_ID: u16 = block_ids::GAL_UTC;
1382
1383    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1384        if data.len() < 35 {
1385            return Err(SbfError::ParseError("GALUtc too short".into()));
1386        }
1387
1388        let svid = data[12];
1389        let source = data[13];
1390        let a_1 = f32::from_le_bytes(data[14..18].try_into().unwrap());
1391        let a_0 = f64::from_le_bytes(data[18..26].try_into().unwrap());
1392        let t_ot = u32::from_le_bytes(data[26..30].try_into().unwrap());
1393        let wn_ot = data[30];
1394        let delta_t_ls = data[31] as i8;
1395        let wn_lsf = data[32];
1396        let dn = data[33];
1397        let delta_t_lsf = data[34] as i8;
1398
1399        Ok(Self {
1400            tow_ms: header.tow_ms,
1401            wnc: header.wnc,
1402            svid,
1403            source,
1404            a_1,
1405            a_0,
1406            t_ot,
1407            wn_ot,
1408            delta_t_ls,
1409            wn_lsf,
1410            dn,
1411            delta_t_lsf,
1412        })
1413    }
1414}
1415
1416// ============================================================================
1417// GALGstGps Block
1418// ============================================================================
1419
1420/// GALGstGps block (Block ID 4032)
1421///
1422/// Galileo GST to GPS time parameters.
1423#[derive(Debug, Clone)]
1424pub struct GalGstGpsBlock {
1425    tow_ms: u32,
1426    wnc: u16,
1427    /// SVID (71-106 for Galileo)
1428    pub svid: u8,
1429    /// Signal source (F/NAV or I/NAV)
1430    pub source: u8,
1431    /// GST-GPS drift (10^9 ns/s)
1432    pub a_1g: f32,
1433    /// GST-GPS offset (10^9 ns)
1434    pub a_0g: f32,
1435    /// Reference time (s)
1436    pub t_og: u32,
1437    /// Reference week
1438    pub wn_og: u8,
1439}
1440
1441impl GalGstGpsBlock {
1442    pub fn tow_seconds(&self) -> f64 {
1443        self.tow_ms as f64 * 0.001
1444    }
1445    pub fn tow_ms(&self) -> u32 {
1446        self.tow_ms
1447    }
1448    pub fn wnc(&self) -> u16 {
1449        self.wnc
1450    }
1451
1452    /// Get PRN number (1-36)
1453    pub fn prn(&self) -> u8 {
1454        if self.svid >= 71 && self.svid <= 106 {
1455            self.svid - 70
1456        } else {
1457            self.svid
1458        }
1459    }
1460
1461    pub fn gst_gps_offset_s(&self) -> Option<f32> {
1462        f32_or_none(self.a_0g)
1463    }
1464
1465    pub fn gst_gps_drift_s_per_s(&self) -> Option<f32> {
1466        f32_or_none(self.a_1g)
1467    }
1468}
1469
1470impl SbfBlockParse for GalGstGpsBlock {
1471    const BLOCK_ID: u16 = block_ids::GAL_GST_GPS;
1472
1473    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1474        if data.len() < 27 {
1475            return Err(SbfError::ParseError("GALGstGps too short".into()));
1476        }
1477
1478        let svid = data[12];
1479        let source = data[13];
1480        let a_1g = f32::from_le_bytes(data[14..18].try_into().unwrap());
1481        let a_0g = f32::from_le_bytes(data[18..22].try_into().unwrap());
1482        let t_og = u32::from_le_bytes(data[22..26].try_into().unwrap());
1483        let wn_og = data[26];
1484
1485        Ok(Self {
1486            tow_ms: header.tow_ms,
1487            wnc: header.wnc,
1488            svid,
1489            source,
1490            a_1g,
1491            a_0g,
1492            t_og,
1493            wn_og,
1494        })
1495    }
1496}
1497
1498// ============================================================================
1499// GALAuthStatus Block
1500// ============================================================================
1501
1502/// GALAuthStatus (4245) — Galileo OSNMA authentication status.
1503#[derive(Debug, Clone)]
1504pub struct GalAuthStatusBlock {
1505    tow_ms: u32,
1506    wnc: u16,
1507    /// OSNMA status bitfield (see Septentrio SBF reference: Status, progress, time source, …).
1508    pub osnma_status: u16,
1509    /// Trusted time delta (seconds).
1510    pub trusted_time_delta: f32,
1511    /// Galileo active authentication mask (64 bit).
1512    pub gal_active_mask: u64,
1513    /// Galileo authentic mask (64 bit).
1514    pub gal_authentic_mask: u64,
1515    /// GPS active authentication mask (64 bit).
1516    pub gps_active_mask: u64,
1517    /// GPS authentic mask (64 bit).
1518    pub gps_authentic_mask: u64,
1519}
1520
1521impl GalAuthStatusBlock {
1522    pub fn tow_ms(&self) -> u32 {
1523        self.tow_ms
1524    }
1525    pub fn wnc(&self) -> u16 {
1526        self.wnc
1527    }
1528    pub fn tow_seconds(&self) -> f64 {
1529        self.tow_ms as f64 * 0.001
1530    }
1531
1532    pub fn trusted_time_delta_s(&self) -> Option<f32> {
1533        f32_or_none(self.trusted_time_delta)
1534    }
1535}
1536
1537impl SbfBlockParse for GalAuthStatusBlock {
1538    const BLOCK_ID: u16 = block_ids::GAL_AUTH_STATUS;
1539
1540    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1541        if data.len() < 50 {
1542            return Err(SbfError::ParseError("GALAuthStatus too short".into()));
1543        }
1544
1545        let osnma_status = u16::from_le_bytes([data[12], data[13]]);
1546        let trusted_time_delta = f32::from_le_bytes(data[14..18].try_into().unwrap());
1547        let gal_active_mask = u64::from_le_bytes(data[18..26].try_into().unwrap());
1548        let gal_authentic_mask = u64::from_le_bytes(data[26..34].try_into().unwrap());
1549        let gps_active_mask = u64::from_le_bytes(data[34..42].try_into().unwrap());
1550        let gps_authentic_mask = u64::from_le_bytes(data[42..50].try_into().unwrap());
1551
1552        Ok(Self {
1553            tow_ms: header.tow_ms,
1554            wnc: header.wnc,
1555            osnma_status,
1556            trusted_time_delta,
1557            gal_active_mask,
1558            gal_authentic_mask,
1559            gps_active_mask,
1560            gps_authentic_mask,
1561        })
1562    }
1563}
1564
1565// ============================================================================
1566// GALSARRLM Block
1567// ============================================================================
1568
1569/// GALSARRLM block (Block ID 4034)
1570///
1571/// Decoded Galileo search-and-rescue return link message (RLM).
1572#[derive(Debug, Clone)]
1573pub struct GalSarRlmBlock {
1574    tow_ms: u32,
1575    wnc: u16,
1576    /// SVID (71-106 for Galileo)
1577    pub svid: u8,
1578    /// Message source (2=I/NAV, 16=F/NAV)
1579    pub source: u8,
1580    /// RLM payload length in bits (typically 80 or 160)
1581    rlm_length_bits: u8,
1582    /// RLM payload words, MSB-first within each word.
1583    rlm_bits_words: Vec<u32>,
1584}
1585
1586impl GalSarRlmBlock {
1587    pub fn tow_seconds(&self) -> f64 {
1588        self.tow_ms as f64 * 0.001
1589    }
1590    pub fn tow_ms(&self) -> u32 {
1591        self.tow_ms
1592    }
1593    pub fn wnc(&self) -> u16 {
1594        self.wnc
1595    }
1596
1597    /// Get PRN number (1-36) when SVID is in Galileo range.
1598    pub fn prn(&self) -> u8 {
1599        if (71..=106).contains(&self.svid) {
1600            self.svid - 70
1601        } else {
1602            self.svid
1603        }
1604    }
1605
1606    pub fn rlm_length_bits(&self) -> u8 {
1607        self.rlm_length_bits
1608    }
1609
1610    /// Raw 32-bit words containing the RLM payload bits.
1611    pub fn rlm_bits_words(&self) -> &[u32] {
1612        &self.rlm_bits_words
1613    }
1614
1615    /// Return one RLM bit by index (0-based), with bit 0 as the MSB of word 0.
1616    pub fn bit(&self, bit_index: usize) -> Option<bool> {
1617        if bit_index >= self.rlm_length_bits as usize {
1618            return None;
1619        }
1620
1621        let word_index = bit_index / 32;
1622        let bit_in_word = 31 - (bit_index % 32);
1623        self.rlm_bits_words
1624            .get(word_index)
1625            .map(|word| ((word >> bit_in_word) & 1) != 0)
1626    }
1627}
1628
1629impl SbfBlockParse for GalSarRlmBlock {
1630    const BLOCK_ID: u16 = block_ids::GAL_SAR_RLM;
1631
1632    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1633        let block_len = header.length as usize;
1634        let data_len = block_len.saturating_sub(2);
1635        if data_len < 18 || data.len() < data_len {
1636            return Err(SbfError::ParseError("GALSARRLM too short".into()));
1637        }
1638
1639        let svid = data[12];
1640        let source = data[13];
1641        let rlm_length_bits = data[14];
1642
1643        let word_count = match rlm_length_bits {
1644            80 => 3,
1645            160 => 5,
1646            0 => 0,
1647            _ => usize::from(rlm_length_bits).div_ceil(32),
1648        };
1649        let required_len = 18 + word_count * 4;
1650        if data_len < required_len {
1651            return Err(SbfError::ParseError("GALSARRLM payload too short".into()));
1652        }
1653
1654        let mut rlm_bits_words = Vec::with_capacity(word_count);
1655        let mut offset = 18;
1656        for _ in 0..word_count {
1657            rlm_bits_words.push(u32::from_le_bytes(
1658                data[offset..offset + 4].try_into().unwrap(),
1659            ));
1660            offset += 4;
1661        }
1662
1663        Ok(Self {
1664            tow_ms: header.tow_ms,
1665            wnc: header.wnc,
1666            svid,
1667            source,
1668            rlm_length_bits,
1669            rlm_bits_words,
1670        })
1671    }
1672}
1673
1674// ============================================================================
1675// GPSCNav Block
1676// ============================================================================
1677
1678/// GPSCNav block (Block ID 4042)
1679///
1680/// Decoded GPS CNAV navigation data from L2C and/or L5 signals.
1681/// Contains ephemeris from MT10/11 and clock/ISC corrections from MT30.
1682#[derive(Debug, Clone)]
1683pub struct GpsCNavBlock {
1684    tow_ms: u32,
1685    wnc: u16,
1686    /// PRN number (1-32)
1687    pub prn: u8,
1688    /// Flags bit field (alert, integrity, L2C phasing, L2C/L5 used)
1689    pub flags: u8,
1690    /// Week number (13 bits from MT10)
1691    pub wn: u16,
1692    /// L1/L2/L5 signal health (3 bits from MT10)
1693    pub health: u8,
1694    /// Elevation-Dependent accuracy index (URA_ED)
1695    pub ura_ed: i8,
1696    /// Data predict time of week (seconds)
1697    pub t_op: u32,
1698    /// Ephemeris reference time (seconds)
1699    pub t_oe: u32,
1700    /// Semi-major axis (m)
1701    pub a: f64,
1702    /// Change rate in semi-major axis (m/s)
1703    pub a_dot: f64,
1704    /// Mean motion difference (semi-circles/s)
1705    pub delta_n: f32,
1706    /// Rate of mean motion difference (semi-circles/s^2)
1707    pub delta_n_dot: f32,
1708    /// Mean anomaly at reference time (semi-circles)
1709    pub m_0: f64,
1710    /// Eccentricity
1711    pub e: f64,
1712    /// Argument of perigee (semi-circles)
1713    pub omega: f64,
1714    /// Right ascension at reference time (semi-circles)
1715    pub omega_0: f64,
1716    /// Rate of right ascension (semi-circles/s)
1717    pub omega_dot: f64,
1718    /// Inclination angle at reference time (semi-circles)
1719    pub i_0: f64,
1720    /// Rate of inclination (semi-circles/s)
1721    pub i_dot: f32,
1722    /// Sine harmonic inclination correction (rad)
1723    pub c_is: f32,
1724    /// Cosine harmonic inclination correction (rad)
1725    pub c_ic: f32,
1726    /// Sine harmonic radius correction (m)
1727    pub c_rs: f32,
1728    /// Cosine harmonic radius correction (m)
1729    pub c_rc: f32,
1730    /// Sine harmonic latitude correction (rad)
1731    pub c_us: f32,
1732    /// Cosine harmonic latitude correction (rad)
1733    pub c_uc: f32,
1734    /// Clock reference time (seconds)
1735    pub t_oc: u32,
1736    /// Non-Elevation-Dependent accuracy index 0
1737    pub ura_ned0: i8,
1738    /// Non-Elevation-Dependent accuracy change index
1739    pub ura_ned1: u8,
1740    /// Non-Elevation-Dependent accuracy change rate index
1741    pub ura_ned2: u8,
1742    /// Week number associated with t_op (modulo 256)
1743    pub wn_op: u8,
1744    /// Clock drift rate (s/s^2)
1745    pub a_f2: f32,
1746    /// Clock drift (s/s)
1747    pub a_f1: f32,
1748    /// Clock bias (s)
1749    pub a_f0: f64,
1750    /// Group delay differential (s)
1751    pub t_gd: f32,
1752    /// Inter-Signal Correction for L1C/A (s)
1753    pub isc_l1ca: f32,
1754    /// Inter-Signal Correction for L2C (s)
1755    pub isc_l2c: f32,
1756    /// Inter-Signal Correction for L5I (s)
1757    pub isc_l5i5: f32,
1758    /// Inter-Signal Correction for L5Q (s)
1759    pub isc_l5q5: f32,
1760}
1761
1762impl GpsCNavBlock {
1763    pub fn tow_seconds(&self) -> f64 {
1764        self.tow_ms as f64 * 0.001
1765    }
1766    pub fn tow_ms(&self) -> u32 {
1767        self.tow_ms
1768    }
1769    pub fn wnc(&self) -> u16 {
1770        self.wnc
1771    }
1772
1773    /// Check if alert bit is set
1774    pub fn is_alert(&self) -> bool {
1775        (self.flags & 0x01) != 0
1776    }
1777
1778    /// Check if L2C was used for decoding
1779    pub fn l2c_used(&self) -> bool {
1780        (self.flags & 0x40) != 0
1781    }
1782
1783    /// Check if L5 was used for decoding
1784    pub fn l5_used(&self) -> bool {
1785        (self.flags & 0x80) != 0
1786    }
1787
1788    /// Check if satellite is healthy
1789    pub fn is_healthy(&self) -> bool {
1790        self.health == 0
1791    }
1792
1793    /// Group delay (None if DNU)
1794    pub fn group_delay_s(&self) -> Option<f32> {
1795        f32_or_none(self.t_gd)
1796    }
1797
1798    /// ISC L1C/A (None if DNU)
1799    pub fn isc_l1ca_s(&self) -> Option<f32> {
1800        f32_or_none(self.isc_l1ca)
1801    }
1802
1803    /// ISC L2C (None if DNU)
1804    pub fn isc_l2c_s(&self) -> Option<f32> {
1805        f32_or_none(self.isc_l2c)
1806    }
1807
1808    /// ISC L5I5 (None if DNU)
1809    pub fn isc_l5i5_s(&self) -> Option<f32> {
1810        f32_or_none(self.isc_l5i5)
1811    }
1812
1813    /// ISC L5Q5 (None if DNU)
1814    pub fn isc_l5q5_s(&self) -> Option<f32> {
1815        f32_or_none(self.isc_l5q5)
1816    }
1817}
1818
1819impl SbfBlockParse for GpsCNavBlock {
1820    const BLOCK_ID: u16 = block_ids::GPS_CNAV;
1821
1822    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1823        // Minimum size: header (12) + fields up to ISC_L5Q5 (170 bytes)
1824        if data.len() < 170 {
1825            return Err(SbfError::ParseError("GPSCNav too short".into()));
1826        }
1827
1828        // Offsets from data start (after sync):
1829        // 12: PRNidx (u1)
1830        // 13: Flags (u1)
1831        // 14-15: WN (u2)
1832        // 16: Health (u1)
1833        // 17: URA_ED (i1)
1834        // 18-21: t_op (u4)
1835        // 22-25: t_oe (u4)
1836        // 26-33: A (f8)
1837        // 34-41: A_DOT (f8)
1838        // 42-45: DELTA_N (f4)
1839        // 46-49: DELTA_N_DOT (f4)
1840        // 50-57: M_0 (f8)
1841        // 58-65: e (f8)
1842        // 66-73: omega (f8)
1843        // 74-81: OMEGA_0 (f8)
1844        // 82-89: OMEGADOT (f8)
1845        // 90-97: i_0 (f8)
1846        // 98-101: IDOT (f4)
1847        // 102-105: C_is (f4)
1848        // 106-109: C_ic (f4)
1849        // 110-113: C_rs (f4)
1850        // 114-117: C_rc (f4)
1851        // 118-121: C_us (f4)
1852        // 122-125: C_uc (f4)
1853        // 126-129: t_oc (u4)
1854        // 130: URA_NED0 (i1)
1855        // 131: URA_NED1 (u1)
1856        // 132: URA_NED2 (u1)
1857        // 133: WN_op (u1)
1858        // 134-137: a_f2 (f4)
1859        // 138-141: a_f1 (f4)
1860        // 142-149: a_f0 (f8)
1861        // 150-153: T_gd (f4)
1862        // 154-157: ISC_L1CA (f4)
1863        // 158-161: ISC_L2C (f4)
1864        // 162-165: ISC_L5I5 (f4)
1865        // 166-169: ISC_L5Q5 (f4)
1866
1867        let prn = data[12];
1868        let flags = data[13];
1869        let wn = u16::from_le_bytes([data[14], data[15]]);
1870        let health = data[16];
1871        let ura_ed = data[17] as i8;
1872        let t_op = u32::from_le_bytes(data[18..22].try_into().unwrap());
1873        let t_oe = u32::from_le_bytes(data[22..26].try_into().unwrap());
1874        let a = f64::from_le_bytes(data[26..34].try_into().unwrap());
1875        let a_dot = f64::from_le_bytes(data[34..42].try_into().unwrap());
1876        let delta_n = f32::from_le_bytes(data[42..46].try_into().unwrap());
1877        let delta_n_dot = f32::from_le_bytes(data[46..50].try_into().unwrap());
1878        let m_0 = f64::from_le_bytes(data[50..58].try_into().unwrap());
1879        let e = f64::from_le_bytes(data[58..66].try_into().unwrap());
1880        let omega = f64::from_le_bytes(data[66..74].try_into().unwrap());
1881        let omega_0 = f64::from_le_bytes(data[74..82].try_into().unwrap());
1882        let omega_dot = f64::from_le_bytes(data[82..90].try_into().unwrap());
1883        let i_0 = f64::from_le_bytes(data[90..98].try_into().unwrap());
1884        let i_dot = f32::from_le_bytes(data[98..102].try_into().unwrap());
1885        let c_is = f32::from_le_bytes(data[102..106].try_into().unwrap());
1886        let c_ic = f32::from_le_bytes(data[106..110].try_into().unwrap());
1887        let c_rs = f32::from_le_bytes(data[110..114].try_into().unwrap());
1888        let c_rc = f32::from_le_bytes(data[114..118].try_into().unwrap());
1889        let c_us = f32::from_le_bytes(data[118..122].try_into().unwrap());
1890        let c_uc = f32::from_le_bytes(data[122..126].try_into().unwrap());
1891        let t_oc = u32::from_le_bytes(data[126..130].try_into().unwrap());
1892        let ura_ned0 = data[130] as i8;
1893        let ura_ned1 = data[131];
1894        let ura_ned2 = data[132];
1895        let wn_op = data[133];
1896        let a_f2 = f32::from_le_bytes(data[134..138].try_into().unwrap());
1897        let a_f1 = f32::from_le_bytes(data[138..142].try_into().unwrap());
1898        let a_f0 = f64::from_le_bytes(data[142..150].try_into().unwrap());
1899        let t_gd = f32::from_le_bytes(data[150..154].try_into().unwrap());
1900        let isc_l1ca = f32::from_le_bytes(data[154..158].try_into().unwrap());
1901        let isc_l2c = f32::from_le_bytes(data[158..162].try_into().unwrap());
1902        let isc_l5i5 = f32::from_le_bytes(data[162..166].try_into().unwrap());
1903        let isc_l5q5 = f32::from_le_bytes(data[166..170].try_into().unwrap());
1904
1905        Ok(Self {
1906            tow_ms: header.tow_ms,
1907            wnc: header.wnc,
1908            prn,
1909            flags,
1910            wn,
1911            health,
1912            ura_ed,
1913            t_op,
1914            t_oe,
1915            a,
1916            a_dot,
1917            delta_n,
1918            delta_n_dot,
1919            m_0,
1920            e,
1921            omega,
1922            omega_0,
1923            omega_dot,
1924            i_0,
1925            i_dot,
1926            c_is,
1927            c_ic,
1928            c_rs,
1929            c_rc,
1930            c_us,
1931            c_uc,
1932            t_oc,
1933            ura_ned0,
1934            ura_ned1,
1935            ura_ned2,
1936            wn_op,
1937            a_f2,
1938            a_f1,
1939            a_f0,
1940            t_gd,
1941            isc_l1ca,
1942            isc_l2c,
1943            isc_l5i5,
1944            isc_l5q5,
1945        })
1946    }
1947}
1948
1949// ============================================================================
1950// BDSIon Block
1951// ============================================================================
1952
1953/// BDSIon block (Block ID 4120)
1954///
1955/// BeiDou ionosphere parameters (Klobuchar coefficients) from D1/D2 nav message.
1956#[derive(Debug, Clone)]
1957pub struct BdsIonBlock {
1958    tow_ms: u32,
1959    wnc: u16,
1960    /// PRN of the BeiDou satellite (SVID, see 4.1.9)
1961    pub prn: u8,
1962    /// Vertical delay coefficient 0 (s)
1963    pub alpha_0: f32,
1964    /// Vertical delay coefficient 1 (s/semi-circle)
1965    pub alpha_1: f32,
1966    /// Vertical delay coefficient 2 (s/semi-circle^2)
1967    pub alpha_2: f32,
1968    /// Vertical delay coefficient 3 (s/semi-circle^3)
1969    pub alpha_3: f32,
1970    /// Model period coefficient 0 (s)
1971    pub beta_0: f32,
1972    /// Model period coefficient 1 (s/semi-circle)
1973    pub beta_1: f32,
1974    /// Model period coefficient 2 (s/semi-circle^2)
1975    pub beta_2: f32,
1976    /// Model period coefficient 3 (s/semi-circle^3)
1977    pub beta_3: f32,
1978}
1979
1980impl BdsIonBlock {
1981    pub fn tow_seconds(&self) -> f64 {
1982        self.tow_ms as f64 * 0.001
1983    }
1984    pub fn tow_ms(&self) -> u32 {
1985        self.tow_ms
1986    }
1987    pub fn wnc(&self) -> u16 {
1988        self.wnc
1989    }
1990
1991    pub fn alpha_0(&self) -> Option<f32> {
1992        f32_or_none(self.alpha_0)
1993    }
1994
1995    pub fn beta_0(&self) -> Option<f32> {
1996        f32_or_none(self.beta_0)
1997    }
1998}
1999
2000impl SbfBlockParse for BdsIonBlock {
2001    const BLOCK_ID: u16 = block_ids::BDS_ION;
2002
2003    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2004        // Header (12) + PRN (1) + Reserved (1) + 8 x f32 (32) = 46 bytes minimum
2005        if data.len() < 46 {
2006            return Err(SbfError::ParseError("BDSIon too short".into()));
2007        }
2008
2009        // Offsets:
2010        // 12: PRN (u1)
2011        // 13: Reserved (u1)
2012        // 14-17: alpha_0 (f4)
2013        // 18-21: alpha_1 (f4)
2014        // 22-25: alpha_2 (f4)
2015        // 26-29: alpha_3 (f4)
2016        // 30-33: beta_0 (f4)
2017        // 34-37: beta_1 (f4)
2018        // 38-41: beta_2 (f4)
2019        // 42-45: beta_3 (f4)
2020
2021        let prn = data[12];
2022        // data[13] is reserved
2023        let alpha_0 = f32::from_le_bytes(data[14..18].try_into().unwrap());
2024        let alpha_1 = f32::from_le_bytes(data[18..22].try_into().unwrap());
2025        let alpha_2 = f32::from_le_bytes(data[22..26].try_into().unwrap());
2026        let alpha_3 = f32::from_le_bytes(data[26..30].try_into().unwrap());
2027        let beta_0 = f32::from_le_bytes(data[30..34].try_into().unwrap());
2028        let beta_1 = f32::from_le_bytes(data[34..38].try_into().unwrap());
2029        let beta_2 = f32::from_le_bytes(data[38..42].try_into().unwrap());
2030        let beta_3 = f32::from_le_bytes(data[42..46].try_into().unwrap());
2031
2032        Ok(Self {
2033            tow_ms: header.tow_ms,
2034            wnc: header.wnc,
2035            prn,
2036            alpha_0,
2037            alpha_1,
2038            alpha_2,
2039            alpha_3,
2040            beta_0,
2041            beta_1,
2042            beta_2,
2043            beta_3,
2044        })
2045    }
2046}
2047
2048// ============================================================================
2049// BDSCNav1 Block
2050// ============================================================================
2051
2052/// BDSCNav1 block (Block ID 4251)
2053///
2054/// BeiDou B-CNAV1 navigation data from B1C signal.
2055#[derive(Debug, Clone)]
2056pub struct BdsCNav1Block {
2057    tow_ms: u32,
2058    wnc: u16,
2059    /// PRN index within BeiDou constellation (1 for C01, etc.)
2060    pub prn_idx: u8,
2061    /// Flags: bits 0-1 = satellite type (1: GEO, 2: IGSO, 3: MEO)
2062    pub flags: u8,
2063    /// Ephemeris reference time (seconds)
2064    pub t_oe: u32,
2065    /// Semi-major axis (m)
2066    pub a: f64,
2067    /// Change rate in semi-major axis (m/s)
2068    pub a_dot: f64,
2069    /// Mean motion difference (semi-circles/s)
2070    pub delta_n0: f32,
2071    /// Rate of mean motion difference (semi-circles/s^2)
2072    pub delta_n0_dot: f32,
2073    /// Mean anomaly (semi-circles)
2074    pub m_0: f64,
2075    /// Eccentricity
2076    pub e: f64,
2077    /// Argument of perigee (semi-circles)
2078    pub omega: f64,
2079    /// Longitude of ascending node (semi-circles)
2080    pub omega_0: f64,
2081    /// Rate of right ascension (semi-circles/s)
2082    pub omega_dot: f32,
2083    /// Inclination angle (semi-circles)
2084    pub i_0: f64,
2085    /// Rate of inclination (semi-circles/s)
2086    pub i_dot: f32,
2087    /// Sine harmonic inclination correction (rad)
2088    pub c_is: f32,
2089    /// Cosine harmonic inclination correction (rad)
2090    pub c_ic: f32,
2091    /// Sine harmonic radius correction (m)
2092    pub c_rs: f32,
2093    /// Cosine harmonic radius correction (m)
2094    pub c_rc: f32,
2095    /// Sine harmonic latitude correction (rad)
2096    pub c_us: f32,
2097    /// Cosine harmonic latitude correction (rad)
2098    pub c_uc: f32,
2099    /// Clock reference time (seconds)
2100    pub t_oc: u32,
2101    /// Clock drift rate (s/s^2)
2102    pub a_2: f32,
2103    /// Clock drift (s/s)
2104    pub a_1: f32,
2105    /// Clock bias (s)
2106    pub a_0: f64,
2107    /// Time of week for data prediction (seconds)
2108    pub t_op: u32,
2109    /// Satellite orbit radius and clock bias accuracy index
2110    pub sisai_ocb: u8,
2111    /// Combined SISAI_oc1 and SISAI_oc2 (bits 0-2: oc2, bits 3-5: oc1)
2112    pub sisai_oc12: u8,
2113    /// Satellite orbit along-track and cross-track accuracy index
2114    pub sisai_oe: u8,
2115    /// Signal in space monitoring accuracy index
2116    pub sismai: u8,
2117    /// Health and integrity flags
2118    pub health_if: u8,
2119    /// Issue of Data Ephemeris
2120    pub iode: u8,
2121    /// Issue of Data Clock
2122    pub iodc: u16,
2123    /// Group delay between B1C data and pilot (s)
2124    pub isc_b1cd: f32,
2125    /// Group delay of B1C pilot (s)
2126    pub t_gd_b1cp: f32,
2127    /// Group delay of B2a pilot (s)
2128    pub t_gd_b2ap: f32,
2129}
2130
2131impl BdsCNav1Block {
2132    pub fn tow_seconds(&self) -> f64 {
2133        self.tow_ms as f64 * 0.001
2134    }
2135    pub fn tow_ms(&self) -> u32 {
2136        self.tow_ms
2137    }
2138    pub fn wnc(&self) -> u16 {
2139        self.wnc
2140    }
2141
2142    /// Get satellite type (1: GEO, 2: IGSO, 3: MEO)
2143    pub fn satellite_type(&self) -> u8 {
2144        self.flags & 0x03
2145    }
2146
2147    /// Check if satellite is GEO
2148    pub fn is_geo(&self) -> bool {
2149        self.satellite_type() == 1
2150    }
2151
2152    /// Check if satellite is IGSO
2153    pub fn is_igso(&self) -> bool {
2154        self.satellite_type() == 2
2155    }
2156
2157    /// Check if satellite is MEO
2158    pub fn is_meo(&self) -> bool {
2159        self.satellite_type() == 3
2160    }
2161
2162    /// Check if satellite is healthy (bits 6-7 of health_if == 0)
2163    pub fn is_healthy(&self) -> bool {
2164        (self.health_if & 0xC0) == 0
2165    }
2166
2167    /// ISC B1Cd (None if DNU)
2168    pub fn isc_b1cd_s(&self) -> Option<f32> {
2169        f32_or_none(self.isc_b1cd)
2170    }
2171
2172    /// T_GD B1Cp (None if DNU)
2173    pub fn t_gd_b1cp_s(&self) -> Option<f32> {
2174        f32_or_none(self.t_gd_b1cp)
2175    }
2176
2177    /// T_GD B2ap (None if DNU)
2178    pub fn t_gd_b2ap_s(&self) -> Option<f32> {
2179        f32_or_none(self.t_gd_b2ap)
2180    }
2181}
2182
2183impl SbfBlockParse for BdsCNav1Block {
2184    const BLOCK_ID: u16 = block_ids::BDS_CNAV1;
2185
2186    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2187        // Calculate minimum size based on field layout
2188        // Header fields (12) + PRNidx(1) + Flags(1) + t_oe(4) + A(8) + A_DOT(8) +
2189        // DELTA_n0(4) + DELTA_n0_DOT(4) + M_0(8) + e(8) + omega(8) + OMEGA_0(8) +
2190        // OMEGADOT(4) + i_0(8) + IDOT(4) + C_is(4) + C_ic(4) + C_rs(4) + C_rc(4) +
2191        // C_us(4) + C_uc(4) + t_oc(4) + a_2(4) + a_1(4) + a_0(8) + t_op(4) +
2192        // SISAI_ocb(1) + SISAI_oc12(1) + SISAI_oe(1) + SISMAI(1) + HealthIF(1) +
2193        // IODE(1) + IODC(2) + ISC_B1Cd(4) + T_GDB1Cp(4) + T_GDB2ap(4)
2194        // = 12 + 1 + 1 + 4 + 8 + 8 + 4 + 4 + 8 + 8 + 8 + 8 + 4 + 8 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 8 + 4 + 1 + 1 + 1 + 1 + 1 + 1 + 2 + 4 + 4 + 4 = 158 bytes
2195        if data.len() < 158 {
2196            return Err(SbfError::ParseError("BDSCNav1 too short".into()));
2197        }
2198
2199        // Offsets:
2200        // 12: PRNidx (u1)
2201        // 13: Flags (u1)
2202        // 14-17: t_oe (u4)
2203        // 18-25: A (f8)
2204        // 26-33: A_DOT (f8)
2205        // 34-37: DELTA_n0 (f4)
2206        // 38-41: DELTA_n0_DOT (f4)
2207        // 42-49: M_0 (f8)
2208        // 50-57: e (f8)
2209        // 58-65: omega (f8)
2210        // 66-73: OMEGA_0 (f8)
2211        // 74-77: OMEGADOT (f4)
2212        // 78-85: i_0 (f8)
2213        // 86-89: IDOT (f4)
2214        // 90-93: C_is (f4)
2215        // 94-97: C_ic (f4)
2216        // 98-101: C_rs (f4)
2217        // 102-105: C_rc (f4)
2218        // 106-109: C_us (f4)
2219        // 110-113: C_uc (f4)
2220        // 114-117: t_oc (u4)
2221        // 118-121: a_2 (f4)
2222        // 122-125: a_1 (f4)
2223        // 126-133: a_0 (f8)
2224        // 134-137: t_op (u4)
2225        // 138: SISAI_ocb (u1)
2226        // 139: SISAI_oc12 (u1)
2227        // 140: SISAI_oe (u1)
2228        // 141: SISMAI (u1)
2229        // 142: HealthIF (u1)
2230        // 143: IODE (u1)
2231        // 144-145: IODC (u2)
2232        // 146-149: ISC_B1Cd (f4)
2233        // 150-153: T_GDB1Cp (f4)
2234        // 154-157: T_GDB2ap (f4)
2235
2236        let prn_idx = data[12];
2237        let flags = data[13];
2238        let t_oe = u32::from_le_bytes(data[14..18].try_into().unwrap());
2239        let a = f64::from_le_bytes(data[18..26].try_into().unwrap());
2240        let a_dot = f64::from_le_bytes(data[26..34].try_into().unwrap());
2241        let delta_n0 = f32::from_le_bytes(data[34..38].try_into().unwrap());
2242        let delta_n0_dot = f32::from_le_bytes(data[38..42].try_into().unwrap());
2243        let m_0 = f64::from_le_bytes(data[42..50].try_into().unwrap());
2244        let e = f64::from_le_bytes(data[50..58].try_into().unwrap());
2245        let omega = f64::from_le_bytes(data[58..66].try_into().unwrap());
2246        let omega_0 = f64::from_le_bytes(data[66..74].try_into().unwrap());
2247        let omega_dot = f32::from_le_bytes(data[74..78].try_into().unwrap());
2248        let i_0 = f64::from_le_bytes(data[78..86].try_into().unwrap());
2249        let i_dot = f32::from_le_bytes(data[86..90].try_into().unwrap());
2250        let c_is = f32::from_le_bytes(data[90..94].try_into().unwrap());
2251        let c_ic = f32::from_le_bytes(data[94..98].try_into().unwrap());
2252        let c_rs = f32::from_le_bytes(data[98..102].try_into().unwrap());
2253        let c_rc = f32::from_le_bytes(data[102..106].try_into().unwrap());
2254        let c_us = f32::from_le_bytes(data[106..110].try_into().unwrap());
2255        let c_uc = f32::from_le_bytes(data[110..114].try_into().unwrap());
2256        let t_oc = u32::from_le_bytes(data[114..118].try_into().unwrap());
2257        let a_2 = f32::from_le_bytes(data[118..122].try_into().unwrap());
2258        let a_1 = f32::from_le_bytes(data[122..126].try_into().unwrap());
2259        let a_0 = f64::from_le_bytes(data[126..134].try_into().unwrap());
2260        let t_op = u32::from_le_bytes(data[134..138].try_into().unwrap());
2261        let sisai_ocb = data[138];
2262        let sisai_oc12 = data[139];
2263        let sisai_oe = data[140];
2264        let sismai = data[141];
2265        let health_if = data[142];
2266        let iode = data[143];
2267        let iodc = u16::from_le_bytes([data[144], data[145]]);
2268        let isc_b1cd = f32::from_le_bytes(data[146..150].try_into().unwrap());
2269        let t_gd_b1cp = f32::from_le_bytes(data[150..154].try_into().unwrap());
2270        let t_gd_b2ap = f32::from_le_bytes(data[154..158].try_into().unwrap());
2271
2272        Ok(Self {
2273            tow_ms: header.tow_ms,
2274            wnc: header.wnc,
2275            prn_idx,
2276            flags,
2277            t_oe,
2278            a,
2279            a_dot,
2280            delta_n0,
2281            delta_n0_dot,
2282            m_0,
2283            e,
2284            omega,
2285            omega_0,
2286            omega_dot,
2287            i_0,
2288            i_dot,
2289            c_is,
2290            c_ic,
2291            c_rs,
2292            c_rc,
2293            c_us,
2294            c_uc,
2295            t_oc,
2296            a_2,
2297            a_1,
2298            a_0,
2299            t_op,
2300            sisai_ocb,
2301            sisai_oc12,
2302            sisai_oe,
2303            sismai,
2304            health_if,
2305            iode,
2306            iodc,
2307            isc_b1cd,
2308            t_gd_b1cp,
2309            t_gd_b2ap,
2310        })
2311    }
2312}
2313
2314// ============================================================================
2315// GPSRawCA Block
2316// ============================================================================
2317
2318/// GPSRawCA block (Block ID 4017)
2319///
2320/// Raw GPS C/A navigation subframe bits (L1).
2321#[derive(Debug, Clone)]
2322pub struct GpsRawCaBlock {
2323    tow_ms: u32,
2324    wnc: u16,
2325    /// Satellite ID (1-32 for GPS)
2326    pub svid: u8,
2327    /// CRC check: 0=failed, 1=passed
2328    pub crc_passed: u8,
2329    /// Viterbi decoder error count
2330    pub viterbi_count: u8,
2331    /// Signal source
2332    pub source: u8,
2333    /// Frequency number
2334    pub freq_nr: u8,
2335    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2336    pub nav_bits: [u8; 40],
2337}
2338
2339impl GpsRawCaBlock {
2340    pub fn tow_seconds(&self) -> f64 {
2341        self.tow_ms as f64 * 0.001
2342    }
2343    pub fn tow_ms(&self) -> u32 {
2344        self.tow_ms
2345    }
2346    pub fn wnc(&self) -> u16 {
2347        self.wnc
2348    }
2349    /// Whether CRC check passed
2350    pub fn crc_ok(&self) -> bool {
2351        self.crc_passed != 0
2352    }
2353    /// Raw navigation bits as slice
2354    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2355        &self.nav_bits
2356    }
2357}
2358
2359impl SbfBlockParse for GpsRawCaBlock {
2360    const BLOCK_ID: u16 = block_ids::GPS_RAW_CA;
2361
2362    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2363        // Header (12) + SVID(1) + CRCPassed(1) + ViterbiCount(1) + Source(1) + FreqNr(1) + NAVBits(40)
2364        const MIN_LEN: usize = 57;
2365        if data.len() < MIN_LEN {
2366            return Err(SbfError::ParseError("GPSRawCA too short".into()));
2367        }
2368
2369        let mut nav_bits = [0u8; 40];
2370        nav_bits.copy_from_slice(&data[17..57]);
2371
2372        Ok(Self {
2373            tow_ms: header.tow_ms,
2374            wnc: header.wnc,
2375            svid: data[12],
2376            crc_passed: data[13],
2377            viterbi_count: data[14],
2378            source: data[15],
2379            freq_nr: data[16],
2380            nav_bits,
2381        })
2382    }
2383}
2384
2385// ============================================================================
2386// GPSRawL2C Block
2387// ============================================================================
2388
2389/// GPSRawL2C block (Block ID 4018)
2390///
2391/// Raw GPS L2C navigation frame bits.
2392#[derive(Debug, Clone)]
2393pub struct GpsRawL2CBlock {
2394    tow_ms: u32,
2395    wnc: u16,
2396    /// Satellite ID (1-32 for GPS)
2397    pub svid: u8,
2398    /// CRC check: 0=failed, 1=passed
2399    pub crc_passed: u8,
2400    /// Viterbi decoder error count
2401    pub viterbi_count: u8,
2402    /// Signal source
2403    pub source: u8,
2404    /// Frequency number
2405    pub freq_nr: u8,
2406    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2407    pub nav_bits: [u8; 40],
2408}
2409
2410impl GpsRawL2CBlock {
2411    pub fn tow_seconds(&self) -> f64 {
2412        self.tow_ms as f64 * 0.001
2413    }
2414    pub fn tow_ms(&self) -> u32 {
2415        self.tow_ms
2416    }
2417    pub fn wnc(&self) -> u16 {
2418        self.wnc
2419    }
2420    pub fn crc_ok(&self) -> bool {
2421        self.crc_passed != 0
2422    }
2423    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2424        &self.nav_bits
2425    }
2426}
2427
2428impl SbfBlockParse for GpsRawL2CBlock {
2429    const BLOCK_ID: u16 = block_ids::GPS_RAW_L2C;
2430
2431    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2432        const MIN_LEN: usize = 57;
2433        if data.len() < MIN_LEN {
2434            return Err(SbfError::ParseError("GPSRawL2C too short".into()));
2435        }
2436
2437        let mut nav_bits = [0u8; 40];
2438        nav_bits.copy_from_slice(&data[17..57]);
2439
2440        Ok(Self {
2441            tow_ms: header.tow_ms,
2442            wnc: header.wnc,
2443            svid: data[12],
2444            crc_passed: data[13],
2445            viterbi_count: data[14],
2446            source: data[15],
2447            freq_nr: data[16],
2448            nav_bits,
2449        })
2450    }
2451}
2452
2453// ============================================================================
2454// GPSRawL5 Block
2455// ============================================================================
2456
2457/// GPSRawL5 block (Block ID 4019)
2458///
2459/// Raw GPS L5 navigation frame bits.
2460#[derive(Debug, Clone)]
2461pub struct GpsRawL5Block {
2462    tow_ms: u32,
2463    wnc: u16,
2464    /// Satellite ID (1-32 for GPS)
2465    pub svid: u8,
2466    /// CRC check: 0=failed, 1=passed
2467    pub crc_passed: u8,
2468    /// Viterbi decoder error count
2469    pub viterbi_count: u8,
2470    /// Signal source
2471    pub source: u8,
2472    /// Frequency number
2473    pub freq_nr: u8,
2474    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2475    pub nav_bits: [u8; 40],
2476}
2477
2478impl GpsRawL5Block {
2479    pub fn tow_seconds(&self) -> f64 {
2480        self.tow_ms as f64 * 0.001
2481    }
2482    pub fn tow_ms(&self) -> u32 {
2483        self.tow_ms
2484    }
2485    pub fn wnc(&self) -> u16 {
2486        self.wnc
2487    }
2488    pub fn crc_ok(&self) -> bool {
2489        self.crc_passed != 0
2490    }
2491    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2492        &self.nav_bits
2493    }
2494}
2495
2496impl SbfBlockParse for GpsRawL5Block {
2497    const BLOCK_ID: u16 = block_ids::GPS_RAW_L5;
2498
2499    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2500        const MIN_LEN: usize = 57;
2501        if data.len() < MIN_LEN {
2502            return Err(SbfError::ParseError("GPSRawL5 too short".into()));
2503        }
2504
2505        let mut nav_bits = [0u8; 40];
2506        nav_bits.copy_from_slice(&data[17..57]);
2507
2508        Ok(Self {
2509            tow_ms: header.tow_ms,
2510            wnc: header.wnc,
2511            svid: data[12],
2512            crc_passed: data[13],
2513            viterbi_count: data[14],
2514            source: data[15],
2515            freq_nr: data[16],
2516            nav_bits,
2517        })
2518    }
2519}
2520
2521// ============================================================================
2522// GLORawCA Block
2523// ============================================================================
2524
2525/// GLORawCA block (Block ID 4026)
2526///
2527/// Raw GLONASS CA navigation string bits.
2528#[derive(Debug, Clone)]
2529pub struct GloRawCaBlock {
2530    tow_ms: u32,
2531    wnc: u16,
2532    /// GLONASS slot (38-61)
2533    pub svid: u8,
2534    /// CRC check: 0=failed, 1=passed
2535    pub crc_passed: u8,
2536    /// Viterbi decoder error count
2537    pub viterbi_count: u8,
2538    /// Signal source
2539    pub source: u8,
2540    /// Frequency number (-7 to +6)
2541    pub freq_nr: u8,
2542    /// Raw navigation bits (3 × u4 = 12 bytes, 96 bits)
2543    pub nav_bits: [u8; 12],
2544}
2545
2546impl GloRawCaBlock {
2547    pub fn tow_seconds(&self) -> f64 {
2548        self.tow_ms as f64 * 0.001
2549    }
2550    pub fn tow_ms(&self) -> u32 {
2551        self.tow_ms
2552    }
2553    pub fn wnc(&self) -> u16 {
2554        self.wnc
2555    }
2556    pub fn crc_ok(&self) -> bool {
2557        self.crc_passed != 0
2558    }
2559    pub fn nav_bits_slice(&self) -> &[u8; 12] {
2560        &self.nav_bits
2561    }
2562}
2563
2564impl SbfBlockParse for GloRawCaBlock {
2565    const BLOCK_ID: u16 = block_ids::GLO_RAW_CA;
2566
2567    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2568        // Header (12) + SVID(1) + CRCPassed(1) + ViterbiCount(1) + Source(1) + FreqNr(1) + NAVBits(12)
2569        const MIN_LEN: usize = 29;
2570        if data.len() < MIN_LEN {
2571            return Err(SbfError::ParseError("GLORawCA too short".into()));
2572        }
2573
2574        let mut nav_bits = [0u8; 12];
2575        nav_bits.copy_from_slice(&data[17..29]);
2576
2577        Ok(Self {
2578            tow_ms: header.tow_ms,
2579            wnc: header.wnc,
2580            svid: data[12],
2581            crc_passed: data[13],
2582            viterbi_count: data[14],
2583            source: data[15],
2584            freq_nr: data[16],
2585            nav_bits,
2586        })
2587    }
2588}
2589
2590// ============================================================================
2591// GALRawFNAV Block
2592// ============================================================================
2593
2594/// GALRawFNAV block (Block ID 4022)
2595///
2596/// Raw Galileo F/NAV navigation bits.
2597#[derive(Debug, Clone)]
2598pub struct GalRawFnavBlock {
2599    tow_ms: u32,
2600    wnc: u16,
2601    /// Galileo SVID (71-102)
2602    pub svid: u8,
2603    /// CRC check: 0=failed, 1=passed
2604    pub crc_passed: u8,
2605    /// Viterbi decoder error count
2606    pub viterbi_count: u8,
2607    /// Signal source
2608    pub source: u8,
2609    /// Frequency number
2610    pub freq_nr: u8,
2611    /// Raw navigation bits (8 × u4 = 32 bytes, 256 bits)
2612    pub nav_bits: [u8; 32],
2613}
2614
2615impl GalRawFnavBlock {
2616    pub fn tow_seconds(&self) -> f64 {
2617        self.tow_ms as f64 * 0.001
2618    }
2619    pub fn tow_ms(&self) -> u32 {
2620        self.tow_ms
2621    }
2622    pub fn wnc(&self) -> u16 {
2623        self.wnc
2624    }
2625    pub fn crc_ok(&self) -> bool {
2626        self.crc_passed != 0
2627    }
2628    pub fn nav_bits_slice(&self) -> &[u8; 32] {
2629        &self.nav_bits
2630    }
2631}
2632
2633impl SbfBlockParse for GalRawFnavBlock {
2634    const BLOCK_ID: u16 = block_ids::GAL_RAW_FNAV;
2635
2636    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2637        const MIN_LEN: usize = 49;
2638        if data.len() < MIN_LEN {
2639            return Err(SbfError::ParseError("GALRawFNAV too short".into()));
2640        }
2641
2642        let mut nav_bits = [0u8; 32];
2643        nav_bits.copy_from_slice(&data[17..49]);
2644
2645        Ok(Self {
2646            tow_ms: header.tow_ms,
2647            wnc: header.wnc,
2648            svid: data[12],
2649            crc_passed: data[13],
2650            viterbi_count: data[14],
2651            source: data[15],
2652            freq_nr: data[16],
2653            nav_bits,
2654        })
2655    }
2656}
2657
2658// ============================================================================
2659// GALRawINAV Block
2660// ============================================================================
2661
2662/// GALRawINAV block (Block ID 4023)
2663///
2664/// Raw Galileo I/NAV navigation bits.
2665#[derive(Debug, Clone)]
2666pub struct GalRawInavBlock {
2667    tow_ms: u32,
2668    wnc: u16,
2669    /// Galileo SVID (71-102)
2670    pub svid: u8,
2671    /// CRC check: 0=failed, 1=passed
2672    pub crc_passed: u8,
2673    /// Viterbi decoder error count
2674    pub viterbi_count: u8,
2675    /// Signal source
2676    pub source: u8,
2677    /// Frequency number
2678    pub freq_nr: u8,
2679    /// Raw navigation bits (8 × u4 = 32 bytes, 256 bits)
2680    pub nav_bits: [u8; 32],
2681}
2682
2683impl GalRawInavBlock {
2684    pub fn tow_seconds(&self) -> f64 {
2685        self.tow_ms as f64 * 0.001
2686    }
2687    pub fn tow_ms(&self) -> u32 {
2688        self.tow_ms
2689    }
2690    pub fn wnc(&self) -> u16 {
2691        self.wnc
2692    }
2693    pub fn crc_ok(&self) -> bool {
2694        self.crc_passed != 0
2695    }
2696    pub fn nav_bits_slice(&self) -> &[u8; 32] {
2697        &self.nav_bits
2698    }
2699}
2700
2701impl SbfBlockParse for GalRawInavBlock {
2702    const BLOCK_ID: u16 = block_ids::GAL_RAW_INAV;
2703
2704    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2705        const MIN_LEN: usize = 49;
2706        if data.len() < MIN_LEN {
2707            return Err(SbfError::ParseError("GALRawINAV too short".into()));
2708        }
2709
2710        let mut nav_bits = [0u8; 32];
2711        nav_bits.copy_from_slice(&data[17..49]);
2712
2713        Ok(Self {
2714            tow_ms: header.tow_ms,
2715            wnc: header.wnc,
2716            svid: data[12],
2717            crc_passed: data[13],
2718            viterbi_count: data[14],
2719            source: data[15],
2720            freq_nr: data[16],
2721            nav_bits,
2722        })
2723    }
2724}
2725
2726// ============================================================================
2727// GALRawCNAV Block
2728// ============================================================================
2729
2730/// GALRawCNAV block (Block ID 4024)
2731///
2732/// Raw Galileo CNAV navigation bits.
2733#[derive(Debug, Clone)]
2734pub struct GalRawCnavBlock {
2735    tow_ms: u32,
2736    wnc: u16,
2737    /// Galileo SVID (71-102)
2738    pub svid: u8,
2739    /// CRC check: 0=failed, 1=passed
2740    pub crc_passed: u8,
2741    /// Viterbi decoder error count
2742    pub viterbi_count: u8,
2743    /// Signal source
2744    pub source: u8,
2745    /// Frequency number
2746    pub freq_nr: u8,
2747    /// Raw navigation bits (16 × u4 = 64 bytes, 512 bits)
2748    pub nav_bits: [u8; 64],
2749}
2750
2751impl GalRawCnavBlock {
2752    pub fn tow_seconds(&self) -> f64 {
2753        self.tow_ms as f64 * 0.001
2754    }
2755    pub fn tow_ms(&self) -> u32 {
2756        self.tow_ms
2757    }
2758    pub fn wnc(&self) -> u16 {
2759        self.wnc
2760    }
2761    pub fn crc_ok(&self) -> bool {
2762        self.crc_passed != 0
2763    }
2764    pub fn nav_bits_slice(&self) -> &[u8; 64] {
2765        &self.nav_bits
2766    }
2767}
2768
2769impl SbfBlockParse for GalRawCnavBlock {
2770    const BLOCK_ID: u16 = block_ids::GAL_RAW_CNAV;
2771
2772    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2773        const MIN_LEN: usize = 81;
2774        if data.len() < MIN_LEN {
2775            return Err(SbfError::ParseError("GALRawCNAV too short".into()));
2776        }
2777
2778        let mut nav_bits = [0u8; 64];
2779        nav_bits.copy_from_slice(&data[17..81]);
2780
2781        Ok(Self {
2782            tow_ms: header.tow_ms,
2783            wnc: header.wnc,
2784            svid: data[12],
2785            crc_passed: data[13],
2786            viterbi_count: data[14],
2787            source: data[15],
2788            freq_nr: data[16],
2789            nav_bits,
2790        })
2791    }
2792}
2793
2794// ============================================================================
2795// GEORawL1 Block
2796// ============================================================================
2797
2798/// GEORawL1 block (Block ID 4020)
2799///
2800/// Raw SBAS L1 navigation bits.
2801#[derive(Debug, Clone)]
2802pub struct GeoRawL1Block {
2803    tow_ms: u32,
2804    wnc: u16,
2805    /// SBAS PRN (120-158)
2806    pub svid: u8,
2807    /// CRC check: 0=failed, 1=passed
2808    pub crc_passed: u8,
2809    /// Viterbi decoder error count
2810    pub viterbi_count: u8,
2811    /// Signal source
2812    pub source: u8,
2813    /// Frequency number
2814    pub freq_nr: u8,
2815    /// Raw navigation bits (8 × u4 = 32 bytes, 256 bits)
2816    pub nav_bits: [u8; 32],
2817}
2818
2819impl GeoRawL1Block {
2820    pub fn tow_seconds(&self) -> f64 {
2821        self.tow_ms as f64 * 0.001
2822    }
2823    pub fn tow_ms(&self) -> u32 {
2824        self.tow_ms
2825    }
2826    pub fn wnc(&self) -> u16 {
2827        self.wnc
2828    }
2829    pub fn crc_ok(&self) -> bool {
2830        self.crc_passed != 0
2831    }
2832    pub fn nav_bits_slice(&self) -> &[u8; 32] {
2833        &self.nav_bits
2834    }
2835}
2836
2837impl SbfBlockParse for GeoRawL1Block {
2838    const BLOCK_ID: u16 = block_ids::GEO_RAW_L1;
2839
2840    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2841        const MIN_LEN: usize = 49;
2842        if data.len() < MIN_LEN {
2843            return Err(SbfError::ParseError("GEORawL1 too short".into()));
2844        }
2845
2846        let mut nav_bits = [0u8; 32];
2847        nav_bits.copy_from_slice(&data[17..49]);
2848
2849        Ok(Self {
2850            tow_ms: header.tow_ms,
2851            wnc: header.wnc,
2852            svid: data[12],
2853            crc_passed: data[13],
2854            viterbi_count: data[14],
2855            source: data[15],
2856            freq_nr: data[16],
2857            nav_bits,
2858        })
2859    }
2860}
2861
2862// ============================================================================
2863// CMPRaw Block
2864// ============================================================================
2865
2866/// CMPRaw block (Block ID 4047)
2867///
2868/// Raw BeiDou navigation bits.
2869#[derive(Debug, Clone)]
2870pub struct CmpRawBlock {
2871    tow_ms: u32,
2872    wnc: u16,
2873    /// BeiDou SVID (141-172)
2874    pub svid: u8,
2875    /// CRC check: 0=failed, 1=passed
2876    pub crc_passed: u8,
2877    /// Viterbi decoder error count
2878    pub viterbi_count: u8,
2879    /// Signal source
2880    pub source: u8,
2881    /// Frequency number
2882    pub freq_nr: u8,
2883    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2884    pub nav_bits: [u8; 40],
2885}
2886
2887impl CmpRawBlock {
2888    pub fn tow_seconds(&self) -> f64 {
2889        self.tow_ms as f64 * 0.001
2890    }
2891    pub fn tow_ms(&self) -> u32 {
2892        self.tow_ms
2893    }
2894    pub fn wnc(&self) -> u16 {
2895        self.wnc
2896    }
2897    pub fn crc_ok(&self) -> bool {
2898        self.crc_passed != 0
2899    }
2900    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2901        &self.nav_bits
2902    }
2903}
2904
2905impl SbfBlockParse for CmpRawBlock {
2906    const BLOCK_ID: u16 = block_ids::CMP_RAW;
2907
2908    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2909        const MIN_LEN: usize = 57;
2910        if data.len() < MIN_LEN {
2911            return Err(SbfError::ParseError("CMPRaw too short".into()));
2912        }
2913
2914        let mut nav_bits = [0u8; 40];
2915        nav_bits.copy_from_slice(&data[17..57]);
2916
2917        Ok(Self {
2918            tow_ms: header.tow_ms,
2919            wnc: header.wnc,
2920            svid: data[12],
2921            crc_passed: data[13],
2922            viterbi_count: data[14],
2923            source: data[15],
2924            freq_nr: data[16],
2925            nav_bits,
2926        })
2927    }
2928}
2929
2930// ============================================================================
2931// QZSRawL1CA Block
2932// ============================================================================
2933
2934/// QZSRawL1CA block (Block ID 4066)
2935///
2936/// Raw QZSS L1 C/A navigation bits.
2937#[derive(Debug, Clone)]
2938pub struct QzsRawL1CaBlock {
2939    tow_ms: u32,
2940    wnc: u16,
2941    /// QZSS SVID (181-187)
2942    pub svid: u8,
2943    /// CRC check: 0=failed, 1=passed
2944    pub crc_passed: u8,
2945    /// Viterbi decoder error count
2946    pub viterbi_count: u8,
2947    /// Signal source
2948    pub source: u8,
2949    /// Frequency number
2950    pub freq_nr: u8,
2951    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2952    pub nav_bits: [u8; 40],
2953}
2954
2955impl QzsRawL1CaBlock {
2956    pub fn tow_seconds(&self) -> f64 {
2957        self.tow_ms as f64 * 0.001
2958    }
2959    pub fn tow_ms(&self) -> u32 {
2960        self.tow_ms
2961    }
2962    pub fn wnc(&self) -> u16 {
2963        self.wnc
2964    }
2965    pub fn crc_ok(&self) -> bool {
2966        self.crc_passed != 0
2967    }
2968    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2969        &self.nav_bits
2970    }
2971}
2972
2973impl SbfBlockParse for QzsRawL1CaBlock {
2974    const BLOCK_ID: u16 = block_ids::QZS_RAW_L1CA;
2975
2976    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2977        const MIN_LEN: usize = 57;
2978        if data.len() < MIN_LEN {
2979            return Err(SbfError::ParseError("QZSRawL1CA too short".into()));
2980        }
2981
2982        let mut nav_bits = [0u8; 40];
2983        nav_bits.copy_from_slice(&data[17..57]);
2984
2985        Ok(Self {
2986            tow_ms: header.tow_ms,
2987            wnc: header.wnc,
2988            svid: data[12],
2989            crc_passed: data[13],
2990            viterbi_count: data[14],
2991            source: data[15],
2992            freq_nr: data[16],
2993            nav_bits,
2994        })
2995    }
2996}
2997
2998// ============================================================================
2999// QZSRawL2C Block
3000// ============================================================================
3001
3002/// QZSRawL2C block (Block ID 4067)
3003///
3004/// Raw QZSS L2C navigation bits.
3005#[derive(Debug, Clone)]
3006pub struct QzsRawL2CBlock {
3007    tow_ms: u32,
3008    wnc: u16,
3009    /// QZSS SVID (181-187)
3010    pub svid: u8,
3011    /// CRC check: 0=failed, 1=passed
3012    pub crc_passed: u8,
3013    /// Viterbi decoder error count
3014    pub viterbi_count: u8,
3015    /// Signal source
3016    pub source: u8,
3017    /// Frequency number
3018    pub freq_nr: u8,
3019    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
3020    pub nav_bits: [u8; 40],
3021}
3022
3023impl QzsRawL2CBlock {
3024    pub fn tow_seconds(&self) -> f64 {
3025        self.tow_ms as f64 * 0.001
3026    }
3027    pub fn tow_ms(&self) -> u32 {
3028        self.tow_ms
3029    }
3030    pub fn wnc(&self) -> u16 {
3031        self.wnc
3032    }
3033    pub fn crc_ok(&self) -> bool {
3034        self.crc_passed != 0
3035    }
3036    pub fn nav_bits_slice(&self) -> &[u8; 40] {
3037        &self.nav_bits
3038    }
3039}
3040
3041impl SbfBlockParse for QzsRawL2CBlock {
3042    const BLOCK_ID: u16 = block_ids::QZS_RAW_L2C;
3043
3044    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
3045        const MIN_LEN: usize = 57;
3046        if data.len() < MIN_LEN {
3047            return Err(SbfError::ParseError("QZSRawL2C too short".into()));
3048        }
3049
3050        let mut nav_bits = [0u8; 40];
3051        nav_bits.copy_from_slice(&data[17..57]);
3052
3053        Ok(Self {
3054            tow_ms: header.tow_ms,
3055            wnc: header.wnc,
3056            svid: data[12],
3057            crc_passed: data[13],
3058            viterbi_count: data[14],
3059            source: data[15],
3060            freq_nr: data[16],
3061            nav_bits,
3062        })
3063    }
3064}
3065
3066// ============================================================================
3067// QZSRawL5 Block
3068// ============================================================================
3069
3070/// QZSRawL5 block (Block ID 4068)
3071///
3072/// Raw QZSS L5 navigation bits.
3073#[derive(Debug, Clone)]
3074pub struct QzsRawL5Block {
3075    tow_ms: u32,
3076    wnc: u16,
3077    /// QZSS SVID (181-187)
3078    pub svid: u8,
3079    /// CRC check: 0=failed, 1=passed
3080    pub crc_passed: u8,
3081    /// Viterbi decoder error count
3082    pub viterbi_count: u8,
3083    /// Signal source
3084    pub source: u8,
3085    /// Frequency number
3086    pub freq_nr: u8,
3087    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
3088    pub nav_bits: [u8; 40],
3089}
3090
3091impl QzsRawL5Block {
3092    pub fn tow_seconds(&self) -> f64 {
3093        self.tow_ms as f64 * 0.001
3094    }
3095    pub fn tow_ms(&self) -> u32 {
3096        self.tow_ms
3097    }
3098    pub fn wnc(&self) -> u16 {
3099        self.wnc
3100    }
3101    pub fn crc_ok(&self) -> bool {
3102        self.crc_passed != 0
3103    }
3104    pub fn nav_bits_slice(&self) -> &[u8; 40] {
3105        &self.nav_bits
3106    }
3107}
3108
3109impl SbfBlockParse for QzsRawL5Block {
3110    const BLOCK_ID: u16 = block_ids::QZS_RAW_L5;
3111
3112    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
3113        const MIN_LEN: usize = 57;
3114        if data.len() < MIN_LEN {
3115            return Err(SbfError::ParseError("QZSRawL5 too short".into()));
3116        }
3117
3118        let mut nav_bits = [0u8; 40];
3119        nav_bits.copy_from_slice(&data[17..57]);
3120
3121        Ok(Self {
3122            tow_ms: header.tow_ms,
3123            wnc: header.wnc,
3124            svid: data[12],
3125            crc_passed: data[13],
3126            viterbi_count: data[14],
3127            source: data[15],
3128            freq_nr: data[16],
3129            nav_bits,
3130        })
3131    }
3132}
3133
3134// ============================================================================
3135// GEOIonoDelay Block
3136// ============================================================================
3137
3138/// One ionospheric delay correction entry in `GEOIonoDelay`.
3139#[derive(Debug, Clone)]
3140pub struct GeoIonoDelayIdc {
3141    /// Sequence number in the IGP mask (1..201)
3142    pub igp_mask_no: u8,
3143    /// Grid Ionospheric Vertical Error Indicator (0..15)
3144    pub givei: u8,
3145    vertical_delay_m_raw: f32,
3146}
3147
3148impl GeoIonoDelayIdc {
3149    /// Vertical delay estimate in meters.
3150    /// Returns `None` when the receiver marks the value as do-not-use.
3151    pub fn vertical_delay_m(&self) -> Option<f32> {
3152        f32_or_none(self.vertical_delay_m_raw)
3153    }
3154
3155    /// Raw vertical delay value from the block payload.
3156    pub fn vertical_delay_m_raw(&self) -> f32 {
3157        self.vertical_delay_m_raw
3158    }
3159}
3160
3161/// GEOIonoDelay block (Block ID 5933)
3162///
3163/// SBAS MT26 ionospheric delay corrections.
3164#[derive(Debug, Clone)]
3165pub struct GeoIonoDelayBlock {
3166    tow_ms: u32,
3167    wnc: u16,
3168    /// ID of the SBAS satellite from which MT26 was received
3169    pub prn: u8,
3170    /// SBAS band number
3171    pub band_nbr: u8,
3172    /// Issue of data ionosphere
3173    pub iodi: u8,
3174    /// Ionospheric delay correction entries
3175    pub idc: Vec<GeoIonoDelayIdc>,
3176}
3177
3178impl GeoIonoDelayBlock {
3179    pub fn tow_seconds(&self) -> f64 {
3180        self.tow_ms as f64 * 0.001
3181    }
3182    pub fn tow_ms(&self) -> u32 {
3183        self.tow_ms
3184    }
3185    pub fn wnc(&self) -> u16 {
3186        self.wnc
3187    }
3188
3189    pub fn num_idc(&self) -> usize {
3190        self.idc.len()
3191    }
3192}
3193
3194impl SbfBlockParse for GeoIonoDelayBlock {
3195    const BLOCK_ID: u16 = block_ids::GEO_IONO_DELAY;
3196
3197    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
3198        // Header (12) + PRN (1) + BandNbr (1) + IODI (1) + N (1) + SBLength (1) + Reserved (1)
3199        if data.len() < 18 {
3200            return Err(SbfError::ParseError("GEOIonoDelay too short".into()));
3201        }
3202
3203        let prn = data[12];
3204        let band_nbr = data[13];
3205        let iodi = data[14];
3206        let n = data[15] as usize;
3207        let sb_length = data[16] as usize;
3208
3209        if sb_length < 8 {
3210            return Err(SbfError::ParseError(
3211                "GEOIonoDelay SBLength too small".into(),
3212            ));
3213        }
3214
3215        let mut idc = Vec::with_capacity(n);
3216        let mut offset = 18;
3217
3218        for _ in 0..n {
3219            if offset + sb_length > data.len() {
3220                break;
3221            }
3222
3223            let igp_mask_no = data[offset];
3224            let givei = data[offset + 1];
3225            let vertical_delay_m_raw =
3226                f32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
3227
3228            idc.push(GeoIonoDelayIdc {
3229                igp_mask_no,
3230                givei,
3231                vertical_delay_m_raw,
3232            });
3233
3234            offset += sb_length;
3235        }
3236
3237        Ok(Self {
3238            tow_ms: header.tow_ms,
3239            wnc: header.wnc,
3240            prn,
3241            band_nbr,
3242            iodi,
3243            idc,
3244        })
3245    }
3246}
3247
3248#[cfg(test)]
3249mod tests {
3250    use super::*;
3251    use crate::blocks::SbfBlock;
3252    use crate::header::{SbfHeader, SBF_SYNC};
3253
3254    fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
3255        SbfHeader {
3256            crc: 0,
3257            block_id,
3258            block_rev: 0,
3259            length: (data_len + 2) as u16,
3260            tow_ms,
3261            wnc,
3262        }
3263    }
3264
3265    #[test]
3266    fn test_gps_alm_accessors() {
3267        let block = GpsAlmBlock {
3268            tow_ms: 1000,
3269            wnc: 2000,
3270            prn: 5,
3271            e: F32_DNU,
3272            t_oa: 100,
3273            delta_i: 0.1,
3274            omega_dot: 0.2,
3275            sqrt_a: 5153.5,
3276            omega_0: 1.0,
3277            omega: 1.1,
3278            m_0: 0.5,
3279            a_f1: 0.0,
3280            a_f0: 0.0,
3281            wn_a: 10,
3282            as_config: 1,
3283            health8: 0,
3284            health6: 0,
3285        };
3286
3287        assert!((block.tow_seconds() - 1.0).abs() < 1e-6);
3288        assert!(block.eccentricity().is_none());
3289        assert!((block.semi_major_axis_m().unwrap() - 5153.5_f32.powi(2)).abs() < 1e-3);
3290    }
3291
3292    #[test]
3293    fn test_gps_alm_parse() {
3294        let mut data = vec![0u8; 57];
3295        data[12] = 7;
3296        data[13..17].copy_from_slice(&0.02_f32.to_le_bytes());
3297        data[17..21].copy_from_slice(&1234_u32.to_le_bytes());
3298        data[21..25].copy_from_slice(&0.1_f32.to_le_bytes());
3299        data[25..29].copy_from_slice(&0.2_f32.to_le_bytes());
3300        data[29..33].copy_from_slice(&5153.8_f32.to_le_bytes());
3301        data[33..37].copy_from_slice(&1.0_f32.to_le_bytes());
3302        data[37..41].copy_from_slice(&1.1_f32.to_le_bytes());
3303        data[41..45].copy_from_slice(&0.5_f32.to_le_bytes());
3304        data[45..49].copy_from_slice(&0.01_f32.to_le_bytes());
3305        data[49..53].copy_from_slice(&0.02_f32.to_le_bytes());
3306        data[53] = 12;
3307        data[54] = 1;
3308        data[55] = 0;
3309        data[56] = 0;
3310
3311        let header = header_for(block_ids::GPS_ALM, data.len(), 5000, 2001);
3312        let block = GpsAlmBlock::parse(&header, &data).unwrap();
3313
3314        assert_eq!(block.prn, 7);
3315        assert_eq!(block.wnc(), 2001);
3316        assert_eq!(block.t_oa, 1234);
3317        assert!((block.eccentricity().unwrap() - 0.02).abs() < 1e-6);
3318    }
3319
3320    #[test]
3321    fn test_gps_ion_accessors() {
3322        let block = GpsIonBlock {
3323            tow_ms: 2500,
3324            wnc: 2100,
3325            prn: 3,
3326            alpha_0: F32_DNU,
3327            alpha_1: 0.1,
3328            alpha_2: 0.2,
3329            alpha_3: 0.3,
3330            beta_0: 1.0,
3331            beta_1: 2.0,
3332            beta_2: 3.0,
3333            beta_3: 4.0,
3334        };
3335
3336        assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
3337        assert!(block.alpha_0().is_none());
3338        assert!((block.beta_0().unwrap() - 1.0).abs() < 1e-6);
3339    }
3340
3341    #[test]
3342    fn test_gps_ion_parse() {
3343        let mut data = vec![0u8; 45];
3344        data[12] = 8;
3345        data[13..17].copy_from_slice(&0.1_f32.to_le_bytes());
3346        data[17..21].copy_from_slice(&0.2_f32.to_le_bytes());
3347        data[21..25].copy_from_slice(&0.3_f32.to_le_bytes());
3348        data[25..29].copy_from_slice(&0.4_f32.to_le_bytes());
3349        data[29..33].copy_from_slice(&1.1_f32.to_le_bytes());
3350        data[33..37].copy_from_slice(&1.2_f32.to_le_bytes());
3351        data[37..41].copy_from_slice(&1.3_f32.to_le_bytes());
3352        data[41..45].copy_from_slice(&1.4_f32.to_le_bytes());
3353
3354        let header = header_for(block_ids::GPS_ION, data.len(), 8000, 2002);
3355        let block = GpsIonBlock::parse(&header, &data).unwrap();
3356
3357        assert_eq!(block.prn, 8);
3358        assert!((block.alpha_1 - 0.2).abs() < 1e-6);
3359        assert!((block.beta_3 - 1.4).abs() < 1e-6);
3360    }
3361
3362    #[test]
3363    fn test_gps_utc_accessors() {
3364        let block = GpsUtcBlock {
3365            tow_ms: 3000,
3366            wnc: 2200,
3367            prn: 1,
3368            a_1: 0.001,
3369            a_0: F64_DNU,
3370            t_ot: 4000,
3371            wn_t: 12,
3372            delta_t_ls: 18,
3373            wn_lsf: 13,
3374            dn: 2,
3375            delta_t_lsf: 19,
3376        };
3377
3378        assert!((block.tow_seconds() - 3.0).abs() < 1e-6);
3379        assert!(block.utc_bias_s().is_none());
3380        assert!((block.utc_drift_s_per_s().unwrap() - 0.001).abs() < 1e-6);
3381    }
3382
3383    #[test]
3384    fn test_gps_utc_parse() {
3385        let mut data = vec![0u8; 34];
3386        data[12] = 2;
3387        data[13..17].copy_from_slice(&0.001_f32.to_le_bytes());
3388        data[17..25].copy_from_slice(&(-0.5_f64).to_le_bytes());
3389        data[25..29].copy_from_slice(&12345_u32.to_le_bytes());
3390        data[29] = 6;
3391        data[30] = 18u8;
3392        data[31] = 7;
3393        data[32] = 4;
3394        data[33] = 19u8;
3395
3396        let header = header_for(block_ids::GPS_UTC, data.len(), 9000, 2003);
3397        let block = GpsUtcBlock::parse(&header, &data).unwrap();
3398
3399        assert_eq!(block.prn, 2);
3400        assert_eq!(block.wn_t, 6);
3401        assert!((block.utc_bias_s().unwrap() + 0.5).abs() < 1e-9);
3402    }
3403
3404    #[test]
3405    fn test_glo_alm_accessors() {
3406        let block = GloAlmBlock {
3407            tow_ms: 500,
3408            wnc: 2300,
3409            svid: 40,
3410            freq_nr: -3,
3411            epsilon: F32_DNU,
3412            t_oa: 200,
3413            delta_i: 0.1,
3414            lambda: 0.2,
3415            t_ln: 100.0,
3416            omega: 0.3,
3417            delta_t: 0.4,
3418            d_delta_t: 0.5,
3419            tau: 0.0,
3420            wn_a: 5,
3421            c: 0,
3422            n: 10,
3423            m_type: 1,
3424            n_4: 2,
3425        };
3426
3427        assert!((block.tow_seconds() - 0.5).abs() < 1e-6);
3428        assert_eq!(block.slot(), 3);
3429        assert!(block.eccentricity().is_none());
3430        assert!(block.clock_bias_s().is_some());
3431    }
3432
3433    #[test]
3434    fn test_glo_alm_parse() {
3435        let mut data = vec![0u8; 56];
3436        data[12] = 38;
3437        data[13] = 250u8;
3438        data[14..18].copy_from_slice(&0.01_f32.to_le_bytes());
3439        data[18..22].copy_from_slice(&200_u32.to_le_bytes());
3440        data[22..26].copy_from_slice(&0.1_f32.to_le_bytes());
3441        data[26..30].copy_from_slice(&0.2_f32.to_le_bytes());
3442        data[30..34].copy_from_slice(&300.0_f32.to_le_bytes());
3443        data[34..38].copy_from_slice(&0.3_f32.to_le_bytes());
3444        data[38..42].copy_from_slice(&0.4_f32.to_le_bytes());
3445        data[42..46].copy_from_slice(&0.5_f32.to_le_bytes());
3446        data[46..50].copy_from_slice(&0.6_f32.to_le_bytes());
3447        data[50] = 3;
3448        data[51] = 0;
3449        data[52..54].copy_from_slice(&15_u16.to_le_bytes());
3450        data[54] = 1;
3451        data[55] = 2;
3452
3453        let header = header_for(block_ids::GLO_ALM, data.len(), 11000, 2301);
3454        let block = GloAlmBlock::parse(&header, &data).unwrap();
3455
3456        assert_eq!(block.svid, 38);
3457        assert_eq!(block.slot(), 1);
3458        assert_eq!(block.n, 15);
3459        assert!((block.clock_bias_s().unwrap() - 0.6).abs() < 1e-6);
3460    }
3461
3462    #[test]
3463    fn test_glo_time_accessors() {
3464        let block = GloTimeBlock {
3465            tow_ms: 750,
3466            wnc: 2400,
3467            svid: 41,
3468            freq_nr: 1,
3469            n_4: 3,
3470            kp: 2,
3471            n: 12,
3472            tau_gps: F32_DNU,
3473            tau_c: 0.2,
3474            b1: 0.01,
3475            b2: 0.02,
3476        };
3477
3478        assert!((block.tow_seconds() - 0.75).abs() < 1e-6);
3479        assert_eq!(block.slot(), 4);
3480        assert!(block.gps_glonass_offset_s().is_none());
3481        assert!((block.time_scale_correction_s().unwrap() - 0.2).abs() < 1e-9);
3482    }
3483
3484    #[test]
3485    fn test_glo_time_parse() {
3486        let mut data = vec![0u8; 38];
3487        data[12] = 39;
3488        data[13] = 1;
3489        data[14] = 5;
3490        data[15] = 1;
3491        data[16..18].copy_from_slice(&9_u16.to_le_bytes());
3492        data[18..22].copy_from_slice(&0.123_f32.to_le_bytes());
3493        data[22..30].copy_from_slice(&(-0.5_f64).to_le_bytes());
3494        data[30..34].copy_from_slice(&0.01_f32.to_le_bytes());
3495        data[34..38].copy_from_slice(&0.02_f32.to_le_bytes());
3496
3497        let header = header_for(block_ids::GLO_TIME, data.len(), 12000, 2401);
3498        let block = GloTimeBlock::parse(&header, &data).unwrap();
3499
3500        assert_eq!(block.svid, 39);
3501        assert_eq!(block.slot(), 2);
3502        assert_eq!(block.n, 9);
3503        assert!((block.time_scale_correction_s().unwrap() + 0.5).abs() < 1e-9);
3504    }
3505
3506    #[test]
3507    fn test_gal_alm_accessors() {
3508        let block = GalAlmBlock {
3509            tow_ms: 1500,
3510            wnc: 2500,
3511            svid: 71,
3512            source: 1,
3513            e: F32_DNU,
3514            t_oa: 100,
3515            delta_i: 0.1,
3516            omega_dot: 0.2,
3517            delta_sqrt_a: 0.3,
3518            omega_0: 1.0,
3519            omega: 1.1,
3520            m_0: 0.5,
3521            a_f1: 0.01,
3522            a_f0: 0.02,
3523            wn_a: 7,
3524            svid_a: 72,
3525            health: 0,
3526            ioda: 4,
3527        };
3528
3529        assert!((block.tow_seconds() - 1.5).abs() < 1e-6);
3530        assert_eq!(block.prn(), 1);
3531        assert!(block.eccentricity().is_none());
3532        assert!((block.delta_sqrt_a().unwrap() - 0.3).abs() < 1e-6);
3533    }
3534
3535    #[test]
3536    fn test_gal_alm_parse() {
3537        let mut data = vec![0u8; 59];
3538        data[12] = 72;
3539        data[13] = 2;
3540        data[14..18].copy_from_slice(&0.02_f32.to_le_bytes());
3541        data[18..22].copy_from_slice(&500_u32.to_le_bytes());
3542        data[22..26].copy_from_slice(&0.1_f32.to_le_bytes());
3543        data[26..30].copy_from_slice(&0.2_f32.to_le_bytes());
3544        data[30..34].copy_from_slice(&0.3_f32.to_le_bytes());
3545        data[34..38].copy_from_slice(&1.0_f32.to_le_bytes());
3546        data[38..42].copy_from_slice(&1.1_f32.to_le_bytes());
3547        data[42..46].copy_from_slice(&0.5_f32.to_le_bytes());
3548        data[46..50].copy_from_slice(&0.01_f32.to_le_bytes());
3549        data[50..54].copy_from_slice(&0.02_f32.to_le_bytes());
3550        data[54] = 9;
3551        data[55] = 73;
3552        data[56..58].copy_from_slice(&0x1234_u16.to_le_bytes());
3553        data[58] = 6;
3554
3555        let header = header_for(block_ids::GAL_ALM, data.len(), 13000, 2501);
3556        let block = GalAlmBlock::parse(&header, &data).unwrap();
3557
3558        assert_eq!(block.svid, 72);
3559        assert_eq!(block.prn(), 2);
3560        assert_eq!(block.wn_a, 9);
3561        assert_eq!(block.health, 0x1234);
3562    }
3563
3564    #[test]
3565    fn test_gal_ion_accessors() {
3566        let block = GalIonBlock {
3567            tow_ms: 1600,
3568            wnc: 2600,
3569            svid: 75,
3570            source: 16,
3571            a_i0: F32_DNU,
3572            a_i1: 0.1,
3573            a_i2: 0.2,
3574            storm_flags: 1,
3575        };
3576
3577        assert!((block.tow_seconds() - 1.6).abs() < 1e-6);
3578        assert!(block.is_fnav());
3579        assert!(!block.is_inav());
3580        assert!(block.a_i0().is_none());
3581    }
3582
3583    #[test]
3584    fn test_gal_ion_parse() {
3585        let mut data = vec![0u8; 27];
3586        data[12] = 80;
3587        data[13] = 1;
3588        data[14..18].copy_from_slice(&0.1_f32.to_le_bytes());
3589        data[18..22].copy_from_slice(&0.2_f32.to_le_bytes());
3590        data[22..26].copy_from_slice(&0.3_f32.to_le_bytes());
3591        data[26] = 2;
3592
3593        let header = header_for(block_ids::GAL_ION, data.len(), 14000, 2601);
3594        let block = GalIonBlock::parse(&header, &data).unwrap();
3595
3596        assert_eq!(block.svid, 80);
3597        assert!((block.a_i1 - 0.2).abs() < 1e-6);
3598        assert_eq!(block.storm_flags, 2);
3599    }
3600
3601    #[test]
3602    fn test_gal_utc_accessors() {
3603        let block = GalUtcBlock {
3604            tow_ms: 1700,
3605            wnc: 2700,
3606            svid: 76,
3607            source: 1,
3608            a_1: 0.001,
3609            a_0: F64_DNU,
3610            t_ot: 1000,
3611            wn_ot: 5,
3612            delta_t_ls: 18,
3613            wn_lsf: 6,
3614            dn: 3,
3615            delta_t_lsf: 19,
3616        };
3617
3618        assert!((block.tow_seconds() - 1.7).abs() < 1e-6);
3619        assert_eq!(block.prn(), 6);
3620        assert!(block.utc_bias_s().is_none());
3621        assert!((block.utc_drift_s_per_s().unwrap() - 0.001).abs() < 1e-6);
3622    }
3623
3624    #[test]
3625    fn test_gal_utc_parse() {
3626        let mut data = vec![0u8; 35];
3627        data[12] = 74;
3628        data[13] = 2;
3629        data[14..18].copy_from_slice(&0.002_f32.to_le_bytes());
3630        data[18..26].copy_from_slice(&1.25_f64.to_le_bytes());
3631        data[26..30].copy_from_slice(&800_u32.to_le_bytes());
3632        data[30] = 4;
3633        data[31] = 18u8;
3634        data[32] = 5;
3635        data[33] = 2;
3636        data[34] = 19u8;
3637
3638        let header = header_for(block_ids::GAL_UTC, data.len(), 15000, 2701);
3639        let block = GalUtcBlock::parse(&header, &data).unwrap();
3640
3641        assert_eq!(block.svid, 74);
3642        assert_eq!(block.wn_ot, 4);
3643        assert!((block.utc_bias_s().unwrap() - 1.25).abs() < 1e-9);
3644    }
3645
3646    #[test]
3647    fn test_gal_gst_gps_accessors() {
3648        let block = GalGstGpsBlock {
3649            tow_ms: 1800,
3650            wnc: 2800,
3651            svid: 71,
3652            source: 1,
3653            a_1g: F32_DNU,
3654            a_0g: 0.3,
3655            t_og: 7,
3656            wn_og: 8,
3657        };
3658
3659        assert!((block.tow_seconds() - 1.8).abs() < 1e-6);
3660        assert_eq!(block.prn(), 1);
3661        assert!(block.gst_gps_drift_s_per_s().is_none());
3662        assert!((block.gst_gps_offset_s().unwrap() - 0.3).abs() < 1e-6);
3663    }
3664
3665    #[test]
3666    fn test_gal_gst_gps_parse() {
3667        let mut data = vec![0u8; 27];
3668        data[12] = 72;
3669        data[13] = 0;
3670        data[14..18].copy_from_slice(&0.01_f32.to_le_bytes());
3671        data[18..22].copy_from_slice(&0.02_f32.to_le_bytes());
3672        data[22..26].copy_from_slice(&9_u32.to_le_bytes());
3673        data[26] = 10;
3674
3675        let header = header_for(block_ids::GAL_GST_GPS, data.len(), 16000, 2801);
3676        let block = GalGstGpsBlock::parse(&header, &data).unwrap();
3677
3678        assert_eq!(block.svid, 72);
3679        assert_eq!(block.t_og, 9);
3680        assert_eq!(block.wn_og, 10);
3681    }
3682
3683    #[test]
3684    fn test_gps_cnav_parse() {
3685        let mut data = vec![0u8; 170];
3686        data[12] = 12;
3687        data[13] = 0x80;
3688        data[14..16].copy_from_slice(&2045_u16.to_le_bytes());
3689        data[17] = (-2_i8) as u8;
3690        data[18..22].copy_from_slice(&1000_u32.to_le_bytes());
3691        data[22..26].copy_from_slice(&2000_u32.to_le_bytes());
3692        data[50..58].copy_from_slice(&0.5_f64.to_le_bytes());
3693        data[150..154].copy_from_slice(&(-1.25_f32).to_le_bytes());
3694        data[166..170].copy_from_slice(&0.0002_f32.to_le_bytes());
3695
3696        let header = header_for(block_ids::GPS_CNAV, data.len(), 17000, 2900);
3697        let block = GpsCNavBlock::parse(&header, &data).unwrap();
3698
3699        assert_eq!(block.prn, 12);
3700        assert_eq!(block.wn, 2045);
3701        assert_eq!(block.ura_ed, -2);
3702        assert_eq!(block.t_oe, 2000);
3703        assert!((block.m_0 - 0.5).abs() < 1e-12);
3704        assert!((block.t_gd + 1.25).abs() < 1e-6);
3705        assert!((block.isc_l5q5 - 0.0002).abs() < 1e-9);
3706    }
3707
3708    #[test]
3709    fn test_bds_ion_parse() {
3710        let mut data = vec![0u8; 46];
3711        data[12] = 7;
3712        data[14..18].copy_from_slice(&0.1_f32.to_le_bytes());
3713        data[30..34].copy_from_slice(&1.1_f32.to_le_bytes());
3714        data[42..46].copy_from_slice(&4.4_f32.to_le_bytes());
3715
3716        let header = header_for(block_ids::BDS_ION, data.len(), 18000, 2901);
3717        let block = BdsIonBlock::parse(&header, &data).unwrap();
3718
3719        assert_eq!(block.prn, 7);
3720        assert!((block.alpha_0 - 0.1).abs() < 1e-6);
3721        assert!((block.beta_0 - 1.1).abs() < 1e-6);
3722        assert!((block.beta_3 - 4.4).abs() < 1e-6);
3723    }
3724
3725    #[test]
3726    fn test_bds_cnav1_parse() {
3727        let mut data = vec![0u8; 158];
3728        data[12] = 3;
3729        data[13] = 0x02;
3730        data[14..18].copy_from_slice(&345600_u32.to_le_bytes());
3731        data[74..78].copy_from_slice(&1.25_f32.to_le_bytes());
3732        data[143] = 9;
3733        data[144..146].copy_from_slice(&512_u16.to_le_bytes());
3734        data[146..150].copy_from_slice(&0.001_f32.to_le_bytes());
3735        data[154..158].copy_from_slice(&(-0.002_f32).to_le_bytes());
3736
3737        let header = header_for(block_ids::BDS_CNAV1, data.len(), 19000, 2902);
3738        let block = BdsCNav1Block::parse(&header, &data).unwrap();
3739
3740        assert_eq!(block.prn_idx, 3);
3741        assert_eq!(block.flags & 0x03, 0x02);
3742        assert_eq!(block.t_oe, 345600);
3743        assert!((block.omega_dot - 1.25).abs() < 1e-6);
3744        assert_eq!(block.iode, 9);
3745        assert_eq!(block.iodc, 512);
3746        assert!((block.isc_b1cd - 0.001).abs() < 1e-6);
3747        assert!((block.t_gd_b2ap + 0.002).abs() < 1e-6);
3748    }
3749
3750    #[test]
3751    fn test_geo_iono_delay_accessors() {
3752        let block = GeoIonoDelayBlock {
3753            tow_ms: 2500,
3754            wnc: 2100,
3755            prn: 120,
3756            band_nbr: 3,
3757            iodi: 5,
3758            idc: vec![GeoIonoDelayIdc {
3759                igp_mask_no: 10,
3760                givei: 4,
3761                vertical_delay_m_raw: F32_DNU,
3762            }],
3763        };
3764
3765        assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
3766        assert_eq!(block.num_idc(), 1);
3767        assert!(block.idc[0].vertical_delay_m().is_none());
3768    }
3769
3770    #[test]
3771    fn test_geo_iono_delay_parse() {
3772        let mut data = vec![0u8; 18 + (2 * 8)];
3773        data[12] = 120; // PRN
3774        data[13] = 3; // BandNbr
3775        data[14] = 5; // IODI
3776        data[15] = 2; // N
3777        data[16] = 8; // SBLength
3778        data[17] = 0; // Reserved
3779
3780        // IDC #1
3781        data[18] = 10; // IGPMaskNo
3782        data[19] = 4; // GIVEI
3783        data[22..26].copy_from_slice(&12.5_f32.to_le_bytes()); // VerticalDelay
3784
3785        // IDC #2
3786        data[26] = 11; // IGPMaskNo
3787        data[27] = 5; // GIVEI
3788        data[30..34].copy_from_slice(&F32_DNU.to_le_bytes()); // VerticalDelay DNU
3789
3790        let header = header_for(block_ids::GEO_IONO_DELAY, data.len(), 4321, 2024);
3791        let block = GeoIonoDelayBlock::parse(&header, &data).unwrap();
3792
3793        assert_eq!(block.prn, 120);
3794        assert_eq!(block.band_nbr, 3);
3795        assert_eq!(block.iodi, 5);
3796        assert_eq!(block.num_idc(), 2);
3797        assert_eq!(block.idc[0].igp_mask_no, 10);
3798        assert_eq!(block.idc[1].givei, 5);
3799        assert!((block.idc[0].vertical_delay_m().unwrap() - 12.5).abs() < 1e-6);
3800        assert!(block.idc[1].vertical_delay_m().is_none());
3801    }
3802
3803    #[test]
3804    fn test_geo_iono_delay_sbf_block_parse() {
3805        let total_len = 36usize; // sync + header + payload (+padding)
3806        let mut data = vec![0u8; total_len];
3807        data[0..2].copy_from_slice(&SBF_SYNC);
3808        data[2..4].copy_from_slice(&0_u16.to_le_bytes()); // CRC
3809        data[4..6].copy_from_slice(&block_ids::GEO_IONO_DELAY.to_le_bytes()); // ID/Rev
3810        data[6..8].copy_from_slice(&(total_len as u16).to_le_bytes()); // Length
3811        data[8..12].copy_from_slice(&9876_u32.to_le_bytes()); // TOW
3812        data[12..14].copy_from_slice(&2025_u16.to_le_bytes()); // WNc
3813
3814        // Payload starts at absolute offset 14 (block_data offset 12)
3815        data[14] = 120; // PRN
3816        data[15] = 2; // BandNbr
3817        data[16] = 7; // IODI
3818        data[17] = 2; // N
3819        data[18] = 8; // SBLength
3820        data[19] = 0; // Reserved
3821
3822        // IDC #1
3823        data[20] = 10; // IGPMaskNo
3824        data[21] = 3; // GIVEI
3825        data[24..28].copy_from_slice(&8.25_f32.to_le_bytes());
3826
3827        // IDC #2
3828        data[28] = 11; // IGPMaskNo
3829        data[29] = 6; // GIVEI
3830        data[32..36].copy_from_slice(&9.75_f32.to_le_bytes());
3831
3832        let (block, used) = SbfBlock::parse(&data).unwrap();
3833        assert_eq!(used, total_len);
3834        assert_eq!(block.block_id(), block_ids::GEO_IONO_DELAY);
3835        match block {
3836            SbfBlock::GeoIonoDelay(geo) => {
3837                assert_eq!(geo.tow_ms(), 9876);
3838                assert_eq!(geo.wnc(), 2025);
3839                assert_eq!(geo.num_idc(), 2);
3840                assert!((geo.idc[0].vertical_delay_m().unwrap() - 8.25).abs() < 1e-6);
3841                assert!((geo.idc[1].vertical_delay_m().unwrap() - 9.75).abs() < 1e-6);
3842            }
3843            _ => panic!("Expected GeoIonoDelay block"),
3844        }
3845    }
3846
3847    #[test]
3848    fn test_gps_raw_ca_parse() {
3849        let header = header_for(4017, 57, 5000, 2150);
3850        let mut data = vec![0u8; 57];
3851        data[12] = 12; // SVID
3852        data[13] = 1; // CRCPassed
3853        data[14] = 0; // ViterbiCount
3854        data[15] = 2; // Source
3855        data[16] = 0; // FreqNr
3856        data[17..57].copy_from_slice(&[0xABu8; 40]); // NAVBits
3857
3858        let block = GpsRawCaBlock::parse(&header, &data).unwrap();
3859        assert_eq!(block.tow_seconds(), 5.0);
3860        assert_eq!(block.wnc(), 2150);
3861        assert_eq!(block.svid, 12);
3862        assert!(block.crc_ok());
3863        assert_eq!(block.viterbi_count, 0);
3864        assert_eq!(block.nav_bits_slice()[0], 0xAB);
3865    }
3866
3867    #[test]
3868    fn test_gps_raw_ca_too_short() {
3869        let header = header_for(4017, 57, 0, 0);
3870        let data = [0u8; 50];
3871        assert!(GpsRawCaBlock::parse(&header, &data).is_err());
3872    }
3873
3874    #[test]
3875    fn test_gps_raw_l2c_parse() {
3876        let header = header_for(4018, 57, 6000, 2200);
3877        let mut data = vec![0u8; 57];
3878        data[12] = 8;
3879        data[13] = 1;
3880        data[17..57].copy_from_slice(&[0xCDu8; 40]);
3881
3882        let block = GpsRawL2CBlock::parse(&header, &data).unwrap();
3883        assert_eq!(block.tow_seconds(), 6.0);
3884        assert_eq!(block.svid, 8);
3885        assert!(block.crc_ok());
3886        assert_eq!(block.nav_bits_slice()[0], 0xCD);
3887    }
3888
3889    #[test]
3890    fn test_gps_raw_l5_parse() {
3891        let header = header_for(4019, 57, 7000, 2250);
3892        let mut data = vec![0u8; 57];
3893        data[12] = 15;
3894        data[13] = 0;
3895
3896        let block = GpsRawL5Block::parse(&header, &data).unwrap();
3897        assert_eq!(block.tow_seconds(), 7.0);
3898        assert_eq!(block.svid, 15);
3899        assert!(!block.crc_ok());
3900    }
3901
3902    #[test]
3903    fn test_glo_raw_ca_parse() {
3904        let header = header_for(4026, 29, 8000, 2300);
3905        let mut data = vec![0u8; 29];
3906        data[12] = 45;
3907        data[13] = 1;
3908        data[16] = 3;
3909        data[17..29].copy_from_slice(&[0x12u8; 12]);
3910
3911        let block = GloRawCaBlock::parse(&header, &data).unwrap();
3912        assert_eq!(block.tow_seconds(), 8.0);
3913        assert_eq!(block.svid, 45);
3914        assert_eq!(block.freq_nr, 3);
3915        assert_eq!(block.nav_bits_slice()[0], 0x12);
3916    }
3917
3918    #[test]
3919    fn test_glo_raw_ca_too_short() {
3920        let header = header_for(4026, 29, 0, 0);
3921        let data = [0u8; 25];
3922        assert!(GloRawCaBlock::parse(&header, &data).is_err());
3923    }
3924
3925    #[test]
3926    fn test_gal_raw_fnav_parse() {
3927        let header = header_for(4022, 49, 1000, 2100);
3928        let mut data = vec![0u8; 49];
3929        data[12] = 85; // Galileo SVID
3930        data[13] = 1;
3931        data[17..49].copy_from_slice(&[0x11u8; 32]);
3932        let block = GalRawFnavBlock::parse(&header, &data).unwrap();
3933        assert_eq!(block.tow_seconds(), 1.0);
3934        assert_eq!(block.svid, 85);
3935        assert!(block.crc_ok());
3936        assert_eq!(block.nav_bits_slice()[0], 0x11);
3937    }
3938
3939    #[test]
3940    fn test_gal_raw_inav_parse() {
3941        let header = header_for(4023, 49, 2000, 2101);
3942        let mut data = vec![0u8; 49];
3943        data[12] = 72;
3944        data[13] = 0;
3945        let block = GalRawInavBlock::parse(&header, &data).unwrap();
3946        assert_eq!(block.tow_seconds(), 2.0);
3947        assert!(!block.crc_ok());
3948    }
3949
3950    #[test]
3951    fn test_gal_raw_cnav_parse() {
3952        let header = header_for(4024, 81, 3000, 2102);
3953        let mut data = vec![0u8; 81];
3954        data[12] = 90;
3955        data[17..81].copy_from_slice(&[0x22u8; 64]);
3956        let block = GalRawCnavBlock::parse(&header, &data).unwrap();
3957        assert_eq!(block.tow_seconds(), 3.0);
3958        assert_eq!(block.nav_bits_slice()[63], 0x22);
3959    }
3960
3961    #[test]
3962    fn test_geo_raw_l1_parse() {
3963        let header = header_for(4020, 49, 4000, 2103);
3964        let mut data = vec![0u8; 49];
3965        data[12] = 135; // SBAS PRN
3966        data[13] = 1;
3967        data[17..49].copy_from_slice(&[0x33u8; 32]);
3968        let block = GeoRawL1Block::parse(&header, &data).unwrap();
3969        assert_eq!(block.tow_seconds(), 4.0);
3970        assert_eq!(block.svid, 135);
3971    }
3972
3973    #[test]
3974    fn test_cmp_raw_parse() {
3975        let header = header_for(4047, 57, 5000, 2104);
3976        let mut data = vec![0u8; 57];
3977        data[12] = 155; // BeiDou SVID
3978        data[17..57].copy_from_slice(&[0x44u8; 40]);
3979        let block = CmpRawBlock::parse(&header, &data).unwrap();
3980        assert_eq!(block.tow_seconds(), 5.0);
3981        assert_eq!(block.nav_bits_slice()[39], 0x44);
3982    }
3983
3984    #[test]
3985    fn test_qzs_raw_l1ca_parse() {
3986        let header = header_for(4066, 57, 6000, 2105);
3987        let mut data = vec![0u8; 57];
3988        data[12] = 183; // QZSS SVID
3989        data[13] = 1;
3990        let block = QzsRawL1CaBlock::parse(&header, &data).unwrap();
3991        assert_eq!(block.tow_seconds(), 6.0);
3992        assert_eq!(block.svid, 183);
3993    }
3994
3995    #[test]
3996    fn test_qzs_raw_l2c_parse() {
3997        let header = header_for(4067, 57, 7000, 2106);
3998        let mut data = vec![0u8; 57];
3999        data[12] = 186;
4000        let block = QzsRawL2CBlock::parse(&header, &data).unwrap();
4001        assert_eq!(block.tow_seconds(), 7.0);
4002    }
4003
4004    #[test]
4005    fn test_qzs_raw_l5_parse() {
4006        let header = header_for(4068, 57, 8000, 2107);
4007        let mut data = vec![0u8; 57];
4008        data[12] = 181;
4009        let block = QzsRawL5Block::parse(&header, &data).unwrap();
4010        assert_eq!(block.tow_seconds(), 8.0);
4011    }
4012
4013    #[test]
4014    fn test_gal_raw_fnav_too_short() {
4015        let header = header_for(4022, 49, 0, 0);
4016        assert!(GalRawFnavBlock::parse(&header, &[0u8; 40]).is_err());
4017    }
4018
4019    #[test]
4020    fn test_gal_raw_cnav_too_short() {
4021        let header = header_for(4024, 81, 0, 0);
4022        assert!(GalRawCnavBlock::parse(&header, &[0u8; 70]).is_err());
4023    }
4024
4025    #[test]
4026    fn test_gal_sar_rlm_accessors() {
4027        let block = GalSarRlmBlock {
4028            tow_ms: 4500,
4029            wnc: 2200,
4030            svid: 74,
4031            source: 2,
4032            rlm_length_bits: 80,
4033            rlm_bits_words: vec![0x8000_0000, 0, 0x0001_0000],
4034        };
4035
4036        assert!((block.tow_seconds() - 4.5).abs() < 1e-6);
4037        assert_eq!(block.prn(), 4);
4038        assert_eq!(block.rlm_length_bits(), 80);
4039        assert_eq!(block.rlm_bits_words().len(), 3);
4040        assert_eq!(block.bit(0), Some(true));
4041        assert_eq!(block.bit(1), Some(false));
4042        assert_eq!(block.bit(79), Some(true));
4043        assert_eq!(block.bit(80), None);
4044    }
4045
4046    #[test]
4047    fn test_gal_sar_rlm_parse() {
4048        let mut data = vec![0u8; 30];
4049        data[12] = 72; // SVID
4050        data[13] = 16; // Source (F/NAV)
4051        data[14] = 80; // RLMLength in bits
4052        data[18..22].copy_from_slice(&0x1234_5678_u32.to_le_bytes());
4053        data[22..26].copy_from_slice(&0x9abc_def0_u32.to_le_bytes());
4054        data[26..30].copy_from_slice(&0x0001_0000_u32.to_le_bytes());
4055
4056        let header = header_for(block_ids::GAL_SAR_RLM, data.len(), 3210, 2048);
4057        let block = GalSarRlmBlock::parse(&header, &data).unwrap();
4058
4059        assert_eq!(block.svid, 72);
4060        assert_eq!(block.source, 16);
4061        assert_eq!(block.rlm_length_bits(), 80);
4062        assert_eq!(
4063            block.rlm_bits_words(),
4064            &[0x1234_5678, 0x9abc_def0, 0x0001_0000]
4065        );
4066        assert_eq!(block.bit(79), Some(true));
4067    }
4068
4069    #[test]
4070    fn test_gal_sar_rlm_too_short() {
4071        let header = header_for(block_ids::GAL_SAR_RLM, 30, 0, 0);
4072        assert!(GalSarRlmBlock::parse(&header, &[0u8; 20]).is_err());
4073    }
4074}