1use std::collections::BTreeMap;
10
11use crate::id::GnssSystem;
12
13use super::{MsmKind, MsmMessage};
14
15const WEEK_MS: u64 = 604_800_000;
16const DAY_MS: u64 = 86_400_000;
17const GLONASS_DAY_UNKNOWN: u32 = 7;
18const GLONASS_DAY_SHIFT: u32 = 27;
19const GLONASS_MS_MASK: u32 = (1 << GLONASS_DAY_SHIFT) - 1;
20
21pub const LLI_LOSS_OF_LOCK: u8 = 0b001;
23
24pub const LLI_HALF_CYCLE: u8 = 0b010;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub struct PreviousLock {
30 pub min_lock_time_ms: Option<u32>,
35 pub elapsed_ms: u64,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub struct CellLli {
43 pub satellite_id: u8,
45 pub signal_id: u8,
47 pub lli: u8,
49 pub min_lock_time_ms: Option<u32>,
54}
55
56#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
57struct CellKey {
58 system: GnssSystem,
59 satellite_id: u8,
60 signal_id: u8,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Eq)]
64struct CellState {
65 raw_epoch_time: u32,
66 min_lock_time_ms: Option<u32>,
67}
68
69#[derive(Clone, Debug, Default)]
76pub struct LockTimeTracker {
77 cells: BTreeMap<CellKey, CellState>,
78}
79
80impl LockTimeTracker {
81 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn observe(&mut self, message: &MsmMessage) -> Vec<CellLli> {
94 let mut out = Vec::with_capacity(message.signals.len());
95 for signal in &message.signals {
96 let key = CellKey {
97 system: message.system,
98 satellite_id: signal.satellite_id,
99 signal_id: signal.signal_id,
100 };
101 let min_lock_time_ms = signal.minimum_lock_time_ms(message.kind);
102 let previous = self.cells.get(&key).map(|state| PreviousLock {
103 min_lock_time_ms: state.min_lock_time_ms,
104 elapsed_ms: msm_epoch_dt_ms(
105 message.system,
106 state.raw_epoch_time,
107 message.header.epoch_time,
108 ),
109 });
110 let lli = derive_lli(previous, min_lock_time_ms, signal.half_cycle_ambiguity);
111 out.push(CellLli {
112 satellite_id: signal.satellite_id,
113 signal_id: signal.signal_id,
114 lli,
115 min_lock_time_ms,
116 });
117 if previous.is_none_or(|prev| prev.elapsed_ms != 0) {
118 self.cells.insert(
119 key,
120 CellState {
121 raw_epoch_time: message.header.epoch_time,
122 min_lock_time_ms,
123 },
124 );
125 }
126 }
127 out
128 }
129
130 pub fn reset(&mut self) {
132 self.cells.clear();
133 }
134}
135
136pub fn minimum_lock_time_ms(kind: MsmKind, indicator: u16) -> Option<u32> {
142 match kind {
143 MsmKind::Msm4 => df402_minimum_lock_time_ms(indicator),
144 MsmKind::Msm7 => df407_minimum_lock_time_ms(indicator),
145 }
146}
147
148pub fn derive_lli(
155 previous: Option<PreviousLock>,
156 current_min_lock_ms: Option<u32>,
157 half_cycle_ambiguity: bool,
158) -> u8 {
159 let mut lli = if half_cycle_ambiguity {
160 LLI_HALF_CYCLE
161 } else {
162 0
163 };
164
165 let Some(previous) = previous else {
166 return lli;
167 };
168 let Some(current) = current_min_lock_ms else {
169 return lli | LLI_LOSS_OF_LOCK;
170 };
171
172 let decreased = previous
173 .min_lock_time_ms
174 .is_some_and(|previous_min| current < previous_min);
175 let uncovered_gap = (current as u64) < previous.elapsed_ms;
176 if decreased || uncovered_gap {
177 lli |= LLI_LOSS_OF_LOCK;
178 }
179 lli
180}
181
182pub fn msm_epoch_dt_ms(system: GnssSystem, previous: u32, current: u32) -> u64 {
188 if system == GnssSystem::Glonass {
189 let prev_day = previous >> GLONASS_DAY_SHIFT;
190 let now_day = current >> GLONASS_DAY_SHIFT;
191 let prev_ms = u64::from(previous & GLONASS_MS_MASK);
192 let now_ms = u64::from(current & GLONASS_MS_MASK);
193 if prev_day == GLONASS_DAY_UNKNOWN || now_day == GLONASS_DAY_UNKNOWN {
194 modulo_elapsed_ms(prev_ms, now_ms, DAY_MS)
195 } else {
196 let prev = u64::from(prev_day) * DAY_MS + prev_ms;
197 let now = u64::from(now_day) * DAY_MS + now_ms;
198 modulo_elapsed_ms(prev, now, WEEK_MS)
199 }
200 } else {
201 modulo_elapsed_ms(u64::from(previous), u64::from(current), WEEK_MS)
202 }
203}
204
205pub fn msm_signal_rinex_code(system: GnssSystem, signal_id: u8) -> Option<&'static str> {
211 signal_table(system)
212 .get(usize::from(signal_id))
213 .copied()
214 .flatten()
215}
216
217fn df402_minimum_lock_time_ms(indicator: u16) -> Option<u32> {
218 match indicator {
219 0 => Some(0),
220 1..=15 => Some(1u32 << (u32::from(indicator) + 4)),
221 _ => None,
222 }
223}
224
225fn df407_minimum_lock_time_ms(indicator: u16) -> Option<u32> {
226 let n = u32::from(indicator);
227 match n {
228 0..=63 => Some(n),
229 64..=703 => {
230 let segment = (n - 64) / 32;
231 let start = 64 + segment * 32;
232 let scale = 1u32 << (segment + 1);
233 let start_value = scale * start - scale * 32 * (segment + 1);
234 Some(start_value + scale * (n - start))
235 }
236 704 => Some(67_108_864),
237 _ => None,
238 }
239}
240
241fn modulo_elapsed_ms(previous: u64, current: u64, modulus: u64) -> u64 {
242 (current + modulus - (previous % modulus)) % modulus
243}
244
245fn signal_table(system: GnssSystem) -> &'static [Option<&'static str>; 33] {
246 match system {
247 GnssSystem::Gps => &GPS_SIGNALS,
248 GnssSystem::Glonass => &GLONASS_SIGNALS,
249 GnssSystem::Galileo => &GALILEO_SIGNALS,
250 GnssSystem::Sbas => &SBAS_SIGNALS,
251 GnssSystem::Qzss => &QZSS_SIGNALS,
252 GnssSystem::BeiDou => &BEIDOU_SIGNALS,
253 GnssSystem::Navic => &NAVIC_SIGNALS,
254 }
255}
256
257const N: Option<&str> = None;
258
259#[rustfmt::skip]
260const GPS_SIGNALS: [Option<&str>; 33] = [
261 N,
262 N, Some("1C"), Some("1P"), Some("1W"), N, N, N,
263 Some("2C"), Some("2P"), Some("2W"), N, N, N, N,
264 Some("2S"), Some("2L"), Some("2X"), N, N, N, N,
265 Some("5I"), Some("5Q"), Some("5X"), N, N, N, N, N,
266 Some("1S"), Some("1L"), Some("1X"),
267];
268
269#[rustfmt::skip]
270const GLONASS_SIGNALS: [Option<&str>; 33] = [
271 N,
272 N, Some("1C"), Some("1P"), N, N, N, N,
273 Some("2C"), Some("2P"), N, N, N, N, Some("3I"),
274 Some("3Q"), Some("3X"), N, Some("4A"), Some("4B"), Some("4X"), N,
275 Some("6A"), Some("6B"), Some("6X"), N, N, N, N, N,
276 N, N, N,
277];
278
279#[rustfmt::skip]
280const GALILEO_SIGNALS: [Option<&str>; 33] = [
281 N,
282 N, Some("1C"), Some("1A"), Some("1B"), Some("1X"), Some("1Z"), N,
283 Some("6C"), Some("6A"), Some("6B"), Some("6X"), Some("6Z"), N,
284 Some("7I"), Some("7Q"), Some("7X"), N, Some("8I"), Some("8Q"), Some("8X"), N,
285 Some("5I"), Some("5Q"), Some("5X"), N, N, N, N, N,
286 N, N, N,
287];
288
289#[rustfmt::skip]
290const SBAS_SIGNALS: [Option<&str>; 33] = [
291 N,
292 N, Some("1C"), N, N, N, N, N,
293 N, N, N, N, N, N, N,
294 N, N, N, N, N, N, N,
295 Some("5I"), Some("5Q"), Some("5X"), N, N, N, N, N,
296 N, N, N,
297];
298
299#[rustfmt::skip]
300const QZSS_SIGNALS: [Option<&str>; 33] = [
301 N,
302 N, Some("1C"), N, N, Some("1E"), Some("1Z"), Some("1B"),
303 N, Some("6S"), Some("6L"), Some("6X"), Some("6E"), Some("6Z"), N,
304 Some("2S"), Some("2L"), Some("2X"), N, N, N, N,
305 Some("5I"), Some("5Q"), Some("5X"), Some("5D"), Some("5P"), Some("5Z"), N, N,
306 Some("1S"), Some("1L"), Some("1X"),
307];
308
309#[rustfmt::skip]
310const BEIDOU_SIGNALS: [Option<&str>; 33] = [
311 N,
312 N, Some("2I"), Some("2Q"), Some("2X"), Some("1S"), Some("1L"), Some("1Z"),
313 Some("6I"), Some("6Q"), Some("6X"), Some("6D"), Some("6P"), Some("6Z"), Some("7I"),
314 Some("7Q"), Some("7X"), N, Some("8D"), Some("8P"), Some("8X"), N,
315 Some("5D"), Some("5P"), Some("5X"), Some("7D"), Some("7P"), Some("7Z"), N, N,
316 Some("1D"), Some("1P"), Some("1X"),
317];
318
319#[rustfmt::skip]
320const NAVIC_SIGNALS: [Option<&str>; 33] = [
321 N,
322 N, Some("1D"), Some("1P"), Some("1X"), N, N, N,
323 Some("9A"), Some("9B"), Some("9C"), Some("9X"), N, N, N,
324 N, N, N, N, N, N, N,
325 Some("5A"), Some("5B"), Some("5C"), Some("5X"), N, N, N, N,
326 N, N, N,
327];
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 #[test]
334 fn df407_boundaries_are_monotone_and_pinned() {
335 let pinned = [
336 (0, Some(0)),
337 (1, Some(1)),
338 (63, Some(63)),
339 (64, Some(64)),
340 (95, Some(126)),
341 (96, Some(128)),
342 (127, Some(252)),
343 (128, Some(256)),
344 (159, Some(504)),
345 (160, Some(512)),
346 (191, Some(1008)),
347 (192, Some(1024)),
348 (223, Some(2016)),
349 (224, Some(2048)),
350 (255, Some(4032)),
351 (256, Some(4096)),
352 (287, Some(8064)),
353 (288, Some(8192)),
354 (319, Some(16128)),
355 (320, Some(16384)),
356 (351, Some(32256)),
357 (352, Some(32768)),
358 (383, Some(64512)),
359 (384, Some(65536)),
360 (415, Some(129024)),
361 (416, Some(131072)),
362 (447, Some(258048)),
363 (448, Some(262144)),
364 (479, Some(516096)),
365 (480, Some(524288)),
366 (511, Some(1032192)),
367 (512, Some(1048576)),
368 (543, Some(2064384)),
369 (544, Some(2097152)),
370 (575, Some(4128768)),
371 (576, Some(4194304)),
372 (607, Some(8257536)),
373 (608, Some(8388608)),
374 (639, Some(16515072)),
375 (640, Some(16777216)),
376 (671, Some(33030144)),
377 (672, Some(33554432)),
378 (703, Some(66060288)),
379 (704, Some(67108864)),
380 (705, None),
381 (800, None),
382 (1023, None),
383 ];
384 for (indicator, expected) in pinned {
385 assert_eq!(
386 minimum_lock_time_ms(MsmKind::Msm7, indicator),
387 expected,
388 "DF407 {indicator}"
389 );
390 }
391 for indicator in 1..=704 {
392 let prev = minimum_lock_time_ms(MsmKind::Msm7, indicator - 1).unwrap();
393 let now = minimum_lock_time_ms(MsmKind::Msm7, indicator).unwrap();
394 assert!(now > prev, "DF407 must increase at {indicator}");
395 }
396
397 let segment_starts = [
398 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480, 512, 544, 576, 608,
399 640, 672, 704,
400 ];
401 for start in segment_starts {
402 let previous_step = if start == 64 {
403 1
404 } else {
405 1u32 << ((start - 64) / 32)
406 };
407 let joined = minimum_lock_time_ms(MsmKind::Msm7, start - 1).unwrap() + previous_step;
408 assert_eq!(
409 minimum_lock_time_ms(MsmKind::Msm7, start).unwrap(),
410 joined,
411 "DF407 segment join at {start}"
412 );
413 }
414 }
415}