1use crate::constants::{C_M_S, F_E1_HZ, F_E5A_HZ, F_L1_HZ, F_L2_HZ};
9use crate::error::{Error, Result};
10use crate::id::{GnssSatelliteId, GnssSystem};
11use crate::rtcm::bits::{BitReader, BitWriter};
12
13const HAS_ORBIT_RADIAL_SCALE_M: f64 = 0.0025;
14const HAS_ORBIT_ALONG_CROSS_SCALE_M: f64 = 0.0080;
15const HAS_CLOCK_SCALE_M: f64 = 0.0025;
16const HAS_CODE_BIAS_SCALE_M: f64 = 0.02;
17const HAS_PHASE_BIAS_SCALE_CYCLES: f64 = 0.01;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct HasMt1Header {
22 pub toh_s: u16,
24 pub mask: bool,
26 pub orbit: bool,
28 pub clock_full_set: bool,
30 pub clock_subset: bool,
32 pub code_bias: bool,
34 pub phase_bias: bool,
36 pub reserved: u8,
38 pub mask_id: u8,
40 pub iod_set_id: u8,
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct HasGnssMask {
47 pub system: GnssSystem,
49 pub satellites: Vec<u8>,
51 pub signals: Vec<u8>,
53 pub cell_mask: Option<Vec<bool>>,
55 pub nav_message: u8,
57}
58
59#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct HasMaskBlock {
62 pub systems: Vec<HasGnssMask>,
64}
65
66#[derive(Clone, Debug, PartialEq)]
68pub struct HasOrbitBlock {
69 pub validity_interval: u8,
71 pub records: Vec<HasOrbitCorrection>,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq)]
77pub struct HasOrbitCorrection {
78 pub sat: GnssSatelliteId,
80 pub iode: u32,
82 pub radial_m: f64,
84 pub along_m: f64,
86 pub cross_m: f64,
88}
89
90#[derive(Clone, Debug, PartialEq)]
92pub struct HasClockBlock {
93 pub validity_interval: u8,
95 pub records: Vec<HasClockCorrection>,
97}
98
99#[derive(Clone, Copy, Debug, PartialEq)]
101pub struct HasClockCorrection {
102 pub sat: GnssSatelliteId,
104 pub correction_m: f64,
106}
107
108#[derive(Clone, Debug, PartialEq)]
110pub struct HasCodeBiasBlock {
111 pub validity_interval: u8,
113 pub records: Vec<HasCodeBias>,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct HasCodeBias {
120 pub sat: GnssSatelliteId,
122 pub signal_id: u8,
124 pub bias_m: f64,
126}
127
128#[derive(Clone, Debug, PartialEq)]
130pub struct HasPhaseBiasBlock {
131 pub validity_interval: u8,
133 pub records: Vec<HasPhaseBias>,
135}
136
137#[derive(Clone, Copy, Debug, PartialEq)]
139pub struct HasPhaseBias {
140 pub sat: GnssSatelliteId,
142 pub signal_id: u8,
144 pub bias_cycles: f64,
146 pub bias_m: f64,
148 pub discontinuity_indicator: u8,
150}
151
152#[derive(Clone, Debug, PartialEq)]
154pub struct HasMt1Message {
155 pub header: HasMt1Header,
157 pub mask: Option<HasMaskBlock>,
159 pub orbit: Option<HasOrbitBlock>,
161 pub clock_full_set: Option<HasClockBlock>,
163 pub clock_subset: Option<HasClockBlock>,
165 pub code_bias: Option<HasCodeBiasBlock>,
167 pub phase_bias: Option<HasPhaseBiasBlock>,
169 pub padding_bits: Vec<bool>,
171}
172
173impl HasMt1Message {
174 pub fn decode(body: &[u8]) -> Result<Self> {
176 let mut r = BitReader::new(body);
177 let header = read_header(&mut r)?;
178 let mask = if header.mask {
179 Some(read_mask_block(&mut r)?)
180 } else {
181 None
182 };
183 let Some(mask_ref) = mask.as_ref() else {
184 if header.orbit
185 || header.clock_full_set
186 || header.clock_subset
187 || header.code_bias
188 || header.phase_bias
189 {
190 return Err(Error::Parse(
191 "HAS MT1 correction blocks require a mask in this stateless decoder"
192 .to_string(),
193 ));
194 }
195 return Ok(Self {
196 header,
197 mask,
198 orbit: None,
199 clock_full_set: None,
200 clock_subset: None,
201 code_bias: None,
202 phase_bias: None,
203 padding_bits: read_padding_bits(&mut r)?,
204 });
205 };
206
207 let orbit = if header.orbit {
208 Some(read_orbit_block(&mut r, mask_ref)?)
209 } else {
210 None
211 };
212 let clock_full_set = if header.clock_full_set {
213 Some(read_clock_full_set_block(&mut r, mask_ref)?)
214 } else {
215 None
216 };
217 let clock_subset = if header.clock_subset {
218 Some(read_clock_subset_block(&mut r, mask_ref)?)
219 } else {
220 None
221 };
222 let code_bias = if header.code_bias {
223 Some(read_code_bias_block(&mut r, mask_ref)?)
224 } else {
225 None
226 };
227 let phase_bias = if header.phase_bias {
228 Some(read_phase_bias_block(&mut r, mask_ref)?)
229 } else {
230 None
231 };
232
233 Ok(Self {
234 header,
235 mask,
236 orbit,
237 clock_full_set,
238 clock_subset,
239 code_bias,
240 phase_bias,
241 padding_bits: read_padding_bits(&mut r)?,
242 })
243 }
244
245 pub fn encode(&self) -> Vec<u8> {
247 let mut w = BitWriter::new();
248 write_header(&mut w, self.header);
249 if let Some(mask) = &self.mask {
250 write_mask_block(&mut w, mask);
251 }
252 if let Some(orbit) = &self.orbit {
253 write_orbit_block(&mut w, orbit);
254 }
255 if let Some(clock) = &self.clock_full_set {
256 write_clock_full_set_block(&mut w, self.mask.as_ref().expect("HAS mask"), clock);
257 }
258 if let Some(clock) = &self.clock_subset {
259 write_clock_subset_block(&mut w, self.mask.as_ref().expect("HAS mask"), clock);
260 }
261 if let Some(code_bias) = &self.code_bias {
262 write_code_bias_block(&mut w, self.mask.as_ref().expect("HAS mask"), code_bias);
263 }
264 if let Some(phase_bias) = &self.phase_bias {
265 write_phase_bias_block(&mut w, self.mask.as_ref().expect("HAS mask"), phase_bias);
266 }
267 for &bit in &self.padding_bits {
268 w.push_flag(bit);
269 }
270 w.into_bytes()
271 }
272}
273
274pub fn has_mt1_reference_j2000_s(
276 reception_gst: crate::astro::time::model::GnssWeekTow,
277 toh_s: u16,
278) -> Result<f64> {
279 if reception_gst.system != crate::astro::time::model::TimeScale::Gst {
280 return Err(Error::InvalidInput(
281 "HAS reception time must use GST".to_string(),
282 ));
283 }
284 let reception_s =
285 f64::from(reception_gst.week) * crate::constants::SECONDS_PER_WEEK + reception_gst.tow_s;
286 let hour = (reception_s / crate::constants::SECONDS_PER_HOUR).floor();
287 let candidate = hour * crate::constants::SECONDS_PER_HOUR + f64::from(toh_s);
288 let gst_s = if candidate <= reception_s {
289 candidate
290 } else {
291 candidate - crate::constants::SECONDS_PER_HOUR
292 };
293 Ok(gst_s - crate::constants::GPS_EPOCH_TO_J2000_S)
294}
295
296pub const fn has_validity_interval_s(index: u8) -> Option<f64> {
298 match index {
299 0 => Some(5.0),
300 1 => Some(10.0),
301 2 => Some(15.0),
302 3 => Some(20.0),
303 4 => Some(30.0),
304 5 => Some(60.0),
305 6 => Some(90.0),
306 7 => Some(120.0),
307 8 => Some(180.0),
308 9 => Some(240.0),
309 10 => Some(300.0),
310 11 => Some(600.0),
311 12 => Some(900.0),
312 13 => Some(1800.0),
313 14 => Some(3600.0),
314 _ => None,
315 }
316}
317
318fn read_header(r: &mut BitReader<'_>) -> Result<HasMt1Header> {
319 Ok(HasMt1Header {
320 toh_s: r.u(12)? as u16,
321 mask: r.flag()?,
322 orbit: r.flag()?,
323 clock_full_set: r.flag()?,
324 clock_subset: r.flag()?,
325 code_bias: r.flag()?,
326 phase_bias: r.flag()?,
327 reserved: r.u(4)? as u8,
328 mask_id: r.u(5)? as u8,
329 iod_set_id: r.u(5)? as u8,
330 })
331}
332
333fn write_header(w: &mut BitWriter, header: HasMt1Header) {
334 w.push_u(u64::from(header.toh_s), 12);
335 w.push_flag(header.mask);
336 w.push_flag(header.orbit);
337 w.push_flag(header.clock_full_set);
338 w.push_flag(header.clock_subset);
339 w.push_flag(header.code_bias);
340 w.push_flag(header.phase_bias);
341 w.push_u(u64::from(header.reserved), 4);
342 w.push_u(u64::from(header.mask_id), 5);
343 w.push_u(u64::from(header.iod_set_id), 5);
344}
345
346fn read_mask_block(r: &mut BitReader<'_>) -> Result<HasMaskBlock> {
347 let nsys = r.u(4)? as usize;
348 if nsys == 0 {
349 return Err(Error::Parse(
350 "HAS MT1 mask Nsys is reserved value 0".to_string(),
351 ));
352 }
353 let mut systems = Vec::with_capacity(nsys);
354 for _ in 0..nsys {
355 let gnss_id = r.u(4)? as u8;
356 let system = has_gnss_system(gnss_id)?;
357 let sat_mask = r.u(40)?;
358 let signal_mask = r.u(16)? as u16;
359 let cell_mask_available = r.flag()?;
360 let satellites = mask_indices(sat_mask, 40)
361 .into_iter()
362 .map(|idx| idx + 1)
363 .collect::<Vec<_>>();
364 let signals = mask_indices(u64::from(signal_mask), 16);
365 let cell_mask = if cell_mask_available {
366 let mut cells = Vec::with_capacity(satellites.len() * signals.len());
367 for _ in 0..satellites.len() * signals.len() {
368 cells.push(r.flag()?);
369 }
370 Some(cells)
371 } else {
372 None
373 };
374 let nav_message = r.u(3)? as u8;
375 systems.push(HasGnssMask {
376 system,
377 satellites,
378 signals,
379 cell_mask,
380 nav_message,
381 });
382 }
383 Ok(HasMaskBlock { systems })
384}
385
386fn write_mask_block(w: &mut BitWriter, mask: &HasMaskBlock) {
387 w.push_u(mask.systems.len() as u64, 4);
388 for system in &mask.systems {
389 w.push_u(u64::from(has_gnss_id(system.system).expect("HAS GNSS")), 4);
390 let sat_mask = mask_from_indices(system.satellites.iter().map(|prn| prn - 1), 40);
391 let signal_mask = mask_from_indices(system.signals.iter().copied(), 16);
392 w.push_u(sat_mask, 40);
393 w.push_u(signal_mask, 16);
394 w.push_flag(system.cell_mask.is_some());
395 if let Some(cells) = &system.cell_mask {
396 for &cell in cells {
397 w.push_flag(cell);
398 }
399 }
400 w.push_u(u64::from(system.nav_message), 3);
401 }
402}
403
404fn read_orbit_block(r: &mut BitReader<'_>, mask: &HasMaskBlock) -> Result<HasOrbitBlock> {
405 let validity_interval = r.u(4)? as u8;
406 if has_validity_interval_s(validity_interval).is_none() {
407 return Err(Error::Parse("HAS orbit VI is reserved".to_string()));
408 }
409 let mut records = Vec::new();
410 for sat in mask_satellites(mask)? {
411 let iode = r.u(iode_bits(sat.system))? as u32;
412 let radial = r.i(13)? as i16;
413 let along = r.i(12)? as i16;
414 let cross = r.i(12)? as i16;
415 if radial == -(1 << 12) || along == -(1 << 11) || cross == -(1 << 11) {
416 continue;
417 }
418 records.push(HasOrbitCorrection {
419 sat,
420 iode,
421 radial_m: f64::from(radial) * HAS_ORBIT_RADIAL_SCALE_M,
422 along_m: f64::from(along) * HAS_ORBIT_ALONG_CROSS_SCALE_M,
423 cross_m: f64::from(cross) * HAS_ORBIT_ALONG_CROSS_SCALE_M,
424 });
425 }
426 Ok(HasOrbitBlock {
427 validity_interval,
428 records,
429 })
430}
431
432fn write_orbit_block(w: &mut BitWriter, orbit: &HasOrbitBlock) {
433 w.push_u(u64::from(orbit.validity_interval), 4);
434 for rec in &orbit.records {
435 w.push_u(u64::from(rec.iode), iode_bits(rec.sat.system));
436 w.push_i((rec.radial_m / HAS_ORBIT_RADIAL_SCALE_M).round() as i64, 13);
437 w.push_i(
438 (rec.along_m / HAS_ORBIT_ALONG_CROSS_SCALE_M).round() as i64,
439 12,
440 );
441 w.push_i(
442 (rec.cross_m / HAS_ORBIT_ALONG_CROSS_SCALE_M).round() as i64,
443 12,
444 );
445 }
446}
447
448fn read_clock_full_set_block(r: &mut BitReader<'_>, mask: &HasMaskBlock) -> Result<HasClockBlock> {
449 let validity_interval = r.u(4)? as u8;
450 if has_validity_interval_s(validity_interval).is_none() {
451 return Err(Error::Parse("HAS clock VI is reserved".to_string()));
452 }
453 let multipliers = read_clock_multipliers(r, mask.systems.len())?;
454 let mut records = Vec::new();
455 for (system_idx, system_mask) in mask.systems.iter().enumerate() {
456 let multiplier = multipliers[system_idx];
457 for &prn in &system_mask.satellites {
458 let dcc = r.i(13)? as i16;
459 if let Some(correction_m) = has_clock_value_m(dcc, multiplier) {
460 records.push(HasClockCorrection {
461 sat: has_satellite(system_mask.system, prn)?,
462 correction_m,
463 });
464 }
465 }
466 }
467 Ok(HasClockBlock {
468 validity_interval,
469 records,
470 })
471}
472
473fn write_clock_full_set_block(w: &mut BitWriter, mask: &HasMaskBlock, clock: &HasClockBlock) {
474 w.push_u(u64::from(clock.validity_interval), 4);
475 for _ in &mask.systems {
476 w.push_u(0, 2);
477 }
478 for system_mask in &mask.systems {
479 for &prn in &system_mask.satellites {
480 let sat = has_satellite(system_mask.system, prn).expect("HAS sat");
481 let raw = clock
482 .records
483 .iter()
484 .find(|record| record.sat == sat)
485 .map(|record| (record.correction_m / HAS_CLOCK_SCALE_M).round() as i64)
486 .unwrap_or(-(1 << 12));
487 w.push_i(raw, 13);
488 }
489 }
490}
491
492fn read_clock_subset_block(r: &mut BitReader<'_>, mask: &HasMaskBlock) -> Result<HasClockBlock> {
493 let validity_interval = r.u(4)? as u8;
494 if has_validity_interval_s(validity_interval).is_none() {
495 return Err(Error::Parse("HAS clock subset VI is reserved".to_string()));
496 }
497 let nsys_sub = r.u(4)? as usize;
498 if nsys_sub == 0 {
499 return Err(Error::Parse(
500 "HAS clock subset Nsys_sub is reserved value 0".to_string(),
501 ));
502 }
503 let mut records = Vec::new();
504 for _ in 0..nsys_sub {
505 let system = has_gnss_system(r.u(4)? as u8)?;
506 let multiplier = dcm_multiplier(r.u(2)? as u8);
507 let Some(system_mask) = mask.systems.iter().find(|m| m.system == system) else {
508 return Err(Error::Parse(
509 "HAS clock subset GNSS not in mask".to_string(),
510 ));
511 };
512 let mut subset = Vec::with_capacity(system_mask.satellites.len());
513 for _ in 0..system_mask.satellites.len() {
514 subset.push(r.flag()?);
515 }
516 for (&present, &prn) in subset.iter().zip(&system_mask.satellites) {
517 if !present {
518 continue;
519 }
520 let dcc = r.i(13)? as i16;
521 if let Some(correction_m) = has_clock_value_m(dcc, multiplier) {
522 records.push(HasClockCorrection {
523 sat: has_satellite(system, prn)?,
524 correction_m,
525 });
526 }
527 }
528 }
529 Ok(HasClockBlock {
530 validity_interval,
531 records,
532 })
533}
534
535fn write_clock_subset_block(w: &mut BitWriter, mask: &HasMaskBlock, clock: &HasClockBlock) {
536 w.push_u(u64::from(clock.validity_interval), 4);
537 let systems = mask
538 .systems
539 .iter()
540 .filter(|system_mask| {
541 clock
542 .records
543 .iter()
544 .any(|record| record.sat.system == system_mask.system)
545 })
546 .collect::<Vec<_>>();
547 w.push_u(systems.len() as u64, 4);
548 for system_mask in systems {
549 w.push_u(
550 u64::from(has_gnss_id(system_mask.system).expect("HAS GNSS")),
551 4,
552 );
553 w.push_u(0, 2);
554 let mut selected = Vec::with_capacity(system_mask.satellites.len());
555 for &prn in &system_mask.satellites {
556 let sat = has_satellite(system_mask.system, prn).expect("HAS sat");
557 let present = clock.records.iter().any(|record| record.sat == sat);
558 selected.push((sat, present));
559 w.push_flag(present);
560 }
561 for (sat, present) in selected {
562 if present {
563 let raw = clock
564 .records
565 .iter()
566 .find(|record| record.sat == sat)
567 .map(|record| (record.correction_m / HAS_CLOCK_SCALE_M).round() as i64)
568 .expect("selected clock");
569 w.push_i(raw, 13);
570 }
571 }
572 }
573}
574
575fn read_code_bias_block(r: &mut BitReader<'_>, mask: &HasMaskBlock) -> Result<HasCodeBiasBlock> {
576 let validity_interval = r.u(4)? as u8;
577 if has_validity_interval_s(validity_interval).is_none() {
578 return Err(Error::Parse("HAS code-bias VI is reserved".to_string()));
579 }
580 let mut records = Vec::new();
581 for cell in mask_cells(mask)? {
582 let raw = r.i(11)? as i16;
583 if raw == -(1 << 10) {
584 continue;
585 }
586 records.push(HasCodeBias {
587 sat: cell.sat,
588 signal_id: cell.signal_id,
589 bias_m: f64::from(raw) * HAS_CODE_BIAS_SCALE_M,
590 });
591 }
592 Ok(HasCodeBiasBlock {
593 validity_interval,
594 records,
595 })
596}
597
598fn write_code_bias_block(w: &mut BitWriter, mask: &HasMaskBlock, code_bias: &HasCodeBiasBlock) {
599 w.push_u(u64::from(code_bias.validity_interval), 4);
600 for cell in mask_cells(mask).expect("HAS cells") {
601 let raw = code_bias
602 .records
603 .iter()
604 .find(|record| record.sat == cell.sat && record.signal_id == cell.signal_id)
605 .map(|record| (record.bias_m / HAS_CODE_BIAS_SCALE_M).round() as i64)
606 .unwrap_or(-(1 << 10));
607 w.push_i(raw, 11);
608 }
609}
610
611fn read_phase_bias_block(r: &mut BitReader<'_>, mask: &HasMaskBlock) -> Result<HasPhaseBiasBlock> {
612 let validity_interval = r.u(4)? as u8;
613 if has_validity_interval_s(validity_interval).is_none() {
614 return Err(Error::Parse("HAS phase-bias VI is reserved".to_string()));
615 }
616 let mut records = Vec::new();
617 for cell in mask_cells(mask)? {
618 let raw = r.i(11)? as i16;
619 let discontinuity_indicator = r.u(2)? as u8;
620 if raw == -(1 << 10) {
621 continue;
622 }
623 let bias_cycles = f64::from(raw) * HAS_PHASE_BIAS_SCALE_CYCLES;
624 let frequency_hz = has_signal_frequency_hz(cell.sat.system, cell.signal_id)?;
625 records.push(HasPhaseBias {
626 sat: cell.sat,
627 signal_id: cell.signal_id,
628 bias_cycles,
629 bias_m: bias_cycles * C_M_S / frequency_hz,
630 discontinuity_indicator,
631 });
632 }
633 Ok(HasPhaseBiasBlock {
634 validity_interval,
635 records,
636 })
637}
638
639fn write_phase_bias_block(w: &mut BitWriter, mask: &HasMaskBlock, phase_bias: &HasPhaseBiasBlock) {
640 w.push_u(u64::from(phase_bias.validity_interval), 4);
641 for cell in mask_cells(mask).expect("HAS cells") {
642 if let Some(record) = phase_bias
643 .records
644 .iter()
645 .find(|record| record.sat == cell.sat && record.signal_id == cell.signal_id)
646 {
647 w.push_i(
648 (record.bias_cycles / HAS_PHASE_BIAS_SCALE_CYCLES).round() as i64,
649 11,
650 );
651 w.push_u(u64::from(record.discontinuity_indicator), 2);
652 } else {
653 w.push_i(-(1 << 10), 11);
654 w.push_u(0, 2);
655 }
656 }
657}
658
659fn read_clock_multipliers(r: &mut BitReader<'_>, count: usize) -> Result<Vec<f64>> {
660 let mut out = Vec::with_capacity(count);
661 for _ in 0..count {
662 out.push(dcm_multiplier(r.u(2)? as u8));
663 }
664 Ok(out)
665}
666
667const fn dcm_multiplier(value: u8) -> f64 {
668 match value {
669 0 => 1.0,
670 1 => 2.0,
671 2 => 3.0,
672 _ => 4.0,
673 }
674}
675
676fn has_clock_value_m(raw: i16, multiplier: f64) -> Option<f64> {
677 if raw == -(1 << 12) || raw == (1 << 12) - 1 {
678 None
679 } else {
680 Some(f64::from(raw) * HAS_CLOCK_SCALE_M * multiplier)
681 }
682}
683
684#[derive(Clone, Copy)]
685struct HasCell {
686 sat: GnssSatelliteId,
687 signal_id: u8,
688}
689
690fn mask_satellites(mask: &HasMaskBlock) -> Result<Vec<GnssSatelliteId>> {
691 let mut out = Vec::new();
692 for system in &mask.systems {
693 for &prn in &system.satellites {
694 out.push(has_satellite(system.system, prn)?);
695 }
696 }
697 Ok(out)
698}
699
700fn mask_cells(mask: &HasMaskBlock) -> Result<Vec<HasCell>> {
701 let mut out = Vec::new();
702 for system in &mask.systems {
703 for (sat_idx, &prn) in system.satellites.iter().enumerate() {
704 let sat = has_satellite(system.system, prn)?;
705 for (sig_idx, &signal_id) in system.signals.iter().enumerate() {
706 let present = system
707 .cell_mask
708 .as_ref()
709 .map(|cells| cells[sat_idx * system.signals.len() + sig_idx])
710 .unwrap_or(true);
711 if present {
712 out.push(HasCell { sat, signal_id });
713 }
714 }
715 }
716 }
717 Ok(out)
718}
719
720fn read_padding_bits(r: &mut BitReader<'_>) -> Result<Vec<bool>> {
721 let mut padding = Vec::with_capacity(r.remaining_bits());
722 while r.remaining_bits() > 0 {
723 padding.push(r.flag()?);
724 }
725 Ok(padding)
726}
727
728fn has_gnss_system(gnss_id: u8) -> Result<GnssSystem> {
729 match gnss_id {
730 0 => Ok(GnssSystem::Gps),
731 2 => Ok(GnssSystem::Galileo),
732 _ => Err(Error::Parse(format!("unsupported HAS GNSS id {gnss_id}"))),
733 }
734}
735
736fn has_satellite(system: GnssSystem, prn: u8) -> Result<GnssSatelliteId> {
737 GnssSatelliteId::new(system, prn)
738 .map_err(|err| Error::Parse(format!("invalid HAS satellite {system:?} {prn}: {err}")))
739}
740
741fn has_gnss_id(system: GnssSystem) -> Option<u8> {
742 match system {
743 GnssSystem::Gps => Some(0),
744 GnssSystem::Galileo => Some(2),
745 _ => None,
746 }
747}
748
749fn iode_bits(system: GnssSystem) -> usize {
750 match system {
751 GnssSystem::Galileo => 10,
752 _ => 8,
753 }
754}
755
756fn mask_indices(mask: u64, width: usize) -> Vec<u8> {
757 let mut out = Vec::new();
758 for idx in 0..width {
759 if ((mask >> (width - 1 - idx)) & 1) != 0 {
760 out.push(idx as u8);
761 }
762 }
763 out
764}
765
766fn mask_from_indices(indices: impl IntoIterator<Item = u8>, width: usize) -> u64 {
767 let mut mask = 0_u64;
768 for idx in indices {
769 mask |= 1_u64 << (width - 1 - usize::from(idx));
770 }
771 mask
772}
773
774fn has_signal_frequency_hz(system: GnssSystem, signal_id: u8) -> Result<f64> {
775 match (system, signal_id) {
776 (GnssSystem::Gps, 0 | 3 | 4 | 5) => Ok(F_L1_HZ),
777 (GnssSystem::Gps, 6..=9) => Ok(F_L2_HZ),
778 (GnssSystem::Gps, 11..=13) => Ok(F_E5A_HZ),
779 (GnssSystem::Galileo, 0..=2) => Ok(F_E1_HZ),
780 (GnssSystem::Galileo, 3..=5) => Ok(F_E5A_HZ),
781 (GnssSystem::Galileo, 6..=8) => Ok(1_207_140_000.0),
782 (GnssSystem::Galileo, 9..=11) => Ok(1_191_795_000.0),
783 (GnssSystem::Galileo, 12..=14) => Ok(1_278_750_000.0),
784 _ => Err(Error::Parse(format!(
785 "unsupported HAS signal {signal_id} for {system:?}"
786 ))),
787 }
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793
794 #[test]
795 fn mt1_mask_orbit_clock_and_bias_blocks_roundtrip() {
796 let sat = GnssSatelliteId::new(GnssSystem::Gps, 3).unwrap();
797 let message = HasMt1Message {
798 header: HasMt1Header {
799 toh_s: 1234,
800 mask: true,
801 orbit: true,
802 clock_full_set: true,
803 clock_subset: false,
804 code_bias: true,
805 phase_bias: true,
806 reserved: 0,
807 mask_id: 7,
808 iod_set_id: 9,
809 },
810 mask: Some(HasMaskBlock {
811 systems: vec![HasGnssMask {
812 system: GnssSystem::Gps,
813 satellites: vec![3],
814 signals: vec![0, 9],
815 cell_mask: None,
816 nav_message: 0,
817 }],
818 }),
819 orbit: Some(HasOrbitBlock {
820 validity_interval: 5,
821 records: vec![HasOrbitCorrection {
822 sat,
823 iode: 42,
824 radial_m: 1.25,
825 along_m: -2.0,
826 cross_m: 3.0,
827 }],
828 }),
829 clock_full_set: Some(HasClockBlock {
830 validity_interval: 5,
831 records: vec![HasClockCorrection {
832 sat,
833 correction_m: -0.75,
834 }],
835 }),
836 clock_subset: None,
837 code_bias: Some(HasCodeBiasBlock {
838 validity_interval: 5,
839 records: vec![
840 HasCodeBias {
841 sat,
842 signal_id: 0,
843 bias_m: 0.24,
844 },
845 HasCodeBias {
846 sat,
847 signal_id: 9,
848 bias_m: -0.46,
849 },
850 ],
851 }),
852 phase_bias: Some(HasPhaseBiasBlock {
853 validity_interval: 5,
854 records: vec![
855 HasPhaseBias {
856 sat,
857 signal_id: 0,
858 bias_cycles: 1.25,
859 bias_m: 1.25 * C_M_S / F_L1_HZ,
860 discontinuity_indicator: 1,
861 },
862 HasPhaseBias {
863 sat,
864 signal_id: 9,
865 bias_cycles: -2.5,
866 bias_m: -2.5 * C_M_S / F_L2_HZ,
867 discontinuity_indicator: 2,
868 },
869 ],
870 }),
871 padding_bits: Vec::new(),
872 };
873
874 let body = message.encode();
875 let decoded = HasMt1Message::decode(&body).unwrap();
876 assert_eq!(decoded, message);
877 }
878
879 #[test]
880 fn toh_reference_uses_current_or_previous_gst_hour() {
881 let reception = crate::astro::time::model::GnssWeekTow::new(
882 crate::astro::time::model::TimeScale::Gst,
883 2400,
884 3605.0,
885 )
886 .unwrap();
887 let same_hour = has_mt1_reference_j2000_s(reception, 5).unwrap();
888 let expected_same = f64::from(reception.week) * crate::constants::SECONDS_PER_WEEK + 3605.0
889 - crate::constants::GPS_EPOCH_TO_J2000_S;
890 assert_eq!(same_hour.to_bits(), expected_same.to_bits());
891
892 let previous_hour = has_mt1_reference_j2000_s(reception, 3599).unwrap();
893 assert_eq!(previous_hour.to_bits(), (expected_same - 6.0).to_bits());
894 }
895}