gnss_rs/
sv.rs

1//! Space vehicles
2use crate::constellation::Constellation;
3use hifitime::{Epoch, TimeScale};
4use thiserror::Error;
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9/// ̀SV describes a Satellite Vehicle
10#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
11#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
12pub struct SV {
13    /// PRN identification # for this vehicle
14    pub prn: u8,
15    /// `GNSS` Constellation to which this vehicle belongs to
16    pub constellation: Constellation,
17}
18
19/*
20 * Database, built by build.rs, for detailed SBAS vehicle identification
21 */
22include!(concat!(env!("OUT_DIR"), "/sbas.rs"));
23
24/// ̀Parsing & identification related errors
25#[derive(Error, Debug, Clone, PartialEq)]
26pub enum ParsingError {
27    #[error("constellation parsing error")]
28    ConstellationParsing(#[from] crate::constellation::ParsingError),
29    #[error("sv prn# parsing error")]
30    PRNParsing(#[from] std::num::ParseIntError),
31}
32
33impl SV {
34    /// Builds a new SV
35    /// ```
36    /// extern crate gnss_rs as gnss;
37    ///
38    /// use gnss::sv;
39    /// use gnss::prelude::*;
40    /// use std::str::FromStr;
41    /// use hifitime::{TimeScale, Epoch};
42    ///
43    /// let sv = SV::new(Constellation::GPS, 1);
44    /// assert_eq!(sv.constellation, Constellation::GPS);
45    /// assert_eq!(sv.prn, 1);
46    /// assert_eq!(sv, sv!("G01"));
47    /// assert_eq!(sv.launched_date(), None);
48    ///
49    /// let launched_date = Epoch::from_str("2021-11-01T00:00:00 UTC")
50    ///     .unwrap();
51    /// assert_eq!(
52    ///     sv!("S23").launched_date(),
53    ///     Some(launched_date));
54    /// ```
55    pub const fn new(constellation: Constellation, prn: u8) -> Self {
56        Self { prn, constellation }
57    }
58    /// Returns the Timescale to which this SV belongs to.
59    /// ```
60    /// extern crate gnss_rs as gnss;
61    ///
62    /// use hifitime::TimeScale;
63    /// use gnss::sv;
64    /// use gnss::prelude::*;
65    /// use std::str::FromStr;
66    ///
67    /// assert_eq!(sv!("G01").timescale(), Some(TimeScale::GPST));
68    /// assert_eq!(sv!("E13").timescale(), Some(TimeScale::GST));
69    /// ```
70    pub fn timescale(&self) -> Option<TimeScale> {
71        self.constellation.timescale()
72    }
73    /*
74     * Tries to retrieve SBAS detailed definitions for self.
75     * For that, we use the PRN number, add +100 (SBAS def.) and use it
76     * as an identifier
77     */
78    pub(crate) fn sbas_definitions(prn: u8) -> Option<&'static SBASHelper<'static>> {
79        let to_find = (prn as u16) + 100;
80        SBAS_VEHICLES
81            .iter()
82            .filter(|e| e.prn == to_find)
83            .reduce(|e, _| e)
84    }
85    /// Returns datetime at which Self was either launched or its serviced was deployed.
86    /// This only applies to SBAS vehicles. Datetime expressed as [Epoch] at midnight UTC.
87    pub fn launched_date(&self) -> Option<Epoch> {
88        let definition = SV::sbas_definitions(self.prn)?;
89        Some(Epoch::from_gregorian_utc_at_midnight(
90            definition.launched_year,
91            definition.launched_month,
92            definition.launched_day,
93        ))
94    }
95    /// Returns True if Self is a BeiDou geostationnary vehicle
96    pub fn is_beidou_geo(&self) -> bool {
97        self.constellation == Constellation::BeiDou && (self.prn < 6 || self.prn > 58)
98    }
99}
100
101impl std::str::FromStr for SV {
102    type Err = ParsingError;
103    /*
104     * Parse SV from "XYY" standardized format.
105     * On "sbas" crate feature, we have the ability to identify
106     * vehicles in detail. For example S23 is Eutelsat 5WB.
107     */
108    fn from_str(string: &str) -> Result<Self, Self::Err> {
109        let constellation = Constellation::from_str(&string[0..1])?;
110        let prn = string[1..].trim().parse::<u8>()?;
111        let mut ret = SV::new(constellation, prn);
112        if constellation.is_sbas() {
113            // map the SXX to meaningful SBAS
114            if let Some(sbas) = SV::sbas_definitions(prn) {
115                // this can't fail because the SBAS database only
116                // contains valid Constellations
117                ret.constellation = Constellation::from_str(sbas.constellation).unwrap();
118            }
119        }
120        Ok(ret)
121    }
122}
123
124impl std::fmt::UpperHex for SV {
125    /*
126     * Possibly detailed identity for SBAS vehicles
127     */
128    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
129        if self.constellation.is_sbas() {
130            if let Some(sbas) = SV::sbas_definitions(self.prn) {
131                write!(f, "{}", sbas.id)
132            } else {
133                write!(f, "{:x}", self)
134            }
135        } else {
136            write!(f, "{:x}", self)
137        }
138    }
139}
140
141impl std::fmt::LowerHex for SV {
142    /*
143     * Prints self as XYY standard format
144     */
145    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
146        write!(f, "{:x}{:02}", self.constellation, self.prn)
147    }
148}
149
150impl std::fmt::Display for SV {
151    /*
152     * Prints self as XYY standard format
153     */
154    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
155        write!(f, "{:x}{:02}", self.constellation, self.prn)
156    }
157}
158
159#[cfg(test)]
160mod test {
161    use super::*;
162    use std::str::FromStr;
163    #[test]
164    fn from_str() {
165        for (descriptor, expected) in vec![
166            ("G01", SV::new(Constellation::GPS, 1)),
167            ("G 1", SV::new(Constellation::GPS, 1)),
168            ("G33", SV::new(Constellation::GPS, 33)),
169            ("C01", SV::new(Constellation::BeiDou, 1)),
170            ("C 3", SV::new(Constellation::BeiDou, 3)),
171            ("R01", SV::new(Constellation::Glonass, 1)),
172            ("R 1", SV::new(Constellation::Glonass, 1)),
173            ("C254", SV::new(Constellation::BeiDou, 254)),
174            ("E4 ", SV::new(Constellation::Galileo, 4)),
175            ("R 9", SV::new(Constellation::Glonass, 9)),
176            ("I 3", SV::new(Constellation::IRNSS, 3)),
177            ("I09", SV::new(Constellation::IRNSS, 9)),
178            ("I16", SV::new(Constellation::IRNSS, 16)),
179        ] {
180            let sv = SV::from_str(descriptor);
181            assert!(
182                sv.is_ok(),
183                "failed to parse sv from \"{}\" - {:?}",
184                descriptor,
185                sv.err().unwrap()
186            );
187            let sv = sv.unwrap();
188            assert_eq!(
189                sv, expected,
190                "badly identified {} from \"{}\"",
191                sv, descriptor
192            );
193        }
194    }
195    #[test]
196    fn sbas_from_str() {
197        for (desc, parsed, lowerhex, upperhex) in vec![
198            ("S 3", SV::new(Constellation::SBAS, 3), "S03", "S03"),
199            (
200                "S22",
201                SV::new(Constellation::AusNZ, 22),
202                "S22",
203                "INMARSAT-4F1",
204            ),
205            ("S23", SV::new(Constellation::EGNOS, 23), "S23", "ASTRA-5B"),
206            ("S25", SV::new(Constellation::SDCM, 25), "S25", "Luch-5A"),
207            ("S 5", SV::new(Constellation::SBAS, 5), "S05", "S05"),
208            ("S48", SV::new(Constellation::ASAL, 48), "S48", "ALCOMSAT-1"),
209        ] {
210            let sv = SV::from_str(desc).unwrap();
211            assert_eq!(sv, parsed, "failed to parse correct sv from \"{}\"", desc);
212            assert_eq!(format!("{:x}", sv), lowerhex);
213            assert_eq!(format!("{:X}", sv), upperhex);
214            assert!(sv.constellation.is_sbas(), "should be sbas");
215        }
216    }
217    #[test]
218    fn sbas_db_sanity() {
219        for sbas in SBAS_VEHICLES.iter() {
220            /* verify PRN */
221            assert!(sbas.prn > 100);
222
223            /* verify constellation */
224            let constellation = Constellation::from_str(sbas.constellation);
225            assert!(
226                constellation.is_ok(),
227                "sbas database should only contain valid constellations: \"{}\"",
228                sbas.constellation,
229            );
230
231            let constellation = constellation.unwrap();
232            assert_eq!(constellation.timescale(), Some(TimeScale::GPST));
233
234            /* verify launch date */
235            let _ = Epoch::from_gregorian_utc_at_midnight(
236                sbas.launched_year,
237                sbas.launched_month,
238                sbas.launched_day,
239            );
240        }
241    }
242    #[test]
243    fn test_beidou_geo() {
244        assert_eq!(SV::from_str("G01").unwrap().is_beidou_geo(), false);
245        assert_eq!(SV::from_str("E01").unwrap().is_beidou_geo(), false);
246        assert_eq!(SV::from_str("C01").unwrap().is_beidou_geo(), true);
247        assert_eq!(SV::from_str("C02").unwrap().is_beidou_geo(), true);
248        assert_eq!(SV::from_str("C06").unwrap().is_beidou_geo(), false);
249        assert_eq!(SV::from_str("C48").unwrap().is_beidou_geo(), false);
250        assert_eq!(SV::from_str("C59").unwrap().is_beidou_geo(), true);
251        assert_eq!(SV::from_str("C60").unwrap().is_beidou_geo(), true);
252    }
253}