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_observation_frequency_hz(
332 system: GnssSystem,
333 code: &str,
334 rinex_version: f64,
335 glonass_channel: Option<i8>,
336) -> Option<f64> {
337 let mut chars = code.chars();
338 let _kind = chars.next()?;
339 let band = chars.next()?;
340 let tracking = chars.next();
341 rinex_signal_frequency_hz(system, band, tracking, Some(rinex_version), glonass_channel)
342}
343
344fn rinex_signal_frequency_hz(
345 system: GnssSystem,
346 band: char,
347 tracking: Option<char>,
348 rinex_version: Option<f64>,
349 glonass_channel: Option<i8>,
350) -> Option<f64> {
351 let frequency_hz = match (system, band, glonass_channel) {
352 (GnssSystem::Gps, '1', _) => frequency_hz(system, CarrierBand::L1),
353 (GnssSystem::Gps, '2', _) => frequency_hz(system, CarrierBand::L2),
354 (GnssSystem::Gps, '5', _) => frequency_hz(system, CarrierBand::L5),
355 (GnssSystem::Qzss, '1', _) => frequency_hz(system, CarrierBand::L1),
356 (GnssSystem::Qzss, '2', _) => frequency_hz(system, CarrierBand::L2),
357 (GnssSystem::Qzss, '5', _) => frequency_hz(system, CarrierBand::L5),
358 (GnssSystem::Galileo, '1', _) => frequency_hz(system, CarrierBand::E1),
359 (GnssSystem::Galileo, '5', _) => frequency_hz(system, CarrierBand::E5a),
360 (GnssSystem::Galileo, '6', _) => frequency_hz(system, CarrierBand::E6),
361 (GnssSystem::Galileo, '7', _) => frequency_hz(system, CarrierBand::E5b),
362 (GnssSystem::Galileo, '8', _) => frequency_hz(system, CarrierBand::E5),
363 (GnssSystem::BeiDou, _, _) => {
364 frequency_hz(system, rinex_beidou_band(band, tracking, rinex_version)?)
365 }
366 (GnssSystem::Glonass, '1', Some(channel)) => {
367 Some(F_GLONASS_G1_BASE_HZ + f64::from(channel) * F_GLONASS_G1_STEP_HZ)
368 }
369 (GnssSystem::Glonass, '2', Some(channel)) => {
370 Some(F_GLONASS_G2_BASE_HZ + f64::from(channel) * F_GLONASS_G2_STEP_HZ)
371 }
372 _ => None,
373 }?;
374 valid_frequency_hz(frequency_hz)
375}
376
377fn rinex_beidou_band(
378 band: char,
379 tracking: Option<char>,
380 rinex_version: Option<f64>,
381) -> Option<CarrierBand> {
382 match band {
383 '1' if tracking == Some('I') && rinex_version.is_some_and(is_rinex_302) => {
384 Some(CarrierBand::B1i)
385 }
386 '1' => Some(CarrierBand::B1c),
387 '2' => Some(CarrierBand::B1i),
388 '5' => Some(CarrierBand::B2a),
389 '6' => Some(CarrierBand::B3i),
390 '7' => Some(CarrierBand::B2b),
391 '8' => Some(CarrierBand::B2),
392 _ => None,
393 }
394}
395
396fn is_rinex_302(version: f64) -> bool {
397 (3.015..3.025).contains(&version)
398}
399
400pub fn rinex_band_wavelength_m(
402 system: GnssSystem,
403 band: char,
404 glonass_channel: Option<i8>,
405) -> Option<f64> {
406 rinex_band_frequency_hz(system, band, glonass_channel).and_then(wavelength_for_frequency)
407}
408
409pub fn rinex_observation_wavelength_m(
411 system: GnssSystem,
412 code: &str,
413 rinex_version: f64,
414 glonass_channel: Option<i8>,
415) -> Option<f64> {
416 rinex_observation_frequency_hz(system, code, rinex_version, glonass_channel)
417 .and_then(wavelength_for_frequency)
418}
419
420fn valid_frequency_hz(frequency_hz: f64) -> Option<f64> {
421 validate::finite_positive(frequency_hz, "frequency_hz").ok()
422}
423
424pub(crate) fn wavelength_for_frequency(frequency_hz: f64) -> Option<f64> {
425 valid_frequency_hz(frequency_hz).map(|frequency_hz| C_M_S / frequency_hz)
426}
427
428pub const fn default_iono_free_pair(system: GnssSystem) -> Option<CarrierPair> {
430 match system {
431 GnssSystem::Gps => Some(CarrierPair::new(CarrierBand::L1, CarrierBand::L2)),
432 GnssSystem::Galileo => Some(CarrierPair::new(CarrierBand::E1, CarrierBand::E5a)),
433 GnssSystem::BeiDou => Some(CarrierPair::new(CarrierBand::B1i, CarrierBand::B3i)),
434 _ => None,
435 }
436}
437
438pub const fn glonass_g1_frequency_hz(channel: i8) -> f64 {
448 F_GLONASS_G1_BASE_HZ + (channel as f64) * F_GLONASS_G1_STEP_HZ
449}
450
451pub const fn default_spp_carrier(system: GnssSystem) -> Option<CarrierBand> {
453 match system {
454 GnssSystem::Gps => Some(CarrierBand::L1),
455 GnssSystem::Galileo => Some(CarrierBand::E1),
456 GnssSystem::BeiDou => Some(CarrierBand::B1i),
457 _ => None,
458 }
459}
460
461pub const fn default_spp_frequency_hz(system: GnssSystem) -> Option<f64> {
463 match default_spp_carrier(system) {
464 Some(band) => frequency_hz(system, band),
465 None => None,
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn iono_free_table_matches_supported_pairs() {
475 assert_eq!(
476 default_iono_free_pair(GnssSystem::Gps),
477 Some(CarrierPair::new(CarrierBand::L1, CarrierBand::L2))
478 );
479 assert_eq!(
480 default_iono_free_pair(GnssSystem::Galileo),
481 Some(CarrierPair::new(CarrierBand::E1, CarrierBand::E5a))
482 );
483 assert_eq!(
484 default_iono_free_pair(GnssSystem::BeiDou),
485 Some(CarrierPair::new(CarrierBand::B1i, CarrierBand::B3i))
486 );
487 assert_eq!(default_iono_free_pair(GnssSystem::Glonass), None);
488 assert_eq!(iono_free_carrier_frequencies().len(), 6);
489 }
490
491 #[test]
492 fn carrier_band_labels_are_canonical_lowercase() {
493 let cases = [
494 (CarrierBand::L1, "l1"),
495 (CarrierBand::L2, "l2"),
496 (CarrierBand::L5, "l5"),
497 (CarrierBand::E1, "e1"),
498 (CarrierBand::E5a, "e5a"),
499 (CarrierBand::E5b, "e5b"),
500 (CarrierBand::E5, "e5"),
501 (CarrierBand::E6, "e6"),
502 (CarrierBand::B1c, "b1c"),
503 (CarrierBand::B1i, "b1i"),
504 (CarrierBand::B2a, "b2a"),
505 (CarrierBand::B2b, "b2b"),
506 (CarrierBand::B2, "b2"),
507 (CarrierBand::B3i, "b3i"),
508 (CarrierBand::G1, "g1"),
509 (CarrierBand::G2, "g2"),
510 ];
511 for (band, label) in cases {
512 assert_eq!(band.as_str(), label);
513 assert_eq!(band.name(), label);
514 assert_eq!(band.to_string(), label);
515 assert_eq!(CarrierBand::from_name(label), Some(band));
516 }
517 }
518
519 #[test]
520 fn rinex_band_table_matches_existing_frequency_bits() {
521 let cases: [(GnssSystem, char, Option<i8>, f64); 16] = [
522 (GnssSystem::Gps, '1', None, 1_575_420_000.0),
523 (GnssSystem::Gps, '2', None, 1_227_600_000.0),
524 (GnssSystem::Gps, '5', None, 1_176_450_000.0),
525 (GnssSystem::Qzss, '1', None, 1_575_420_000.0),
526 (GnssSystem::Qzss, '2', None, 1_227_600_000.0),
527 (GnssSystem::Qzss, '5', None, 1_176_450_000.0),
528 (GnssSystem::Galileo, '6', None, 1_278_750_000.0),
529 (GnssSystem::Galileo, '7', None, 1_207_140_000.0),
530 (GnssSystem::Galileo, '8', None, 1_191_795_000.0),
531 (GnssSystem::BeiDou, '1', None, 1_575_420_000.0),
532 (GnssSystem::BeiDou, '2', None, 1_561_098_000.0),
533 (GnssSystem::BeiDou, '6', None, 1_268_520_000.0),
534 (GnssSystem::BeiDou, '7', None, 1_207_140_000.0),
535 (GnssSystem::BeiDou, '8', None, 1_191_795_000.0),
536 (GnssSystem::Glonass, '1', Some(1), 1_602_562_500.0),
537 (GnssSystem::Glonass, '2', Some(1), 1_246_437_500.0),
538 ];
539 for (system, band, channel, expected) in cases {
540 assert_eq!(
541 rinex_band_frequency_hz(system, band, channel).map(f64::to_bits),
542 Some(expected.to_bits())
543 );
544 }
545 assert_eq!(
546 rinex_band_frequency_hz(GnssSystem::Glonass, '1', None),
547 None
548 );
549 }
550
551 #[test]
552 fn rinex_observation_code_resolves_beidou_302_b1i() {
553 for code in ["C1I", "L1I"] {
554 assert_eq!(
555 rinex_observation_frequency_hz(GnssSystem::BeiDou, code, 3.02, None)
556 .map(f64::to_bits),
557 Some(F_B1I_HZ.to_bits())
558 );
559 }
560 assert_eq!(
561 rinex_observation_frequency_hz(GnssSystem::BeiDou, "L1X", 3.03, None).map(f64::to_bits),
562 Some(F_L1_HZ.to_bits())
563 );
564 }
565
566 #[test]
567 fn wavelength_helpers_use_c_over_frequency() {
568 let wavelength = wavelength_m(GnssSystem::Gps, CarrierBand::L1).unwrap();
569 assert_eq!(wavelength.to_bits(), (C_M_S / F_L1_HZ).to_bits());
570
571 let glonass_wavelength = rinex_band_wavelength_m(GnssSystem::Glonass, '1', Some(-7))
572 .expect("GLONASS G1 channel");
573 let frequency = 1_602_000_000.0 + f64::from(-7) * 562_500.0;
574 assert_eq!(glonass_wavelength.to_bits(), (C_M_S / frequency).to_bits());
575 }
576
577 #[test]
578 fn glonass_g1_spp_carrier_matches_rinex_band_table() {
579 for channel in [-7_i8, -1, 0, 1, 6] {
580 assert_eq!(
581 glonass_g1_frequency_hz(channel).to_bits(),
582 rinex_band_frequency_hz(GnssSystem::Glonass, '1', Some(channel))
583 .expect("GLONASS G1 channel frequency")
584 .to_bits()
585 );
586 }
587 }
588
589 #[test]
590 fn frequency_validation_rejects_invalid_runtime_values() {
591 assert_eq!(valid_frequency_hz(f64::NAN), None);
592 assert_eq!(valid_frequency_hz(f64::INFINITY), None);
593 assert_eq!(valid_frequency_hz(0.0), None);
594 assert_eq!(valid_frequency_hz(-1.0), None);
595 assert_eq!(wavelength_for_frequency(f64::NAN), None);
596 }
597}