Skip to main content

sbf_tools/blocks/
measurement.rs

1//! Measurement blocks (MeasEpoch_v2)
2
3use crate::error::{SbfError, SbfResult};
4use crate::header::SbfHeader;
5use crate::types::{SatelliteId, SignalType};
6
7use super::block_ids;
8use super::dnu::{u16_or_none, u8_or_none, I32_DNU, U16_DNU};
9use super::SbfBlockParse;
10
11// ============================================================================
12// MeasEpoch Type1 Sub-block (raw)
13// ============================================================================
14
15/// Raw Type1 sub-block data from MeasEpoch
16#[derive(Debug, Clone)]
17pub struct MeasEpochType1Raw {
18    pub rx_channel: u8,
19    pub signal_type: u8,
20    pub svid: u8,
21    pub misc: u8,
22    pub code_lsb: u32,
23    pub doppler: i32,
24    pub carrier_lsb: u16,
25    pub carrier_msb: i8,
26    pub cn0: u8,
27    pub lock_time: u16,
28    pub obs_info: u8,
29    pub n2: u8,
30}
31
32// ============================================================================
33// Satellite Measurement (processed)
34// ============================================================================
35
36/// Processed satellite measurement from MeasEpoch
37#[derive(Debug, Clone)]
38pub struct SatelliteMeasurement {
39    /// Satellite ID
40    pub sat_id: SatelliteId,
41    /// Signal type
42    pub signal_type: SignalType,
43    /// Raw CN0 value (use cn0_dbhz() for scaling per SBF spec)
44    cn0_raw: u8,
45    /// Raw Doppler value (multiply by 0.0001 for Hz)
46    doppler_raw: i32,
47    /// Whether `doppler_raw` is available as an absolute Doppler value.
48    doppler_valid: bool,
49    /// Raw lock time
50    lock_time_raw: u16,
51    /// Whether `lock_time_raw` is available.
52    lock_time_valid: bool,
53    /// Observation info flags
54    pub obs_info: u8,
55}
56
57impl SatelliteMeasurement {
58    /// Get CN0 in dB-Hz (scaled per SBF Reference Guide)
59    pub fn cn0_dbhz(&self) -> f64 {
60        self.cn0_dbhz_opt().unwrap_or(0.0)
61    }
62
63    /// Get CN0 in dB-Hz, or `None` when the raw C/N0 field is unavailable.
64    pub fn cn0_dbhz_opt(&self) -> Option<f64> {
65        let cn0_raw = u8_or_none(self.cn0_raw)?;
66        let base = cn0_raw as f64 * 0.25;
67        match self.signal_type {
68            // Signal numbers 1 and 2 (GPS L1P, GPS L2P) have no +10 dB offset
69            SignalType::L1PY | SignalType::L2P => Some(base),
70            _ => Some(base + 10.0),
71        }
72    }
73
74    /// Get raw CN0 value
75    pub fn cn0_raw(&self) -> u8 {
76        self.cn0_raw
77    }
78
79    /// Check if CN0 is valid (not 255)
80    pub fn cn0_valid(&self) -> bool {
81        u8_or_none(self.cn0_raw).is_some()
82    }
83
84    /// Get Doppler in Hz (scaled).
85    ///
86    /// Returns `0.0` when the absolute Doppler field is unavailable. Use
87    /// [`Self::doppler_hz_opt`] to distinguish unavailable from a real zero.
88    pub fn doppler_hz(&self) -> f64 {
89        self.doppler_hz_opt().unwrap_or(0.0)
90    }
91
92    /// Get Doppler in Hz (scaled), or `None` when unavailable.
93    pub fn doppler_hz_opt(&self) -> Option<f64> {
94        if self.doppler_valid {
95            Some(self.doppler_raw as f64 * 0.0001)
96        } else {
97            None
98        }
99    }
100
101    /// Get raw Doppler value
102    pub fn doppler_raw(&self) -> i32 {
103        self.doppler_raw
104    }
105
106    /// Get lock time in seconds.
107    ///
108    /// Returns `0.0` when the lock time field is unavailable. Use
109    /// [`Self::lock_time_seconds_opt`] to distinguish unavailable from a real zero.
110    pub fn lock_time_seconds(&self) -> f64 {
111        self.lock_time_seconds_opt().unwrap_or(0.0)
112    }
113
114    /// Get lock time in seconds, or `None` when unavailable.
115    pub fn lock_time_seconds_opt(&self) -> Option<f64> {
116        if self.lock_time_valid {
117            Some(self.lock_time_raw as f64)
118        } else {
119            None
120        }
121    }
122
123    /// Get raw lock time value
124    pub fn lock_time_raw(&self) -> u16 {
125        self.lock_time_raw
126    }
127
128    /// Check if half-cycle ambiguity is resolved.
129    ///
130    /// Per SBF, bit 2 is set when a half-cycle ambiguity is present.
131    pub fn half_cycle_resolved(&self) -> bool {
132        (self.obs_info & 0x04) == 0
133    }
134
135    /// Check if smoothing is active.
136    ///
137    /// Per SBF, bit 0 indicates code smoothing.
138    pub fn smoothing_active(&self) -> bool {
139        (self.obs_info & 0x01) != 0
140    }
141}
142
143// ============================================================================
144// MeasEpoch Block
145// ============================================================================
146
147/// MeasEpoch_v2 block (Block ID 4027)
148///
149/// Contains satellite measurements including code, carrier, Doppler, and CN0.
150#[derive(Debug, Clone)]
151pub struct MeasEpochBlock {
152    /// Time of week in milliseconds
153    tow_ms: u32,
154    /// GPS week number
155    wnc: u16,
156    /// Number of Type1 sub-blocks
157    pub n1: u8,
158    /// Length of each Type1 sub-block
159    pub sb1_length: u8,
160    /// Length of each Type2 sub-block
161    pub sb2_length: u8,
162    /// Common flags
163    pub common_flags: u8,
164    /// Cumulative clock jumps modulo 256 ms (raw field value).
165    pub cum_clk_jumps: u8,
166    /// Satellite measurements
167    pub measurements: Vec<SatelliteMeasurement>,
168}
169
170impl MeasEpochBlock {
171    /// Get TOW in seconds
172    pub fn tow_seconds(&self) -> f64 {
173        self.tow_ms as f64 * 0.001
174    }
175
176    /// Get raw TOW in milliseconds
177    pub fn tow_ms(&self) -> u32 {
178        self.tow_ms
179    }
180
181    /// Get week number
182    pub fn wnc(&self) -> u16 {
183        self.wnc
184    }
185
186    /// Get number of satellites with measurements
187    pub fn num_satellites(&self) -> usize {
188        self.measurements.len()
189    }
190
191    /// Get measurements for a specific satellite
192    pub fn measurements_for_sat(&self, sat_id: &SatelliteId) -> Vec<&SatelliteMeasurement> {
193        self.measurements
194            .iter()
195            .filter(|m| &m.sat_id == sat_id)
196            .collect()
197    }
198
199    /// Get all valid CN0 measurements
200    pub fn valid_cn0_measurements(&self) -> Vec<&SatelliteMeasurement> {
201        self.measurements.iter().filter(|m| m.cn0_valid()).collect()
202    }
203}
204
205impl SbfBlockParse for MeasEpochBlock {
206    const BLOCK_ID: u16 = block_ids::MEAS_EPOCH;
207
208    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
209        let full_len = header.length as usize;
210        if data.len() < full_len - 2 {
211            // -2 for sync bytes not in data
212            return Err(SbfError::IncompleteBlock {
213                needed: full_len,
214                have: data.len() + 2,
215            });
216        }
217
218        // MeasEpoch structure (offsets from data start, which is after sync):
219        // 0-1: CRC, 2-3: ID, 4-5: Length
220        // 6-9: TOW, 10-11: WNc
221        // 12: N1, 13: SB1Length, 14: SB2Length
222        // 15: CommonFlags, 16: CumClkJumps
223        // 17: Reserved (Rev 1+)
224        // Type1 sub-blocks start at offset 17 (Rev 0) or 18 (Rev 1+)
225
226        if data.len() < 17 {
227            return Err(SbfError::ParseError("MeasEpoch too short".into()));
228        }
229
230        let n1 = data[12];
231        let sb1_length = data[13];
232        let sb2_length = data[14];
233        let common_flags = data[15];
234        let cum_clk_jumps = data[16];
235
236        if sb1_length == 0 {
237            return Err(SbfError::ParseError("MeasEpoch SB1Length is zero".into()));
238        }
239
240        let sb1_length_usize = sb1_length as usize;
241        let sb2_length_usize = sb2_length as usize;
242
243        // Type1 sub-blocks start at offset 17 (Rev 0) or 18 (Rev 1+)
244        let mut offset = 17;
245        if header.block_rev >= 1 {
246            offset += 1; // Reserved byte in Rev 1+
247        }
248
249        let mut measurements = Vec::new();
250
251        // Helper to extract signal number from type field
252        let signal_number = |type_field: u8, obs_info: u8| -> u8 {
253            let sig_idx = type_field & 0x1F;
254            if sig_idx == 31 {
255                32 + ((obs_info >> 3) & 0x1F)
256            } else {
257                sig_idx
258            }
259        };
260
261        for _ in 0..n1 {
262            if offset + sb1_length_usize > data.len() {
263                return Err(SbfError::ParseError(
264                    "MeasEpoch SB1 exceeds block length".into(),
265                ));
266            }
267
268            // Type-1 sub-block structure (20 bytes typical):
269            // 0: RxChannel, 1: Type, 2: SVID, 3: Misc
270            // 4-7: CodeLSB, 8-11: Doppler, 12-13: CarrierLSB
271            // 14: CarrierMSB, 15: CN0, 16-17: LockTime
272            // 18: ObsInfo, 19: N2
273
274            let svid = data[offset + 2];
275            let type_field = data[offset + 1];
276
277            let (doppler, doppler_valid) = if sb1_length_usize > 11 {
278                let raw = i32::from_le_bytes([
279                    data[offset + 8],
280                    data[offset + 9],
281                    data[offset + 10],
282                    data[offset + 11],
283                ]);
284                (raw, raw != I32_DNU)
285            } else {
286                (0, false)
287            };
288
289            let cn0_raw = if sb1_length_usize > 15 {
290                data[offset + 15]
291            } else {
292                255
293            };
294
295            let (lock_time, lock_time_valid) = if sb1_length_usize > 17 {
296                let raw = u16::from_le_bytes([data[offset + 16], data[offset + 17]]);
297                (raw, raw != U16_DNU)
298            } else {
299                (0, false)
300            };
301
302            let obs_info = if sb1_length_usize > 18 {
303                data[offset + 18]
304            } else {
305                0
306            };
307
308            let n2 = if sb1_length_usize > 19 {
309                data[offset + 19]
310            } else {
311                0
312            };
313
314            // Parse primary signal measurement
315            if let Some(sat_id) = SatelliteId::from_svid(svid) {
316                let sig_num = signal_number(type_field, obs_info);
317                let signal_type = SignalType::from_signal_number(sig_num);
318
319                measurements.push(SatelliteMeasurement {
320                    sat_id: sat_id.clone(),
321                    signal_type,
322                    cn0_raw,
323                    doppler_raw: doppler,
324                    doppler_valid,
325                    lock_time_raw: lock_time,
326                    lock_time_valid,
327                    obs_info,
328                });
329
330                offset += sb1_length_usize;
331
332                // Parse Type2 sub-blocks (additional signals for same satellite)
333                if sb2_length_usize > 0 {
334                    for _ in 0..n2 {
335                        if offset + sb2_length_usize > data.len() {
336                            return Err(SbfError::ParseError(
337                                "MeasEpoch SB2 exceeds block length".into(),
338                            ));
339                        }
340
341                        // Type-2 sub-block structure:
342                        // 0: Type, 1: LockTime (short), 2: CN0
343                        // 3: OffsetMSB, 4: CarrierMSB, 5: ObsInfo
344                        // 6-7: CodeOffsetLSB, 8-9: CarrierLSB, 10-11: DopplerOffsetLSB
345
346                        let type2_field = data[offset];
347                        let cn0_raw_2 = if sb2_length_usize > 2 {
348                            data[offset + 2]
349                        } else {
350                            255
351                        };
352                        let (lock_time_2, lock_time_valid_2) = if sb2_length_usize > 1 {
353                            let raw = data[offset + 1];
354                            (raw as u16, u8_or_none(raw).is_some())
355                        } else {
356                            (0, false)
357                        };
358                        let obs_info_2 = if sb2_length_usize > 5 {
359                            data[offset + 5]
360                        } else {
361                            0
362                        };
363
364                        let sig_num_2 = signal_number(type2_field, obs_info_2);
365                        let signal_type_2 = SignalType::from_signal_number(sig_num_2);
366
367                        measurements.push(SatelliteMeasurement {
368                            sat_id: sat_id.clone(),
369                            signal_type: signal_type_2,
370                            cn0_raw: cn0_raw_2,
371                            doppler_raw: 0, // Type2 has offset, not absolute
372                            doppler_valid: false,
373                            lock_time_raw: lock_time_2,
374                            lock_time_valid: lock_time_valid_2,
375                            obs_info: obs_info_2,
376                        });
377
378                        offset += sb2_length_usize;
379                    }
380                }
381            } else {
382                // Skip invalid SVID
383                offset += sb1_length_usize;
384                if sb2_length_usize > 0 {
385                    let n2_skip = if sb1_length_usize > 19 {
386                        data[offset - sb1_length_usize + 19]
387                    } else {
388                        0
389                    };
390                    let skip_bytes =
391                        sb2_length_usize
392                            .checked_mul(n2_skip as usize)
393                            .ok_or_else(|| {
394                                SbfError::ParseError("MeasEpoch SB2 length overflow".into())
395                            })?;
396                    if offset + skip_bytes > data.len() {
397                        return Err(SbfError::ParseError(
398                            "MeasEpoch SB2 exceeds block length".into(),
399                        ));
400                    }
401                    offset += skip_bytes;
402                }
403            }
404        }
405
406        Ok(Self {
407            tow_ms: header.tow_ms,
408            wnc: header.wnc,
409            n1,
410            sb1_length,
411            sb2_length,
412            common_flags,
413            cum_clk_jumps,
414            measurements,
415        })
416    }
417}
418
419// ============================================================================
420// MeasExtra Block
421// ============================================================================
422
423/// MeasExtra channel information
424#[derive(Debug, Clone)]
425pub struct MeasExtraChannel {
426    /// Receiver channel
427    pub rx_channel: u8,
428    /// Signal type (decoded)
429    pub signal_type: SignalType,
430    /// Raw signal type field
431    signal_type_raw: u8,
432    /// Decoded global SBF signal number
433    signal_number: u8,
434    /// Multipath correction (raw, millimeters)
435    mp_correction_raw: i16,
436    /// Smoothing correction (raw, millimeters)
437    smoothing_correction_raw: i16,
438    /// Code variance (raw)
439    code_var_raw: u16,
440    /// Carrier variance (raw)
441    carrier_var_raw: u16,
442    /// Lock time in seconds (raw)
443    lock_time_raw: u16,
444    /// Cumulative loss of continuity
445    pub cum_loss_cont: u8,
446    /// Carrier phase multipath correction (raw, 1/512 cycles) when present
447    car_mp_correction_raw: Option<i8>,
448    /// Info flags
449    pub info: u8,
450    /// Misc bitfield when present (rev 3+ sub-block layout)
451    misc_raw: Option<u8>,
452}
453
454impl MeasExtraChannel {
455    /// Get raw signal type value
456    pub fn signal_type_raw(&self) -> u8 {
457        self.signal_type_raw
458    }
459
460    /// Get decoded global SBF signal number
461    pub fn signal_number(&self) -> u8 {
462        self.signal_number
463    }
464
465    /// Get antenna ID from the Type field (bits 5-7)
466    pub fn antenna_id(&self) -> u8 {
467        (self.signal_type_raw >> 5) & 0x07
468    }
469
470    /// Multipath correction in meters
471    pub fn mp_correction_m(&self) -> f64 {
472        self.mp_correction_raw as f64 * 0.001
473    }
474
475    /// Smoothing correction in meters
476    pub fn smoothing_correction_m(&self) -> f64 {
477        self.smoothing_correction_raw as f64 * 0.001
478    }
479
480    /// Code variance in m^2
481    pub fn code_var_m2(&self) -> f64 {
482        self.code_var_m2_opt().unwrap_or(0.0)
483    }
484
485    /// Code variance in m^2, or `None` when unavailable.
486    pub fn code_var_m2_opt(&self) -> Option<f64> {
487        u16_or_none(self.code_var_raw).map(|raw| raw as f64 * 0.0001)
488    }
489
490    /// Raw code variance field from the SBF block.
491    pub fn code_var_raw(&self) -> u16 {
492        self.code_var_raw
493    }
494
495    /// Carrier variance in cycles^2
496    pub fn carrier_var_cycles2(&self) -> f64 {
497        self.carrier_var_cycles2_opt().unwrap_or(0.0)
498    }
499
500    /// Carrier variance in cycles^2, or `None` when unavailable.
501    pub fn carrier_var_cycles2_opt(&self) -> Option<f64> {
502        u16_or_none(self.carrier_var_raw).map(|raw| raw as f64 * 0.000001)
503    }
504
505    /// Raw carrier variance field from the SBF block.
506    pub fn carrier_var_raw(&self) -> u16 {
507        self.carrier_var_raw
508    }
509
510    /// Lock time in seconds
511    pub fn lock_time_seconds(&self) -> f64 {
512        self.lock_time_seconds_opt().unwrap_or(0.0)
513    }
514
515    /// Lock time in seconds, or `None` when unavailable.
516    pub fn lock_time_seconds_opt(&self) -> Option<f64> {
517        u16_or_none(self.lock_time_raw).map(|raw| raw as f64)
518    }
519
520    /// Raw lock time value
521    pub fn lock_time_raw(&self) -> u16 {
522        self.lock_time_raw
523    }
524
525    /// Raw carrier multipath correction in units of 1/512 cycles
526    pub fn car_mp_correction_raw(&self) -> Option<i8> {
527        self.car_mp_correction_raw
528    }
529
530    /// Carrier multipath correction in cycles (when present)
531    pub fn car_mp_correction_cycles(&self) -> Option<f64> {
532        self.car_mp_correction_raw.map(|v| v as f64 / 512.0)
533    }
534
535    /// Get raw Misc bitfield (rev 3+)
536    pub fn misc_raw(&self) -> Option<u8> {
537        self.misc_raw
538    }
539
540    /// C/N0 high-resolution extension in dB-Hz offset (rev 3+, bits 0-2)
541    pub fn cn0_high_res_dbhz_offset(&self) -> Option<f64> {
542        self.misc_raw.map(|misc| (misc & 0x07) as f64 * 0.03125)
543    }
544}
545
546/// MeasExtra block (Block ID 4000)
547///
548/// Additional measurement data such as multipath corrections and variances.
549#[derive(Debug, Clone)]
550pub struct MeasExtraBlock {
551    /// Time of week in milliseconds
552    tow_ms: u32,
553    /// GPS week number
554    wnc: u16,
555    /// Number of sub-blocks
556    pub n: u8,
557    /// Sub-block length
558    pub sb_length: u8,
559    /// Doppler variance factor
560    doppler_var_factor: f32,
561    /// Channel data
562    pub channels: Vec<MeasExtraChannel>,
563}
564
565impl MeasExtraBlock {
566    pub fn tow_seconds(&self) -> f64 {
567        self.tow_ms as f64 * 0.001
568    }
569    pub fn tow_ms(&self) -> u32 {
570        self.tow_ms
571    }
572    pub fn wnc(&self) -> u16 {
573        self.wnc
574    }
575
576    /// Doppler variance factor
577    pub fn doppler_var_factor(&self) -> f32 {
578        self.doppler_var_factor
579    }
580
581    /// Number of channels
582    pub fn num_channels(&self) -> usize {
583        self.channels.len()
584    }
585}
586
587impl SbfBlockParse for MeasExtraBlock {
588    const BLOCK_ID: u16 = block_ids::MEAS_EXTRA;
589
590    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
591        if data.len() < 18 {
592            return Err(SbfError::ParseError("MeasExtra too short".into()));
593        }
594
595        // Offsets:
596        // 12: N
597        // 13: SBLength
598        // 14-17: DopplerVarFactor (f4)
599        let n = data[12];
600        let sb_length = data[13];
601
602        if sb_length < 14 {
603            return Err(SbfError::ParseError("MeasExtra SBLength too small".into()));
604        }
605
606        let doppler_var_factor = f32::from_le_bytes(data[14..18].try_into().unwrap());
607
608        let sb_length_usize = sb_length as usize;
609        let mut channels = Vec::new();
610        let mut offset = 18;
611
612        for _ in 0..n {
613            if offset + sb_length_usize > data.len() {
614                return Err(SbfError::ParseError(
615                    "MeasExtra sub-block exceeds block length".into(),
616                ));
617            }
618
619            let rx_channel = data[offset];
620            let signal_type_raw = data[offset + 1];
621            let mp_correction_raw = i16::from_le_bytes([data[offset + 2], data[offset + 3]]);
622            let smoothing_correction_raw = i16::from_le_bytes([data[offset + 4], data[offset + 5]]);
623            let code_var_raw = u16::from_le_bytes([data[offset + 6], data[offset + 7]]);
624            let carrier_var_raw = u16::from_le_bytes([data[offset + 8], data[offset + 9]]);
625            let lock_time_raw = u16::from_le_bytes([data[offset + 10], data[offset + 11]]);
626            let cum_loss_cont = data[offset + 12];
627            let (car_mp_correction_raw, info, misc_raw) = if sb_length_usize >= 16 {
628                // Rev 3+ layout includes CarMPCorr, Info, and Misc.
629                (
630                    Some(data[offset + 13] as i8),
631                    data[offset + 14],
632                    Some(data[offset + 15]),
633                )
634            } else if sb_length_usize >= 15 {
635                // Intermediate layout includes CarMPCorr and Info.
636                (Some(data[offset + 13] as i8), data[offset + 14], None)
637            } else {
638                // Legacy layout has Info directly after CumLossCont.
639                (None, data[offset + 13], None)
640            };
641
642            let sig_idx_lo = signal_type_raw & 0x1F;
643            let signal_number = if sig_idx_lo == 31 {
644                misc_raw
645                    .map(|misc| 32 + ((misc >> 3) & 0x1F))
646                    .unwrap_or(sig_idx_lo)
647            } else {
648                sig_idx_lo
649            };
650
651            channels.push(MeasExtraChannel {
652                rx_channel,
653                signal_type: SignalType::from_signal_number(signal_number),
654                signal_type_raw,
655                signal_number,
656                mp_correction_raw,
657                smoothing_correction_raw,
658                code_var_raw,
659                carrier_var_raw,
660                lock_time_raw,
661                cum_loss_cont,
662                car_mp_correction_raw,
663                info,
664                misc_raw,
665            });
666
667            offset += sb_length_usize;
668        }
669
670        Ok(Self {
671            tow_ms: header.tow_ms,
672            wnc: header.wnc,
673            n,
674            sb_length,
675            doppler_var_factor,
676            channels,
677        })
678    }
679}
680
681// ============================================================================
682// IQCorr Block
683// ============================================================================
684
685/// IQ correlation channel sub-block
686#[derive(Debug, Clone)]
687pub struct IqCorrChannel {
688    pub rx_channel: u8,
689    pub signal_type: u8,
690    pub svid: u8,
691    pub corr_iq_msb: u8,
692    pub corr_i_lsb: u8,
693    pub corr_q_lsb: u8,
694    pub carrier_phase_lsb: u16,
695}
696
697/// IQCorr block (Block ID 4046)
698///
699/// Signal-quality metrics from I/Q correlation.
700#[derive(Debug, Clone)]
701pub struct IqCorrBlock {
702    tow_ms: u32,
703    wnc: u16,
704    pub n: u8,
705    pub sb_length: u8,
706    /// Correlation duration in ms
707    pub corr_duration: u8,
708    pub cum_clk_jumps: i8,
709    pub channels: Vec<IqCorrChannel>,
710}
711
712impl IqCorrBlock {
713    pub fn tow_seconds(&self) -> f64 {
714        self.tow_ms as f64 * 0.001
715    }
716    pub fn tow_ms(&self) -> u32 {
717        self.tow_ms
718    }
719    pub fn wnc(&self) -> u16 {
720        self.wnc
721    }
722    pub fn num_channels(&self) -> usize {
723        self.channels.len()
724    }
725}
726
727impl SbfBlockParse for IqCorrBlock {
728    const BLOCK_ID: u16 = block_ids::IQ_CORR;
729
730    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
731        // Header: N, SBLength, CorrDuration, CumClkJumps = 4 bytes at offset 12
732        const MIN_HEADER: usize = 16;
733        if data.len() < MIN_HEADER {
734            return Err(SbfError::ParseError("IQCorr too short".into()));
735        }
736
737        let n = data[12];
738        let sb_length = data[13];
739        let corr_duration = data[14];
740        let cum_clk_jumps = data[15] as i8;
741
742        if sb_length < 8 {
743            return Err(SbfError::ParseError("IQCorr SBLength too small".into()));
744        }
745
746        let sb_length_usize = sb_length as usize;
747        let mut channels = Vec::new();
748        let mut offset = 16;
749
750        for _ in 0..n {
751            if offset + sb_length_usize > data.len() {
752                return Err(SbfError::ParseError(
753                    "IQCorr sub-block exceeds block length".into(),
754                ));
755            }
756
757            channels.push(IqCorrChannel {
758                rx_channel: data[offset],
759                signal_type: data[offset + 1],
760                svid: data[offset + 2],
761                corr_iq_msb: data[offset + 3],
762                corr_i_lsb: data[offset + 4],
763                corr_q_lsb: data[offset + 5],
764                carrier_phase_lsb: u16::from_le_bytes([data[offset + 6], data[offset + 7]]),
765            });
766
767            offset += sb_length_usize;
768        }
769
770        Ok(Self {
771            tow_ms: header.tow_ms,
772            wnc: header.wnc,
773            n,
774            sb_length,
775            corr_duration,
776            cum_clk_jumps,
777            channels,
778        })
779    }
780}
781
782// ============================================================================
783// EndOfMeas Block
784// ============================================================================
785
786/// EndOfMeas block (Block ID 5922)
787///
788/// Marker indicating end of measurement blocks for current epoch.
789#[derive(Debug, Clone)]
790pub struct EndOfMeasBlock {
791    tow_ms: u32,
792    wnc: u16,
793}
794
795impl EndOfMeasBlock {
796    pub fn tow_seconds(&self) -> f64 {
797        self.tow_ms as f64 * 0.001
798    }
799    pub fn tow_ms(&self) -> u32 {
800        self.tow_ms
801    }
802    pub fn wnc(&self) -> u16 {
803        self.wnc
804    }
805}
806
807impl SbfBlockParse for EndOfMeasBlock {
808    const BLOCK_ID: u16 = block_ids::END_OF_MEAS;
809
810    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
811        if data.len() < 12 {
812            return Err(SbfError::ParseError("EndOfMeas too short".into()));
813        }
814
815        Ok(Self {
816            tow_ms: header.tow_ms,
817            wnc: header.wnc,
818        })
819    }
820}
821
822#[cfg(test)]
823mod tests {
824    use super::*;
825    use crate::header::SbfHeader;
826    use crate::types::Constellation;
827
828    #[test]
829    fn test_satellite_measurement_cn0() {
830        let meas = SatelliteMeasurement {
831            sat_id: SatelliteId::new(Constellation::GPS, 1),
832            signal_type: SignalType::L1CA,
833            cn0_raw: 160, // 160 * 0.25 + 10 = 50 dB-Hz
834            doppler_raw: 1000,
835            doppler_valid: true,
836            lock_time_raw: 10,
837            lock_time_valid: true,
838            obs_info: 0,
839        };
840
841        assert_eq!(meas.cn0_dbhz(), 50.0);
842        assert_eq!(meas.cn0_dbhz_opt(), Some(50.0));
843        assert!(meas.cn0_valid());
844        assert_eq!(meas.doppler_hz_opt(), Some(0.1));
845        assert_eq!(meas.lock_time_seconds_opt(), Some(10.0));
846    }
847
848    #[test]
849    fn test_satellite_measurement_cn0_gps_p_no_offset() {
850        let meas = SatelliteMeasurement {
851            sat_id: SatelliteId::new(Constellation::GPS, 1),
852            signal_type: SignalType::L1PY, // GPS L1P (signal number 1)
853            cn0_raw: 160,                  // 160 * 0.25 = 40 dB-Hz (no +10 dB)
854            doppler_raw: 1000,
855            doppler_valid: true,
856            lock_time_raw: 10,
857            lock_time_valid: true,
858            obs_info: 0,
859        };
860
861        assert_eq!(meas.cn0_dbhz(), 40.0);
862    }
863
864    #[test]
865    fn test_satellite_measurement_invalid_cn0() {
866        let meas = SatelliteMeasurement {
867            sat_id: SatelliteId::new(Constellation::GPS, 1),
868            signal_type: SignalType::L1CA,
869            cn0_raw: 255,
870            doppler_raw: 0,
871            doppler_valid: false,
872            lock_time_raw: 0,
873            lock_time_valid: false,
874            obs_info: 0,
875        };
876
877        assert!(!meas.cn0_valid());
878        assert_eq!(meas.cn0_dbhz_opt(), None);
879        assert_eq!(meas.cn0_dbhz(), 0.0);
880    }
881
882    #[test]
883    fn test_lock_time_encoding() {
884        // Linear encoding (seconds)
885        let meas = SatelliteMeasurement {
886            sat_id: SatelliteId::new(Constellation::GPS, 1),
887            signal_type: SignalType::L1CA,
888            cn0_raw: 160,
889            doppler_raw: 0,
890            doppler_valid: true,
891            lock_time_raw: 30,
892            lock_time_valid: true,
893            obs_info: 0,
894        };
895        assert_eq!(meas.lock_time_seconds(), 30.0);
896        assert_eq!(meas.lock_time_seconds_opt(), Some(30.0));
897
898        // Larger values are still linear, just clipped by the receiver if too large
899        let meas2 = SatelliteMeasurement {
900            lock_time_raw: 96,
901            ..meas
902        };
903        assert_eq!(meas2.lock_time_seconds(), 96.0);
904    }
905
906    #[test]
907    fn test_satellite_measurement_doppler_and_lock_time_dnu() {
908        let meas = SatelliteMeasurement {
909            sat_id: SatelliteId::new(Constellation::GPS, 1),
910            signal_type: SignalType::L1CA,
911            cn0_raw: 160,
912            doppler_raw: I32_DNU,
913            doppler_valid: false,
914            lock_time_raw: U16_DNU,
915            lock_time_valid: false,
916            obs_info: 0,
917        };
918
919        assert_eq!(meas.doppler_raw(), I32_DNU);
920        assert_eq!(meas.doppler_hz_opt(), None);
921        assert_eq!(meas.doppler_hz(), 0.0);
922        assert_eq!(meas.lock_time_raw(), U16_DNU);
923        assert_eq!(meas.lock_time_seconds_opt(), None);
924        assert_eq!(meas.lock_time_seconds(), 0.0);
925    }
926
927    #[test]
928    fn test_meas_extra_scaling() {
929        let channel = MeasExtraChannel {
930            rx_channel: 3,
931            signal_type: SignalType::L1CA,
932            signal_type_raw: 0,
933            signal_number: 0,
934            mp_correction_raw: 1234,
935            smoothing_correction_raw: -500,
936            code_var_raw: 200,
937            carrier_var_raw: 150,
938            lock_time_raw: 45,
939            cum_loss_cont: 2,
940            car_mp_correction_raw: None,
941            info: 1,
942            misc_raw: None,
943        };
944
945        assert!((channel.mp_correction_m() - 1.234).abs() < 1e-6);
946        assert!((channel.smoothing_correction_m() + 0.5).abs() < 1e-6);
947        assert!((channel.code_var_m2() - 0.02).abs() < 1e-6);
948        assert!((channel.code_var_m2_opt().unwrap() - 0.02).abs() < 1e-6);
949        assert_eq!(channel.code_var_raw(), 200);
950        assert!((channel.carrier_var_cycles2() - 0.00015).abs() < 1e-9);
951        assert!((channel.carrier_var_cycles2_opt().unwrap() - 0.00015).abs() < 1e-9);
952        assert_eq!(channel.carrier_var_raw(), 150);
953        assert_eq!(channel.lock_time_seconds_opt(), Some(45.0));
954        assert_eq!(channel.lock_time_raw(), 45);
955        assert_eq!(channel.signal_type_raw(), 0);
956        assert_eq!(channel.signal_number(), 0);
957        assert_eq!(channel.antenna_id(), 0);
958        assert_eq!(channel.car_mp_correction_raw(), None);
959        assert_eq!(channel.misc_raw(), None);
960    }
961
962    #[test]
963    fn test_meas_extra_channel_dnu_handling() {
964        let channel = MeasExtraChannel {
965            rx_channel: 3,
966            signal_type: SignalType::L1CA,
967            signal_type_raw: 0,
968            signal_number: 0,
969            mp_correction_raw: 0,
970            smoothing_correction_raw: 0,
971            code_var_raw: U16_DNU,
972            carrier_var_raw: U16_DNU,
973            lock_time_raw: U16_DNU,
974            cum_loss_cont: 0,
975            car_mp_correction_raw: None,
976            info: 0,
977            misc_raw: None,
978        };
979
980        assert_eq!(channel.code_var_raw(), U16_DNU);
981        assert_eq!(channel.code_var_m2_opt(), None);
982        assert_eq!(channel.code_var_m2(), 0.0);
983        assert_eq!(channel.carrier_var_raw(), U16_DNU);
984        assert_eq!(channel.carrier_var_cycles2_opt(), None);
985        assert_eq!(channel.carrier_var_cycles2(), 0.0);
986        assert_eq!(channel.lock_time_seconds_opt(), None);
987        assert_eq!(channel.lock_time_seconds(), 0.0);
988    }
989
990    #[test]
991    fn test_meas_extra_doppler_factor() {
992        let block = MeasExtraBlock {
993            tow_ms: 1000,
994            wnc: 2000,
995            n: 0,
996            sb_length: 14,
997            doppler_var_factor: 1.5,
998            channels: Vec::new(),
999        };
1000
1001        assert_eq!(block.tow_seconds(), 1.0);
1002        assert_eq!(block.wnc(), 2000);
1003        assert!((block.doppler_var_factor() - 1.5).abs() < 1e-6);
1004        assert_eq!(block.num_channels(), 0);
1005    }
1006
1007    #[test]
1008    fn test_iq_corr_parse() {
1009        let mut data = vec![0u8; 32];
1010        data[6..10].copy_from_slice(&5000u32.to_le_bytes());
1011        data[10..12].copy_from_slice(&2100u16.to_le_bytes());
1012        data[12] = 1; // N
1013        data[13] = 8; // SBLength
1014        data[14] = 20; // CorrDuration 20ms
1015        data[15] = 0; // CumClkJumps
1016        data[16] = 2; // RxChannel
1017        data[17] = 0; // Type
1018        data[18] = 7; // SVID
1019        data[19] = 10; // CorrIQ_MSB
1020        data[20] = 5; // CorrI_LSB
1021        data[21] = 3; // CorrQ_LSB
1022        data[22..24].copy_from_slice(&1000u16.to_le_bytes()); // CarrierPhaseLSB
1023
1024        let header = SbfHeader {
1025            crc: 0,
1026            block_id: block_ids::IQ_CORR,
1027            block_rev: 0,
1028            length: 32,
1029            tow_ms: 5000,
1030            wnc: 2100,
1031        };
1032        let block = IqCorrBlock::parse(&header, &data).unwrap();
1033        assert_eq!(block.tow_seconds(), 5.0);
1034        assert_eq!(block.wnc(), 2100);
1035        assert_eq!(block.n, 1);
1036        assert_eq!(block.corr_duration, 20);
1037        assert_eq!(block.num_channels(), 1);
1038        assert_eq!(block.channels[0].rx_channel, 2);
1039        assert_eq!(block.channels[0].svid, 7);
1040        assert_eq!(block.channels[0].carrier_phase_lsb, 1000);
1041    }
1042
1043    #[test]
1044    fn test_end_of_meas_accessors() {
1045        let end = EndOfMeasBlock {
1046            tow_ms: 2500,
1047            wnc: 123,
1048        };
1049        assert_eq!(end.tow_ms(), 2500);
1050        assert_eq!(end.wnc(), 123);
1051        assert_eq!(end.tow_seconds(), 2.5);
1052    }
1053
1054    #[test]
1055    fn test_meas_extra_parse() {
1056        let mut data = vec![0u8; 18 + 14];
1057        data[12] = 1; // N
1058        data[13] = 14; // SBLength
1059        data[14..18].copy_from_slice(&1.25_f32.to_le_bytes());
1060
1061        let offset = 18;
1062        data[offset] = 5; // RxChannel
1063        data[offset + 1] = 0; // Signal type (L1CA)
1064        data[offset + 2..offset + 4].copy_from_slice(&1000_i16.to_le_bytes());
1065        data[offset + 4..offset + 6].copy_from_slice(&(-200_i16).to_le_bytes());
1066        data[offset + 6..offset + 8].copy_from_slice(&500_u16.to_le_bytes());
1067        data[offset + 8..offset + 10].copy_from_slice(&250_u16.to_le_bytes());
1068        data[offset + 10..offset + 12].copy_from_slice(&60_u16.to_le_bytes());
1069        data[offset + 12] = 3;
1070        data[offset + 13] = 0xA5;
1071
1072        let header = SbfHeader {
1073            crc: 0,
1074            block_id: block_ids::MEAS_EXTRA,
1075            block_rev: 0,
1076            length: (data.len() + 2) as u16,
1077            tow_ms: 123456,
1078            wnc: 321,
1079        };
1080
1081        let parsed = MeasExtraBlock::parse(&header, &data).expect("parse");
1082        assert_eq!(parsed.tow_ms(), 123456);
1083        assert_eq!(parsed.wnc(), 321);
1084        assert_eq!(parsed.n, 1);
1085        assert_eq!(parsed.sb_length, 14);
1086        assert!((parsed.doppler_var_factor() - 1.25).abs() < 1e-6);
1087        assert_eq!(parsed.num_channels(), 1);
1088
1089        let ch = &parsed.channels[0];
1090        assert_eq!(ch.rx_channel, 5);
1091        assert_eq!(ch.signal_type, SignalType::L1CA);
1092        assert_eq!(ch.signal_type_raw(), 0);
1093        assert_eq!(ch.signal_number(), 0);
1094        assert_eq!(ch.antenna_id(), 0);
1095        assert!((ch.mp_correction_m() - 1.0).abs() < 1e-6);
1096        assert!((ch.smoothing_correction_m() + 0.2).abs() < 1e-6);
1097        assert!((ch.code_var_m2() - 0.05).abs() < 1e-6);
1098        assert!((ch.carrier_var_cycles2() - 0.00025).abs() < 1e-9);
1099        assert_eq!(ch.lock_time_raw(), 60);
1100        assert_eq!(ch.cum_loss_cont, 3);
1101        assert_eq!(ch.car_mp_correction_raw(), None);
1102        assert_eq!(ch.info, 0xA5);
1103        assert_eq!(ch.misc_raw(), None);
1104    }
1105
1106    #[test]
1107    fn test_meas_extra_parse_extended_type_and_misc() {
1108        let mut data = vec![0u8; 18 + 16];
1109        data[12] = 1; // N
1110        data[13] = 16; // SBLength (includes CarMPCorr, Info, Misc)
1111        data[14..18].copy_from_slice(&2.0_f32.to_le_bytes());
1112
1113        let offset = 18;
1114        data[offset] = 7; // RxChannel
1115        data[offset + 1] = 0x5F; // antenna ID 2 (bits 5-7), SigIdxLo = 31
1116        data[offset + 2..offset + 4].copy_from_slice(&0_i16.to_le_bytes());
1117        data[offset + 4..offset + 6].copy_from_slice(&0_i16.to_le_bytes());
1118        data[offset + 6..offset + 8].copy_from_slice(&100_u16.to_le_bytes());
1119        data[offset + 8..offset + 10].copy_from_slice(&1024_u16.to_le_bytes());
1120        data[offset + 10..offset + 12].copy_from_slice(&11_u16.to_le_bytes());
1121        data[offset + 12] = 9; // CumLossCont
1122        data[offset + 13] = (-64_i8) as u8; // CarMPCorr
1123        data[offset + 14] = 0xB4; // Info
1124        data[offset + 15] = 0x33; // Misc: CN0HighRes=3, SigIdxHi=6 => signal number 38
1125
1126        let header = SbfHeader {
1127            crc: 0,
1128            block_id: block_ids::MEAS_EXTRA,
1129            block_rev: 3,
1130            length: (data.len() + 2) as u16,
1131            tow_ms: 500,
1132            wnc: 2222,
1133        };
1134
1135        let parsed = MeasExtraBlock::parse(&header, &data).expect("parse");
1136        assert_eq!(parsed.num_channels(), 1);
1137
1138        let ch = &parsed.channels[0];
1139        assert_eq!(ch.rx_channel, 7);
1140        assert_eq!(ch.signal_type_raw(), 0x5F);
1141        assert_eq!(ch.antenna_id(), 2);
1142        assert_eq!(ch.signal_number(), 38);
1143        assert_eq!(ch.signal_type, SignalType::QZSSL1CB);
1144        assert_eq!(ch.cum_loss_cont, 9);
1145        assert_eq!(ch.info, 0xB4);
1146        assert_eq!(ch.car_mp_correction_raw(), Some(-64));
1147        assert_eq!(ch.misc_raw(), Some(0x33));
1148        assert!((ch.car_mp_correction_cycles().expect("carmp") + 0.125).abs() < 1e-9);
1149        assert!((ch.cn0_high_res_dbhz_offset().expect("cn0 hi-res") - 0.09375).abs() < 1e-9);
1150    }
1151
1152    #[test]
1153    fn test_end_of_meas_parse() {
1154        let data = vec![0u8; 12];
1155        let header = SbfHeader {
1156            crc: 0,
1157            block_id: block_ids::END_OF_MEAS,
1158            block_rev: 0,
1159            length: (data.len() + 2) as u16,
1160            tow_ms: 1000,
1161            wnc: 45,
1162        };
1163
1164        let parsed = EndOfMeasBlock::parse(&header, &data).expect("parse");
1165        assert_eq!(parsed.tow_ms(), 1000);
1166        assert_eq!(parsed.wnc(), 45);
1167    }
1168}