1use hifitime::{Epoch, TimeScale};
3use thiserror::Error;
4
5#[cfg(feature = "serde")]
6use serde::{Deserialize, Serialize};
7
8use crate::constellation::{Constellation, ParsingError as ConstellationParsingError};
9
10use std::num::ParseIntError;
11
12use std::str::FromStr;
13
14#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
16#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
17pub struct SV {
18 pub prn: u8,
20 pub constellation: Constellation,
22}
23
24include!(concat!(env!("OUT_DIR"), "/sbas.rs"));
28
29#[derive(Error, Debug, Clone, PartialEq)]
31pub enum ParsingError {
32 #[error("constellation parsing error: {0}")]
33 ConstellationParsing(#[from] ConstellationParsingError),
34
35 #[error("satellite number parsing error: {0}")]
36 PRNParsing(#[from] ParseIntError),
37}
38
39impl SV {
40 pub const fn new(constellation: Constellation, prn: u8) -> Self {
58 Self { prn, constellation }
59 }
60
61 pub fn new_sbas(prn: u8) -> Option<Self> {
89 let definition = Self::sbas_definitions(prn)?;
90
91 if let Ok(constellation) = Constellation::from_str(definition.constellation) {
92 Some(Self { prn, constellation })
93 } else {
94 None
95 }
96 }
97
98 pub fn timescale(&self) -> Option<TimeScale> {
111 self.constellation.timescale()
112 }
113
114 pub(crate) fn sbas_definitions(prn: u8) -> Option<&'static SBASHelper<'static>> {
117 let to_find = (prn as u16) + 100;
118 SBAS_VEHICLES
119 .iter()
120 .filter(|e| e.prn == to_find)
121 .reduce(|e, _| e)
122 }
123
124 pub fn launch_date(&self) -> Option<Epoch> {
127 let definition = SV::sbas_definitions(self.prn)?;
128
129 Some(Epoch::from_gregorian_utc_at_midnight(
130 definition.launch_year,
131 definition.launch_month,
132 definition.launch_day,
133 ))
134 }
135
136 pub fn is_beidou_geo(&self) -> bool {
138 self.constellation == Constellation::BeiDou && (self.prn < 6 || self.prn > 58)
139 }
140}
141
142impl std::str::FromStr for SV {
143 type Err = ParsingError;
144 fn from_str(string: &str) -> Result<Self, Self::Err> {
150 let constellation = Constellation::from_str(&string[0..1])?;
151 let prn = string[1..].trim().parse::<u8>()?;
152 let mut ret = SV::new(constellation, prn);
153 if constellation.is_sbas() {
154 if let Some(sbas) = SV::sbas_definitions(prn) {
156 ret.constellation = Constellation::from_str(sbas.constellation).unwrap();
159 }
160 }
161 Ok(ret)
162 }
163}
164
165impl std::fmt::UpperHex for SV {
166 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
170 if self.constellation.is_sbas() {
171 if let Some(sbas) = SV::sbas_definitions(self.prn) {
172 write!(f, "{}", sbas.id)
173 } else {
174 write!(f, "{:x}", self)
175 }
176 } else {
177 write!(f, "{:x}", self)
178 }
179 }
180}
181
182impl std::fmt::LowerHex for SV {
183 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
187 write!(f, "{:x}{:02}", self.constellation, self.prn)
188 }
189}
190
191impl std::fmt::Display for SV {
192 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
196 write!(f, "{:x}{:02}", self.constellation, self.prn)
197 }
198}
199
200#[cfg(test)]
201mod test {
202 use super::*;
203 use std::str::FromStr;
204 #[test]
205 fn from_str() {
206 for (descriptor, expected) in vec![
207 ("G01", SV::new(Constellation::GPS, 1)),
208 ("G 1", SV::new(Constellation::GPS, 1)),
209 ("G33", SV::new(Constellation::GPS, 33)),
210 ("C01", SV::new(Constellation::BeiDou, 1)),
211 ("C 3", SV::new(Constellation::BeiDou, 3)),
212 ("R01", SV::new(Constellation::Glonass, 1)),
213 ("R 1", SV::new(Constellation::Glonass, 1)),
214 ("C254", SV::new(Constellation::BeiDou, 254)),
215 ("E4 ", SV::new(Constellation::Galileo, 4)),
216 ("R 9", SV::new(Constellation::Glonass, 9)),
217 ("I 3", SV::new(Constellation::IRNSS, 3)),
218 ("I09", SV::new(Constellation::IRNSS, 9)),
219 ("I16", SV::new(Constellation::IRNSS, 16)),
220 ] {
221 let sv = SV::from_str(descriptor);
222 assert!(
223 sv.is_ok(),
224 "failed to parse sv from \"{}\" - {:?}",
225 descriptor,
226 sv.err().unwrap()
227 );
228 let sv = sv.unwrap();
229 assert_eq!(
230 sv, expected,
231 "badly identified {} from \"{}\"",
232 sv, descriptor
233 );
234 }
235 }
236 #[test]
237 fn sbas_from_str() {
238 for (desc, parsed, lowerhex, upperhex) in vec![
239 ("S 3", SV::new(Constellation::SBAS, 3), "S03", "S03"),
240 (
241 "S22",
242 SV::new(Constellation::AusNZ, 22),
243 "S22",
244 "INMARSAT-4F1",
245 ),
246 ("S23", SV::new(Constellation::EGNOS, 23), "S23", "ASTRA-5B"),
247 ("S25", SV::new(Constellation::SDCM, 25), "S25", "Luch-5A"),
248 ("S 5", SV::new(Constellation::SBAS, 5), "S05", "S05"),
249 ("S48", SV::new(Constellation::ASAL, 48), "S48", "ALCOMSAT-1"),
250 ] {
251 let sv = SV::from_str(desc).unwrap();
252 assert_eq!(sv, parsed, "failed to parse correct sv from \"{}\"", desc);
253 assert_eq!(format!("{:x}", sv), lowerhex);
254 assert_eq!(format!("{:X}", sv), upperhex);
255 assert!(sv.constellation.is_sbas(), "should be sbas");
256 }
257 }
258 #[test]
259 fn sbas_db_sanity() {
260 for sbas in SBAS_VEHICLES.iter() {
261 assert!(sbas.prn > 100);
263
264 let constellation = Constellation::from_str(sbas.constellation);
266 assert!(
267 constellation.is_ok(),
268 "sbas database should only contain valid constellations: \"{}\"",
269 sbas.constellation,
270 );
271
272 let constellation = constellation.unwrap();
273 assert_eq!(constellation.timescale(), Some(TimeScale::GPST));
274
275 let _ = Epoch::from_gregorian_utc_at_midnight(
277 sbas.launch_year,
278 sbas.launch_month,
279 sbas.launch_day,
280 );
281 }
282 }
283 #[test]
284 fn test_beidou_geo() {
285 assert_eq!(SV::from_str("G01").unwrap().is_beidou_geo(), false);
286 assert_eq!(SV::from_str("E01").unwrap().is_beidou_geo(), false);
287 assert_eq!(SV::from_str("C01").unwrap().is_beidou_geo(), true);
288 assert_eq!(SV::from_str("C02").unwrap().is_beidou_geo(), true);
289 assert_eq!(SV::from_str("C06").unwrap().is_beidou_geo(), false);
290 assert_eq!(SV::from_str("C48").unwrap().is_beidou_geo(), false);
291 assert_eq!(SV::from_str("C59").unwrap().is_beidou_geo(), true);
292 assert_eq!(SV::from_str("C60").unwrap().is_beidou_geo(), true);
293 }
294}