Skip to main content

sbf_tools/blocks/
meas3.rs

1//! Meas3 measurement blocks (4109–4113)
2//!
3//! Layout follows Septentrio SBF definitions: `Meas3Ranges` holds packed `M3SatData`
4//! sub-blocks; the extension blocks share a `Flags` field (antenna index in bits 0–2)
5//! followed by opaque payload bytes tied to the paired `Meas3Ranges` epoch.
6
7use crate::error::{SbfError, SbfResult};
8use crate::header::SbfHeader;
9
10use super::block_ids;
11use super::SbfBlockParse;
12
13// ============================================================================
14// Meas3Ranges (4109)
15// ============================================================================
16
17/// Meas3Ranges block — code, phase, and CN0 (packed satellite data).
18#[derive(Debug, Clone)]
19pub struct Meas3RangesBlock {
20    tow_ms: u32,
21    wnc: u16,
22    pub common_flags: u8,
23    pub cum_clk_jumps: u8,
24    pub constellations: u16,
25    pub misc: u8,
26    pub reserved: u8,
27    /// Raw `Data` field: packed [`M3SatData`](https://github.com/PointOneNav/polaris/blob/master/third_party/septentrio/sbfdef.h) bytes.
28    pub data: Vec<u8>,
29}
30
31impl Meas3RangesBlock {
32    pub fn tow_seconds(&self) -> f64 {
33        self.tow_ms as f64 * 0.001
34    }
35
36    pub fn tow_ms(&self) -> u32 {
37        self.tow_ms
38    }
39
40    pub fn wnc(&self) -> u16 {
41        self.wnc
42    }
43
44    /// Antenna index from `Misc` bits 0–2 (Septentrio convention).
45    pub fn antenna_id(&self) -> u8 {
46        self.misc & 0x07
47    }
48
49    pub fn reference_epoch_interval_ms(&self) -> u32 {
50        match self.misc >> 4 {
51            0 => 1,
52            1 => 500,
53            2 => 1000,
54            3 => 2000,
55            4 => 5000,
56            5 => 10_000,
57            6 => 15_000,
58            7 => 30_000,
59            8 => 60_000,
60            9 => 120_000,
61            _ => 1,
62        }
63    }
64
65    pub fn is_reference_epoch(&self) -> bool {
66        self.tow_ms.checked_rem(self.reference_epoch_interval_ms()) == Some(0)
67    }
68
69    pub fn reference_epoch_contains_pr_rate(&self) -> bool {
70        (self.misc & 0x08) != 0
71    }
72
73    pub fn has_scrambled_measurements(&self) -> bool {
74        (self.common_flags & 0x80) != 0
75    }
76}
77
78impl SbfBlockParse for Meas3RangesBlock {
79    const BLOCK_ID: u16 = block_ids::MEAS3_RANGES;
80
81    fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
82        let full_len = header.length as usize;
83        if data.len() < full_len.saturating_sub(2) {
84            return Err(SbfError::IncompleteBlock {
85                needed: full_len,
86                have: data.len() + 2,
87            });
88        }
89        if data.len() < 18 {
90            return Err(SbfError::ParseError("Meas3Ranges too short".into()));
91        }
92
93        let common_flags = data[12];
94        let cum_clk_jumps = data[13];
95        let constellations = u16::from_le_bytes([data[14], data[15]]);
96        let misc = data[16];
97        let reserved = data[17];
98        let payload = data[18..].to_vec();
99
100        Ok(Self {
101            tow_ms: header.tow_ms,
102            wnc: header.wnc,
103            common_flags,
104            cum_clk_jumps,
105            constellations,
106            misc,
107            reserved,
108            data: payload,
109        })
110    }
111}
112
113// ============================================================================
114// Meas3 extension blocks (4110–4113): Flags + payload
115// ============================================================================
116
117macro_rules! impl_meas3_ext {
118    (
119        $name:ident,
120        $block_id:expr,
121        $doc:literal
122    ) => {
123        #[doc = $doc]
124        #[derive(Debug, Clone)]
125        pub struct $name {
126            tow_ms: u32,
127            wnc: u16,
128            pub flags: u8,
129            pub data: Vec<u8>,
130        }
131
132        impl $name {
133            pub fn tow_seconds(&self) -> f64 {
134                self.tow_ms as f64 * 0.001
135            }
136
137            pub fn tow_ms(&self) -> u32 {
138                self.tow_ms
139            }
140
141            pub fn wnc(&self) -> u16 {
142                self.wnc
143            }
144
145            /// Antenna index from `Flags` bits 0–2.
146            pub fn antenna_id(&self) -> u8 {
147                (self.flags & 0x07) as u8
148            }
149        }
150
151        impl SbfBlockParse for $name {
152            const BLOCK_ID: u16 = $block_id;
153
154            fn parse(header: &SbfHeader, data: &[u8]) -> SbfResult<Self> {
155                let full_len = header.length as usize;
156                if data.len() < full_len.saturating_sub(2) {
157                    return Err(SbfError::IncompleteBlock {
158                        needed: full_len,
159                        have: data.len() + 2,
160                    });
161                }
162                if data.len() < 13 {
163                    return Err(SbfError::ParseError(
164                        concat!(stringify!($name), " too short").into(),
165                    ));
166                }
167
168                let flags = data[12];
169                let payload = data[13..].to_vec();
170
171                Ok(Self {
172                    tow_ms: header.tow_ms,
173                    wnc: header.wnc,
174                    flags,
175                    data: payload,
176                })
177            }
178        }
179    };
180}
181
182impl_meas3_ext!(
183    Meas3Cn0HiResBlock,
184    block_ids::MEAS3_CN0_HI_RES,
185    "Meas3CN0HiRes — fractional C/N0 extension (paired with Meas3Ranges)."
186);
187
188impl_meas3_ext!(
189    Meas3DopplerBlock,
190    block_ids::MEAS3_DOPPLER,
191    "Meas3Doppler — Doppler extension (paired with Meas3Ranges)."
192);
193
194impl_meas3_ext!(
195    Meas3PpBlock,
196    block_ids::MEAS3_PP,
197    "Meas3PP — post-processing flags extension (paired with Meas3Ranges)."
198);
199
200impl_meas3_ext!(
201    Meas3MpBlock,
202    block_ids::MEAS3_MP,
203    "Meas3MP — multipath correction extension (paired with Meas3Ranges)."
204);
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::blocks::SbfBlock;
210
211    /// Full SBF block: `[sync][CRC][ID][Len][TOW][WNc][body...]`. `body` starts at index 14.
212    fn build_block(block_id: u16, block_rev: u8, body: &[u8]) -> Vec<u8> {
213        let block_data_len = 12 + body.len();
214        let mut total_len = (2 + block_data_len) as u16;
215        while (total_len as usize & 0x03) != 0 {
216            total_len += 1;
217        }
218        let mut data = vec![0u8; total_len as usize];
219        data[0] = 0x24;
220        data[1] = 0x40;
221
222        let id_rev = block_id | ((block_rev as u16 & 0x07) << 13);
223        data[4..6].copy_from_slice(&id_rev.to_le_bytes());
224        data[6..8].copy_from_slice(&total_len.to_le_bytes());
225        data[8..12].copy_from_slice(&123_456u32.to_le_bytes());
226        data[12..14].copy_from_slice(&2150u16.to_le_bytes());
227        data[14..14 + body.len()].copy_from_slice(body);
228        data
229    }
230
231    #[test]
232    fn meas3_ranges_parses_header_and_payload() {
233        // 6-byte Meas3 fixed header + 4-byte Data so block length stays 4-byte aligned without extra padding bytes.
234        let body = [
235            0u8, 0, 0, 0, // CommonFlags, CumClkJumps, Constellations u16
236            0x03, 0xAA, // misc, reserved
237            0x01, 0x02, 0x03, 0x04, // payload
238        ];
239        let raw = build_block(block_ids::MEAS3_RANGES, 0, &body);
240
241        let (block, consumed) = SbfBlock::parse(&raw).unwrap();
242        assert_eq!(consumed, raw.len());
243        match block {
244            SbfBlock::Meas3Ranges(m) => {
245                assert_eq!(m.tow_ms(), 123_456);
246                assert_eq!(m.wnc(), 2150);
247                assert_eq!(m.common_flags, 0);
248                assert_eq!(m.cum_clk_jumps, 0);
249                assert_eq!(m.constellations, 0);
250                assert_eq!(m.misc, 0x03);
251                assert_eq!(m.reserved, 0xAA);
252                assert_eq!(m.antenna_id(), 0x03);
253                assert_eq!(m.data, vec![0x01, 0x02, 0x03, 0x04]);
254            }
255            other => panic!("expected Meas3Ranges, got {:?}", other),
256        }
257    }
258
259    #[test]
260    fn meas3_cn0_hi_res_parses_flags_and_bytes() {
261        // Use a 5-byte payload so the block stays 4-byte aligned without trailing SBF padding.
262        let mut ext = vec![0u8; 1 + 5];
263        ext[0] = 0x05;
264        ext[1..].copy_from_slice(&[0xAAu8, 10, 20, 30, 40]);
265        let raw = build_block(block_ids::MEAS3_CN0_HI_RES, 1, &ext);
266
267        let (block, _) = SbfBlock::parse(&raw).unwrap();
268        match block {
269            SbfBlock::Meas3Cn0HiRes(m) => {
270                assert_eq!(m.flags, 0x05);
271                assert_eq!(m.antenna_id(), 0x05);
272                assert_eq!(m.data, vec![0xAA, 10, 20, 30, 40]);
273            }
274            other => panic!("expected Meas3Cn0HiRes, got {:?}", other),
275        }
276    }
277
278    #[test]
279    fn meas3_doppler_pp_mp_parse() {
280        for (id, label) in [
281            (block_ids::MEAS3_DOPPLER, "doppler"),
282            (block_ids::MEAS3_PP, "pp"),
283            (block_ids::MEAS3_MP, "mp"),
284        ] {
285            // Use a 5-byte payload so the block stays 4-byte aligned without trailing SBF padding.
286            let mut ext = vec![0u8; 1 + 5];
287            ext[0] = 0x02;
288            ext[1..].copy_from_slice(&[0xCCu8, 1, 2, 3, 4]);
289            let raw = build_block(id, 0, &ext);
290
291            let (block, _) = SbfBlock::parse(&raw).unwrap();
292            match (label, block) {
293                ("doppler", SbfBlock::Meas3Doppler(m)) => {
294                    assert_eq!(m.antenna_id(), 2);
295                    assert_eq!(m.data, vec![0xCC, 1, 2, 3, 4]);
296                }
297                ("pp", SbfBlock::Meas3Pp(m)) => {
298                    assert_eq!(m.data, vec![0xCC, 1, 2, 3, 4]);
299                }
300                ("mp", SbfBlock::Meas3Mp(m)) => {
301                    assert_eq!(m.data, vec![0xCC, 1, 2, 3, 4]);
302                }
303                (_, other) => panic!("{}: unexpected {:?}", label, other),
304            }
305        }
306    }
307}