diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index c0f42aa6..2568654e 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -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 { + 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, pub uuid: Uuid, @@ -159,7 +174,7 @@ pub enum CoreBluetoothReply { ReadResult(Vec), ReadRssi(i16), Connected, - ServicesDiscovered(BTreeSet), + ServicesDiscovered(BTreeSet, u16), State(CBPeripheralState), Ok, Err(String), @@ -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); } } diff --git a/src/corebluetooth/peripheral.rs b/src/corebluetooth/peripheral.rs index 229daaa6..5936bf76 100644 --- a/src/corebluetooth/peripheral.rs +++ b/src/corebluetooth/peripheral.rs @@ -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())); } @@ -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::::into)?) = services; + self.shared + .mtu + .store(mtu, std::sync::atomic::Ordering::Relaxed); return Ok(()); } CoreBluetoothReply::Err(msg) => return Err(Error::RuntimeError(msg)), diff --git a/tests/android/rust/src/lib.rs b/tests/android/rust/src/lib.rs index f56d58aa..cb2c6649 100644 --- a/tests/android/rust/src/lib.rs +++ b/tests/android/rust/src/lib.rs @@ -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, diff --git a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt index ccb5dd7f..8967b95d 100644 --- a/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt +++ b/tests/android/src/androidTest/kotlin/com/nonpolynomial/btleplug/test/BleIntegrationTest.kt @@ -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() diff --git a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt index ab43ee4e..466b32d8 100644 --- a/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt +++ b/tests/android/src/main/kotlin/com/nonpolynomial/btleplug/test/NativeTests.kt @@ -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() diff --git a/tests/common/test_cases.rs b/tests/common/test_cases.rs index 5915f5e4..22e2c6a8 100644 --- a/tests/common/test_cases.rs +++ b/tests/common/test_cases.rs @@ -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 {}", diff --git a/tests/test_mtu_after_connection.rs b/tests/test_mtu_after_connection.rs deleted file mode 100644 index 50f549b1..00000000 --- a/tests/test_mtu_after_connection.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod common; - -#[tokio::test] -#[ignore = "requires BLE test peripheral"] -async fn test_mtu_after_connection() { - common::test_cases::test_mtu_after_connection().await; -} diff --git a/tests/test_mtu_after_service_discovery.rs b/tests/test_mtu_after_service_discovery.rs new file mode 100644 index 00000000..603ea1eb --- /dev/null +++ b/tests/test_mtu_after_service_discovery.rs @@ -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; +}