Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/corebluetooth/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ use std::{
use tokio::runtime;
use uuid::Uuid;

/// ATT Write Command PDUs reserve one byte for the opcode and two bytes for
/// the attribute handle (Bluetooth Core Specification, Vol 3, Part F, 3.4.5.3).
const ATT_WRITE_COMMAND_HEADER_LEN: usize = 3;

fn maximum_write_value_length_to_att_mtu(maximum_write_value_length: usize) -> Result<u16, String> {
maximum_write_value_length
.checked_add(ATT_WRITE_COMMAND_HEADER_LEN)
.and_then(|mtu| u16::try_from(mtu).ok())
.ok_or_else(|| {
format!(
"CoreBluetooth maximum write value length {maximum_write_value_length} cannot be represented as a u16 ATT MTU"
)
})
}

struct DescriptorInternal {
pub descriptor: Retained<CBDescriptor>,
pub uuid: Uuid,
Expand Down Expand Up @@ -159,7 +174,7 @@ pub enum CoreBluetoothReply {
ReadResult(Vec<u8>),
ReadRssi(i16),
Connected,
ServicesDiscovered(BTreeSet<Service>),
ServicesDiscovered(BTreeSet<Service>, u16),
State(CBPeripheralState),
Ok,
Err(String),
Expand Down Expand Up @@ -346,12 +361,24 @@ impl PeripheralInternal {
.collect(),
})
.collect();
// CoreBluetooth exposes the maximum characteristic value length for
// a write, not the ATT MTU. Sample it after discovery, then account
// for the ATT Write Command header to infer the full ATT MTU.
let maximum_write_value_length = unsafe {
self.peripheral.maximumWriteValueLengthForType(
CBCharacteristicWriteType::CBCharacteristicWriteWithoutResponse,
)
};
let reply = match maximum_write_value_length_to_att_mtu(maximum_write_value_length) {
Ok(mtu) => CoreBluetoothReply::ServicesDiscovered(services, mtu),
Err(error) => CoreBluetoothReply::Err(error),
};
self.services_discovered_future_state
.take()
.unwrap()
.lock()
.unwrap()
.set_reply(CoreBluetoothReply::ServicesDiscovered(services));
.set_reply(reply);
}
}

Expand Down
8 changes: 7 additions & 1 deletion src/corebluetooth/peripheral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,9 @@ impl api::Peripheral for Peripheral {
.await?;
match fut.await {
CoreBluetoothReply::Connected => {
self.shared
.mtu
.store(api::DEFAULT_MTU_SIZE, std::sync::atomic::Ordering::Relaxed);
self.shared
.emit_event(CentralEvent::DeviceConnected(self.shared.uuid.into()));
}
Expand Down Expand Up @@ -330,8 +333,11 @@ impl api::Peripheral for Peripheral {
})
.await?;
match fut.await {
CoreBluetoothReply::ServicesDiscovered(services) => {
CoreBluetoothReply::ServicesDiscovered(services, mtu) => {
*(self.shared.services.lock().map_err(Into::<Error>::into)?) = services;
self.shared
.mtu
.store(mtu, std::sync::atomic::Ordering::Relaxed);
return Ok(());
}
CoreBluetoothReply::Err(msg) => return Err(Error::RuntimeError(msg)),
Expand Down
4 changes: 2 additions & 2 deletions tests/android/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ jni_test!(
test_cases::test_descriptor_discovery
);
jni_test!(
Java_com_nonpolynomial_btleplug_test_NativeTests_testMtuAfterConnection,
test_cases::test_mtu_after_connection
Java_com_nonpolynomial_btleplug_test_NativeTests_testMtuAfterServiceDiscovery,
test_cases::test_mtu_after_service_discovery
);
jni_test!(
Java_com_nonpolynomial_btleplug_test_NativeTests_testReadRssi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class BleIntegrationTest {
@Test fun testDescriptorDiscovery() = NativeTests.testDescriptorDiscovery()

// ── Device Info ─────────────────────────────────────────────────
@Test fun testMtuAfterConnection() = NativeTests.testMtuAfterConnection()
@Test fun testMtuAfterServiceDiscovery() = NativeTests.testMtuAfterServiceDiscovery()
@Test fun testReadRssi() = NativeTests.testReadRssi()
@Test fun testPropertiesContainPeripheralInfo() = NativeTests.testPropertiesContainPeripheralInfo()
@Test fun testConnectionParameters() = NativeTests.testConnectionParameters()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ object NativeTests {
external fun testDescriptorDiscovery()

// Device Info
external fun testMtuAfterConnection()
external fun testMtuAfterServiceDiscovery()
external fun testReadRssi()
external fun testPropertiesContainPeripheralInfo()
external fun testConnectionParameters()
Expand Down
11 changes: 10 additions & 1 deletion tests/common/test_cases.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,9 +581,18 @@ pub async fn test_descriptor_discovery() {

// ── Device Info ─────────────────────────────────────────────────────

pub async fn test_mtu_after_connection() {
pub async fn test_mtu_after_service_discovery() {
let peripheral = peripheral_finder::find_and_connect().await;
let mtu = peripheral.mtu();

#[cfg(any(target_os = "macos", target_os = "ios"))]
assert!(
mtu > 23,
"CoreBluetooth MTU should be negotiated above the default, got {}",
mtu
);

#[cfg(not(any(target_os = "macos", target_os = "ios")))]
assert!(
mtu >= 23,
"MTU should be at least 23 (default), got {}",
Expand Down
7 changes: 0 additions & 7 deletions tests/test_mtu_after_connection.rs

This file was deleted.

7 changes: 7 additions & 0 deletions tests/test_mtu_after_service_discovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod common;

#[tokio::test]
#[ignore = "requires BLE test peripheral"]
async fn test_mtu_after_service_discovery() {
common::test_cases::test_mtu_after_service_discovery().await;
}
Loading