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// GALSARRLM Block
1500// ============================================================================
1501
1502/// GALSARRLM block (Block ID 4034)
1503///
1504/// Decoded Galileo search-and-rescue return link message (RLM).
1505#[derive(Debug, Clone)]
1506pub struct GalSarRlmBlock {
1507    tow_ms: u32,
1508    wnc: u16,
1509    /// SVID (71-106 for Galileo)
1510    pub svid: u8,
1511    /// Message source (2=I/NAV, 16=F/NAV)
1512    pub source: u8,
1513    /// RLM payload length in bits (typically 80 or 160)
1514    rlm_length_bits: u8,
1515    /// RLM payload words, MSB-first within each word.
1516    rlm_bits_words: Vec<u32>,
1517}
1518
1519impl GalSarRlmBlock {
1520    pub fn tow_seconds(&self) -> f64 {
1521        self.tow_ms as f64 * 0.001
1522    }
1523    pub fn tow_ms(&self) -> u32 {
1524        self.tow_ms
1525    }
1526    pub fn wnc(&self) -> u16 {
1527        self.wnc
1528    }
1529
1530    /// Get PRN number (1-36) when SVID is in Galileo range.
1531    pub fn prn(&self) -> u8 {
1532        if (71..=106).contains(&self.svid) {
1533            self.svid - 70
1534        } else {
1535            self.svid
1536        }
1537    }
1538
1539    pub fn rlm_length_bits(&self) -> u8 {
1540        self.rlm_length_bits
1541    }
1542
1543    /// Raw 32-bit words containing the RLM payload bits.
1544    pub fn rlm_bits_words(&self) -> &[u32] {
1545        &self.rlm_bits_words
1546    }
1547
1548    /// Return one RLM bit by index (0-based), with bit 0 as the MSB of word 0.
1549    pub fn bit(&self, bit_index: usize) -> Option<bool> {
1550        if bit_index >= self.rlm_length_bits as usize {
1551            return None;
1552        }
1553
1554        let word_index = bit_index / 32;
1555        let bit_in_word = 31 - (bit_index % 32);
1556        self.rlm_bits_words
1557            .get(word_index)
1558            .map(|word| ((word >> bit_in_word) & 1) != 0)
1559    }
1560}
1561
1562impl SbfBlockParse for GalSarRlmBlock {
1563    const BLOCK_ID: u16 = block_ids::GAL_SAR_RLM;
1564
1565    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1566        let block_len = header.length as usize;
1567        let data_len = block_len.saturating_sub(2);
1568        if data_len < 18 || data.len() < data_len {
1569            return Err(SbfError::ParseError("GALSARRLM too short".into()));
1570        }
1571
1572        let svid = data[12];
1573        let source = data[13];
1574        let rlm_length_bits = data[14];
1575
1576        let word_count = match rlm_length_bits {
1577            80 => 3,
1578            160 => 5,
1579            0 => 0,
1580            _ => usize::from(rlm_length_bits).div_ceil(32),
1581        };
1582        let required_len = 18 + word_count * 4;
1583        if data_len < required_len {
1584            return Err(SbfError::ParseError("GALSARRLM payload too short".into()));
1585        }
1586
1587        let mut rlm_bits_words = Vec::with_capacity(word_count);
1588        let mut offset = 18;
1589        for _ in 0..word_count {
1590            rlm_bits_words.push(u32::from_le_bytes(
1591                data[offset..offset + 4].try_into().unwrap(),
1592            ));
1593            offset += 4;
1594        }
1595
1596        Ok(Self {
1597            tow_ms: header.tow_ms,
1598            wnc: header.wnc,
1599            svid,
1600            source,
1601            rlm_length_bits,
1602            rlm_bits_words,
1603        })
1604    }
1605}
1606
1607// ============================================================================
1608// GPSCNav Block
1609// ============================================================================
1610
1611/// GPSCNav block (Block ID 4042)
1612///
1613/// Decoded GPS CNAV navigation data from L2C and/or L5 signals.
1614/// Contains ephemeris from MT10/11 and clock/ISC corrections from MT30.
1615#[derive(Debug, Clone)]
1616pub struct GpsCNavBlock {
1617    tow_ms: u32,
1618    wnc: u16,
1619    /// PRN number (1-32)
1620    pub prn: u8,
1621    /// Flags bit field (alert, integrity, L2C phasing, L2C/L5 used)
1622    pub flags: u8,
1623    /// Week number (13 bits from MT10)
1624    pub wn: u16,
1625    /// L1/L2/L5 signal health (3 bits from MT10)
1626    pub health: u8,
1627    /// Elevation-Dependent accuracy index (URA_ED)
1628    pub ura_ed: i8,
1629    /// Data predict time of week (seconds)
1630    pub t_op: u32,
1631    /// Ephemeris reference time (seconds)
1632    pub t_oe: u32,
1633    /// Semi-major axis (m)
1634    pub a: f64,
1635    /// Change rate in semi-major axis (m/s)
1636    pub a_dot: f64,
1637    /// Mean motion difference (semi-circles/s)
1638    pub delta_n: f32,
1639    /// Rate of mean motion difference (semi-circles/s^2)
1640    pub delta_n_dot: f32,
1641    /// Mean anomaly at reference time (semi-circles)
1642    pub m_0: f64,
1643    /// Eccentricity
1644    pub e: f64,
1645    /// Argument of perigee (semi-circles)
1646    pub omega: f64,
1647    /// Right ascension at reference time (semi-circles)
1648    pub omega_0: f64,
1649    /// Rate of right ascension (semi-circles/s)
1650    pub omega_dot: f64,
1651    /// Inclination angle at reference time (semi-circles)
1652    pub i_0: f64,
1653    /// Rate of inclination (semi-circles/s)
1654    pub i_dot: f32,
1655    /// Sine harmonic inclination correction (rad)
1656    pub c_is: f32,
1657    /// Cosine harmonic inclination correction (rad)
1658    pub c_ic: f32,
1659    /// Sine harmonic radius correction (m)
1660    pub c_rs: f32,
1661    /// Cosine harmonic radius correction (m)
1662    pub c_rc: f32,
1663    /// Sine harmonic latitude correction (rad)
1664    pub c_us: f32,
1665    /// Cosine harmonic latitude correction (rad)
1666    pub c_uc: f32,
1667    /// Clock reference time (seconds)
1668    pub t_oc: u32,
1669    /// Non-Elevation-Dependent accuracy index 0
1670    pub ura_ned0: i8,
1671    /// Non-Elevation-Dependent accuracy change index
1672    pub ura_ned1: u8,
1673    /// Non-Elevation-Dependent accuracy change rate index
1674    pub ura_ned2: u8,
1675    /// Week number associated with t_op (modulo 256)
1676    pub wn_op: u8,
1677    /// Clock drift rate (s/s^2)
1678    pub a_f2: f32,
1679    /// Clock drift (s/s)
1680    pub a_f1: f32,
1681    /// Clock bias (s)
1682    pub a_f0: f64,
1683    /// Group delay differential (s)
1684    pub t_gd: f32,
1685    /// Inter-Signal Correction for L1C/A (s)
1686    pub isc_l1ca: f32,
1687    /// Inter-Signal Correction for L2C (s)
1688    pub isc_l2c: f32,
1689    /// Inter-Signal Correction for L5I (s)
1690    pub isc_l5i5: f32,
1691    /// Inter-Signal Correction for L5Q (s)
1692    pub isc_l5q5: f32,
1693}
1694
1695impl GpsCNavBlock {
1696    pub fn tow_seconds(&self) -> f64 {
1697        self.tow_ms as f64 * 0.001
1698    }
1699    pub fn tow_ms(&self) -> u32 {
1700        self.tow_ms
1701    }
1702    pub fn wnc(&self) -> u16 {
1703        self.wnc
1704    }
1705
1706    /// Check if alert bit is set
1707    pub fn is_alert(&self) -> bool {
1708        (self.flags & 0x01) != 0
1709    }
1710
1711    /// Check if L2C was used for decoding
1712    pub fn l2c_used(&self) -> bool {
1713        (self.flags & 0x40) != 0
1714    }
1715
1716    /// Check if L5 was used for decoding
1717    pub fn l5_used(&self) -> bool {
1718        (self.flags & 0x80) != 0
1719    }
1720
1721    /// Check if satellite is healthy
1722    pub fn is_healthy(&self) -> bool {
1723        self.health == 0
1724    }
1725
1726    /// Group delay (None if DNU)
1727    pub fn group_delay_s(&self) -> Option<f32> {
1728        f32_or_none(self.t_gd)
1729    }
1730
1731    /// ISC L1C/A (None if DNU)
1732    pub fn isc_l1ca_s(&self) -> Option<f32> {
1733        f32_or_none(self.isc_l1ca)
1734    }
1735
1736    /// ISC L2C (None if DNU)
1737    pub fn isc_l2c_s(&self) -> Option<f32> {
1738        f32_or_none(self.isc_l2c)
1739    }
1740
1741    /// ISC L5I5 (None if DNU)
1742    pub fn isc_l5i5_s(&self) -> Option<f32> {
1743        f32_or_none(self.isc_l5i5)
1744    }
1745
1746    /// ISC L5Q5 (None if DNU)
1747    pub fn isc_l5q5_s(&self) -> Option<f32> {
1748        f32_or_none(self.isc_l5q5)
1749    }
1750}
1751
1752impl SbfBlockParse for GpsCNavBlock {
1753    const BLOCK_ID: u16 = block_ids::GPS_CNAV;
1754
1755    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1756        // Minimum size: header (12) + fields up to ISC_L5Q5 (170 bytes)
1757        if data.len() < 170 {
1758            return Err(SbfError::ParseError("GPSCNav too short".into()));
1759        }
1760
1761        // Offsets from data start (after sync):
1762        // 12: PRNidx (u1)
1763        // 13: Flags (u1)
1764        // 14-15: WN (u2)
1765        // 16: Health (u1)
1766        // 17: URA_ED (i1)
1767        // 18-21: t_op (u4)
1768        // 22-25: t_oe (u4)
1769        // 26-33: A (f8)
1770        // 34-41: A_DOT (f8)
1771        // 42-45: DELTA_N (f4)
1772        // 46-49: DELTA_N_DOT (f4)
1773        // 50-57: M_0 (f8)
1774        // 58-65: e (f8)
1775        // 66-73: omega (f8)
1776        // 74-81: OMEGA_0 (f8)
1777        // 82-89: OMEGADOT (f8)
1778        // 90-97: i_0 (f8)
1779        // 98-101: IDOT (f4)
1780        // 102-105: C_is (f4)
1781        // 106-109: C_ic (f4)
1782        // 110-113: C_rs (f4)
1783        // 114-117: C_rc (f4)
1784        // 118-121: C_us (f4)
1785        // 122-125: C_uc (f4)
1786        // 126-129: t_oc (u4)
1787        // 130: URA_NED0 (i1)
1788        // 131: URA_NED1 (u1)
1789        // 132: URA_NED2 (u1)
1790        // 133: WN_op (u1)
1791        // 134-137: a_f2 (f4)
1792        // 138-141: a_f1 (f4)
1793        // 142-149: a_f0 (f8)
1794        // 150-153: T_gd (f4)
1795        // 154-157: ISC_L1CA (f4)
1796        // 158-161: ISC_L2C (f4)
1797        // 162-165: ISC_L5I5 (f4)
1798        // 166-169: ISC_L5Q5 (f4)
1799
1800        let prn = data[12];
1801        let flags = data[13];
1802        let wn = u16::from_le_bytes([data[14], data[15]]);
1803        let health = data[16];
1804        let ura_ed = data[17] as i8;
1805        let t_op = u32::from_le_bytes(data[18..22].try_into().unwrap());
1806        let t_oe = u32::from_le_bytes(data[22..26].try_into().unwrap());
1807        let a = f64::from_le_bytes(data[26..34].try_into().unwrap());
1808        let a_dot = f64::from_le_bytes(data[34..42].try_into().unwrap());
1809        let delta_n = f32::from_le_bytes(data[42..46].try_into().unwrap());
1810        let delta_n_dot = f32::from_le_bytes(data[46..50].try_into().unwrap());
1811        let m_0 = f64::from_le_bytes(data[50..58].try_into().unwrap());
1812        let e = f64::from_le_bytes(data[58..66].try_into().unwrap());
1813        let omega = f64::from_le_bytes(data[66..74].try_into().unwrap());
1814        let omega_0 = f64::from_le_bytes(data[74..82].try_into().unwrap());
1815        let omega_dot = f64::from_le_bytes(data[82..90].try_into().unwrap());
1816        let i_0 = f64::from_le_bytes(data[90..98].try_into().unwrap());
1817        let i_dot = f32::from_le_bytes(data[98..102].try_into().unwrap());
1818        let c_is = f32::from_le_bytes(data[102..106].try_into().unwrap());
1819        let c_ic = f32::from_le_bytes(data[106..110].try_into().unwrap());
1820        let c_rs = f32::from_le_bytes(data[110..114].try_into().unwrap());
1821        let c_rc = f32::from_le_bytes(data[114..118].try_into().unwrap());
1822        let c_us = f32::from_le_bytes(data[118..122].try_into().unwrap());
1823        let c_uc = f32::from_le_bytes(data[122..126].try_into().unwrap());
1824        let t_oc = u32::from_le_bytes(data[126..130].try_into().unwrap());
1825        let ura_ned0 = data[130] as i8;
1826        let ura_ned1 = data[131];
1827        let ura_ned2 = data[132];
1828        let wn_op = data[133];
1829        let a_f2 = f32::from_le_bytes(data[134..138].try_into().unwrap());
1830        let a_f1 = f32::from_le_bytes(data[138..142].try_into().unwrap());
1831        let a_f0 = f64::from_le_bytes(data[142..150].try_into().unwrap());
1832        let t_gd = f32::from_le_bytes(data[150..154].try_into().unwrap());
1833        let isc_l1ca = f32::from_le_bytes(data[154..158].try_into().unwrap());
1834        let isc_l2c = f32::from_le_bytes(data[158..162].try_into().unwrap());
1835        let isc_l5i5 = f32::from_le_bytes(data[162..166].try_into().unwrap());
1836        let isc_l5q5 = f32::from_le_bytes(data[166..170].try_into().unwrap());
1837
1838        Ok(Self {
1839            tow_ms: header.tow_ms,
1840            wnc: header.wnc,
1841            prn,
1842            flags,
1843            wn,
1844            health,
1845            ura_ed,
1846            t_op,
1847            t_oe,
1848            a,
1849            a_dot,
1850            delta_n,
1851            delta_n_dot,
1852            m_0,
1853            e,
1854            omega,
1855            omega_0,
1856            omega_dot,
1857            i_0,
1858            i_dot,
1859            c_is,
1860            c_ic,
1861            c_rs,
1862            c_rc,
1863            c_us,
1864            c_uc,
1865            t_oc,
1866            ura_ned0,
1867            ura_ned1,
1868            ura_ned2,
1869            wn_op,
1870            a_f2,
1871            a_f1,
1872            a_f0,
1873            t_gd,
1874            isc_l1ca,
1875            isc_l2c,
1876            isc_l5i5,
1877            isc_l5q5,
1878        })
1879    }
1880}
1881
1882// ============================================================================
1883// BDSIon Block
1884// ============================================================================
1885
1886/// BDSIon block (Block ID 4120)
1887///
1888/// BeiDou ionosphere parameters (Klobuchar coefficients) from D1/D2 nav message.
1889#[derive(Debug, Clone)]
1890pub struct BdsIonBlock {
1891    tow_ms: u32,
1892    wnc: u16,
1893    /// PRN of the BeiDou satellite (SVID, see 4.1.9)
1894    pub prn: u8,
1895    /// Vertical delay coefficient 0 (s)
1896    pub alpha_0: f32,
1897    /// Vertical delay coefficient 1 (s/semi-circle)
1898    pub alpha_1: f32,
1899    /// Vertical delay coefficient 2 (s/semi-circle^2)
1900    pub alpha_2: f32,
1901    /// Vertical delay coefficient 3 (s/semi-circle^3)
1902    pub alpha_3: f32,
1903    /// Model period coefficient 0 (s)
1904    pub beta_0: f32,
1905    /// Model period coefficient 1 (s/semi-circle)
1906    pub beta_1: f32,
1907    /// Model period coefficient 2 (s/semi-circle^2)
1908    pub beta_2: f32,
1909    /// Model period coefficient 3 (s/semi-circle^3)
1910    pub beta_3: f32,
1911}
1912
1913impl BdsIonBlock {
1914    pub fn tow_seconds(&self) -> f64 {
1915        self.tow_ms as f64 * 0.001
1916    }
1917    pub fn tow_ms(&self) -> u32 {
1918        self.tow_ms
1919    }
1920    pub fn wnc(&self) -> u16 {
1921        self.wnc
1922    }
1923
1924    pub fn alpha_0(&self) -> Option<f32> {
1925        f32_or_none(self.alpha_0)
1926    }
1927
1928    pub fn beta_0(&self) -> Option<f32> {
1929        f32_or_none(self.beta_0)
1930    }
1931}
1932
1933impl SbfBlockParse for BdsIonBlock {
1934    const BLOCK_ID: u16 = block_ids::BDS_ION;
1935
1936    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1937        // Header (12) + PRN (1) + Reserved (1) + 8 x f32 (32) = 46 bytes minimum
1938        if data.len() < 46 {
1939            return Err(SbfError::ParseError("BDSIon too short".into()));
1940        }
1941
1942        // Offsets:
1943        // 12: PRN (u1)
1944        // 13: Reserved (u1)
1945        // 14-17: alpha_0 (f4)
1946        // 18-21: alpha_1 (f4)
1947        // 22-25: alpha_2 (f4)
1948        // 26-29: alpha_3 (f4)
1949        // 30-33: beta_0 (f4)
1950        // 34-37: beta_1 (f4)
1951        // 38-41: beta_2 (f4)
1952        // 42-45: beta_3 (f4)
1953
1954        let prn = data[12];
1955        // data[13] is reserved
1956        let alpha_0 = f32::from_le_bytes(data[14..18].try_into().unwrap());
1957        let alpha_1 = f32::from_le_bytes(data[18..22].try_into().unwrap());
1958        let alpha_2 = f32::from_le_bytes(data[22..26].try_into().unwrap());
1959        let alpha_3 = f32::from_le_bytes(data[26..30].try_into().unwrap());
1960        let beta_0 = f32::from_le_bytes(data[30..34].try_into().unwrap());
1961        let beta_1 = f32::from_le_bytes(data[34..38].try_into().unwrap());
1962        let beta_2 = f32::from_le_bytes(data[38..42].try_into().unwrap());
1963        let beta_3 = f32::from_le_bytes(data[42..46].try_into().unwrap());
1964
1965        Ok(Self {
1966            tow_ms: header.tow_ms,
1967            wnc: header.wnc,
1968            prn,
1969            alpha_0,
1970            alpha_1,
1971            alpha_2,
1972            alpha_3,
1973            beta_0,
1974            beta_1,
1975            beta_2,
1976            beta_3,
1977        })
1978    }
1979}
1980
1981// ============================================================================
1982// BDSCNav1 Block
1983// ============================================================================
1984
1985/// BDSCNav1 block (Block ID 4251)
1986///
1987/// BeiDou B-CNAV1 navigation data from B1C signal.
1988#[derive(Debug, Clone)]
1989pub struct BdsCNav1Block {
1990    tow_ms: u32,
1991    wnc: u16,
1992    /// PRN index within BeiDou constellation (1 for C01, etc.)
1993    pub prn_idx: u8,
1994    /// Flags: bits 0-1 = satellite type (1: GEO, 2: IGSO, 3: MEO)
1995    pub flags: u8,
1996    /// Ephemeris reference time (seconds)
1997    pub t_oe: u32,
1998    /// Semi-major axis (m)
1999    pub a: f64,
2000    /// Change rate in semi-major axis (m/s)
2001    pub a_dot: f64,
2002    /// Mean motion difference (semi-circles/s)
2003    pub delta_n0: f32,
2004    /// Rate of mean motion difference (semi-circles/s^2)
2005    pub delta_n0_dot: f32,
2006    /// Mean anomaly (semi-circles)
2007    pub m_0: f64,
2008    /// Eccentricity
2009    pub e: f64,
2010    /// Argument of perigee (semi-circles)
2011    pub omega: f64,
2012    /// Longitude of ascending node (semi-circles)
2013    pub omega_0: f64,
2014    /// Rate of right ascension (semi-circles/s)
2015    pub omega_dot: f32,
2016    /// Inclination angle (semi-circles)
2017    pub i_0: f64,
2018    /// Rate of inclination (semi-circles/s)
2019    pub i_dot: f32,
2020    /// Sine harmonic inclination correction (rad)
2021    pub c_is: f32,
2022    /// Cosine harmonic inclination correction (rad)
2023    pub c_ic: f32,
2024    /// Sine harmonic radius correction (m)
2025    pub c_rs: f32,
2026    /// Cosine harmonic radius correction (m)
2027    pub c_rc: f32,
2028    /// Sine harmonic latitude correction (rad)
2029    pub c_us: f32,
2030    /// Cosine harmonic latitude correction (rad)
2031    pub c_uc: f32,
2032    /// Clock reference time (seconds)
2033    pub t_oc: u32,
2034    /// Clock drift rate (s/s^2)
2035    pub a_2: f32,
2036    /// Clock drift (s/s)
2037    pub a_1: f32,
2038    /// Clock bias (s)
2039    pub a_0: f64,
2040    /// Time of week for data prediction (seconds)
2041    pub t_op: u32,
2042    /// Satellite orbit radius and clock bias accuracy index
2043    pub sisai_ocb: u8,
2044    /// Combined SISAI_oc1 and SISAI_oc2 (bits 0-2: oc2, bits 3-5: oc1)
2045    pub sisai_oc12: u8,
2046    /// Satellite orbit along-track and cross-track accuracy index
2047    pub sisai_oe: u8,
2048    /// Signal in space monitoring accuracy index
2049    pub sismai: u8,
2050    /// Health and integrity flags
2051    pub health_if: u8,
2052    /// Issue of Data Ephemeris
2053    pub iode: u8,
2054    /// Issue of Data Clock
2055    pub iodc: u16,
2056    /// Group delay between B1C data and pilot (s)
2057    pub isc_b1cd: f32,
2058    /// Group delay of B1C pilot (s)
2059    pub t_gd_b1cp: f32,
2060    /// Group delay of B2a pilot (s)
2061    pub t_gd_b2ap: f32,
2062}
2063
2064impl BdsCNav1Block {
2065    pub fn tow_seconds(&self) -> f64 {
2066        self.tow_ms as f64 * 0.001
2067    }
2068    pub fn tow_ms(&self) -> u32 {
2069        self.tow_ms
2070    }
2071    pub fn wnc(&self) -> u16 {
2072        self.wnc
2073    }
2074
2075    /// Get satellite type (1: GEO, 2: IGSO, 3: MEO)
2076    pub fn satellite_type(&self) -> u8 {
2077        self.flags & 0x03
2078    }
2079
2080    /// Check if satellite is GEO
2081    pub fn is_geo(&self) -> bool {
2082        self.satellite_type() == 1
2083    }
2084
2085    /// Check if satellite is IGSO
2086    pub fn is_igso(&self) -> bool {
2087        self.satellite_type() == 2
2088    }
2089
2090    /// Check if satellite is MEO
2091    pub fn is_meo(&self) -> bool {
2092        self.satellite_type() == 3
2093    }
2094
2095    /// Check if satellite is healthy (bits 6-7 of health_if == 0)
2096    pub fn is_healthy(&self) -> bool {
2097        (self.health_if & 0xC0) == 0
2098    }
2099
2100    /// ISC B1Cd (None if DNU)
2101    pub fn isc_b1cd_s(&self) -> Option<f32> {
2102        f32_or_none(self.isc_b1cd)
2103    }
2104
2105    /// T_GD B1Cp (None if DNU)
2106    pub fn t_gd_b1cp_s(&self) -> Option<f32> {
2107        f32_or_none(self.t_gd_b1cp)
2108    }
2109
2110    /// T_GD B2ap (None if DNU)
2111    pub fn t_gd_b2ap_s(&self) -> Option<f32> {
2112        f32_or_none(self.t_gd_b2ap)
2113    }
2114}
2115
2116impl SbfBlockParse for BdsCNav1Block {
2117    const BLOCK_ID: u16 = block_ids::BDS_CNAV1;
2118
2119    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2120        // Calculate minimum size based on field layout
2121        // Header fields (12) + PRNidx(1) + Flags(1) + t_oe(4) + A(8) + A_DOT(8) +
2122        // DELTA_n0(4) + DELTA_n0_DOT(4) + M_0(8) + e(8) + omega(8) + OMEGA_0(8) +
2123        // OMEGADOT(4) + i_0(8) + IDOT(4) + C_is(4) + C_ic(4) + C_rs(4) + C_rc(4) +
2124        // C_us(4) + C_uc(4) + t_oc(4) + a_2(4) + a_1(4) + a_0(8) + t_op(4) +
2125        // SISAI_ocb(1) + SISAI_oc12(1) + SISAI_oe(1) + SISMAI(1) + HealthIF(1) +
2126        // IODE(1) + IODC(2) + ISC_B1Cd(4) + T_GDB1Cp(4) + T_GDB2ap(4)
2127        // = 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
2128        if data.len() < 158 {
2129            return Err(SbfError::ParseError("BDSCNav1 too short".into()));
2130        }
2131
2132        // Offsets:
2133        // 12: PRNidx (u1)
2134        // 13: Flags (u1)
2135        // 14-17: t_oe (u4)
2136        // 18-25: A (f8)
2137        // 26-33: A_DOT (f8)
2138        // 34-37: DELTA_n0 (f4)
2139        // 38-41: DELTA_n0_DOT (f4)
2140        // 42-49: M_0 (f8)
2141        // 50-57: e (f8)
2142        // 58-65: omega (f8)
2143        // 66-73: OMEGA_0 (f8)
2144        // 74-77: OMEGADOT (f4)
2145        // 78-85: i_0 (f8)
2146        // 86-89: IDOT (f4)
2147        // 90-93: C_is (f4)
2148        // 94-97: C_ic (f4)
2149        // 98-101: C_rs (f4)
2150        // 102-105: C_rc (f4)
2151        // 106-109: C_us (f4)
2152        // 110-113: C_uc (f4)
2153        // 114-117: t_oc (u4)
2154        // 118-121: a_2 (f4)
2155        // 122-125: a_1 (f4)
2156        // 126-133: a_0 (f8)
2157        // 134-137: t_op (u4)
2158        // 138: SISAI_ocb (u1)
2159        // 139: SISAI_oc12 (u1)
2160        // 140: SISAI_oe (u1)
2161        // 141: SISMAI (u1)
2162        // 142: HealthIF (u1)
2163        // 143: IODE (u1)
2164        // 144-145: IODC (u2)
2165        // 146-149: ISC_B1Cd (f4)
2166        // 150-153: T_GDB1Cp (f4)
2167        // 154-157: T_GDB2ap (f4)
2168
2169        let prn_idx = data[12];
2170        let flags = data[13];
2171        let t_oe = u32::from_le_bytes(data[14..18].try_into().unwrap());
2172        let a = f64::from_le_bytes(data[18..26].try_into().unwrap());
2173        let a_dot = f64::from_le_bytes(data[26..34].try_into().unwrap());
2174        let delta_n0 = f32::from_le_bytes(data[34..38].try_into().unwrap());
2175        let delta_n0_dot = f32::from_le_bytes(data[38..42].try_into().unwrap());
2176        let m_0 = f64::from_le_bytes(data[42..50].try_into().unwrap());
2177        let e = f64::from_le_bytes(data[50..58].try_into().unwrap());
2178        let omega = f64::from_le_bytes(data[58..66].try_into().unwrap());
2179        let omega_0 = f64::from_le_bytes(data[66..74].try_into().unwrap());
2180        let omega_dot = f32::from_le_bytes(data[74..78].try_into().unwrap());
2181        let i_0 = f64::from_le_bytes(data[78..86].try_into().unwrap());
2182        let i_dot = f32::from_le_bytes(data[86..90].try_into().unwrap());
2183        let c_is = f32::from_le_bytes(data[90..94].try_into().unwrap());
2184        let c_ic = f32::from_le_bytes(data[94..98].try_into().unwrap());
2185        let c_rs = f32::from_le_bytes(data[98..102].try_into().unwrap());
2186        let c_rc = f32::from_le_bytes(data[102..106].try_into().unwrap());
2187        let c_us = f32::from_le_bytes(data[106..110].try_into().unwrap());
2188        let c_uc = f32::from_le_bytes(data[110..114].try_into().unwrap());
2189        let t_oc = u32::from_le_bytes(data[114..118].try_into().unwrap());
2190        let a_2 = f32::from_le_bytes(data[118..122].try_into().unwrap());
2191        let a_1 = f32::from_le_bytes(data[122..126].try_into().unwrap());
2192        let a_0 = f64::from_le_bytes(data[126..134].try_into().unwrap());
2193        let t_op = u32::from_le_bytes(data[134..138].try_into().unwrap());
2194        let sisai_ocb = data[138];
2195        let sisai_oc12 = data[139];
2196        let sisai_oe = data[140];
2197        let sismai = data[141];
2198        let health_if = data[142];
2199        let iode = data[143];
2200        let iodc = u16::from_le_bytes([data[144], data[145]]);
2201        let isc_b1cd = f32::from_le_bytes(data[146..150].try_into().unwrap());
2202        let t_gd_b1cp = f32::from_le_bytes(data[150..154].try_into().unwrap());
2203        let t_gd_b2ap = f32::from_le_bytes(data[154..158].try_into().unwrap());
2204
2205        Ok(Self {
2206            tow_ms: header.tow_ms,
2207            wnc: header.wnc,
2208            prn_idx,
2209            flags,
2210            t_oe,
2211            a,
2212            a_dot,
2213            delta_n0,
2214            delta_n0_dot,
2215            m_0,
2216            e,
2217            omega,
2218            omega_0,
2219            omega_dot,
2220            i_0,
2221            i_dot,
2222            c_is,
2223            c_ic,
2224            c_rs,
2225            c_rc,
2226            c_us,
2227            c_uc,
2228            t_oc,
2229            a_2,
2230            a_1,
2231            a_0,
2232            t_op,
2233            sisai_ocb,
2234            sisai_oc12,
2235            sisai_oe,
2236            sismai,
2237            health_if,
2238            iode,
2239            iodc,
2240            isc_b1cd,
2241            t_gd_b1cp,
2242            t_gd_b2ap,
2243        })
2244    }
2245}
2246
2247// ============================================================================
2248// GPSRawCA Block
2249// ============================================================================
2250
2251/// GPSRawCA block (Block ID 4017)
2252///
2253/// Raw GPS C/A navigation subframe bits (L1).
2254#[derive(Debug, Clone)]
2255pub struct GpsRawCaBlock {
2256    tow_ms: u32,
2257    wnc: u16,
2258    /// Satellite ID (1-32 for GPS)
2259    pub svid: u8,
2260    /// CRC check: 0=failed, 1=passed
2261    pub crc_passed: u8,
2262    /// Viterbi decoder error count
2263    pub viterbi_count: u8,
2264    /// Signal source
2265    pub source: u8,
2266    /// Frequency number
2267    pub freq_nr: u8,
2268    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2269    pub nav_bits: [u8; 40],
2270}
2271
2272impl GpsRawCaBlock {
2273    pub fn tow_seconds(&self) -> f64 {
2274        self.tow_ms as f64 * 0.001
2275    }
2276    pub fn tow_ms(&self) -> u32 {
2277        self.tow_ms
2278    }
2279    pub fn wnc(&self) -> u16 {
2280        self.wnc
2281    }
2282    /// Whether CRC check passed
2283    pub fn crc_ok(&self) -> bool {
2284        self.crc_passed != 0
2285    }
2286    /// Raw navigation bits as slice
2287    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2288        &self.nav_bits
2289    }
2290}
2291
2292impl SbfBlockParse for GpsRawCaBlock {
2293    const BLOCK_ID: u16 = block_ids::GPS_RAW_CA;
2294
2295    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2296        // Header (12) + SVID(1) + CRCPassed(1) + ViterbiCount(1) + Source(1) + FreqNr(1) + NAVBits(40)
2297        const MIN_LEN: usize = 57;
2298        if data.len() < MIN_LEN {
2299            return Err(SbfError::ParseError("GPSRawCA too short".into()));
2300        }
2301
2302        let mut nav_bits = [0u8; 40];
2303        nav_bits.copy_from_slice(&data[17..57]);
2304
2305        Ok(Self {
2306            tow_ms: header.tow_ms,
2307            wnc: header.wnc,
2308            svid: data[12],
2309            crc_passed: data[13],
2310            viterbi_count: data[14],
2311            source: data[15],
2312            freq_nr: data[16],
2313            nav_bits,
2314        })
2315    }
2316}
2317
2318// ============================================================================
2319// GPSRawL2C Block
2320// ============================================================================
2321
2322/// GPSRawL2C block (Block ID 4018)
2323///
2324/// Raw GPS L2C navigation frame bits.
2325#[derive(Debug, Clone)]
2326pub struct GpsRawL2CBlock {
2327    tow_ms: u32,
2328    wnc: u16,
2329    /// Satellite ID (1-32 for GPS)
2330    pub svid: u8,
2331    /// CRC check: 0=failed, 1=passed
2332    pub crc_passed: u8,
2333    /// Viterbi decoder error count
2334    pub viterbi_count: u8,
2335    /// Signal source
2336    pub source: u8,
2337    /// Frequency number
2338    pub freq_nr: u8,
2339    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2340    pub nav_bits: [u8; 40],
2341}
2342
2343impl GpsRawL2CBlock {
2344    pub fn tow_seconds(&self) -> f64 {
2345        self.tow_ms as f64 * 0.001
2346    }
2347    pub fn tow_ms(&self) -> u32 {
2348        self.tow_ms
2349    }
2350    pub fn wnc(&self) -> u16 {
2351        self.wnc
2352    }
2353    pub fn crc_ok(&self) -> bool {
2354        self.crc_passed != 0
2355    }
2356    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2357        &self.nav_bits
2358    }
2359}
2360
2361impl SbfBlockParse for GpsRawL2CBlock {
2362    const BLOCK_ID: u16 = block_ids::GPS_RAW_L2C;
2363
2364    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2365        const MIN_LEN: usize = 57;
2366        if data.len() < MIN_LEN {
2367            return Err(SbfError::ParseError("GPSRawL2C too short".into()));
2368        }
2369
2370        let mut nav_bits = [0u8; 40];
2371        nav_bits.copy_from_slice(&data[17..57]);
2372
2373        Ok(Self {
2374            tow_ms: header.tow_ms,
2375            wnc: header.wnc,
2376            svid: data[12],
2377            crc_passed: data[13],
2378            viterbi_count: data[14],
2379            source: data[15],
2380            freq_nr: data[16],
2381            nav_bits,
2382        })
2383    }
2384}
2385
2386// ============================================================================
2387// GPSRawL5 Block
2388// ============================================================================
2389
2390/// GPSRawL5 block (Block ID 4019)
2391///
2392/// Raw GPS L5 navigation frame bits.
2393#[derive(Debug, Clone)]
2394pub struct GpsRawL5Block {
2395    tow_ms: u32,
2396    wnc: u16,
2397    /// Satellite ID (1-32 for GPS)
2398    pub svid: u8,
2399    /// CRC check: 0=failed, 1=passed
2400    pub crc_passed: u8,
2401    /// Viterbi decoder error count
2402    pub viterbi_count: u8,
2403    /// Signal source
2404    pub source: u8,
2405    /// Frequency number
2406    pub freq_nr: u8,
2407    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2408    pub nav_bits: [u8; 40],
2409}
2410
2411impl GpsRawL5Block {
2412    pub fn tow_seconds(&self) -> f64 {
2413        self.tow_ms as f64 * 0.001
2414    }
2415    pub fn tow_ms(&self) -> u32 {
2416        self.tow_ms
2417    }
2418    pub fn wnc(&self) -> u16 {
2419        self.wnc
2420    }
2421    pub fn crc_ok(&self) -> bool {
2422        self.crc_passed != 0
2423    }
2424    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2425        &self.nav_bits
2426    }
2427}
2428
2429impl SbfBlockParse for GpsRawL5Block {
2430    const BLOCK_ID: u16 = block_ids::GPS_RAW_L5;
2431
2432    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2433        const MIN_LEN: usize = 57;
2434        if data.len() < MIN_LEN {
2435            return Err(SbfError::ParseError("GPSRawL5 too short".into()));
2436        }
2437
2438        let mut nav_bits = [0u8; 40];
2439        nav_bits.copy_from_slice(&data[17..57]);
2440
2441        Ok(Self {
2442            tow_ms: header.tow_ms,
2443            wnc: header.wnc,
2444            svid: data[12],
2445            crc_passed: data[13],
2446            viterbi_count: data[14],
2447            source: data[15],
2448            freq_nr: data[16],
2449            nav_bits,
2450        })
2451    }
2452}
2453
2454// ============================================================================
2455// GLORawCA Block
2456// ============================================================================
2457
2458/// GLORawCA block (Block ID 4026)
2459///
2460/// Raw GLONASS CA navigation string bits.
2461#[derive(Debug, Clone)]
2462pub struct GloRawCaBlock {
2463    tow_ms: u32,
2464    wnc: u16,
2465    /// GLONASS slot (38-61)
2466    pub svid: u8,
2467    /// CRC check: 0=failed, 1=passed
2468    pub crc_passed: u8,
2469    /// Viterbi decoder error count
2470    pub viterbi_count: u8,
2471    /// Signal source
2472    pub source: u8,
2473    /// Frequency number (-7 to +6)
2474    pub freq_nr: u8,
2475    /// Raw navigation bits (3 × u4 = 12 bytes, 96 bits)
2476    pub nav_bits: [u8; 12],
2477}
2478
2479impl GloRawCaBlock {
2480    pub fn tow_seconds(&self) -> f64 {
2481        self.tow_ms as f64 * 0.001
2482    }
2483    pub fn tow_ms(&self) -> u32 {
2484        self.tow_ms
2485    }
2486    pub fn wnc(&self) -> u16 {
2487        self.wnc
2488    }
2489    pub fn crc_ok(&self) -> bool {
2490        self.crc_passed != 0
2491    }
2492    pub fn nav_bits_slice(&self) -> &[u8; 12] {
2493        &self.nav_bits
2494    }
2495}
2496
2497impl SbfBlockParse for GloRawCaBlock {
2498    const BLOCK_ID: u16 = block_ids::GLO_RAW_CA;
2499
2500    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2501        // Header (12) + SVID(1) + CRCPassed(1) + ViterbiCount(1) + Source(1) + FreqNr(1) + NAVBits(12)
2502        const MIN_LEN: usize = 29;
2503        if data.len() < MIN_LEN {
2504            return Err(SbfError::ParseError("GLORawCA too short".into()));
2505        }
2506
2507        let mut nav_bits = [0u8; 12];
2508        nav_bits.copy_from_slice(&data[17..29]);
2509
2510        Ok(Self {
2511            tow_ms: header.tow_ms,
2512            wnc: header.wnc,
2513            svid: data[12],
2514            crc_passed: data[13],
2515            viterbi_count: data[14],
2516            source: data[15],
2517            freq_nr: data[16],
2518            nav_bits,
2519        })
2520    }
2521}
2522
2523// ============================================================================
2524// GALRawFNAV Block
2525// ============================================================================
2526
2527/// GALRawFNAV block (Block ID 4022)
2528///
2529/// Raw Galileo F/NAV navigation bits.
2530#[derive(Debug, Clone)]
2531pub struct GalRawFnavBlock {
2532    tow_ms: u32,
2533    wnc: u16,
2534    /// Galileo SVID (71-102)
2535    pub svid: u8,
2536    /// CRC check: 0=failed, 1=passed
2537    pub crc_passed: u8,
2538    /// Viterbi decoder error count
2539    pub viterbi_count: u8,
2540    /// Signal source
2541    pub source: u8,
2542    /// Frequency number
2543    pub freq_nr: u8,
2544    /// Raw navigation bits (8 × u4 = 32 bytes, 256 bits)
2545    pub nav_bits: [u8; 32],
2546}
2547
2548impl GalRawFnavBlock {
2549    pub fn tow_seconds(&self) -> f64 {
2550        self.tow_ms as f64 * 0.001
2551    }
2552    pub fn tow_ms(&self) -> u32 {
2553        self.tow_ms
2554    }
2555    pub fn wnc(&self) -> u16 {
2556        self.wnc
2557    }
2558    pub fn crc_ok(&self) -> bool {
2559        self.crc_passed != 0
2560    }
2561    pub fn nav_bits_slice(&self) -> &[u8; 32] {
2562        &self.nav_bits
2563    }
2564}
2565
2566impl SbfBlockParse for GalRawFnavBlock {
2567    const BLOCK_ID: u16 = block_ids::GAL_RAW_FNAV;
2568
2569    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2570        const MIN_LEN: usize = 49;
2571        if data.len() < MIN_LEN {
2572            return Err(SbfError::ParseError("GALRawFNAV too short".into()));
2573        }
2574
2575        let mut nav_bits = [0u8; 32];
2576        nav_bits.copy_from_slice(&data[17..49]);
2577
2578        Ok(Self {
2579            tow_ms: header.tow_ms,
2580            wnc: header.wnc,
2581            svid: data[12],
2582            crc_passed: data[13],
2583            viterbi_count: data[14],
2584            source: data[15],
2585            freq_nr: data[16],
2586            nav_bits,
2587        })
2588    }
2589}
2590
2591// ============================================================================
2592// GALRawINAV Block
2593// ============================================================================
2594
2595/// GALRawINAV block (Block ID 4023)
2596///
2597/// Raw Galileo I/NAV navigation bits.
2598#[derive(Debug, Clone)]
2599pub struct GalRawInavBlock {
2600    tow_ms: u32,
2601    wnc: u16,
2602    /// Galileo SVID (71-102)
2603    pub svid: u8,
2604    /// CRC check: 0=failed, 1=passed
2605    pub crc_passed: u8,
2606    /// Viterbi decoder error count
2607    pub viterbi_count: u8,
2608    /// Signal source
2609    pub source: u8,
2610    /// Frequency number
2611    pub freq_nr: u8,
2612    /// Raw navigation bits (8 × u4 = 32 bytes, 256 bits)
2613    pub nav_bits: [u8; 32],
2614}
2615
2616impl GalRawInavBlock {
2617    pub fn tow_seconds(&self) -> f64 {
2618        self.tow_ms as f64 * 0.001
2619    }
2620    pub fn tow_ms(&self) -> u32 {
2621        self.tow_ms
2622    }
2623    pub fn wnc(&self) -> u16 {
2624        self.wnc
2625    }
2626    pub fn crc_ok(&self) -> bool {
2627        self.crc_passed != 0
2628    }
2629    pub fn nav_bits_slice(&self) -> &[u8; 32] {
2630        &self.nav_bits
2631    }
2632}
2633
2634impl SbfBlockParse for GalRawInavBlock {
2635    const BLOCK_ID: u16 = block_ids::GAL_RAW_INAV;
2636
2637    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2638        const MIN_LEN: usize = 49;
2639        if data.len() < MIN_LEN {
2640            return Err(SbfError::ParseError("GALRawINAV too short".into()));
2641        }
2642
2643        let mut nav_bits = [0u8; 32];
2644        nav_bits.copy_from_slice(&data[17..49]);
2645
2646        Ok(Self {
2647            tow_ms: header.tow_ms,
2648            wnc: header.wnc,
2649            svid: data[12],
2650            crc_passed: data[13],
2651            viterbi_count: data[14],
2652            source: data[15],
2653            freq_nr: data[16],
2654            nav_bits,
2655        })
2656    }
2657}
2658
2659// ============================================================================
2660// GALRawCNAV Block
2661// ============================================================================
2662
2663/// GALRawCNAV block (Block ID 4024)
2664///
2665/// Raw Galileo CNAV navigation bits.
2666#[derive(Debug, Clone)]
2667pub struct GalRawCnavBlock {
2668    tow_ms: u32,
2669    wnc: u16,
2670    /// Galileo SVID (71-102)
2671    pub svid: u8,
2672    /// CRC check: 0=failed, 1=passed
2673    pub crc_passed: u8,
2674    /// Viterbi decoder error count
2675    pub viterbi_count: u8,
2676    /// Signal source
2677    pub source: u8,
2678    /// Frequency number
2679    pub freq_nr: u8,
2680    /// Raw navigation bits (16 × u4 = 64 bytes, 512 bits)
2681    pub nav_bits: [u8; 64],
2682}
2683
2684impl GalRawCnavBlock {
2685    pub fn tow_seconds(&self) -> f64 {
2686        self.tow_ms as f64 * 0.001
2687    }
2688    pub fn tow_ms(&self) -> u32 {
2689        self.tow_ms
2690    }
2691    pub fn wnc(&self) -> u16 {
2692        self.wnc
2693    }
2694    pub fn crc_ok(&self) -> bool {
2695        self.crc_passed != 0
2696    }
2697    pub fn nav_bits_slice(&self) -> &[u8; 64] {
2698        &self.nav_bits
2699    }
2700}
2701
2702impl SbfBlockParse for GalRawCnavBlock {
2703    const BLOCK_ID: u16 = block_ids::GAL_RAW_CNAV;
2704
2705    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2706        const MIN_LEN: usize = 81;
2707        if data.len() < MIN_LEN {
2708            return Err(SbfError::ParseError("GALRawCNAV too short".into()));
2709        }
2710
2711        let mut nav_bits = [0u8; 64];
2712        nav_bits.copy_from_slice(&data[17..81]);
2713
2714        Ok(Self {
2715            tow_ms: header.tow_ms,
2716            wnc: header.wnc,
2717            svid: data[12],
2718            crc_passed: data[13],
2719            viterbi_count: data[14],
2720            source: data[15],
2721            freq_nr: data[16],
2722            nav_bits,
2723        })
2724    }
2725}
2726
2727// ============================================================================
2728// GEORawL1 Block
2729// ============================================================================
2730
2731/// GEORawL1 block (Block ID 4020)
2732///
2733/// Raw SBAS L1 navigation bits.
2734#[derive(Debug, Clone)]
2735pub struct GeoRawL1Block {
2736    tow_ms: u32,
2737    wnc: u16,
2738    /// SBAS PRN (120-158)
2739    pub svid: u8,
2740    /// CRC check: 0=failed, 1=passed
2741    pub crc_passed: u8,
2742    /// Viterbi decoder error count
2743    pub viterbi_count: u8,
2744    /// Signal source
2745    pub source: u8,
2746    /// Frequency number
2747    pub freq_nr: u8,
2748    /// Raw navigation bits (8 × u4 = 32 bytes, 256 bits)
2749    pub nav_bits: [u8; 32],
2750}
2751
2752impl GeoRawL1Block {
2753    pub fn tow_seconds(&self) -> f64 {
2754        self.tow_ms as f64 * 0.001
2755    }
2756    pub fn tow_ms(&self) -> u32 {
2757        self.tow_ms
2758    }
2759    pub fn wnc(&self) -> u16 {
2760        self.wnc
2761    }
2762    pub fn crc_ok(&self) -> bool {
2763        self.crc_passed != 0
2764    }
2765    pub fn nav_bits_slice(&self) -> &[u8; 32] {
2766        &self.nav_bits
2767    }
2768}
2769
2770impl SbfBlockParse for GeoRawL1Block {
2771    const BLOCK_ID: u16 = block_ids::GEO_RAW_L1;
2772
2773    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2774        const MIN_LEN: usize = 49;
2775        if data.len() < MIN_LEN {
2776            return Err(SbfError::ParseError("GEORawL1 too short".into()));
2777        }
2778
2779        let mut nav_bits = [0u8; 32];
2780        nav_bits.copy_from_slice(&data[17..49]);
2781
2782        Ok(Self {
2783            tow_ms: header.tow_ms,
2784            wnc: header.wnc,
2785            svid: data[12],
2786            crc_passed: data[13],
2787            viterbi_count: data[14],
2788            source: data[15],
2789            freq_nr: data[16],
2790            nav_bits,
2791        })
2792    }
2793}
2794
2795// ============================================================================
2796// CMPRaw Block
2797// ============================================================================
2798
2799/// CMPRaw block (Block ID 4047)
2800///
2801/// Raw BeiDou navigation bits.
2802#[derive(Debug, Clone)]
2803pub struct CmpRawBlock {
2804    tow_ms: u32,
2805    wnc: u16,
2806    /// BeiDou SVID (141-172)
2807    pub svid: u8,
2808    /// CRC check: 0=failed, 1=passed
2809    pub crc_passed: u8,
2810    /// Viterbi decoder error count
2811    pub viterbi_count: u8,
2812    /// Signal source
2813    pub source: u8,
2814    /// Frequency number
2815    pub freq_nr: u8,
2816    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2817    pub nav_bits: [u8; 40],
2818}
2819
2820impl CmpRawBlock {
2821    pub fn tow_seconds(&self) -> f64 {
2822        self.tow_ms as f64 * 0.001
2823    }
2824    pub fn tow_ms(&self) -> u32 {
2825        self.tow_ms
2826    }
2827    pub fn wnc(&self) -> u16 {
2828        self.wnc
2829    }
2830    pub fn crc_ok(&self) -> bool {
2831        self.crc_passed != 0
2832    }
2833    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2834        &self.nav_bits
2835    }
2836}
2837
2838impl SbfBlockParse for CmpRawBlock {
2839    const BLOCK_ID: u16 = block_ids::CMP_RAW;
2840
2841    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2842        const MIN_LEN: usize = 57;
2843        if data.len() < MIN_LEN {
2844            return Err(SbfError::ParseError("CMPRaw too short".into()));
2845        }
2846
2847        let mut nav_bits = [0u8; 40];
2848        nav_bits.copy_from_slice(&data[17..57]);
2849
2850        Ok(Self {
2851            tow_ms: header.tow_ms,
2852            wnc: header.wnc,
2853            svid: data[12],
2854            crc_passed: data[13],
2855            viterbi_count: data[14],
2856            source: data[15],
2857            freq_nr: data[16],
2858            nav_bits,
2859        })
2860    }
2861}
2862
2863// ============================================================================
2864// QZSRawL1CA Block
2865// ============================================================================
2866
2867/// QZSRawL1CA block (Block ID 4066)
2868///
2869/// Raw QZSS L1 C/A navigation bits.
2870#[derive(Debug, Clone)]
2871pub struct QzsRawL1CaBlock {
2872    tow_ms: u32,
2873    wnc: u16,
2874    /// QZSS SVID (181-187)
2875    pub svid: u8,
2876    /// CRC check: 0=failed, 1=passed
2877    pub crc_passed: u8,
2878    /// Viterbi decoder error count
2879    pub viterbi_count: u8,
2880    /// Signal source
2881    pub source: u8,
2882    /// Frequency number
2883    pub freq_nr: u8,
2884    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2885    pub nav_bits: [u8; 40],
2886}
2887
2888impl QzsRawL1CaBlock {
2889    pub fn tow_seconds(&self) -> f64 {
2890        self.tow_ms as f64 * 0.001
2891    }
2892    pub fn tow_ms(&self) -> u32 {
2893        self.tow_ms
2894    }
2895    pub fn wnc(&self) -> u16 {
2896        self.wnc
2897    }
2898    pub fn crc_ok(&self) -> bool {
2899        self.crc_passed != 0
2900    }
2901    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2902        &self.nav_bits
2903    }
2904}
2905
2906impl SbfBlockParse for QzsRawL1CaBlock {
2907    const BLOCK_ID: u16 = block_ids::QZS_RAW_L1CA;
2908
2909    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2910        const MIN_LEN: usize = 57;
2911        if data.len() < MIN_LEN {
2912            return Err(SbfError::ParseError("QZSRawL1CA too short".into()));
2913        }
2914
2915        let mut nav_bits = [0u8; 40];
2916        nav_bits.copy_from_slice(&data[17..57]);
2917
2918        Ok(Self {
2919            tow_ms: header.tow_ms,
2920            wnc: header.wnc,
2921            svid: data[12],
2922            crc_passed: data[13],
2923            viterbi_count: data[14],
2924            source: data[15],
2925            freq_nr: data[16],
2926            nav_bits,
2927        })
2928    }
2929}
2930
2931// ============================================================================
2932// QZSRawL2C Block
2933// ============================================================================
2934
2935/// QZSRawL2C block (Block ID 4067)
2936///
2937/// Raw QZSS L2C navigation bits.
2938#[derive(Debug, Clone)]
2939pub struct QzsRawL2CBlock {
2940    tow_ms: u32,
2941    wnc: u16,
2942    /// QZSS SVID (181-187)
2943    pub svid: u8,
2944    /// CRC check: 0=failed, 1=passed
2945    pub crc_passed: u8,
2946    /// Viterbi decoder error count
2947    pub viterbi_count: u8,
2948    /// Signal source
2949    pub source: u8,
2950    /// Frequency number
2951    pub freq_nr: u8,
2952    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
2953    pub nav_bits: [u8; 40],
2954}
2955
2956impl QzsRawL2CBlock {
2957    pub fn tow_seconds(&self) -> f64 {
2958        self.tow_ms as f64 * 0.001
2959    }
2960    pub fn tow_ms(&self) -> u32 {
2961        self.tow_ms
2962    }
2963    pub fn wnc(&self) -> u16 {
2964        self.wnc
2965    }
2966    pub fn crc_ok(&self) -> bool {
2967        self.crc_passed != 0
2968    }
2969    pub fn nav_bits_slice(&self) -> &[u8; 40] {
2970        &self.nav_bits
2971    }
2972}
2973
2974impl SbfBlockParse for QzsRawL2CBlock {
2975    const BLOCK_ID: u16 = block_ids::QZS_RAW_L2C;
2976
2977    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
2978        const MIN_LEN: usize = 57;
2979        if data.len() < MIN_LEN {
2980            return Err(SbfError::ParseError("QZSRawL2C too short".into()));
2981        }
2982
2983        let mut nav_bits = [0u8; 40];
2984        nav_bits.copy_from_slice(&data[17..57]);
2985
2986        Ok(Self {
2987            tow_ms: header.tow_ms,
2988            wnc: header.wnc,
2989            svid: data[12],
2990            crc_passed: data[13],
2991            viterbi_count: data[14],
2992            source: data[15],
2993            freq_nr: data[16],
2994            nav_bits,
2995        })
2996    }
2997}
2998
2999// ============================================================================
3000// QZSRawL5 Block
3001// ============================================================================
3002
3003/// QZSRawL5 block (Block ID 4068)
3004///
3005/// Raw QZSS L5 navigation bits.
3006#[derive(Debug, Clone)]
3007pub struct QzsRawL5Block {
3008    tow_ms: u32,
3009    wnc: u16,
3010    /// QZSS SVID (181-187)
3011    pub svid: u8,
3012    /// CRC check: 0=failed, 1=passed
3013    pub crc_passed: u8,
3014    /// Viterbi decoder error count
3015    pub viterbi_count: u8,
3016    /// Signal source
3017    pub source: u8,
3018    /// Frequency number
3019    pub freq_nr: u8,
3020    /// Raw navigation bits (10 × u4 = 40 bytes, 300 bits)
3021    pub nav_bits: [u8; 40],
3022}
3023
3024impl QzsRawL5Block {
3025    pub fn tow_seconds(&self) -> f64 {
3026        self.tow_ms as f64 * 0.001
3027    }
3028    pub fn tow_ms(&self) -> u32 {
3029        self.tow_ms
3030    }
3031    pub fn wnc(&self) -> u16 {
3032        self.wnc
3033    }
3034    pub fn crc_ok(&self) -> bool {
3035        self.crc_passed != 0
3036    }
3037    pub fn nav_bits_slice(&self) -> &[u8; 40] {
3038        &self.nav_bits
3039    }
3040}
3041
3042impl SbfBlockParse for QzsRawL5Block {
3043    const BLOCK_ID: u16 = block_ids::QZS_RAW_L5;
3044
3045    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
3046        const MIN_LEN: usize = 57;
3047        if data.len() < MIN_LEN {
3048            return Err(SbfError::ParseError("QZSRawL5 too short".into()));
3049        }
3050
3051        let mut nav_bits = [0u8; 40];
3052        nav_bits.copy_from_slice(&data[17..57]);
3053
3054        Ok(Self {
3055            tow_ms: header.tow_ms,
3056            wnc: header.wnc,
3057            svid: data[12],
3058            crc_passed: data[13],
3059            viterbi_count: data[14],
3060            source: data[15],
3061            freq_nr: data[16],
3062            nav_bits,
3063        })
3064    }
3065}
3066
3067// ============================================================================
3068// GEOIonoDelay Block
3069// ============================================================================
3070
3071/// One ionospheric delay correction entry in `GEOIonoDelay`.
3072#[derive(Debug, Clone)]
3073pub struct GeoIonoDelayIdc {
3074    /// Sequence number in the IGP mask (1..201)
3075    pub igp_mask_no: u8,
3076    /// Grid Ionospheric Vertical Error Indicator (0..15)
3077    pub givei: u8,
3078    vertical_delay_m_raw: f32,
3079}
3080
3081impl GeoIonoDelayIdc {
3082    /// Vertical delay estimate in meters.
3083    /// Returns `None` when the receiver marks the value as do-not-use.
3084    pub fn vertical_delay_m(&self) -> Option<f32> {
3085        f32_or_none(self.vertical_delay_m_raw)
3086    }
3087
3088    /// Raw vertical delay value from the block payload.
3089    pub fn vertical_delay_m_raw(&self) -> f32 {
3090        self.vertical_delay_m_raw
3091    }
3092}
3093
3094/// GEOIonoDelay block (Block ID 5933)
3095///
3096/// SBAS MT26 ionospheric delay corrections.
3097#[derive(Debug, Clone)]
3098pub struct GeoIonoDelayBlock {
3099    tow_ms: u32,
3100    wnc: u16,
3101    /// ID of the SBAS satellite from which MT26 was received
3102    pub prn: u8,
3103    /// SBAS band number
3104    pub band_nbr: u8,
3105    /// Issue of data ionosphere
3106    pub iodi: u8,
3107    /// Ionospheric delay correction entries
3108    pub idc: Vec<GeoIonoDelayIdc>,
3109}
3110
3111impl GeoIonoDelayBlock {
3112    pub fn tow_seconds(&self) -> f64 {
3113        self.tow_ms as f64 * 0.001
3114    }
3115    pub fn tow_ms(&self) -> u32 {
3116        self.tow_ms
3117    }
3118    pub fn wnc(&self) -> u16 {
3119        self.wnc
3120    }
3121
3122    pub fn num_idc(&self) -> usize {
3123        self.idc.len()
3124    }
3125}
3126
3127impl SbfBlockParse for GeoIonoDelayBlock {
3128    const BLOCK_ID: u16 = block_ids::GEO_IONO_DELAY;
3129
3130    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
3131        // Header (12) + PRN (1) + BandNbr (1) + IODI (1) + N (1) + SBLength (1) + Reserved (1)
3132        if data.len() < 18 {
3133            return Err(SbfError::ParseError("GEOIonoDelay too short".into()));
3134        }
3135
3136        let prn = data[12];
3137        let band_nbr = data[13];
3138        let iodi = data[14];
3139        let n = data[15] as usize;
3140        let sb_length = data[16] as usize;
3141
3142        if sb_length < 8 {
3143            return Err(SbfError::ParseError(
3144                "GEOIonoDelay SBLength too small".into(),
3145            ));
3146        }
3147
3148        let mut idc = Vec::with_capacity(n);
3149        let mut offset = 18;
3150
3151        for _ in 0..n {
3152            if offset + sb_length > data.len() {
3153                break;
3154            }
3155
3156            let igp_mask_no = data[offset];
3157            let givei = data[offset + 1];
3158            let vertical_delay_m_raw =
3159                f32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
3160
3161            idc.push(GeoIonoDelayIdc {
3162                igp_mask_no,
3163                givei,
3164                vertical_delay_m_raw,
3165            });
3166
3167            offset += sb_length;
3168        }
3169
3170        Ok(Self {
3171            tow_ms: header.tow_ms,
3172            wnc: header.wnc,
3173            prn,
3174            band_nbr,
3175            iodi,
3176            idc,
3177        })
3178    }
3179}
3180
3181#[cfg(test)]
3182mod tests {
3183    use super::*;
3184    use crate::blocks::SbfBlock;
3185    use crate::header::{SbfHeader, SBF_SYNC};
3186
3187    fn header_for(block_id: u16, data_len: usize, tow_ms: u32, wnc: u16) -> SbfHeader {
3188        SbfHeader {
3189            crc: 0,
3190            block_id,
3191            block_rev: 0,
3192            length: (data_len + 2) as u16,
3193            tow_ms,
3194            wnc,
3195        }
3196    }
3197
3198    #[test]
3199    fn test_gps_alm_accessors() {
3200        let block = GpsAlmBlock {
3201            tow_ms: 1000,
3202            wnc: 2000,
3203            prn: 5,
3204            e: F32_DNU,
3205            t_oa: 100,
3206            delta_i: 0.1,
3207            omega_dot: 0.2,
3208            sqrt_a: 5153.5,
3209            omega_0: 1.0,
3210            omega: 1.1,
3211            m_0: 0.5,
3212            a_f1: 0.0,
3213            a_f0: 0.0,
3214            wn_a: 10,
3215            as_config: 1,
3216            health8: 0,
3217            health6: 0,
3218        };
3219
3220        assert!((block.tow_seconds() - 1.0).abs() < 1e-6);
3221        assert!(block.eccentricity().is_none());
3222        assert!((block.semi_major_axis_m().unwrap() - 5153.5_f32.powi(2)).abs() < 1e-3);
3223    }
3224
3225    #[test]
3226    fn test_gps_alm_parse() {
3227        let mut data = vec![0u8; 57];
3228        data[12] = 7;
3229        data[13..17].copy_from_slice(&0.02_f32.to_le_bytes());
3230        data[17..21].copy_from_slice(&1234_u32.to_le_bytes());
3231        data[21..25].copy_from_slice(&0.1_f32.to_le_bytes());
3232        data[25..29].copy_from_slice(&0.2_f32.to_le_bytes());
3233        data[29..33].copy_from_slice(&5153.8_f32.to_le_bytes());
3234        data[33..37].copy_from_slice(&1.0_f32.to_le_bytes());
3235        data[37..41].copy_from_slice(&1.1_f32.to_le_bytes());
3236        data[41..45].copy_from_slice(&0.5_f32.to_le_bytes());
3237        data[45..49].copy_from_slice(&0.01_f32.to_le_bytes());
3238        data[49..53].copy_from_slice(&0.02_f32.to_le_bytes());
3239        data[53] = 12;
3240        data[54] = 1;
3241        data[55] = 0;
3242        data[56] = 0;
3243
3244        let header = header_for(block_ids::GPS_ALM, data.len(), 5000, 2001);
3245        let block = GpsAlmBlock::parse(&header, &data).unwrap();
3246
3247        assert_eq!(block.prn, 7);
3248        assert_eq!(block.wnc(), 2001);
3249        assert_eq!(block.t_oa, 1234);
3250        assert!((block.eccentricity().unwrap() - 0.02).abs() < 1e-6);
3251    }
3252
3253    #[test]
3254    fn test_gps_ion_accessors() {
3255        let block = GpsIonBlock {
3256            tow_ms: 2500,
3257            wnc: 2100,
3258            prn: 3,
3259            alpha_0: F32_DNU,
3260            alpha_1: 0.1,
3261            alpha_2: 0.2,
3262            alpha_3: 0.3,
3263            beta_0: 1.0,
3264            beta_1: 2.0,
3265            beta_2: 3.0,
3266            beta_3: 4.0,
3267        };
3268
3269        assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
3270        assert!(block.alpha_0().is_none());
3271        assert!((block.beta_0().unwrap() - 1.0).abs() < 1e-6);
3272    }
3273
3274    #[test]
3275    fn test_gps_ion_parse() {
3276        let mut data = vec![0u8; 45];
3277        data[12] = 8;
3278        data[13..17].copy_from_slice(&0.1_f32.to_le_bytes());
3279        data[17..21].copy_from_slice(&0.2_f32.to_le_bytes());
3280        data[21..25].copy_from_slice(&0.3_f32.to_le_bytes());
3281        data[25..29].copy_from_slice(&0.4_f32.to_le_bytes());
3282        data[29..33].copy_from_slice(&1.1_f32.to_le_bytes());
3283        data[33..37].copy_from_slice(&1.2_f32.to_le_bytes());
3284        data[37..41].copy_from_slice(&1.3_f32.to_le_bytes());
3285        data[41..45].copy_from_slice(&1.4_f32.to_le_bytes());
3286
3287        let header = header_for(block_ids::GPS_ION, data.len(), 8000, 2002);
3288        let block = GpsIonBlock::parse(&header, &data).unwrap();
3289
3290        assert_eq!(block.prn, 8);
3291        assert!((block.alpha_1 - 0.2).abs() < 1e-6);
3292        assert!((block.beta_3 - 1.4).abs() < 1e-6);
3293    }
3294
3295    #[test]
3296    fn test_gps_utc_accessors() {
3297        let block = GpsUtcBlock {
3298            tow_ms: 3000,
3299            wnc: 2200,
3300            prn: 1,
3301            a_1: 0.001,
3302            a_0: F64_DNU,
3303            t_ot: 4000,
3304            wn_t: 12,
3305            delta_t_ls: 18,
3306            wn_lsf: 13,
3307            dn: 2,
3308            delta_t_lsf: 19,
3309        };
3310
3311        assert!((block.tow_seconds() - 3.0).abs() < 1e-6);
3312        assert!(block.utc_bias_s().is_none());
3313        assert!((block.utc_drift_s_per_s().unwrap() - 0.001).abs() < 1e-6);
3314    }
3315
3316    #[test]
3317    fn test_gps_utc_parse() {
3318        let mut data = vec![0u8; 34];
3319        data[12] = 2;
3320        data[13..17].copy_from_slice(&0.001_f32.to_le_bytes());
3321        data[17..25].copy_from_slice(&(-0.5_f64).to_le_bytes());
3322        data[25..29].copy_from_slice(&12345_u32.to_le_bytes());
3323        data[29] = 6;
3324        data[30] = 18u8;
3325        data[31] = 7;
3326        data[32] = 4;
3327        data[33] = 19u8;
3328
3329        let header = header_for(block_ids::GPS_UTC, data.len(), 9000, 2003);
3330        let block = GpsUtcBlock::parse(&header, &data).unwrap();
3331
3332        assert_eq!(block.prn, 2);
3333        assert_eq!(block.wn_t, 6);
3334        assert!((block.utc_bias_s().unwrap() + 0.5).abs() < 1e-9);
3335    }
3336
3337    #[test]
3338    fn test_glo_alm_accessors() {
3339        let block = GloAlmBlock {
3340            tow_ms: 500,
3341            wnc: 2300,
3342            svid: 40,
3343            freq_nr: -3,
3344            epsilon: F32_DNU,
3345            t_oa: 200,
3346            delta_i: 0.1,
3347            lambda: 0.2,
3348            t_ln: 100.0,
3349            omega: 0.3,
3350            delta_t: 0.4,
3351            d_delta_t: 0.5,
3352            tau: 0.0,
3353            wn_a: 5,
3354            c: 0,
3355            n: 10,
3356            m_type: 1,
3357            n_4: 2,
3358        };
3359
3360        assert!((block.tow_seconds() - 0.5).abs() < 1e-6);
3361        assert_eq!(block.slot(), 3);
3362        assert!(block.eccentricity().is_none());
3363        assert!(block.clock_bias_s().is_some());
3364    }
3365
3366    #[test]
3367    fn test_glo_alm_parse() {
3368        let mut data = vec![0u8; 56];
3369        data[12] = 38;
3370        data[13] = 250u8;
3371        data[14..18].copy_from_slice(&0.01_f32.to_le_bytes());
3372        data[18..22].copy_from_slice(&200_u32.to_le_bytes());
3373        data[22..26].copy_from_slice(&0.1_f32.to_le_bytes());
3374        data[26..30].copy_from_slice(&0.2_f32.to_le_bytes());
3375        data[30..34].copy_from_slice(&300.0_f32.to_le_bytes());
3376        data[34..38].copy_from_slice(&0.3_f32.to_le_bytes());
3377        data[38..42].copy_from_slice(&0.4_f32.to_le_bytes());
3378        data[42..46].copy_from_slice(&0.5_f32.to_le_bytes());
3379        data[46..50].copy_from_slice(&0.6_f32.to_le_bytes());
3380        data[50] = 3;
3381        data[51] = 0;
3382        data[52..54].copy_from_slice(&15_u16.to_le_bytes());
3383        data[54] = 1;
3384        data[55] = 2;
3385
3386        let header = header_for(block_ids::GLO_ALM, data.len(), 11000, 2301);
3387        let block = GloAlmBlock::parse(&header, &data).unwrap();
3388
3389        assert_eq!(block.svid, 38);
3390        assert_eq!(block.slot(), 1);
3391        assert_eq!(block.n, 15);
3392        assert!((block.clock_bias_s().unwrap() - 0.6).abs() < 1e-6);
3393    }
3394
3395    #[test]
3396    fn test_glo_time_accessors() {
3397        let block = GloTimeBlock {
3398            tow_ms: 750,
3399            wnc: 2400,
3400            svid: 41,
3401            freq_nr: 1,
3402            n_4: 3,
3403            kp: 2,
3404            n: 12,
3405            tau_gps: F32_DNU,
3406            tau_c: 0.2,
3407            b1: 0.01,
3408            b2: 0.02,
3409        };
3410
3411        assert!((block.tow_seconds() - 0.75).abs() < 1e-6);
3412        assert_eq!(block.slot(), 4);
3413        assert!(block.gps_glonass_offset_s().is_none());
3414        assert!((block.time_scale_correction_s().unwrap() - 0.2).abs() < 1e-9);
3415    }
3416
3417    #[test]
3418    fn test_glo_time_parse() {
3419        let mut data = vec![0u8; 38];
3420        data[12] = 39;
3421        data[13] = 1;
3422        data[14] = 5;
3423        data[15] = 1;
3424        data[16..18].copy_from_slice(&9_u16.to_le_bytes());
3425        data[18..22].copy_from_slice(&0.123_f32.to_le_bytes());
3426        data[22..30].copy_from_slice(&(-0.5_f64).to_le_bytes());
3427        data[30..34].copy_from_slice(&0.01_f32.to_le_bytes());
3428        data[34..38].copy_from_slice(&0.02_f32.to_le_bytes());
3429
3430        let header = header_for(block_ids::GLO_TIME, data.len(), 12000, 2401);
3431        let block = GloTimeBlock::parse(&header, &data).unwrap();
3432
3433        assert_eq!(block.svid, 39);
3434        assert_eq!(block.slot(), 2);
3435        assert_eq!(block.n, 9);
3436        assert!((block.time_scale_correction_s().unwrap() + 0.5).abs() < 1e-9);
3437    }
3438
3439    #[test]
3440    fn test_gal_alm_accessors() {
3441        let block = GalAlmBlock {
3442            tow_ms: 1500,
3443            wnc: 2500,
3444            svid: 71,
3445            source: 1,
3446            e: F32_DNU,
3447            t_oa: 100,
3448            delta_i: 0.1,
3449            omega_dot: 0.2,
3450            delta_sqrt_a: 0.3,
3451            omega_0: 1.0,
3452            omega: 1.1,
3453            m_0: 0.5,
3454            a_f1: 0.01,
3455            a_f0: 0.02,
3456            wn_a: 7,
3457            svid_a: 72,
3458            health: 0,
3459            ioda: 4,
3460        };
3461
3462        assert!((block.tow_seconds() - 1.5).abs() < 1e-6);
3463        assert_eq!(block.prn(), 1);
3464        assert!(block.eccentricity().is_none());
3465        assert!((block.delta_sqrt_a().unwrap() - 0.3).abs() < 1e-6);
3466    }
3467
3468    #[test]
3469    fn test_gal_alm_parse() {
3470        let mut data = vec![0u8; 59];
3471        data[12] = 72;
3472        data[13] = 2;
3473        data[14..18].copy_from_slice(&0.02_f32.to_le_bytes());
3474        data[18..22].copy_from_slice(&500_u32.to_le_bytes());
3475        data[22..26].copy_from_slice(&0.1_f32.to_le_bytes());
3476        data[26..30].copy_from_slice(&0.2_f32.to_le_bytes());
3477        data[30..34].copy_from_slice(&0.3_f32.to_le_bytes());
3478        data[34..38].copy_from_slice(&1.0_f32.to_le_bytes());
3479        data[38..42].copy_from_slice(&1.1_f32.to_le_bytes());
3480        data[42..46].copy_from_slice(&0.5_f32.to_le_bytes());
3481        data[46..50].copy_from_slice(&0.01_f32.to_le_bytes());
3482        data[50..54].copy_from_slice(&0.02_f32.to_le_bytes());
3483        data[54] = 9;
3484        data[55] = 73;
3485        data[56..58].copy_from_slice(&0x1234_u16.to_le_bytes());
3486        data[58] = 6;
3487
3488        let header = header_for(block_ids::GAL_ALM, data.len(), 13000, 2501);
3489        let block = GalAlmBlock::parse(&header, &data).unwrap();
3490
3491        assert_eq!(block.svid, 72);
3492        assert_eq!(block.prn(), 2);
3493        assert_eq!(block.wn_a, 9);
3494        assert_eq!(block.health, 0x1234);
3495    }
3496
3497    #[test]
3498    fn test_gal_ion_accessors() {
3499        let block = GalIonBlock {
3500            tow_ms: 1600,
3501            wnc: 2600,
3502            svid: 75,
3503            source: 16,
3504            a_i0: F32_DNU,
3505            a_i1: 0.1,
3506            a_i2: 0.2,
3507            storm_flags: 1,
3508        };
3509
3510        assert!((block.tow_seconds() - 1.6).abs() < 1e-6);
3511        assert!(block.is_fnav());
3512        assert!(!block.is_inav());
3513        assert!(block.a_i0().is_none());
3514    }
3515
3516    #[test]
3517    fn test_gal_ion_parse() {
3518        let mut data = vec![0u8; 27];
3519        data[12] = 80;
3520        data[13] = 1;
3521        data[14..18].copy_from_slice(&0.1_f32.to_le_bytes());
3522        data[18..22].copy_from_slice(&0.2_f32.to_le_bytes());
3523        data[22..26].copy_from_slice(&0.3_f32.to_le_bytes());
3524        data[26] = 2;
3525
3526        let header = header_for(block_ids::GAL_ION, data.len(), 14000, 2601);
3527        let block = GalIonBlock::parse(&header, &data).unwrap();
3528
3529        assert_eq!(block.svid, 80);
3530        assert!((block.a_i1 - 0.2).abs() < 1e-6);
3531        assert_eq!(block.storm_flags, 2);
3532    }
3533
3534    #[test]
3535    fn test_gal_utc_accessors() {
3536        let block = GalUtcBlock {
3537            tow_ms: 1700,
3538            wnc: 2700,
3539            svid: 76,
3540            source: 1,
3541            a_1: 0.001,
3542            a_0: F64_DNU,
3543            t_ot: 1000,
3544            wn_ot: 5,
3545            delta_t_ls: 18,
3546            wn_lsf: 6,
3547            dn: 3,
3548            delta_t_lsf: 19,
3549        };
3550
3551        assert!((block.tow_seconds() - 1.7).abs() < 1e-6);
3552        assert_eq!(block.prn(), 6);
3553        assert!(block.utc_bias_s().is_none());
3554        assert!((block.utc_drift_s_per_s().unwrap() - 0.001).abs() < 1e-6);
3555    }
3556
3557    #[test]
3558    fn test_gal_utc_parse() {
3559        let mut data = vec![0u8; 35];
3560        data[12] = 74;
3561        data[13] = 2;
3562        data[14..18].copy_from_slice(&0.002_f32.to_le_bytes());
3563        data[18..26].copy_from_slice(&1.25_f64.to_le_bytes());
3564        data[26..30].copy_from_slice(&800_u32.to_le_bytes());
3565        data[30] = 4;
3566        data[31] = 18u8;
3567        data[32] = 5;
3568        data[33] = 2;
3569        data[34] = 19u8;
3570
3571        let header = header_for(block_ids::GAL_UTC, data.len(), 15000, 2701);
3572        let block = GalUtcBlock::parse(&header, &data).unwrap();
3573
3574        assert_eq!(block.svid, 74);
3575        assert_eq!(block.wn_ot, 4);
3576        assert!((block.utc_bias_s().unwrap() - 1.25).abs() < 1e-9);
3577    }
3578
3579    #[test]
3580    fn test_gal_gst_gps_accessors() {
3581        let block = GalGstGpsBlock {
3582            tow_ms: 1800,
3583            wnc: 2800,
3584            svid: 71,
3585            source: 1,
3586            a_1g: F32_DNU,
3587            a_0g: 0.3,
3588            t_og: 7,
3589            wn_og: 8,
3590        };
3591
3592        assert!((block.tow_seconds() - 1.8).abs() < 1e-6);
3593        assert_eq!(block.prn(), 1);
3594        assert!(block.gst_gps_drift_s_per_s().is_none());
3595        assert!((block.gst_gps_offset_s().unwrap() - 0.3).abs() < 1e-6);
3596    }
3597
3598    #[test]
3599    fn test_gal_gst_gps_parse() {
3600        let mut data = vec![0u8; 27];
3601        data[12] = 72;
3602        data[13] = 0;
3603        data[14..18].copy_from_slice(&0.01_f32.to_le_bytes());
3604        data[18..22].copy_from_slice(&0.02_f32.to_le_bytes());
3605        data[22..26].copy_from_slice(&9_u32.to_le_bytes());
3606        data[26] = 10;
3607
3608        let header = header_for(block_ids::GAL_GST_GPS, data.len(), 16000, 2801);
3609        let block = GalGstGpsBlock::parse(&header, &data).unwrap();
3610
3611        assert_eq!(block.svid, 72);
3612        assert_eq!(block.t_og, 9);
3613        assert_eq!(block.wn_og, 10);
3614    }
3615
3616    #[test]
3617    fn test_gps_cnav_parse() {
3618        let mut data = vec![0u8; 170];
3619        data[12] = 12;
3620        data[13] = 0x80;
3621        data[14..16].copy_from_slice(&2045_u16.to_le_bytes());
3622        data[17] = (-2_i8) as u8;
3623        data[18..22].copy_from_slice(&1000_u32.to_le_bytes());
3624        data[22..26].copy_from_slice(&2000_u32.to_le_bytes());
3625        data[50..58].copy_from_slice(&0.5_f64.to_le_bytes());
3626        data[150..154].copy_from_slice(&(-1.25_f32).to_le_bytes());
3627        data[166..170].copy_from_slice(&0.0002_f32.to_le_bytes());
3628
3629        let header = header_for(block_ids::GPS_CNAV, data.len(), 17000, 2900);
3630        let block = GpsCNavBlock::parse(&header, &data).unwrap();
3631
3632        assert_eq!(block.prn, 12);
3633        assert_eq!(block.wn, 2045);
3634        assert_eq!(block.ura_ed, -2);
3635        assert_eq!(block.t_oe, 2000);
3636        assert!((block.m_0 - 0.5).abs() < 1e-12);
3637        assert!((block.t_gd + 1.25).abs() < 1e-6);
3638        assert!((block.isc_l5q5 - 0.0002).abs() < 1e-9);
3639    }
3640
3641    #[test]
3642    fn test_bds_ion_parse() {
3643        let mut data = vec![0u8; 46];
3644        data[12] = 7;
3645        data[14..18].copy_from_slice(&0.1_f32.to_le_bytes());
3646        data[30..34].copy_from_slice(&1.1_f32.to_le_bytes());
3647        data[42..46].copy_from_slice(&4.4_f32.to_le_bytes());
3648
3649        let header = header_for(block_ids::BDS_ION, data.len(), 18000, 2901);
3650        let block = BdsIonBlock::parse(&header, &data).unwrap();
3651
3652        assert_eq!(block.prn, 7);
3653        assert!((block.alpha_0 - 0.1).abs() < 1e-6);
3654        assert!((block.beta_0 - 1.1).abs() < 1e-6);
3655        assert!((block.beta_3 - 4.4).abs() < 1e-6);
3656    }
3657
3658    #[test]
3659    fn test_bds_cnav1_parse() {
3660        let mut data = vec![0u8; 158];
3661        data[12] = 3;
3662        data[13] = 0x02;
3663        data[14..18].copy_from_slice(&345600_u32.to_le_bytes());
3664        data[74..78].copy_from_slice(&1.25_f32.to_le_bytes());
3665        data[143] = 9;
3666        data[144..146].copy_from_slice(&512_u16.to_le_bytes());
3667        data[146..150].copy_from_slice(&0.001_f32.to_le_bytes());
3668        data[154..158].copy_from_slice(&(-0.002_f32).to_le_bytes());
3669
3670        let header = header_for(block_ids::BDS_CNAV1, data.len(), 19000, 2902);
3671        let block = BdsCNav1Block::parse(&header, &data).unwrap();
3672
3673        assert_eq!(block.prn_idx, 3);
3674        assert_eq!(block.flags & 0x03, 0x02);
3675        assert_eq!(block.t_oe, 345600);
3676        assert!((block.omega_dot - 1.25).abs() < 1e-6);
3677        assert_eq!(block.iode, 9);
3678        assert_eq!(block.iodc, 512);
3679        assert!((block.isc_b1cd - 0.001).abs() < 1e-6);
3680        assert!((block.t_gd_b2ap + 0.002).abs() < 1e-6);
3681    }
3682
3683    #[test]
3684    fn test_geo_iono_delay_accessors() {
3685        let block = GeoIonoDelayBlock {
3686            tow_ms: 2500,
3687            wnc: 2100,
3688            prn: 120,
3689            band_nbr: 3,
3690            iodi: 5,
3691            idc: vec![GeoIonoDelayIdc {
3692                igp_mask_no: 10,
3693                givei: 4,
3694                vertical_delay_m_raw: F32_DNU,
3695            }],
3696        };
3697
3698        assert!((block.tow_seconds() - 2.5).abs() < 1e-6);
3699        assert_eq!(block.num_idc(), 1);
3700        assert!(block.idc[0].vertical_delay_m().is_none());
3701    }
3702
3703    #[test]
3704    fn test_geo_iono_delay_parse() {
3705        let mut data = vec![0u8; 18 + (2 * 8)];
3706        data[12] = 120; // PRN
3707        data[13] = 3; // BandNbr
3708        data[14] = 5; // IODI
3709        data[15] = 2; // N
3710        data[16] = 8; // SBLength
3711        data[17] = 0; // Reserved
3712
3713        // IDC #1
3714        data[18] = 10; // IGPMaskNo
3715        data[19] = 4; // GIVEI
3716        data[22..26].copy_from_slice(&12.5_f32.to_le_bytes()); // VerticalDelay
3717
3718        // IDC #2
3719        data[26] = 11; // IGPMaskNo
3720        data[27] = 5; // GIVEI
3721        data[30..34].copy_from_slice(&F32_DNU.to_le_bytes()); // VerticalDelay DNU
3722
3723        let header = header_for(block_ids::GEO_IONO_DELAY, data.len(), 4321, 2024);
3724        let block = GeoIonoDelayBlock::parse(&header, &data).unwrap();
3725
3726        assert_eq!(block.prn, 120);
3727        assert_eq!(block.band_nbr, 3);
3728        assert_eq!(block.iodi, 5);
3729        assert_eq!(block.num_idc(), 2);
3730        assert_eq!(block.idc[0].igp_mask_no, 10);
3731        assert_eq!(block.idc[1].givei, 5);
3732        assert!((block.idc[0].vertical_delay_m().unwrap() - 12.5).abs() < 1e-6);
3733        assert!(block.idc[1].vertical_delay_m().is_none());
3734    }
3735
3736    #[test]
3737    fn test_geo_iono_delay_sbf_block_parse() {
3738        let total_len = 36usize; // sync + header + payload (+padding)
3739        let mut data = vec![0u8; total_len];
3740        data[0..2].copy_from_slice(&SBF_SYNC);
3741        data[2..4].copy_from_slice(&0_u16.to_le_bytes()); // CRC
3742        data[4..6].copy_from_slice(&block_ids::GEO_IONO_DELAY.to_le_bytes()); // ID/Rev
3743        data[6..8].copy_from_slice(&(total_len as u16).to_le_bytes()); // Length
3744        data[8..12].copy_from_slice(&9876_u32.to_le_bytes()); // TOW
3745        data[12..14].copy_from_slice(&2025_u16.to_le_bytes()); // WNc
3746
3747        // Payload starts at absolute offset 14 (block_data offset 12)
3748        data[14] = 120; // PRN
3749        data[15] = 2; // BandNbr
3750        data[16] = 7; // IODI
3751        data[17] = 2; // N
3752        data[18] = 8; // SBLength
3753        data[19] = 0; // Reserved
3754
3755        // IDC #1
3756        data[20] = 10; // IGPMaskNo
3757        data[21] = 3; // GIVEI
3758        data[24..28].copy_from_slice(&8.25_f32.to_le_bytes());
3759
3760        // IDC #2
3761        data[28] = 11; // IGPMaskNo
3762        data[29] = 6; // GIVEI
3763        data[32..36].copy_from_slice(&9.75_f32.to_le_bytes());
3764
3765        let (block, used) = SbfBlock::parse(&data).unwrap();
3766        assert_eq!(used, total_len);
3767        assert_eq!(block.block_id(), block_ids::GEO_IONO_DELAY);
3768        match block {
3769            SbfBlock::GeoIonoDelay(geo) => {
3770                assert_eq!(geo.tow_ms(), 9876);
3771                assert_eq!(geo.wnc(), 2025);
3772                assert_eq!(geo.num_idc(), 2);
3773                assert!((geo.idc[0].vertical_delay_m().unwrap() - 8.25).abs() < 1e-6);
3774                assert!((geo.idc[1].vertical_delay_m().unwrap() - 9.75).abs() < 1e-6);
3775            }
3776            _ => panic!("Expected GeoIonoDelay block"),
3777        }
3778    }
3779
3780    #[test]
3781    fn test_gps_raw_ca_parse() {
3782        let header = header_for(4017, 57, 5000, 2150);
3783        let mut data = vec![0u8; 57];
3784        data[12] = 12; // SVID
3785        data[13] = 1; // CRCPassed
3786        data[14] = 0; // ViterbiCount
3787        data[15] = 2; // Source
3788        data[16] = 0; // FreqNr
3789        data[17..57].copy_from_slice(&[0xABu8; 40]); // NAVBits
3790
3791        let block = GpsRawCaBlock::parse(&header, &data).unwrap();
3792        assert_eq!(block.tow_seconds(), 5.0);
3793        assert_eq!(block.wnc(), 2150);
3794        assert_eq!(block.svid, 12);
3795        assert!(block.crc_ok());
3796        assert_eq!(block.viterbi_count, 0);
3797        assert_eq!(block.nav_bits_slice()[0], 0xAB);
3798    }
3799
3800    #[test]
3801    fn test_gps_raw_ca_too_short() {
3802        let header = header_for(4017, 57, 0, 0);
3803        let data = [0u8; 50];
3804        assert!(GpsRawCaBlock::parse(&header, &data).is_err());
3805    }
3806
3807    #[test]
3808    fn test_gps_raw_l2c_parse() {
3809        let header = header_for(4018, 57, 6000, 2200);
3810        let mut data = vec![0u8; 57];
3811        data[12] = 8;
3812        data[13] = 1;
3813        data[17..57].copy_from_slice(&[0xCDu8; 40]);
3814
3815        let block = GpsRawL2CBlock::parse(&header, &data).unwrap();
3816        assert_eq!(block.tow_seconds(), 6.0);
3817        assert_eq!(block.svid, 8);
3818        assert!(block.crc_ok());
3819        assert_eq!(block.nav_bits_slice()[0], 0xCD);
3820    }
3821
3822    #[test]
3823    fn test_gps_raw_l5_parse() {
3824        let header = header_for(4019, 57, 7000, 2250);
3825        let mut data = vec![0u8; 57];
3826        data[12] = 15;
3827        data[13] = 0;
3828
3829        let block = GpsRawL5Block::parse(&header, &data).unwrap();
3830        assert_eq!(block.tow_seconds(), 7.0);
3831        assert_eq!(block.svid, 15);
3832        assert!(!block.crc_ok());
3833    }
3834
3835    #[test]
3836    fn test_glo_raw_ca_parse() {
3837        let header = header_for(4026, 29, 8000, 2300);
3838        let mut data = vec![0u8; 29];
3839        data[12] = 45;
3840        data[13] = 1;
3841        data[16] = 3;
3842        data[17..29].copy_from_slice(&[0x12u8; 12]);
3843
3844        let block = GloRawCaBlock::parse(&header, &data).unwrap();
3845        assert_eq!(block.tow_seconds(), 8.0);
3846        assert_eq!(block.svid, 45);
3847        assert_eq!(block.freq_nr, 3);
3848        assert_eq!(block.nav_bits_slice()[0], 0x12);
3849    }
3850
3851    #[test]
3852    fn test_glo_raw_ca_too_short() {
3853        let header = header_for(4026, 29, 0, 0);
3854        let data = [0u8; 25];
3855        assert!(GloRawCaBlock::parse(&header, &data).is_err());
3856    }
3857
3858    #[test]
3859    fn test_gal_raw_fnav_parse() {
3860        let header = header_for(4022, 49, 1000, 2100);
3861        let mut data = vec![0u8; 49];
3862        data[12] = 85; // Galileo SVID
3863        data[13] = 1;
3864        data[17..49].copy_from_slice(&[0x11u8; 32]);
3865        let block = GalRawFnavBlock::parse(&header, &data).unwrap();
3866        assert_eq!(block.tow_seconds(), 1.0);
3867        assert_eq!(block.svid, 85);
3868        assert!(block.crc_ok());
3869        assert_eq!(block.nav_bits_slice()[0], 0x11);
3870    }
3871
3872    #[test]
3873    fn test_gal_raw_inav_parse() {
3874        let header = header_for(4023, 49, 2000, 2101);
3875        let mut data = vec![0u8; 49];
3876        data[12] = 72;
3877        data[13] = 0;
3878        let block = GalRawInavBlock::parse(&header, &data).unwrap();
3879        assert_eq!(block.tow_seconds(), 2.0);
3880        assert!(!block.crc_ok());
3881    }
3882
3883    #[test]
3884    fn test_gal_raw_cnav_parse() {
3885        let header = header_for(4024, 81, 3000, 2102);
3886        let mut data = vec![0u8; 81];
3887        data[12] = 90;
3888        data[17..81].copy_from_slice(&[0x22u8; 64]);
3889        let block = GalRawCnavBlock::parse(&header, &data).unwrap();
3890        assert_eq!(block.tow_seconds(), 3.0);
3891        assert_eq!(block.nav_bits_slice()[63], 0x22);
3892    }
3893
3894    #[test]
3895    fn test_geo_raw_l1_parse() {
3896        let header = header_for(4020, 49, 4000, 2103);
3897        let mut data = vec![0u8; 49];
3898        data[12] = 135; // SBAS PRN
3899        data[13] = 1;
3900        data[17..49].copy_from_slice(&[0x33u8; 32]);
3901        let block = GeoRawL1Block::parse(&header, &data).unwrap();
3902        assert_eq!(block.tow_seconds(), 4.0);
3903        assert_eq!(block.svid, 135);
3904    }
3905
3906    #[test]
3907    fn test_cmp_raw_parse() {
3908        let header = header_for(4047, 57, 5000, 2104);
3909        let mut data = vec![0u8; 57];
3910        data[12] = 155; // BeiDou SVID
3911        data[17..57].copy_from_slice(&[0x44u8; 40]);
3912        let block = CmpRawBlock::parse(&header, &data).unwrap();
3913        assert_eq!(block.tow_seconds(), 5.0);
3914        assert_eq!(block.nav_bits_slice()[39], 0x44);
3915    }
3916
3917    #[test]
3918    fn test_qzs_raw_l1ca_parse() {
3919        let header = header_for(4066, 57, 6000, 2105);
3920        let mut data = vec![0u8; 57];
3921        data[12] = 183; // QZSS SVID
3922        data[13] = 1;
3923        let block = QzsRawL1CaBlock::parse(&header, &data).unwrap();
3924        assert_eq!(block.tow_seconds(), 6.0);
3925        assert_eq!(block.svid, 183);
3926    }
3927
3928    #[test]
3929    fn test_qzs_raw_l2c_parse() {
3930        let header = header_for(4067, 57, 7000, 2106);
3931        let mut data = vec![0u8; 57];
3932        data[12] = 186;
3933        let block = QzsRawL2CBlock::parse(&header, &data).unwrap();
3934        assert_eq!(block.tow_seconds(), 7.0);
3935    }
3936
3937    #[test]
3938    fn test_qzs_raw_l5_parse() {
3939        let header = header_for(4068, 57, 8000, 2107);
3940        let mut data = vec![0u8; 57];
3941        data[12] = 181;
3942        let block = QzsRawL5Block::parse(&header, &data).unwrap();
3943        assert_eq!(block.tow_seconds(), 8.0);
3944    }
3945
3946    #[test]
3947    fn test_gal_raw_fnav_too_short() {
3948        let header = header_for(4022, 49, 0, 0);
3949        assert!(GalRawFnavBlock::parse(&header, &[0u8; 40]).is_err());
3950    }
3951
3952    #[test]
3953    fn test_gal_raw_cnav_too_short() {
3954        let header = header_for(4024, 81, 0, 0);
3955        assert!(GalRawCnavBlock::parse(&header, &[0u8; 70]).is_err());
3956    }
3957
3958    #[test]
3959    fn test_gal_sar_rlm_accessors() {
3960        let block = GalSarRlmBlock {
3961            tow_ms: 4500,
3962            wnc: 2200,
3963            svid: 74,
3964            source: 2,
3965            rlm_length_bits: 80,
3966            rlm_bits_words: vec![0x8000_0000, 0, 0x0001_0000],
3967        };
3968
3969        assert!((block.tow_seconds() - 4.5).abs() < 1e-6);
3970        assert_eq!(block.prn(), 4);
3971        assert_eq!(block.rlm_length_bits(), 80);
3972        assert_eq!(block.rlm_bits_words().len(), 3);
3973        assert_eq!(block.bit(0), Some(true));
3974        assert_eq!(block.bit(1), Some(false));
3975        assert_eq!(block.bit(79), Some(true));
3976        assert_eq!(block.bit(80), None);
3977    }
3978
3979    #[test]
3980    fn test_gal_sar_rlm_parse() {
3981        let mut data = vec![0u8; 30];
3982        data[12] = 72; // SVID
3983        data[13] = 16; // Source (F/NAV)
3984        data[14] = 80; // RLMLength in bits
3985        data[18..22].copy_from_slice(&0x1234_5678_u32.to_le_bytes());
3986        data[22..26].copy_from_slice(&0x9abc_def0_u32.to_le_bytes());
3987        data[26..30].copy_from_slice(&0x0001_0000_u32.to_le_bytes());
3988
3989        let header = header_for(block_ids::GAL_SAR_RLM, data.len(), 3210, 2048);
3990        let block = GalSarRlmBlock::parse(&header, &data).unwrap();
3991
3992        assert_eq!(block.svid, 72);
3993        assert_eq!(block.source, 16);
3994        assert_eq!(block.rlm_length_bits(), 80);
3995        assert_eq!(
3996            block.rlm_bits_words(),
3997            &[0x1234_5678, 0x9abc_def0, 0x0001_0000]
3998        );
3999        assert_eq!(block.bit(79), Some(true));
4000    }
4001
4002    #[test]
4003    fn test_gal_sar_rlm_too_short() {
4004        let header = header_for(block_ids::GAL_SAR_RLM, 30, 0, 0);
4005        assert!(GalSarRlmBlock::parse(&header, &[0u8; 20]).is_err());
4006    }
4007}