From f35cadd500caa1468402564483d6c879b513355f Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Mon, 7 Jul 2025 18:04:49 +0200 Subject: [PATCH 1/2] fix: corebluetooth canSendWriteWithoutResponse --- src/corebluetooth/internal.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 93597bc7..38824155 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -38,6 +38,7 @@ use std::{ fmt::{self, Debug, Formatter}, ops::Deref, thread, + time::Duration, }; use tokio::runtime; use uuid::Uuid; @@ -887,6 +888,19 @@ impl CoreBluetoothInternal { { trace!("Writing value! With kind {:?}", kind); unsafe { + if kind == WriteType::WithoutResponse { + // probably better idea would be to wait for the result of peripheral.peripheralIsReadyToSendWriteWithoutResponse + let mut attempts = 0; + while !peripheral.peripheral.canSendWriteWithoutResponse() + && attempts < 100 + { + attempts += 1; + // min. connection interval time is 15ms. see the document: + // https://developer.apple.com/library/archive/qa/qa1931/_index.html + thread::sleep(Duration::from_millis(15)); + } + } + peripheral.peripheral.writeValue_forCharacteristic_type( &NSData::from_vec(data), &characteristic.characteristic, From 3e6056fbe470953db6c662681c718a9032438630 Mon Sep 17 00:00:00 2001 From: Szymon Lesisz Date: Thu, 6 Nov 2025 12:43:45 +0100 Subject: [PATCH 2/2] Add CoreBluetoothFeatures --- Cargo.toml | 1 + src/corebluetooth/internal.rs | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed77ba06..964d10e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ objc2-foundation = { version = "0.2.2", default-features = false, features = [ "NSString", "NSUUID", "NSValue", + "NSProcessInfo", ] } objc2-core-bluetooth = { version = "0.2.2", default-features = false, features = [ "std", diff --git a/src/corebluetooth/internal.rs b/src/corebluetooth/internal.rs index 38824155..ac2550bc 100644 --- a/src/corebluetooth/internal.rs +++ b/src/corebluetooth/internal.rs @@ -31,7 +31,7 @@ use objc2_core_bluetooth::{ CBCharacteristicProperties, CBCharacteristicWriteType, CBDescriptor, CBManager, CBManagerAuthorization, CBManagerState, CBPeripheral, CBPeripheralState, CBService, CBUUID, }; -use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber}; +use objc2_foundation::{NSArray, NSData, NSMutableDictionary, NSNumber, NSProcessInfo}; use std::{ collections::{BTreeSet, HashMap, VecDeque}, ffi::CString, @@ -362,6 +362,12 @@ impl PeripheralInternal { } } +/// Optional CoreBluetooth API capabilities that are available depending on the macOS version. +struct CoreBluetoothFeatures { + /// `peripheral.canSendWriteWithoutResponse` property is available (since macOS 11.2.0) + can_send_write_without_response: bool, +} + // All of CoreBluetooth is basically async. It's all just waiting on delegate // events/callbacks. Therefore, we should be able to round up all of our wacky // ass mut *Object values, keep them in a single struct, in a single thread, and @@ -376,6 +382,7 @@ struct CoreBluetoothInternal { // task::block this when sending even though it'll never actually block. event_sender: Sender, message_receiver: Fuse>, + features: CoreBluetoothFeatures, } impl Debug for CoreBluetoothInternal { @@ -474,6 +481,16 @@ pub enum CoreBluetoothEvent { }, } +fn get_features() -> CoreBluetoothFeatures { + let process_info = NSProcessInfo::processInfo(); + let version = process_info.operatingSystemVersion(); + let current = (version.majorVersion, version.minorVersion); + + CoreBluetoothFeatures { + can_send_write_without_response: current >= (11, 2), + } +} + impl CoreBluetoothInternal { pub fn new( message_receiver: Receiver, @@ -499,6 +516,7 @@ impl CoreBluetoothInternal { event_sender, message_receiver: message_receiver.fuse(), delegate, + features: get_features(), } } @@ -888,7 +906,9 @@ impl CoreBluetoothInternal { { trace!("Writing value! With kind {:?}", kind); unsafe { - if kind == WriteType::WithoutResponse { + if kind == WriteType::WithoutResponse + && self.features.can_send_write_without_response + { // probably better idea would be to wait for the result of peripheral.peripheralIsReadyToSendWriteWithoutResponse let mut attempts = 0; while !peripheral.peripheral.canSendWriteWithoutResponse()