1pub const CLS_NAV: u8 = 0x01;
8pub const CLS_ACK: u8 = 0x05;
10pub const CLS_CFG: u8 = 0x06;
12pub const CLS_MON: u8 = 0x0A;
14pub const CLS_TIM: u8 = 0x0D;
16
17pub const NAV_DOP: u8 = 0x04;
19pub const NAV_PVT: u8 = 0x07;
21pub const NAV_SVIN: u8 = 0x3B;
23pub const NAV_RELPOSNED: u8 = 0x3C;
25pub const ACK_NAK: u8 = 0x00;
27pub const ACK_ACK: u8 = 0x01;
29pub const CFG_VALSET: u8 = 0x8A;
31pub const MON_VER: u8 = 0x04;
33pub const TIM_TP: u8 = 0x01;
35
36const GPS_EPOCH_UNIX_S: i64 = 315_964_800;
38pub const GPS_UTC_LEAP_S: i64 = 18;
40
41fn u16_at(p: &[u8], o: usize) -> u16 {
42 u16::from_le_bytes([p[o], p[o + 1]])
43}
44fn u32_at(p: &[u8], o: usize) -> u32 {
45 u32::from_le_bytes([p[o], p[o + 1], p[o + 2], p[o + 3]])
46}
47fn i32_at(p: &[u8], o: usize) -> i32 {
48 u32_at(p, o) as i32
49}
50
51#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct NavPvt {
54 pub fix_type: u8,
56 pub gnss_fix_ok: bool,
58 pub diff_soln: bool,
60 pub carr_soln: u8,
62 pub num_sv: u8,
64 pub lat_deg: f64,
66 pub lon_deg: f64,
68 pub h_ellipsoid_m: f64,
70 pub h_msl_m: f64,
72 pub utc_ns: Option<i64>,
74}
75
76pub fn nav_pvt(p: &[u8]) -> Option<NavPvt> {
78 if p.len() < 92 {
79 return None;
80 }
81 let flags = p[21];
82 let valid = p[11];
83 let utc_ns = (valid & 0x07 == 0x07)
84 .then(|| {
85 let days = days_from_civil(u16_at(p, 4) as i64, p[6] as i64, p[7] as i64);
86 let secs = days * 86_400 + p[8] as i64 * 3600 + p[9] as i64 * 60 + p[10] as i64;
87 secs * 1_000_000_000 + i32_at(p, 16) as i64
88 })
89 .filter(|_| (1..=12).contains(&p[6]) && (1..=31).contains(&p[7]));
90 Some(NavPvt {
91 fix_type: p[20],
92 gnss_fix_ok: flags & 0x01 != 0,
93 diff_soln: flags & 0x02 != 0,
94 carr_soln: (flags >> 6) & 0x03,
95 num_sv: p[23],
96 lon_deg: i32_at(p, 24) as f64 * 1e-7,
97 lat_deg: i32_at(p, 28) as f64 * 1e-7,
98 h_ellipsoid_m: i32_at(p, 32) as f64 / 1000.0,
99 h_msl_m: i32_at(p, 36) as f64 / 1000.0,
100 utc_ns,
101 })
102}
103
104pub fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
106 let y = if m <= 2 { y - 1 } else { y };
107 let era = y.div_euclid(400);
108 let yoe = y - era * 400;
109 let mp = (m + 9) % 12;
110 let doy = (153 * mp + 2) / 5 + d - 1;
111 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
112 era * 146_097 + doe - 719_468
113}
114
115pub fn nav_dop_hdop(p: &[u8]) -> Option<f64> {
117 (p.len() >= 18).then(|| u16_at(p, 12) as f64 / 100.0)
118}
119
120#[derive(Debug, Clone, Copy, PartialEq)]
122pub struct NavSvin {
123 pub dur_s: u32,
125 pub mean_ecef_m: [f64; 3],
127 pub mean_acc_m: f64,
129 pub valid: bool,
131 pub active: bool,
133}
134
135pub fn nav_svin(p: &[u8]) -> Option<NavSvin> {
137 if p.len() < 40 {
138 return None;
139 }
140 let axis = |o: usize, hp: usize| i32_at(p, o) as f64 * 0.01 + (p[hp] as i8) as f64 * 1e-4;
141 Some(NavSvin {
142 dur_s: u32_at(p, 8),
143 mean_ecef_m: [axis(12, 24), axis(16, 25), axis(20, 26)],
144 mean_acc_m: u32_at(p, 28) as f64 * 1e-4,
145 valid: p[36] != 0,
146 active: p[37] != 0,
147 })
148}
149
150#[derive(Debug, Clone, Copy, PartialEq)]
152pub struct RelPosNed {
153 pub heading_deg: f64,
155 pub acc_heading_deg: f64,
157 pub length_m: f64,
159 pub gnss_fix_ok: bool,
161 pub rel_pos_valid: bool,
163 pub is_moving: bool,
165 pub heading_valid: bool,
167}
168
169pub fn nav_relposned(p: &[u8]) -> Option<RelPosNed> {
171 if p.len() < 64 || p[0] != 0x01 {
172 return None;
173 }
174 let flags = u32_at(p, 60);
175 Some(RelPosNed {
176 heading_deg: i32_at(p, 24) as f64 * 1e-5,
177 acc_heading_deg: u32_at(p, 52) as f64 * 1e-5,
178 length_m: i32_at(p, 20) as f64 * 0.01 + (p[35] as i8) as f64 * 1e-4,
179 gnss_fix_ok: flags & (1 << 0) != 0,
180 rel_pos_valid: flags & (1 << 2) != 0,
181 is_moving: flags & (1 << 5) != 0,
182 heading_valid: flags & (1 << 8) != 0,
183 })
184}
185
186pub fn tim_tp_utc_ns(p: &[u8]) -> Option<i64> {
188 if p.len() < 16 {
189 return None;
190 }
191 let tow_ms = u32_at(p, 0) as i64;
192 let sub_ms = u32_at(p, 4) as i64; let week = u16_at(p, 12) as i64;
194 let utc_base = p[14] & 0x01 != 0;
195 let mut ns = (GPS_EPOCH_UNIX_S + week * 604_800) * 1_000_000_000
196 + tow_ms * 1_000_000
197 + ((sub_ms * 1_000_000) >> 32);
198 if !utc_base {
199 ns -= GPS_UTC_LEAP_S * 1_000_000_000;
200 }
201 Some(ns)
202}
203
204pub fn mon_ver_model(p: &[u8]) -> Option<String> {
207 if p.len() < 40 {
208 return None;
209 }
210 let text = |b: &[u8]| {
211 let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
212 String::from_utf8_lossy(&b[..end]).trim().to_string()
213 };
214 let module = p[40..]
215 .as_chunks::<30>()
216 .0
217 .iter()
218 .map(|c| text(c.as_slice()))
219 .find_map(|e| e.strip_prefix("MOD=").map(str::to_string));
220 Some(match module {
221 Some(m) => format!("u-blox {m}"),
222 None => format!("u-blox hw {}", text(&p[30..40])),
223 })
224}
225
226pub fn ack_target(p: &[u8]) -> Option<(u8, u8)> {
228 (p.len() >= 2).then(|| (p[0], p[1]))
229}