Skip to main content

sidereon_core/
has.rs

1//! Galileo HAS MT1 correction payload parser.
2//!
3//! This module parses the recovered HAS Message Type 1 payload, starting at the
4//! 32-bit MT1 header and ending at the byte-aligned MT1 body. E6 page recovery,
5//! Reed-Solomon reconstruction, and outer transport framing are intentionally
6//! outside this module.
7
8use 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/// HAS MT1 header.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct HasMt1Header {
22    /// Time of hour in GST seconds.
23    pub toh_s: u16,
24    /// Mask content block present.
25    pub mask: bool,
26    /// Orbit correction content block present.
27    pub orbit: bool,
28    /// Clock full-set content block present.
29    pub clock_full_set: bool,
30    /// Clock subset content block present.
31    pub clock_subset: bool,
32    /// Code-bias content block present.
33    pub code_bias: bool,
34    /// Phase-bias content block present.
35    pub phase_bias: bool,
36    /// Reserved 4-bit field.
37    pub reserved: u8,
38    /// Mask identifier.
39    pub mask_id: u8,
40    /// IOD set identifier.
41    pub iod_set_id: u8,
42}
43
44/// One GNSS mask inside an HAS MT1 mask block.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct HasGnssMask {
47    /// Corrected constellation.
48    pub system: GnssSystem,
49    /// Corrected PRNs in HAS mask order.
50    pub satellites: Vec<u8>,
51    /// Corrected signal indices in HAS mask order.
52    pub signals: Vec<u8>,
53    /// Optional row-major satellite by signal cell mask.
54    pub cell_mask: Option<Vec<bool>>,
55    /// HAS navigation-message index.
56    pub nav_message: u8,
57}
58
59/// HAS MT1 mask block.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct HasMaskBlock {
62    /// GNSS-specific masks in transmitted order.
63    pub systems: Vec<HasGnssMask>,
64}
65
66/// HAS MT1 orbit correction block.
67#[derive(Clone, Debug, PartialEq)]
68pub struct HasOrbitBlock {
69    /// Validity interval index.
70    pub validity_interval: u8,
71    /// Orbit records in mask order, excluding unavailable records.
72    pub records: Vec<HasOrbitCorrection>,
73}
74
75/// HAS orbit correction for one satellite.
76#[derive(Clone, Copy, Debug, PartialEq)]
77pub struct HasOrbitCorrection {
78    /// Corrected satellite.
79    pub sat: GnssSatelliteId,
80    /// Reference navigation issue.
81    pub iode: u32,
82    /// Delta radial, meters, additive per HAS ICD.
83    pub radial_m: f64,
84    /// Delta in-track, meters, additive per HAS ICD.
85    pub along_m: f64,
86    /// Delta cross-track, meters, additive per HAS ICD.
87    pub cross_m: f64,
88}
89
90/// HAS MT1 clock full-set or subset correction block.
91#[derive(Clone, Debug, PartialEq)]
92pub struct HasClockBlock {
93    /// Validity interval index.
94    pub validity_interval: u8,
95    /// Clock records in mask order, excluding unavailable and do-not-use records.
96    pub records: Vec<HasClockCorrection>,
97}
98
99/// HAS clock correction for one satellite.
100#[derive(Clone, Copy, Debug, PartialEq)]
101pub struct HasClockCorrection {
102    /// Corrected satellite.
103    pub sat: GnssSatelliteId,
104    /// Delta clock in meters, additive per HAS ICD.
105    pub correction_m: f64,
106}
107
108/// HAS MT1 code-bias block.
109#[derive(Clone, Debug, PartialEq)]
110pub struct HasCodeBiasBlock {
111    /// Validity interval index.
112    pub validity_interval: u8,
113    /// Code-bias records.
114    pub records: Vec<HasCodeBias>,
115}
116
117/// HAS code bias for one satellite signal.
118#[derive(Clone, Copy, Debug, PartialEq)]
119pub struct HasCodeBias {
120    /// Corrected satellite.
121    pub sat: GnssSatelliteId,
122    /// HAS signal index.
123    pub signal_id: u8,
124    /// Bias in meters. Add to the pseudorange observation per HAS ICD.
125    pub bias_m: f64,
126}
127
128/// HAS MT1 phase-bias block.
129#[derive(Clone, Debug, PartialEq)]
130pub struct HasPhaseBiasBlock {
131    /// Validity interval index.
132    pub validity_interval: u8,
133    /// Phase-bias records.
134    pub records: Vec<HasPhaseBias>,
135}
136
137/// HAS phase bias for one satellite signal.
138#[derive(Clone, Copy, Debug, PartialEq)]
139pub struct HasPhaseBias {
140    /// Corrected satellite.
141    pub sat: GnssSatelliteId,
142    /// HAS signal index.
143    pub signal_id: u8,
144    /// Bias in cycles.
145    pub bias_cycles: f64,
146    /// Bias converted to meters using the HAS signal carrier.
147    pub bias_m: f64,
148    /// Phase discontinuity indicator.
149    pub discontinuity_indicator: u8,
150}
151
152/// Decoded HAS MT1 payload.
153#[derive(Clone, Debug, PartialEq)]
154pub struct HasMt1Message {
155    /// MT1 header.
156    pub header: HasMt1Header,
157    /// Mask block, when present.
158    pub mask: Option<HasMaskBlock>,
159    /// Orbit correction block, when present.
160    pub orbit: Option<HasOrbitBlock>,
161    /// Clock full-set correction block, when present.
162    pub clock_full_set: Option<HasClockBlock>,
163    /// Clock subset correction block, when present.
164    pub clock_subset: Option<HasClockBlock>,
165    /// Code-bias block, when present.
166    pub code_bias: Option<HasCodeBiasBlock>,
167    /// Phase-bias block, when present.
168    pub phase_bias: Option<HasPhaseBiasBlock>,
169    /// Remaining non-decoded padding or future-extension bits.
170    pub padding_bits: Vec<bool>,
171}
172
173impl HasMt1Message {
174    /// Decode one recovered HAS MT1 payload.
175    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    /// Encode this message back to an MT1 payload.
246    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
274/// Resolve an HAS MT1 TOH to a J2000 epoch using the GST reception time.
275pub 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
296/// HAS validity interval in seconds for a VI index.
297pub 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}