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
10 changes: 9 additions & 1 deletion src/common/adapter_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@ where
}

pub fn clear_peripherals(&self) {
self.peripherals.clear();
// Retain peripherals that are currently connected.
// This ensures that stopping a scan or clearing discovered devices
// does not evict active connections — matching CoreBluetooth/BlueZ behavior.
self.peripherals.retain(|_id, peripheral| {
// is_connected() is async in the trait, but all platform implementations
// use an AtomicBool internally, so block_on is safe here.
// We use a short-lived runtime to avoid panics if already in an async context.
futures::executor::block_on(peripheral.is_connected()).unwrap_or(false)
});
}

pub fn peripherals(&self) -> Vec<PeripheralType> {
Expand Down
8 changes: 5 additions & 3 deletions src/corebluetooth/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -639,9 +639,11 @@ impl CoreBluetoothInternal {
let id = unsafe { peripheral.identifier() };
let uuid = nsuuid_to_uuid(&id);
let peripheral_name = unsafe { peripheral.name() };
let local_name = peripheral_name
.map(|n| n.to_string())
.or(advertisement_name.clone());
// Prefer advertisement_name (from scan response, usually COMPLETE_LOCAL_NAME)
// over peripheral.name() (GAP cache, often the truncated SHORT_LOCAL_NAME)
let local_name = advertisement_name
.clone()
.or_else(|| peripheral_name.map(|n| n.to_string()));

if self.peripherals.contains_key(&uuid) {
if local_name.is_some() || advertisement_name.is_some() {
Expand Down
12 changes: 11 additions & 1 deletion src/corebluetooth/peripheral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,17 @@ impl Peripheral {
advertisement_name: Option<String>,
) {
if let Ok(mut props) = self.shared.properties.lock() {
props.local_name = local_name;
// Only update local_name if new value is present and is at least as informative
// (i.e. don't let a shorter GAP name overwrite a longer advertisement name)
if let Some(ref new_name) = local_name {
let should_update = match &props.local_name {
None => true,
Some(old) => new_name.len() >= old.len(),
};
if should_update {
props.local_name = local_name;
}
}
props.advertisement_name = advertisement_name;
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/droidplug/peripheral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ impl api::Peripheral for Peripheral {
Ok(())
})?;
// Auto-negotiate maximum MTU (517) after connection
// NOTE(Custom): Upstream currently auto-negotiates MTU to max 517 upon connection.
// If specific business logic requires a manual or custom MTU setup in the future,
// we should re-expose `request_mtu(mtu: usize)` in the `api::Peripheral` trait and map it to `obj.request_mtu` here.
let mtu_future = self.with_obj(|env, obj| {
JSendFuture::try_from(JFuture::from_env(env, obj.request_mtu(517)?)?)
})?;
Expand Down
23 changes: 17 additions & 6 deletions src/winrtble/ble/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ impl BLEWatcher {

// Pre-convert the filter UUIDs once so the handler closure is cheap.
let filter_guids: Vec<windows::core::GUID> = services.iter().map(utils::to_guid).collect();

let matching_devices = std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashSet::new()));

let handler: TypedEventHandler<
BluetoothLEAdvertisementWatcher,
Expand All @@ -60,18 +62,27 @@ impl BLEWatcher {
if let Ok(args) = args.ok() {
// Software service-UUID filter.
if !filter_guids.is_empty() {
let address = args.BluetoothAddress().unwrap_or(0);
let mut is_match = false;

if let Ok(ad) = args.Advertisement() {
if let Ok(ad_uuids) = ad.ServiceUuids() {
let count = ad_uuids.Size().unwrap_or(0);
let advertised: Vec<windows::core::GUID> =
(0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect();
let all_present =
filter_guids.iter().all(|g| advertised.contains(g));
if !all_present {
return Ok(());
if count > 0 {
let advertised: Vec<windows::core::GUID> =
(0..count).filter_map(|i| ad_uuids.GetAt(i).ok()).collect();
is_match = filter_guids.iter().all(|g| advertised.contains(g));
}
}
}

let mut cache = matching_devices.lock().unwrap();
if is_match {
cache.insert(address);
} else if !cache.contains(&address) {
// If the current packet doesn't have the UUID and we haven't seen it before, drop it.
return Ok(());
}
}
on_received(args)?;
}
Expand Down
2 changes: 2 additions & 0 deletions src/winrtble/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ mod advertisement_data_type {
pub const SERVICE_DATA_16_BIT_UUID: u8 = 0x16;
pub const SERVICE_DATA_32_BIT_UUID: u8 = 0x20;
pub const SERVICE_DATA_128_BIT_UUID: u8 = 0x21;
pub const SHORT_LOCAL_NAME: u8 = 0x08;
pub const COMPLETE_LOCAL_NAME: u8 = 0x09;
}
41 changes: 40 additions & 1 deletion src/winrtble/peripheral.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,56 @@ impl Peripheral {
if let Ok(data_sections) = advertisement.DataSections() {
// See if we have any advertised service data before taking a lock to update...
let mut found_service_data = false;
let mut manual_local_name: Option<String> = None;
let mut has_complete_name = false;
for section in &data_sections {
match section.DataType().unwrap() {
advertisement_data_type::SERVICE_DATA_16_BIT_UUID
| advertisement_data_type::SERVICE_DATA_32_BIT_UUID
| advertisement_data_type::SERVICE_DATA_128_BIT_UUID => {
found_service_data = true;
break;
}
advertisement_data_type::COMPLETE_LOCAL_NAME => {
let data = utils::to_vec(&section.Data().unwrap());
if let Ok(name) = String::from_utf8(data) {
let name = name.trim_end_matches('\0').trim().to_string();
if !name.is_empty() {
manual_local_name = Some(name);
has_complete_name = true;
}
}
}
advertisement_data_type::SHORT_LOCAL_NAME => {
// Only use SHORT_LOCAL_NAME if we haven't already found a COMPLETE_LOCAL_NAME
if !has_complete_name {
let data = utils::to_vec(&section.Data().unwrap());
if let Ok(name) = String::from_utf8(data) {
let name = name.trim_end_matches('\0').trim().to_string();
if !name.is_empty() {
manual_local_name = Some(name);
}
}
}
}
_ => {}
}
}

if let Some(name) = manual_local_name {
if !name.is_empty() {
let existing = self.shared.local_name.read().unwrap().clone();
// Only update if: (1) no existing name, or (2) this is a COMPLETE_LOCAL_NAME,
// or (3) new name is longer (prevents SHORT from overwriting COMPLETE across packets)
let should_update = match &existing {
None => true,
Some(old) => has_complete_name || name.len() > old.len(),
};
if should_update {
*self.shared.local_name.write().unwrap() = Some(name.clone());
self.emit_event(CentralEvent::DeviceUpdated(self.shared.address.into()));
}
}
}
if found_service_data {
let mut service_data_guard = self.shared.latest_service_data.write().unwrap();

Expand Down
Loading