Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Group characteristics by service #202

Merged
merged 9 commits into from
Sep 2, 2021
Merged
18 changes: 12 additions & 6 deletions examples/discover_adapters_peripherals.rs
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
.start_scan()
.await
.expect("Can't scan BLE adapter for connected devices...");
time::sleep(Duration::from_secs(2)).await;
time::sleep(Duration::from_secs(10)).await;
let peripherals = adapter.peripherals().await?;
if peripherals.is_empty() {
eprintln!("->>> BLE peripheral devices were not found, sorry. Exiting...");
@@ -53,12 +53,18 @@ async fn main() -> Result<(), Box<dyn Error>> {
"Now connected ({:?}) to peripheral {:?}...",
is_connected, &local_name
);
let chars = peripheral.discover_characteristics().await?;
if is_connected {
println!("Discover peripheral {:?} characteristics...", &local_name);
for characteristic in chars.into_iter() {
println!("{:?}", characteristic);
peripheral.discover_services().await?;
println!("Discover peripheral {:?} services...", &local_name);
for service in peripheral.services() {
println!(
"Service UUID {}, primary: {}",
service.uuid, service.primary
);
for characteristic in service.characteristics {
println!(" {:?}", characteristic);
}
}
if is_connected {
println!("Disconnecting from peripheral {:?}...", &local_name);
peripheral
.disconnect()
4 changes: 2 additions & 2 deletions examples/lights.rs
Original file line number Diff line number Diff line change
@@ -54,8 +54,8 @@ async fn main() -> Result<(), Box<dyn Error>> {
// connect to the device
light.connect().await?;

// discover characteristics
light.discover_characteristics().await?;
// discover services and characteristics
light.discover_services().await?;

// find the characteristic we want
let chars = light.characteristics();
6 changes: 3 additions & 3 deletions examples/subscribe_notify_characteristic.rs
Original file line number Diff line number Diff line change
@@ -62,10 +62,10 @@ async fn main() -> Result<(), Box<dyn Error>> {
"Now connected ({:?}) to peripheral {:?}.",
is_connected, &local_name
);
let chars = peripheral.discover_characteristics().await?;
println!("Discover peripheral {:?} services...", local_name);
peripheral.discover_services().await?;
if is_connected {
println!("Discover peripheral {:?} characteristics...", local_name);
for characteristic in chars.into_iter() {
for characteristic in peripheral.characteristics() {
println!("Checking characteristic {:?}", characteristic);
// Subscribe to notifications from the characteristic with the selected
// UUID.
31 changes: 27 additions & 4 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -112,6 +112,18 @@ impl Default for CharPropFlags {
}
}

/// A GATT service. Services are groups of characteristics, which may be standard or
/// device-specific.
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)]
pub struct Service {
/// The UUID for this service.
pub uuid: Uuid,
/// Whether this is a primary service.
pub primary: bool,
/// The characteristics of this service.
pub characteristics: BTreeSet<Characteristic>,
}

/// A Bluetooth characteristic. Characteristics are the main way you will interact with other
/// bluetooth devices. Characteristics are identified by a UUID which may be standardized
/// (like 0x2803, which identifies a characteristic for reading heart rate measurements) but more
@@ -124,6 +136,8 @@ impl Default for CharPropFlags {
pub struct Characteristic {
/// The UUID for this characteristic. This uniquely identifies its behavior.
pub uuid: Uuid,
/// The UUID of the service this characteristic belongs to.
pub service_uuid: Uuid,
/// The set of properties for this characteristic, which indicate what functionality it
/// supports. If you attempt an operation that is not supported by the characteristics (for
/// example setting notify on one without the NOTIFY flag), that operation will fail.
@@ -186,9 +200,18 @@ pub trait Peripheral: Send + Sync + Clone + Debug {
/// as additional advertising reports are received.
async fn properties(&self) -> Result<Option<PeripheralProperties>>;

/// The set of services we've discovered for this device. This will be empty until
/// `discover_services` is called.
fn services(&self) -> BTreeSet<Service>;

/// The set of characteristics we've discovered for this device. This will be empty until
/// `discover_characteristics` is called.
fn characteristics(&self) -> BTreeSet<Characteristic>;
/// `discover_services` is called.
fn characteristics(&self) -> BTreeSet<Characteristic> {
self.services()
.iter()
.flat_map(|service| service.characteristics.clone().into_iter())
.collect()
}

/// Returns true iff we are currently connected to the device.
async fn is_connected(&self) -> Result<bool>;
@@ -201,8 +224,8 @@ pub trait Peripheral: Send + Sync + Clone + Debug {
/// Terminates a connection to the device.
async fn disconnect(&self) -> Result<()>;

/// Discovers all characteristics for the device.
async fn discover_characteristics(&self) -> Result<Vec<Characteristic>>;
/// Discovers all services for the device, including their characteristics.
async fn discover_services(&self) -> Result<()>;

/// Write some data to the characteristic. Returns an error if the write couldn't be sent or (in
/// the case of a write-with-response) if the device returns an error.
122 changes: 86 additions & 36 deletions src/bluez/peripheral.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
use async_trait::async_trait;
use bluez_async::{
BluetoothEvent, BluetoothSession, CharacteristicEvent, CharacteristicFlags, CharacteristicInfo,
DeviceId, DeviceInfo, MacAddress, WriteOptions,
BluetoothEvent, BluetoothSession, CharacteristicEvent, CharacteristicFlags, CharacteristicId,
CharacteristicInfo, DeviceId, DeviceInfo, MacAddress, ServiceInfo, WriteOptions,
};
use futures::future::ready;
use futures::stream::{Stream, StreamExt};
use std::collections::BTreeSet;
use std::collections::{BTreeSet, HashMap};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

use crate::api::{
self, AddressType, BDAddr, CharPropFlags, Characteristic, PeripheralProperties,
self, AddressType, BDAddr, CharPropFlags, Characteristic, PeripheralProperties, Service,
ValueNotification, WriteType,
};
use crate::{Error, Result};

#[derive(Clone, Debug)]
struct ServiceInternal {
info: ServiceInfo,
characteristics: HashMap<Uuid, CharacteristicInfo>,
}

/// Implementation of [api::Peripheral](crate::api::Peripheral).
#[derive(Clone, Debug)]
pub struct Peripheral {
session: BluetoothSession,
device: DeviceId,
mac_address: BDAddr,
characteristics: Arc<Mutex<Vec<CharacteristicInfo>>>,
services: Arc<Mutex<HashMap<Uuid, ServiceInternal>>>,
}

impl Peripheral {
@@ -30,15 +37,25 @@ impl Peripheral {
session,
device: device.id,
mac_address: (&device.mac_address).into(),
characteristics: Arc::new(Mutex::new(vec![])),
services: Arc::new(Mutex::new(HashMap::new())),
}
}

fn characteristic_info(&self, characteristic: &Characteristic) -> Result<CharacteristicInfo> {
let characteristics = self.characteristics.lock().unwrap();
characteristics
.iter()
.find(|info| info.uuid == characteristic.uuid)
let services = self.services.lock().unwrap();
services
.get(&characteristic.service_uuid)
.ok_or_else(|| {
Error::Other(
format!(
"Service with UUID {} not found.",
characteristic.service_uuid
)
.into(),
)
})?
.characteristics
.get(&characteristic.uuid)
.cloned()
.ok_or_else(|| {
Error::Other(
@@ -76,9 +93,13 @@ impl api::Peripheral for Peripheral {
}))
}

fn characteristics(&self) -> BTreeSet<Characteristic> {
let characteristics = &*self.characteristics.lock().unwrap();
characteristics.iter().map(Characteristic::from).collect()
fn services(&self) -> BTreeSet<Service> {
self.services
.lock()
.unwrap()
.values()
.map(|service| service.into())
.collect()
}

async fn is_connected(&self) -> Result<bool> {
@@ -96,15 +117,24 @@ impl api::Peripheral for Peripheral {
Ok(())
}

async fn discover_characteristics(&self) -> Result<Vec<Characteristic>> {
let mut characteristics = vec![];
async fn discover_services(&self) -> Result<()> {
let mut services_internal = HashMap::new();
let services = self.session.get_services(&self.device).await?;
for service in services {
characteristics.extend(self.session.get_characteristics(&service.id).await?);
let characteristics = self.session.get_characteristics(&service.id).await?;
services_internal.insert(
service.uuid,
ServiceInternal {
info: service,
characteristics: characteristics
.into_iter()
.map(|characteristic| (characteristic.uuid, characteristic))
.collect(),
},
);
}
let converted = characteristics.iter().map(Characteristic::from).collect();
*self.characteristics.lock().unwrap() = characteristics;
Ok(converted)
*self.services.lock().unwrap() = services_internal;
Ok(())
}

async fn write(
@@ -145,38 +175,45 @@ impl api::Peripheral for Peripheral {
async fn notifications(&self) -> Result<Pin<Box<dyn Stream<Item = ValueNotification> + Send>>> {
let device_id = self.device.clone();
let events = self.session.device_event_stream(&device_id).await?;
let characteristics = self.characteristics.clone();
let services = self.services.clone();
Ok(Box::pin(events.filter_map(move |event| {
ready(value_notification(
event,
&device_id,
characteristics.clone(),
))
ready(value_notification(event, &device_id, services.clone()))
})))
}
}

fn value_notification(
event: BluetoothEvent,
device_id: &DeviceId,
characteristics: Arc<Mutex<Vec<CharacteristicInfo>>>,
services: Arc<Mutex<HashMap<Uuid, ServiceInternal>>>,
) -> Option<ValueNotification> {
match event {
BluetoothEvent::Characteristic {
id,
event: CharacteristicEvent::Value { value },
} if id.service().device() == *device_id => {
let characteristics = characteristics.lock().unwrap();
let uuid = characteristics
.iter()
.find(|characteristic| characteristic.id == id)?
.uuid;
let services = services.lock().unwrap();
let uuid = find_characteristic_by_id(&services, id)?.uuid;
Some(ValueNotification { uuid, value })
}
_ => None,
}
}

fn find_characteristic_by_id(
services: &HashMap<Uuid, ServiceInternal>,
characteristic_id: CharacteristicId,
) -> Option<&CharacteristicInfo> {
for service in services.values() {
for characteristic in service.characteristics.values() {
if characteristic.id == characteristic_id {
return Some(characteristic);
}
}
}
None
}

impl From<WriteType> for bluez_async::WriteType {
fn from(write_type: WriteType) -> Self {
match write_type {
@@ -201,11 +238,24 @@ impl From<bluez_async::AddressType> for AddressType {
}
}

impl From<&CharacteristicInfo> for Characteristic {
fn from(characteristic: &CharacteristicInfo) -> Self {
Characteristic {
uuid: characteristic.uuid,
properties: characteristic.flags.into(),
fn make_characteristic(info: &CharacteristicInfo, service_uuid: Uuid) -> Characteristic {
Characteristic {
uuid: info.uuid,
properties: info.flags.into(),
service_uuid,
}
}

impl From<&ServiceInternal> for Service {
fn from(service: &ServiceInternal) -> Self {
Service {
uuid: service.info.uuid,
primary: service.info.primary,
characteristics: service
.characteristics
.iter()
.map(|(_, characteristic)| make_characteristic(characteristic, service.info.uuid))
.collect(),
}
}
}
Loading