From 99e3aa9a47134793963e2e6af71719a7dab508d0 Mon Sep 17 00:00:00 2001 From: Ferdy Date: Fri, 28 Nov 2025 12:46:44 -0700 Subject: [PATCH 1/8] Add BT GATT client and bt fixes --- .github/configs/sdkconfig.defaults | 6 +- .gitignore | 1 + CHANGELOG.md | 8 +- examples/bt_ble_gap_scanner.rs | 4 +- examples/bt_gatt_client.rs | 634 +++++++++ examples/bt_gatt_server.rs | 41 +- src/bt/ble/gap.rs | 249 +++- src/bt/ble/gatt.rs | 6 + src/bt/ble/gatt/client.rs | 1903 ++++++++++++++++++++++++++++ src/bt/ble/gatt/server.rs | 2 +- 10 files changed, 2817 insertions(+), 37 deletions(-) create mode 100644 examples/bt_gatt_client.rs create mode 100644 src/bt/ble/gatt/client.rs diff --git a/.github/configs/sdkconfig.defaults b/.github/configs/sdkconfig.defaults index 8ed0ed300ee..cc99cb69b82 100644 --- a/.github/configs/sdkconfig.defaults +++ b/.github/configs/sdkconfig.defaults @@ -19,15 +19,15 @@ CONFIG_ETH_SPI_ETHERNET_DM9051=y CONFIG_ETH_SPI_ETHERNET_W5500=y CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL=y -# We don't have an example for classic BT - yet - we need to enable class BT -# specifically to workaround this bug in ESP IDF v5.2 (fixed in ESP IDF v5.2.1+): +# We need to enable class specifically to workaround this bug in ESP IDF v5.2 (fixed in ESP IDF v5.2.1+): # https://github.com/espressif/esp-idf/issues/13113 +# And, required for BT Classic examples CONFIG_BT_CLASSIC_ENABLED=y # BLE with Bluedroid CONFIG_BT_ENABLED=y CONFIG_BT_BLUEDROID_ENABLED=y -# Dual mode needed for SPP demo +# Dual mode needed for SPP demo CONFIG_BTDM_CTRL_MODE_BLE_ONLY=n CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n CONFIG_BTDM_CTRL_MODE_BTDM=y diff --git a/.gitignore b/.gitignore index 8daec69797e..98e49f90fa9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.vscode +/.zed /.espressif /.embuild /target diff --git a/CHANGELOG.md b/CHANGELOG.md index b50df7dd5da..f8dae430f18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking - BT: `BtDriver::set_device_name` deprecated and not available with ESP-IDF 6.0+. Use the new `EspGap::set_device_name` instead or the existing `EspBleGap::set_device_name` +- BT: `BleGapEvent::ScanResult` is now a struct instead of a wrapper around esp_ble_gap_cb_param_t_ble_scan_result_evt_param - Implement MQTT outbox limit and get_outbox_size() - Added argument `subprotocol_list` to `ws_handler` to allow subprotocols to be supported by WebSockets - Thread enhancements (#592). Specifically: @@ -29,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `WifiDriver::get_ap_info` not takes `&self` instead of `&mut self`. Convenience method `EspWifi::get_ap_info` that delegates to `WifiDriver::get_ap_info` - BT: Fix BLE not working on the s3 and with ESP-IDF 5.3+ +- BT: Fix `bt_gatt_server` example not allowing reconnect after client connect (#553) and handle sububscribe/unsubscribe to indications +- BT: Fix `EspBleGap::set_security_conf` not setting auth_req_mode, and returning ESP_ERR_INVALID_ARG when setting key sizes - Fix wrong conversion from `ScanType` to `u32` in Wi-Fi configuration - Fix wrong BT configuration version on the c6 (issue #556) - Fix inconsistent mutability in NVS (#567) @@ -46,10 +49,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - OTA: New method - `EspFirmwareInfoLoad::fetch_native` - returning the full native ESP-IDF image descriptor structures - Added `use_serde` feature, which enables the `use_serde` feature of `embedded-svc` crate, allowing to deserialize configuration structs. - OTA: Allow specifying image size to speed up erase -- Bluetooth: New methods `EspBleGap::start_scanning` and `EspBleGap::stop_scanning` +- Bluetooth: New methods `EspBleGap::set_scan_params`, `EspBleGap::start_scanning`, `EspBleGap::stop_scanning`, + `EspBleGap::resolve_adv_data_by_type`, `EspBleGap::disconnect` and `gatt::set_local_mtu` +- Bluetooth: New BLE Gatt Client `EspGattc` - Bluetooth Classic: Added Serial Port Profile, `spp` - New example, `bt_spp_acceptor` to demonstrate usage of bt classic spp profile - New example, `bt_ble_gap_scanner` to demonstrate usage of added ble scanning methods +- New example, `bt_gatt_client` to demonstrate usage of added ble gatt client - New example, `mdns_advertise` to demonstrate mDNS service advertisement - NVS: Implemented `RawHandle` for `EspNvs` - NVS: Added `EspNvs::erase_all` to remove all data stored in an nvs namespace diff --git a/examples/bt_ble_gap_scanner.rs b/examples/bt_ble_gap_scanner.rs index f1a4984d9b1..fa81ed9d149 100644 --- a/examples/bt_ble_gap_scanner.rs +++ b/examples/bt_ble_gap_scanner.rs @@ -135,9 +135,9 @@ mod example { fn on_gap_event(&self, event: BleGapEvent) -> Result<(), EspError> { trace!("Got event: {event:?}"); - if let BleGapEvent::ScanResult(result) = event { + if let BleGapEvent::ScanResult { bda, .. } = event { let mut state = self.state.lock().unwrap(); - let address = BluetoothAddress(BdAddr::from_bytes(result.bda)); + let address = BluetoothAddress(bda); match state.discovered.insert(address) { Ok(true) => info!("Discovered new device {address}"), Err(_) => warn!("Error while storing address: {address}"), diff --git a/examples/bt_gatt_client.rs b/examples/bt_gatt_client.rs new file mode 100644 index 00000000000..cd0cc9a20c1 --- /dev/null +++ b/examples/bt_gatt_client.rs @@ -0,0 +1,634 @@ +//! Example of a BLE GATT client using the ESP IDF Bluedroid BLE bindings. +//! +//! You can test it with the `bt_gatt_server` +//! +//! The example client will scan for, and connect to the server name ESP32. Once connected the +//! client will subscribe and unsubscribe to the indications and write to the "recv" characteristic. +//! It will then disconnect and reconnect, repeating through the indication and writing again. +//! +//! Note that the Bluedroid stack consumes a lot of memory, so `sdkconfig.defaults` should be carefully configured +//! to avoid running out of memory. +//! +//! Here's a working configuration, but you might need to adjust further to your concrete use-case: +//! +//! CONFIG_BT_ENABLED=y +//! CONFIG_BT_BLUEDROID_ENABLED=y +//! CONFIG_BT_CLASSIC_ENABLED=n +//! CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y +//! CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n +//! CONFIG_BTDM_CTRL_MODE_BTDM=n +//! CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y +//! CONFIG_BT_BLE_50_FEATURES_SUPPORTED=n +//! CONFIG_BT_BTC_TASK_STACK_SIZE=15000 +//! CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY=y + +#![allow(unknown_lints)] +#![allow(unexpected_cfgs)] + +#[cfg(not(esp32s2))] +fn main() -> anyhow::Result<()> { + example::main() +} + +#[cfg(esp32s2)] +fn main() -> anyhow::Result<()> { + panic!("ESP32-S2 does not have a BLE radio"); +} + +#[cfg(not(esp32s2))] +mod example { + use std::sync::{Arc, Condvar, Mutex}; + use std::thread; + use std::time::Duration; + + use esp_idf_svc::bt::ble::gap::{ + AdvertisingDataType, BleAddrType, BleGapEvent, EspBleGap, GapSearchEvent, ScanDuplicate, + ScanFilter, ScanParams, ScanType, + }; + use esp_idf_svc::bt::ble::gatt::client::{ + ConnectionId, DbAttrType, EspGattc, GattAuthReq, GattCreateConnParams, GattWriteType, + GattcEvent, ServiceSource, + }; + use esp_idf_svc::bt::ble::gatt::{self, GattInterface, GattStatus, Handle, Property}; + use esp_idf_svc::bt::{BdAddr, Ble, BtDriver, BtStatus, BtUuid}; + use esp_idf_svc::hal::delay::FreeRtos; + use esp_idf_svc::hal::peripherals::Peripherals; + use esp_idf_svc::log::EspLogger; + use esp_idf_svc::nvs::EspDefaultNvsPartition; + use esp_idf_svc::sys::{EspError, ESP_FAIL}; + + use log::{error, info, warn}; + + pub fn main() -> anyhow::Result<()> { + esp_idf_svc::sys::link_patches(); + EspLogger::initialize_default(); + + let peripherals = Peripherals::take()?; + let nvs = EspDefaultNvsPartition::take()?; + + let bt = Arc::new(BtDriver::new(peripherals.modem, Some(nvs.clone()))?); + + let client = ExampleClient::new( + Arc::new(EspBleGap::new(bt.clone())?), + Arc::new(EspGattc::new(bt.clone())?), + ); + + info!("BLE Gap and Gattc initialized"); + + let gap_client = client.clone(); + + client.gap.subscribe(move |event| { + gap_client.check_esp_status(gap_client.on_gap_event(event)); + })?; + + let gattc_client = client.clone(); + + client.gattc.subscribe(move |(gatt_if, event)| { + gattc_client.check_esp_status(gattc_client.on_gattc_event(gatt_if, event)) + })?; + + info!("BLE Gap and Gattc subscriptions initialized"); + + client.gattc.register_app(APP_ID)?; + + info!("Gattc BTP app registered"); + + gatt::set_local_mtu(500)?; + + info!("Gattc BTP app registered"); + + client.wait_for_write_char_handle(); + let mut write_data = 0_u16; + let mut indicate = true; + + loop { + // Subscribe/unsubscribe to indications + if write_data % 10 == 0 { + client.request_indicate(indicate)?; + indicate = !indicate; + } + + client.write_characterisitic(&write_data.to_le_bytes())?; + + info!("Wrote characteristic: {write_data}"); + + write_data = write_data.wrapping_add(1); + + FreeRtos::delay_ms(5000); + + if write_data % 30 == 0 { + client.disconnect()?; + FreeRtos::delay_ms(5000); + client.connect()?; + } + } + } + + const APP_ID: u16 = 0; + + // bt_gatt_server name + pub const SERVER_NAME: &str = "ESP32"; + // bt_gatt_server service UUID + pub const SERVICE_UUID: BtUuid = BtUuid::uuid128(0xad91b201734740479e173bed82d75f9d); + // Write characteristic UUID + pub const WRITE_CHARACTERISITIC_UUID: BtUuid = + BtUuid::uuid128(0xb6fccb5087be44f3ae22f85485ea42c4); + // Indicate characteristic UUID + pub const IND_CHARACTERISTIC_UUID: BtUuid = BtUuid::uuid128(0x503de214868246c4828fd59144da41be); + // Client Characteristic Configuration UUID + pub const IND_DESCRIPTOR_UUID: BtUuid = BtUuid::uuid16(0x2902); + + // Name the types as they are used in the example to get shorter type signatures in the various functions below. + // note that - rather than `Arc`s, you can use regular references as well, but then you have to deal with lifetimes + // and the signatures below will not be `'static`. + type ExBtDriver = BtDriver<'static, Ble>; + type ExEspBleGap = Arc>>; + type ExEspGattc = Arc>>; + + #[derive(Default)] + struct State { + gattc_if: Option, + conn_id: Option, + remote_addr: Option, + connect: bool, + service_start_end_handle: Option<(Handle, Handle)>, + ind_char_handle: Option, + ind_descr_handle: Option, + write_char_handle: Option, + } + + #[derive(Clone)] + pub struct ExampleClient { + gap: ExEspBleGap, + gattc: ExEspGattc, + state: Arc>, + condvar: Arc, + } + + impl ExampleClient { + pub fn new(gap: ExEspBleGap, gattc: ExEspGattc) -> Self { + Self { + gap, + gattc, + state: Arc::new(Mutex::new(Default::default())), + condvar: Arc::new(Condvar::new()), + } + } + + /// The main event handler for the GAP events + fn on_gap_event(&self, event: BleGapEvent) -> Result<(), EspError> { + info!("Got gap event: {event:?}"); + + match event { + BleGapEvent::ScanParameterConfigured(status) => { + self.check_bt_status(status)?; + self.gap.start_scanning(30)?; + } + BleGapEvent::ScanStarted(status) => { + self.check_bt_status(status)?; + info!("Scanning started"); + } + BleGapEvent::ScanResult { + search_evt, + bda, + ble_addr_type, + rssi, + ble_adv, + adv_data_len, + scan_rsp_len, + .. + } => { + if GapSearchEvent::InquiryResult == search_evt { + let name = self.gap.resolve_adv_data_by_type( + &ble_adv, + adv_data_len as u16 + scan_rsp_len as u16, + AdvertisingDataType::NameCmpl, + ); + let name = name.map(str::from_utf8).transpose().ok().flatten(); + + info!("Scan result, device {bda} - rssi {rssi}, name: {name:?}"); + + if let Some(name) = name.filter(|n| *n == SERVER_NAME) { + info!("Device found: {name:?}"); + + let mut state = self.state.lock().unwrap(); + + if !state.connect { + state.connect = true; + info!("Connect to remote {bda}"); + self.gap.stop_scanning()?; + + let conn_params = GattCreateConnParams::new(bda, ble_addr_type); + + self.gattc.enh_open(state.gattc_if.unwrap(), &conn_params)?; + } + } + + // If there are many devices found the logging tends to take too long and the wdt kicks in + thread::sleep(Duration::from_millis(10)); + } + } + BleGapEvent::ScanStopped(status) => { + self.check_bt_status(status)?; + + info!("Scanning stopped"); + } + BleGapEvent::ConnectionParamsConfigured { + addr, + status, + min_int_ms, + max_int_ms, + latency_ms, + conn_int, + timeout_ms, + } => { + info!("Connection params update addr {addr}, status {status:?}, conn_int {conn_int}, latency {latency_ms}, timeout {timeout_ms}, min_int {min_int_ms}, max_int {max_int_ms}"); + } + BleGapEvent::PacketLengthConfigured { + status, + rx_len, + tx_len, + } => { + info!("Packet length update, status {status:?}, rx {rx_len}, tx {tx_len}"); + } + _ => (), + } + + Ok(()) + } + + /// The main event handler for the GATTC events + fn on_gattc_event( + &self, + gattc_if: GattInterface, + event: GattcEvent, + ) -> Result<(), EspError> { + info!("Got gattc event: {event:?}"); + + match event { + GattcEvent::ClientRegistered { status, app_id } => { + self.check_gatt_status(status)?; + if APP_ID == app_id { + self.state.lock().unwrap().gattc_if = Some(gattc_if); + self.connect()?; + } + } + GattcEvent::Connected { conn_id, addr, .. } => { + let mut state = self.state.lock().unwrap(); + + state.conn_id = Some(conn_id); + state.remote_addr = Some(addr); + + self.gattc.mtu_req(gattc_if, conn_id)?; + } + GattcEvent::Open { + status, addr, mtu, .. + } => { + self.check_gatt_status(status)?; + + info!("Open successfully with {addr}, MTU {mtu}"); + } + GattcEvent::DiscoveryCompleted { status, conn_id } => { + self.check_gatt_status(status)?; + + info!("Service discover complete, conn_id {conn_id}"); + + self.gattc + .search_service(gattc_if, conn_id, Some(SERVICE_UUID))?; + } + GattcEvent::Mtu { status, mtu, .. } => { + info!("MTU exchange, status {status:?}, MTU {mtu}"); + } + GattcEvent::SearchResult { + conn_id, + start_handle, + end_handle, + srvc_id, + is_primary, + } => { + info!("Service search result, conn_id {conn_id}, is primary service {is_primary}, start handle {start_handle}, end handle {end_handle}, current handle value {}", srvc_id.inst_id); + + if srvc_id.uuid == SERVICE_UUID { + info!("Service found, uuid {:?}", srvc_id.uuid); + + self.state.lock().unwrap().service_start_end_handle = + Some((start_handle, end_handle)); + } + } + GattcEvent::SearchComplete { + status, + conn_id, + searched_service_source, + } => { + self.check_gatt_status(status)?; + + match searched_service_source { + ServiceSource::RemoteDevice => { + info!("Get service information from remote device") + } + ServiceSource::Nvs => { + info!("Get service information from flash") + } + _ => { + info!("Unknown service source") + } + }; + info!("Service search complete"); + + let mut state = self.state.lock().unwrap(); + + if let Some((start_handle, end_handle)) = state.service_start_end_handle { + let count = self + .gattc + .get_attr_count( + gattc_if, + conn_id, + DbAttrType::Characteristic { + start_handle, + end_handle, + }, + ) + .map_err(|status| { + error!("Get attr count error for service {status:?}"); + EspError::from_infallible::() + })?; + + info!("Found {count} characterisitics"); + + if count > 0 { + // Get the indicator characteristic handle and register for notification + match self.gattc.get_characteristic_by_uuid::<1>( + gattc_if, + conn_id, + start_handle, + end_handle, + IND_CHARACTERISTIC_UUID, + ) { + Ok(chars) => { + if let Some(ind_char_elem) = chars.first() { + if ind_char_elem.properties.contains(Property::Indicate) { + if let Some(remote_addr) = state.remote_addr { + state.ind_char_handle = + Some(ind_char_elem.char_handle); + self.gattc.register_for_notify( + gattc_if, + remote_addr, + ind_char_elem.char_handle, + )?; + } + } else { + error!("Ind characteristic does not have property Indicate"); + } + } else { + error!("No ind characteristic found"); + } + } + Err(status) => { + error!("Get ind characteristic error {status:?}"); + } + }; + + // Get the write characteristic handle and start sending data to the server + match self.gattc.get_characteristic_by_uuid::<1>( + gattc_if, + conn_id, + start_handle, + end_handle, + WRITE_CHARACTERISITIC_UUID, + ) { + Ok(chars) => { + if let Some(write_char_elem) = chars.first() { + if write_char_elem.properties.contains(Property::Write) { + state.write_char_handle = + Some(write_char_elem.char_handle); + + // Let main loop send write + self.condvar.notify_all(); + } else { + error!( + "Write characteristic does not have property Write" + ); + } + } else { + error!("No write characteristic found"); + } + } + Err(status) => { + error!("get write characteristic error {status:?}"); + } + }; + } else { + error!("No characteristics found"); + } + }; + } + GattcEvent::RegisterNotify { status, handle } => { + self.check_gatt_status(status)?; + + info!("Notification register successfully"); + + let mut state = self.state.lock().unwrap(); + + if let Some(conn_id) = state.conn_id { + let count = self + .gattc + .get_attr_count(gattc_if, conn_id, DbAttrType::Descriptor { handle }) + .map_err(|status| { + error!("Get attr count for ind char error {status:?}"); + EspError::from_infallible::() + })?; + + if count > 0 { + match self.gattc.get_descriptor_by_char_handle::<1>( + gattc_if, + conn_id, + handle, + IND_DESCRIPTOR_UUID, + ) { + Ok(descrs) => { + if let Some(descr) = descrs.first() { + if descr.uuid == IND_DESCRIPTOR_UUID { + state.ind_descr_handle = Some(descr.handle); + } + } else { + error!("No ind descriptor found"); + } + } + Err(status) => { + error!("get ind char descriptors error {status:?}"); + } + } + } else { + error!("No ind char descriptors found"); + } + } + } + GattcEvent::Notify { + addr, + handle, + value, + is_notify, + .. + } => { + info!( + "Got is_notify {is_notify}, addr {addr}, handle {handle}, value {value:?}" + ); + } + GattcEvent::WriteDescriptor { status, .. } => { + self.check_gatt_status(status)?; + + info!("Descriptor write successful"); + } + GattcEvent::ServiceChanged { addr } => { + info!("Service change from {addr}"); + } + GattcEvent::WriteCharacteristic { status, .. } => { + self.check_gatt_status(status)?; + + info!("Characteristic write successful"); + } + GattcEvent::Disconnected { addr, reason, .. } => { + let mut state = self.state.lock().unwrap(); + state.connect = false; + state.remote_addr = None; + state.conn_id = None; + state.service_start_end_handle = None; + state.ind_char_handle = None; + state.ind_descr_handle = None; + state.write_char_handle = None; + info!("Disconnected, remote {addr}, reason {reason:?}"); + } + _ => (), + } + Ok(()) + } + + /// Connect to the bt_gatt_server. + /// + /// This sets the scan params with triggers the event `BleGapEvent::ScanParameterConfigured` where the + /// gap callback will start scanning. Scanning must happen before a connect can be made in `BleGapEvent::ScanResult`. + pub fn connect(&self) -> Result<(), EspError> { + if !self.state.lock().unwrap().connect { + let scan_params = ScanParams { + scan_type: ScanType::Active, + own_addr_type: BleAddrType::Public, + scan_filter_policy: ScanFilter::All, + scan_interval: 0x50, + scan_window: 0x30, + scan_duplicate: ScanDuplicate::Disable, + }; + + self.gap.set_scan_params(&scan_params)?; + } + + Ok(()) + } + + /// Disconnect from the bt_gatt_server. + /// + /// This does a physical disconnect, a `gattc.close` can also be used to close a virtual connection which will also disconnect + /// if there are no more virtual connections. + pub fn disconnect(&self) -> Result<(), EspError> { + let state = self.state.lock().unwrap(); + + if let Some(remote_addr) = state.remote_addr { + self.gap.disconnect(remote_addr)?; + } + + Ok(()) + } + + /// Subscribe or unsubsrcibe to the notifications. + /// + /// After registering for notify the CCCD descriptor is written to enable/disbale the notification. + pub fn request_indicate(&self, indicate: bool) -> Result<(), EspError> { + let state = self.state.lock().unwrap(); + + let Some(gattc_if) = state.gattc_if else { + return Ok(()); + }; + let Some(conn_id) = state.conn_id else { + return Ok(()); + }; + + if let Some(ind_descr_handle) = state.ind_descr_handle { + let value = if indicate { + info!("Subscribe indicate"); + 2_u16 + } else { + info!("Unsubscribe indicate"); + 0_u16 + } + .to_le_bytes(); + + self.gattc.write_descriptor( + gattc_if, + conn_id, + ind_descr_handle, + &value, + GattWriteType::RequireResponse, + GattAuthReq::None, + )?; + } + + Ok(()) + } + + /// Wait for the discovery of the write characteristic handle. + pub fn wait_for_write_char_handle(&self) { + let mut state = self.state.lock().unwrap(); + while state.write_char_handle.is_none() { + state = self.condvar.wait(state).unwrap(); + } + } + + // Write some data to the write characteristic. + pub fn write_characterisitic(&self, char_value: &[u8]) -> Result<(), EspError> { + let state = self.state.lock().unwrap(); + + let Some(gattc_if) = state.gattc_if else { + return Ok(()); + }; + let Some(conn_id) = state.conn_id else { + return Ok(()); + }; + + if let Some(write_char_handle) = state.write_char_handle { + self.gattc.write_characteristic( + gattc_if, + conn_id, + write_char_handle, + &char_value, + GattWriteType::RequireResponse, + GattAuthReq::None, + )?; + } + + Ok(()) + } + + fn check_esp_status(&self, status: Result<(), EspError>) { + if let Err(e) = status { + warn!("Got status: {e:?}"); + } + } + + fn check_bt_status(&self, status: BtStatus) -> Result<(), EspError> { + if !matches!(status, BtStatus::Success) { + warn!("Got status: {status:?}"); + Err(EspError::from_infallible::()) + } else { + Ok(()) + } + } + + fn check_gatt_status(&self, status: GattStatus) -> Result<(), EspError> { + if !matches!(status, GattStatus::Ok) { + warn!("Got status: {status:?}"); + Err(EspError::from_infallible::()) + } else { + Ok(()) + } + } + } +} diff --git a/examples/bt_gatt_server.rs b/examples/bt_gatt_server.rs index f1d5c6ffab2..ac2c08d4d1d 100644 --- a/examples/bt_gatt_server.rs +++ b/examples/bt_gatt_server.rs @@ -1,6 +1,6 @@ //! Example of a BLE GATT server using the ESP IDF Bluedroid BLE bindings. //! -//! You can test it with any "GATT Browser" app, like e.g. +//! You can test it with the bt_gatt_client example or any "GATT Browser" app, like e.g. //! the "GATTBrowser" mobile app available on Android. //! //! The example server publishes a single service featuring two characteristics: @@ -11,7 +11,7 @@ //! but also how to broadcast data to all clients that have subscribed to a characteristic, including //! handling indication confirmations. //! -//! Note that the Buedroid stack consumes a lot of memory, so `sdkconfig.defaults` should be carefully configured +//! Note that the Bluedroid stack consumes a lot of memory, so `sdkconfig.defaults` should be carefully configured //! to avoid running out of memory. //! //! Here's a working configuration, but you might need to adjust further to your concrete use-case: @@ -200,13 +200,15 @@ mod example { if state.ind_confirmed.is_none() { let conn = &state.connections[peer_index]; - self.gatts - .indicate(gatt_if, conn.conn_id, ind_handle, data)?; + if conn.subscribed { + self.gatts + .indicate(gatt_if, conn.conn_id, ind_handle, data)?; - state.ind_confirmed = Some(conn.peer); - let conn = &state.connections[peer_index]; + state.ind_confirmed = Some(conn.peer); + let conn = &state.connections[peer_index]; - info!("Indicated data to {}", conn.peer); + info!("Indicated data to {}", conn.peer); + } break; } else { state = self.condvar.wait(state).unwrap(); @@ -351,12 +353,8 @@ mod example { Ok(()) } - /// Create the service and start advertising - /// Called from within the event callback once we are notified that the GATTS app is registered - fn create_service(&self, gatt_if: GattInterface) -> Result<(), EspError> { - self.state.lock().unwrap().gatt_if = Some(gatt_if); - - self.gap.set_device_name("ESP32")?; + /// Set the advertising configuration, effectively starting advertising + fn set_adv_conf(&self) -> Result<(), EspError> { self.gap.set_adv_conf(&AdvConfiguration { include_name: true, include_txpower: true, @@ -365,7 +363,16 @@ mod example { // service_data: todo!(), // manufacturer_data: todo!(), ..Default::default() - })?; + }) + } + + /// Create the service and start advertising + /// Called from within the event callback once we are notified that the GATTS app is registered + fn create_service(&self, gatt_if: GattInterface) -> Result<(), EspError> { + self.state.lock().unwrap().gatt_if = Some(gatt_if); + + self.gap.set_device_name("ESP32")?; + self.set_adv_conf()?; self.gatts.create_service( gatt_if, &GattServiceId { @@ -542,6 +549,9 @@ mod example { .map_err(|_| ()) .unwrap(); + // restart advertising so we can get a new connection + self.set_adv_conf()?; + true } else { false @@ -566,6 +576,9 @@ mod example { .position(|Connection { peer, .. }| *peer == addr) { state.connections.swap_remove(index); + + // restart advertising so we can get a new connection + self.set_adv_conf()?; } Ok(()) diff --git a/src/bt/ble/gap.rs b/src/bt/ble/gap.rs index 845a85b8341..a20af362eaa 100644 --- a/src/bt/ble/gap.rs +++ b/src/bt/ble/gap.rs @@ -2,11 +2,14 @@ use core::borrow::Borrow; use core::fmt::{self, Debug}; use core::marker::PhantomData; use core::{ffi::CStr, ops::BitOr}; +use std::slice; use crate::bt::BtSingleton; use crate::sys::*; use ::log::trace; +use log::error; +use num_enum::TryFromPrimitive; use crate::{ bt::{BdAddr, BleEnabled, BtDriver, BtStatus, BtUuid}, @@ -30,12 +33,15 @@ pub enum AuthenticationRequest { #[default] NoBonding = 0b0000_0000, Bonding = 0b0000_0001, - Mitm = 0b0000_0010, - MitmBonding = 0b0000_0011, - SecureOnly = 0b0000_0100, - SecureBonding = 0b0000_0101, - SecureMitm = 0b0000_0110, - SecureMitmBonding = 0b0000_0111, + Mitm = 0b0000_0100, + MitmBonding = (AuthenticationRequest::Mitm as u8 | AuthenticationRequest::Bonding as u8), + SecureOnly = 0b0000_1000, + SecureBonding = + (AuthenticationRequest::SecureOnly as u8 | AuthenticationRequest::Bonding as u8), + SecureMitm = (AuthenticationRequest::SecureOnly as u8 | AuthenticationRequest::Mitm as u8), + SecureMitmBonding = (AuthenticationRequest::SecureOnly as u8 + | AuthenticationRequest::Mitm as u8 + | AuthenticationRequest::Bonding as u8), } #[derive(Copy, Clone, Eq, PartialEq)] @@ -229,6 +235,149 @@ impl<'a> From<&'a AdvConfiguration<'a>> for esp_ble_adv_data_t { } } +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum AdvertisingDataType { + Flag = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_FLAG, + Srv16Part = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_16SRV_PART, + Srv16Cmpl = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_16SRV_CMPL, + Srv32Part = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_32SRV_PART, + Srv32Cmpl = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_32SRV_CMPL, + Srv128Part = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_128SRV_PART, + Srv128Cmpl = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_128SRV_CMPL, + NameShort = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_NAME_SHORT, + NameCmpl = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_NAME_CMPL, + TxPwr = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_TX_PWR, + DevClass = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_DEV_CLASS, + SmTk = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_SM_TK, + SmOobFlag = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_SM_OOB_FLAG, + IntRange = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_INT_RANGE, + SolSrvUuid = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_SOL_SRV_UUID, + Sol128SrvUuid = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_128SOL_SRV_UUID, + ServiceData = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_SERVICE_DATA, + PublicTarget = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_PUBLIC_TARGET, + RandomTarget = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_RANDOM_TARGET, + Appearance = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_APPEARANCE, + AdvInt = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_ADV_INT, + LeDevAddr = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_LE_DEV_ADDR, + LeRole = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_LE_ROLE, + SpairC256 = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_SPAIR_C256, + SpairR256 = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_SPAIR_R256, + Sol32SrvUuid = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_32SOL_SRV_UUID, + Service32Data = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_32SERVICE_DATA, + Service128Data = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_128SERVICE_DATA, + LeSecureConfirm = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_LE_SECURE_CONFIRM, + LeSecureRandom = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_LE_SECURE_RANDOM, + Uri = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_URI, + IndoorPosition = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_INDOOR_POSITION, + TransDiscData = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_TRANS_DISC_DATA, + LeSupportFeature = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_LE_SUPPORT_FEATURE, + ChanMapUpdate = esp_ble_adv_data_type_ESP_BLE_AD_TYPE_CHAN_MAP_UPDATE, + ManufacturerSpecific = esp_ble_adv_data_type_ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ScanType { + Passive = esp_ble_scan_type_t_BLE_SCAN_TYPE_PASSIVE, + Active = esp_ble_scan_type_t_BLE_SCAN_TYPE_ACTIVE, +} + +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum BleAddrType { + #[default] + Public = esp_ble_addr_type_t_BLE_ADDR_TYPE_PUBLIC, + Random = esp_ble_addr_type_t_BLE_ADDR_TYPE_RANDOM, + RpaPublic = esp_ble_addr_type_t_BLE_ADDR_TYPE_RPA_PUBLIC, + RpaRandom = esp_ble_addr_type_t_BLE_ADDR_TYPE_RPA_RANDOM, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ScanFilter { + /// Accept all : + /// 1. advertisement packets except directed advertising packets not addressed to this device (default). + All = esp_ble_scan_filter_t_BLE_SCAN_FILTER_ALLOW_ALL, + /// Accept only : + /// 1. advertisement packets from devices where the advertiser’s address is in the White list. + /// 2. Directed advertising packets which are not addressed for this device shall be ignored. + OnlyWhitelist = esp_ble_scan_filter_t_BLE_SCAN_FILTER_ALLOW_ONLY_WLST, + /// Accept all : + /// 1. undirected advertisement packets, and + /// 2. directed advertising packets where the initiator address is a resolvable private address, and + /// 3. directed advertising packets addressed to this device. + UndirectedAndDirected = esp_ble_scan_filter_t_BLE_SCAN_FILTER_ALLOW_UND_RPA_DIR, + /// Accept all : + /// 1. advertisement packets from devices where the advertiser’s address is in the White list, and + /// 2. directed advertising packets where the initiator address is a resolvable private address, and + /// 3. directed advertising packets addressed to this device. + WhitelistAndDirected = esp_ble_scan_filter_t_BLE_SCAN_FILTER_ALLOW_WLIST_RPA_DIR, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ScanDuplicate { + Disable = esp_ble_scan_duplicate_t_BLE_SCAN_DUPLICATE_DISABLE, + Enable = esp_ble_scan_duplicate_t_BLE_SCAN_DUPLICATE_ENABLE, + #[cfg(esp_idf_ble_50_feature_support)] + Reset = esp_ble_scan_duplicate_t_BLE_SCAN_DUPLICATE_ENABLE_RESET, + Max = esp_ble_scan_duplicate_t_BLE_SCAN_DUPLICATE_MAX, +} + +pub struct ScanParams { + pub scan_type: ScanType, + pub own_addr_type: BleAddrType, + pub scan_filter_policy: ScanFilter, + pub scan_interval: u16, + pub scan_window: u16, + pub scan_duplicate: ScanDuplicate, +} + +impl From<&ScanParams> for esp_ble_scan_params_t { + fn from(params: &ScanParams) -> Self { + Self { + scan_type: params.scan_type as _, + own_addr_type: params.own_addr_type as _, + scan_filter_policy: params.scan_filter_policy as _, + scan_interval: params.scan_interval, + scan_window: params.scan_window, + scan_duplicate: params.scan_duplicate as _, + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum GapSearchEvent { + InquiryResult = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_RES_EVT, + InquiryComplete = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_CMPL_EVT, + DiscoveryResult = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_RES_EVT, + DiscoveryBleResult = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_BLE_RES_EVT, + DiscoveryComplete = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_CMPL_EVT, + DiDiscoveryComplete = esp_gap_search_evt_t_ESP_GAP_SEARCH_DI_DISC_CMPL_EVT, + SearchCanceled = esp_gap_search_evt_t_ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT, + InquiryDiscardedNum = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_DISCARD_NUM_EVT, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum BtDevType { + Bredr = esp_bt_dev_type_t_ESP_BT_DEVICE_TYPE_BREDR, + Ble = esp_bt_dev_type_t_ESP_BT_DEVICE_TYPE_BLE, + Dumo = esp_bt_dev_type_t_ESP_BT_DEVICE_TYPE_DUMO, +} + +#[repr(u32)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] +pub enum BleEventType { + ConnectableUndirectedAdv = esp_ble_evt_type_t_ESP_BLE_EVT_CONN_ADV, + ConnectableDirectedAdv = esp_ble_evt_type_t_ESP_BLE_EVT_CONN_DIR_ADV, + ScannableUndirectedAdv = esp_ble_evt_type_t_ESP_BLE_EVT_DISC_ADV, + NonconnectableUndirectedAdv = esp_ble_evt_type_t_ESP_BLE_EVT_NON_CONN_ADV, + ScanResponse = esp_ble_evt_type_t_ESP_BLE_EVT_SCAN_RSP, +} + pub struct EventRawData<'a>(pub &'a esp_ble_gap_cb_param_t); impl Debug for EventRawData<'_> { @@ -242,8 +391,20 @@ pub enum BleGapEvent<'a> { AdvertisingConfigured(BtStatus), ScanResponseConfigured(BtStatus), ScanParameterConfigured(BtStatus), - // TODO - ScanResult(esp_ble_gap_cb_param_t_ble_scan_result_evt_param), + ScanResult { + search_evt: GapSearchEvent, + bda: BdAddr, + dev_type: BtDevType, + ble_addr_type: BleAddrType, + ble_evt_type: BleEventType, + rssi: i32, + ble_adv: [u8; 62usize], + flag: i32, + num_resps: i32, + adv_data_len: u8, + scan_rsp_len: u8, + num_dis: u32, + }, RawAdvertisingConfigured(BtStatus), RawScanResponseConfigured(BtStatus), AdvertisingStarted(BtStatus), @@ -375,9 +536,20 @@ impl<'a> From<(esp_gap_ble_cb_event_t, &'a esp_ble_gap_cb_param_t)> for BleGapEv esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT => { Self::ScanParameterConfigured(param.scan_param_cmpl.status.try_into().unwrap()) } - esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_RESULT_EVT => { - Self::ScanResult(param.scan_rst) - } + esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_RESULT_EVT => Self::ScanResult { + search_evt: param.scan_rst.search_evt.try_into().unwrap(), + bda: param.scan_rst.bda.into(), + dev_type: param.scan_rst.dev_type.try_into().unwrap(), + ble_addr_type: param.scan_rst.ble_addr_type.try_into().unwrap(), + ble_evt_type: param.scan_rst.ble_evt_type.try_into().unwrap(), + rssi: param.scan_rst.rssi, + ble_adv: param.scan_rst.ble_adv, + flag: param.scan_rst.flag, + num_resps: param.scan_rst.num_resps, + adv_data_len: param.scan_rst.adv_data_len, + scan_rsp_len: param.scan_rst.scan_rsp_len, + num_dis: param.scan_rst.num_dis, + }, esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT => { Self::RawAdvertisingConfigured( param.adv_data_raw_cmpl.status.try_into().unwrap(), @@ -595,8 +767,17 @@ where core::mem::size_of::() as _, ) }) + .map_err(|e| { + error!("Failed to set security param {param}"); + e + }) } + set( + esp_ble_sm_param_t_ESP_BLE_SM_AUTHEN_REQ_MODE, + &conf.auth_req_mode, + )?; + set( esp_ble_sm_param_t_ESP_BLE_SM_IOCAP_MODE, &conf.io_capabilities, @@ -607,15 +788,15 @@ where } if let Some(responder_key) = &conf.responder_key { - set(esp_ble_sm_param_t_ESP_BLE_SM_SET_RSP_KEY, &responder_key)?; + set(esp_ble_sm_param_t_ESP_BLE_SM_SET_RSP_KEY, responder_key)?; } - if let Some(max_key_size) = &conf.max_key_size { - set(esp_ble_sm_param_t_ESP_BLE_SM_MAX_KEY_SIZE, &max_key_size)?; + if let Some(min_key_size) = &conf.min_key_size { + set(esp_ble_sm_param_t_ESP_BLE_SM_MIN_KEY_SIZE, min_key_size)?; } - if let Some(min_key_size) = &conf.min_key_size { - set(esp_ble_sm_param_t_ESP_BLE_SM_MIN_KEY_SIZE, &min_key_size)?; + if let Some(max_key_size) = &conf.max_key_size { + set(esp_ble_sm_param_t_ESP_BLE_SM_MAX_KEY_SIZE, max_key_size)?; } if let Some(passkey) = &conf.static_passkey { @@ -656,6 +837,14 @@ where esp!(unsafe { esp_ble_set_encryption(&addr.0 as *const _ as *mut _, encryption as u32) }) } + pub fn set_scan_params(&self, params: &ScanParams) -> Result<(), EspError> { + let scan_params = params.into(); + + esp!(unsafe { + esp_ble_gap_set_scan_params(&scan_params as *const esp_ble_scan_params_t as *mut _) + }) + } + pub fn start_scanning(&self, duration: u32) -> Result<(), EspError> { esp!(unsafe { esp_ble_gap_start_scanning(duration) }) } @@ -684,6 +873,30 @@ where esp!(unsafe { esp_ble_gap_stop_advertising() }) } + pub fn resolve_adv_data_by_type( + &self, + adv_data: &[u8], + adv_data_len: u16, + data_type: AdvertisingDataType, + ) -> Option<&[u8]> { + let mut length: u8 = 0; + + let resolve_adv_data = unsafe { + esp_ble_resolve_adv_data_by_type( + adv_data as *const _ as *mut _, + adv_data_len, + data_type as _, + &mut length, + ) + }; + + if length == 0 { + None + } else { + Some(unsafe { slice::from_raw_parts(resolve_adv_data, length as _) }) + } + } + pub fn set_conn_params_conf( &self, addr: BdAddr, @@ -703,6 +916,10 @@ where }) } + pub fn disconnect(&self, addr: BdAddr) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gap_disconnect(&addr.0 as *const _ as *mut _) }) + } + unsafe extern "C" fn event_handler( event: esp_gap_ble_cb_event_t, param: *mut esp_ble_gap_cb_param_t, diff --git a/src/bt/ble/gatt.rs b/src/bt/ble/gatt.rs index 36655184a98..09cbc5acde9 100644 --- a/src/bt/ble/gatt.rs +++ b/src/bt/ble/gatt.rs @@ -4,6 +4,7 @@ use num_enum::TryFromPrimitive; use crate::bt::BtUuid; use crate::sys::*; +pub mod client; pub mod server; pub type GattInterface = u8; @@ -343,3 +344,8 @@ impl Default for GattResponse { Self::new() } } + +/// This function is called to set local MTU, the function is called before BLE connection. +pub fn set_local_mtu(mtu: u16) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gatt_set_local_mtu(mtu) }) +} diff --git a/src/bt/ble/gatt/client.rs b/src/bt/ble/gatt/client.rs new file mode 100644 index 00000000000..8e394086aad --- /dev/null +++ b/src/bt/ble/gatt/client.rs @@ -0,0 +1,1903 @@ +use core::borrow::Borrow; +use core::fmt::{self, Debug}; +use core::marker::PhantomData; + +use ::log::trace; +use enumset::EnumSet; +use num_enum::TryFromPrimitive; + +use crate::bt::ble::gap::BleAddrType; +use crate::bt::ble::gatt::{GattId, Property}; +use crate::bt::{BdAddr, BleEnabled, BtDriver, BtSingleton, BtUuid}; +use crate::sys::*; + +use super::{GattConnParams, GattConnReason, GattInterface, GattStatus, Handle}; + +pub type AppId = u16; +pub type ConnectionId = u16; +pub type TransferId = u32; + +pub const INVALID_HANDLE: u16 = 0; + +#[derive(Debug, Copy, Clone, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum ServiceSource { + /// Service information from a remote device. Relates to BTA_GATTC_SERVICE_INFO_FROM_REMOTE_DEVICE. + RemoteDevice = esp_service_source_t_ESP_GATT_SERVICE_FROM_REMOTE_DEVICE, + /// Service information from NVS flash. Relates to BTA_GATTC_SERVICE_INFO_FROM_NVS_FLASH. + Nvs = esp_service_source_t_ESP_GATT_SERVICE_FROM_NVS_FLASH, + /// Service source is unknown. Relates to BTA_GATTC_SERVICE_INFO_FROM_UNKNOWN + Uknown = esp_service_source_t_ESP_GATT_SERVICE_FROM_UNKNOWN, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, TryFromPrimitive)] +#[repr(u8)] +pub enum LinkRole { + Master = 0, + Slave = 1, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum DbAttrType { + /// Primary service attribute, with start and end handle + PrimaryService { + start_handle: Handle, + end_handle: Handle, + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_PRIMARY_SERVICE, + /// Secondary service attribute + SecondaryService { + start_handle: Handle, + end_handle: Handle, + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_SECONDARY_SERVICE, + /// Characteristic attribute + Characteristic { + start_handle: Handle, + end_handle: Handle, + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_CHARACTERISTIC, + /// Descriptor attribute - with the characteristic handle + Descriptor { handle: Handle } = esp_gatt_db_attr_type_t_ESP_GATT_DB_DESCRIPTOR, + /// Included service attribute + IncludedService { + start_handle: Handle, + end_handle: Handle, + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_INCLUDED_SERVICE, + /// All attribute types + AllAttributes { + start_handle: Handle, + end_handle: Handle, + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_ALL, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum GattAuthReq { + /// No authentication required. Corresponds to BTA_GATT_AUTH_REQ_NONE + None = esp_gatt_auth_req_t_ESP_GATT_AUTH_REQ_NONE, + /// Unauthenticated encryption. Corresponds to BTA_GATT_AUTH_REQ_NO_MITM + NoMitm = esp_gatt_auth_req_t_ESP_GATT_AUTH_REQ_NO_MITM, + /// Authenticated encryption (MITM protection). Corresponds to BTA_GATT_AUTH_REQ_MITM + Mitm = esp_gatt_auth_req_t_ESP_GATT_AUTH_REQ_MITM, + /// Signed data, no MITM protection. Corresponds to BTA_GATT_AUTH_REQ_SIGNED_NO_MITM + SignedNoMitm = esp_gatt_auth_req_t_ESP_GATT_AUTH_REQ_SIGNED_NO_MITM, + /// Signed data with MITM protection. Corresponds to BTA_GATT_AUTH_REQ_SIGNED_MITM + SignedMitm = esp_gatt_auth_req_t_ESP_GATT_AUTH_REQ_SIGNED_MITM, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] +#[repr(u32)] +pub enum GattWriteType { + /// Write operation where no response is needed + NoResponse = esp_gatt_write_type_t_ESP_GATT_WRITE_TYPE_NO_RSP, + /// Write operation that requires a remote response + RequireResponse = esp_gatt_write_type_t_ESP_GATT_WRITE_TYPE_RSP, +} + +#[derive(Debug, Default, Copy, Clone)] +pub struct BleConnParams { + /// Initial scan interval, in units of 0.625ms, the range is 0x0004(2.5ms) to 0xFFFF(10.24s) + pub scan_interval: u16, + /// Initial scan window, in units of 0.625ms, the range is 0x0004(2.5ms) to 0xFFFF(10.24s) + pub scan_window: u16, + /// Minimum connection interval, in units of 1.25ms, the range is 0x0006(7.5ms) to 0x0C80(4s) + pub interval_min: u16, + /// Maximum connection interval, in units of 1.25ms, the range is 0x0006(7.5ms) to 0x0C80(4s) + pub interval_max: u16, + /// Connection latency, the range is 0x0000(0) to 0x01F3(499) + pub latency: u16, + /// Connection supervision timeout, in units of 10ms, the range is from 0x000A(100ms) to 0x0C80(32s) + pub supervision_timeout: u16, + /// Minimum connection event length, in units of 0.625ms, setting to 0 for no preferred parameters + pub min_ce_len: u16, + /// Maximum connection event length, in units of 0.625ms, setting to 0 for no preferred parameters + pub max_ce_len: u16, +} + +impl From for esp_ble_conn_params_t { + fn from(params: BleConnParams) -> Self { + Self { + scan_interval: params.scan_interval, + scan_window: params.scan_window, + interval_min: params.interval_min, + interval_max: params.interval_max, + latency: params.latency, + supervision_timeout: params.supervision_timeout, + min_ce_len: params.min_ce_len, + max_ce_len: params.max_ce_len, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct GattCreateConnParams { + /// The Bluetooth address of the remote device + pub addr: BdAddr, + /// Address type of the remote device + pub addr_type: BleAddrType, + /// Direct connection or background auto connection(by now, background auto connection is not supported) + pub is_direct: bool, + /// Set to true for BLE 5.0 or higher to enable auxiliary connections; set to false for BLE 4.2 or lower. + pub is_aux: bool, + /// Specifies the address type used in the connection request. Set to 0xFF if the address type is unknown. + pub own_addr_type: BleAddrType, + /// Connection parameters for the LE 1M PHY + pub phy_1m_conn_params: Option, + /// Connection parameters for the LE 2M PHY + pub phy_2m_conn_params: Option, + /// Connection parameters for the LE Coded PHY + pub phy_coded_conn_params: Option, +} +impl GattCreateConnParams { + pub fn new(addr: BdAddr, addr_type: BleAddrType) -> Self { + Self { + addr, + addr_type, + is_direct: true, + is_aux: false, + own_addr_type: BleAddrType::Public, + phy_1m_conn_params: None, + phy_2m_conn_params: None, + phy_coded_conn_params: None, + } + } +} + +#[derive(Debug, Clone)] +pub struct GattcService { + /// Indicates if the service is primary + pub is_primary: bool, + /// Service start handle + pub start_handle: Handle, + /// Service end handle + pub end_handle: Handle, + /// Service UUID + pub uuid: BtUuid, +} + +impl From<&esp_gattc_service_elem_t> for GattcService { + fn from(svc: &esp_gattc_service_elem_t) -> Self { + Self { + is_primary: svc.is_primary, + start_handle: svc.start_handle, + end_handle: svc.end_handle, + uuid: svc.uuid.into(), + } + } +} + +#[derive(Debug, Clone)] +pub struct IncludeService { + /// Current attribute handle of the included service + pub handle: Handle, + /// Start handle of the included service + pub incl_srvc_s_handle: Handle, + /// End handle of the included service + pub incl_srvc_e_handle: Handle, + /// Included service UUID + pub uuid: BtUuid, +} + +impl From<&esp_gattc_incl_svc_elem_t> for IncludeService { + fn from(svc: &esp_gattc_incl_svc_elem_t) -> Self { + Self { + handle: svc.handle, + incl_srvc_s_handle: svc.incl_srvc_s_handle, + incl_srvc_e_handle: svc.incl_srvc_e_handle, + uuid: svc.uuid.into(), + } + } +} + +#[derive(Debug, Clone)] +pub struct CharacteristicElement { + /// Characteristic handle + pub char_handle: Handle, + /// Characteristic properties + pub properties: EnumSet, + /// Characteristic UUID + pub uuid: BtUuid, +} + +impl From<&esp_gattc_char_elem_t> for CharacteristicElement { + fn from(elem: &esp_gattc_char_elem_t) -> Self { + Self { + char_handle: elem.char_handle, + properties: EnumSet::from_repr(elem.properties), + uuid: elem.uuid.into(), + } + } +} + +#[derive(Debug, Clone)] +pub struct DescriptorElement { + /// Descriptor handle + pub handle: Handle, + /// Descriptor UUID + pub uuid: BtUuid, +} + +impl From<&esp_gattc_descr_elem_t> for DescriptorElement { + fn from(elem: &esp_gattc_descr_elem_t) -> Self { + Self { + handle: elem.handle, + uuid: elem.uuid.into(), + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum DbElementAttrType { + /// Primary service attribute, with start and end handle + PrimaryService { + start_handle: Handle, + end_handle: Handle, + attribute_handle: Handle, // same as start_handle + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_PRIMARY_SERVICE, + /// Secondary service attribute + SecondaryService { + start_handle: Handle, + end_handle: Handle, + attribute_handle: Handle, // same as start_handle + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_SECONDARY_SERVICE, + /// Characteristic attribute + Characteristic { + handle: Handle, + properties: EnumSet, + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_CHARACTERISTIC, + /// Descriptor attribute - with the characteristic handle + Descriptor { handle: Handle } = esp_gatt_db_attr_type_t_ESP_GATT_DB_DESCRIPTOR, + /// Included service attribute + IncludedService { attribute_handle: Handle } = + esp_gatt_db_attr_type_t_ESP_GATT_DB_INCLUDED_SERVICE, + AllAttributes { + start_handle: Handle, + end_handle: Handle, + attribute_handle: Handle, + properties: EnumSet, + } = esp_gatt_db_attr_type_t_ESP_GATT_DB_ALL, +} + +#[derive(Debug, Clone)] +pub struct DbElement { + /// Attribute UUID. + pub uuid: BtUuid, + /// Attribute type. + pub attr_type: DbElementAttrType, +} + +impl From<&esp_gattc_db_elem_t> for DbElement { + fn from(elem: &esp_gattc_db_elem_t) -> Self { + #[allow(non_upper_case_globals)] + let attr_type = match elem.type_ { + esp_gatt_db_attr_type_t_ESP_GATT_DB_PRIMARY_SERVICE => { + DbElementAttrType::PrimaryService { + start_handle: elem.start_handle, + end_handle: elem.end_handle, + attribute_handle: elem.attribute_handle, + } + } + + esp_gatt_db_attr_type_t_ESP_GATT_DB_SECONDARY_SERVICE => { + DbElementAttrType::SecondaryService { + start_handle: elem.start_handle, + end_handle: elem.end_handle, + attribute_handle: elem.attribute_handle, + } + } + + esp_gatt_db_attr_type_t_ESP_GATT_DB_CHARACTERISTIC => { + DbElementAttrType::Characteristic { + handle: elem.attribute_handle, + properties: EnumSet::from_repr(elem.properties), + } + } + + esp_gatt_db_attr_type_t_ESP_GATT_DB_DESCRIPTOR => DbElementAttrType::Descriptor { + handle: elem.attribute_handle, + }, + + esp_gatt_db_attr_type_t_ESP_GATT_DB_INCLUDED_SERVICE => { + DbElementAttrType::IncludedService { + attribute_handle: elem.attribute_handle, + } + } + + _ => DbElementAttrType::AllAttributes { + start_handle: elem.start_handle, + end_handle: elem.end_handle, + attribute_handle: elem.attribute_handle, + properties: EnumSet::from_repr(elem.properties), + }, + }; + + Self { + attr_type, + uuid: elem.uuid.into(), + } + } +} + +pub struct EventRawData<'a>(pub &'a esp_ble_gattc_cb_param_t); + +impl Debug for EventRawData<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("EventRawData").finish() + } +} + +#[derive(Debug)] +pub enum GattcEvent<'a> { + ClientRegistered { + /// Operation status + status: GattStatus, + /// Application id which input in register API + app_id: AppId, + }, + ClientUnregistered, + Open { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Remote bluetooth device address + addr: BdAddr, + /// MTU size + mtu: u16, + }, + ReadCharacteristic { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Characteristic handle + handle: Handle, + /// Characteristic value + value: Option<&'a [u8]>, + }, + ReadDescriptor { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Descriptor handle + handle: Handle, + /// Descriptor value + value: Option<&'a [u8]>, + }, + ReadMultipleChar { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Characteristic handle + handle: Handle, + /// Concatenated values of all characteristics + value: Option<&'a [u8]>, + }, + ReadMultipleVarChar { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Characteristic handle + handle: Handle, + /// Concatenated values of all characteristics + value: Option<&'a [u8]>, + }, + WriteCharacteristic { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Characteristic handle + handle: Handle, + }, + WriteDescriptor { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Desrciptor handle + handle: Handle, + }, + PrepareWrite { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Characteristic or desrciptor handle + handle: Handle, + /// The position offset to write + offset: u16, + }, + ExecWrite { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + }, + Close { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// Remote bluetooth device address + addr: BdAddr, + /// Indicate the reason of close + reason: GattConnReason, + }, + SearchComplete { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// The source of the service information + searched_service_source: ServiceSource, + }, + SearchResult { + /// Connection ID + conn_id: ConnectionId, + /// Service start handle + start_handle: Handle, + /// Service end handle + end_handle: Handle, + /// Service ID, including service UUID and other information + srvc_id: GattId, + /// True indicates a primary service, false otherwise + is_primary: bool, + }, + Notify { + /// Connection ID + conn_id: ConnectionId, + /// Remote Bluetooth device address + addr: BdAddr, + /// The characteristic or descriptor handle + handle: Handle, + /// Notify attribute value + value: &'a [u8], + /// True means notification; false means indication + is_notify: bool, + }, + RegisterNotify { + /// Operation status + status: GattStatus, + /// The characteristic or descriptor handle + handle: Handle, + }, + UnregisterNotify { + /// Operation status + status: GattStatus, + /// The characteristic or descriptor handle + handle: Handle, + }, + ServiceChanged { + /// Remote Bluetooth device address + addr: BdAddr, + }, + Mtu { + /// Operation status + status: GattStatus, + /// Connection id + conn_id: ConnectionId, + /// MTU size + mtu: u16, + }, + Congest { + /// Connection id + conn_id: ConnectionId, + /// Congested or not + congested: bool, + }, + Connected { + /// Connection ID + conn_id: ConnectionId, + /// Link role + link_role: LinkRole, + /// Remote device address + addr: BdAddr, + /// Remote device address type + addr_type: BleAddrType, + /// Current connection parameters + conn_params: GattConnParams, + /// HCI connection handle + conn_handle: Handle, + }, + Disconnected { + /// Connection ID + conn_id: ConnectionId, + /// Remote device address + addr: BdAddr, + /// Disconnection reason + reason: GattConnReason, + }, + QueueFull { + /// Operation status + status: GattStatus, + /// Connection ID + conn_id: ConnectionId, + /// True indicates the GATTC command queue is full; false otherwise + is_full: bool, + }, + SetAssociation { + /// Operation status + status: GattStatus, + }, + AddressList { + /// Operation status + status: GattStatus, + /// Address list which has been retrieved from the local GATTC cache + address_list: &'a [BdAddr], + }, + DiscoveryCompleted { + /// Operation status + status: GattStatus, + /// Connection ID + conn_id: ConnectionId, + }, + Other { + raw_event: esp_gattc_cb_event_t, + raw_data: EventRawData<'a>, + }, +} + +#[allow(non_upper_case_globals)] +impl<'a> From<(esp_gattc_cb_event_t, &'a esp_ble_gattc_cb_param_t)> for GattcEvent<'a> { + fn from(value: (esp_gattc_cb_event_t, &'a esp_ble_gattc_cb_param_t)) -> Self { + let (event, param) = value; + + match event { + esp_gattc_cb_event_t_ESP_GATTC_REG_EVT => unsafe { + Self::ClientRegistered { + status: param.reg.status.try_into().unwrap(), + app_id: param.reg.app_id, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_UNREG_EVT => Self::ClientUnregistered, + esp_gattc_cb_event_t_ESP_GATTC_OPEN_EVT => unsafe { + Self::Open { + status: param.open.status.try_into().unwrap(), + conn_id: param.open.conn_id, + addr: param.open.remote_bda.into(), + mtu: param.open.mtu, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_READ_CHAR_EVT => unsafe { + Self::ReadCharacteristic { + status: param.read.status.try_into().unwrap(), + conn_id: param.read.conn_id, + handle: param.read.handle, + value: if param.read.value_len > 0 { + Some(core::slice::from_raw_parts( + param.read.value, + param.read.value_len as _, + )) + } else { + None + }, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_WRITE_CHAR_EVT => unsafe { + Self::WriteCharacteristic { + status: param.write.status.try_into().unwrap(), + conn_id: param.write.conn_id, + handle: param.write.handle, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_CLOSE_EVT => unsafe { + Self::Close { + status: param.close.status.try_into().unwrap(), + conn_id: param.close.conn_id, + addr: param.close.remote_bda.into(), + reason: param.close.reason.try_into().unwrap(), + } + }, + esp_gattc_cb_event_t_ESP_GATTC_SEARCH_CMPL_EVT => unsafe { + Self::SearchComplete { + status: param.search_cmpl.status.try_into().unwrap(), + conn_id: param.search_cmpl.conn_id, + searched_service_source: param + .search_cmpl + .searched_service_source + .try_into() + .unwrap(), + } + }, + esp_gattc_cb_event_t_ESP_GATTC_SEARCH_RES_EVT => unsafe { + Self::SearchResult { + conn_id: param.search_res.conn_id, + start_handle: param.search_res.start_handle, + end_handle: param.search_res.end_handle, + srvc_id: param.search_res.srvc_id.into(), + is_primary: param.search_res.is_primary, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_READ_DESCR_EVT => unsafe { + Self::ReadDescriptor { + status: param.read.status.try_into().unwrap(), + conn_id: param.read.conn_id, + handle: param.read.handle, + value: if param.read.value_len > 0 { + Some(core::slice::from_raw_parts( + param.read.value, + param.read.value_len as _, + )) + } else { + None + }, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_WRITE_DESCR_EVT => unsafe { + Self::WriteDescriptor { + status: param.write.status.try_into().unwrap(), + conn_id: param.write.conn_id, + handle: param.write.handle, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_NOTIFY_EVT => unsafe { + Self::Notify { + conn_id: param.notify.conn_id, + addr: param.notify.remote_bda.into(), + handle: param.notify.handle, + value: core::slice::from_raw_parts( + param.notify.value, + param.notify.value_len as _, + ), + is_notify: param.notify.is_notify, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_PREP_WRITE_EVT => unsafe { + Self::PrepareWrite { + status: param.write.status.try_into().unwrap(), + conn_id: param.write.conn_id, + handle: param.write.handle, + offset: param.write.offset, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_EXEC_EVT => unsafe { + Self::ExecWrite { + status: param.exec_cmpl.status.try_into().unwrap(), + conn_id: param.exec_cmpl.conn_id, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_SRVC_CHG_EVT => unsafe { + Self::ServiceChanged { + addr: param.srvc_chg.remote_bda.into(), + } + }, + esp_gattc_cb_event_t_ESP_GATTC_CFG_MTU_EVT => unsafe { + Self::Mtu { + status: param.cfg_mtu.status.try_into().unwrap(), + conn_id: param.cfg_mtu.conn_id, + mtu: param.cfg_mtu.mtu, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_CONGEST_EVT => unsafe { + Self::Congest { + conn_id: param.congest.conn_id, + congested: param.congest.congested, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_REG_FOR_NOTIFY_EVT => unsafe { + Self::RegisterNotify { + status: param.reg_for_notify.status.try_into().unwrap(), + handle: param.reg_for_notify.handle, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_UNREG_FOR_NOTIFY_EVT => unsafe { + Self::UnregisterNotify { + status: param.unreg_for_notify.status.try_into().unwrap(), + handle: param.unreg_for_notify.handle, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_CONNECT_EVT => unsafe { + Self::Connected { + conn_id: param.connect.conn_id, + link_role: param.connect.link_role.try_into().unwrap(), + addr: param.connect.remote_bda.into(), + addr_type: param.connect.ble_addr_type.try_into().unwrap(), + conn_handle: param.connect.conn_handle, + conn_params: GattConnParams { + interval_ms: param.connect.conn_params.interval as u32 * 125 / 100, + latency_ms: param.connect.conn_params.latency as u32 * 125 / 100, + timeout_ms: param.connect.conn_params.timeout as u32 * 10, + }, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_DISCONNECT_EVT => unsafe { + Self::Disconnected { + conn_id: param.disconnect.conn_id, + addr: param.disconnect.remote_bda.into(), + reason: param.disconnect.reason.try_into().unwrap(), + } + }, + esp_gattc_cb_event_t_ESP_GATTC_READ_MULTIPLE_EVT => unsafe { + Self::ReadMultipleChar { + status: param.read.status.try_into().unwrap(), + conn_id: param.read.conn_id, + handle: param.read.handle, + value: if param.read.value_len > 0 { + Some(core::slice::from_raw_parts( + param.read.value, + param.read.value_len as _, + )) + } else { + None + }, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_QUEUE_FULL_EVT => unsafe { + Self::QueueFull { + status: param.queue_full.status.try_into().unwrap(), + conn_id: param.queue_full.conn_id, + is_full: param.queue_full.is_full, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_SET_ASSOC_EVT => unsafe { + Self::SetAssociation { + status: param.set_assoc_cmp.status.try_into().unwrap(), + } + }, + esp_gattc_cb_event_t_ESP_GATTC_GET_ADDR_LIST_EVT => unsafe { + let addr: *mut BdAddr = param.get_addr_list.addr_list as _; + let addr_list = + core::slice::from_raw_parts(addr, param.get_addr_list.num_addr as _); + + Self::AddressList { + status: param.get_addr_list.status.try_into().unwrap(), + address_list: addr_list, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_DIS_SRVC_CMPL_EVT => unsafe { + Self::DiscoveryCompleted { + status: param.dis_srvc_cmpl.status.try_into().unwrap(), + conn_id: param.dis_srvc_cmpl.conn_id, + } + }, + esp_gattc_cb_event_t_ESP_GATTC_READ_MULTI_VAR_EVT => unsafe { + Self::ReadMultipleVarChar { + status: param.read.status.try_into().unwrap(), + conn_id: param.read.conn_id, + handle: param.read.handle, + value: if param.read.value_len > 0 { + Some(core::slice::from_raw_parts( + param.read.value, + param.read.value_len as _, + )) + } else { + None + }, + } + }, + _ => Self::Other { + raw_event: event, + raw_data: EventRawData(param), + }, + } + } +} + +pub struct EspGattc<'d, M, T> +where + T: Borrow>, + M: BleEnabled, +{ + _driver: T, + _p: PhantomData<&'d ()>, + _m: PhantomData, +} + +impl<'d, M, T> EspGattc<'d, M, T> +where + T: Borrow>, + M: BleEnabled, +{ + pub fn new(driver: T) -> Result { + SINGLETON.take()?; + + esp!(unsafe { esp_ble_gattc_register_callback(Some(Self::event_handler)) })?; + + Ok(Self { + _driver: driver, + _p: PhantomData, + _m: PhantomData, + }) + } + + pub fn subscribe(&self, events_cb: F) -> Result<(), EspError> + where + F: FnMut((GattInterface, GattcEvent)) + Send + 'static, + { + SINGLETON.subscribe(events_cb); + + Ok(()) + } + + /// # Safety + /// + /// This method - in contrast to method `subscribe` - allows the user to pass + /// a non-static callback/closure. This enables users to borrow + /// - in the closure - variables that live on the stack - or more generally - in the same + /// scope where the service is created. + /// + /// HOWEVER: care should be taken NOT to call `core::mem::forget()` on the service, + /// as that would immediately lead to an UB (crash). + /// Also note that forgetting the service might happen with `Rc` and `Arc` + /// when circular references are introduced: https://github.com/rust-lang/rust/issues/24456 + /// + /// The reason is that the closure is actually sent to a hidden ESP IDF thread. + /// This means that if the service is forgotten, Rust is free to e.g. unwind the stack + /// and the closure now owned by this other thread will end up with references to variables that no longer exist. + /// + /// The destructor of the service takes care - prior to the service being dropped and e.g. + /// the stack being unwind - to remove the closure from the hidden thread and destroy it. + /// Unfortunately, when the service is forgotten, the un-subscription does not happen + /// and invalid references are left dangling. + /// + /// This "local borrowing" will only be possible to express in a safe way once/if `!Leak` types + /// are introduced to Rust (i.e. the impossibility to "forget" a type and thus not call its destructor). + pub unsafe fn subscribe_nonstatic(&self, events_cb: F) -> Result<(), EspError> + where + F: FnMut((GattInterface, GattcEvent)) + Send + 'd, + { + SINGLETON.subscribe(events_cb); + + Ok(()) + } + + pub fn unsubscribe(&self) -> Result<(), EspError> { + SINGLETON.unsubscribe(); + + Ok(()) + } + + /// Register a GATT Client application. + /// * `app_id` The ID for different application (max id: `0x7fff`) + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ClientRegistered`] + /// 2. The maximum number of applications is limited to 4 + pub fn register_app(&self, app_id: AppId) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_app_register(app_id) }) + } + + /// Unregister a GATT Client application. + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ClientUnregistered`] + pub fn unregister_app(&self, gattc_if: GattInterface) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_app_unregister(gattc_if) }) + } + + /// Create an ACL connection. + /// + /// Note: *Do not* enable `BT_BLE_42_FEATURES_SUPPORTED` and `BT_BLE_50_FEATURES_SUPPORTED` in the menuconfig simultaneously. + /// + /// # Note + /// 1. The function always triggers [`GattcEvent::Connected`] and [`GattcEvent::Open`] + /// 2. When the device acts as GATT server, besides the above two events, this function triggers [`GattsEvent::PeerConnected`](super::server::GattsEvent::PeerConnected) as well.\ + /// 3. This function will establish an ACL connection as a Central and a virtual connection as a GATT Client + /// 4. If the ACL connection already exists, it will create a virtual connection only + pub fn enh_open( + &self, + gattc_if: GattInterface, + conn_params: &GattCreateConnParams, + ) -> Result<(), EspError> { + let mut phy_mask = 0; + + let phy_1m_conn_params: Option = + if let Some(phy_1m_conn_params) = conn_params.phy_1m_conn_params { + phy_mask |= ESP_BLE_PHY_1M_PREF_MASK; + Some(phy_1m_conn_params.into()) + } else { + None + }; + + let phy_2m_conn_params: Option = + if let Some(phy_2m_conn_params) = conn_params.phy_2m_conn_params { + phy_mask |= ESP_BLE_PHY_2M_PREF_MASK; + Some(phy_2m_conn_params.into()) + } else { + None + }; + + let phy_coded_conn_params: Option = + if let Some(phy_coded_conn_params) = conn_params.phy_coded_conn_params { + phy_mask |= ESP_BLE_PHY_CODED_PREF_MASK; + Some(phy_coded_conn_params.into()) + } else { + None + }; + + let conn_params = esp_ble_gatt_creat_conn_params_t { + remote_bda: conn_params.addr.into(), + remote_addr_type: conn_params.addr_type as _, + is_direct: conn_params.is_direct, + is_aux: conn_params.is_aux, + own_addr_type: conn_params.own_addr_type as _, + phy_mask: phy_mask as _, + phy_1m_conn_params: phy_1m_conn_params.as_ref().map_or(core::ptr::null(), |p| p), + phy_2m_conn_params: phy_2m_conn_params.as_ref().map_or(core::ptr::null(), |p| p), + phy_coded_conn_params: phy_coded_conn_params + .as_ref() + .map_or(core::ptr::null(), |p| p), + }; + + esp!(unsafe { esp_ble_gattc_enh_open(gattc_if, &conn_params as *const _ as *mut _) }) + } + + pub fn open( + &self, + gattc_if: GattInterface, + addr: BdAddr, + addr_type: BleAddrType, + is_direct: bool, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_open( + gattc_if, + &addr.raw() as *const _ as *mut _, + addr_type as _, + is_direct, + ) + }) + } + + pub fn aux_open( + &self, + gattc_if: GattInterface, + addr: BdAddr, + addr_type: BleAddrType, + is_direct: bool, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_aux_open( + gattc_if, + &addr.raw() as *const _ as *mut _, + addr_type as _, + is_direct, + ) + }) + } + + /// Close the virtual GATT Client connection. + /// + /// # Note + /// 1. This function triggers [`GattcEvent::Close`] + /// 2. There may be multiple virtual GATT server connections when multiple `app_id` got registered + /// 3. This API closes one virtual GATT server connection only, if there exist other virtual GATT server connections. It does not close the physical connection. + /// 4. The API [`EspBleGap::disconnect()`](crate::bt::ble::gap::EspBleGap::disconnect) can be used to disconnect the physical connection directly. + /// 5. If there is only one virtual GATT connection left, this API will terminate the ACL connection in addition and triggers [`GattcEvent::Disconnect`]. Then there is no need to call `disconnect()` anymore. + pub fn close(&self, gattc_if: GattInterface, conn_id: ConnectionId) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_close(gattc_if, conn_id) }) + } + + /// Configure the MTU size in the GATT channel. + /// + /// #Note + /// 1. This function triggers [`GattcEvent::Mtu`] + /// 2. You should call [`gatt::set_local_mtu()`](crate::bt::ble::gatt::set_local_mtu()) to set the desired MTU size locally before this API. If not set, the GATT channel uses the default MTU size (23 bytes) + pub fn mtu_req(&self, gattc_if: GattInterface, conn_id: ConnectionId) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_send_mtu_req(gattc_if, conn_id) }) + } + + /// Search services from the local GATTC cache. + /// * `filter_uuid` A UUID of the intended service. If None is passed, this API will return all services. + /// + /// # Note + /// 1. This function triggers [`GattcEvent::SearchResult`] each time a service is retrieved + /// 2. This function triggers [`GattcEvent::SearchComplete`] when the search is completed + /// 3. The 128-bit base UUID will be converted to a 16-bit UUID automatically in the search results. Other types of UUID remain unchanged + pub fn search_service( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + filter_uuid: Option, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_search_service( + gattc_if, + conn_id, + filter_uuid.map_or(core::ptr::null_mut(), |f| &f.raw() as *const _ as *mut _), + ) + }) + } + + /// Get the service with the given service UUID in the local GATTC cache. + /// * `svc_uuid` The service UUID. If `None` is passed, the API will retrieve all services. + /// * `offset` The position offset to retrieve + /// + /// Returns the service which has been found in the local GATTC cache + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. [`cache_refresh()`] can be used to discover services again + pub fn get_service( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + svc_uuid: Option, + offset: u16, + ) -> Result, EspError> { + let count = if svc_uuid.is_some() { 1 } else { N as _ }; + + let mut services_raw: heapless::Vec = heapless::Vec::new(); + unsafe { services_raw.set_len(count) }; + + let mut count: u16 = count as u16; + + esp!(unsafe { + esp_ble_gattc_get_service( + gattc_if, + conn_id, + svc_uuid.map_or(core::ptr::null_mut(), |s| &s.raw() as *const _ as *mut _), + services_raw.as_mut_ptr(), + &mut count, + offset, + ) + })?; + + count = if offset > count { + 0 + } else { + (count - offset).min(N as u16) + }; + + let result = services_raw[..count as usize] + .iter() + .map(|service_raw| service_raw.into()) + .inspect(|service| ::log::debug!("Found service {service:?}")) + .collect(); + + Ok(result) + } + + /// Get all characteristics with the given handle range in the local GATTC cache. + /// * `start_handle` The attribute start handle + /// * `end_handle` The attribute end handle + /// * `offset` The position offset to retrieve + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` + pub fn get_all_characteristics( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + start_handle: Handle, + end_handle: Handle, + offset: u16, + ) -> Result, GattStatus> { + let mut chars_raw: heapless::Vec = heapless::Vec::new(); + unsafe { + chars_raw.set_len(N); + } + + let mut count: u16 = N as u16; + + check_gatt_status(unsafe { + esp_ble_gattc_get_all_char( + gattc_if, + conn_id, + start_handle, + end_handle, + chars_raw.as_mut_ptr(), + &mut count, + offset, + ) + })?; + + count = if offset > count { + 0 + } else { + (count - offset).min(N as u16) + }; + + let result = chars_raw[..count as usize] + .iter() + .map(|chars_raw| chars_raw.into()) + .inspect(|char| ::log::debug!("Found characteristic {char:?}")) + .collect(); + + Ok(result) + } + + /// Get all descriptors with the given characteristic in the local GATTC cache. + /// * `char_handle` The given characteristic handle + /// * `offset` The position offset to retrieve + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `char_handle` must be greater than 0 + pub fn get_all_descriptors( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + char_handle: Handle, + offset: u16, + ) -> Result, GattStatus> { + let mut descrs_raw: heapless::Vec = heapless::Vec::new(); + unsafe { + descrs_raw.set_len(N); + } + + let mut count: u16 = N as u16; + + check_gatt_status(unsafe { + esp_ble_gattc_get_all_descr( + gattc_if, + conn_id, + char_handle, + descrs_raw.as_mut_ptr(), + &mut count, + offset, + ) + })?; + + count = if offset > count { + 0 + } else { + (count - offset).min(N as u16) + }; + + let result = descrs_raw[..count as usize] + .iter() + .map(|descrs_raw| descrs_raw.into()) + .inspect(|descr| ::log::debug!("Found descriptor {descr:?}")) + .collect(); + + Ok(result) + } + + /// Get the characteristic with the given characteristic UUID in the local GATTC cache. + /// * `start_handle` The attribute start handle + /// * `end_handle` The attribute end handle + /// * `char_uuid` The characteristic UUID + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` + pub fn get_characteristic_by_uuid( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + start_handle: Handle, + end_handle: Handle, + char_uuid: BtUuid, + ) -> Result, GattStatus> { + let mut chars_raw: heapless::Vec = heapless::Vec::new(); + unsafe { + chars_raw.set_len(N); + } + + let mut count: u16 = N as u16; + + check_gatt_status(unsafe { + esp_ble_gattc_get_char_by_uuid( + gattc_if, + conn_id, + start_handle, + end_handle, + char_uuid.raw(), + chars_raw.as_mut_ptr(), + &mut count, + ) + })?; + + count = count.min(N as u16); + + let result = chars_raw[..count as usize] + .iter() + .map(|chars_raw| chars_raw.into()) + .inspect(|char| ::log::debug!("Found characteristic {char:?}")) + .collect(); + + Ok(result) + } + + /// Get the descriptor with the given characteristic UUID in the local GATTC cache. + /// * `start_handle` The attribute start handle + /// * `end_handle` The attribute end handle + /// * `char_uuid` The characteristic UUID + /// * `descr_uuid` The descriptor UUID + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` + pub fn get_descriptor_by_uuid( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + start_handle: Handle, + end_handle: Handle, + char_uuid: BtUuid, + descr_uuid: BtUuid, + ) -> Result, GattStatus> { + let mut descrs_raw: heapless::Vec = heapless::Vec::new(); + unsafe { + descrs_raw.set_len(N); + } + + let mut count: u16 = N as u16; + + check_gatt_status(unsafe { + esp_ble_gattc_get_descr_by_uuid( + gattc_if, + conn_id, + start_handle, + end_handle, + char_uuid.raw(), + descr_uuid.raw(), + descrs_raw.as_mut_ptr(), + &mut count, + ) + })?; + + count = count.min(N as u16); + + let result = descrs_raw[..count as usize] + .iter() + .map(|descrs_raw| descrs_raw.into()) + .inspect(|descr| ::log::debug!("Found descriptor {descr:?}")) + .collect(); + + Ok(result) + } + + /// Get the descriptor with the given characteristic handle in the local GATTC cache. + /// * `char_handle` The characteristic handle + /// * `descr_uuid` The descriptor UUID + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `char_handle` must be greater than 0 + pub fn get_descriptor_by_char_handle( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + char_handle: Handle, + descr_uuid: BtUuid, + ) -> Result, GattStatus> { + let mut descrs_raw: heapless::Vec = heapless::Vec::new(); + unsafe { + descrs_raw.set_len(N); + } + + let mut count: u16 = N as u16; + + check_gatt_status(unsafe { + esp_ble_gattc_get_descr_by_char_handle( + gattc_if, + conn_id, + char_handle, + descr_uuid.raw(), + descrs_raw.as_mut_ptr(), + &mut count, + ) + })?; + + count = count.min(N as u16); + + let result = descrs_raw[..count as usize] + .iter() + .map(|descrs_raw| descrs_raw.into()) + .inspect(|descr| ::log::debug!("Found descriptor {descr:?}")) + .collect(); + + Ok(result) + } + + /// Get the included services with the given service handle in the local GATTC cache. + /// * `incl_uuid` The included service UUID + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` + pub fn get_include_service( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + start_handle: Handle, + end_handle: Handle, + incl_uuid: BtUuid, + ) -> Result, GattStatus> { + let mut services_raw: heapless::Vec = heapless::Vec::new(); + unsafe { + services_raw.set_len(N); + } + + let mut count: u16 = N as u16; + + check_gatt_status(unsafe { + esp_ble_gattc_get_include_service( + gattc_if, + conn_id, + start_handle, + end_handle, + &incl_uuid.raw() as *const _ as *mut _, + services_raw.as_mut_ptr(), + &mut count, + ) + })?; + + count = count.min(N as u16); + + let result = services_raw[..count as usize] + .iter() + .map(|services_raw| services_raw.into()) + .inspect(|svc| ::log::debug!("Found include service {svc:?}")) + .collect(); + + Ok(result) + } + + /// Get the attribute count with the given service or characteristic in the local GATTC cache. + /// * `type` The attribute type + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` if the `type` is not [`DbAttrType::Descriptor`] + pub fn get_attr_count( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + attr_type: DbAttrType, + ) -> Result { + let mut count: u16 = 0; + + let (start_handle, end_handle, char_handle) = match attr_type { + DbAttrType::PrimaryService { + start_handle, + end_handle, + } => (start_handle, end_handle, INVALID_HANDLE), + DbAttrType::SecondaryService { + start_handle, + end_handle, + } => (start_handle, end_handle, INVALID_HANDLE), + DbAttrType::Characteristic { + start_handle, + end_handle, + } => (start_handle, end_handle, INVALID_HANDLE), + DbAttrType::Descriptor { handle } => (INVALID_HANDLE, INVALID_HANDLE, handle), + DbAttrType::IncludedService { + start_handle, + end_handle, + } => (start_handle, end_handle, INVALID_HANDLE), + DbAttrType::AllAttributes { + start_handle, + end_handle, + } => (start_handle, end_handle, INVALID_HANDLE), + }; + + check_gatt_status(unsafe { + esp_ble_gattc_get_attr_count( + gattc_if, + conn_id, + *(&attr_type as *const _ as *const _), + start_handle, + end_handle, + char_handle, + &mut count, + ) + })?; + + Ok(count as _) + } + + /// Get the GATT database elements. + /// + /// # Note + /// 1. This API does not trigger any event + /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` + pub fn get_db( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + start_handle: Handle, + end_handle: Handle, + ) -> Result, GattStatus> { + let mut dbs_raw: heapless::Vec = heapless::Vec::new(); + + let mut count: u16 = N as u16; + + check_gatt_status(unsafe { + esp_ble_gattc_get_db( + gattc_if, + conn_id, + start_handle, + end_handle, + dbs_raw.as_mut_ptr(), + &mut count, + ) + })?; + + count = count.min(N as u16); + + let result = dbs_raw[..count as usize] + .iter() + .map(|dbs_raw| dbs_raw.into()) + .inspect(|db| ::log::debug!("Found db element {db:?}")) + .collect(); + + Ok(result) + } + + /// Read the characteristics value of the given characteristic handle. + /// * `handle` Characteristic handle to read + /// * `auth_req` Authenticate request type + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ReadCharacteristic`] + /// 2. This function should be called only after the connection has been established + /// 3. `handle` must be greater than 0 + pub fn read_characteristic( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + handle: Handle, + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_read_char(gattc_if, conn_id, handle, auth_req as _) }) + } + + /// Read the characteristics value of the given characteristic UUID. + /// * `start_handle` The attribute start handle + /// * `end_handle` The attribute end handle + /// * `uuid` The pointer to UUID of attribute to read + /// * `auth_req` Authenticate request type + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ReadCharacteristic`] + /// 2. This function should be called only after the connection has been established + /// 3. `start_handle` must be greater than 0, and smaller than `end_handle` + pub fn read_by_type( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + start_handle: Handle, + end_handle: Handle, + uuid: BtUuid, + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_read_by_type( + gattc_if, + conn_id, + start_handle, + end_handle, + &uuid.raw() as *const _ as *mut _, + auth_req as _, + ) + }) + } + + /// Read multiple characteristic or descriptor values. + /// * `read_multi` Handles to read, max 10 handles + /// * `auth_req` Authenticate request type + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ReadMultipleChar`] + /// 2. This function should be called only after the connection has been established + pub fn read_multiple( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + read_multi: &[Handle], + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + if read_multi.is_empty() || read_multi.len() > ESP_GATT_MAX_READ_MULTI_HANDLES as _ { + Err(EspError::from_infallible::())?; + } + + let mut handles: [u16; ESP_GATT_MAX_READ_MULTI_HANDLES as _] = + [0; ESP_GATT_MAX_READ_MULTI_HANDLES as _]; + + handles[..read_multi.len()].copy_from_slice(read_multi); + + let read_multi = esp_gattc_multi_t { + num_attr: read_multi.len() as _, + handles, + }; + + esp!(unsafe { + esp_ble_gattc_read_multiple( + gattc_if, + conn_id, + &read_multi as *const esp_gattc_multi_t as *mut _, + auth_req as _, + ) + }) + } + + /// Read multiple variable length characteristic values. + /// * `read_multi` Handles to read, max 10 handles + /// * `auth_req` Authenticate request type + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ReadMultipleVarChar`] + /// 2. This function should be called only after the connection has been established + pub fn read_multiple_variable( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + read_multi: &[Handle], + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + if read_multi.is_empty() || read_multi.len() > ESP_GATT_MAX_READ_MULTI_HANDLES as _ { + Err(EspError::from_infallible::())?; + } + + let mut handles: [u16; ESP_GATT_MAX_READ_MULTI_HANDLES as _] = + [0; ESP_GATT_MAX_READ_MULTI_HANDLES as _]; + + handles[..read_multi.len()].copy_from_slice(read_multi); + + let read_multi = esp_gattc_multi_t { + num_attr: read_multi.len() as _, + handles, + }; + + esp!(unsafe { + esp_ble_gattc_read_multiple_variable( + gattc_if, + conn_id, + &read_multi as *const esp_gattc_multi_t as *mut _, + auth_req as _, + ) + }) + } + + /// Read a characteristics descriptor. + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ReadDescriptor`] + /// 2. This function should be called only after the connection has been established + /// 3. `handle` must be greater than 0 + pub fn read_descriptor( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + handle: Handle, + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_read_char_descr(gattc_if, conn_id, handle, auth_req as _) }) + } + + /// Write the characteristic value of a given characteristic handle. + /// * `handle` The characteristic handle to write + /// * `value` The value to write + /// * `write_type` The type of Attribute write operation + /// * `auth_req` Authentication request type + /// + /// # Note + /// 1. This function triggers [`GattcEvent::WriteCharacteristic`] + /// 2. This function should be called only after the connection has been established + /// 3. `handle` must be greater than 0 + pub fn write_characteristic( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + handle: Handle, + value: &[u8], + write_type: GattWriteType, + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_write_char( + gattc_if, + conn_id, + handle, + value.len() as _, + if value.is_empty() { + core::ptr::null_mut() + } else { + value.as_ptr() as *const _ as *mut _ + }, + write_type as _, + auth_req as _, + ) + }) + } + + /// Write Characteristic descriptor value of a given descriptor handle. + /// * `handle` The descriptor handle to write + /// * `value` The value to write + /// * `write_type` The type of Attribute write operation + /// * `auth_req` Authentication request type + /// + /// # Note + /// 1. This function triggers [`GattcEvent::WriteDescriptor`] + /// 2. This function should be called only after the connection has been established + /// 3. `handle` must be greater than 0 + pub fn write_descriptor( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + handle: Handle, + value: &[u8], + write_type: GattWriteType, + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_write_char_descr( + gattc_if, + conn_id, + handle, + value.len() as _, + if value.is_empty() { + core::ptr::null_mut() + } else { + value.as_ptr() as *const _ as *mut _ + }, + write_type as _, + auth_req as _, + ) + }) + } + + /// Prepare to write a characteristic value which is longer than the MTU size to a specified characteristic handle. + /// * `handle` The characteristic handle to write + /// * `offset` The position offset to write + /// * `value` The value to write + /// * `auth_req` Authentication request type + /// + /// # Note + /// 1. This function should be called only after the connection has been established + /// 2. After using this API, use [`execute_write()`] to write + /// 3. This function triggers [`GattcEvent::PrepareWrite`] + /// 4. If `value.len()` is less than or equal to MTU size, it is recommended to [`write_characteristic()`] to write directly + /// 5. `handle` must be greater than 0 + pub fn prepare_write_characteristic( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + handle: Handle, + offset: u16, + value: &[u8], + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_prepare_write( + gattc_if, + conn_id, + handle, + offset, + value.len() as _, + if value.is_empty() { + core::ptr::null_mut() + } else { + value.as_ptr() as *const _ as *mut _ + }, + auth_req as _, + ) + }) + } + + /// Prepare to write a characteristic descriptor value at a given handle. + /// * `handle` The characteristic descriptor handle to write + /// * `offset` The position offset to write + /// * `value` The value to write + /// * `auth_req` Authentication request type + /// + /// # Note + /// 1. This function triggers [`GattcEvent::WriteCharacteristic`] + /// 2. This function should be called only after the connection has been established + /// 3. `handle` must be greater than 0 + pub fn prepare_write_descriptor( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + handle: Handle, + offset: u16, + value: &[u8], + auth_req: GattAuthReq, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_prepare_write_char_descr( + gattc_if, + conn_id, + handle, + offset, + value.len() as _, + if value.is_empty() { + core::ptr::null_mut() + } else { + value.as_ptr() as *const _ as *mut _ + }, + auth_req as _, + ) + }) + } + + /// Execute a prepared writing sequence. + /// * `is_execute` True if it is to execute the writing sequence; false if it is to cancel the writing sequence. + /// + /// # Note + /// 1. This function triggers [`GattcEvent::ExecWrite`] + pub fn execute_write( + &self, + gattc_if: GattInterface, + conn_id: ConnectionId, + is_execute: bool, + ) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_execute_write(gattc_if, conn_id, is_execute,) }) + } + + /// Register to receive notification/indication of a characteristic. + /// * `server_bda` Target GATT server device address + /// * `handle` Target GATT characteristic handle + /// + /// # Note + /// 1. This function triggers [`GattcEvent::RegisterNotify`] + /// 2. You should call [`write_descriptor()`] after this API to write Client Characteristic Configuration (CCC) + /// descriptor to the value of 1 (Enable Notification) or 2 (Enable Indication) + /// 3. `handle` must be greater than 0 + pub fn register_for_notify( + &self, + gattc_if: GattInterface, + server_addr: BdAddr, + handle: Handle, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_register_for_notify( + gattc_if, + &server_addr.raw() as *const _ as *mut _, + handle, + ) + }) + } + + /// Unregister the notification of a service. + /// * `server_bda` Target GATT server device address + /// * `handle` Target GATT characteristic handle + /// + /// # Note + /// 1. This function triggers [`GattcEvent::UnregisterNotify`] + /// 2. You should call [`write_descriptor()`] after this API to write Client Characteristic Configuration (CCC) + /// descriptor value to 0 + /// 3. `handle` must be greater than 0 + pub fn unregister_for_notify( + &self, + gattc_if: GattInterface, + server_addr: BdAddr, + handle: Handle, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_unregister_for_notify( + gattc_if, + &server_addr.raw() as *const _ as *mut _, + handle, + ) + }) + } + + /// Refresh the cache of the remote device. + /// * `remote_bda` Remote device address + /// + /// # Note + /// 1. If the device is connected, this API will restart the discovery of service information of the remote device + /// 2. This function triggers [`GattcEvent::DiscoveryCompleted`] only after the ACL connection is established. Otherwise, + /// no events will be triggered + pub fn cache_refresh(&self, remote_bda: BdAddr) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_cache_refresh(&remote_bda.raw() as *const _ as *mut _,) }) + } + + /// Add or remove the association between the address in the local GATTC cache with the source address + /// of the remote device. + /// * `src_addr` The source address intended to be associated to the `assoc_addr` which has been stored in the local GATTC cache + /// * `assoc_addr` The associated device address intended to share the attribute table with the source address + /// * `is_assoc` True if adding the association; false if removing the association + /// + /// # Note + /// 1. This API is primarily used when the client has a stored server-side database (`assoc_addr`) and needs to connect to + /// another device (`src_addr`) with the same attribute database. By invoking this API, the stored database is utilized + /// as the peer server database, eliminating the need for attribute database search and discovery. This reduces + /// processing time and accelerates the connection process + /// 2. The attribute table of a device with `assoc_addr` must be stored in the local GATTC cache first. + /// Then, the attribute table of the device with `src_addr` must be the same as the one with `assoc_addr` + /// 3. This function triggers [`GattcEvent::SetAssociation`] + pub fn cache_assoc( + &self, + gattc_if: GattInterface, + src_addr: BdAddr, + assoc_addr: BdAddr, + is_assoc: bool, + ) -> Result<(), EspError> { + esp!(unsafe { + esp_ble_gattc_cache_assoc( + gattc_if, + &src_addr.raw() as *const _ as *mut _, + &assoc_addr.raw() as *const _ as *mut _, + is_assoc, + ) + }) + } + + /// Get the address list stored in the local GATTC cache. + /// + /// # Note + /// 1. This function triggers [`GattcEvent::AddressList`] + pub fn get_address_list(&self, gattc_if: GattInterface) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_cache_get_addr_list(gattc_if,) }) + } + + /// Clean the service cache of the target device in the local GATTC cache. + /// * `remote_bda` Remote device address + pub fn cache_clean(&self, remote_addr: BdAddr) -> Result<(), EspError> { + esp!(unsafe { esp_ble_gattc_cache_clean(&remote_addr.raw() as *const _ as *mut _,) }) + } + + unsafe extern "C" fn event_handler( + event: esp_gattc_cb_event_t, + gattc_if: esp_gatt_if_t, + param: *mut esp_ble_gattc_cb_param_t, + ) { + let param = unsafe { param.as_ref() }.unwrap(); + let event = GattcEvent::from((event, param)); + + trace!("Got event {{ {event:#?} }}"); + + SINGLETON.call((gattc_if, event)); + } +} + +impl<'d, M, T> Drop for EspGattc<'d, M, T> +where + T: Borrow>, + M: BleEnabled, +{ + fn drop(&mut self) { + self.unsubscribe().unwrap(); + + esp!(unsafe { esp_ble_gattc_register_callback(None) }).unwrap(); + + SINGLETON.release().unwrap(); + } +} + +unsafe impl<'d, M, T> Send for EspGattc<'d, M, T> +where + T: Borrow> + Send, + M: BleEnabled, +{ +} + +// Safe because the ESP IDF Bluedroid APIs all do message passing +// to a dedicated Bluedroid task +unsafe impl<'d, M, T> Sync for EspGattc<'d, M, T> +where + T: Borrow> + Send, + M: BleEnabled, +{ +} + +static SINGLETON: BtSingleton<(GattInterface, GattcEvent), ()> = BtSingleton::new(()); + +fn check_gatt_status(status: esp_gatt_status_t) -> Result<(), GattStatus> { + let status = status.try_into().unwrap(); + if status == GattStatus::Ok { + Ok(()) + } else { + Err(status) + } +} diff --git a/src/bt/ble/gatt/server.rs b/src/bt/ble/gatt/server.rs index 9bb5716cb7a..939fb8dca9c 100644 --- a/src/bt/ble/gatt/server.rs +++ b/src/bt/ble/gatt/server.rs @@ -677,7 +677,7 @@ where } unsafe extern "C" fn event_handler( - event: esp_gap_ble_cb_event_t, + event: esp_gatts_cb_event_t, gatts_if: esp_gatt_if_t, param: *mut esp_ble_gatts_cb_param_t, ) { From 81d9b500177f1276efd83b9dc058cc681d0479d5 Mon Sep 17 00:00:00 2001 From: Ferdy Date: Fri, 28 Nov 2025 13:20:38 -0700 Subject: [PATCH 2/8] Fix usage of slice import --- src/bt/ble/gap.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/bt/ble/gap.rs b/src/bt/ble/gap.rs index a20af362eaa..1dd5ac5b658 100644 --- a/src/bt/ble/gap.rs +++ b/src/bt/ble/gap.rs @@ -2,7 +2,6 @@ use core::borrow::Borrow; use core::fmt::{self, Debug}; use core::marker::PhantomData; use core::{ffi::CStr, ops::BitOr}; -use std::slice; use crate::bt::BtSingleton; use crate::sys::*; @@ -893,7 +892,7 @@ where if length == 0 { None } else { - Some(unsafe { slice::from_raw_parts(resolve_adv_data, length as _) }) + Some(unsafe { core::slice::from_raw_parts(resolve_adv_data, length as _) }) } } From a735d461cdcb183b4513bfc741ad4e060271ffa8 Mon Sep 17 00:00:00 2001 From: Ferdy Date: Fri, 28 Nov 2025 13:51:06 -0700 Subject: [PATCH 3/8] Fix MSRV in gatt client example --- examples/bt_gatt_client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/bt_gatt_client.rs b/examples/bt_gatt_client.rs index cd0cc9a20c1..6af9a639cee 100644 --- a/examples/bt_gatt_client.rs +++ b/examples/bt_gatt_client.rs @@ -204,7 +204,7 @@ mod example { adv_data_len as u16 + scan_rsp_len as u16, AdvertisingDataType::NameCmpl, ); - let name = name.map(str::from_utf8).transpose().ok().flatten(); + let name = name.map(|n| str::from_utf8(n)).transpose().ok().flatten(); info!("Scan result, device {bda} - rssi {rssi}, name: {name:?}"); @@ -598,7 +598,7 @@ mod example { gattc_if, conn_id, write_char_handle, - &char_value, + char_value, GattWriteType::RequireResponse, GattAuthReq::None, )?; From f06e5d605b538be7e2691441f1ce3bf8cc95082e Mon Sep 17 00:00:00 2001 From: Ferdy Date: Fri, 28 Nov 2025 14:07:11 -0700 Subject: [PATCH 4/8] Fix MSRV in gatt client example --- examples/bt_gatt_client.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/bt_gatt_client.rs b/examples/bt_gatt_client.rs index 6af9a639cee..d5c7a248f0a 100644 --- a/examples/bt_gatt_client.rs +++ b/examples/bt_gatt_client.rs @@ -204,7 +204,11 @@ mod example { adv_data_len as u16 + scan_rsp_len as u16, AdvertisingDataType::NameCmpl, ); - let name = name.map(|n| str::from_utf8(n)).transpose().ok().flatten(); + let name = name + .map(|n| std::str::from_utf8(n)) + .transpose() + .ok() + .flatten(); info!("Scan result, device {bda} - rssi {rssi}, name: {name:?}"); From f1bc3710721ddcf946289fd0632cc958c6806fcd Mon Sep 17 00:00:00 2001 From: Ferdy Date: Wed, 3 Dec 2025 15:48:30 -0700 Subject: [PATCH 5/8] Remove heapless requirement and use callers buffer for queries --- examples/bt_gatt_client.rs | 142 +++++++---- src/bt/ble/gap.rs | 41 ++- src/bt/ble/gatt/client.rs | 505 ++++++++++++++++++++----------------- 3 files changed, 400 insertions(+), 288 deletions(-) diff --git a/examples/bt_gatt_client.rs b/examples/bt_gatt_client.rs index d5c7a248f0a..64de2db92fa 100644 --- a/examples/bt_gatt_client.rs +++ b/examples/bt_gatt_client.rs @@ -42,12 +42,11 @@ mod example { use std::time::Duration; use esp_idf_svc::bt::ble::gap::{ - AdvertisingDataType, BleAddrType, BleGapEvent, EspBleGap, GapSearchEvent, ScanDuplicate, - ScanFilter, ScanParams, ScanType, + AdvertisingDataType, BleGapEvent, EspBleGap, GapSearchEvent, ScanParams, }; use esp_idf_svc::bt::ble::gatt::client::{ - ConnectionId, DbAttrType, EspGattc, GattAuthReq, GattCreateConnParams, GattWriteType, - GattcEvent, ServiceSource, + CharacteristicElement, ConnectionId, DbAttrType, DbElement, DescriptorElement, EspGattc, + GattAuthReq, GattCreateConnParams, GattWriteType, GattcEvent, ServiceSource, }; use esp_idf_svc::bt::ble::gatt::{self, GattInterface, GattStatus, Handle, Property}; use esp_idf_svc::bt::{BdAddr, Ble, BtDriver, BtStatus, BtUuid}; @@ -194,17 +193,16 @@ mod example { ble_addr_type, rssi, ble_adv, - adv_data_len, - scan_rsp_len, .. } => { if GapSearchEvent::InquiryResult == search_evt { - let name = self.gap.resolve_adv_data_by_type( - &ble_adv, - adv_data_len as u16 + scan_rsp_len as u16, - AdvertisingDataType::NameCmpl, - ); - let name = name + let name = ble_adv + .and_then(|ble_adv| { + self.gap.resolve_adv_data_by_type( + ble_adv, + AdvertisingDataType::NameCmpl, + ) + }) .map(|n| std::str::from_utf8(n)) .transpose() .ok() @@ -342,7 +340,32 @@ mod example { let mut state = self.state.lock().unwrap(); if let Some((start_handle, end_handle)) = state.service_start_end_handle { - let count = self + // Enumerate all the elements for info purposes + let mut db_results = [DbElement::new(); 10]; + match self.gattc.get_db( + gattc_if, + conn_id, + start_handle, + end_handle, + &mut db_results, + ) { + Ok(db_count) => { + info!("Found {db_count} DB elements"); + + if db_count > 0 { + for db_elem in db_results[..db_count].iter() { + info!("DB element {db_elem:?}"); + } + } else { + info!("No DB elements found?"); + } + } + Err(status) => { + error!("Get all DB elements error {status:?}"); + } + } + + let char_count = self .gattc .get_attr_count( gattc_if, @@ -357,31 +380,39 @@ mod example { EspError::from_infallible::() })?; - info!("Found {count} characterisitics"); + info!("Found {char_count} characteristics"); - if count > 0 { + if char_count > 0 { // Get the indicator characteristic handle and register for notification - match self.gattc.get_characteristic_by_uuid::<1>( + let mut chars = [CharacteristicElement::new(); 1]; + + match self.gattc.get_characteristic_by_uuid( gattc_if, conn_id, start_handle, end_handle, IND_CHARACTERISTIC_UUID, + &mut chars, ) { - Ok(chars) => { - if let Some(ind_char_elem) = chars.first() { - if ind_char_elem.properties.contains(Property::Indicate) { - if let Some(remote_addr) = state.remote_addr { - state.ind_char_handle = - Some(ind_char_elem.char_handle); - self.gattc.register_for_notify( - gattc_if, - remote_addr, - ind_char_elem.char_handle, - )?; + Ok(char_count) => { + if char_count > 0 { + if let Some(ind_char_elem) = chars.first() { + if ind_char_elem + .properties() + .contains(Property::Indicate) + { + if let Some(remote_addr) = state.remote_addr { + state.ind_char_handle = + Some(ind_char_elem.handle()); + self.gattc.register_for_notify( + gattc_if, + remote_addr, + ind_char_elem.handle(), + )?; + } + } else { + error!("Ind characteristic does not have property Indicate"); } - } else { - error!("Ind characteristic does not have property Indicate"); } } else { error!("No ind characteristic found"); @@ -393,32 +424,38 @@ mod example { }; // Get the write characteristic handle and start sending data to the server - match self.gattc.get_characteristic_by_uuid::<1>( + match self.gattc.get_characteristic_by_uuid( gattc_if, conn_id, start_handle, end_handle, WRITE_CHARACTERISITIC_UUID, + &mut chars, ) { - Ok(chars) => { - if let Some(write_char_elem) = chars.first() { - if write_char_elem.properties.contains(Property::Write) { - state.write_char_handle = - Some(write_char_elem.char_handle); - - // Let main loop send write - self.condvar.notify_all(); - } else { - error!( + Ok(char_count) => { + if char_count > 0 { + if let Some(write_char_elem) = chars.first() { + if write_char_elem + .properties() + .contains(Property::Write) + { + state.write_char_handle = + Some(write_char_elem.handle()); + + // Let main loop send write + self.condvar.notify_all(); + } else { + error!( "Write characteristic does not have property Write" ); + } } } else { error!("No write characteristic found"); } } Err(status) => { - error!("get write characteristic error {status:?}"); + error!("Get write characteristic error {status:?}"); } }; } else { @@ -443,23 +480,28 @@ mod example { })?; if count > 0 { - match self.gattc.get_descriptor_by_char_handle::<1>( + let mut descrs = [DescriptorElement::new(); 1]; + + match self.gattc.get_descriptor_by_char_handle( gattc_if, conn_id, handle, IND_DESCRIPTOR_UUID, + &mut descrs, ) { - Ok(descrs) => { - if let Some(descr) = descrs.first() { - if descr.uuid == IND_DESCRIPTOR_UUID { - state.ind_descr_handle = Some(descr.handle); + Ok(descrs_count) => { + if descrs_count > 0 { + if let Some(descr) = descrs.first() { + if descr.uuid() == IND_DESCRIPTOR_UUID { + state.ind_descr_handle = Some(descr.handle()); + } } } else { error!("No ind descriptor found"); } } Err(status) => { - error!("get ind char descriptors error {status:?}"); + error!("Get ind char descriptors error {status:?}"); } } } else { @@ -514,12 +556,8 @@ mod example { pub fn connect(&self) -> Result<(), EspError> { if !self.state.lock().unwrap().connect { let scan_params = ScanParams { - scan_type: ScanType::Active, - own_addr_type: BleAddrType::Public, - scan_filter_policy: ScanFilter::All, scan_interval: 0x50, - scan_window: 0x30, - scan_duplicate: ScanDuplicate::Disable, + ..Default::default() }; self.gap.set_scan_params(&scan_params)?; diff --git a/src/bt/ble/gap.rs b/src/bt/ble/gap.rs index 1dd5ac5b658..bc5d29046a0 100644 --- a/src/bt/ble/gap.rs +++ b/src/bt/ble/gap.rs @@ -6,8 +6,7 @@ use core::{ffi::CStr, ops::BitOr}; use crate::bt::BtSingleton; use crate::sys::*; -use ::log::trace; -use log::error; +use ::log::{error, trace}; use num_enum::TryFromPrimitive; use crate::{ @@ -275,10 +274,11 @@ pub enum AdvertisingDataType { ManufacturerSpecific = esp_ble_adv_data_type_ESP_BLE_AD_MANUFACTURER_SPECIFIC_TYPE, } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum ScanType { Passive = esp_ble_scan_type_t_BLE_SCAN_TYPE_PASSIVE, + #[default] Active = esp_ble_scan_type_t_BLE_SCAN_TYPE_ACTIVE, } @@ -292,11 +292,12 @@ pub enum BleAddrType { RpaRandom = esp_ble_addr_type_t_BLE_ADDR_TYPE_RPA_RANDOM, } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum ScanFilter { /// Accept all : /// 1. advertisement packets except directed advertising packets not addressed to this device (default). + #[default] All = esp_ble_scan_filter_t_BLE_SCAN_FILTER_ALLOW_ALL, /// Accept only : /// 1. advertisement packets from devices where the advertiser’s address is in the White list. @@ -314,9 +315,10 @@ pub enum ScanFilter { WhitelistAndDirected = esp_ble_scan_filter_t_BLE_SCAN_FILTER_ALLOW_WLIST_RPA_DIR, } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] #[repr(u32)] pub enum ScanDuplicate { + #[default] Disable = esp_ble_scan_duplicate_t_BLE_SCAN_DUPLICATE_DISABLE, Enable = esp_ble_scan_duplicate_t_BLE_SCAN_DUPLICATE_ENABLE, #[cfg(esp_idf_ble_50_feature_support)] @@ -324,6 +326,7 @@ pub enum ScanDuplicate { Max = esp_ble_scan_duplicate_t_BLE_SCAN_DUPLICATE_MAX, } +#[derive(Clone, Debug)] pub struct ScanParams { pub scan_type: ScanType, pub own_addr_type: BleAddrType, @@ -333,6 +336,19 @@ pub struct ScanParams { pub scan_duplicate: ScanDuplicate, } +impl Default for ScanParams { + fn default() -> Self { + Self { + scan_type: ScanType::Active, + own_addr_type: BleAddrType::Public, + scan_filter_policy: ScanFilter::All, + scan_interval: 0x50, + scan_window: 0x30, + scan_duplicate: ScanDuplicate::Disable, + } + } +} + impl From<&ScanParams> for esp_ble_scan_params_t { fn from(params: &ScanParams) -> Self { Self { @@ -397,7 +413,7 @@ pub enum BleGapEvent<'a> { ble_addr_type: BleAddrType, ble_evt_type: BleEventType, rssi: i32, - ble_adv: [u8; 62usize], + ble_adv: Option<&'a [u8]>, flag: i32, num_resps: i32, adv_data_len: u8, @@ -542,7 +558,15 @@ impl<'a> From<(esp_gap_ble_cb_event_t, &'a esp_ble_gap_cb_param_t)> for BleGapEv ble_addr_type: param.scan_rst.ble_addr_type.try_into().unwrap(), ble_evt_type: param.scan_rst.ble_evt_type.try_into().unwrap(), rssi: param.scan_rst.rssi, - ble_adv: param.scan_rst.ble_adv, + ble_adv: if param.scan_rst.adv_data_len + param.scan_rst.scan_rsp_len > 0 { + Some( + ¶m.scan_rst.ble_adv[..(param.scan_rst.adv_data_len + + param.scan_rst.scan_rsp_len) + as usize], + ) + } else { + None + }, flag: param.scan_rst.flag, num_resps: param.scan_rst.num_resps, adv_data_len: param.scan_rst.adv_data_len, @@ -875,7 +899,6 @@ where pub fn resolve_adv_data_by_type( &self, adv_data: &[u8], - adv_data_len: u16, data_type: AdvertisingDataType, ) -> Option<&[u8]> { let mut length: u8 = 0; @@ -883,7 +906,7 @@ where let resolve_adv_data = unsafe { esp_ble_resolve_adv_data_by_type( adv_data as *const _ as *mut _, - adv_data_len, + adv_data.len() as _, data_type as _, &mut length, ) diff --git a/src/bt/ble/gatt/client.rs b/src/bt/ble/gatt/client.rs index 8e394086aad..7a91aa2ce0c 100644 --- a/src/bt/ble/gatt/client.rs +++ b/src/bt/ble/gatt/client.rs @@ -147,8 +147,9 @@ pub struct GattCreateConnParams { /// Connection parameters for the LE Coded PHY pub phy_coded_conn_params: Option, } + impl GattCreateConnParams { - pub fn new(addr: BdAddr, addr_type: BleAddrType) -> Self { + pub const fn new(addr: BdAddr, addr_type: BleAddrType) -> Self { Self { addr, addr_type, @@ -162,86 +163,172 @@ impl GattCreateConnParams { } } -#[derive(Debug, Clone)] -pub struct GattcService { - /// Indicates if the service is primary - pub is_primary: bool, - /// Service start handle - pub start_handle: Handle, - /// Service end handle - pub end_handle: Handle, - /// Service UUID - pub uuid: BtUuid, +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct ServiceElement(esp_gattc_service_elem_t); + +impl ServiceElement { + pub const fn new() -> Self { + Self(esp_gattc_service_elem_t { + uuid: BtUuid::uuid16(0).raw(), + is_primary: false, + start_handle: 0, + end_handle: 0, + }) + } + + pub fn uuid(&self) -> BtUuid { + self.0.uuid.into() + } + + pub fn is_primary(&self) -> bool { + self.0.is_primary + } + + pub fn start_handle(&self) -> Handle { + self.0.start_handle + } + pub fn end_handle(&self) -> Handle { + self.0.end_handle + } } -impl From<&esp_gattc_service_elem_t> for GattcService { - fn from(svc: &esp_gattc_service_elem_t) -> Self { - Self { - is_primary: svc.is_primary, - start_handle: svc.start_handle, - end_handle: svc.end_handle, - uuid: svc.uuid.into(), - } +impl Default for ServiceElement { + fn default() -> Self { + Self::new() } } -#[derive(Debug, Clone)] -pub struct IncludeService { - /// Current attribute handle of the included service - pub handle: Handle, - /// Start handle of the included service - pub incl_srvc_s_handle: Handle, - /// End handle of the included service - pub incl_srvc_e_handle: Handle, - /// Included service UUID - pub uuid: BtUuid, +impl Debug for ServiceElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ServiceElement") + .field("uuid", &self.uuid()) + .field("is_primary", &self.0.is_primary) + .field("start_handle", &self.0.start_handle) + .field("end_handle", &self.0.end_handle) + .finish() + } } -impl From<&esp_gattc_incl_svc_elem_t> for IncludeService { - fn from(svc: &esp_gattc_incl_svc_elem_t) -> Self { - Self { - handle: svc.handle, - incl_srvc_s_handle: svc.incl_srvc_s_handle, - incl_srvc_e_handle: svc.incl_srvc_e_handle, - uuid: svc.uuid.into(), - } +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct IncludeServiceElement(esp_gattc_incl_svc_elem_t); + +impl IncludeServiceElement { + pub const fn new() -> Self { + Self(esp_gattc_incl_svc_elem_t { + uuid: BtUuid::uuid16(0).raw(), + handle: 0, + incl_srvc_s_handle: 0, + incl_srvc_e_handle: 0, + }) + } + + pub fn uuid(&self) -> BtUuid { + self.0.uuid.into() + } + + pub fn handle(&self) -> Handle { + self.0.handle + } + pub fn start_handle(&self) -> Handle { + self.0.incl_srvc_s_handle + } + pub fn end_handle(&self) -> Handle { + self.0.incl_srvc_e_handle } } -#[derive(Debug, Clone)] -pub struct CharacteristicElement { - /// Characteristic handle - pub char_handle: Handle, - /// Characteristic properties - pub properties: EnumSet, - /// Characteristic UUID - pub uuid: BtUuid, +impl Default for IncludeServiceElement { + fn default() -> Self { + Self::new() + } } -impl From<&esp_gattc_char_elem_t> for CharacteristicElement { - fn from(elem: &esp_gattc_char_elem_t) -> Self { - Self { - char_handle: elem.char_handle, - properties: EnumSet::from_repr(elem.properties), - uuid: elem.uuid.into(), - } +impl Debug for IncludeServiceElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IncludeService") + .field("uuid", &self.uuid()) + .field("handle", &self.handle()) + .field("start_handle", &self.start_handle()) + .field("end_handle", &self.end_handle()) + .finish() } } -#[derive(Debug, Clone)] -pub struct DescriptorElement { - /// Descriptor handle - pub handle: Handle, - /// Descriptor UUID - pub uuid: BtUuid, +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct CharacteristicElement(esp_gattc_char_elem_t); + +impl CharacteristicElement { + pub const fn new() -> Self { + Self(esp_gattc_char_elem_t { + uuid: BtUuid::uuid16(0).raw(), + char_handle: 0, + properties: 0, + }) + } + + pub fn uuid(&self) -> BtUuid { + self.0.uuid.into() + } + + pub fn handle(&self) -> Handle { + self.0.char_handle + } + + pub fn properties(&self) -> EnumSet { + EnumSet::from_repr(self.0.properties) + } } -impl From<&esp_gattc_descr_elem_t> for DescriptorElement { - fn from(elem: &esp_gattc_descr_elem_t) -> Self { - Self { - handle: elem.handle, - uuid: elem.uuid.into(), - } +impl Default for CharacteristicElement { + fn default() -> Self { + Self::new() + } +} +impl Debug for CharacteristicElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CharacteristicElement") + .field("uuid", &self.uuid()) + .field("handle", &self.handle()) + .field("properties", &self.properties()) + .finish() + } +} + +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct DescriptorElement(esp_gattc_descr_elem_t); + +impl DescriptorElement { + pub const fn new() -> Self { + Self(esp_gattc_descr_elem_t { + uuid: BtUuid::uuid16(0).raw(), + handle: 0, + }) + } + + pub fn uuid(&self) -> BtUuid { + self.0.uuid.into() + } + + pub fn handle(&self) -> Handle { + self.0.handle + } +} + +impl Default for DescriptorElement { + fn default() -> Self { + Self::new() + } +} +impl Debug for DescriptorElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DescriptorElement") + .field("uuid", &self.uuid()) + .field("handle", &self.handle()) + .finish() } } @@ -278,18 +365,31 @@ pub enum DbElementAttrType { } = esp_gatt_db_attr_type_t_ESP_GATT_DB_ALL, } -#[derive(Debug, Clone)] -pub struct DbElement { - /// Attribute UUID. - pub uuid: BtUuid, - /// Attribute type. - pub attr_type: DbElementAttrType, -} +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct DbElement(esp_gattc_db_elem_t); + +impl DbElement { + pub const fn new() -> Self { + Self(esp_gattc_db_elem_t { + uuid: BtUuid::uuid16(0).raw(), + type_: 0, + attribute_handle: 0, + start_handle: 0, + end_handle: 0, + properties: 0, + }) + } + + pub fn uuid(&self) -> BtUuid { + self.0.uuid.into() + } + + pub fn attribute_type(&self) -> DbElementAttrType { + let elem = self.0; -impl From<&esp_gattc_db_elem_t> for DbElement { - fn from(elem: &esp_gattc_db_elem_t) -> Self { #[allow(non_upper_case_globals)] - let attr_type = match elem.type_ { + match elem.type_ { esp_gatt_db_attr_type_t_ESP_GATT_DB_PRIMARY_SERVICE => { DbElementAttrType::PrimaryService { start_handle: elem.start_handle, @@ -329,15 +429,25 @@ impl From<&esp_gattc_db_elem_t> for DbElement { attribute_handle: elem.attribute_handle, properties: EnumSet::from_repr(elem.properties), }, - }; - - Self { - attr_type, - uuid: elem.uuid.into(), } } } +impl Default for DbElement { + fn default() -> Self { + Self::new() + } +} + +impl Debug for DbElement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DbElement") + .field("uuid", &self.uuid()) + .field("attribute_type", &self.attribute_type()) + .finish() + } +} + pub struct EventRawData<'a>(pub &'a esp_ble_gattc_cb_param_t); impl Debug for EventRawData<'_> { @@ -1026,74 +1136,68 @@ where /// Get the service with the given service UUID in the local GATTC cache. /// * `svc_uuid` The service UUID. If `None` is passed, the API will retrieve all services. /// * `offset` The position offset to retrieve + /// * `results` That will be updated with the services found in the local GATTC cache /// - /// Returns the service which has been found in the local GATTC cache + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. [`cache_refresh()`] can be used to discover services again - pub fn get_service( + pub fn get_service( &self, gattc_if: GattInterface, conn_id: ConnectionId, svc_uuid: Option, offset: u16, - ) -> Result, EspError> { - let count = if svc_uuid.is_some() { 1 } else { N as _ }; - - let mut services_raw: heapless::Vec = heapless::Vec::new(); - unsafe { services_raw.set_len(count) }; - - let mut count: u16 = count as u16; + results: &mut [ServiceElement], + ) -> Result { + let mut count: u16 = if svc_uuid.is_some() { + 1 + } else { + results.len() as _ + }; esp!(unsafe { esp_ble_gattc_get_service( gattc_if, conn_id, svc_uuid.map_or(core::ptr::null_mut(), |s| &s.raw() as *const _ as *mut _), - services_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, offset, ) })?; - count = if offset > count { + Ok(if offset > count { 0 } else { - (count - offset).min(N as u16) - }; - - let result = services_raw[..count as usize] - .iter() - .map(|service_raw| service_raw.into()) - .inspect(|service| ::log::debug!("Found service {service:?}")) - .collect(); - - Ok(result) + (count - offset).min(results.len() as u16) as _ + }) } /// Get all characteristics with the given handle range in the local GATTC cache. /// * `start_handle` The attribute start handle /// * `end_handle` The attribute end handle /// * `offset` The position offset to retrieve + /// * `results` That will be updated with the characteristics found in the local GATTC cache + /// + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` - pub fn get_all_characteristics( + pub fn get_all_characteristics( &self, gattc_if: GattInterface, conn_id: ConnectionId, start_handle: Handle, end_handle: Handle, offset: u16, - ) -> Result, GattStatus> { - let mut chars_raw: heapless::Vec = heapless::Vec::new(); - unsafe { - chars_raw.set_len(N); - } - - let mut count: u16 = N as u16; + results: &mut [CharacteristicElement], + ) -> Result { + let mut count: u16 = results.len() as _; check_gatt_status(unsafe { esp_ble_gattc_get_all_char( @@ -1101,96 +1205,80 @@ where conn_id, start_handle, end_handle, - chars_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, offset, ) })?; - count = if offset > count { + Ok(if offset > count { 0 } else { - (count - offset).min(N as u16) - }; - - let result = chars_raw[..count as usize] - .iter() - .map(|chars_raw| chars_raw.into()) - .inspect(|char| ::log::debug!("Found characteristic {char:?}")) - .collect(); - - Ok(result) + (count - offset).min(results.len() as u16) as _ + }) } /// Get all descriptors with the given characteristic in the local GATTC cache. /// * `char_handle` The given characteristic handle /// * `offset` The position offset to retrieve + /// * `results` That will be updated with the descriptors found in the local GATTC cache + /// + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. `char_handle` must be greater than 0 - pub fn get_all_descriptors( + pub fn get_all_descriptors( &self, gattc_if: GattInterface, conn_id: ConnectionId, char_handle: Handle, offset: u16, - ) -> Result, GattStatus> { - let mut descrs_raw: heapless::Vec = heapless::Vec::new(); - unsafe { - descrs_raw.set_len(N); - } - - let mut count: u16 = N as u16; + results: &mut [DescriptorElement], + ) -> Result { + let mut count: u16 = results.len() as _; check_gatt_status(unsafe { esp_ble_gattc_get_all_descr( gattc_if, conn_id, char_handle, - descrs_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, offset, ) })?; - count = if offset > count { + Ok(if offset > count { 0 } else { - (count - offset).min(N as u16) - }; - - let result = descrs_raw[..count as usize] - .iter() - .map(|descrs_raw| descrs_raw.into()) - .inspect(|descr| ::log::debug!("Found descriptor {descr:?}")) - .collect(); - - Ok(result) + (count - offset).min(results.len() as u16) as _ + }) } /// Get the characteristic with the given characteristic UUID in the local GATTC cache. /// * `start_handle` The attribute start handle /// * `end_handle` The attribute end handle /// * `char_uuid` The characteristic UUID + /// * `results` That will be updated with the characteristics found in the local GATTC cache + /// + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` - pub fn get_characteristic_by_uuid( + pub fn get_characteristic_by_uuid( &self, gattc_if: GattInterface, conn_id: ConnectionId, start_handle: Handle, end_handle: Handle, char_uuid: BtUuid, - ) -> Result, GattStatus> { - let mut chars_raw: heapless::Vec = heapless::Vec::new(); - unsafe { - chars_raw.set_len(N); - } - - let mut count: u16 = N as u16; + results: &mut [CharacteristicElement], + ) -> Result { + let mut count: u16 = results.len() as _; check_gatt_status(unsafe { esp_ble_gattc_get_char_by_uuid( @@ -1199,20 +1287,12 @@ where start_handle, end_handle, char_uuid.raw(), - chars_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, ) })?; - count = count.min(N as u16); - - let result = chars_raw[..count as usize] - .iter() - .map(|chars_raw| chars_raw.into()) - .inspect(|char| ::log::debug!("Found characteristic {char:?}")) - .collect(); - - Ok(result) + Ok(count.min(results.len() as u16) as _) } /// Get the descriptor with the given characteristic UUID in the local GATTC cache. @@ -1220,11 +1300,15 @@ where /// * `end_handle` The attribute end handle /// * `char_uuid` The characteristic UUID /// * `descr_uuid` The descriptor UUID + /// * `results` That will be updated with the descriptors found in the local GATTC cache + /// + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` - pub fn get_descriptor_by_uuid( + pub fn get_descriptor_by_uuid( &self, gattc_if: GattInterface, conn_id: ConnectionId, @@ -1232,13 +1316,9 @@ where end_handle: Handle, char_uuid: BtUuid, descr_uuid: BtUuid, - ) -> Result, GattStatus> { - let mut descrs_raw: heapless::Vec = heapless::Vec::new(); - unsafe { - descrs_raw.set_len(N); - } - - let mut count: u16 = N as u16; + results: &mut [DescriptorElement], + ) -> Result { + let mut count: u16 = results.len() as _; check_gatt_status(unsafe { esp_ble_gattc_get_descr_by_uuid( @@ -1248,42 +1328,34 @@ where end_handle, char_uuid.raw(), descr_uuid.raw(), - descrs_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, ) })?; - count = count.min(N as u16); - - let result = descrs_raw[..count as usize] - .iter() - .map(|descrs_raw| descrs_raw.into()) - .inspect(|descr| ::log::debug!("Found descriptor {descr:?}")) - .collect(); - - Ok(result) + Ok(count.min(results.len() as u16) as _) } /// Get the descriptor with the given characteristic handle in the local GATTC cache. /// * `char_handle` The characteristic handle /// * `descr_uuid` The descriptor UUID + /// * `results` That will be updated with the descriptors found in the local GATTC cache + /// + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. `char_handle` must be greater than 0 - pub fn get_descriptor_by_char_handle( + pub fn get_descriptor_by_char_handle( &self, gattc_if: GattInterface, conn_id: ConnectionId, char_handle: Handle, descr_uuid: BtUuid, - ) -> Result, GattStatus> { - let mut descrs_raw: heapless::Vec = heapless::Vec::new(); - unsafe { - descrs_raw.set_len(N); - } - - let mut count: u16 = N as u16; + results: &mut [DescriptorElement], + ) -> Result { + let mut count: u16 = results.len() as _; check_gatt_status(unsafe { esp_ble_gattc_get_descr_by_char_handle( @@ -1291,42 +1363,34 @@ where conn_id, char_handle, descr_uuid.raw(), - descrs_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, ) })?; - count = count.min(N as u16); - - let result = descrs_raw[..count as usize] - .iter() - .map(|descrs_raw| descrs_raw.into()) - .inspect(|descr| ::log::debug!("Found descriptor {descr:?}")) - .collect(); - - Ok(result) + Ok(count.min(results.len() as u16) as _) } /// Get the included services with the given service handle in the local GATTC cache. /// * `incl_uuid` The included service UUID + /// * `results` That will be updated with the include services found in the local GATTC cache + /// + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` - pub fn get_include_service( + pub fn get_include_service( &self, gattc_if: GattInterface, conn_id: ConnectionId, start_handle: Handle, end_handle: Handle, incl_uuid: BtUuid, - ) -> Result, GattStatus> { - let mut services_raw: heapless::Vec = heapless::Vec::new(); - unsafe { - services_raw.set_len(N); - } - - let mut count: u16 = N as u16; + results: &mut [IncludeServiceElement], + ) -> Result { + let mut count: u16 = results.len() as _; check_gatt_status(unsafe { esp_ble_gattc_get_include_service( @@ -1335,20 +1399,12 @@ where start_handle, end_handle, &incl_uuid.raw() as *const _ as *mut _, - services_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, ) })?; - count = count.min(N as u16); - - let result = services_raw[..count as usize] - .iter() - .map(|services_raw| services_raw.into()) - .inspect(|svc| ::log::debug!("Found include service {svc:?}")) - .collect(); - - Ok(result) + Ok(count.min(results.len() as u16) as _) } /// Get the attribute count with the given service or characteristic in the local GATTC cache. @@ -1405,20 +1461,23 @@ where } /// Get the GATT database elements. + /// * `results` That will be updated with the db elements found in the local GATTC cache + /// + /// # Returns + /// The number of elements in `results`, which could be 0; this is not the actual number of elements /// /// # Note /// 1. This API does not trigger any event /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` - pub fn get_db( + pub fn get_db( &self, gattc_if: GattInterface, conn_id: ConnectionId, start_handle: Handle, end_handle: Handle, - ) -> Result, GattStatus> { - let mut dbs_raw: heapless::Vec = heapless::Vec::new(); - - let mut count: u16 = N as u16; + results: &mut [DbElement], + ) -> Result { + let mut count: u16 = results.len() as _; check_gatt_status(unsafe { esp_ble_gattc_get_db( @@ -1426,20 +1485,12 @@ where conn_id, start_handle, end_handle, - dbs_raw.as_mut_ptr(), + results as *const _ as *mut _, &mut count, ) })?; - count = count.min(N as u16); - - let result = dbs_raw[..count as usize] - .iter() - .map(|dbs_raw| dbs_raw.into()) - .inspect(|db| ::log::debug!("Found db element {db:?}")) - .collect(); - - Ok(result) + Ok(count.min(results.len() as u16) as _) } /// Read the characteristics value of the given characteristic handle. From f937a74b39a51a30dd348f81be77b9ba707864a3 Mon Sep 17 00:00:00 2001 From: Ferdy Date: Wed, 3 Dec 2025 16:06:02 -0700 Subject: [PATCH 6/8] Fix clippy too many args... --- src/bt/ble/gatt/client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bt/ble/gatt/client.rs b/src/bt/ble/gatt/client.rs index 7a91aa2ce0c..6b902dc1115 100644 --- a/src/bt/ble/gatt/client.rs +++ b/src/bt/ble/gatt/client.rs @@ -1308,6 +1308,7 @@ where /// # Note /// 1. This API does not trigger any event /// 2. `start_handle` must be greater than 0, and smaller than `end_handle` + #[allow(clippy::too_many_arguments)] pub fn get_descriptor_by_uuid( &self, gattc_if: GattInterface, From 7da136cbe8ed0635fe6c44dce8538cfbea752501 Mon Sep 17 00:00:00 2001 From: Ferdy Date: Mon, 8 Dec 2025 18:47:25 -0700 Subject: [PATCH 7/8] Fix GAP scan result, fields are specific to result event type --- examples/bt_gatt_client.rs | 22 ++++--- src/bt/ble/gap.rs | 127 ++++++++++++++++++++++++------------- 2 files changed, 95 insertions(+), 54 deletions(-) diff --git a/examples/bt_gatt_client.rs b/examples/bt_gatt_client.rs index 64de2db92fa..e4d4cff0ab6 100644 --- a/examples/bt_gatt_client.rs +++ b/examples/bt_gatt_client.rs @@ -42,7 +42,7 @@ mod example { use std::time::Duration; use esp_idf_svc::bt::ble::gap::{ - AdvertisingDataType, BleGapEvent, EspBleGap, GapSearchEvent, ScanParams, + AdvertisingDataType, BleGapEvent, EspBleGap, GapSearchEvent, GapSearchResult, ScanParams, }; use esp_idf_svc::bt::ble::gatt::client::{ CharacteristicElement, ConnectionId, DbAttrType, DbElement, DescriptorElement, EspGattc, @@ -187,15 +187,17 @@ mod example { self.check_bt_status(status)?; info!("Scanning started"); } - BleGapEvent::ScanResult { - search_evt, - bda, - ble_addr_type, - rssi, - ble_adv, - .. - } => { - if GapSearchEvent::InquiryResult == search_evt { + BleGapEvent::ScanResult(search_evt) => { + if matches!(search_evt, GapSearchEvent::InquiryComplete(_)) { + info!("Scan completed, no server {SERVER_NAME} found"); + } else if let GapSearchEvent::InquiryResult(GapSearchResult { + bda, + ble_addr_type, + rssi, + ble_adv, + .. + }) = search_evt + { let name = ble_adv .and_then(|ble_adv| { self.gap.resolve_adv_data_by_type( diff --git a/src/bt/ble/gap.rs b/src/bt/ble/gap.rs index bc5d29046a0..8f5ec4ab06e 100644 --- a/src/bt/ble/gap.rs +++ b/src/bt/ble/gap.rs @@ -362,17 +362,31 @@ impl From<&ScanParams> for esp_ble_scan_params_t { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] +#[derive(Copy, Clone, Debug)] #[repr(u32)] -pub enum GapSearchEvent { - InquiryResult = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_RES_EVT, - InquiryComplete = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_CMPL_EVT, - DiscoveryResult = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_RES_EVT, - DiscoveryBleResult = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_BLE_RES_EVT, - DiscoveryComplete = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_CMPL_EVT, - DiDiscoveryComplete = esp_gap_search_evt_t_ESP_GAP_SEARCH_DI_DISC_CMPL_EVT, +pub enum GapSearchEvent<'a> { + InquiryResult(GapSearchResult<'a>) = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_RES_EVT, + InquiryComplete(i32) = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_CMPL_EVT, + DiscoveryResult(GapSearchResult<'a>) = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_RES_EVT, + DiscoveryBleResult(GapSearchResult<'a>) = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_BLE_RES_EVT, + DiscoveryComplete(i32) = esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_CMPL_EVT, + DiDiscoveryComplete(i32) = esp_gap_search_evt_t_ESP_GAP_SEARCH_DI_DISC_CMPL_EVT, SearchCanceled = esp_gap_search_evt_t_ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT, - InquiryDiscardedNum = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_DISCARD_NUM_EVT, + InquiryDiscardedNum(i32) = esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_DISCARD_NUM_EVT, + UnknownEvent, +} + +#[derive(Copy, Clone, Debug)] +pub struct GapSearchResult<'a> { + pub bda: BdAddr, + pub dev_type: BtDevType, + pub ble_addr_type: BleAddrType, + pub ble_evt_type: BleEventType, + pub rssi: i32, + pub ble_adv: Option<&'a [u8]>, + pub flag: i32, + pub adv_data_len: u8, + pub scan_rsp_len: u8, } #[derive(Copy, Clone, Debug, Eq, PartialEq, TryFromPrimitive)] @@ -406,20 +420,7 @@ pub enum BleGapEvent<'a> { AdvertisingConfigured(BtStatus), ScanResponseConfigured(BtStatus), ScanParameterConfigured(BtStatus), - ScanResult { - search_evt: GapSearchEvent, - bda: BdAddr, - dev_type: BtDevType, - ble_addr_type: BleAddrType, - ble_evt_type: BleEventType, - rssi: i32, - ble_adv: Option<&'a [u8]>, - flag: i32, - num_resps: i32, - adv_data_len: u8, - scan_rsp_len: u8, - num_dis: u32, - }, + ScanResult(GapSearchEvent<'a>), RawAdvertisingConfigured(BtStatus), RawScanResponseConfigured(BtStatus), AdvertisingStarted(BtStatus), @@ -551,28 +552,66 @@ impl<'a> From<(esp_gap_ble_cb_event_t, &'a esp_ble_gap_cb_param_t)> for BleGapEv esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT => { Self::ScanParameterConfigured(param.scan_param_cmpl.status.try_into().unwrap()) } - esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_RESULT_EVT => Self::ScanResult { - search_evt: param.scan_rst.search_evt.try_into().unwrap(), - bda: param.scan_rst.bda.into(), - dev_type: param.scan_rst.dev_type.try_into().unwrap(), - ble_addr_type: param.scan_rst.ble_addr_type.try_into().unwrap(), - ble_evt_type: param.scan_rst.ble_evt_type.try_into().unwrap(), - rssi: param.scan_rst.rssi, - ble_adv: if param.scan_rst.adv_data_len + param.scan_rst.scan_rsp_len > 0 { - Some( - ¶m.scan_rst.ble_adv[..(param.scan_rst.adv_data_len - + param.scan_rst.scan_rsp_len) - as usize], - ) + esp_gap_ble_cb_event_t_ESP_GAP_BLE_SCAN_RESULT_EVT => { + let search_evt = param.scan_rst.search_evt; + + let gap_search_evt = if search_evt + == esp_gap_search_evt_t_ESP_GAP_SEARCH_SEARCH_CANCEL_CMPL_EVT + { + GapSearchEvent::SearchCanceled + } else if search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_RES_EVT + || search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_RES_EVT + || search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_BLE_RES_EVT + { + let search_result = GapSearchResult { + bda: param.scan_rst.bda.into(), + dev_type: param.scan_rst.dev_type.try_into().unwrap(), + ble_addr_type: param.scan_rst.ble_addr_type.try_into().unwrap(), + ble_evt_type: param.scan_rst.ble_evt_type.try_into().unwrap(), + rssi: param.scan_rst.rssi, + ble_adv: if param.scan_rst.adv_data_len + param.scan_rst.scan_rsp_len + > 0 + { + Some( + ¶m.scan_rst.ble_adv[..(param.scan_rst.adv_data_len + + param.scan_rst.scan_rsp_len) + as usize], + ) + } else { + None + }, + flag: param.scan_rst.flag, + adv_data_len: param.scan_rst.adv_data_len, + scan_rsp_len: param.scan_rst.scan_rsp_len, + }; + + if search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_RES_EVT { + GapSearchEvent::InquiryResult(search_result) + } else if search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_RES_EVT { + GapSearchEvent::DiscoveryResult(search_result) + } else { + GapSearchEvent::DiscoveryBleResult(search_result) + } } else { - None - }, - flag: param.scan_rst.flag, - num_resps: param.scan_rst.num_resps, - adv_data_len: param.scan_rst.adv_data_len, - scan_rsp_len: param.scan_rst.scan_rsp_len, - num_dis: param.scan_rst.num_dis, - }, + let num_resps = param.scan_rst.num_resps; + if search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_CMPL_EVT { + GapSearchEvent::InquiryComplete(num_resps) + } else if search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_DISC_CMPL_EVT { + GapSearchEvent::DiscoveryComplete(num_resps) + } else if search_evt == esp_gap_search_evt_t_ESP_GAP_SEARCH_DI_DISC_CMPL_EVT + { + GapSearchEvent::DiDiscoveryComplete(num_resps) + } else if search_evt + == esp_gap_search_evt_t_ESP_GAP_SEARCH_INQ_DISCARD_NUM_EVT + { + GapSearchEvent::InquiryDiscardedNum(num_resps) + } else { + GapSearchEvent::UnknownEvent + } + }; + + Self::ScanResult(gap_search_evt) + } esp_gap_ble_cb_event_t_ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT => { Self::RawAdvertisingConfigured( param.adv_data_raw_cmpl.status.try_into().unwrap(), From b4d3ab9f555d5f6eac6bcf3f47acf5ad42b82306 Mon Sep 17 00:00:00 2001 From: Ferdy Date: Mon, 8 Dec 2025 19:12:26 -0700 Subject: [PATCH 8/8] Fix gap scanner example to use updated scan result --- examples/bt_ble_gap_scanner.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/bt_ble_gap_scanner.rs b/examples/bt_ble_gap_scanner.rs index fa81ed9d149..a43d2cb358e 100644 --- a/examples/bt_ble_gap_scanner.rs +++ b/examples/bt_ble_gap_scanner.rs @@ -37,7 +37,7 @@ mod example { use core::hash::{Hash, Hasher}; use std::sync::{Arc, Mutex}; - use esp_idf_svc::bt::ble::gap::{BleGapEvent, EspBleGap}; + use esp_idf_svc::bt::ble::gap::{BleGapEvent, EspBleGap, GapSearchEvent, GapSearchResult}; use esp_idf_svc::bt::{BdAddr, Ble, BtDriver}; use esp_idf_svc::hal::delay::FreeRtos; use esp_idf_svc::hal::peripherals::Peripherals; @@ -135,7 +135,11 @@ mod example { fn on_gap_event(&self, event: BleGapEvent) -> Result<(), EspError> { trace!("Got event: {event:?}"); - if let BleGapEvent::ScanResult { bda, .. } = event { + if let BleGapEvent::ScanResult(GapSearchEvent::InquiryResult(GapSearchResult { + bda, + .. + })) = event + { let mut state = self.state.lock().unwrap(); let address = BluetoothAddress(bda); match state.discovered.insert(address) {