1use core::fmt;
8
9use crate::constants::{C_M_S, F_B1I_HZ, F_B3I_HZ, F_E1_HZ, F_E5A_HZ, F_L1_HZ, F_L2_HZ};
10use crate::validate;
11use crate::GnssSystem;
12
13const F_E6_HZ: f64 = 1_278_750_000.0;
14const F_E5B_HZ: f64 = 1_207_140_000.0;
15const F_E5_HZ: f64 = 1_191_795_000.0;
16const F_GLONASS_G1_BASE_HZ: f64 = 1_602_000_000.0;
17const F_GLONASS_G1_STEP_HZ: f64 = 562_500.0;
18const F_GLONASS_G2_BASE_HZ: f64 = 1_246_000_000.0;
19const F_GLONASS_G2_STEP_HZ: f64 = 437_500.0;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
23pub enum CarrierBand {
24 L1,
26 L2,
28 L5,
30 E1,
32 E5a,
34 E5b,
36 E5,
38 E6,
40 B1c,
42 B1i,
44 B2a,
46 B2b,
48 B2,
50 B3i,
52 G1,
54 G2,
56}
57
58impl CarrierBand {
59 pub fn from_name(name: &str) -> Option<Self> {
61 match name {
62 "l1" => Some(Self::L1),
63 "l2" => Some(Self::L2),
64 "l5" => Some(Self::L5),
65 "e1" => Some(Self::E1),
66 "e5a" => Some(Self::E5a),
67 "e5b" => Some(Self::E5b),
68 "e5" => Some(Self::E5),
69 "e6" => Some(Self::E6),
70 "b1c" => Some(Self::B1c),
71 "b1i" => Some(Self::B1i),
72 "b2a" => Some(Self::B2a),
73 "b2b" => Some(Self::B2b),
74 "b2" => Some(Self::B2),
75 "b3i" => Some(Self::B3i),
76 "g1" => Some(Self::G1),
77 "g2" => Some(Self::G2),
78 _ => None,
79 }
80 }
81
82 pub fn from_iono_free_name(name: &str) -> Option<Self> {
84 match name {
85 "l1" => Some(Self::L1),
86 "l2" => Some(Self::L2),
87 "e1" => Some(Self::E1),
88 "e5a" => Some(Self::E5a),
89 "b1i" => Some(Self::B1i),
90 "b3i" => Some(Self::B3i),
91 _ => None,
92 }
93 }
94
95 pub const fn as_str(&self) -> &'static str {
97 match *self {
98 Self::L1 => "l1",
99 Self::L2 => "l2",
100 Self::L5 => "l5",
101 Self::E1 => "e1",
102 Self::E5a => "e5a",
103 Self::E5b => "e5b",
104 Self::E5 => "e5",
105 Self::E6 => "e6",
106 Self::B1c => "b1c",
107 Self::B1i => "b1i",
108 Self::B2a => "b2a",
109 Self::B2b => "b2b",
110 Self::B2 => "b2",
111 Self::B3i => "b3i",
112 Self::G1 => "g1",
113 Self::G2 => "g2",
114 }
115 }
116
117 pub const fn name(self) -> &'static str {
119 self.as_str()
120 }
121}
122
123impl fmt::Display for CarrierBand {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.write_str(self.as_str())
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub struct CarrierPair {
132 pub band1: CarrierBand,
134 pub band2: CarrierBand,
136}
137
138impl CarrierPair {
139 pub const fn new(band1: CarrierBand, band2: CarrierBand) -> Self {
141 Self { band1, band2 }
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq)]
147pub struct CarrierFrequency {
148 pub system: GnssSystem,
150 pub band: CarrierBand,
152 pub frequency_hz: f64,
154}
155
156pub const fn fixed_carrier_frequencies() -> [CarrierFrequency; 17] {
159 [
160 CarrierFrequency {
161 system: GnssSystem::Gps,
162 band: CarrierBand::L1,
163 frequency_hz: F_L1_HZ,
164 },
165 CarrierFrequency {
166 system: GnssSystem::Gps,
167 band: CarrierBand::L2,
168 frequency_hz: F_L2_HZ,
169 },
170 CarrierFrequency {
171 system: GnssSystem::Gps,
172 band: CarrierBand::L5,
173 frequency_hz: F_E5A_HZ,
174 },
175 CarrierFrequency {
176 system: GnssSystem::Qzss,
177 band: CarrierBand::L1,
178 frequency_hz: F_L1_HZ,
179 },
180 CarrierFrequency {
181 system: GnssSystem::Qzss,
182 band: CarrierBand::L2,
183 frequency_hz: F_L2_HZ,
184 },
185 CarrierFrequency {
186 system: GnssSystem::Qzss,
187 band: CarrierBand::L5,
188 frequency_hz: F_E5A_HZ,
189 },
190 CarrierFrequency {
191 system: GnssSystem::Galileo,
192 band: CarrierBand::E1,
193 frequency_hz: F_E1_HZ,
194 },
195 CarrierFrequency {
196 system: GnssSystem::Galileo,
197 band: CarrierBand::E5a,
198 frequency_hz: F_E5A_HZ,
199 },
200 CarrierFrequency {
201 system: GnssSystem::Galileo,
202 band: CarrierBand::E6,
203 frequency_hz: F_E6_HZ,
204 },
205 CarrierFrequency {
206 system: GnssSystem::Galileo,
207 band: CarrierBand::E5b,
208 frequency_hz: F_E5B_HZ,
209 },
210 CarrierFrequency {
211 system: GnssSystem::Galileo,
212 band: CarrierBand::E5,
213 frequency_hz: F_E5_HZ,
214 },
215 CarrierFrequency {
216 system: GnssSystem::BeiDou,
217 band: CarrierBand::B1c,
218 frequency_hz: F_L1_HZ,
219 },
220 CarrierFrequency {
221 system: GnssSystem::BeiDou,
222 band: CarrierBand::B1i,
223 frequency_hz: F_B1I_HZ,
224 },
225 CarrierFrequency {
226 system: GnssSystem::BeiDou,
227 band: CarrierBand::B2a,
228 frequency_hz: F_E5A_HZ,
229 },
230 CarrierFrequency {
231 system: GnssSystem::BeiDou,
232 band: CarrierBand::B3i,
233 frequency_hz: F_B3I_HZ,
234 },
235 CarrierFrequency {
236 system: GnssSystem::BeiDou,
237 band: CarrierBand::B2b,
238 frequency_hz: F_E5B_HZ,
239 },
240 CarrierFrequency {
241 system: GnssSystem::BeiDou,
242 band: CarrierBand::B2,
243 frequency_hz: F_E5_HZ,
244 },
245 ]
246}
247
248pub const fn iono_free_carrier_frequencies() -> [CarrierFrequency; 6] {
250 [
251 CarrierFrequency {
252 system: GnssSystem::Gps,
253 band: CarrierBand::L1,
254 frequency_hz: F_L1_HZ,
255 },
256 CarrierFrequency {
257 system: GnssSystem::Gps,
258 band: CarrierBand::L2,
259 frequency_hz: F_L2_HZ,
260 },
261 CarrierFrequency {
262 system: GnssSystem::Galileo,
263 band: CarrierBand::E1,
264 frequency_hz: F_E1_HZ,
265 },
266 CarrierFrequency {
267 system: GnssSystem::Galileo,
268 band: CarrierBand::E5a,
269 frequency_hz: F_E5A_HZ,
270 },
271 CarrierFrequency {
272 system: GnssSystem::BeiDou,
273 band: CarrierBand::B1i,
274 frequency_hz: F_B1I_HZ,
275 },
276 CarrierFrequency {
277 system: GnssSystem::BeiDou,
278 band: CarrierBand::B3i,
279 frequency_hz: F_B3I_HZ,
280 },
281 ]
282}
283
284pub const fn frequency_hz(system: GnssSystem, band: CarrierBand) -> Option<f64> {
286 match (system, band) {
287 (GnssSystem::Gps, CarrierBand::L1) => Some(F_L1_HZ),
288 (GnssSystem::Gps, CarrierBand::L2) => Some(F_L2_HZ),
289 (GnssSystem::Gps, CarrierBand::L5) => Some(F_E5A_HZ),
290 (GnssSystem::Qzss, CarrierBand::L1) => Some(F_L1_HZ),
291 (GnssSystem::Qzss, CarrierBand::L2) => Some(F_L2_HZ),
292 (GnssSystem::Qzss, CarrierBand::L5) => Some(F_E5A_HZ),
293 (GnssSystem::Galileo, CarrierBand::E1) => Some(F_E1_HZ),
294 (GnssSystem::Galileo, CarrierBand::E5a) => Some(F_E5A_HZ),
295 (GnssSystem::Galileo, CarrierBand::E6) => Some(F_E6_HZ),
296 (GnssSystem::Galileo, CarrierBand::E5b) => Some(F_E5B_HZ),
297 (GnssSystem::Galileo, CarrierBand::E5) => Some(F_E5_HZ),
298 (GnssSystem::BeiDou, CarrierBand::B1c) => Some(F_L1_HZ),
299 (GnssSystem::BeiDou, CarrierBand::B1i) => Some(F_B1I_HZ),
300 (GnssSystem::BeiDou, CarrierBand::B2a) => Some(F_E5A_HZ),
301 (GnssSystem::BeiDou, CarrierBand::B3i) => Some(F_B3I_HZ),
302 (GnssSystem::BeiDou, CarrierBand::B2b) => Some(F_E5B_HZ),
303 (GnssSystem::BeiDou, CarrierBand::B2) => Some(F_E5_HZ),
304 _ => None,
305 }
306}
307
308pub fn wavelength_m(system: GnssSystem, band: CarrierBand) -> Option<f64> {
310 frequency_hz(system, band).and_then(wavelength_for_frequency)
311}
312
313pub fn rinex_band_frequency_hz(
318 system: GnssSystem,
319 band: char,
320 glonass_channel: Option<i8>,
321) -> Option<f64> {
322 rinex_signal_frequency_hz(system, band, None, None, glonass_channel)
323}
324
325pub fn rinex_band_frequency_hz_classified(
330 system: GnssSystem,
331 band: char,
332 glonass_channel: Option<i8>,
333) -> crate::Result<Option<f64>> {
334 if matches!(
335 (system, band, glonass_channel),
336 (GnssSystem::Glonass, '1' | '2', None)
337 ) {
338 return Err(crate::Error::MissingGlonassChannel);
339 }
340 Ok(rinex_band_frequency_hz(system, band, glonass_channel))
341}
342
343pub fn rinex_observation_frequency_hz(
350 system: GnssSystem,
351 code: &str,
352 rinex_version: f64,
353 glonass_channel: Option<i8>,
354) -> Option<f64> {
355 let mut chars = code.chars();
356 let _kind = chars.next()?;
357 let band = chars.next()?;
358 let tracking = chars.next();
359 rinex_signal_frequency_hz(system, band, tracking, Some(rinex_version), glonass_channel)
360}
361
362fn rinex_signal_frequency_hz(
363 system: GnssSystem,
364 band: char,
365 tracking: Option<char>,
366 rinex_version: Option<f64>,
367 glonass_channel: Option<i8>,
368) -> Option<f64> {
369 let frequency_hz = match (system, band, glonass_channel) {
370 (GnssSystem::Gps, '1', _) => frequency_hz(system, CarrierBand::L1),
371 (GnssSystem::Gps, '2', _) => frequency_hz(system, CarrierBand::L2),
372 (GnssSystem::Gps, '5', _) => frequency_hz(system, CarrierBand::L5),
373 (GnssSystem::Qzss, '1', _) => frequency_hz(system, CarrierBand::L1),
374 (GnssSystem::Qzss, '2', _) => frequency_hz(system, CarrierBand::L2),
375 (GnssSystem::Qzss, '5', _) => frequency_hz(system, CarrierBand::L5),
376 (GnssSystem::Galileo, '1', _) => frequency_hz(system, CarrierBand::E1),
377 (GnssSystem::Galileo, '5', _) => frequency_hz(system, CarrierBand::E5a),
378 (GnssSystem::Galileo, '6', _) => frequency_hz(system, CarrierBand::E6),
379 (GnssSystem::Galileo, '7', _) => frequency_hz(system, CarrierBand::E5b),
380 (GnssSystem::Galileo, '8', _) => frequency_hz(system, CarrierBand::E5),
381 (GnssSystem::BeiDou, _, _) => {
382 frequency_hz(system, rinex_beidou_band(band, tracking, rinex_version)?)
383 }
384 (GnssSystem::Glonass, '1', Some(channel)) => {
385 Some(F_GLONASS_G1_BASE_HZ + f64::from(channel) * F_GLONASS_G1_STEP_HZ)
386 }
387 (GnssSystem::Glonass, '2', Some(channel)) => {
388 Some(F_GLONASS_G2_BASE_HZ + f64::from(channel) * F_GLONASS_G2_STEP_HZ)
389 }
390 _ => None,
391 }?;
392 valid_frequency_hz(frequency_hz)
393}
394
395fn rinex_beidou_band(
396 band: char,
397 tracking: Option<char>,
398 rinex_version: Option<f64>,
399) -> Option<CarrierBand> {
400 match band {
401 '1' if tracking == Some('I') && rinex_version.is_some_and(is_rinex_302) => {
402 Some(CarrierBand::B1i)
403 }
404 '1' => Some(CarrierBand::B1c),
405 '2' => Some(CarrierBand::B1i),
406 '5' => Some(CarrierBand::B2a),
407 '6' => Some(CarrierBand::B3i),
408 '7' => Some(CarrierBand::B2b),
409 '8' => Some(CarrierBand::B2),
410 _ => None,
411 }
412}
413
414fn is_rinex_302(version: f64) -> bool {
415 (3.015..3.025).contains(&version)
416}
417
418pub fn rinex_band_wavelength_m(
420 system: GnssSystem,
421 band: char,
422 glonass_channel: Option<i8>,
423) -> Option<f64> {
424 rinex_band_frequency_hz(system, band, glonass_channel).and_then(wavelength_for_frequency)
425}
426
427pub fn rinex_observation_wavelength_m(
429 system: GnssSystem,
430 code: &str,
431 rinex_version: f64,
432 glonass_channel: Option<i8>,
433) -> Option<f64> {
434 rinex_observation_frequency_hz(system, code, rinex_version, glonass_channel)
435 .and_then(wavelength_for_frequency)
436}
437
438fn valid_frequency_hz(frequency_hz: f64) -> Option<f64> {
439 validate::finite_positive(frequency_hz, "frequency_hz").ok()
440}
441
442pub(crate) fn wavelength_for_frequency(frequency_hz: f64) -> Option<f64> {
443 valid_frequency_hz(frequency_hz).map(|frequency_hz| C_M_S / frequency_hz)
444}
445
446pub const fn default_iono_free_pair(system: GnssSystem) -> Option<CarrierPair> {
448 match system {
449 GnssSystem::Gps => Some(CarrierPair::new(CarrierBand::L1, CarrierBand::L2)),
450 GnssSystem::Galileo => Some(CarrierPair::new(CarrierBand::E1, CarrierBand::E5a)),
451 GnssSystem::BeiDou => Some(CarrierPair::new(CarrierBand::B1i, CarrierBand::B3i)),
452 _ => None,
453 }
454}
455
456pub const fn glonass_g1_frequency_hz(channel: i8) -> f64 {
466 F_GLONASS_G1_BASE_HZ + (channel as f64) * F_GLONASS_G1_STEP_HZ
467}
468
469pub const fn default_spp_carrier(system: GnssSystem) -> Option<CarrierBand> {
471 match system {
472 GnssSystem::Gps => Some(CarrierBand::L1),
473 GnssSystem::Galileo => Some(CarrierBand::E1),
474 GnssSystem::BeiDou => Some(CarrierBand::B1i),
475 _ => None,
476 }
477}
478
479pub const fn default_spp_frequency_hz(system: GnssSystem) -> Option<f64> {
481 match default_spp_carrier(system) {
482 Some(band) => frequency_hz(system, band),
483 None => None,
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn iono_free_table_matches_supported_pairs() {
493 assert_eq!(
494 default_iono_free_pair(GnssSystem::Gps),
495 Some(CarrierPair::new(CarrierBand::L1, CarrierBand::L2))
496 );
497 assert_eq!(
498 default_iono_free_pair(GnssSystem::Galileo),
499 Some(CarrierPair::new(CarrierBand::E1, CarrierBand::E5a))
500 );
501 assert_eq!(
502 default_iono_free_pair(GnssSystem::BeiDou),
503 Some(CarrierPair::new(CarrierBand::B1i, CarrierBand::B3i))
504 );
505 assert_eq!(default_iono_free_pair(GnssSystem::Glonass), None);
506 assert_eq!(iono_free_carrier_frequencies().len(), 6);
507 }
508
509 #[test]
510 fn carrier_band_labels_are_canonical_lowercase() {
511 let cases = [
512 (CarrierBand::L1, "l1"),
513 (CarrierBand::L2, "l2"),
514 (CarrierBand::L5, "l5"),
515 (CarrierBand::E1, "e1"),
516 (CarrierBand::E5a, "e5a"),
517 (CarrierBand::E5b, "e5b"),
518 (CarrierBand::E5, "e5"),
519 (CarrierBand::E6, "e6"),
520 (CarrierBand::B1c, "b1c"),
521 (CarrierBand::B1i, "b1i"),
522 (CarrierBand::B2a, "b2a"),
523 (CarrierBand::B2b, "b2b"),
524 (CarrierBand::B2, "b2"),
525 (CarrierBand::B3i, "b3i"),
526 (CarrierBand::G1, "g1"),
527 (CarrierBand::G2, "g2"),
528 ];
529 for (band, label) in cases {
530 assert_eq!(band.as_str(), label);
531 assert_eq!(band.name(), label);
532 assert_eq!(band.to_string(), label);
533 assert_eq!(CarrierBand::from_name(label), Some(band));
534 }
535 }
536
537 #[test]
538 fn rinex_band_table_matches_existing_frequency_bits() {
539 let cases: [(GnssSystem, char, Option<i8>, f64); 16] = [
540 (GnssSystem::Gps, '1', None, 1_575_420_000.0),
541 (GnssSystem::Gps, '2', None, 1_227_600_000.0),
542 (GnssSystem::Gps, '5', None, 1_176_450_000.0),
543 (GnssSystem::Qzss, '1', None, 1_575_420_000.0),
544 (GnssSystem::Qzss, '2', None, 1_227_600_000.0),
545 (GnssSystem::Qzss, '5', None, 1_176_450_000.0),
546 (GnssSystem::Galileo, '6', None, 1_278_750_000.0),
547 (GnssSystem::Galileo, '7', None, 1_207_140_000.0),
548 (GnssSystem::Galileo, '8', None, 1_191_795_000.0),
549 (GnssSystem::BeiDou, '1', None, 1_575_420_000.0),
550 (GnssSystem::BeiDou, '2', None, 1_561_098_000.0),
551 (GnssSystem::BeiDou, '6', None, 1_268_520_000.0),
552 (GnssSystem::BeiDou, '7', None, 1_207_140_000.0),
553 (GnssSystem::BeiDou, '8', None, 1_191_795_000.0),
554 (GnssSystem::Glonass, '1', Some(1), 1_602_562_500.0),
555 (GnssSystem::Glonass, '2', Some(1), 1_246_437_500.0),
556 ];
557 for (system, band, channel, expected) in cases {
558 assert_eq!(
559 rinex_band_frequency_hz(system, band, channel).map(f64::to_bits),
560 Some(expected.to_bits())
561 );
562 }
563 assert_eq!(
564 rinex_band_frequency_hz(GnssSystem::Glonass, '1', None),
565 None
566 );
567 }
568
569 #[test]
570 fn classified_rinex_band_lookup_distinguishes_missing_channels() {
571 for band in ['1', '2'] {
572 assert_eq!(
573 rinex_band_frequency_hz_classified(GnssSystem::Glonass, band, None),
574 Err(crate::Error::MissingGlonassChannel)
575 );
576 assert_eq!(
577 rinex_band_frequency_hz(GnssSystem::Glonass, band, None),
578 None
579 );
580 }
581 assert_eq!(
582 rinex_band_frequency_hz_classified(GnssSystem::Glonass, '3', None),
583 Ok(None)
584 );
585 }
586
587 #[test]
588 fn classified_rinex_band_lookup_preserves_frequency_bits() {
589 let cases: [(GnssSystem, char, Option<i8>, f64); 3] = [
590 (GnssSystem::Gps, '1', None, 1_575_420_000.0),
591 (GnssSystem::Glonass, '1', Some(-7), 1_598_062_500.0),
592 (GnssSystem::Glonass, '2', Some(6), 1_248_625_000.0),
593 ];
594 for (system, band, channel, expected) in cases {
595 assert_eq!(
596 rinex_band_frequency_hz_classified(system, band, channel)
597 .map(|frequency| frequency.map(f64::to_bits)),
598 Ok(Some(expected.to_bits()))
599 );
600 }
601 }
602
603 #[test]
604 fn rinex_observation_code_resolves_beidou_302_b1i() {
605 for code in ["C1I", "L1I"] {
606 assert_eq!(
607 rinex_observation_frequency_hz(GnssSystem::BeiDou, code, 3.02, None)
608 .map(f64::to_bits),
609 Some(F_B1I_HZ.to_bits())
610 );
611 }
612 assert_eq!(
613 rinex_observation_frequency_hz(GnssSystem::BeiDou, "L1X", 3.03, None).map(f64::to_bits),
614 Some(F_L1_HZ.to_bits())
615 );
616 }
617
618 #[test]
619 fn wavelength_helpers_use_c_over_frequency() {
620 let wavelength = wavelength_m(GnssSystem::Gps, CarrierBand::L1).unwrap();
621 assert_eq!(wavelength.to_bits(), (C_M_S / F_L1_HZ).to_bits());
622
623 let glonass_wavelength = rinex_band_wavelength_m(GnssSystem::Glonass, '1', Some(-7))
624 .expect("GLONASS G1 channel");
625 let frequency = 1_602_000_000.0 + f64::from(-7) * 562_500.0;
626 assert_eq!(glonass_wavelength.to_bits(), (C_M_S / frequency).to_bits());
627 }
628
629 #[test]
630 fn glonass_g1_spp_carrier_matches_rinex_band_table() {
631 for channel in [-7_i8, -1, 0, 1, 6] {
632 assert_eq!(
633 glonass_g1_frequency_hz(channel).to_bits(),
634 rinex_band_frequency_hz(GnssSystem::Glonass, '1', Some(channel))
635 .expect("GLONASS G1 channel frequency")
636 .to_bits()
637 );
638 }
639 }
640
641 #[test]
642 fn frequency_validation_rejects_invalid_runtime_values() {
643 assert_eq!(valid_frequency_hz(f64::NAN), None);
644 assert_eq!(valid_frequency_hz(f64::INFINITY), None);
645 assert_eq!(valid_frequency_hz(0.0), None);
646 assert_eq!(valid_frequency_hz(-1.0), None);
647 assert_eq!(wavelength_for_frequency(f64::NAN), None);
648 }
649}