embassy_supervisor/stamped.rs
1//! A signal that remembers when it was last written.
2
3use embassy_time::{Duration, Instant};
4use portable_atomic::{AtomicBool, AtomicU32, Ordering};
5
6/// A signal wrapper that stamps every write with the time it happened, so a
7/// reader can ask how old the value is.
8///
9/// The read-side half of write freshness. The monitor side already exists:
10/// `writes: [X observed beat]` with a `beat_timeout:` reports a node that has
11/// not written recently. `Stamped` gives the *consumer* the same fact at the
12/// point of use — [`age`](Self::age), [`is_fresh`](Self::is_fresh),
13/// [`read_fresh`](Self::read_fresh) — for the case the monitor cannot cover:
14/// a value that is still being written but must not be trusted past a
15/// certain age. What neither can tell is whether a fresh value is *valid*;
16/// a plausible-looking drift needs a consumer that understands the value.
17///
18/// Writes go through [`w`](Self::w), reads through [`r`](Self::r); there is
19/// deliberately no `Deref`, so an unstamped write path does not exist. It
20/// records the time and nothing else — compose `Stamped<Counted<T>>` when
21/// the write count matters too. Costs one `AtomicU32` and one `AtomicBool`
22/// beside the wrapped value. The stamp is `Instant` ticks truncated to
23/// `u32`, like the node heartbeat: ages are exact up to `u32::MAX` ticks.
24pub struct Stamped<T> {
25 inner: T,
26 stamp: AtomicU32,
27 written: AtomicBool,
28}
29
30impl<T> Stamped<T> {
31 /// Wrap `inner`, never written.
32 pub const fn new(inner: T) -> Self {
33 Self {
34 inner,
35 stamp: AtomicU32::new(0),
36 written: AtomicBool::new(false),
37 }
38 }
39
40 /// Borrow the inner signal for a write, stamping now.
41 pub fn w(&self) -> &T {
42 self.stamp
43 .store(Instant::now().as_ticks() as u32, Ordering::Release);
44 self.written.store(true, Ordering::Release);
45 &self.inner
46 }
47
48 /// Borrow the inner signal for a read.
49 pub fn r(&self) -> &T {
50 &self.inner
51 }
52
53 /// Borrow the inner signal without touching the stamp.
54 pub fn inner(&self) -> &T {
55 &self.inner
56 }
57
58 /// Time since the last stamped write, `None` until the first.
59 pub fn age(&self) -> Option<Duration> {
60 if !self.written.load(Ordering::Acquire) {
61 return None;
62 }
63 let now = Instant::now().as_ticks() as u32;
64 let ticks = now.wrapping_sub(self.stamp.load(Ordering::Acquire));
65 Some(Duration::from_ticks(u64::from(ticks)))
66 }
67
68 /// Has the signal been written within `max_age`?
69 pub fn is_fresh(&self, max_age: Duration) -> bool {
70 self.age().is_some_and(|age| age <= max_age)
71 }
72
73 /// The inner signal if it was written within `max_age`, else `None`.
74 pub fn read_fresh(&self, max_age: Duration) -> Option<&T> {
75 self.is_fresh(max_age).then_some(&self.inner)
76 }
77}
78
79#[cfg(feature = "coupling-observe")]
80impl<T: crate::Observable> crate::Observable for Stamped<T> {
81 fn change_token(&self) -> u32 {
82 self.inner.change_token()
83 }
84}
85
86#[cfg(feature = "dataflow")]
87impl<T: crate::Sink> crate::Sink for Stamped<T> {
88 type Item = T::Item;
89 fn put(&self, v: T::Item) {
90 self.w().put(v);
91 }
92}
93
94#[cfg(feature = "dataflow")]
95impl<T: crate::Source> crate::Source for Stamped<T> {
96 type Item = T::Item;
97 fn get(&self) -> T::Item {
98 self.r().get()
99 }
100}