1use serde::Serialize;
8
9use crate::ubx::NavSvin;
10use crate::valset::{self, RxPort};
11
12pub const CONFIRM_TIMEOUT_NS: u64 = 10_000_000_000;
15
16pub const E_UNSUPPORTED: &str = "E_GNSS_UNSUPPORTED";
18
19#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum BaseMode {
22 Off,
24 Survey {
26 min_dur_s: u32,
28 accuracy_m: f64,
30 },
31 Fixed(Llh),
33}
34
35impl BaseMode {
36 pub fn as_str(&self) -> &'static str {
38 match self {
39 BaseMode::Off => "off",
40 BaseMode::Survey { .. } => "survey",
41 BaseMode::Fixed(_) => "fixed",
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
48pub struct Llh {
49 pub lat_deg: f64,
51 pub lon_deg: f64,
53 pub h_ellipsoid_m: f64,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
59#[serde(rename_all = "snake_case")]
60pub enum BaseState {
61 Off,
63 Surveying,
65 Fixed,
67 Failed,
69}
70
71#[derive(Debug, Clone, Default)]
73pub struct Capability {
74 pub writable: bool,
76 pub model: Option<String>,
78 pub port: RxPort,
80}
81
82pub const BASE_MODULES: [&str; 3] = ["ZED-F9P", "NEO-F9P", "ZED-X20P"];
87
88pub fn survey_capable(model: &str) -> bool {
92 model
93 .split_whitespace()
94 .any(|w| BASE_MODULES.iter().any(|m| m.eq_ignore_ascii_case(w)))
95}
96
97#[derive(Debug, Clone)]
99pub struct Base {
100 mode: BaseMode,
101 state: BaseState,
102 reason: Option<String>,
103 commanded_ns: Option<u64>,
104 confirmed: bool,
105 elapsed_s: u32,
106 accuracy_m: Option<f64>,
107 fixed: Option<Llh>,
108 min_dur_s: u32,
109 target_accuracy_m: f64,
110}
111
112impl Base {
113 pub fn new(min_dur_s: u32, target_accuracy_m: f64) -> Self {
115 Self {
116 mode: BaseMode::Off,
117 state: BaseState::Off,
118 reason: None,
119 commanded_ns: None,
120 confirmed: false,
121 elapsed_s: 0,
122 accuracy_m: None,
123 fixed: None,
124 min_dur_s,
125 target_accuracy_m,
126 }
127 }
128
129 pub fn mode(&self) -> BaseMode {
131 self.mode
132 }
133 pub fn state(&self) -> BaseState {
135 self.state
136 }
137 pub fn reason(&self) -> Option<&str> {
139 self.reason.as_deref()
140 }
141 pub fn defaults(&self) -> (u32, f64) {
143 (self.min_dur_s, self.target_accuracy_m)
144 }
145 pub fn active(&self) -> bool {
147 !matches!(self.mode, BaseMode::Off) && self.state != BaseState::Failed
148 }
149
150 pub fn confirmed(&self) -> bool {
153 self.confirmed && self.state != BaseState::Failed
154 }
155
156 fn fail(&mut self, reason: String) {
157 tracing::warn!(reason = %reason, mode = self.mode.as_str(), "gnss_base_failed");
158 self.state = BaseState::Failed;
159 self.reason = Some(reason);
160 self.commanded_ns = None;
161 }
162
163 pub fn request(&mut self, mode: BaseMode) {
167 self.reset(mode);
168 }
169
170 fn reset(&mut self, mode: BaseMode) {
171 self.mode = mode;
172 self.reason = None;
173 self.confirmed = false;
174 self.elapsed_s = 0;
175 self.accuracy_m = None;
176 self.fixed = None;
177 self.commanded_ns = None;
178 self.state = BaseState::Off;
179 if let BaseMode::Survey {
180 min_dur_s,
181 accuracy_m,
182 } = mode
183 {
184 self.min_dur_s = min_dur_s;
185 self.target_accuracy_m = accuracy_m;
186 }
187 }
188
189 pub fn apply(
194 &mut self,
195 mode: BaseMode,
196 cap: &Capability,
197 now: u64,
198 ) -> Result<Option<Vec<u8>>, (&'static str, String)> {
199 self.reset(mode);
200 if mode == BaseMode::Off {
201 return Ok(cap.writable.then(|| valset::off(cap.port)));
202 }
203 if !cap.writable {
204 let detail = "the receiver is attached through the sensor hub and cannot be configured";
205 self.fail(detail.to_string());
206 return Err((E_UNSUPPORTED, detail.to_string()));
207 }
208 if let Some(model) = cap.model.as_deref().filter(|m| !survey_capable(m)) {
209 let detail = format!("receiver model {model} has no base mode");
210 self.fail(detail.clone());
211 return Err((E_UNSUPPORTED, detail));
212 }
213 self.commanded_ns = Some(now);
214 let bytes = match mode {
215 BaseMode::Survey {
216 min_dur_s,
217 accuracy_m,
218 } => {
219 self.state = BaseState::Surveying;
220 valset::survey_in(cap.port, min_dur_s, accuracy_m)
221 }
222 BaseMode::Fixed(p) => valset::fixed(cap.port, p.lat_deg, p.lon_deg, p.h_ellipsoid_m),
223 BaseMode::Off => unreachable!("handled above"),
224 };
225 Ok(Some(bytes))
226 }
227
228 pub fn on_model(&mut self, model: &str) {
230 if self.mode != BaseMode::Off && self.state != BaseState::Failed && !survey_capable(model) {
231 self.fail(format!("receiver model {model} has no base mode"));
232 }
233 }
234
235 pub fn on_svin(&mut self, s: &NavSvin) {
237 if !matches!(self.mode, BaseMode::Survey { .. }) || self.state != BaseState::Surveying {
238 return;
239 }
240 if !(s.active || s.valid) {
241 return;
242 }
243 self.confirmed = true;
244 self.elapsed_s = s.dur_s;
245 self.accuracy_m = Some(s.mean_acc_m);
246 if s.valid {
247 self.state = BaseState::Fixed;
248 self.fixed = Some(ecef_to_llh(s.mean_ecef_m));
249 }
250 }
251
252 pub fn on_valset_ack(&mut self, ok: bool) {
254 if self.commanded_ns.is_none() || self.state == BaseState::Failed {
255 return;
256 }
257 if !ok {
258 self.fail("the receiver rejected the base configuration".to_string());
259 return;
260 }
261 if let BaseMode::Fixed(p) = self.mode {
262 self.confirmed = true;
263 self.state = BaseState::Fixed;
264 self.fixed = Some(p);
265 }
266 }
267
268 pub fn tick(&mut self, now: u64) {
270 let Some(t) = self.commanded_ns else {
271 return;
272 };
273 if self.confirmed || self.state == BaseState::Failed {
274 return;
275 }
276 if now.saturating_sub(t) > CONFIRM_TIMEOUT_NS {
277 let reason = match self.mode {
278 BaseMode::Survey { .. } => "no survey-in report (NAV-SVIN) from the receiver",
279 _ => "the receiver did not acknowledge the base configuration",
280 };
281 self.fail(reason.to_string());
282 }
283 }
284
285 pub fn view(&self) -> BaseView {
287 BaseView {
288 mode: self.mode.as_str(),
289 state: self.state,
290 elapsed_s: self.elapsed_s,
291 min_dur_s: self.min_dur_s,
292 accuracy_m: self.accuracy_m,
293 target_accuracy_m: self.target_accuracy_m,
294 fixed: self.fixed.filter(|_| self.state == BaseState::Fixed),
295 }
296 }
297}
298
299#[derive(Debug, Clone, PartialEq, Serialize)]
301pub struct BaseView {
302 pub mode: &'static str,
304 pub state: BaseState,
306 pub elapsed_s: u32,
308 pub min_dur_s: u32,
310 pub accuracy_m: Option<f64>,
312 pub target_accuracy_m: f64,
314 pub fixed: Option<Llh>,
316}
317
318pub fn ecef_to_llh([x, y, z]: [f64; 3]) -> Llh {
320 const A: f64 = 6_378_137.0;
321 const F: f64 = 1.0 / 298.257_223_563;
322 let e2 = F * (2.0 - F);
323 let p = (x * x + y * y).sqrt();
324 let lon = y.atan2(x);
325 let mut lat = z.atan2(p * (1.0 - e2));
326 let mut h = 0.0;
327 for _ in 0..8 {
328 let s = lat.sin();
329 let n = A / (1.0 - e2 * s * s).sqrt();
330 h = if lat.cos().abs() > 1e-9 {
331 p / lat.cos() - n
332 } else {
333 z.abs() - n * (1.0 - e2)
334 };
335 lat = z.atan2(p * (1.0 - e2 * n / (n + h)));
336 }
337 Llh {
338 lat_deg: lat.to_degrees(),
339 lon_deg: lon.to_degrees(),
340 h_ellipsoid_m: h,
341 }
342}
343
344#[cfg(test)]
345mod tests;