umsh_ulcp_runtime/
transport_policy.rs

1//! Pure ULCP session transport arbitration.
2
3#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4pub enum Transport {
5    Usb,
6    Ble,
7}
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct SessionArbitration {
11    active: Option<Transport>,
12    generation: u32,
13}
14
15impl SessionArbitration {
16    pub const fn new(generation: u32) -> Self {
17        Self {
18            active: None,
19            generation,
20        }
21    }
22
23    /// Start a fresh protocol session for `transport` and invalidate every
24    /// frame queued by the previous session.
25    pub fn attach(&mut self, transport: Transport) {
26        self.generation = self.generation.wrapping_add(1);
27        self.active = Some(transport);
28    }
29
30    /// Detach only if the edge belongs to the active session. A late detach
31    /// from a displaced transport must not tear down its replacement.
32    pub fn detach(&mut self, transport: Transport) -> bool {
33        if self.active == Some(transport) {
34            self.active = None;
35            true
36        } else {
37            false
38        }
39    }
40
41    #[cfg(test)]
42    pub const fn active(&self) -> Option<Transport> {
43        self.active
44    }
45
46    pub const fn generation(&self) -> u32 {
47        self.generation
48    }
49
50    pub fn accepts_frame(&self, transport: Transport) -> bool {
51        self.active == Some(transport)
52    }
53
54    pub const fn destination(&self) -> Option<(Transport, u32)> {
55        match self.active {
56            Some(transport) => Some((transport, self.generation)),
57            None => None,
58        }
59    }
60
61    #[cfg(test)]
62    pub fn is_current(&self, generation: u32) -> bool {
63        self.generation == generation
64    }
65
66    pub fn advertising_allowed(&self) -> bool {
67        self.active != Some(Transport::Usb)
68    }
69}
70
71/// Stop yielding transport writes as soon as their session generation is
72/// displaced. The generation is checked on every `next()` call, which means an
73/// async caller checks again after each completed USB write or BLE notify.
74pub struct GenerationChecked<I, L> {
75    items: I,
76    load_generation: L,
77    expected: u32,
78    stale: bool,
79}
80
81impl<I, L> GenerationChecked<I, L> {
82    pub const fn stale(&self) -> bool {
83        self.stale
84    }
85}
86
87impl<I, L> Iterator for GenerationChecked<I, L>
88where
89    I: Iterator,
90    L: FnMut() -> u32,
91{
92    type Item = I::Item;
93
94    fn next(&mut self) -> Option<Self::Item> {
95        if (self.load_generation)() != self.expected {
96            self.stale = true;
97            return None;
98        }
99        self.items.next()
100    }
101}
102
103pub fn generation_checked<I, L>(
104    items: I,
105    expected: u32,
106    load_generation: L,
107) -> GenerationChecked<I::IntoIter, L>
108where
109    I: IntoIterator,
110    L: FnMut() -> u32,
111{
112    GenerationChecked {
113        items: items.into_iter(),
114        load_generation,
115        expected,
116        stale: false,
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use core::cell::Cell;
124
125    #[test]
126    fn attachment_displaces_previous_transport_and_invalidates_frames() {
127        let mut state = SessionArbitration::new(10);
128        state.attach(Transport::Ble);
129        let ble_generation = state.generation();
130        assert!(state.accepts_frame(Transport::Ble));
131        assert!(state.advertising_allowed());
132
133        state.attach(Transport::Usb);
134        assert_eq!(state.active(), Some(Transport::Usb));
135        assert!(!state.accepts_frame(Transport::Ble));
136        assert!(state.accepts_frame(Transport::Usb));
137        assert!(!state.is_current(ble_generation));
138        assert!(!state.advertising_allowed());
139    }
140
141    #[test]
142    fn late_displaced_detach_does_not_clear_replacement() {
143        let mut state = SessionArbitration::new(0);
144        state.attach(Transport::Ble);
145        state.attach(Transport::Usb);
146        assert!(!state.detach(Transport::Ble));
147        assert_eq!(state.active(), Some(Transport::Usb));
148        assert!(state.detach(Transport::Usb));
149        assert_eq!(state.active(), None);
150        assert!(state.advertising_allowed());
151    }
152
153    #[test]
154    fn no_session_has_no_output_destination_or_accepted_frames() {
155        let state = SessionArbitration::new(7);
156        assert_eq!(state.destination(), None);
157        assert!(!state.accepts_frame(Transport::Usb));
158        assert!(!state.accepts_frame(Transport::Ble));
159        assert!(state.advertising_allowed());
160    }
161
162    #[test]
163    fn generation_wrap_still_invalidates_previous_session() {
164        let mut state = SessionArbitration::new(u32::MAX);
165        assert!(!state.is_current(0));
166        state.attach(Transport::Ble);
167        assert_eq!(state.destination(), Some((Transport::Ble, 0)));
168        assert!(state.is_current(0));
169        state.attach(Transport::Usb);
170        assert_eq!(state.generation(), 1);
171        assert!(!state.is_current(0));
172    }
173
174    #[test]
175    fn generation_checked_stops_between_transport_writes() {
176        let generation = Cell::new(4);
177        let mut writes = generation_checked(["one", "two", "three"], 4, || generation.get());
178        assert_eq!(writes.next(), Some("one"));
179
180        // Models displacement while the first async write is awaiting its
181        // completion. The next chunk/segment must never be returned.
182        generation.set(5);
183        assert_eq!(writes.next(), None);
184        assert!(writes.stale());
185    }
186
187    #[test]
188    fn generation_checked_yields_every_write_for_current_session() {
189        let writes: heapless::Vec<_, 4> = generation_checked(0..4, 9, || 9).collect();
190        assert_eq!(writes.as_slice(), &[0, 1, 2, 3]);
191    }
192}