umsh_gnss/pump.rs
1//! The receiver orchestration loop.
2//!
3//! This is the part that would otherwise be copied into every firmware:
4//! power the receiver when it is wanted, read its UART, hand each
5//! completed fix upward, and put it back to sleep when it is not. It is
6//! generic over the three things that actually differ between boards —
7//! the byte stream, the power sequencing, and what is done with a fix —
8//! so a new board contributes those and nothing else.
9//!
10//! `#[embassy_executor::task]` functions cannot be generic, so a firmware
11//! still writes a task shim; the shim is a handful of lines that
12//! constructs a UART and a control type and calls [`run`]. The loop
13//! itself lives here, once, for both cargo workspaces.
14//!
15//! # Off means off
16//!
17//! While disabled the receiver is powered down, not merely ignored. On
18//! most of these boards it is the largest continuous load there is, so a
19//! loop that kept reading and discarded the sentences would save nothing
20//! that matters.
21//!
22//! The exception is [`rtc_read_once`], for boards whose only surviving
23//! clock is the one inside the receiver. That reads the *time* out of a
24//! receiver that is otherwise off, discards everything positional it sees
25//! on the way, and returns the receiver to its off state. It is a clock
26//! operation, and is gated on whether the receiver's time is trusted
27//! rather than on whether positioning is enabled.
28
29use embassy_futures::select::{Either, select};
30use embedded_hal_async::delay::DelayNs;
31use embedded_io_async::Read;
32
33use crate::driver::{Driver, Fix};
34
35/// The exact `embedded-io-async` [`run`] is generic over.
36///
37/// Re-exported because a consumer that wraps its UART — to count bytes
38/// during bringup, to inject a fault — has to implement *this* crate's
39/// `Read`, and a workspace can easily hold two versions of it. Without
40/// this the wrapper compiles and then fails the bound with an error that
41/// points at the impl and says it does not exist.
42pub use embedded_io_async;
43
44/// The exact `embedded-hal-async` delay trait, re-exported for the same
45/// reason.
46pub use embedded_hal_async;
47
48/// How long [`rtc_read_once`] waits for a dated sentence before giving
49/// up, in milliseconds.
50///
51/// A receiver whose backup domain stayed powered emits a dated `RMC`
52/// within a second or two of its main domain coming up — it is reading
53/// its own clock, not searching for satellites. Ten seconds is generous
54/// enough to cover a slow start and short enough that a receiver which
55/// has lost its clock does not hold up a boot.
56pub const RTC_READ_TIMEOUT_MS: u32 = 10_000;
57
58/// Pause before retrying a receiver whose UART stopped making sense, in
59/// milliseconds. Long enough that a receiver failing hard does not become
60/// a busy loop.
61const RETRY_BACKOFF_MS: u32 = 500;
62
63/// Pause after a read that returned nothing, in milliseconds.
64const IDLE_BACKOFF_MS: u32 = 100;
65
66/// Board-specific power sequencing for one receiver.
67///
68/// Implementations live in each BSP, because this is the only part that
69/// genuinely differs: an enable pin here, a standby pin there, a reset
70/// pulse on one board and not another.
71// Single-executor embedded consumers; `Send` futures are irrelevant here,
72// as with the embassy ecosystem's own async traits.
73#[allow(async_fn_in_trait)]
74pub trait Power {
75 /// Bring the receiver up and leave it emitting sentences.
76 ///
77 /// Idempotent: the pump calls it whenever it believes the receiver
78 /// should be running, including after an error.
79 async fn power_on(&mut self);
80
81 /// Put the receiver in the lowest power state this board can reach.
82 ///
83 /// On a board whose receiver holds the only surviving real-time
84 /// clock, that state keeps the backup domain alive — which is not an
85 /// exception to "off means off" so much as a statement that the
86 /// domain in question is a clock rather than a receiver.
87 async fn power_off(&mut self);
88}
89
90/// What the firmware does with what the receiver says.
91///
92/// Deliberately not "store this in a global": a sink decides what a fix
93/// means. The runtime's implementation folds it into the ULCP property
94/// surface, the wall clock, and the advertised identity; a test's counts
95/// what it was given.
96#[allow(async_fn_in_trait)]
97pub trait Sink {
98 /// One completed fix cycle.
99 ///
100 /// Called for every cycle, including the empty ones a searching
101 /// receiver produces — "still nothing" is a fact worth having, and a
102 /// sink that only heard about successes could not tell a receiver
103 /// that is searching from one that is not running.
104 async fn fix(&mut self, fix: &Fix);
105}
106
107/// Whether the receiver should be running, and a way to wait for that to
108/// change.
109///
110/// Separate from [`Power`] because the two have different owners: the
111/// answer comes from `PROP_GNSS_ENABLED` by way of the device-domain
112/// mirror, while the sequencing belongs to the board.
113#[allow(async_fn_in_trait)]
114pub trait Enable {
115 /// Whether the receiver is wanted right now.
116 fn enabled(&self) -> bool;
117
118 /// Complete when the answer changes.
119 ///
120 /// **Must be cancellation-safe.** The pump drops and re-creates this
121 /// future every time a byte arrives, so an implementation that loses
122 /// a change it was cancelled on would leave the receiver powered
123 /// after it was switched off. An `embassy_sync::watch::Watch`
124 /// receiver behaves correctly; a bare `Signal` does not.
125 async fn changed(&mut self);
126}
127
128/// Drive one receiver forever.
129///
130/// Powers the receiver whenever `enable` says it is wanted, parses
131/// everything it emits, and hands each completed cycle to `sink`. Never
132/// returns.
133///
134/// Read errors are treated as the receiver having gone away: the pump
135/// powers it down, waits a moment, and brings it back. A UART that
136/// overruns because the executor was busy elsewhere is the common cause,
137/// and it costs one fix cycle rather than a permanently deaf receiver.
138pub async fn run<R, P, E, S, D>(
139 mut uart: R,
140 mut power: P,
141 mut enable: E,
142 mut sink: S,
143 mut delay: D,
144) -> !
145where
146 R: Read,
147 P: Power,
148 E: Enable,
149 S: Sink,
150 D: DelayNs,
151{
152 let mut driver = Driver::new();
153 let mut buf = [0u8; 64];
154
155 // Park the receiver before anything else. The pump owns its power
156 // state from here on, and whatever state it was left in belongs to
157 // whoever ran before — a bootloader, a previous image, or a reset
158 // that did not reach the pin.
159 power.power_off().await;
160
161 loop {
162 while !enable.enabled() {
163 enable.changed().await;
164 }
165
166 power.power_on().await;
167 // Bytes from before the power cycle describe where the device
168 // was, not where it is.
169 driver.reset();
170
171 // Read until the receiver is switched off or the link fails.
172 let failed = loop {
173 match select(uart.read(&mut buf), enable.changed()).await {
174 Either::First(Ok(0)) => {
175 // A closed stream is not something to spin on.
176 delay.delay_ms(IDLE_BACKOFF_MS).await;
177 }
178 Either::First(Ok(len)) => {
179 for &byte in &buf[..len] {
180 if let Some(fix) = driver.push(byte) {
181 sink.fix(&fix).await;
182 }
183 }
184 }
185 // Cycle the receiver rather than keep reading a stream
186 // that has stopped making sense.
187 Either::First(Err(_)) => break true,
188 Either::Second(()) => {
189 if !enable.enabled() {
190 break false;
191 }
192 }
193 }
194 };
195
196 power.power_off().await;
197 if failed {
198 // Pause before trying again, so a receiver that is failing
199 // hard does not become a busy loop.
200 delay.delay_ms(RETRY_BACKOFF_MS).await;
201 }
202 }
203}
204
205/// Power the receiver just long enough to read the time out of it, then
206/// return it to its off state.
207///
208/// For boards where the receiver's own real-time-clock domain is the only
209/// clock that survives a power cycle. The domain cannot speak a UART on
210/// its own, so reading it mechanically requires bringing the main domain
211/// up — but what is being read is a clock, and everything positional seen
212/// along the way is discarded.
213///
214/// Returns the first instant the receiver reports, or `None` if it
215/// reports none within [`RTC_READ_TIMEOUT_MS`] — which is what a receiver
216/// that lost its backup power looks like.
217///
218/// The caller decides whether to believe the answer: this is governed by
219/// `PROP_GNSS_TIME_TRUST`, not by `PROP_GNSS_ENABLED`.
220pub async fn rtc_read_once<R, P, D>(
221 mut uart: R,
222 power: &mut P,
223 mut delay: D,
224) -> Option<crate::DateTime>
225where
226 R: Read,
227 P: Power,
228 D: DelayNs,
229{
230 let mut driver = Driver::new();
231 let mut buf = [0u8; 64];
232
233 power.power_on().await;
234 let found = match select(
235 async {
236 loop {
237 let Ok(len) = uart.read(&mut buf).await else {
238 // Nothing to recover to: this is a bounded one-shot,
239 // and the deadline below ends it either way.
240 core::future::pending::<()>().await;
241 unreachable!()
242 };
243 for &byte in &buf[..len] {
244 // A cycle's position is nobody's business here; only
245 // the instant leaves this function.
246 if let Some(fix) = driver.push(byte)
247 && let Some(at) = fix.time
248 {
249 return at;
250 }
251 }
252 }
253 },
254 delay.delay_ms(RTC_READ_TIMEOUT_MS),
255 )
256 .await
257 {
258 Either::First(at) => Some(at),
259 Either::Second(()) => None,
260 };
261 power.power_off().await;
262 found
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use core::cell::RefCell;
269 use core::convert::Infallible;
270 use std::rc::Rc;
271 use std::vec::Vec;
272
273 /// What the mock receiver did, in order, so a test can assert on the
274 /// sequencing rather than only on the outcome.
275 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
276 enum Event {
277 On,
278 Off,
279 }
280
281 #[derive(Default)]
282 struct Shared {
283 events: Vec<Event>,
284 fixes: Vec<Fix>,
285 }
286
287 #[derive(Clone, Default)]
288 struct Log(Rc<RefCell<Shared>>);
289
290 impl Log {
291 fn events(&self) -> Vec<Event> {
292 self.0.borrow().events.clone()
293 }
294
295 fn fixes(&self) -> Vec<Fix> {
296 self.0.borrow().fixes.clone()
297 }
298 }
299
300 struct MockPower(Log);
301
302 impl Power for MockPower {
303 async fn power_on(&mut self) {
304 self.0.0.borrow_mut().events.push(Event::On);
305 }
306
307 async fn power_off(&mut self) {
308 self.0.0.borrow_mut().events.push(Event::Off);
309 }
310 }
311
312 /// A delay that never elapses: the pump's backoffs and the RTC-read
313 /// timeout exist to bound real hardware, and a test that let them
314 /// fire would be testing the clock rather than the loop.
315 struct StalledDelay;
316
317 impl DelayNs for StalledDelay {
318 async fn delay_ns(&mut self, _ns: u32) {
319 core::future::pending::<()>().await
320 }
321 }
322
323 /// A delay that elapses immediately, for the one test that wants the
324 /// timeout to win.
325 struct InstantDelay;
326
327 impl DelayNs for InstantDelay {
328 async fn delay_ns(&mut self, _ns: u32) {}
329 }
330
331 struct MockSink(Log);
332
333 impl Sink for MockSink {
334 async fn fix(&mut self, fix: &Fix) {
335 self.0.0.borrow_mut().fixes.push(*fix);
336 }
337 }
338
339 /// A scripted byte stream that never ends: once the script runs out
340 /// it parks forever, which is what an idle UART looks like.
341 struct MockUart {
342 script: Vec<u8>,
343 offset: usize,
344 /// Shared, because the pump takes the UART by value and a test
345 /// still has to see what was sent to it.
346 written: Rc<RefCell<Vec<u8>>>,
347 }
348
349 impl MockUart {
350 fn new(text: &str) -> Self {
351 Self {
352 script: text.bytes().collect(),
353 offset: 0,
354 written: Rc::new(RefCell::new(Vec::new())),
355 }
356 }
357
358 /// A handle on everything written to this port.
359 fn written(&self) -> Rc<RefCell<Vec<u8>>> {
360 Rc::clone(&self.written)
361 }
362 }
363
364 impl embedded_io_async::ErrorType for MockUart {
365 type Error = Infallible;
366 }
367
368 impl Read for MockUart {
369 async fn read(&mut self, buf: &mut [u8]) -> Result<usize, Infallible> {
370 if self.offset == self.script.len() {
371 core::future::pending::<()>().await;
372 unreachable!()
373 }
374 // One byte a call, so the pump's buffering is exercised
375 // rather than short-circuited by a single large read.
376 buf[0] = self.script[self.offset];
377 self.offset += 1;
378 Ok(1)
379 }
380 }
381
382 /// An enable source that yields a fixed script of states.
383 struct MockEnable {
384 states: Vec<bool>,
385 index: usize,
386 }
387
388 impl MockEnable {
389 fn new(states: &[bool]) -> Self {
390 Self {
391 states: states.to_vec(),
392 index: 0,
393 }
394 }
395 }
396
397 impl Enable for MockEnable {
398 fn enabled(&self) -> bool {
399 self.states[self.index.min(self.states.len() - 1)]
400 }
401
402 async fn changed(&mut self) {
403 if self.index + 1 < self.states.len() {
404 self.index += 1;
405 } else {
406 core::future::pending::<()>().await;
407 }
408 }
409 }
410
411 /// A checksummed NMEA line.
412 fn line(body: &str) -> std::string::String {
413 let checksum = body.bytes().fold(0u8, |sum, byte| sum ^ byte);
414 std::format!("${body}*{checksum:02X}\r\n")
415 }
416
417 /// Run a future until it stops making progress, which for a pump that
418 /// never returns is the only way to observe it.
419 fn poll_until_stalled(future: impl core::future::Future<Output = ()>) {
420 use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
421
422 const VTABLE: RawWakerVTable =
423 RawWakerVTable::new(|data| RawWaker::new(data, &VTABLE), |_| {}, |_| {}, |_| {});
424 let waker = unsafe { Waker::from_raw(RawWaker::new(core::ptr::null(), &VTABLE)) };
425 let mut context = Context::from_waker(&waker);
426 let mut future = core::pin::pin!(future);
427 // Bounded: the pump is an infinite loop, so this asks "has it
428 // done everything it can with the script it was given".
429 for _ in 0..10_000 {
430 if let Poll::Ready(()) = future.as_mut().poll(&mut context) {
431 return;
432 }
433 }
434 }
435
436 /// A full cycle of sentences: a three-dimensional fix.
437 fn fix_cycle() -> std::string::String {
438 std::format!(
439 "{}{}{}",
440 line("GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,"),
441 line("GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1"),
442 line("GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230326,003.1,W"),
443 )
444 }
445
446 #[test]
447 fn a_disabled_receiver_is_powered_down_and_never_read() {
448 let log = Log::default();
449 poll_until_stalled(async {
450 let _ = run(
451 MockUart::new(&fix_cycle()),
452 MockPower(log.clone()),
453 MockEnable::new(&[false]),
454 MockSink(log.clone()),
455 StalledDelay,
456 )
457 .await;
458 });
459 assert_eq!(
460 log.events(),
461 [Event::Off],
462 "a disabled receiver was powered"
463 );
464 assert!(log.fixes().is_empty(), "a disabled receiver produced a fix");
465 }
466
467 #[test]
468 fn an_enabled_receiver_is_powered_and_its_fixes_reach_the_sink() {
469 let log = Log::default();
470 poll_until_stalled(async {
471 let _ = run(
472 MockUart::new(&fix_cycle()),
473 MockPower(log.clone()),
474 MockEnable::new(&[true]),
475 MockSink(log.clone()),
476 StalledDelay,
477 )
478 .await;
479 });
480 assert_eq!(log.events(), [Event::Off, Event::On]);
481 let fixes = log.fixes();
482 assert_eq!(fixes.len(), 1);
483 assert_eq!(fixes[0].quality, crate::FixQuality::ThreeD);
484 assert_eq!(fixes[0].altitude_m, Some(545));
485 }
486
487 /// Switching the receiver off must actually power it down, not merely
488 /// stop reporting: it is the largest continuous load on the board.
489 #[test]
490 fn switching_off_powers_the_receiver_down() {
491 let log = Log::default();
492 poll_until_stalled(async {
493 let _ = run(
494 MockUart::new(&fix_cycle()),
495 MockPower(log.clone()),
496 MockEnable::new(&[true, false]),
497 MockSink(log.clone()),
498 StalledDelay,
499 )
500 .await;
501 });
502 assert_eq!(log.events(), [Event::Off, Event::On, Event::Off]);
503 }
504
505 /// Switching back on re-powers the receiver and resumes parsing.
506 #[test]
507 fn switching_back_on_restarts_the_receiver() {
508 let log = Log::default();
509 poll_until_stalled(async {
510 let _ = run(
511 MockUart::new(&fix_cycle()),
512 MockPower(log.clone()),
513 MockEnable::new(&[false, true]),
514 MockSink(log.clone()),
515 StalledDelay,
516 )
517 .await;
518 });
519 assert_eq!(log.events(), [Event::Off, Event::On]);
520 assert_eq!(log.fixes().len(), 1);
521 }
522
523 /// Every cycle reaches the sink, including the empty ones a searching
524 /// receiver produces: "still nothing" is how a sink tells searching
525 /// from not running.
526 #[test]
527 fn a_searching_receiver_still_reports_each_cycle() {
528 let log = Log::default();
529 let script = std::format!(
530 "{}{}{}",
531 line("GPRMC,,V,,,,,,,,,,N"),
532 line("GPRMC,081836.00,V,,,,,,,130926,,"),
533 fix_cycle(),
534 );
535 poll_until_stalled(async {
536 let _ = run(
537 MockUart::new(&script),
538 MockPower(log.clone()),
539 MockEnable::new(&[true]),
540 MockSink(log.clone()),
541 StalledDelay,
542 )
543 .await;
544 });
545 let fixes = log.fixes();
546 assert_eq!(fixes.len(), 3);
547 assert_eq!(fixes[0].quality, crate::FixQuality::None);
548 assert_eq!(fixes[0].time, None);
549 // The middle cycle is the one the receiver-RTC design rests on:
550 // a time with no fix behind it.
551 assert!(fixes[1].time.is_some());
552 assert!(!fixes[1].time_from_fix);
553 assert_eq!(fixes[2].quality, crate::FixQuality::ThreeD);
554 assert!(fixes[2].time_from_fix);
555 }
556
557 /// A receiver powered up mid-sentence is the normal cold-start case;
558 /// the leading garbage costs at most the sentence it landed in.
559 #[test]
560 fn a_torn_first_sentence_costs_only_itself() {
561 let log = Log::default();
562 let script = std::format!("038,N,01131.000,E*11\r\n{}", fix_cycle());
563 poll_until_stalled(async {
564 let _ = run(
565 MockUart::new(&script),
566 MockPower(log.clone()),
567 MockEnable::new(&[true]),
568 MockSink(log.clone()),
569 StalledDelay,
570 )
571 .await;
572 });
573 assert_eq!(log.fixes().len(), 1);
574 }
575
576 #[test]
577 fn the_receiver_rtc_read_powers_down_again_afterwards() {
578 let log = Log::default();
579 let mut power = MockPower(log.clone());
580 let mut found = None;
581 poll_until_stalled(async {
582 found = rtc_read_once(
583 MockUart::new(&line("GPRMC,081836.00,V,,,,,,,130926,,")),
584 &mut power,
585 StalledDelay,
586 )
587 .await;
588 });
589 assert_eq!(
590 found.map(|at| (at.year, at.month, at.day, at.hour, at.minute)),
591 Some((2026, 9, 13, 8, 18))
592 );
593 assert_eq!(
594 log.events(),
595 [Event::On, Event::Off],
596 "the read left the receiver powered"
597 );
598 }
599
600 /// A receiver whose backup domain lost power emits sentences with no
601 /// date in them. The read gives up rather than hanging a boot.
602 #[test]
603 fn a_receiver_with_no_clock_yields_nothing_and_still_powers_down() {
604 let log = Log::default();
605 let mut power = MockPower(log.clone());
606 let mut found = Some(crate::DateTime::EPOCH);
607 poll_until_stalled(async {
608 found = rtc_read_once(
609 MockUart::new(&line("GPRMC,,V,,,,,,,,,,N")),
610 &mut power,
611 InstantDelay,
612 )
613 .await;
614 });
615 assert_eq!(found, None, "the read invented a time");
616 assert_eq!(
617 log.events(),
618 [Event::On, Event::Off],
619 "a timed-out read left the receiver powered"
620 );
621 }
622}