Skip to main content

sidereon_core/rtcm/
msm.rs

1//! RTCM 3 Multiple Signal Message (MSM) observations, types MSM4 and MSM7.
2//!
3//! The MSM family carries multi-constellation, multi-signal pseudorange,
4//! carrier-phase, phase-range-rate, lock-time, and carrier-to-noise observations
5//! in one compact message (RTCM 10403.3, Section 3.5). This module decodes and
6//! re-encodes the two highest-value members:
7//!
8//!   * **MSM4** - full pseudoranges and phase ranges with the standard
9//!     resolution (message numbers 1074 / 1084 / 1094 / 1124, and the SBAS /
10//!     QZSS / NavIC siblings).
11//!   * **MSM7** - full pseudoranges, phase ranges, phase-range-rates, and
12//!     extended resolution (message numbers 1077 / 1087 / 1097 / 1127, and the
13//!     siblings).
14//!
15//! The message number alone fixes both the constellation and the MSM type via
16//! the regular RTCM numbering (`107x` GPS, `108x` GLONASS, `109x` Galileo, `110x`
17//! SBAS, `111x` QZSS, `112x` BeiDou, `113x` NavIC; the trailing digit is the MSM
18//! type). Other MSM types (1, 2, 3, 5, 6) are left to the caller as
19//! [`super::Message::Unsupported`].
20//!
21//! ## Field-major packing
22//!
23//! MSM does not store one record per observation. The body is a common header,
24//! then the satellite block with every field laid out column-first (all the
25//! rough-range integers, then all the rough-range remainders, ...), then the
26//! signal block laid out the same way over the active cells. The cell set is the
27//! cross product of the satellite mask and signal mask, pruned by the cell mask.
28//!
29//! ## Canonical representation
30//!
31//! Field values are stored as the raw transmitted integers (the
32//! `DFxxx`-numbered quantities), not pre-scaled engineering units, so the IR is
33//! an exact, loss-free image of the wire bits and `decode` -> `encode`
34//! round-trips byte-for-byte. Each accessor documents the standard scale factor
35//! so a consumer can recover meters, milliseconds, or dB-Hz when needed.
36
37use crate::error::{Error, Result};
38use crate::id::GnssSystem;
39
40use super::bits::{BitReader, BitWriter, OutOfInput};
41use super::DecodeResult;
42
43/// Which MSM variant a message is.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum MsmKind {
46    /// MSM4: full pseudorange + phase range, standard resolution.
47    Msm4,
48    /// MSM7: full pseudorange + phase range + phase-range-rate, extended
49    /// resolution.
50    Msm7,
51}
52
53/// The MSM message header, common to every MSM type (RTCM 10403.3 Table 3.5-78).
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub struct MsmHeader {
56    /// Reference station identifier (DF003).
57    pub reference_station_id: u16,
58    /// GNSS epoch time, the raw 30-bit field. Its meaning is constellation
59    /// specific: milliseconds of the GPS/Galileo/BeiDou week, or, for GLONASS,
60    /// a 3-bit day-of-week joined with a 27-bit millisecond-of-day count.
61    pub epoch_time: u32,
62    /// Multiple message bit (DF393): more MSM messages share this epoch.
63    pub multiple_message: bool,
64    /// Issue of data station (DF409).
65    pub iods: u8,
66    /// Reserved field DF001 (7 bits), preserved for exact round-trip.
67    pub reserved: u8,
68    /// Clock steering indicator (DF411).
69    pub clock_steering: u8,
70    /// External clock indicator (DF412).
71    pub external_clock: u8,
72    /// Divergence-free smoothing indicator (DF417).
73    pub divergence_free_smoothing: bool,
74    /// Smoothing interval (DF418).
75    pub smoothing_interval: u8,
76}
77
78/// Per-satellite data for one MSM satellite.
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub struct MsmSatellite {
81    /// Satellite identifier: the 1-based index of the set bit in the satellite
82    /// mask (DF394). For most constellations this equals the PRN / slot number.
83    pub id: u8,
84    /// Rough range, whole milliseconds (DF397). The value 255 marks the
85    /// satellite range as invalid.
86    pub rough_range_ms: u8,
87    /// Rough range remainder, in units of 1/1024 ms (DF398, scale 2^-10 ms).
88    pub rough_range_mod1: u16,
89    /// Extended satellite info (DF419), present only in MSM7. For GLONASS this
90    /// is the frequency channel number.
91    pub extended_info: Option<u8>,
92    /// Rough phase-range-rate in whole m/s (DF399), present only in MSM7.
93    pub rough_phase_range_rate_m_s: Option<i16>,
94}
95
96/// Per-cell signal data for one active (satellite, signal) pair.
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98pub struct MsmSignal {
99    /// Owning satellite id (1-based satellite-mask index).
100    pub satellite_id: u8,
101    /// Signal id: the 1-based index of the set bit in the signal mask (DF395).
102    pub signal_id: u8,
103    /// Fine pseudorange (DF400 for MSM4, scale 2^-24 ms; DF405 for MSM7, scale
104    /// 2^-29 ms). The MSM4 invalid marker is -16384.
105    pub fine_pseudorange: i32,
106    /// Fine phase range (DF401 for MSM4, scale 2^-29 ms; DF406 for MSM7, scale
107    /// 2^-31 ms).
108    pub fine_phase_range: i32,
109    /// Phase-range lock-time indicator (DF402, 4-bit, for MSM4; DF407, 10-bit,
110    /// for MSM7).
111    pub lock_time_indicator: u16,
112    /// Half-cycle ambiguity indicator (DF420).
113    pub half_cycle_ambiguity: bool,
114    /// Carrier-to-noise density ratio (DF403, 1 dB-Hz, for MSM4; DF408, scale
115    /// 2^-4 dB-Hz, for MSM7).
116    pub cnr: u16,
117    /// Fine phase-range-rate (DF404, scale 0.0001 m/s), present only in MSM7.
118    pub fine_phase_range_rate: Option<i16>,
119}
120
121impl MsmSignal {
122    /// Minimum continuous-lock time encoded by this signal's lock indicator.
123    ///
124    /// The caller supplies the owning message kind because MSM4 carries DF402
125    /// while MSM7 carries DF407. Returns `None` for values outside the
126    /// indicator's defined range.
127    pub fn minimum_lock_time_ms(&self, kind: MsmKind) -> Option<u32> {
128        super::lli::minimum_lock_time_ms(kind, self.lock_time_indicator)
129    }
130}
131
132/// A decoded MSM4 or MSM7 observation message.
133#[derive(Clone, Debug, PartialEq, Eq)]
134pub struct MsmMessage {
135    /// The message number (e.g. 1077).
136    pub message_number: u16,
137    /// The constellation, derived from the message number.
138    pub system: GnssSystem,
139    /// The MSM variant (MSM4 or MSM7).
140    pub kind: MsmKind,
141    /// Common MSM header.
142    pub header: MsmHeader,
143    /// Active satellites, in ascending id order.
144    pub satellites: Vec<MsmSatellite>,
145    /// Active signal cells, in satellite-major then signal order.
146    pub signals: Vec<MsmSignal>,
147}
148
149/// Map an MSM message number to its constellation and (supported) MSM type.
150///
151/// Returns `None` for numbers outside the MSM range and for MSM types this
152/// module does not decode (1, 2, 3, 5, 6).
153pub(crate) fn msm_kind(message_number: u16) -> Option<(GnssSystem, MsmKind)> {
154    if !(1071..=1137).contains(&message_number) {
155        return None;
156    }
157    let group = (message_number - 1071) / 10;
158    let system = match group {
159        0 => GnssSystem::Gps,
160        1 => GnssSystem::Glonass,
161        2 => GnssSystem::Galileo,
162        3 => GnssSystem::Sbas,
163        4 => GnssSystem::Qzss,
164        5 => GnssSystem::BeiDou,
165        6 => GnssSystem::Navic,
166        _ => return None,
167    };
168    let kind = match message_number % 10 {
169        4 => MsmKind::Msm4,
170        7 => MsmKind::Msm7,
171        _ => return None,
172    };
173    Some((system, kind))
174}
175
176/// True if `message_number` is an MSM type decoded by this module.
177pub(crate) fn is_supported_msm(message_number: u16) -> bool {
178    msm_kind(message_number).is_some()
179}
180
181impl MsmMessage {
182    /// Decode an MSM4 / MSM7 message body (without the transport frame).
183    pub fn decode(body: &[u8]) -> Result<Self> {
184        Self::decode_inner(body).map_err(Into::into)
185    }
186
187    pub(crate) fn decode_inner(body: &[u8]) -> DecodeResult<Self> {
188        let mut r = BitReader::new(body);
189        let message_number = r.u(12)? as u16;
190        let (system, kind) = msm_kind(message_number).ok_or_else(|| {
191            Error::Parse(format!(
192                "message {message_number} is not a supported MSM4/MSM7 type"
193            ))
194        })?;
195
196        let header = MsmHeader {
197            reference_station_id: r.u(12)? as u16,
198            epoch_time: r.u(30)? as u32,
199            multiple_message: r.flag()?,
200            iods: r.u(3)? as u8,
201            reserved: r.u(7)? as u8,
202            clock_steering: r.u(2)? as u8,
203            external_clock: r.u(2)? as u8,
204            divergence_free_smoothing: r.flag()?,
205            smoothing_interval: r.u(3)? as u8,
206        };
207
208        let satellite_mask = r.u(64)?;
209        let signal_mask = r.u(32)? as u32;
210        let sat_ids = set_bits(satellite_mask, 64);
211        let sig_ids = set_bits_u32(signal_mask);
212
213        let nsat = sat_ids.len();
214        let nsig = sig_ids.len();
215
216        // Cell mask: nsat * nsig bits, satellite-major.
217        let mut cell_present = Vec::with_capacity(nsat * nsig);
218        for _ in 0..nsat * nsig {
219            cell_present.push(r.flag()?);
220        }
221
222        // Satellite block (column-major).
223        let mut rough_range_ms = Vec::with_capacity(nsat);
224        for _ in 0..nsat {
225            rough_range_ms.push(r.u(8)? as u8);
226        }
227        let extended_info = if kind == MsmKind::Msm7 {
228            let mut v = Vec::with_capacity(nsat);
229            for _ in 0..nsat {
230                v.push(Some(r.u(4)? as u8));
231            }
232            v
233        } else {
234            vec![None; nsat]
235        };
236        let mut rough_range_mod1 = Vec::with_capacity(nsat);
237        for _ in 0..nsat {
238            rough_range_mod1.push(r.u(10)? as u16);
239        }
240        let rough_prr = if kind == MsmKind::Msm7 {
241            let mut v = Vec::with_capacity(nsat);
242            for _ in 0..nsat {
243                v.push(Some(r.i(14)? as i16));
244            }
245            v
246        } else {
247            vec![None; nsat]
248        };
249
250        let satellites: Vec<MsmSatellite> = (0..nsat)
251            .map(|s| MsmSatellite {
252                id: sat_ids[s],
253                rough_range_ms: rough_range_ms[s],
254                rough_range_mod1: rough_range_mod1[s],
255                extended_info: extended_info[s],
256                rough_phase_range_rate_m_s: rough_prr[s],
257            })
258            .collect();
259
260        // The ordered list of active (satellite, signal) cells.
261        let cells = active_cells(&sat_ids, &sig_ids, &cell_present);
262        let ncell = cells.len();
263
264        // Signal block (column-major over cells).
265        let signals = match kind {
266            MsmKind::Msm4 => {
267                let fine_pr = read_vec(&mut r, ncell, |rr| rr.i(15).map(|v| v as i32))?;
268                let fine_ph = read_vec(&mut r, ncell, |rr| rr.i(22).map(|v| v as i32))?;
269                let lock = read_vec(&mut r, ncell, |rr| rr.u(4).map(|v| v as u16))?;
270                let half = read_vec(&mut r, ncell, |rr| rr.flag())?;
271                let cnr = read_vec(&mut r, ncell, |rr| rr.u(6).map(|v| v as u16))?;
272                cells
273                    .iter()
274                    .enumerate()
275                    .map(|(c, &(sat, sig))| MsmSignal {
276                        satellite_id: sat,
277                        signal_id: sig,
278                        fine_pseudorange: fine_pr[c],
279                        fine_phase_range: fine_ph[c],
280                        lock_time_indicator: lock[c],
281                        half_cycle_ambiguity: half[c],
282                        cnr: cnr[c],
283                        fine_phase_range_rate: None,
284                    })
285                    .collect()
286            }
287            MsmKind::Msm7 => {
288                let fine_pr = read_vec(&mut r, ncell, |rr| rr.i(20).map(|v| v as i32))?;
289                let fine_ph = read_vec(&mut r, ncell, |rr| rr.i(24).map(|v| v as i32))?;
290                let lock = read_vec(&mut r, ncell, |rr| rr.u(10).map(|v| v as u16))?;
291                let half = read_vec(&mut r, ncell, |rr| rr.flag())?;
292                let cnr = read_vec(&mut r, ncell, |rr| rr.u(10).map(|v| v as u16))?;
293                let fine_prr = read_vec(&mut r, ncell, |rr| rr.i(15).map(|v| v as i16))?;
294                cells
295                    .iter()
296                    .enumerate()
297                    .map(|(c, &(sat, sig))| MsmSignal {
298                        satellite_id: sat,
299                        signal_id: sig,
300                        fine_pseudorange: fine_pr[c],
301                        fine_phase_range: fine_ph[c],
302                        lock_time_indicator: lock[c],
303                        half_cycle_ambiguity: half[c],
304                        cnr: cnr[c],
305                        fine_phase_range_rate: Some(fine_prr[c]),
306                    })
307                    .collect()
308            }
309        };
310
311        Ok(Self {
312            message_number,
313            system,
314            kind,
315            header,
316            satellites,
317            signals,
318        })
319    }
320
321    /// Encode this message back into an MSM body (without the transport frame).
322    pub fn encode(&self) -> Vec<u8> {
323        let mut w = BitWriter::new();
324        w.push_u(u64::from(self.message_number), 12);
325        w.push_u(u64::from(self.header.reference_station_id), 12);
326        w.push_u(u64::from(self.header.epoch_time), 30);
327        w.push_flag(self.header.multiple_message);
328        w.push_u(u64::from(self.header.iods), 3);
329        w.push_u(u64::from(self.header.reserved), 7);
330        w.push_u(u64::from(self.header.clock_steering), 2);
331        w.push_u(u64::from(self.header.external_clock), 2);
332        w.push_flag(self.header.divergence_free_smoothing);
333        w.push_u(u64::from(self.header.smoothing_interval), 3);
334
335        // Reconstruct the satellite ids (sorted) and the satellite mask.
336        let mut sat_ids: Vec<u8> = self.satellites.iter().map(|s| s.id).collect();
337        sat_ids.sort_unstable();
338        let mut satellite_mask: u64 = 0;
339        for &id in &sat_ids {
340            satellite_mask |= 1u64 << (64 - u32::from(id));
341        }
342
343        // Signal ids = sorted union of signals referenced by the cells.
344        let mut sig_ids: Vec<u8> = self.signals.iter().map(|s| s.signal_id).collect();
345        sig_ids.sort_unstable();
346        sig_ids.dedup();
347        let mut signal_mask: u32 = 0;
348        for &id in &sig_ids {
349            signal_mask |= 1u32 << (32 - u32::from(id));
350        }
351
352        w.push_u(satellite_mask, 64);
353        w.push_u(u64::from(signal_mask), 32);
354
355        // Cell mask, satellite-major, plus the ordered active cell list.
356        let mut ordered_cells: Vec<(u8, u8)> = Vec::new();
357        for &sat in &sat_ids {
358            for &sig in &sig_ids {
359                let present = self
360                    .signals
361                    .iter()
362                    .any(|s| s.satellite_id == sat && s.signal_id == sig);
363                w.push_flag(present);
364                if present {
365                    ordered_cells.push((sat, sig));
366                }
367            }
368        }
369
370        // Satellite block, column-major, in the same sorted id order.
371        let sat_by_id = |id: u8| self.satellites.iter().find(|s| s.id == id);
372        for &id in &sat_ids {
373            let sat = sat_by_id(id);
374            w.push_u(u64::from(sat.map_or(0, |s| s.rough_range_ms)), 8);
375        }
376        if self.kind == MsmKind::Msm7 {
377            for &id in &sat_ids {
378                let ext = sat_by_id(id).and_then(|s| s.extended_info).unwrap_or(0);
379                w.push_u(u64::from(ext), 4);
380            }
381        }
382        for &id in &sat_ids {
383            let sat = sat_by_id(id);
384            w.push_u(u64::from(sat.map_or(0, |s| s.rough_range_mod1)), 10);
385        }
386        if self.kind == MsmKind::Msm7 {
387            for &id in &sat_ids {
388                let prr = sat_by_id(id)
389                    .and_then(|s| s.rough_phase_range_rate_m_s)
390                    .unwrap_or(0);
391                w.push_i(i64::from(prr), 14);
392            }
393        }
394
395        // Signal block, column-major over the ordered cells.
396        let cell_signal = |sat: u8, sig: u8| {
397            self.signals
398                .iter()
399                .find(|s| s.satellite_id == sat && s.signal_id == sig)
400                .expect("ordered cell must reference an existing signal")
401        };
402        let ordered: Vec<&MsmSignal> = ordered_cells
403            .iter()
404            .map(|&(sat, sig)| cell_signal(sat, sig))
405            .collect();
406
407        match self.kind {
408            MsmKind::Msm4 => {
409                for s in &ordered {
410                    w.push_i(i64::from(s.fine_pseudorange), 15);
411                }
412                for s in &ordered {
413                    w.push_i(i64::from(s.fine_phase_range), 22);
414                }
415                for s in &ordered {
416                    w.push_u(u64::from(s.lock_time_indicator), 4);
417                }
418                for s in &ordered {
419                    w.push_flag(s.half_cycle_ambiguity);
420                }
421                for s in &ordered {
422                    w.push_u(u64::from(s.cnr), 6);
423                }
424            }
425            MsmKind::Msm7 => {
426                for s in &ordered {
427                    w.push_i(i64::from(s.fine_pseudorange), 20);
428                }
429                for s in &ordered {
430                    w.push_i(i64::from(s.fine_phase_range), 24);
431                }
432                for s in &ordered {
433                    w.push_u(u64::from(s.lock_time_indicator), 10);
434                }
435                for s in &ordered {
436                    w.push_flag(s.half_cycle_ambiguity);
437                }
438                for s in &ordered {
439                    w.push_u(u64::from(s.cnr), 10);
440                }
441                for s in &ordered {
442                    w.push_i(i64::from(s.fine_phase_range_rate.unwrap_or(0)), 15);
443                }
444            }
445        }
446
447        w.into_bytes()
448    }
449}
450
451/// Read `n` values with `f`, collecting into a vector.
452fn read_vec<T>(
453    r: &mut BitReader<'_>,
454    n: usize,
455    mut f: impl FnMut(&mut BitReader<'_>) -> std::result::Result<T, OutOfInput>,
456) -> std::result::Result<Vec<T>, OutOfInput> {
457    let mut v = Vec::with_capacity(n);
458    for _ in 0..n {
459        v.push(f(r)?);
460    }
461    Ok(v)
462}
463
464/// The 1-based positions of the set bits in `mask`, scanning from the MSB of an
465/// `n`-bit field.
466fn set_bits(mask: u64, n: u32) -> Vec<u8> {
467    let mut ids = Vec::new();
468    for i in 0..n {
469        if (mask >> (n - 1 - i)) & 1 == 1 {
470            ids.push((i + 1) as u8);
471        }
472    }
473    ids
474}
475
476/// The 1-based positions of the set bits in a 32-bit signal mask.
477fn set_bits_u32(mask: u32) -> Vec<u8> {
478    let mut ids = Vec::new();
479    for i in 0..32u32 {
480        if (mask >> (31 - i)) & 1 == 1 {
481            ids.push((i + 1) as u8);
482        }
483    }
484    ids
485}
486
487/// Build the ordered active-cell list from the masks and the cell-present bits.
488fn active_cells(sat_ids: &[u8], sig_ids: &[u8], cell_present: &[bool]) -> Vec<(u8, u8)> {
489    let nsig = sig_ids.len();
490    let mut cells = Vec::new();
491    for (si, &sat) in sat_ids.iter().enumerate() {
492        for (gi, &sig) in sig_ids.iter().enumerate() {
493            if cell_present[si * nsig + gi] {
494                cells.push((sat, sig));
495            }
496        }
497    }
498    cells
499}