umsh_ulcp_runtime/
ble_controller.rs

1//! Application-owned controller adapters: fail-closed privacy initialization
2//! and an ACL completion fence for the final response before forgetting a host.
3pub use bt_hci;
4use bt_hci::{
5    ControllerToHostPacket, FromHciBytes, WriteHci,
6    cmd::{self, AsyncCmd, Cmd, SyncCmd, le::*},
7    controller::{Controller, ControllerCmdAsync, ControllerCmdSync},
8    data::{AclPacket, AclPacketBoundary, IsoPacket, SyncPacket},
9    event::{EventKind, NumberOfCompletedPackets},
10};
11use core::cell::RefCell;
12use embassy_sync::blocking_mutex::{Mutex, raw::CriticalSectionRawMutex};
13use trouble_host::{
14    att::{AttClient, AttReq},
15    prelude::Uuid,
16};
17
18/// Includes equality probes, not just reads: Find By Type Value is a name oracle.
19pub fn accesses_device_name(request: AttClient<'_>, name_handle: u16) -> bool {
20    let AttClient::Request(request) = request else {
21        return false;
22    };
23    let in_range = |start, end| start <= name_handle && name_handle <= end;
24    match request {
25        AttReq::Read { handle } | AttReq::ReadBlob { handle, .. } => handle == name_handle,
26        AttReq::ReadMultiple { handles } => handles
27            .chunks_exact(2)
28            .any(|h| u16::from_le_bytes([h[0], h[1]]) == name_handle),
29        AttReq::ReadByType {
30            start,
31            end,
32            attribute_type,
33        } => in_range(start, end) && attribute_type == Uuid::Uuid16([0x00, 0x2a]),
34        AttReq::ReadByGroupType {
35            start,
36            end,
37            group_type,
38        } => in_range(start, end) && group_type == Uuid::Uuid16([0x00, 0x2a]),
39        AttReq::FindByTypeValue {
40            start_handle,
41            end_handle,
42            att_type,
43            ..
44        } => in_range(start_handle, end_handle) && att_type == 0x2a00,
45        _ => false,
46    }
47}
48
49#[derive(Default)]
50struct State {
51    failed: bool,
52    runner_exited: bool,
53    local_irk: bool,
54    resolution: bool,
55    timeout: bool,
56    private_advertiser: bool,
57    advertising_invalidated: bool,
58    sent: u64,
59    completed: u64,
60    disconnected: bool,
61    tx_failed: bool,
62    notify_handle: u16,
63    queued_notifications: u64,
64    written_notifications: u64,
65    notification_remaining: usize,
66    notification_active: bool,
67    fence_notification: u64,
68    fence_packet: Option<u64>,
69    fence_transmitted: bool,
70}
71
72impl State {
73    fn ready(&self) -> bool {
74        !self.failed
75            && !self.runner_exited
76            && !self.advertising_invalidated
77            && self.local_irk
78            && self.resolution
79            && self.timeout
80            && self.private_advertiser
81    }
82    fn acl(&mut self, data: &[u8], continuing: bool) {
83        self.sent += 1;
84        if !continuing {
85            // Firmware notifications always start with the L2CAP/ATT headers.
86            self.notification_active = data.len() >= 7
87                && data[2..4] == [4, 0]
88                && data[4] == 0x1b
89                && u16::from_le_bytes([data[5], data[6]]) == self.notify_handle;
90            self.notification_remaining = if self.notification_active {
91                usize::from(u16::from_le_bytes([data[0], data[1]])) + 4
92            } else {
93                0
94            };
95        }
96        if self.notification_active {
97            self.notification_remaining = self.notification_remaining.saturating_sub(data.len());
98            if self.notification_remaining == 0 {
99                self.notification_active = false;
100                self.written_notifications += 1;
101                if self.fence_notification == self.written_notifications {
102                    self.fence_packet = Some(self.sent);
103                    self.latch_fence();
104                }
105            }
106        }
107    }
108    fn latch_fence(&mut self) {
109        self.fence_transmitted |= !self.tx_failed
110            && !self.disconnected
111            && self
112                .fence_packet
113                .is_some_and(|packet| self.completed >= packet);
114    }
115    fn completed_packets(&mut self, count: u16) {
116        self.completed += u64::from(count);
117        self.latch_fence();
118    }
119    fn fence_done(&self) -> bool {
120        // A host may disconnect immediately after receiving the response.
121        // Preserve a completion observed before that disconnect; completion
122        // credits flushed after disconnection cannot authorize a handoff.
123        self.fence_transmitted
124    }
125}
126
127pub struct ControllerState(
128    Mutex<CriticalSectionRawMutex, RefCell<State>>,
129    embassy_sync::signal::Signal<CriticalSectionRawMutex, ()>,
130);
131impl Default for ControllerState {
132    fn default() -> Self {
133        Self::new()
134    }
135}
136impl ControllerState {
137    pub const fn new() -> Self {
138        Self(
139            Mutex::new(RefCell::new(State {
140                failed: false,
141                runner_exited: false,
142                local_irk: false,
143                resolution: false,
144                timeout: false,
145                private_advertiser: false,
146                advertising_invalidated: false,
147                sent: 0,
148                completed: 0,
149                disconnected: false,
150                tx_failed: false,
151                notify_handle: 0,
152                queued_notifications: 0,
153                written_notifications: 0,
154                notification_remaining: 0,
155                notification_active: false,
156                fence_notification: 0,
157                fence_packet: None,
158                fence_transmitted: false,
159            })),
160            embassy_sync::signal::Signal::new(),
161        )
162    }
163    fn with<R>(&self, f: impl FnOnce(&mut State) -> R) -> R {
164        self.0.lock(|s| f(&mut s.borrow_mut()))
165    }
166    /// Called only after the previous runner and stack have completely stopped.
167    pub fn reset(&self) {
168        self.with(|s| *s = State::default());
169        self.1.reset();
170    }
171    /// A pairing-boundary transition invalidates an in-flight configuration.
172    /// Only rebuilding the host, with a fresh advertising address, clears it.
173    pub fn invalidate_advertising(&self) {
174        self.with(|s| s.advertising_invalidated = true);
175        self.1.signal(());
176    }
177    pub fn failed(&self) -> bool {
178        self.with(|s| s.failed)
179    }
180    pub fn fail(&self) {
181        self.with(|s| s.failed = true);
182        self.1.signal(());
183    }
184    /// Transport/runner exits request reconstruction, not a privacy lockout.
185    /// A privacy failure remains latched even if the runner subsequently exits.
186    pub fn runner_exited(&self) {
187        self.with(|s| s.runner_exited = true);
188        self.1.signal(());
189    }
190    pub fn needs_recovery(&self) -> bool {
191        self.with(|s| s.runner_exited && !s.failed)
192    }
193    /// Trouble publishes its generic initialized flag before syncing privacy.
194    /// Do not request an advertising operation until that sync has completed:
195    /// an already active operation can make the host skip resolving-list sync.
196    pub async fn wait_for_privacy(&self) -> bool {
197        loop {
198            let status = self.with(|s| {
199                if s.failed || s.runner_exited || s.advertising_invalidated {
200                    Some(false)
201                } else if s.local_irk && s.resolution && s.timeout {
202                    Some(true)
203                } else {
204                    None
205                }
206            });
207            if let Some(ready) = status {
208                return ready;
209            }
210            self.1.wait().await;
211        }
212    }
213    /// Count every firmware notification, before enqueueing it. The ordinal
214    /// distinguishes even byte-identical earlier responses still in the queue.
215    pub fn notification(&self, handle: u16, final_reply: bool) {
216        self.with(|s| {
217            s.notify_handle = handle;
218            s.queued_notifications += 1;
219            if final_reply {
220                s.fence_notification = s.queued_notifications;
221                s.fence_packet = None;
222                s.fence_transmitted = false;
223            }
224        });
225    }
226    pub fn fence_done(&self) -> bool {
227        self.with(|s| s.fence_done())
228    }
229    pub fn link_failed(&self) -> bool {
230        self.with(|s| s.tx_failed || s.disconnected)
231    }
232}
233
234/// Owns the controller on ESP32; on nRF it owns a forwarding borrow so the
235/// SDC and its hardware resources survive host-stack reconstruction.
236pub struct Guarded<C> {
237    inner: C,
238    state: &'static ControllerState,
239}
240impl<C> Guarded<C> {
241    pub fn new(inner: C, state: &'static ControllerState) -> Self {
242        Self { inner, state }
243    }
244}
245impl<C: embedded_io::ErrorType> embedded_io::ErrorType for Guarded<C> {
246    type Error = C::Error;
247}
248impl<C: Controller> Controller for Guarded<C> {
249    async fn write_acl_data(&self, packet: &AclPacket<'_>) -> Result<(), Self::Error> {
250        // Reserve the sequence before awaiting: a controller completion can
251        // race the return of the write future on another runner branch.
252        self.state.with(|s| {
253            s.acl(
254                packet.data(),
255                packet.boundary_flag() == AclPacketBoundary::Continuing,
256            )
257        });
258        let result = self.inner.write_acl_data(packet).await;
259        if result.is_err() {
260            self.state.with(|s| s.tx_failed = true);
261        }
262        result
263    }
264    async fn write_sync_data(&self, packet: &SyncPacket<'_>) -> Result<(), Self::Error> {
265        self.inner.write_sync_data(packet).await
266    }
267    async fn write_iso_data(&self, packet: &IsoPacket<'_>) -> Result<(), Self::Error> {
268        self.inner.write_iso_data(packet).await
269    }
270    async fn read<'a>(&self, buf: &'a mut [u8]) -> Result<ControllerToHostPacket<'a>, Self::Error> {
271        let packet = self.inner.read(buf).await?;
272        if let ControllerToHostPacket::Event(event) = &packet {
273            match event.kind {
274                EventKind::NumberOfCompletedPackets => {
275                    if let Ok(completed) =
276                        NumberOfCompletedPackets::from_hci_bytes_complete(event.data)
277                    {
278                        for entry in completed.completed_packets {
279                            if let Ok(count) = entry.num_completed_packets() {
280                                self.state.with(|s| s.completed_packets(count));
281                            }
282                        }
283                    }
284                }
285                EventKind::DisconnectionComplete => self.state.with(|s| s.disconnected = true),
286                _ => {}
287            }
288        }
289        Ok(packet)
290    }
291}
292
293impl<C, T> ControllerCmdSync<T> for Guarded<C>
294where
295    C: ControllerCmdSync<T> + ControllerCmdSync<LeReadResolvingListSize>,
296    T: SyncCmd,
297{
298    async fn exec(&self, command: &T) -> Result<T::Return, cmd::Error<Self::Error>> {
299        let opcode = T::OPCODE;
300        let tracked = opcode == LeSetAdvParams::OPCODE
301            || opcode == LeSetAdvEnable::OPCODE
302            || opcode == LeAddDeviceToResolvingList::OPCODE
303            || opcode == LeSetPrivacyMode::OPCODE
304            || opcode == LeClearResolvingList::OPCODE
305            || opcode == LeSetAddrResolutionEnable::OPCODE
306            || opcode == LeSetResolvablePrivateAddrTimeout::OPCODE;
307        if !tracked {
308            return self.inner.exec(command).await;
309        }
310        let mut bytes = [0; 40];
311        command
312            .params()
313            .write_hci(&mut bytes[..])
314            .map_err(|_| cmd::Error::Hci(bt_hci::param::Error::INVALID_HCI_PARAMETERS))?;
315        if opcode == LeSetAdvEnable::OPCODE && bytes[0] != 0 && !self.state.with(|s| s.ready()) {
316            return Err(cmd::Error::Hci(bt_hci::param::Error::CMD_DISALLOWED));
317        }
318        if opcode == LeClearResolvingList::OPCODE {
319            let capacity = self.inner.exec(&LeReadResolvingListSize::new()).await;
320            // Four retained peers plus Trouble's local-IRK sentinel entry.
321            match capacity {
322                Ok(size) if size >= 5 => {}
323                result => {
324                    crate::log::debug_log(format_args!(
325                        "BLE privacy resolving-list capacity {result:?}; require at least 5"
326                    ));
327                    self.state.fail();
328                    return Err(cmd::Error::Hci(bt_hci::param::Error::CMD_DISALLOWED));
329                }
330            }
331            self.state.with(|s| {
332                s.local_irk = false;
333                s.resolution = false;
334            });
335        }
336        let result = self.inner.exec(command).await;
337        if let Err(error) = &result {
338            crate::log::debug_log(format_args!(
339                "BLE privacy HCI command 0x{:04x} failed: {error:?}",
340                opcode.to_raw()
341            ));
342        }
343        self.state.with(|s| {
344            if result.is_err() {
345                s.failed = true;
346                return;
347            }
348            if opcode == LeAddDeviceToResolvingList::OPCODE && bytes[..7] == [0; 7] {
349                s.local_irk = bytes[23..39] != [0; 16];
350                if !s.local_irk {
351                    s.failed = true;
352                }
353            }
354            if opcode == LeSetAddrResolutionEnable::OPCODE {
355                s.resolution = bytes[0] != 0;
356            }
357            if opcode == LeSetResolvablePrivateAddrTimeout::OPCODE {
358                s.timeout = u64::from(u16::from_le_bytes([bytes[0], bytes[1]]))
359                    == crate::ble_privacy::RPA_TIMEOUT_SECS;
360            }
361            if opcode == LeSetAdvParams::OPCODE {
362                s.private_advertiser = matches!(bytes[5], 2 | 3);
363            }
364        });
365        self.state.1.signal(());
366        result
367    }
368}
369impl<C: ControllerCmdAsync<T>, T: AsyncCmd> ControllerCmdAsync<T> for Guarded<C> {
370    async fn exec(&self, command: &T) -> Result<(), cmd::Error<Self::Error>> {
371        self.inner.exec(command).await
372    }
373}
374
375pub struct Borrowed<'a, C>(pub &'a C);
376impl<C: embedded_io::ErrorType> embedded_io::ErrorType for Borrowed<'_, C> {
377    type Error = C::Error;
378}
379impl<C: Controller> Controller for Borrowed<'_, C> {
380    async fn write_acl_data(&self, p: &AclPacket<'_>) -> Result<(), Self::Error> {
381        self.0.write_acl_data(p).await
382    }
383    async fn write_sync_data(&self, p: &SyncPacket<'_>) -> Result<(), Self::Error> {
384        self.0.write_sync_data(p).await
385    }
386    async fn write_iso_data(&self, p: &IsoPacket<'_>) -> Result<(), Self::Error> {
387        self.0.write_iso_data(p).await
388    }
389    async fn read<'a>(&self, buf: &'a mut [u8]) -> Result<ControllerToHostPacket<'a>, Self::Error> {
390        self.0.read(buf).await
391    }
392}
393impl<C: ControllerCmdSync<T>, T: SyncCmd> ControllerCmdSync<T> for Borrowed<'_, C> {
394    async fn exec(&self, command: &T) -> Result<T::Return, cmd::Error<Self::Error>> {
395        self.0.exec(command).await
396    }
397}
398impl<C: ControllerCmdAsync<T>, T: AsyncCmd> ControllerCmdAsync<T> for Borrowed<'_, C> {
399    async fn exec(&self, command: &T) -> Result<(), cmd::Error<Self::Error>> {
400        self.0.exec(command).await
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    #[test]
408    fn every_supported_name_read_and_equality_probe_is_guarded() {
409        let reads = [
410            AttReq::Read { handle: 3 },
411            AttReq::ReadBlob {
412                handle: 3,
413                offset: 12,
414            },
415            AttReq::ReadMultiple {
416                handles: &[2, 0, 3, 0],
417            },
418            AttReq::ReadByType {
419                start: 1,
420                end: 9,
421                attribute_type: Uuid::Uuid16([0x00, 0x2a]),
422            },
423            AttReq::ReadByGroupType {
424                start: 1,
425                end: 9,
426                group_type: Uuid::Uuid16([0x00, 0x2a]),
427            },
428            AttReq::FindByTypeValue {
429                start_handle: 1,
430                end_handle: 9,
431                att_type: 0x2a00,
432                att_value: b"Alice",
433            },
434        ];
435        for read in reads {
436            assert!(accesses_device_name(AttClient::Request(read), 3));
437        }
438        assert!(!accesses_device_name(
439            AttClient::Request(AttReq::Read { handle: 7 }),
440            3
441        ));
442        assert!(!accesses_device_name(
443            AttClient::Request(AttReq::ReadByType {
444                start: 4,
445                end: 9,
446                attribute_type: Uuid::Uuid16([0x00, 0x2a])
447            }),
448            3
449        ));
450    }
451    #[test]
452    fn fence_waits_for_the_right_notification_and_last_acl_fragment() {
453        let mut s = State {
454            notify_handle: 3,
455            fence_notification: 2,
456            ..State::default()
457        };
458        let earlier = [4, 0, 4, 0, 0x1b, 3, 0, 42];
459        s.acl(&earlier, false);
460        s.completed_packets(1);
461        assert!(!s.fence_done());
462        s.acl(&[6, 0, 4, 0, 0x1b, 3, 0, 42], false);
463        s.completed_packets(1);
464        assert!(!s.fence_done());
465        s.acl(&[43, 44], true);
466        assert!(!s.fence_done());
467        s.completed_packets(1);
468        assert!(s.fence_done());
469        s.disconnected = true;
470        assert!(
471            s.fence_done(),
472            "a subsequent disconnect cannot undo a transmitted response"
473        );
474        let mut canceled = State {
475            notify_handle: 3,
476            fence_notification: 1,
477            ..State::default()
478        };
479        canceled.acl(&earlier, false);
480        canceled.disconnected = true;
481        canceled.completed_packets(1);
482        assert!(
483            !canceled.fence_done(),
484            "disconnect credits cannot confirm transmission"
485        );
486    }
487    #[test]
488    fn privacy_is_fail_closed() {
489        let mut s = State::default();
490        assert!(!s.ready());
491        s.local_irk = true;
492        s.resolution = true;
493        s.timeout = true;
494        s.private_advertiser = true;
495        assert!(s.ready());
496        s.failed = true;
497        assert!(!s.ready());
498    }
499    struct Fake {
500        capacity: u8,
501        fail: Option<cmd::Opcode>,
502        calls: RefCell<std::vec::Vec<(cmd::Opcode, std::vec::Vec<u8>)>>,
503    }
504    impl embedded_io::ErrorType for Fake {
505        type Error = core::convert::Infallible;
506    }
507    impl Controller for Fake {
508        async fn write_acl_data(&self, _: &AclPacket<'_>) -> Result<(), Self::Error> {
509            Ok(())
510        }
511        async fn write_sync_data(&self, _: &SyncPacket<'_>) -> Result<(), Self::Error> {
512            Ok(())
513        }
514        async fn write_iso_data(&self, _: &IsoPacket<'_>) -> Result<(), Self::Error> {
515            Ok(())
516        }
517        async fn read<'a>(
518            &self,
519            _: &'a mut [u8],
520        ) -> Result<ControllerToHostPacket<'a>, Self::Error> {
521            core::future::pending().await
522        }
523    }
524    impl<T: SyncCmd> ControllerCmdSync<T> for Fake {
525        async fn exec(&self, command: &T) -> Result<T::Return, cmd::Error<Self::Error>> {
526            let mut data = [0; 40];
527            command.params().write_hci(&mut data[..]).unwrap();
528            self.calls
529                .borrow_mut()
530                .push((T::OPCODE, data[..command.params().size()].to_vec()));
531            if self.fail == Some(T::OPCODE) {
532                return Err(cmd::Error::Hci(bt_hci::param::Error::CMD_DISALLOWED));
533            }
534            let capacity = [self.capacity];
535            let result: &[u8] = if T::OPCODE == LeReadResolvingListSize::OPCODE {
536                &capacity
537            } else {
538                &[]
539            };
540            Ok(T::Return::from_hci_bytes_complete(result).unwrap())
541        }
542    }
543    fn controller(capacity: u8, fail: Option<cmd::Opcode>) -> Guarded<Fake> {
544        Guarded::new(
545            Fake {
546                capacity,
547                fail,
548                calls: RefCell::new(std::vec::Vec::new()),
549            },
550            Box::leak(Box::new(ControllerState::new())),
551        )
552    }
553    fn advertising_params(private: bool) -> LeSetAdvParams {
554        use bt_hci::param::*;
555        LeSetAdvParams::new(
556            Duration::from_micros(crate::ble_privacy::ADVERTISING_INTERVAL_US),
557            Duration::from_micros(crate::ble_privacy::ADVERTISING_INTERVAL_US),
558            AdvKind::AdvInd,
559            if private {
560                AddrKind::RESOLVABLE_PRIVATE_OR_RANDOM
561            } else {
562                AddrKind::RANDOM
563            },
564            AddrKind::PUBLIC,
565            BdAddr::default(),
566            AdvChannelMap::ALL,
567            AdvFilterPolicy::default(),
568        )
569    }
570    async fn setup(c: &Guarded<Fake>) {
571        use bt_hci::param::*;
572        c.exec(&LeSetResolvablePrivateAddrTimeout::new(
573            Duration::from_secs(crate::ble_privacy::RPA_TIMEOUT_SECS.try_into().unwrap()),
574        ))
575        .await
576        .unwrap();
577        c.exec(&LeClearResolvingList::new()).await.unwrap();
578        c.exec(&LeAddDeviceToResolvingList::new(
579            AddrKind::PUBLIC,
580            BdAddr::default(),
581            [0; 16],
582            [1; 16],
583        ))
584        .await
585        .unwrap();
586        c.exec(&LeSetAddrResolutionEnable::new(true)).await.unwrap();
587        c.exec(&advertising_params(true)).await.unwrap();
588    }
589    #[test]
590    fn hci_enable_requires_capacity_local_irk_timeout_and_private_address() {
591        embassy_futures::block_on(async {
592            let c = controller(5, None);
593            assert!(c.exec(&LeSetAdvEnable::new(true)).await.is_err());
594            setup(&c).await;
595            c.exec(&LeSetAdvEnable::new(true)).await.unwrap();
596            let calls = c.inner.calls.borrow();
597            let (_, bytes) = calls
598                .iter()
599                .find(|(op, _)| *op == LeSetAdvParams::OPCODE)
600                .unwrap();
601            assert_eq!(&bytes[..4], &[0x6a, 0x03, 0x6a, 0x03]); // 874 x 625 us
602            drop(calls);
603            c.exec(&advertising_params(false)).await.unwrap();
604            assert!(c.exec(&LeSetAdvEnable::new(true)).await.is_err());
605            c.exec(&advertising_params(true)).await.unwrap();
606            c.state.invalidate_advertising();
607            assert!(c.exec(&LeSetAdvEnable::new(true)).await.is_err());
608            assert!(!c.state.failed()); // expected pairing-boundary restart, not a fault
609        });
610    }
611    #[test]
612    fn controller_failures_cannot_fall_back_to_static_advertising() {
613        embassy_futures::block_on(async {
614            for capacity in [0, 4] {
615                let c = controller(capacity, None);
616                assert!(c.exec(&LeClearResolvingList::new()).await.is_err());
617                assert!(c.state.failed());
618                assert!(c.exec(&LeSetAdvEnable::new(true)).await.is_err());
619            }
620            let c = controller(5, Some(LeAddDeviceToResolvingList::OPCODE));
621            c.exec(&LeClearResolvingList::new()).await.unwrap();
622            use bt_hci::param::*;
623            assert!(
624                c.exec(&LeAddDeviceToResolvingList::new(
625                    AddrKind::PUBLIC,
626                    BdAddr::default(),
627                    [0; 16],
628                    [1; 16]
629                ))
630                .await
631                .is_err()
632            );
633            // Trouble tolerates this command failure; the adapter must not.
634            assert!(c.state.failed());
635            assert!(c.exec(&LeSetAdvEnable::new(true)).await.is_err());
636        });
637    }
638    #[test]
639    fn advertising_waits_for_privacy_initialization_or_explicit_failure() {
640        use core::{
641            future::Future,
642            task::{Context, Poll, Waker},
643        };
644        let c = controller(5, None);
645        let mut wait = core::pin::pin!(c.state.wait_for_privacy());
646        let mut cx = Context::from_waker(Waker::noop());
647        assert_eq!(wait.as_mut().poll(&mut cx), Poll::Pending);
648        embassy_futures::block_on(setup(&c));
649        assert_eq!(wait.as_mut().poll(&mut cx), Poll::Ready(true));
650        c.state.reset();
651        let mut wait = core::pin::pin!(c.state.wait_for_privacy());
652        assert_eq!(wait.as_mut().poll(&mut cx), Poll::Pending);
653        c.state.fail();
654        assert_eq!(wait.as_mut().poll(&mut cx), Poll::Ready(false));
655        c.state.reset();
656        c.state.invalidate_advertising();
657        assert!(!embassy_futures::block_on(c.state.wait_for_privacy()));
658    }
659
660    #[test]
661    fn runner_recovery_never_clears_a_privacy_failure() {
662        let c = controller(5, None);
663        embassy_futures::block_on(setup(&c));
664        c.state.runner_exited();
665        assert!(c.state.needs_recovery());
666        assert!(!c.state.failed());
667        assert!(!embassy_futures::block_on(c.state.wait_for_privacy()));
668        assert!(embassy_futures::block_on(c.exec(&LeSetAdvEnable::new(true))).is_err());
669        c.state.fail();
670        c.state.runner_exited();
671        assert!(c.state.failed());
672        assert!(!c.state.needs_recovery());
673        c.state.reset();
674        assert!(!c.state.needs_recovery());
675        embassy_futures::block_on(setup(&c));
676        assert!(embassy_futures::block_on(c.exec(&LeSetAdvEnable::new(true))).is_ok());
677    }
678}