Skip to main content

sbf_tools/blocks/
sbas.rs

1//! SBAS (Space-Based Augmentation System) blocks
2//!
3//! GEOMT00, GEOPRNMask, GEOFastCorr and related SBAS message blocks.
4
5use crate::error::{SbfError, SbfResult};
6use crate::header::SbfHeader;
7
8use super::block_ids;
9use super::dnu::{F32_DNU, F64_DNU};
10use super::SbfBlockParse;
11
12// ============================================================================
13// GEOMT00 Block
14// ============================================================================
15
16/// GEOMT00 block (Block ID 5925)
17///
18/// SBAS MT00 message (null message / test).
19#[derive(Debug, Clone)]
20pub struct GeoMt00Block {
21    tow_ms: u32,
22    wnc: u16,
23    /// SBAS PRN (120-158)
24    pub prn: u8,
25}
26
27impl GeoMt00Block {
28    pub fn tow_seconds(&self) -> f64 {
29        self.tow_ms as f64 * 0.001
30    }
31    pub fn tow_ms(&self) -> u32 {
32        self.tow_ms
33    }
34    pub fn wnc(&self) -> u16 {
35        self.wnc
36    }
37}
38
39impl SbfBlockParse for GeoMt00Block {
40    const BLOCK_ID: u16 = block_ids::GEO_MT00;
41
42    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
43        if data.len() < 13 {
44            return Err(SbfError::ParseError("GEOMT00 too short".into()));
45        }
46
47        Ok(Self {
48            tow_ms: header.tow_ms,
49            wnc: header.wnc,
50            prn: data[12],
51        })
52    }
53}
54
55// ============================================================================
56// GEOPRNMask Block
57// ============================================================================
58
59/// GEOPRNMask block (Block ID 5926)
60///
61/// SBAS MT01 PRN mask assignments.
62#[derive(Debug, Clone)]
63pub struct GeoPrnMaskBlock {
64    tow_ms: u32,
65    wnc: u16,
66    /// SBAS PRN
67    pub prn: u8,
68    /// Issue of Data PRN mask
69    pub iodp: u8,
70    /// Number of PRNs in mask
71    pub nbr_prns: u8,
72    /// PRN mask array (up to 51 entries)
73    pub prn_mask: Vec<u8>,
74}
75
76impl GeoPrnMaskBlock {
77    pub fn tow_seconds(&self) -> f64 {
78        self.tow_ms as f64 * 0.001
79    }
80    pub fn tow_ms(&self) -> u32 {
81        self.tow_ms
82    }
83    pub fn wnc(&self) -> u16 {
84        self.wnc
85    }
86}
87
88impl SbfBlockParse for GeoPrnMaskBlock {
89    const BLOCK_ID: u16 = block_ids::GEO_PRN_MASK;
90
91    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
92        if data.len() < 16 {
93            return Err(SbfError::ParseError("GEOPRNMask too short".into()));
94        }
95
96        let prn = data[12];
97        let iodp = data[13];
98        let nbr_prns = data[14] as usize;
99
100        let prn_mask_len = nbr_prns.min(data.len().saturating_sub(15));
101        let prn_mask: Vec<u8> = data[15..15 + prn_mask_len].to_vec();
102
103        Ok(Self {
104            tow_ms: header.tow_ms,
105            wnc: header.wnc,
106            prn,
107            iodp,
108            nbr_prns: nbr_prns as u8,
109            prn_mask,
110        })
111    }
112}
113
114// ============================================================================
115// GEOFastCorr Block
116// ============================================================================
117
118/// One fast correction entry in `GEOFastCorr`.
119#[derive(Debug, Clone)]
120pub struct GeoFastCorrEntry {
121    /// PRN mask number
122    pub prn_mask_no: u8,
123    /// User Differential Range Error index
124    pub udrei: u8,
125    prc_m: f32,
126}
127
128impl GeoFastCorrEntry {
129    /// Pseudo-Range Correction in meters.
130    /// Returns `None` when do-not-use.
131    pub fn prc_m(&self) -> Option<f32> {
132        if self.prc_m == F32_DNU {
133            None
134        } else {
135            Some(self.prc_m)
136        }
137    }
138    pub fn prc_m_raw(&self) -> f32 {
139        self.prc_m
140    }
141}
142
143/// GEOFastCorr block (Block ID 5927)
144///
145/// SBAS MT02-05/24 fast corrections.
146#[derive(Debug, Clone)]
147pub struct GeoFastCorrBlock {
148    tow_ms: u32,
149    wnc: u16,
150    /// SBAS PRN
151    pub prn: u8,
152    /// Message type
153    pub mt: u8,
154    /// Issue of Data PRN mask
155    pub iodp: u8,
156    /// Issue of Data Fast
157    pub iodf: u8,
158    /// Number of corrections
159    pub n: u8,
160    /// Sub-block length
161    pub sb_length: u8,
162    /// Fast correction entries
163    pub corrections: Vec<GeoFastCorrEntry>,
164}
165
166impl GeoFastCorrBlock {
167    pub fn tow_seconds(&self) -> f64 {
168        self.tow_ms as f64 * 0.001
169    }
170    pub fn tow_ms(&self) -> u32 {
171        self.tow_ms
172    }
173    pub fn wnc(&self) -> u16 {
174        self.wnc
175    }
176    pub fn num_corrections(&self) -> usize {
177        self.corrections.len()
178    }
179}
180
181impl SbfBlockParse for GeoFastCorrBlock {
182    const BLOCK_ID: u16 = block_ids::GEO_FAST_CORR;
183
184    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
185        if data.len() < 20 {
186            return Err(SbfError::ParseError("GEOFastCorr too short".into()));
187        }
188
189        let prn = data[12];
190        let mt = data[13];
191        let iodp = data[14];
192        let iodf = data[15];
193        let n = data[16] as usize;
194        let sb_length = data[17] as usize;
195
196        if sb_length < 6 {
197            return Err(SbfError::ParseError(
198                "GEOFastCorr SBLength too small".into(),
199            ));
200        }
201
202        let mut corrections = Vec::with_capacity(n);
203        let mut offset = 18;
204
205        for _ in 0..n {
206            if offset + sb_length > data.len() {
207                break;
208            }
209
210            let prn_mask_no = data[offset];
211            let udrei = data[offset + 1];
212            let prc_m = f32::from_le_bytes(data[offset + 2..offset + 6].try_into().unwrap());
213
214            corrections.push(GeoFastCorrEntry {
215                prn_mask_no,
216                udrei,
217                prc_m,
218            });
219
220            offset += sb_length;
221        }
222
223        Ok(Self {
224            tow_ms: header.tow_ms,
225            wnc: header.wnc,
226            prn,
227            mt,
228            iodp,
229            iodf,
230            n: n as u8,
231            sb_length: sb_length as u8,
232            corrections,
233        })
234    }
235}
236
237// ============================================================================
238// GEONav Block
239// ============================================================================
240
241/// GEONav block (Block ID 5896)
242///
243/// SBAS MT09 navigation message with GEO satellite ephemeris.
244#[derive(Debug, Clone)]
245pub struct GeoNavBlock {
246    tow_ms: u32,
247    wnc: u16,
248    /// SBAS PRN (120-158)
249    pub prn: u8,
250    /// Issue of Data Navigation (spare bits)
251    pub iodn_spare: u16,
252    /// User Range Accuracy
253    pub ura: u16,
254    /// Reference time (seconds)
255    pub t0: u32,
256    xg_m: f64,
257    yg_m: f64,
258    zg_m: f64,
259    xgd_mps: f64,
260    ygd_mps: f64,
261    zgd_mps: f64,
262    xgdd_mps2: f64,
263    ygdd_mps2: f64,
264    zgdd_mps2: f64,
265    ag_f0_s: f32,
266    ag_f1_sps: f32,
267}
268
269impl GeoNavBlock {
270    pub fn tow_seconds(&self) -> f64 {
271        self.tow_ms as f64 * 0.001
272    }
273    pub fn tow_ms(&self) -> u32 {
274        self.tow_ms
275    }
276    pub fn wnc(&self) -> u16 {
277        self.wnc
278    }
279    pub fn position_x_m(&self) -> Option<f64> {
280        if self.xg_m == F64_DNU {
281            None
282        } else {
283            Some(self.xg_m)
284        }
285    }
286    pub fn position_y_m(&self) -> Option<f64> {
287        if self.yg_m == F64_DNU {
288            None
289        } else {
290            Some(self.yg_m)
291        }
292    }
293    pub fn position_z_m(&self) -> Option<f64> {
294        if self.zg_m == F64_DNU {
295            None
296        } else {
297            Some(self.zg_m)
298        }
299    }
300    pub fn velocity_x_mps(&self) -> Option<f64> {
301        if self.xgd_mps == F64_DNU {
302            None
303        } else {
304            Some(self.xgd_mps)
305        }
306    }
307    pub fn velocity_y_mps(&self) -> Option<f64> {
308        if self.ygd_mps == F64_DNU {
309            None
310        } else {
311            Some(self.ygd_mps)
312        }
313    }
314    pub fn velocity_z_mps(&self) -> Option<f64> {
315        if self.zgd_mps == F64_DNU {
316            None
317        } else {
318            Some(self.zgd_mps)
319        }
320    }
321    pub fn acceleration_x_mps2(&self) -> Option<f64> {
322        if self.xgdd_mps2 == F64_DNU {
323            None
324        } else {
325            Some(self.xgdd_mps2)
326        }
327    }
328    pub fn acceleration_y_mps2(&self) -> Option<f64> {
329        if self.ygdd_mps2 == F64_DNU {
330            None
331        } else {
332            Some(self.ygdd_mps2)
333        }
334    }
335    pub fn acceleration_z_mps2(&self) -> Option<f64> {
336        if self.zgdd_mps2 == F64_DNU {
337            None
338        } else {
339            Some(self.zgdd_mps2)
340        }
341    }
342    pub fn clock_bias_s(&self) -> Option<f32> {
343        if self.ag_f0_s == F32_DNU {
344            None
345        } else {
346            Some(self.ag_f0_s)
347        }
348    }
349    pub fn clock_drift_sps(&self) -> Option<f32> {
350        if self.ag_f1_sps == F32_DNU {
351            None
352        } else {
353            Some(self.ag_f1_sps)
354        }
355    }
356}
357
358impl SbfBlockParse for GeoNavBlock {
359    const BLOCK_ID: u16 = block_ids::GEO_NAV;
360
361    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
362        const MIN_LEN: usize = 101;
363        if data.len() < MIN_LEN {
364            return Err(SbfError::ParseError("GEONav too short".into()));
365        }
366
367        let prn = data[12];
368        let iodn_spare = u16::from_le_bytes([data[13], data[14]]);
369        let ura = u16::from_le_bytes([data[15], data[16]]);
370        let t0 = u32::from_le_bytes([data[17], data[18], data[19], data[20]]);
371        let xg_m = f64::from_le_bytes(data[21..29].try_into().unwrap());
372        let yg_m = f64::from_le_bytes(data[29..37].try_into().unwrap());
373        let zg_m = f64::from_le_bytes(data[37..45].try_into().unwrap());
374        let xgd_mps = f64::from_le_bytes(data[45..53].try_into().unwrap());
375        let ygd_mps = f64::from_le_bytes(data[53..61].try_into().unwrap());
376        let zgd_mps = f64::from_le_bytes(data[61..69].try_into().unwrap());
377        let xgdd_mps2 = f64::from_le_bytes(data[69..77].try_into().unwrap());
378        let ygdd_mps2 = f64::from_le_bytes(data[77..85].try_into().unwrap());
379        let zgdd_mps2 = f64::from_le_bytes(data[85..93].try_into().unwrap());
380        let ag_f0_s = f32::from_le_bytes(data[93..97].try_into().unwrap());
381        let ag_f1_sps = f32::from_le_bytes(data[97..101].try_into().unwrap());
382
383        Ok(Self {
384            tow_ms: header.tow_ms,
385            wnc: header.wnc,
386            prn,
387            iodn_spare,
388            ura,
389            t0,
390            xg_m,
391            yg_m,
392            zg_m,
393            xgd_mps,
394            ygd_mps,
395            zgd_mps,
396            xgdd_mps2,
397            ygdd_mps2,
398            zgdd_mps2,
399            ag_f0_s,
400            ag_f1_sps,
401        })
402    }
403}
404
405// ============================================================================
406// GEOIntegrity Block
407// ============================================================================
408
409/// GEOIntegrity block (Block ID 5928)
410///
411/// SBAS MT06 integrity information (UDRE indices).
412#[derive(Debug, Clone)]
413pub struct GeoIntegrityBlock {
414    tow_ms: u32,
415    wnc: u16,
416    /// SBAS PRN
417    pub prn: u8,
418    /// Issue of Data Fast (4 values)
419    pub iodf: [u8; 4],
420    /// UDRE indices (51 values)
421    pub udrei: [u8; 51],
422}
423
424impl GeoIntegrityBlock {
425    pub fn tow_seconds(&self) -> f64 {
426        self.tow_ms as f64 * 0.001
427    }
428    pub fn tow_ms(&self) -> u32 {
429        self.tow_ms
430    }
431    pub fn wnc(&self) -> u16 {
432        self.wnc
433    }
434}
435
436impl SbfBlockParse for GeoIntegrityBlock {
437    const BLOCK_ID: u16 = block_ids::GEO_INTEGRITY;
438
439    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
440        const MIN_LEN: usize = 68;
441        if data.len() < MIN_LEN {
442            return Err(SbfError::ParseError("GEOIntegrity too short".into()));
443        }
444
445        let prn = data[12];
446        let mut iodf = [0u8; 4];
447        iodf.copy_from_slice(&data[13..17]);
448        let mut udrei = [0u8; 51];
449        udrei.copy_from_slice(&data[17..68]);
450
451        Ok(Self {
452            tow_ms: header.tow_ms,
453            wnc: header.wnc,
454            prn,
455            iodf,
456            udrei,
457        })
458    }
459}
460
461// ============================================================================
462// GEOAlm Block
463// ============================================================================
464
465/// GEOAlm block (Block ID 5897)
466///
467/// SBAS MT17 satellite almanac.
468#[derive(Debug, Clone)]
469pub struct GeoAlmBlock {
470    tow_ms: u32,
471    wnc: u16,
472    /// SBAS PRN
473    pub prn: u8,
474    /// Data ID
475    pub data_id: u8,
476    /// Health flags
477    pub health: u16,
478    /// Reference time (seconds)
479    pub t0: u32,
480    xg_m: f64,
481    yg_m: f64,
482    zg_m: f64,
483    xgd_mps: f64,
484    ygd_mps: f64,
485    zgd_mps: f64,
486}
487
488impl GeoAlmBlock {
489    pub fn tow_seconds(&self) -> f64 {
490        self.tow_ms as f64 * 0.001
491    }
492    pub fn tow_ms(&self) -> u32 {
493        self.tow_ms
494    }
495    pub fn wnc(&self) -> u16 {
496        self.wnc
497    }
498    pub fn position_x_m(&self) -> Option<f64> {
499        if self.xg_m == F64_DNU {
500            None
501        } else {
502            Some(self.xg_m)
503        }
504    }
505    pub fn position_y_m(&self) -> Option<f64> {
506        if self.yg_m == F64_DNU {
507            None
508        } else {
509            Some(self.yg_m)
510        }
511    }
512    pub fn position_z_m(&self) -> Option<f64> {
513        if self.zg_m == F64_DNU {
514            None
515        } else {
516            Some(self.zg_m)
517        }
518    }
519    pub fn velocity_x_mps(&self) -> Option<f64> {
520        if self.xgd_mps == F64_DNU {
521            None
522        } else {
523            Some(self.xgd_mps)
524        }
525    }
526    pub fn velocity_y_mps(&self) -> Option<f64> {
527        if self.ygd_mps == F64_DNU {
528            None
529        } else {
530            Some(self.ygd_mps)
531        }
532    }
533    pub fn velocity_z_mps(&self) -> Option<f64> {
534        if self.zgd_mps == F64_DNU {
535            None
536        } else {
537            Some(self.zgd_mps)
538        }
539    }
540}
541
542impl SbfBlockParse for GeoAlmBlock {
543    const BLOCK_ID: u16 = block_ids::GEO_ALM;
544
545    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
546        const MIN_LEN: usize = 68;
547        if data.len() < MIN_LEN {
548            return Err(SbfError::ParseError("GEOAlm too short".into()));
549        }
550
551        let prn = data[12];
552        let data_id = data[13];
553        let health = u16::from_le_bytes([data[14], data[15]]);
554        let t0 = u32::from_le_bytes([data[16], data[17], data[18], data[19]]);
555        let xg_m = f64::from_le_bytes(data[20..28].try_into().unwrap());
556        let yg_m = f64::from_le_bytes(data[28..36].try_into().unwrap());
557        let zg_m = f64::from_le_bytes(data[36..44].try_into().unwrap());
558        let xgd_mps = f64::from_le_bytes(data[44..52].try_into().unwrap());
559        let ygd_mps = f64::from_le_bytes(data[52..60].try_into().unwrap());
560        let zgd_mps = f64::from_le_bytes(data[60..68].try_into().unwrap());
561
562        Ok(Self {
563            tow_ms: header.tow_ms,
564            wnc: header.wnc,
565            prn,
566            data_id,
567            health,
568            t0,
569            xg_m,
570            yg_m,
571            zg_m,
572            xgd_mps,
573            ygd_mps,
574            zgd_mps,
575        })
576    }
577}
578
579// ============================================================================
580// GEOFastCorrDegr Block
581// ============================================================================
582
583/// GEOFastCorrDegr block (Block ID 5929)
584///
585/// SBAS MT07 fast correction degradation (AI indices per PRN mask).
586#[derive(Debug, Clone)]
587pub struct GeoFastCorrDegrBlock {
588    tow_ms: u32,
589    wnc: u16,
590    /// SBAS PRN
591    pub prn: u8,
592    /// Issue of Data PRN mask
593    pub iodp: u8,
594    /// System latency (seconds)
595    pub t_lat: u8,
596    /// Degradation factor indices (51 entries)
597    pub ai: [u8; 51],
598}
599
600impl GeoFastCorrDegrBlock {
601    pub fn tow_seconds(&self) -> f64 {
602        self.tow_ms as f64 * 0.001
603    }
604    pub fn tow_ms(&self) -> u32 {
605        self.tow_ms
606    }
607    pub fn wnc(&self) -> u16 {
608        self.wnc
609    }
610}
611
612impl SbfBlockParse for GeoFastCorrDegrBlock {
613    const BLOCK_ID: u16 = block_ids::GEO_FAST_CORR_DEGR;
614
615    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
616        const MIN_LEN: usize = 66;
617        if data.len() < MIN_LEN {
618            return Err(SbfError::ParseError("GEOFastCorrDegr too short".into()));
619        }
620
621        let prn = data[12];
622        let iodp = data[13];
623        let t_lat = data[14];
624        let mut ai = [0u8; 51];
625        ai.copy_from_slice(&data[15..66]);
626
627        Ok(Self {
628            tow_ms: header.tow_ms,
629            wnc: header.wnc,
630            prn,
631            iodp,
632            t_lat,
633            ai,
634        })
635    }
636}
637
638// ============================================================================
639// GEODegrFactors Block
640// ============================================================================
641
642/// GEODegrFactors block (Block ID 5930)
643///
644/// SBAS MT10 degradation factors for integrity bounds.
645#[derive(Debug, Clone)]
646pub struct GeoDegrFactorsBlock {
647    tow_ms: u32,
648    wnc: u16,
649    /// SBAS PRN
650    pub prn: u8,
651    brrc: f64,
652    cltc_lsb: f64,
653    cltc_v1: f64,
654    pub iltc_v1: u32,
655    cltc_v0: f64,
656    pub iltc_v0: u32,
657    cgeo_lsb: f64,
658    cgeo_v: f64,
659    pub igeo: u32,
660    cer: f32,
661    ciono_step: f64,
662    pub iiono: u32,
663    ciono_ramp: f64,
664    pub rss_udre: u8,
665    pub rss_iono: u8,
666    ccovariance: f64,
667}
668
669impl GeoDegrFactorsBlock {
670    pub fn tow_seconds(&self) -> f64 {
671        self.tow_ms as f64 * 0.001
672    }
673    pub fn tow_ms(&self) -> u32 {
674        self.tow_ms
675    }
676    pub fn wnc(&self) -> u16 {
677        self.wnc
678    }
679    pub fn brrc(&self) -> Option<f64> {
680        if self.brrc == F64_DNU {
681            None
682        } else {
683            Some(self.brrc)
684        }
685    }
686    pub fn cltc_lsb(&self) -> Option<f64> {
687        if self.cltc_lsb == F64_DNU {
688            None
689        } else {
690            Some(self.cltc_lsb)
691        }
692    }
693    pub fn cltc_v1(&self) -> Option<f64> {
694        if self.cltc_v1 == F64_DNU {
695            None
696        } else {
697            Some(self.cltc_v1)
698        }
699    }
700    pub fn cltc_v0(&self) -> Option<f64> {
701        if self.cltc_v0 == F64_DNU {
702            None
703        } else {
704            Some(self.cltc_v0)
705        }
706    }
707    pub fn cgeo_lsb(&self) -> Option<f64> {
708        if self.cgeo_lsb == F64_DNU {
709            None
710        } else {
711            Some(self.cgeo_lsb)
712        }
713    }
714    pub fn cgeo_v(&self) -> Option<f64> {
715        if self.cgeo_v == F64_DNU {
716            None
717        } else {
718            Some(self.cgeo_v)
719        }
720    }
721    pub fn cer(&self) -> Option<f32> {
722        if self.cer == F32_DNU {
723            None
724        } else {
725            Some(self.cer)
726        }
727    }
728    pub fn ciono_step(&self) -> Option<f64> {
729        if self.ciono_step == F64_DNU {
730            None
731        } else {
732            Some(self.ciono_step)
733        }
734    }
735    pub fn ciono_ramp(&self) -> Option<f64> {
736        if self.ciono_ramp == F64_DNU {
737            None
738        } else {
739            Some(self.ciono_ramp)
740        }
741    }
742    pub fn ccovariance(&self) -> Option<f64> {
743        if self.ccovariance == F64_DNU {
744            None
745        } else {
746            Some(self.ccovariance)
747        }
748    }
749}
750
751impl SbfBlockParse for GeoDegrFactorsBlock {
752    const BLOCK_ID: u16 = block_ids::GEO_DEGR_FACTORS;
753
754    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
755        const MIN_LEN: usize = 107;
756        if data.len() < MIN_LEN {
757            return Err(SbfError::ParseError("GEODegrFactors too short".into()));
758        }
759
760        let prn = data[12];
761        let brrc = f64::from_le_bytes(data[13..21].try_into().unwrap());
762        let cltc_lsb = f64::from_le_bytes(data[21..29].try_into().unwrap());
763        let cltc_v1 = f64::from_le_bytes(data[29..37].try_into().unwrap());
764        let iltc_v1 = u32::from_le_bytes([data[37], data[38], data[39], data[40]]);
765        let cltc_v0 = f64::from_le_bytes(data[41..49].try_into().unwrap());
766        let iltc_v0 = u32::from_le_bytes([data[49], data[50], data[51], data[52]]);
767        let cgeo_lsb = f64::from_le_bytes(data[53..61].try_into().unwrap());
768        let cgeo_v = f64::from_le_bytes(data[61..69].try_into().unwrap());
769        let igeo = u32::from_le_bytes([data[69], data[70], data[71], data[72]]);
770        let cer = f32::from_le_bytes(data[73..77].try_into().unwrap());
771        let ciono_step = f64::from_le_bytes(data[77..85].try_into().unwrap());
772        let iiono = u32::from_le_bytes([data[85], data[86], data[87], data[88]]);
773        let ciono_ramp = f64::from_le_bytes(data[89..97].try_into().unwrap());
774        let rss_udre = data[97];
775        let rss_iono = data[98];
776        let ccovariance = f64::from_le_bytes(data[99..107].try_into().unwrap());
777
778        Ok(Self {
779            tow_ms: header.tow_ms,
780            wnc: header.wnc,
781            prn,
782            brrc,
783            cltc_lsb,
784            cltc_v1,
785            iltc_v1,
786            cltc_v0,
787            iltc_v0,
788            cgeo_lsb,
789            cgeo_v,
790            igeo,
791            cer,
792            ciono_step,
793            iiono,
794            ciono_ramp,
795            rss_udre,
796            rss_iono,
797            ccovariance,
798        })
799    }
800}
801
802// ============================================================================
803// GEOServiceLevel Block
804// ============================================================================
805
806/// One service region in GEOServiceLevel.
807#[derive(Debug, Clone)]
808pub struct GeoServiceRegion {
809    /// Latitude 1 (degrees)
810    pub latitude1: i8,
811    /// Latitude 2 (degrees)
812    pub latitude2: i8,
813    /// Longitude 1 (degrees)
814    pub longitude1: i16,
815    /// Longitude 2 (degrees)
816    pub longitude2: i16,
817    /// Region shape code
818    pub region_shape: u8,
819}
820
821/// GEOServiceLevel block (Block ID 5917)
822///
823/// SBAS MT27 service message with service regions.
824#[derive(Debug, Clone)]
825pub struct GeoServiceLevelBlock {
826    tow_ms: u32,
827    wnc: u16,
828    /// SBAS PRN
829    pub prn: u8,
830    /// Issue of Data Service
831    pub iods: u8,
832    /// Number of messages
833    pub nr_messages: u8,
834    /// Message number
835    pub message_nr: u8,
836    /// Priority code
837    pub priority_code: u8,
838    /// UDREI delta inside
839    pub d_udrei_in: u8,
840    /// UDREI delta outside
841    pub d_udrei_out: u8,
842    /// Number of regions
843    pub n: u8,
844    /// Sub-block length
845    pub sb_length: u8,
846    /// Service regions
847    pub regions: Vec<GeoServiceRegion>,
848}
849
850impl GeoServiceLevelBlock {
851    pub fn tow_seconds(&self) -> f64 {
852        self.tow_ms as f64 * 0.001
853    }
854    pub fn tow_ms(&self) -> u32 {
855        self.tow_ms
856    }
857    pub fn wnc(&self) -> u16 {
858        self.wnc
859    }
860    pub fn num_regions(&self) -> usize {
861        self.regions.len()
862    }
863}
864
865impl SbfBlockParse for GeoServiceLevelBlock {
866    const BLOCK_ID: u16 = block_ids::GEO_SERVICE_LEVEL;
867
868    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
869        const MIN_LEN: usize = 21;
870        if data.len() < MIN_LEN {
871            return Err(SbfError::ParseError("GEOServiceLevel too short".into()));
872        }
873
874        let prn = data[12];
875        let iods = data[13];
876        let nr_messages = data[14];
877        let message_nr = data[15];
878        let priority_code = data[16];
879        let d_udrei_in = data[17];
880        let d_udrei_out = data[18];
881        let n = data[19] as usize;
882        let sb_length = data[20] as usize;
883
884        if sb_length < 7 {
885            return Err(SbfError::ParseError(
886                "GEOServiceLevel SBLength too small".into(),
887            ));
888        }
889
890        let mut regions = Vec::with_capacity(n);
891        let mut offset = 21;
892
893        for _ in 0..n {
894            if offset + sb_length > data.len() {
895                return Err(SbfError::ParseError(
896                    "GEOServiceLevel data truncated: fewer regions than N".into(),
897                ));
898            }
899
900            let latitude1 = data[offset] as i8;
901            let latitude2 = data[offset + 1] as i8;
902            let longitude1 = i16::from_le_bytes([data[offset + 2], data[offset + 3]]);
903            let longitude2 = i16::from_le_bytes([data[offset + 4], data[offset + 5]]);
904            let region_shape = data[offset + 6];
905
906            regions.push(GeoServiceRegion {
907                latitude1,
908                latitude2,
909                longitude1,
910                longitude2,
911                region_shape,
912            });
913
914            offset += sb_length;
915        }
916
917        Ok(Self {
918            tow_ms: header.tow_ms,
919            wnc: header.wnc,
920            prn,
921            iods,
922            nr_messages,
923            message_nr,
924            priority_code,
925            d_udrei_in,
926            d_udrei_out,
927            n: n as u8,
928            sb_length: sb_length as u8,
929            regions,
930        })
931    }
932}
933
934// ============================================================================
935// GEONetworkTime Block
936// ============================================================================
937
938/// GEONetworkTime block (Block ID 5918)
939///
940/// SBAS MT12 network time / UTC offset parameters.
941#[derive(Debug, Clone)]
942pub struct GeoNetworkTimeBlock {
943    tow_ms: u32,
944    wnc: u16,
945    /// SBAS PRN
946    pub prn: u8,
947    /// Time offset drift (s/s)
948    pub a1: f32,
949    /// Time offset (s)
950    pub a0: f64,
951    /// Reference time (s)
952    pub t_ot: u32,
953    /// Reference week
954    pub wn_t: u8,
955    /// Current offset (s)
956    pub del_t_1s: i8,
957    /// Leap second week
958    pub wn_lsf: u8,
959    /// Leap second day
960    pub dn: u8,
961    /// Future offset (s)
962    pub del_t_lsf: i8,
963    /// UTC standard ID
964    pub utc_std_id: u8,
965    /// GPS week number
966    pub gps_wn: u16,
967    /// GPS time of week
968    pub gps_tow: u32,
969    /// GLONASS indicator
970    pub glonass_ind: u8,
971}
972
973impl GeoNetworkTimeBlock {
974    pub fn tow_seconds(&self) -> f64 {
975        self.tow_ms as f64 * 0.001
976    }
977    pub fn tow_ms(&self) -> u32 {
978        self.tow_ms
979    }
980    pub fn wnc(&self) -> u16 {
981        self.wnc
982    }
983}
984
985impl SbfBlockParse for GeoNetworkTimeBlock {
986    const BLOCK_ID: u16 = block_ids::GEO_NETWORK_TIME;
987
988    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
989        const MIN_LEN: usize = 42;
990        if data.len() < MIN_LEN {
991            return Err(SbfError::ParseError("GEONetworkTime too short".into()));
992        }
993
994        let prn = data[12];
995        let a1 = f32::from_le_bytes(data[13..17].try_into().unwrap());
996        let a0 = f64::from_le_bytes(data[17..25].try_into().unwrap());
997        let t_ot = u32::from_le_bytes([data[25], data[26], data[27], data[28]]);
998        let wn_t = data[29];
999        let del_t_1s = data[30] as i8;
1000        let wn_lsf = data[31];
1001        let dn = data[32];
1002        let del_t_lsf = data[33] as i8;
1003        let utc_std_id = data[34];
1004        let gps_wn = u16::from_le_bytes([data[35], data[36]]);
1005        let gps_tow = u32::from_le_bytes([data[37], data[38], data[39], data[40]]);
1006        let glonass_ind = data[41];
1007
1008        Ok(Self {
1009            tow_ms: header.tow_ms,
1010            wnc: header.wnc,
1011            prn,
1012            a1,
1013            a0,
1014            t_ot,
1015            wn_t,
1016            del_t_1s,
1017            wn_lsf,
1018            dn,
1019            del_t_lsf,
1020            utc_std_id,
1021            gps_wn,
1022            gps_tow,
1023            glonass_ind,
1024        })
1025    }
1026}
1027
1028// ============================================================================
1029// GEOIGPMask Block
1030// ============================================================================
1031
1032/// GEOIGPMask block (Block ID 5931)
1033///
1034/// SBAS MT18 ionospheric grid point mask.
1035#[derive(Debug, Clone)]
1036pub struct GeoIgpMaskBlock {
1037    tow_ms: u32,
1038    wnc: u16,
1039    /// SBAS PRN
1040    pub prn: u8,
1041    /// Number of bands
1042    pub nbr_bands: u8,
1043    /// Band number
1044    pub band_nbr: u8,
1045    /// Issue of Data Ionosphere
1046    pub iodi: u8,
1047    /// Number of IGPs
1048    pub nbr_igps: u8,
1049    /// IGP mask array
1050    pub igp_mask: Vec<u8>,
1051}
1052
1053impl GeoIgpMaskBlock {
1054    pub fn tow_seconds(&self) -> f64 {
1055        self.tow_ms as f64 * 0.001
1056    }
1057    pub fn tow_ms(&self) -> u32 {
1058        self.tow_ms
1059    }
1060    pub fn wnc(&self) -> u16 {
1061        self.wnc
1062    }
1063    pub fn num_igps(&self) -> usize {
1064        self.igp_mask.len()
1065    }
1066}
1067
1068impl SbfBlockParse for GeoIgpMaskBlock {
1069    const BLOCK_ID: u16 = block_ids::GEO_IGP_MASK;
1070
1071    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1072        const MIN_LEN: usize = 18;
1073        if data.len() < MIN_LEN {
1074            return Err(SbfError::ParseError("GEOIGPMask too short".into()));
1075        }
1076
1077        let prn = data[12];
1078        let nbr_bands = data[13];
1079        let band_nbr = data[14];
1080        let iodi = data[15];
1081        let nbr_igps = data[16] as usize;
1082
1083        let igp_mask_len = nbr_igps.min(data.len().saturating_sub(17));
1084        let igp_mask: Vec<u8> = data[17..17 + igp_mask_len].to_vec();
1085
1086        Ok(Self {
1087            tow_ms: header.tow_ms,
1088            wnc: header.wnc,
1089            prn,
1090            nbr_bands,
1091            band_nbr,
1092            iodi,
1093            nbr_igps: nbr_igps as u8,
1094            igp_mask,
1095        })
1096    }
1097}
1098
1099// ============================================================================
1100// GEOLongTermCorr Block
1101// ============================================================================
1102
1103/// One long-term correction entry in `GeoLongTermCorrBlock`.
1104#[derive(Debug, Clone)]
1105pub struct GeoLongTermCorrEntry {
1106    /// Velocity code present flag
1107    pub velocity_code: u8,
1108    /// PRN mask number
1109    pub prn_mask_no: u8,
1110    /// Issue of Data PRN mask
1111    pub iodp: u8,
1112    /// Issue of Data Ephemeris
1113    pub iode: u8,
1114    dx_m: f32,
1115    dy_m: f32,
1116    dz_m: f32,
1117    dx_rate_mps: f32,
1118    dy_rate_mps: f32,
1119    dz_rate_mps: f32,
1120    da_f0_s: f32,
1121    da_f1_sps: f32,
1122    /// Reference time (seconds)
1123    pub t_oe: u32,
1124}
1125
1126impl GeoLongTermCorrEntry {
1127    pub fn dx_m(&self) -> Option<f32> {
1128        if self.dx_m == F32_DNU {
1129            None
1130        } else {
1131            Some(self.dx_m)
1132        }
1133    }
1134    pub fn dy_m(&self) -> Option<f32> {
1135        if self.dy_m == F32_DNU {
1136            None
1137        } else {
1138            Some(self.dy_m)
1139        }
1140    }
1141    pub fn dz_m(&self) -> Option<f32> {
1142        if self.dz_m == F32_DNU {
1143            None
1144        } else {
1145            Some(self.dz_m)
1146        }
1147    }
1148    pub fn dx_rate_mps(&self) -> Option<f32> {
1149        if self.dx_rate_mps == F32_DNU {
1150            None
1151        } else {
1152            Some(self.dx_rate_mps)
1153        }
1154    }
1155    pub fn dy_rate_mps(&self) -> Option<f32> {
1156        if self.dy_rate_mps == F32_DNU {
1157            None
1158        } else {
1159            Some(self.dy_rate_mps)
1160        }
1161    }
1162    pub fn dz_rate_mps(&self) -> Option<f32> {
1163        if self.dz_rate_mps == F32_DNU {
1164            None
1165        } else {
1166            Some(self.dz_rate_mps)
1167        }
1168    }
1169    pub fn da_f0_s(&self) -> Option<f32> {
1170        if self.da_f0_s == F32_DNU {
1171            None
1172        } else {
1173            Some(self.da_f0_s)
1174        }
1175    }
1176    pub fn da_f1_sps(&self) -> Option<f32> {
1177        if self.da_f1_sps == F32_DNU {
1178            None
1179        } else {
1180            Some(self.da_f1_sps)
1181        }
1182    }
1183}
1184
1185/// GEOLongTermCorr block (Block ID 5932)
1186///
1187/// SBAS MT24/25 long-term satellite error corrections.
1188#[derive(Debug, Clone)]
1189pub struct GeoLongTermCorrBlock {
1190    tow_ms: u32,
1191    wnc: u16,
1192    /// SBAS PRN
1193    pub prn: u8,
1194    /// Number of corrections
1195    pub n: u8,
1196    /// Sub-block length
1197    pub sb_length: u8,
1198    /// Long-term correction entries
1199    pub corrections: Vec<GeoLongTermCorrEntry>,
1200}
1201
1202impl GeoLongTermCorrBlock {
1203    pub fn tow_seconds(&self) -> f64 {
1204        self.tow_ms as f64 * 0.001
1205    }
1206    pub fn tow_ms(&self) -> u32 {
1207        self.tow_ms
1208    }
1209    pub fn wnc(&self) -> u16 {
1210        self.wnc
1211    }
1212    pub fn num_corrections(&self) -> usize {
1213        self.corrections.len()
1214    }
1215}
1216
1217impl SbfBlockParse for GeoLongTermCorrBlock {
1218    const BLOCK_ID: u16 = block_ids::GEO_LONG_TERM_CORR;
1219
1220    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1221        const MIN_LEN: usize = 16;
1222        if data.len() < MIN_LEN {
1223            return Err(SbfError::ParseError("GEOLongTermCorr too short".into()));
1224        }
1225
1226        let prn = data[12];
1227        let n = data[13] as usize;
1228        let sb_length = data[14] as usize;
1229
1230        if sb_length < 28 {
1231            return Err(SbfError::ParseError(
1232                "GEOLongTermCorr SBLength too small".into(),
1233            ));
1234        }
1235
1236        let mut corrections = Vec::with_capacity(n);
1237        let mut offset = 15;
1238
1239        for _ in 0..n {
1240            if offset + sb_length > data.len() {
1241                break;
1242            }
1243
1244            let velocity_code = data[offset];
1245            let prn_mask_no = data[offset + 1];
1246            let iodp = data[offset + 2];
1247            let iode = data[offset + 3];
1248            let dx_m = f32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
1249            let dy_m = f32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
1250            let dz_m = f32::from_le_bytes(data[offset + 12..offset + 16].try_into().unwrap());
1251
1252            let (dx_rate_mps, dy_rate_mps, dz_rate_mps, da_f0_s, da_f1_sps, t_oe) =
1253                if sb_length >= 40 {
1254                    (
1255                        f32::from_le_bytes(data[offset + 16..offset + 20].try_into().unwrap()),
1256                        f32::from_le_bytes(data[offset + 20..offset + 24].try_into().unwrap()),
1257                        f32::from_le_bytes(data[offset + 24..offset + 28].try_into().unwrap()),
1258                        f32::from_le_bytes(data[offset + 28..offset + 32].try_into().unwrap()),
1259                        f32::from_le_bytes(data[offset + 32..offset + 36].try_into().unwrap()),
1260                        u32::from_le_bytes([
1261                            data[offset + 36],
1262                            data[offset + 37],
1263                            data[offset + 38],
1264                            data[offset + 39],
1265                        ]),
1266                    )
1267                } else {
1268                    let t_oe = u32::from_le_bytes([
1269                        data[offset + 24],
1270                        data[offset + 25],
1271                        data[offset + 26],
1272                        data[offset + 27],
1273                    ]);
1274                    (
1275                        F32_DNU,
1276                        F32_DNU,
1277                        F32_DNU,
1278                        f32::from_le_bytes(data[offset + 16..offset + 20].try_into().unwrap()),
1279                        f32::from_le_bytes(data[offset + 20..offset + 24].try_into().unwrap()),
1280                        t_oe,
1281                    )
1282                };
1283
1284            corrections.push(GeoLongTermCorrEntry {
1285                velocity_code,
1286                prn_mask_no,
1287                iodp,
1288                iode,
1289                dx_m,
1290                dy_m,
1291                dz_m,
1292                dx_rate_mps,
1293                dy_rate_mps,
1294                dz_rate_mps,
1295                da_f0_s,
1296                da_f1_sps,
1297                t_oe,
1298            });
1299
1300            offset += sb_length;
1301        }
1302
1303        Ok(Self {
1304            tow_ms: header.tow_ms,
1305            wnc: header.wnc,
1306            prn,
1307            n: n as u8,
1308            sb_length: sb_length as u8,
1309            corrections,
1310        })
1311    }
1312}
1313
1314// ============================================================================
1315// GEOClockEphCovMatrix Block
1316// ============================================================================
1317
1318/// One covariance matrix entry in `GeoClockEphCovMatrixBlock`.
1319#[derive(Debug, Clone)]
1320pub struct GeoClockEphCovMatrixEntry {
1321    /// PRN mask number
1322    pub prn_mask_no: u8,
1323    /// Scale exponent
1324    pub scale_exp: u8,
1325    /// Covariance elements (E11, E22, E33, E44)
1326    pub e11: u16,
1327    pub e22: u16,
1328    pub e33: u16,
1329    pub e44: u16,
1330    /// Off-diagonal elements
1331    pub e12: i16,
1332    pub e13: i16,
1333    pub e14: i16,
1334    pub e23: i16,
1335    pub e24: i16,
1336    pub e34: i16,
1337}
1338
1339impl GeoClockEphCovMatrixEntry {
1340    /// Scale factor: 2^scale_exp
1341    pub fn scale_factor(&self) -> f64 {
1342        2f64.powi(self.scale_exp as i32)
1343    }
1344}
1345
1346/// GEOClockEphCovMatrix block (Block ID 5934)
1347///
1348/// SBAS MT28 clock-ephemeris covariance matrix.
1349#[derive(Debug, Clone)]
1350pub struct GeoClockEphCovMatrixBlock {
1351    tow_ms: u32,
1352    wnc: u16,
1353    /// SBAS PRN
1354    pub prn: u8,
1355    /// Issue of Data PRN mask
1356    pub iodp: u8,
1357    /// Number of covariance entries
1358    pub n: u8,
1359    /// Sub-block length
1360    pub sb_length: u8,
1361    /// Covariance matrix entries
1362    pub entries: Vec<GeoClockEphCovMatrixEntry>,
1363}
1364
1365impl GeoClockEphCovMatrixBlock {
1366    pub fn tow_seconds(&self) -> f64 {
1367        self.tow_ms as f64 * 0.001
1368    }
1369    pub fn tow_ms(&self) -> u32 {
1370        self.tow_ms
1371    }
1372    pub fn wnc(&self) -> u16 {
1373        self.wnc
1374    }
1375    pub fn num_entries(&self) -> usize {
1376        self.entries.len()
1377    }
1378}
1379
1380impl SbfBlockParse for GeoClockEphCovMatrixBlock {
1381    const BLOCK_ID: u16 = block_ids::GEO_CLOCK_EPH_COV_MATRIX;
1382
1383    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
1384        const MIN_LEN: usize = 17;
1385        if data.len() < MIN_LEN {
1386            return Err(SbfError::ParseError(
1387                "GEOClockEphCovMatrix too short".into(),
1388            ));
1389        }
1390
1391        let prn = data[12];
1392        let iodp = data[13];
1393        let n = data[14] as usize;
1394        let sb_length = data[15] as usize;
1395
1396        if sb_length < 22 {
1397            return Err(SbfError::ParseError(
1398                "GEOClockEphCovMatrix SBLength too small".into(),
1399            ));
1400        }
1401
1402        let mut entries = Vec::with_capacity(n);
1403        let mut offset = 16;
1404
1405        for _ in 0..n {
1406            if offset + sb_length > data.len() {
1407                break;
1408            }
1409
1410            let prn_mask_no = data[offset];
1411            let scale_exp = data[offset + 1];
1412            let e11 = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
1413            let e22 = u16::from_le_bytes([data[offset + 4], data[offset + 5]]);
1414            let e33 = u16::from_le_bytes([data[offset + 6], data[offset + 7]]);
1415            let e44 = u16::from_le_bytes([data[offset + 8], data[offset + 9]]);
1416            let e12 = i16::from_le_bytes([data[offset + 10], data[offset + 11]]);
1417            let e13 = i16::from_le_bytes([data[offset + 12], data[offset + 13]]);
1418            let e14 = i16::from_le_bytes([data[offset + 14], data[offset + 15]]);
1419            let e23 = i16::from_le_bytes([data[offset + 16], data[offset + 17]]);
1420            let e24 = i16::from_le_bytes([data[offset + 18], data[offset + 19]]);
1421            let e34 = i16::from_le_bytes([data[offset + 20], data[offset + 21]]);
1422
1423            entries.push(GeoClockEphCovMatrixEntry {
1424                prn_mask_no,
1425                scale_exp,
1426                e11,
1427                e22,
1428                e33,
1429                e44,
1430                e12,
1431                e13,
1432                e14,
1433                e23,
1434                e24,
1435                e34,
1436            });
1437
1438            offset += sb_length;
1439        }
1440
1441        Ok(Self {
1442            tow_ms: header.tow_ms,
1443            wnc: header.wnc,
1444            prn,
1445            iodp,
1446            n: n as u8,
1447            sb_length: sb_length as u8,
1448            entries,
1449        })
1450    }
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455    use super::*;
1456    use crate::header::SbfHeader;
1457
1458    fn header_for(block_id: u16, tow_ms: u32, wnc: u16) -> SbfHeader {
1459        SbfHeader {
1460            crc: 0,
1461            block_id,
1462            block_rev: 0,
1463            length: 32,
1464            tow_ms,
1465            wnc,
1466        }
1467    }
1468
1469    #[test]
1470    fn test_geo_mt00_parse() {
1471        let header = header_for(5925, 1000, 2200);
1472        let mut data = vec![0u8; 13];
1473        data[12] = 131; // PRN 131
1474
1475        let block = GeoMt00Block::parse(&header, &data).unwrap();
1476        assert_eq!(block.tow_seconds(), 1.0);
1477        assert_eq!(block.wnc(), 2200);
1478        assert_eq!(block.prn, 131);
1479    }
1480
1481    #[test]
1482    fn test_geo_mt00_too_short() {
1483        let header = header_for(5925, 0, 0);
1484        let data = [0u8; 10];
1485        assert!(GeoMt00Block::parse(&header, &data).is_err());
1486    }
1487
1488    #[test]
1489    fn test_geo_prn_mask_parse() {
1490        let header = header_for(5926, 2000, 2100);
1491        let mut data = vec![0u8; 20];
1492        data[12] = 120;
1493        data[13] = 5;
1494        data[14] = 3; // NbrPRNs = 3
1495        data[15] = 1;
1496        data[16] = 2;
1497        data[17] = 3;
1498
1499        let block = GeoPrnMaskBlock::parse(&header, &data).unwrap();
1500        assert_eq!(block.prn, 120);
1501        assert_eq!(block.iodp, 5);
1502        assert_eq!(block.nbr_prns, 3);
1503        assert_eq!(block.prn_mask, vec![1, 2, 3]);
1504    }
1505
1506    #[test]
1507    fn test_geo_fast_corr_parse() {
1508        let header = header_for(5927, 3000, 2000);
1509        let mut data = vec![0u8; 30];
1510        data[12] = 124;
1511        data[13] = 2; // MT
1512        data[14] = 1;
1513        data[15] = 0;
1514        data[16] = 1; // N = 1
1515        data[17] = 6; // SBLength = 6
1516        data[18] = 10; // PRNMaskNo
1517        data[19] = 2; // UDREI
1518        data[20..24].copy_from_slice(&2.5_f32.to_le_bytes()); // PRC = 2.5 m
1519
1520        let block = GeoFastCorrBlock::parse(&header, &data).unwrap();
1521        assert_eq!(block.prn, 124);
1522        assert_eq!(block.mt, 2);
1523        assert_eq!(block.n, 1);
1524        assert_eq!(block.corrections.len(), 1);
1525        assert_eq!(block.corrections[0].prn_mask_no, 10);
1526        assert_eq!(block.corrections[0].udrei, 2);
1527        assert!((block.corrections[0].prc_m().unwrap() - 2.5).abs() < 1e-6);
1528    }
1529
1530    #[test]
1531    fn test_geo_fast_corr_dnu() {
1532        let header = header_for(5927, 0, 0);
1533        let mut data = vec![0u8; 30];
1534        data[16] = 1;
1535        data[17] = 6;
1536        data[20..24].copy_from_slice(&F32_DNU.to_le_bytes());
1537
1538        let block = GeoFastCorrBlock::parse(&header, &data).unwrap();
1539        assert!(block.corrections[0].prc_m().is_none());
1540    }
1541
1542    #[test]
1543    fn test_geo_nav_parse() {
1544        let header = header_for(5896, 5000, 2200);
1545        let mut data = vec![0u8; 101];
1546        data[12] = 124;
1547        data[13..15].copy_from_slice(&1u16.to_le_bytes());
1548        data[15..17].copy_from_slice(&2u16.to_le_bytes());
1549        data[17..21].copy_from_slice(&100u32.to_le_bytes());
1550        data[21..29].copy_from_slice(&12345678.0_f64.to_le_bytes());
1551        data[29..37].copy_from_slice(&23456789.0_f64.to_le_bytes());
1552        data[37..45].copy_from_slice(&34567890.0_f64.to_le_bytes());
1553        data[93..97].copy_from_slice(&0.0001_f32.to_le_bytes());
1554        data[97..101].copy_from_slice(&0.00001_f32.to_le_bytes());
1555
1556        let block = GeoNavBlock::parse(&header, &data).unwrap();
1557        assert_eq!(block.tow_seconds(), 5.0);
1558        assert_eq!(block.wnc(), 2200);
1559        assert_eq!(block.prn, 124);
1560        assert_eq!(block.t0, 100);
1561        assert!((block.position_x_m().unwrap() - 12345678.0).abs() < 1e-6);
1562        assert!((block.clock_bias_s().unwrap() - 0.0001).abs() < 1e-9);
1563    }
1564
1565    #[test]
1566    fn test_geo_nav_dnu() {
1567        let header = header_for(5896, 0, 0);
1568        let mut data = vec![0u8; 101];
1569        data[21..29].copy_from_slice(&F64_DNU.to_le_bytes());
1570        data[93..97].copy_from_slice(&F32_DNU.to_le_bytes());
1571
1572        let block = GeoNavBlock::parse(&header, &data).unwrap();
1573        assert!(block.position_x_m().is_none());
1574        assert!(block.clock_bias_s().is_none());
1575    }
1576
1577    #[test]
1578    fn test_geo_integrity_parse() {
1579        let header = header_for(5928, 1000, 2100);
1580        let mut data = vec![0u8; 68];
1581        data[12] = 120;
1582        data[13..17].copy_from_slice(&[1, 2, 3, 4]);
1583        data[17..68].fill(5);
1584
1585        let block = GeoIntegrityBlock::parse(&header, &data).unwrap();
1586        assert_eq!(block.tow_seconds(), 1.0);
1587        assert_eq!(block.prn, 120);
1588        assert_eq!(block.iodf, [1, 2, 3, 4]);
1589        assert_eq!(block.udrei[0], 5);
1590    }
1591
1592    #[test]
1593    fn test_geo_alm_parse() {
1594        let header = header_for(5897, 2000, 2050);
1595        let mut data = vec![0u8; 68];
1596        data[12] = 131;
1597        data[13] = 1;
1598        data[14..16].copy_from_slice(&0u16.to_le_bytes());
1599        data[16..20].copy_from_slice(&500u32.to_le_bytes());
1600        data[20..28].copy_from_slice(&1e7_f64.to_le_bytes());
1601        data[28..36].copy_from_slice(&2e7_f64.to_le_bytes());
1602        data[36..44].copy_from_slice(&3e7_f64.to_le_bytes());
1603
1604        let block = GeoAlmBlock::parse(&header, &data).unwrap();
1605        assert_eq!(block.prn, 131);
1606        assert_eq!(block.data_id, 1);
1607        assert_eq!(block.t0, 500);
1608        assert!((block.position_x_m().unwrap() - 1e7).abs() < 1.0);
1609    }
1610
1611    #[test]
1612    fn test_geo_network_time_parse() {
1613        let header = header_for(5918, 3000, 2000);
1614        let mut data = vec![0u8; 42];
1615        data[12] = 124;
1616        data[13..17].copy_from_slice(&0.0001_f32.to_le_bytes());
1617        data[17..25].copy_from_slice(&0.5_f64.to_le_bytes());
1618        data[25..29].copy_from_slice(&1000u32.to_le_bytes());
1619        data[29] = 100;
1620        data[30] = 18i8 as u8;
1621        data[35..37].copy_from_slice(&2300u16.to_le_bytes());
1622        data[37..41].copy_from_slice(&43200000u32.to_le_bytes());
1623
1624        let block = GeoNetworkTimeBlock::parse(&header, &data).unwrap();
1625        assert_eq!(block.prn, 124);
1626        assert_eq!(block.gps_wn, 2300);
1627        assert_eq!(block.gps_tow, 43200000);
1628    }
1629
1630    #[test]
1631    fn test_geo_fast_corr_degr_parse() {
1632        let header = header_for(5929, 1000, 2200);
1633        let mut data = vec![0u8; 66];
1634        data[12] = 124;
1635        data[13] = 3;
1636        data[14] = 5;
1637        data[15..20].copy_from_slice(&[1, 2, 3, 4, 5]);
1638
1639        let block = GeoFastCorrDegrBlock::parse(&header, &data).unwrap();
1640        assert_eq!(block.tow_seconds(), 1.0);
1641        assert_eq!(block.prn, 124);
1642        assert_eq!(block.iodp, 3);
1643        assert_eq!(block.t_lat, 5);
1644        assert_eq!(block.ai[0], 1);
1645        assert_eq!(block.ai[4], 5);
1646    }
1647
1648    #[test]
1649    fn test_geo_fast_corr_degr_too_short() {
1650        let header = header_for(5929, 0, 0);
1651        let data = vec![0u8; 50];
1652        assert!(GeoFastCorrDegrBlock::parse(&header, &data).is_err());
1653    }
1654
1655    #[test]
1656    fn test_geo_degr_factors_parse() {
1657        let header = header_for(5930, 2000, 2100);
1658        let mut data = vec![0u8; 107];
1659        data[12] = 120;
1660        data[13..21].copy_from_slice(&1.5_f64.to_le_bytes());
1661        data[21..29].copy_from_slice(&0.125_f64.to_le_bytes());
1662        data[37..41].copy_from_slice(&60u32.to_le_bytes());
1663        data[97] = 1;
1664        data[98] = 0;
1665
1666        let block = GeoDegrFactorsBlock::parse(&header, &data).unwrap();
1667        assert_eq!(block.tow_seconds(), 2.0);
1668        assert_eq!(block.prn, 120);
1669        assert!((block.brrc().unwrap() - 1.5).abs() < 1e-10);
1670        assert_eq!(block.iltc_v1, 60);
1671        assert_eq!(block.rss_udre, 1);
1672    }
1673
1674    #[test]
1675    fn test_geo_degr_factors_dnu() {
1676        let header = header_for(5930, 0, 0);
1677        let mut data = vec![0u8; 107];
1678        data[13..21].copy_from_slice(&F64_DNU.to_le_bytes());
1679        data[73..77].copy_from_slice(&F32_DNU.to_le_bytes());
1680
1681        let block = GeoDegrFactorsBlock::parse(&header, &data).unwrap();
1682        assert!(block.brrc().is_none());
1683        assert!(block.cer().is_none());
1684    }
1685
1686    #[test]
1687    fn test_geo_service_level_parse() {
1688        let header = header_for(5917, 3000, 2000);
1689        let mut data = vec![0u8; 28];
1690        data[12] = 124;
1691        data[13] = 1;
1692        data[14] = 2;
1693        data[15] = 0;
1694        data[16] = 0;
1695        data[17] = 1;
1696        data[18] = 2;
1697        data[19] = 1;
1698        data[20] = 7;
1699        data[21] = 45i8 as u8;
1700        data[22] = 50i8 as u8;
1701        data[23..25].copy_from_slice(&(-120i16).to_le_bytes());
1702        data[25..27].copy_from_slice(&(-115i16).to_le_bytes());
1703        data[27] = 0;
1704
1705        let block = GeoServiceLevelBlock::parse(&header, &data).unwrap();
1706        assert_eq!(block.tow_seconds(), 3.0);
1707        assert_eq!(block.prn, 124);
1708        assert_eq!(block.iods, 1);
1709        assert_eq!(block.n, 1);
1710        assert_eq!(block.regions.len(), 1);
1711        assert_eq!(block.regions[0].latitude1, 45);
1712        assert_eq!(block.regions[0].latitude2, 50);
1713        assert_eq!(block.regions[0].longitude1, -120);
1714    }
1715
1716    #[test]
1717    fn test_geo_service_level_too_short() {
1718        let header = header_for(5917, 0, 0);
1719        let data = vec![0u8; 15];
1720        assert!(GeoServiceLevelBlock::parse(&header, &data).is_err());
1721    }
1722
1723    #[test]
1724    fn test_geo_service_level_truncated_regions() {
1725        let header = header_for(5917, 0, 0);
1726        let mut data = vec![0u8; 28];
1727        data[19] = 2;
1728        data[20] = 7;
1729        assert!(GeoServiceLevelBlock::parse(&header, &data).is_err());
1730    }
1731
1732    #[test]
1733    fn test_geo_igp_mask_parse() {
1734        let header = header_for(5931, 5000, 2100);
1735        let mut data = vec![0u8; 22];
1736        data[12] = 124;
1737        data[13] = 1;
1738        data[14] = 0;
1739        data[15] = 5;
1740        data[16] = 4;
1741        data[17] = 0xAB;
1742        data[18] = 0xCD;
1743        data[19] = 0xEF;
1744        data[20] = 0x12;
1745
1746        let block = GeoIgpMaskBlock::parse(&header, &data).unwrap();
1747        assert_eq!(block.tow_seconds(), 5.0);
1748        assert_eq!(block.wnc(), 2100);
1749        assert_eq!(block.prn, 124);
1750        assert_eq!(block.nbr_bands, 1);
1751        assert_eq!(block.band_nbr, 0);
1752        assert_eq!(block.iodi, 5);
1753        assert_eq!(block.nbr_igps, 4);
1754        assert_eq!(block.igp_mask.len(), 4);
1755        assert_eq!(block.igp_mask[0], 0xAB);
1756        assert_eq!(block.igp_mask[3], 0x12);
1757    }
1758
1759    #[test]
1760    fn test_geo_igp_mask_too_short() {
1761        let header = header_for(5931, 0, 0);
1762        let data = vec![0u8; 15];
1763        assert!(GeoIgpMaskBlock::parse(&header, &data).is_err());
1764    }
1765
1766    #[test]
1767    fn test_geo_long_term_corr_parse() {
1768        let header = header_for(5932, 1000, 2200);
1769        let mut data = vec![0u8; 56];
1770        data[12] = 131;
1771        data[13] = 1;
1772        data[14] = 40;
1773        data[15] = 1;
1774        data[16] = 2;
1775        data[17] = 3;
1776        data[18] = 4;
1777        data[19..23].copy_from_slice(&(1.5f32).to_le_bytes());
1778        data[23..27].copy_from_slice(&(2.5f32).to_le_bytes());
1779        data[27..31].copy_from_slice(&(3.5f32).to_le_bytes());
1780        data[31..35].copy_from_slice(&(0.1f32).to_le_bytes());
1781        data[35..39].copy_from_slice(&(0.2f32).to_le_bytes());
1782        data[39..43].copy_from_slice(&(0.3f32).to_le_bytes());
1783        data[43..47].copy_from_slice(&(1e-6f32).to_le_bytes());
1784        data[47..51].copy_from_slice(&(1e-10f32).to_le_bytes());
1785        data[51..55].copy_from_slice(&86400u32.to_le_bytes());
1786
1787        let block = GeoLongTermCorrBlock::parse(&header, &data).unwrap();
1788        assert_eq!(block.tow_seconds(), 1.0);
1789        assert_eq!(block.prn, 131);
1790        assert_eq!(block.n, 1);
1791        assert_eq!(block.corrections.len(), 1);
1792        let c = &block.corrections[0];
1793        assert_eq!(c.prn_mask_no, 2);
1794        assert_eq!(c.iodp, 3);
1795        assert_eq!(c.iode, 4);
1796        assert_eq!(c.dx_m(), Some(1.5));
1797        assert_eq!(c.t_oe, 86400);
1798    }
1799
1800    #[test]
1801    fn test_geo_long_term_corr_dnu() {
1802        let header = header_for(5932, 0, 0);
1803        let mut data = vec![0u8; 56];
1804        data[13] = 1;
1805        data[14] = 40;
1806        data[19..23].copy_from_slice(&F32_DNU.to_le_bytes());
1807        data[23..27].copy_from_slice(&F32_DNU.to_le_bytes());
1808        data[27..31].copy_from_slice(&F32_DNU.to_le_bytes());
1809
1810        let block = GeoLongTermCorrBlock::parse(&header, &data).unwrap();
1811        assert!(block.corrections[0].dx_m().is_none());
1812        assert!(block.corrections[0].dy_m().is_none());
1813        assert!(block.corrections[0].dz_m().is_none());
1814    }
1815
1816    #[test]
1817    fn test_geo_clock_eph_cov_matrix_parse() {
1818        let header = header_for(5934, 2000, 100);
1819        let mut data = vec![0u8; 40];
1820        data[12] = 120;
1821        data[13] = 7;
1822        data[14] = 1;
1823        data[15] = 22;
1824        data[16] = 1;
1825        data[17] = 3;
1826        data[18..20].copy_from_slice(&100u16.to_le_bytes());
1827        data[20..22].copy_from_slice(&200u16.to_le_bytes());
1828        data[22..24].copy_from_slice(&300u16.to_le_bytes());
1829        data[24..26].copy_from_slice(&400u16.to_le_bytes());
1830        data[26..28].copy_from_slice(&(-10i16).to_le_bytes());
1831        data[28..30].copy_from_slice(&(-20i16).to_le_bytes());
1832        data[30..32].copy_from_slice(&(-30i16).to_le_bytes());
1833        data[32..34].copy_from_slice(&(-40i16).to_le_bytes());
1834        data[34..36].copy_from_slice(&(-50i16).to_le_bytes());
1835        data[36..38].copy_from_slice(&(-60i16).to_le_bytes());
1836
1837        let block = GeoClockEphCovMatrixBlock::parse(&header, &data).unwrap();
1838        assert_eq!(block.tow_seconds(), 2.0);
1839        assert_eq!(block.prn, 120);
1840        assert_eq!(block.iodp, 7);
1841        assert_eq!(block.n, 1);
1842        assert_eq!(block.entries.len(), 1);
1843        let e = &block.entries[0];
1844        assert_eq!(e.prn_mask_no, 1);
1845        assert_eq!(e.scale_exp, 3);
1846        assert_eq!(e.e11, 100);
1847        assert_eq!(e.e22, 200);
1848        assert_eq!(e.e34, -60);
1849        assert_eq!(e.scale_factor(), 8.0);
1850    }
1851
1852    #[test]
1853    fn test_geo_clock_eph_cov_matrix_too_short() {
1854        let header = header_for(5934, 0, 0);
1855        let data = vec![0u8; 14];
1856        assert!(GeoClockEphCovMatrixBlock::parse(&header, &data).is_err());
1857    }
1858}