Skip to main content

sbf_tools/
types.rs

1//! Common SBF types for GNSS data representation
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6/// GNSS constellation identifier
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9pub enum Constellation {
10    GPS,
11    GLONASS,
12    Galileo,
13    BeiDou,
14    QZSS,
15    SBAS,
16    NavIC,
17    Unknown(u8),
18}
19
20impl Constellation {
21    /// Convert GNSS ID (from SBF GNSSId field) to Constellation
22    ///
23    /// Per SBF Reference Guide:
24    /// - 0: GPS
25    /// - 1: SBAS
26    /// - 2: Galileo
27    /// - 3: BeiDou
28    /// - 4: IMES (not supported)
29    /// - 5: QZSS
30    /// - 6: GLONASS
31    /// - 7: NavIC/IRNSS
32    pub fn from_gnss_id(id: u8) -> Self {
33        match id {
34            0 => Constellation::GPS,
35            1 => Constellation::SBAS,
36            2 => Constellation::Galileo,
37            3 => Constellation::BeiDou,
38            5 => Constellation::QZSS,
39            6 => Constellation::GLONASS,
40            7 => Constellation::NavIC,
41            _ => Constellation::Unknown(id),
42        }
43    }
44
45    /// Short name for the constellation
46    pub fn short_name(&self) -> &'static str {
47        match self {
48            Constellation::GPS => "GPS",
49            Constellation::GLONASS => "GLO",
50            Constellation::Galileo => "GAL",
51            Constellation::BeiDou => "BDS",
52            Constellation::QZSS => "QZS",
53            Constellation::SBAS => "SBA",
54            Constellation::NavIC => "NAV",
55            Constellation::Unknown(_) => "UNK",
56        }
57    }
58
59    /// Single character prefix for satellite IDs
60    pub fn prefix(&self) -> char {
61        match self {
62            Constellation::GPS => 'G',
63            Constellation::GLONASS => 'R',
64            Constellation::Galileo => 'E',
65            Constellation::BeiDou => 'C',
66            Constellation::QZSS => 'J',
67            Constellation::SBAS => 'S',
68            Constellation::NavIC => 'I',
69            Constellation::Unknown(_) => 'X',
70        }
71    }
72}
73
74impl std::fmt::Display for Constellation {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}", self.short_name())
77    }
78}
79
80/// Satellite identifier (constellation + PRN)
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83pub struct SatelliteId {
84    pub constellation: Constellation,
85    pub prn: u8,
86}
87
88impl SatelliteId {
89    /// Create a new satellite ID
90    pub fn new(constellation: Constellation, prn: u8) -> Self {
91        Self { constellation, prn }
92    }
93
94    /// Decode SVID into constellation + PRN per SBF spec.
95    ///
96    /// SVID ranges per SBF Reference Guide:
97    /// - 1-37: GPS
98    /// - 38-61: GLONASS (slot number = SVID - 37)
99    /// - 62: GLONASS (slot number 0)
100    /// - 63-68: GLONASS (slot number = SVID - 38)
101    /// - 71-106: Galileo (PRN = SVID - 70)
102    /// - 107-119: L-Band (MSS) satellites
103    /// - 120-140: SBAS (PRN = SVID - 100)
104    /// - 141-180: BeiDou (PRN = SVID - 140)
105    /// - 181-190: QZSS (PRN = SVID - 180)
106    /// - 191-197: NavIC/IRNSS (PRN = SVID - 190)
107    /// - 198-215: SBAS (PRN = SVID - 157)
108    /// - 216-222: NavIC/IRNSS (PRN = SVID - 208)
109    /// - 223-245: BeiDou (PRN = SVID - 182)
110    pub fn from_svid(svid: u8) -> Option<Self> {
111        if svid == 0 {
112            return None;
113        }
114
115        let (constellation, prn) = match svid {
116            1..=37 => (Constellation::GPS, svid),
117            38..=61 => (Constellation::GLONASS, svid - 37),
118            62 => (Constellation::GLONASS, 0),
119            63..=68 => (Constellation::GLONASS, svid - 38),
120            71..=106 => (Constellation::Galileo, svid - 70),
121            120..=140 => (Constellation::SBAS, svid - 100),
122            141..=180 => (Constellation::BeiDou, svid - 140),
123            181..=190 => (Constellation::QZSS, svid - 180),
124            191..=197 => (Constellation::NavIC, svid - 190),
125            198..=215 => (Constellation::SBAS, svid - 157),
126            216..=222 => (Constellation::NavIC, svid - 208),
127            223..=245 => (Constellation::BeiDou, svid - 182),
128            _ => (Constellation::Unknown(svid), svid),
129        };
130
131        Some(Self { constellation, prn })
132    }
133
134    /// Convert back to SVID
135    pub fn to_svid(&self) -> u8 {
136        match self.constellation {
137            Constellation::GPS => self.prn,
138            Constellation::GLONASS => {
139                if self.prn == 0 {
140                    62
141                } else if self.prn <= 24 {
142                    self.prn + 37
143                } else {
144                    self.prn + 38
145                }
146            }
147            Constellation::Galileo => self.prn + 70,
148            Constellation::SBAS => {
149                if self.prn <= 40 {
150                    self.prn + 100
151                } else {
152                    self.prn + 157
153                }
154            }
155            Constellation::BeiDou => {
156                if self.prn <= 40 {
157                    self.prn + 140
158                } else {
159                    self.prn + 182
160                }
161            }
162            Constellation::QZSS => self.prn + 180,
163            Constellation::NavIC => {
164                if self.prn <= 7 {
165                    self.prn + 190
166                } else {
167                    self.prn + 208
168                }
169            }
170            Constellation::Unknown(svid) => svid,
171        }
172    }
173
174    /// Generate a short key like "G01", "E05", "R24"
175    pub fn key(&self) -> String {
176        format!("{}{:02}", self.constellation.prefix(), self.prn)
177    }
178}
179
180impl std::fmt::Display for SatelliteId {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        write!(f, "{}", self.key())
183    }
184}
185
186/// Signal type for tracking
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
188#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
189pub enum SignalType {
190    // GPS signals
191    L1CA,
192    L1C,
193    L1PY,
194    L2C,
195    L2P,
196    L2PY,
197    L5,
198    // Galileo signals
199    E1,
200    E5a,
201    E5b,
202    E5AltBOC,
203    E6,
204    // GLONASS signals
205    G1CA,
206    G1P,
207    G2CA,
208    G2P,
209    G3,
210    // BeiDou signals
211    B1I,
212    B1C,
213    B2I,
214    B2a,
215    B2b,
216    B3I,
217    // QZSS signals
218    QZSSL1CA,
219    QZSSL2C,
220    QZSSL5,
221    QZSSL1C,
222    QZSSL1S,
223    QZSSL1CB,
224    QZSSL5S,
225    QZSSL6,
226    // SBAS signals
227    SBASL1,
228    SBASL5,
229    // MSS signals
230    LBand,
231    // NavIC signals
232    NavICL5,
233    NavICL1,
234    // Generic/fallback
235    Other(u8),
236}
237
238impl SignalType {
239    /// Decode signal type from global SBF signal number (per SBF Reference Guide section 4.1.10)
240    pub fn from_signal_number(sig_num: u8) -> Self {
241        match sig_num {
242            // GPS signals (0-5)
243            0 => SignalType::L1CA,
244            1 => SignalType::L1PY,
245            2 => SignalType::L2P,
246            3 => SignalType::L2C,
247            4 => SignalType::L5,
248            5 => SignalType::L1C,
249            // QZSS signals (6-7)
250            6 => SignalType::QZSSL1CA,
251            7 => SignalType::QZSSL2C,
252            // GLONASS signals (8-12)
253            8 => SignalType::G1CA,
254            9 => SignalType::G1P,
255            10 => SignalType::G2P,
256            11 => SignalType::G2CA,
257            12 => SignalType::G3,
258            // BeiDou signals (13-14)
259            13 => SignalType::B1C,
260            14 => SignalType::B2a,
261            // NavIC (15)
262            15 => SignalType::NavICL5,
263            // Galileo signals (17, 19-22)
264            17 => SignalType::E1,
265            19 => SignalType::E6,
266            20 => SignalType::E5a,
267            21 => SignalType::E5b,
268            22 => SignalType::E5AltBOC,
269            // MSS signals (23)
270            23 => SignalType::LBand,
271            // SBAS signals (24-25)
272            24 => SignalType::SBASL1,
273            25 => SignalType::SBASL5,
274            // QZSS signals (26-27)
275            26 => SignalType::QZSSL5,
276            27 => SignalType::QZSSL6,
277            // BeiDou signals (28-30)
278            28 => SignalType::B1I,
279            29 => SignalType::B2I,
280            30 => SignalType::B3I,
281            // QZSS signals (32-33)
282            32 => SignalType::QZSSL1C,
283            33 => SignalType::QZSSL1S,
284            // BeiDou signal (34)
285            34 => SignalType::B2b,
286            // NavIC (37)
287            37 => SignalType::NavICL1,
288            // QZSS signals (38-39)
289            38 => SignalType::QZSSL1CB,
290            39 => SignalType::QZSSL5S,
291            _ => SignalType::Other(sig_num),
292        }
293    }
294
295    /// Decode signal type from constellation-specific signal number
296    pub fn from_signal_number_and_constellation(
297        sig_num: u8,
298        constellation: &Constellation,
299    ) -> Self {
300        match constellation {
301            Constellation::GPS => match sig_num {
302                0 => SignalType::L1CA,
303                1 => SignalType::L1PY,
304                2 => SignalType::L2P,
305                3 => SignalType::L2C,
306                4 => SignalType::L5,
307                5 => SignalType::L1C,
308                _ => SignalType::Other(sig_num),
309            },
310            Constellation::GLONASS => match sig_num {
311                0 => SignalType::G1CA,
312                1 => SignalType::G1P,
313                2 => SignalType::G2P,
314                3 => SignalType::G2CA,
315                4 => SignalType::G3,
316                _ => SignalType::Other(sig_num),
317            },
318            Constellation::Galileo => match sig_num {
319                0 => SignalType::E1,
320                1 => SignalType::E5a,
321                2 => SignalType::E5b,
322                3 => SignalType::E5AltBOC,
323                4 => SignalType::E6,
324                _ => SignalType::Other(sig_num),
325            },
326            Constellation::BeiDou => match sig_num {
327                0 => SignalType::B1I,
328                1 => SignalType::B2I,
329                2 => SignalType::B3I,
330                3 => SignalType::B1C,
331                4 => SignalType::B2a,
332                5 => SignalType::B2b,
333                _ => SignalType::Other(sig_num),
334            },
335            Constellation::QZSS => match sig_num {
336                0 => SignalType::QZSSL1CA,
337                1 => SignalType::QZSSL1S,
338                2 => SignalType::L2P,
339                3 => SignalType::QZSSL2C,
340                4 => SignalType::QZSSL5,
341                5 => SignalType::QZSSL1C,
342                _ => SignalType::Other(sig_num),
343            },
344            Constellation::SBAS => match sig_num {
345                0 => SignalType::SBASL1,
346                1 => SignalType::SBASL5,
347                _ => SignalType::Other(sig_num),
348            },
349            Constellation::NavIC => match sig_num {
350                0 => SignalType::NavICL5,
351                1 => SignalType::NavICL1,
352                _ => SignalType::Other(sig_num),
353            },
354            Constellation::Unknown(_) => SignalType::Other(sig_num),
355        }
356    }
357
358    /// Short name suitable for display
359    pub fn name(&self) -> &'static str {
360        match self {
361            SignalType::L1CA => "L1CA",
362            SignalType::L1C => "L1C",
363            SignalType::L1PY => "L1PY",
364            SignalType::L2C => "L2C",
365            SignalType::L2P => "L2P",
366            SignalType::L2PY => "L2PY",
367            SignalType::L5 => "L5",
368            SignalType::E1 => "E1",
369            SignalType::E5a => "E5a",
370            SignalType::E5b => "E5b",
371            SignalType::E5AltBOC => "E5",
372            SignalType::E6 => "E6",
373            SignalType::G1CA => "G1",
374            SignalType::G1P => "G1P",
375            SignalType::G2CA => "G2",
376            SignalType::G2P => "G2P",
377            SignalType::G3 => "G3",
378            SignalType::B1I => "B1I",
379            SignalType::B1C => "B1C",
380            SignalType::B2I => "B2I",
381            SignalType::B2a => "B2a",
382            SignalType::B2b => "B2b",
383            SignalType::B3I => "B3I",
384            SignalType::QZSSL1CA => "L1",
385            SignalType::QZSSL2C => "L2C",
386            SignalType::QZSSL5 => "L5",
387            SignalType::QZSSL1C => "L1C",
388            SignalType::QZSSL1S => "L1S",
389            SignalType::QZSSL1CB => "L1CB",
390            SignalType::QZSSL5S => "L5S",
391            SignalType::QZSSL6 => "L6",
392            SignalType::SBASL1 => "L1",
393            SignalType::SBASL5 => "L5",
394            SignalType::LBand => "LBand",
395            SignalType::NavICL5 => "L5",
396            SignalType::NavICL1 => "L1",
397            SignalType::Other(_) => "?",
398        }
399    }
400
401    /// Get the frequency band category
402    pub fn band(&self) -> &'static str {
403        match self {
404            SignalType::L1CA | SignalType::L1C | SignalType::L1PY => "L1",
405            SignalType::L2C | SignalType::L2P | SignalType::L2PY => "L2",
406            SignalType::L5 => "L5",
407            SignalType::E1 => "L1",
408            SignalType::E5a => "L5",
409            SignalType::E5b => "E5b",
410            SignalType::E5AltBOC => "E5",
411            SignalType::E6 => "E6",
412            SignalType::G1CA | SignalType::G1P => "G1",
413            SignalType::G2CA | SignalType::G2P => "G2",
414            SignalType::G3 => "G3",
415            SignalType::B1I | SignalType::B1C => "B1",
416            SignalType::B2I | SignalType::B2a | SignalType::B2b => "B2",
417            SignalType::B3I => "B3",
418            SignalType::QZSSL1CA
419            | SignalType::QZSSL1C
420            | SignalType::QZSSL1S
421            | SignalType::QZSSL1CB => "L1",
422            SignalType::QZSSL2C => "L2",
423            SignalType::QZSSL5 | SignalType::QZSSL5S => "L5",
424            SignalType::QZSSL6 => "L6",
425            SignalType::SBASL1 => "L1",
426            SignalType::SBASL5 => "L5",
427            SignalType::LBand => "LBand",
428            SignalType::NavICL5 => "L5",
429            SignalType::NavICL1 => "L1",
430            SignalType::Other(_) => "?",
431        }
432    }
433}
434
435impl std::fmt::Display for SignalType {
436    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437        write!(f, "{}", self.name())
438    }
439}
440
441/// PVT solution mode
442#[derive(Debug, Clone, Copy, PartialEq, Eq)]
443#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
444pub enum PvtMode {
445    NoFix,
446    StandAlone,
447    Dgps,
448    Fixed,
449    FixedPpp,
450    Float,
451    FloatPpp,
452    Sbas,
453    RtkFixed,
454    RtkFloat,
455    MovingBaseRtkFixed,
456    MovingBaseRtkFloat,
457    Ppp,
458    Unknown(u8),
459}
460
461impl PvtMode {
462    /// Decode PVT mode from mode byte (bits 0-3)
463    pub fn from_mode_byte(mode: u8) -> Self {
464        match mode & 0x0F {
465            0 => PvtMode::NoFix,
466            1 => PvtMode::StandAlone,
467            2 => PvtMode::Dgps,
468            3 => PvtMode::Fixed,
469            4 => PvtMode::FixedPpp,
470            5 => PvtMode::Float,
471            6 => PvtMode::FloatPpp,
472            7 => PvtMode::Sbas,
473            8 => PvtMode::RtkFixed,
474            9 => PvtMode::RtkFloat,
475            10 => PvtMode::MovingBaseRtkFixed,
476            11 => PvtMode::MovingBaseRtkFloat,
477            12 => PvtMode::Ppp,
478            n => PvtMode::Unknown(n),
479        }
480    }
481
482    /// Check if this is a valid fix (2D or 3D)
483    pub fn has_fix(&self) -> bool {
484        !matches!(self, PvtMode::NoFix | PvtMode::Unknown(_))
485    }
486
487    /// Check if this is an RTK solution
488    pub fn is_rtk(&self) -> bool {
489        matches!(
490            self,
491            PvtMode::RtkFixed
492                | PvtMode::RtkFloat
493                | PvtMode::MovingBaseRtkFixed
494                | PvtMode::MovingBaseRtkFloat
495        )
496    }
497
498    /// Check if this is a PPP solution
499    pub fn is_ppp(&self) -> bool {
500        matches!(self, PvtMode::Ppp | PvtMode::FixedPpp | PvtMode::FloatPpp)
501    }
502}
503
504impl std::fmt::Display for PvtMode {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        let s = match self {
507            PvtMode::NoFix => "No Fix",
508            PvtMode::StandAlone => "Stand-Alone",
509            PvtMode::Dgps => "DGPS",
510            PvtMode::Fixed => "Fixed",
511            PvtMode::FixedPpp => "Fixed PPP",
512            PvtMode::Float => "Float",
513            PvtMode::FloatPpp => "Float PPP",
514            PvtMode::Sbas => "SBAS",
515            PvtMode::RtkFixed => "RTK Fixed",
516            PvtMode::RtkFloat => "RTK Float",
517            PvtMode::MovingBaseRtkFixed => "Moving Base RTK Fixed",
518            PvtMode::MovingBaseRtkFloat => "Moving Base RTK Float",
519            PvtMode::Ppp => "PPP",
520            PvtMode::Unknown(_) => "Unknown",
521        };
522        write!(f, "{}", s)
523    }
524}
525
526/// PVT error codes
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
529pub enum PvtError {
530    None,
531    NotEnoughMeasurements,
532    NotEnoughEphemerides,
533    DopTooLarge,
534    ResidualsTooLarge,
535    NoCovergence,
536    NotEnoughMeasurementsAfterRejection,
537    PositionProhibited,
538    NotEnoughDiffCorr,
539    BaseStationCoordinatesUnavailable,
540    Unknown(u8),
541}
542
543impl PvtError {
544    /// Decode PVT error from error byte
545    pub fn from_error_byte(error: u8) -> Self {
546        match error {
547            0 => PvtError::None,
548            1 => PvtError::NotEnoughMeasurements,
549            2 => PvtError::NotEnoughEphemerides,
550            3 => PvtError::DopTooLarge,
551            4 => PvtError::ResidualsTooLarge,
552            5 => PvtError::NoCovergence,
553            6 => PvtError::NotEnoughMeasurementsAfterRejection,
554            7 => PvtError::PositionProhibited,
555            8 => PvtError::NotEnoughDiffCorr,
556            9 => PvtError::BaseStationCoordinatesUnavailable,
557            n => PvtError::Unknown(n),
558        }
559    }
560
561    /// Check if there's no error
562    pub fn is_ok(&self) -> bool {
563        matches!(self, PvtError::None)
564    }
565}
566
567impl std::fmt::Display for PvtError {
568    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
569        let s = match self {
570            PvtError::None => "None",
571            PvtError::NotEnoughMeasurements => "Not enough measurements",
572            PvtError::NotEnoughEphemerides => "Not enough ephemerides",
573            PvtError::DopTooLarge => "DOP too large",
574            PvtError::ResidualsTooLarge => "Residuals too large",
575            PvtError::NoCovergence => "No convergence",
576            PvtError::NotEnoughMeasurementsAfterRejection => {
577                "Not enough measurements after rejection"
578            }
579            PvtError::PositionProhibited => "Position prohibited",
580            PvtError::NotEnoughDiffCorr => "Not enough differential corrections",
581            PvtError::BaseStationCoordinatesUnavailable => "Base station coordinates unavailable",
582            PvtError::Unknown(_) => "Unknown",
583        };
584        write!(f, "{}", s)
585    }
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    #[test]
593    fn test_satellite_id_from_svid() {
594        // GPS
595        assert_eq!(
596            SatelliteId::from_svid(1),
597            Some(SatelliteId::new(Constellation::GPS, 1))
598        );
599        assert_eq!(
600            SatelliteId::from_svid(32),
601            Some(SatelliteId::new(Constellation::GPS, 32))
602        );
603
604        // GLONASS
605        assert_eq!(
606            SatelliteId::from_svid(38),
607            Some(SatelliteId::new(Constellation::GLONASS, 1))
608        );
609        assert_eq!(
610            SatelliteId::from_svid(61),
611            Some(SatelliteId::new(Constellation::GLONASS, 24))
612        );
613
614        // Galileo
615        assert_eq!(
616            SatelliteId::from_svid(71),
617            Some(SatelliteId::new(Constellation::Galileo, 1))
618        );
619        assert_eq!(
620            SatelliteId::from_svid(106),
621            Some(SatelliteId::new(Constellation::Galileo, 36))
622        );
623
624        // BeiDou
625        assert_eq!(
626            SatelliteId::from_svid(141),
627            Some(SatelliteId::new(Constellation::BeiDou, 1))
628        );
629
630        // QZSS
631        assert_eq!(
632            SatelliteId::from_svid(181),
633            Some(SatelliteId::new(Constellation::QZSS, 1))
634        );
635
636        // NavIC/IRNSS (extended range)
637        assert_eq!(
638            SatelliteId::from_svid(216),
639            Some(SatelliteId::new(Constellation::NavIC, 8))
640        );
641        assert_eq!(
642            SatelliteId::from_svid(222),
643            Some(SatelliteId::new(Constellation::NavIC, 14))
644        );
645
646        // SBAS
647        assert_eq!(
648            SatelliteId::from_svid(120),
649            Some(SatelliteId::new(Constellation::SBAS, 20))
650        );
651
652        // Invalid
653        assert_eq!(SatelliteId::from_svid(0), None);
654    }
655
656    #[test]
657    fn test_satellite_id_key() {
658        let gps = SatelliteId::new(Constellation::GPS, 1);
659        assert_eq!(gps.key(), "G01");
660
661        let gal = SatelliteId::new(Constellation::Galileo, 12);
662        assert_eq!(gal.key(), "E12");
663
664        let glo = SatelliteId::new(Constellation::GLONASS, 5);
665        assert_eq!(glo.key(), "R05");
666    }
667
668    #[test]
669    fn test_satellite_id_to_svid_navic_ranges() {
670        let navic_legacy = SatelliteId::new(Constellation::NavIC, 7);
671        assert_eq!(navic_legacy.to_svid(), 197);
672
673        let navic_extended = SatelliteId::new(Constellation::NavIC, 8);
674        assert_eq!(navic_extended.to_svid(), 216);
675    }
676
677    #[test]
678    fn test_signal_type() {
679        assert_eq!(SignalType::from_signal_number(0), SignalType::L1CA);
680        assert_eq!(SignalType::from_signal_number(17), SignalType::E1);
681        assert_eq!(SignalType::from_signal_number(28), SignalType::B1I);
682        assert_eq!(SignalType::from_signal_number(23), SignalType::LBand);
683        assert_eq!(SignalType::from_signal_number(38), SignalType::QZSSL1CB);
684        assert_eq!(SignalType::from_signal_number(39), SignalType::QZSSL5S);
685    }
686
687    #[test]
688    fn test_pvt_mode() {
689        assert!(!PvtMode::NoFix.has_fix());
690        assert!(PvtMode::StandAlone.has_fix());
691        assert!(PvtMode::RtkFixed.is_rtk());
692        assert!(PvtMode::Ppp.is_ppp());
693    }
694}