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}