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