pub(crate) mod bdaddr;
pub mod bleuuid;
use crate::Result;
use async_trait::async_trait;
use bitflags::bitflags;
use futures::stream::Stream;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "serde")]
use serde_cr as serde;
use std::{
collections::{BTreeSet, HashMap, HashSet},
fmt::{self, Debug, Display, Formatter},
hash::Hash,
pin::Pin,
time::Duration,
};
use uuid::Uuid;
pub use self::bdaddr::{BDAddr, ParseBDAddrError};
use crate::platform::PeripheralId;
pub const DEFAULT_MTU_SIZE: u16 = 23;
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_cr")
)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
pub enum AddressType {
Random,
#[default]
Public,
}
impl AddressType {
#[allow(clippy::should_implement_trait)]
pub fn from_str(v: &str) -> Option<AddressType> {
match v {
"public" => Some(AddressType::Public),
"random" => Some(AddressType::Random),
_ => None,
}
}
pub fn from_u8(v: u8) -> Option<AddressType> {
match v {
1 => Some(AddressType::Public),
2 => Some(AddressType::Random),
_ => None,
}
}
pub fn num(&self) -> u8 {
match *self {
AddressType::Public => 1,
AddressType::Random => 2,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ValueNotification {
pub uuid: Uuid,
pub service_uuid: Uuid,
pub value: Vec<u8>,
}
bitflags! {
#[derive(Default, Debug, PartialEq, Eq, Ord, PartialOrd, Clone, Copy)]
pub struct CharPropFlags: u8 {
const BROADCAST = 0x01;
const READ = 0x02;
const WRITE_WITHOUT_RESPONSE = 0x04;
const WRITE = 0x08;
const NOTIFY = 0x10;
const INDICATE = 0x20;
const AUTHENTICATED_SIGNED_WRITES = 0x40;
const EXTENDED_PROPERTIES = 0x80;
}
}
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)]
pub struct Service {
pub uuid: Uuid,
pub primary: bool,
pub characteristics: BTreeSet<Characteristic>,
}
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)]
pub struct Characteristic {
pub uuid: Uuid,
pub service_uuid: Uuid,
pub properties: CharPropFlags,
pub descriptors: BTreeSet<Descriptor>,
}
impl Display for Characteristic {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"uuid: {:?}, char properties: {:?}",
self.uuid, self.properties
)
}
}
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)]
pub struct Descriptor {
pub uuid: Uuid,
pub service_uuid: Uuid,
pub characteristic_uuid: Uuid,
}
impl Display for Descriptor {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "uuid: {:?}", self.uuid)
}
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_cr")
)]
#[derive(Debug, Default, Clone)]
pub struct PeripheralProperties {
pub address: BDAddr,
pub address_type: Option<AddressType>,
pub local_name: Option<String>,
pub advertisement_name: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
pub appearance: Option<u16>,
pub tx_power_level: Option<i16>,
pub rssi: Option<i16>,
pub manufacturer_data: HashMap<u16, Vec<u8>>,
pub service_data: HashMap<Uuid, Vec<u8>>,
pub services: Vec<Uuid>,
pub class: Option<u32>,
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_cr")
)]
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct ScanFilter {
pub services: Vec<Uuid>,
}
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct RetrievePeripheralsOptions {
pub identifiers: Option<Vec<PeripheralId>>,
pub services: Option<Vec<Uuid>>,
}
#[allow(dead_code)] pub(crate) fn matches_identifier<T: Eq>(candidate: &T, requested: &[T]) -> bool {
requested.iter().any(|requested| requested == candidate)
}
#[allow(dead_code)] pub(crate) fn matches_service(candidate_services: &[Uuid], requested: &[Uuid]) -> bool {
requested
.iter()
.any(|requested| candidate_services.contains(requested))
}
#[allow(dead_code)] pub(crate) fn matches_retrieval_selectors(
candidate_id: &PeripheralId,
candidate_services: &[Uuid],
options: &RetrievePeripheralsOptions,
) -> bool {
let id_match = options
.identifiers
.as_deref()
.is_some_and(|requested| matches_identifier(candidate_id, requested));
let service_match = options
.services
.as_deref()
.is_some_and(|requested| matches_service(candidate_services, requested));
if options.identifiers.is_none() && options.services.is_none() {
true
} else {
id_match || service_match
}
}
#[allow(dead_code)] pub(crate) fn merge_retrieved_peripherals<P, K, F>(
peripherals: impl IntoIterator<Item = P>,
id: F,
) -> Vec<P>
where
K: Eq + Hash,
F: Fn(&P) -> K,
{
let mut seen = HashSet::new();
peripherals
.into_iter()
.filter(|peripheral| seen.insert(id(peripheral)))
.collect()
}
#[cfg(test)]
fn unsupported_retrieve_peripherals<P>() -> Result<Vec<P>> {
Err(crate::Error::NotSupported(
"retrieve_peripherals".to_string(),
))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ConnectionParameters {
pub interval_us: u32,
pub latency: u16,
pub supervision_timeout_us: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionParameterPreset {
Balanced,
ThroughputOptimized,
PowerOptimized,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WriteType {
WithResponse,
WithoutResponse,
}
#[async_trait]
pub trait Peripheral: Send + Sync + Clone + Debug {
fn id(&self) -> PeripheralId;
fn address(&self) -> BDAddr;
fn mtu(&self) -> u16;
async fn properties(&self) -> Result<Option<PeripheralProperties>>;
fn services(&self) -> BTreeSet<Service>;
fn characteristics(&self) -> BTreeSet<Characteristic> {
self.services()
.iter()
.flat_map(|service| service.characteristics.clone().into_iter())
.collect()
}
async fn is_connected(&self) -> Result<bool>;
async fn connect(&self) -> Result<()>;
async fn connect_with_timeout(&self, timeout: Duration) -> Result<()> {
tokio::time::timeout(timeout, self.connect())
.await
.map_err(|_| crate::Error::TimedOut(timeout))?
}
async fn disconnect(&self) -> Result<()>;
async fn discover_services(&self) -> Result<()>;
async fn discover_services_with_timeout(&self, timeout: Duration) -> Result<()> {
tokio::time::timeout(timeout, self.discover_services())
.await
.map_err(|_| crate::Error::TimedOut(timeout))?
}
async fn write(
&self,
characteristic: &Characteristic,
data: &[u8],
write_type: WriteType,
) -> Result<()>;
async fn read(&self, characteristic: &Characteristic) -> Result<Vec<u8>>;
async fn subscribe(&self, characteristic: &Characteristic) -> Result<()>;
async fn unsubscribe(&self, characteristic: &Characteristic) -> Result<()>;
async fn notifications(&self) -> Result<Pin<Box<dyn Stream<Item = ValueNotification> + Send>>>;
async fn write_descriptor(&self, descriptor: &Descriptor, data: &[u8]) -> Result<()>;
async fn read_descriptor(&self, descriptor: &Descriptor) -> Result<Vec<u8>>;
async fn connection_parameters(&self) -> Result<Option<ConnectionParameters>> {
Err(crate::Error::NotSupported(
"connection_parameters".to_string(),
))
}
async fn request_connection_parameters(
&self,
_preset: ConnectionParameterPreset,
) -> Result<()> {
Err(crate::Error::NotSupported(
"request_connection_parameters".to_string(),
))
}
async fn read_rssi(&self) -> Result<i16> {
Err(crate::Error::NotSupported("read_rssi".to_string()))
}
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_cr")
)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CentralState {
Unknown = 0,
PoweredOn = 1,
PoweredOff = 2,
}
#[cfg_attr(
feature = "serde",
derive(Serialize, Deserialize),
serde(crate = "serde_cr")
)]
#[derive(Debug, Clone)]
pub enum CentralEvent {
DeviceDiscovered(PeripheralId),
DeviceUpdated(PeripheralId),
DeviceConnected(PeripheralId),
DeviceDisconnected(PeripheralId),
DeviceServicesModified(PeripheralId),
ManufacturerDataAdvertisement {
id: PeripheralId,
manufacturer_data: HashMap<u16, Vec<u8>>,
},
ServiceDataAdvertisement {
id: PeripheralId,
service_data: HashMap<Uuid, Vec<u8>>,
},
ServicesAdvertisement {
id: PeripheralId,
services: Vec<Uuid>,
},
RssiUpdate {
id: PeripheralId,
rssi: i16,
},
StateUpdate(CentralState),
}
#[async_trait]
pub trait Central: Send + Sync + Clone {
type Peripheral: Peripheral;
async fn events(&self) -> Result<Pin<Box<dyn Stream<Item = CentralEvent> + Send>>>;
async fn start_scan(&self, filter: ScanFilter) -> Result<()>;
async fn stop_scan(&self) -> Result<()>;
async fn peripherals(&self) -> Result<Vec<Self::Peripheral>>;
async fn retrieve_peripherals(
&self,
_options: RetrievePeripheralsOptions,
) -> Result<Vec<Self::Peripheral>> {
Err(crate::Error::NotSupported(
"retrieve_peripherals".to_string(),
))
}
async fn peripheral(&self, id: &PeripheralId) -> Result<Self::Peripheral>;
async fn add_peripheral(&self, address: &PeripheralId) -> Result<Self::Peripheral>;
async fn clear_peripherals(&self) -> Result<()>;
async fn adapter_info(&self) -> Result<String>;
async fn adapter_address(&self) -> Result<Option<BDAddr>> {
Ok(None)
}
async fn adapter_state(&self) -> Result<CentralState>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retrieve_options_are_publicly_constructible() {
let options = RetrievePeripheralsOptions {
identifiers: Some(Vec::new()),
services: Some(vec![Uuid::nil()]),
};
assert_eq!(options.services, Some(vec![Uuid::nil()]));
assert_eq!(options.identifiers, Some(Vec::new()));
}
#[test]
fn retrieve_options_default_is_explicit() {
assert_eq!(RetrievePeripheralsOptions::default().identifiers, None);
assert_eq!(RetrievePeripheralsOptions::default().services, None);
}
#[test]
fn retrieve_empty_identifier_selector_matches_nothing() {
assert!(!matches_identifier(&1_u8, &[]));
}
#[test]
fn retrieve_empty_service_selector_matches_nothing() {
assert!(!matches_service(&[Uuid::nil()], &[]));
}
#[test]
fn retrieve_selector_matching_uses_any_value() {
assert!(matches_identifier(&2_u8, &[1, 2, 3]));
assert!(matches_service(
&[Uuid::nil()],
&[Uuid::from_u128(1), Uuid::nil()],
));
}
#[test]
fn retrieve_unknown_identifiers_are_omitted() {
let requested = [1_u8, 2_u8];
assert!(!matches_identifier(&3, &requested));
}
#[test]
fn retrieve_order_is_preserved() {
let merged = merge_retrieved_peripherals([3_u8, 1, 2], |value| *value);
assert_eq!(merged, vec![3, 1, 2]);
}
#[test]
fn retrieve_combined_selectors_use_union() {
assert!(matches_service(&[Uuid::nil()], &[Uuid::nil()]));
assert!(!matches_service(&[], &[Uuid::nil()]));
}
#[test]
fn retrieve_results_are_deduplicated() {
let merged = merge_retrieved_peripherals([1_u8, 2, 1, 3, 2], |value| *value);
assert_eq!(merged, vec![1, 2, 3]);
}
#[test]
fn retrieve_peripherals_default_is_not_supported() {
let error = unsupported_retrieve_peripherals::<u8>().unwrap_err();
assert!(matches!(
error,
crate::Error::NotSupported(operation) if operation == "retrieve_peripherals"
));
}
}
#[async_trait]
pub trait Manager {
type Adapter: Central;
async fn adapters(&self) -> Result<Vec<Self::Adapter>>;
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use super::PeripheralProperties;
#[test]
fn peripheral_properties_round_trip_appearance() {
let properties = PeripheralProperties {
appearance: Some(0x0340),
..PeripheralProperties::default()
};
let value = serde_json::to_value(&properties).unwrap();
assert_eq!(value["appearance"], 0x0340);
let decoded: PeripheralProperties = serde_json::from_value(value).unwrap();
assert_eq!(decoded.appearance, Some(0x0340));
}
#[test]
fn peripheral_properties_missing_appearance_defaults_to_none() {
let mut value = serde_json::to_value(PeripheralProperties::default()).unwrap();
value.as_object_mut().unwrap().remove("appearance").unwrap();
let properties: PeripheralProperties = serde_json::from_value(value).unwrap();
assert_eq!(properties.appearance, None);
}
}