bleasy/
lib.rs

1//! High-level BLE communication library.
2//!
3//! The goal of this library is to provide an easy-to-use interface
4//! for communicating with BLE devices, that satisfies most use cases.
5//!
6//! ## Usage
7//!
8//! Here is an example on how to find a device with battery level characteristic and read
9//! a value from that characteristic:
10//!
11//! ```rust,no_run
12//! use bleasy::common::characteristics::BATTERY_LEVEL;
13//! use bleasy::{Error, ScanConfig, Scanner};
14//! use tokio_stream::StreamExt;
15//!
16//! #[tokio::main]
17//! async fn main() -> Result<(), Error> {
18//!     rsutil::log::Log4rsConfig::default().initialize().unwrap();
19//!
20//!     // Create a filter for devices that have battery level characteristic
21//!     let config = ScanConfig::default()
22//!         .filter_by_characteristics(|uuids| uuids.contains(&BATTERY_LEVEL))
23//!         .stop_after_first_match();
24//!
25//!     // Start scanning for devices
26//!     let mut scanner = Scanner::new();
27//!     scanner.start(config).await?;
28//!
29//!     // Take the first discovered device
30//!     let device = scanner.device_stream().next().await.unwrap();
31//!     println!("{:?}", device);
32//!
33//!     // Read the battery level
34//!     let battery_level = device.characteristic(BATTERY_LEVEL).await?.unwrap();
35//!     println!("Battery level: {:?}", battery_level.read().await?);
36//!
37//!     Ok(())
38//! }
39//!```
40
41#![warn(clippy::all, future_incompatible, nonstandard_style, rust_2018_idioms)]
42
43mod characteristic;
44mod device;
45mod scanner;
46
47pub mod common;
48
49pub use self::{
50    characteristic::Characteristic,
51    device::{Device, DeviceEvent},
52    scanner::{
53        config::{Filter, ScanConfig},
54        Scanner,
55    },
56};
57pub use btleplug::{api::BDAddr, Error, Result};
58
59#[cfg(test)]
60mod tests {
61    use crate::{Device, Filter, ScanConfig, Scanner};
62    use btleplug::{api::BDAddr, Error};
63    use std::{future::Future, time::Duration};
64    use tokio_stream::StreamExt;
65    use uuid::Uuid;
66
67    #[tokio::test]
68    async fn test_discover() -> anyhow::Result<()> {
69        rsutil::log::Log4rsConfig::default().initialize().unwrap();
70
71        let duration = Duration::from_secs(10);
72        let config = ScanConfig::default().stop_after_timeout(duration);
73
74        let mut scanner = Scanner::new();
75        scanner.start(config).await?;
76
77        while let Some(device) = scanner.device_stream()?.next().await {
78            println!("Found device: {}", device.address());
79        }
80
81        Ok(())
82    }
83
84    async fn device_stream<T: Future<Output = ()>>(
85        scanner: Scanner,
86        callback: impl Fn(Device) -> T,
87    ) {
88        let duration = Duration::from_millis(15_000);
89        if let Err(_) = tokio::time::timeout(duration, async move {
90            if let Ok(mut stream) = scanner.device_stream() {
91                while let Some(device) = stream.next().await {
92                    callback(device).await;
93                    break;
94                }
95            }
96        })
97        .await
98        {
99            eprintln!("timeout....");
100        }
101    }
102
103    #[tokio::test]
104    async fn test_filter_by_address() -> Result<(), Error> {
105        rsutil::log::Log4rsConfig::default().initialize().unwrap();
106
107        let mac_addr = [0xE3, 0x9E, 0x2A, 0x4D, 0xAA, 0x97];
108        let filers = vec![Filter::Address("E3:9E:2A:4D:AA:97".into())];
109        let cfg = ScanConfig::default()
110            .with_filters(&filers)
111            .stop_after_first_match();
112        let mut scanner = Scanner::default();
113
114        scanner.start(cfg).await?;
115        device_stream(scanner, |device| async move {
116            assert_eq!(device.address(), BDAddr::from(mac_addr));
117        })
118        .await;
119
120        Ok(())
121    }
122
123    #[tokio::test]
124    async fn test_filter_by_character() -> Result<(), Error> {
125        rsutil::log::Log4rsConfig::default().initialize().unwrap();
126
127        let filers = vec![Filter::Characteristic(Uuid::from_u128(
128            0x6e400001_b5a3_f393_e0a9_e50e24dcca9e,
129        ))];
130        let cfg = ScanConfig::default()
131            .with_filters(&filers)
132            .stop_after_first_match();
133        let mut scanner = Scanner::default();
134
135        scanner.start(cfg).await?;
136        device_stream(scanner, |device| async move {
137            println!("device: {:?} found", device);
138        })
139        .await;
140
141        Ok(())
142    }
143
144    #[tokio::test]
145    async fn test_filter_by_name() -> Result<(), Error> {
146        rsutil::log::Log4rsConfig::default().initialize().unwrap();
147
148        let name = "73429485";
149        let filers = vec![Filter::Name(name.into())];
150        let cfg = ScanConfig::default()
151            .with_filters(&filers)
152            .stop_after_first_match();
153        let mut scanner = Scanner::default();
154
155        scanner.start(cfg).await?;
156        device_stream(scanner, |device| async move {
157            assert_eq!(device.local_name().await, Some(name.into()));
158        })
159        .await;
160
161        Ok(())
162    }
163
164    #[tokio::test]
165    async fn test_filter_by_rssi() -> Result<(), Error> {
166        rsutil::log::Log4rsConfig::default().initialize().unwrap();
167
168        let filers = vec![Filter::Rssi(-70)];
169        let cfg = ScanConfig::default()
170            .with_filters(&filers)
171            .stop_after_first_match();
172        let mut scanner = Scanner::default();
173
174        scanner.start(cfg).await?;
175        device_stream(scanner, |device| async move {
176            println!("device: {:?} found", device);
177        })
178        .await;
179
180        Ok(())
181    }
182
183    #[tokio::test]
184    async fn test_filter_by_service() -> Result<(), Error> {
185        rsutil::log::Log4rsConfig::default().initialize().unwrap();
186
187        let service = Uuid::from_u128(0x6e400001_b5a3_f393_e0a9_e50e24dcca9e);
188        let filers = vec![Filter::Service(service)];
189        let cfg = ScanConfig::default()
190            .with_filters(&filers)
191            .stop_after_first_match();
192        let mut scanner = Scanner::default();
193
194        scanner.start(cfg).await?;
195        device_stream(scanner, |device| async move {
196            println!("device: {:?} found", device);
197        })
198        .await;
199
200        Ok(())
201    }
202}