Skip to main content

oxstack_cardputer/
ble.rs

1//! Bluetooth LE: Espressif's controller (via `esp-radio`) under the
2//! pure-Rust `trouble` host stack. Requires the `ble` feature.
3//!
4//! ```ignore
5//! let controller = ble::controller(board.radio.bt.reborrow())?;
6//! let mut resources = ble::ScanResources::new();
7//! let stack = ble::stack(controller, &mut resources);
8//! loop {
9//!     ble::scan(&stack, Duration::from_secs(5), |adv| {
10//!         info!("{} dBm {} {:?}", adv.rssi, adv.address, adv.name());
11//!     })
12//!     .await?;
13//! }
14//! ```
15//!
16//! GATT services are declared in the application with trouble's
17//! `#[gatt_server]` / `#[gatt_service]` macros (see `ble_peripheral`).
18//!
19//! Create the controller and stack **once per boot**: trouble keeps the
20//! host inside the resources without ever dropping it, so a second
21//! controller would find the first still registered with the radio.
22//! Scans, advertising sessions and connections on that stack can be
23//! started as often as needed.
24//!
25//! BLE alone fits in the reclaimed-RAM heap. Running Wi-Fi at the same time
26//! (coexistence) needs more; see [`crate::radio`].
27
28use core::cell::RefCell;
29
30pub use bt_hci;
31use bt_hci::param::LeAdvReportsIter;
32use embassy_futures::select::{Either, select};
33use embassy_time::{Duration, Timer};
34use esp_hal::efuse::{self, InterfaceMacAddress};
35use esp_hal::peripherals::BT;
36use esp_radio::ble::controller::{BleConnector, BleConnectorError};
37pub use trouble_host;
38use trouble_host::prelude::{
39    AdStructure, Address, DefaultPacketPool, EventHandler, ExternalController, HostResources,
40    ScanConfig, Scanner, Stack,
41};
42
43/// HCI command slots the controller adapter can have in flight (the value
44/// trouble's ESP32 examples use).
45pub const COMMAND_SLOTS: usize = 20;
46
47/// The HCI controller type to hand to `trouble_host::new`.
48pub type Controller<'d> = ExternalController<BleConnector<'d>, COMMAND_SLOTS>;
49
50/// Error starting the BLE controller.
51pub type InitError = esp_radio::ble::controller::BleInitError;
52
53/// Errors from the host stack.
54pub type HostError = trouble_host::BleHostError<BleConnectorError>;
55
56/// Host resources sized for scanning (one connection slot, one channel).
57/// Peripherals with GATT services usually want more; see the trouble docs.
58pub type ScanResources<'d> = HostResources<Controller<'d>, DefaultPacketPool, 1, 1>;
59
60/// Starts the Bluetooth controller.
61pub fn controller<'d>(bt: BT<'d>) -> Result<Controller<'d>, InitError> {
62    Ok(ExternalController::new(BleConnector::new(
63        bt,
64        Default::default(),
65    )?))
66}
67
68/// This device's BLE address: a static random address derived from the
69/// chip's factory Bluetooth MAC, so it is stable across boots and unique per
70/// board. (Static random addresses must have the two top bits set.)
71pub fn address() -> Address {
72    let mac = efuse::interface_mac_address(InterfaceMacAddress::Bluetooth);
73    let mut le = [0u8; 6];
74    for (dst, src) in le.iter_mut().zip(mac.as_bytes().iter().rev()) {
75        *dst = *src; // BLE addresses are little-endian on the air.
76    }
77    le[5] |= 0xC0;
78    Address::random(le)
79}
80
81/// Builds the host stack on `controller` with this device's [`address`].
82pub fn stack<
83    'r,
84    'd,
85    const CONNS: usize,
86    const CHANNELS: usize,
87    const ADV: usize,
88    const BONDS: usize,
89>(
90    controller: Controller<'d>,
91    resources: &'r mut HostResources<
92        Controller<'d>,
93        DefaultPacketPool,
94        CONNS,
95        CHANNELS,
96        ADV,
97        BONDS,
98    >,
99) -> Stack<'r, Controller<'d>, DefaultPacketPool> {
100    trouble_host::new(controller, resources)
101        .set_random_address(address())
102        .build()
103}
104
105/// One advertising report, as passed to the [`scan`] callback.
106#[derive(Clone, Copy, Debug)]
107pub struct Advertisement<'a> {
108    /// The advertiser's address.
109    pub address: Address,
110    /// Signal strength in dBm.
111    pub rssi: i8,
112    /// Raw advertising (or scan response) data: a sequence of AD structures.
113    pub data: &'a [u8],
114}
115
116impl<'a> Advertisement<'a> {
117    /// The device name, if this report carries one. Names often arrive in
118    /// the scan response rather than the advertisement, so remember the
119    /// first one seen per address.
120    pub fn name(&self) -> Option<&'a str> {
121        let mut short = None;
122        for ad in AdStructure::decode(self.data).flatten() {
123            match ad {
124                AdStructure::CompleteLocalName(n) => return core::str::from_utf8(n).ok(),
125                AdStructure::ShortenedLocalName(n) => short = core::str::from_utf8(n).ok(),
126                _ => {}
127            }
128        }
129        short
130    }
131
132    /// The decoded AD structures (flags, service UUIDs, names, …).
133    pub fn structures(&self) -> impl Iterator<Item = AdStructure<'a>> {
134        AdStructure::decode(self.data).flatten()
135    }
136}
137
138struct Reports<F>(RefCell<F>);
139
140impl<F: FnMut(&Advertisement<'_>)> EventHandler for Reports<F> {
141    fn on_adv_reports(&self, mut reports: LeAdvReportsIter<'_>) {
142        let mut f = self.0.borrow_mut();
143        while let Some(Ok(r)) = reports.next() {
144            f(&Advertisement {
145                address: Address::new(r.addr_kind, r.addr),
146                rssi: r.rssi,
147                data: r.data,
148            });
149        }
150    }
151}
152
153/// Scans for `duration`, calling `on_advertisement` for every report
154/// (duplicates included, so signal strength stays current). Active
155/// scanning, 100 ms interval and window, so names usually arrive too.
156///
157/// Runs the host while scanning; nothing else can use `stack` meanwhile.
158pub async fn scan<'d>(
159    stack: &Stack<'_, Controller<'d>, DefaultPacketPool>,
160    duration: Duration,
161    on_advertisement: impl FnMut(&Advertisement<'_>),
162) -> Result<(), HostError> {
163    let mut runner = stack.runner();
164    let mut scanner = Scanner::new(stack.central());
165    let reports = Reports(RefCell::new(on_advertisement));
166    let config = ScanConfig {
167        active: true,
168        interval: Duration::from_millis(100),
169        window: Duration::from_millis(100),
170        ..Default::default()
171    };
172    let session = async {
173        let session = scanner.scan(&config).await?;
174        Timer::after(duration).await;
175        drop(session);
176        // The session's drop asks the host to disable scanning; give the
177        // runner a moment to send that command before it is stopped.
178        Timer::after(Duration::from_millis(100)).await;
179        Ok(())
180    };
181    match select(runner.run_with_handler(&reports), session).await {
182        Either::First(r) => r,
183        Either::Second(r) => r,
184    }
185}