├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── features.md ├── bug_report.md └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE.md ├── README.md ├── examples ├── discover_adapters_peripherals.rs ├── event_driven_discovery.rs ├── lights.rs └── subscribe_notify_characteristic.rs ├── rustfmt.toml └── src ├── api ├── bdaddr.rs ├── bleuuid.rs └── mod.rs ├── bluez ├── adapter.rs ├── manager.rs ├── mod.rs └── peripheral.rs ├── common ├── adapter_manager.rs ├── mod.rs └── util.rs ├── corebluetooth ├── adapter.rs ├── central_delegate.rs ├── ffi.rs ├── future.rs ├── internal.rs ├── manager.rs ├── mod.rs ├── peripheral.rs └── utils │ ├── core_bluetooth.rs │ └── mod.rs ├── droidplug ├── adapter.rs ├── java │ ├── .gitignore │ ├── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── nonpolynomial │ │ └── btleplug │ │ └── android │ │ └── impl │ │ ├── Adapter.java │ │ ├── BluetoothException.java │ │ ├── NoSuchCharacteristicException.java │ │ ├── NotConnectedException.java │ │ ├── Peripheral.java │ │ ├── PermissionDeniedException.java │ │ ├── ScanFilter.java │ │ ├── UnexpectedCallbackException.java │ │ └── UnexpectedCharacteristicException.java ├── jni │ ├── mod.rs │ └── objects.rs ├── manager.rs ├── mod.rs └── peripheral.rs ├── lib.rs ├── platform.rs ├── serde.rs └── winrtble ├── adapter.rs ├── ble ├── characteristic.rs ├── descriptor.rs ├── device.rs ├── mod.rs ├── service.rs └── watcher.rs ├── manager.rs ├── mod.rs ├── peripheral.rs └── utils.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: qdot 2 | patreon: qdot 3 | ko_fi: qdot76367 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Problems with btleplug modules 4 | title: '' 5 | labels: bug 6 | 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Expected behavior** 13 | A clear and concise description of what you expected to happen. 14 | 15 | **Actual behavior** 16 | A clear and concise description of what actually happened. 17 | 18 | **Additional context** 19 | Add any other context about the problem here. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Requests 3 | about: Requests for features, including new modules, device support, etc... 4 | title: '' 5 | labels: Features 6 | 7 | --- 8 | 9 | **Feature Description** 10 | 11 | Clear definition of the feature, possibly including any help you could provide on implementing this feature, or resources you would need help with implementation. 12 | -------------------------------------------------------------------------------- /.github/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Problems with btleplug modules 4 | title: '' 5 | labels: bug 6 | assignees: 'qdot' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Actual behavior** 17 | A clear and concise description of what actually happened. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [dev, master] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | grcov-version: 0.8.0 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | target: 18 | - macos 19 | - linux 20 | - windows 21 | - android 22 | include: 23 | - target: macos 24 | os: macOS-latest 25 | cbt: aarch64-apple-darwin 26 | - target: linux 27 | os: ubuntu-latest 28 | cbt: x86_64-unknown-linux-gnu 29 | - target: windows 30 | os: windows-latest 31 | cbt: x86_64-pc-windows-msvc 32 | - target: android 33 | os: ubuntu-latest 34 | cbt: aarch64-linux-android 35 | 36 | runs-on: ${{ matrix.os }} 37 | 38 | env: 39 | CARGO_BUILD_TARGET: ${{ matrix.cbt }} 40 | 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Install dependencies 44 | if: ${{ runner.os == 'Linux' }} 45 | run: | 46 | sudo apt-get update 47 | sudo apt-get install libdbus-1-dev 48 | - uses: actions/setup-java@v2 49 | if: ${{ matrix.target == 'android' }} 50 | with: 51 | distribution: 'zulu' 52 | java-version: '17' 53 | - name: Setup NDK 54 | if: ${{ matrix.target == 'android' }} 55 | uses: nttld/setup-ndk@v1 56 | id: setup-ndk 57 | with: 58 | ndk-version: r25b 59 | local-cache: true 60 | - name: rust toolchain 61 | if: ${{ matrix.target == 'android' }} 62 | uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | target: aarch64-linux-android 66 | toolchain: stable 67 | override: true 68 | - name: Check 69 | run: cargo check --all --bins --examples 70 | - name: Check without default features 71 | run: cargo check --all --bins --examples --no-default-features 72 | - name: Check with all features 73 | run: cargo check --all --bins --examples --all-features 74 | - name: Run tests 75 | if: ${{ matrix.target != 'android' }} 76 | run: cargo test --all 77 | - name: Run clippy 78 | uses: actions-rs/clippy-check@v1 79 | with: 80 | name: clippy ${{ matrix.os }} 81 | token: ${{ secrets.GITHUB_TOKEN }} 82 | args: --all-features 83 | 84 | format: 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v2 88 | - name: Format Rust code 89 | run: cargo fmt --all -- --check 90 | 91 | # coverage: 92 | # strategy: 93 | # matrix: 94 | # os: 95 | # - macOS-latest 96 | # - ubuntu-latest 97 | # - windows-latest 98 | # runs-on: ${{ matrix.os }} 99 | # 100 | # env: 101 | # RUSTC_BOOTSTRAP: 1 102 | # steps: 103 | # - uses: actions/checkout@v2 104 | # - name: Install dependencies 105 | # if: ${{ runner.os == 'Linux' }} 106 | # run: sudo apt-get install libdbus-1-dev 107 | # - name: Cache grcov 108 | # uses: actions/cache@v2 109 | # with: 110 | # path: | 111 | # ~/.cargo/bin/grcov 112 | # ~/.cargo/bin/grcov.exe 113 | # ~/.cargo/.crates.toml 114 | # ~/.cargo/.crates2.json 115 | # key: ${{ runner.os }}-cargo-bin-${{ env.grcov-version }} 116 | # - name: Install grcov 117 | # uses: actions-rs/install@v0.1 118 | # with: 119 | # crate: grcov 120 | # version: ${{ env.grcov-version }} 121 | # - name: Install llvm-tools 122 | # run: rustup component add llvm-tools-preview 123 | # - name: Build for coverage 124 | # run: cargo build --all-features 125 | # env: 126 | # RUSTFLAGS: "-Zinstrument-coverage -Ccodegen-units=1" 127 | # - name: Run tests with coverage 128 | # run: cargo test --all-features 129 | # env: 130 | # RUSTFLAGS: "-Zinstrument-coverage" 131 | # LLVM_PROFILE_FILE: "test-coverage-%p-%m.profraw" 132 | # - name: Convert coverage 133 | # run: grcov . -s . --binary-path target/debug/ -t lcov --branch --ignore-not-existing -o target/debug/lcov.info 134 | # - name: Upload coverage to codecov.io 135 | # uses: codecov/codecov-action@v1 136 | # with: 137 | # directory: ./target/debug 138 | # fail_ci_if_error: true 139 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/**/*.xml 2 | .idea/ 3 | /target/ 4 | **/*.rs.bk 5 | Cargo.lock 6 | **/.vscode* 7 | /workspace.xml 8 | **/*.code-workspace -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.11.8 (2025-04-20) 2 | 3 | ## Features 4 | 5 | - Update dependencies 6 | - Including to windows-rs 0.61, which required some small code changes 7 | 8 | # 0.11.7 (2024-12-21) 9 | 10 | ## Features 11 | 12 | - Deserialize BDAddr from non-borrowed strings 13 | - Thanks icewind1991, ekuinox! 14 | - Add support for Extended Advertising on Android 15 | - Thanks Jakouf! 16 | 17 | ## Bugfixes 18 | 19 | - Call GetDefaultAdapter() instead of GetAdapter() on Android 20 | - Use BluetoothCacheMode::Uncached for services fetching on windows 21 | - This *may* cause issues with connection times, we'll see how it works out 22 | - Characteristics with duplicate UUIDs (but differing handles) no longer overwrite each other 23 | - Thanks blackspherefollower! 24 | - CoreBluetooth now fulfills all open characteristic futures on disconnect 25 | - Thanks szymonlesisz! 26 | 27 | # 0.11.6 (2024-10-06) 28 | 29 | ## Features 30 | 31 | - Move from objc to objc2 32 | - Now using an actually updated/maintained coreAPI library! Thanks madsmtm! 33 | - Implement CentralState for mac/linux/windows to tell when bluetooth is on/off 34 | - Thanks szymonlesisz! 35 | - Still needs Android impl. 36 | 37 | ## Bugfixes 38 | 39 | - Make local_name on CoreBluetooth match that of Windows/Linux returns when possible. 40 | - Thanks yuyoyuppe! 41 | - Fix descriptor reading on CoreBluetooth 42 | - Thanks kovapatrik! 43 | - Fix one of the many, many NullPointerException issues in droidplug 44 | - Thanks blackspherefollower! 45 | - There are so many more though, esp when we don't have correct permissions on Android. 46 | 47 | # 0.11.5 (2024-01-10) 48 | 49 | ## Bugfixes 50 | 51 | - Fix issue with Windows failing to read characteristic descriptors 52 | 53 | # 0.11.4 (2024-01-01) 54 | 55 | ## Bugfixes 56 | 57 | - Fix issue with manufacturer data not being consistently found on windows 58 | - Fix UUID used for finding characteristics on windows 59 | - Peripheral connection failure now returns an error 60 | - Peripheral service discovery failure now returns an error 61 | 62 | # 0.11.3 (2023-11-18) 63 | 64 | ## Bugfixes 65 | 66 | - CoreBluetooth: Fix missing include 67 | 68 | # 0.11.2 (2023-11-18) 69 | 70 | ## Bugfixes 71 | 72 | - Android: Fix advertisements with invalid UTF-8 strings not appearing 73 | - All Platforms: Fix clippy warnings 74 | 75 | # 0.11.1 (2023-09-08) 76 | 77 | ## Bugfixes 78 | 79 | - Windows/UWP: Internally held BTLE services now automatically disconnect when device disconnect is 80 | called. 81 | 82 | # 0.11.0 (2023-07-04) 83 | 84 | ## Features 85 | 86 | - Add scan filtering for android and windows 87 | - Implement serde Serialize/Deserliaze for PeripheralProperties, ScannFilter (#310, #314) 88 | - Add device class to properties (#319) 89 | - Add descriptor discovery and read/write across all platforms (#316) 90 | 91 | ## Bugfixes 92 | 93 | - Update RSSI w/ advertisements on CoreBluetooth (#306) 94 | - Fix issues with various unhandled exceptions on Android (#311) 95 | 96 | # 0.10.5 (2023-04-13) 97 | 98 | ## Features 99 | 100 | - Add RSSI readings for Android 101 | 102 | ## Bugfixes 103 | 104 | - Link conditionally against macOS AppKit based on platform 105 | - Improve error propagation on Windows 106 | - Reset connected state to false on disconnect on windows 107 | - Set DuplicateData = true for bluez 108 | 109 | # 0.10.4 (2022-11-27) 110 | 111 | ## Bugfixes 112 | 113 | - Change common CoreBluetooth log message from error to info level 114 | 115 | # 0.10.3 (2022-11-05) 116 | 117 | ## Bugfixes 118 | 119 | - Add PeripheralId Display implementation for Android PeripheralId 120 | 121 | # 0.10.2 (2022-10-30) 122 | 123 | ## Features 124 | 125 | - Implement Display on PeripheralId 126 | 127 | ## Bugfixes 128 | 129 | - Fix issues with panics on device disconnect on macOS 130 | 131 | # 0.10.1 (2022-09-23) 132 | 133 | ## Features 134 | 135 | - Add ability to disconnect devices on macOS/iOS 136 | 137 | # 0.10.0 (2022-07-30) 138 | 139 | ## Features 140 | 141 | - Add Android Support 142 | 143 | ## Breaking Changes 144 | 145 | - Update to Uuid v1, which is incompatible with Uuid v0.x. This may cause issues in upgrades. 146 | 147 | # 0.9.2 (2022-03-05) 148 | 149 | ## Features 150 | 151 | - UWP (Windows) devices now disconnect on drop or calls to disconnect 152 | - Improve characteristic finding resilience on UWP (Windows) 153 | 154 | ## Bugfixes 155 | 156 | - Update to windows-rs 0.33 157 | - Should fix issues with COM casting panics in older versions of windows 158 | - Fix panic when multiple discovery calls are made on corebluetooth (macOS) 159 | - Update Dashmap version to resolve RUSTSEC-2022-0002 160 | 161 | # 0.9.1 (2022-01-12) 162 | 163 | ## Features 164 | 165 | - `BDAddr` and `PeripheralId` are now guaranteed to implement `Hash`, `Ord` and `PartialOrd` on all 166 | platforms. 167 | 168 | ## Bugfixes 169 | 170 | - Linux implementation will now synthesise `DeviceConnected` events at the start of the event stream 171 | for all peripherals which were already connected at the point that the event stream was requested. 172 | - `Central` methods on Linux will now correctly only affect the correct Bluetooth adapter, rather 173 | than all adapters on the system. 174 | - Filters are now supported for macOS, allowing the library to work on macOS >= 12. 175 | 176 | # 0.9.0 (2021-10-20) 177 | 178 | ## Features 179 | 180 | - Added Received Signal Strength Indicator (RSSI) peripheral property. 181 | - Peripheral `notifications()` streams can now be queried before any 182 | connection and remain valid independent of any re-connections. 183 | - Characteristics are now grouped by service, and infomation about services is available. The old 184 | API to access characteristics without specifying a service UUIDs is retained for backwards 185 | compatibility. 186 | - Better logging and other minor improvements in examples. 187 | - Added method to get adapter information. For now it only works on Linux. 188 | 189 | ## Breaking changes 190 | 191 | - Removed `CentralEvent::DeviceLost`. It wasn't emitted anywhere. 192 | - Changed tx_power_level type from i8 to i16. 193 | - Removed `PeripheralProperties::discovery_count`. 194 | - New `PeripheralId` type is used as an opaque ID for peripherals, as the MAC address is not 195 | available on all platforms. 196 | - Added optional `ScanFilter` parameter to `Central::start_scan` to filter by service UUIDs. 197 | 198 | ## Bugfixes 199 | 200 | - `Peripheral::is_connected` now works on Mac OS, and works better on Windows. 201 | - Fixed bug on Windows where RSSI was reported as TX power. 202 | - Report address type on Windows. 203 | - Report all advertised service UUIDs on Windows, rather than only those in the most recent 204 | advertisement. 205 | - Fixed bug with service caching on Windows. 206 | - Fixed bug with concurrent streams not working on Linux. 207 | 208 | # 0.8.1 (2021-08-14) 209 | 210 | ## Bugfixes 211 | 212 | - Errors now Sync/Send (usable with Anyhow) 213 | - Characteristic properties now properly reported on windows 214 | 215 | # 0.8.0 (2021-07-27) 216 | 217 | ## Features 218 | 219 | - Overhaul API, moving to async based system 220 | 221 | ## Breaking Changes 222 | 223 | - Pretty much everything? The whole API got rewritten. All hail the new flesh. 224 | 225 | # 0.7.3 (2021-07-25) 226 | 227 | ## Bugfixes 228 | 229 | - Fix issue with characteristic array not updating on Win10 230 | - #172: Fix setting local_name in macOS 231 | 232 | # 0.7.2 (2021-04-04) 233 | 234 | ## Bugfixes 235 | 236 | - Windows UWP characteristic methods now return errors instead of unwrapping everything. 237 | 238 | # 0.7.1 (2021-03-01) 239 | 240 | ## Bugfixes 241 | 242 | - Fixed commit/merge issues with 0.7.0 that ended up with incorrect dependencies being brought in. 243 | 244 | # 0.7.0 (2021-02-28) (Yanked) 245 | 246 | ## Breaking API Changes 247 | 248 | - Move to using Uuid crate instead of having an internal type. 249 | - Remove discover_characteristics_in_range (unused or duplicated elsewhere) 250 | - write() commands are now passed a WriteType for specifying WriteWith/WithoutResponse 251 | - Variants added to CentralEvent enum, may break exhaustive checks 252 | 253 | ## Features 254 | 255 | - Add capabilities for service and manufacturer advertisements 256 | - Lots of CoreBluetooth cleanup 257 | - Update to using windows library (instead of winrt) 258 | - Replace usage of async_std for channels in macOS with futures crate 259 | 260 | ## Bugfixes 261 | 262 | - De-escalate log message levels, so there are less message spams at the info level. 263 | 264 | # 0.6.0 (2021-02-04) 265 | 266 | ## Breaking API Changes 267 | 268 | - Removed many _async methods that were unimplemented 269 | - Stopped returning write values when not needed. 270 | 271 | ## Features 272 | 273 | - Complete rewrite of Bluez core 274 | - Now uses DBus API instead of raw socket access 275 | - Windows support moved to WinRT library 276 | - Move from failure to thiserror for error handling 277 | - failure was deprecated a while ago 278 | 279 | ## Bugfixes 280 | 281 | - Windows UWP no longer panics on scan when radio not connected. 282 | 283 | # 0.5.5 (2021-01-18) 284 | 285 | ## Bugfixes 286 | 287 | - Fix dependency issue with async-std channels 288 | 289 | # 0.5.4 (2020-10-06) 290 | 291 | ## Bugfixes 292 | 293 | - Fix issue where library panics whenever a characteristic is read instead of 294 | notified on macOS. 295 | 296 | # 0.5.3 (2020-10-05) 297 | 298 | ## Bugfixes 299 | 300 | - Fix issue where library panics whenever a characteristic is written without 301 | response on macOS. 302 | 303 | # 0.5.2 (2020-10-04) 304 | 305 | ## Features 306 | 307 | - UUID now takes simplified inputs for from_str() 308 | - Read/Write added for CoreBluetooth 309 | - Example improvements 310 | 311 | ## Bugfixes 312 | 313 | - Windows UWP characteristics now actually reads on read(), instead of just 314 | returning [] 315 | 316 | ## Bugfixes 317 | 318 | # 0.5.1 (2020-08-03) 319 | 320 | ## Bugfixes 321 | 322 | * Fixed issue with peripheral updates in adapter manager wiping out peripherals 323 | completely (#64) 324 | * Ran rustfmt (misformatted code is a bug ok?) 325 | 326 | # 0.5.0 (2020-07-26) 327 | 328 | ## Features 329 | 330 | * Moved events from callbacks to channels (currently using std::channel, will 331 | change to future::Stream once we go async). 332 | * Moved from using Arc>> to Arc>. Slightly 333 | cleaner, less locking boilerplate. 334 | 335 | ## Bugfixes 336 | 337 | * Centralized peripheral management into the AdapterManager class, which should 338 | clean up a bunch of bugs both filed and not. 339 | 340 | # 0.4.4 (2020-07-22) 341 | 342 | ## Bugfixes 343 | 344 | * Fix peripheral connect panic caused by uuid length on macOS (#43) 345 | * Windows/macOS devices now emit events on device disconnect (#54) 346 | 347 | # 0.4.3 (2020-06-05) 348 | 349 | ## Features 350 | 351 | * Allow notification handlers to be FnMut 352 | * Added new examples 353 | * Update dependencies 354 | 355 | ## Bugfixes 356 | 357 | * Fix local_name on macOS 10.15 358 | 359 | # 0.4.2 (2020-04-18) 360 | 361 | ## Features 362 | 363 | * Some types now capable of serde de/serialization, using "serde" feature 364 | * Added new examples 365 | 366 | ## Bugfixes 367 | 368 | * Adapters functions now return vectors of some kind of adapter on all 369 | platforms. 370 | * Bluez notification handlers now live with the peripheral. 371 | * Bluez defaults to active scan. 372 | * Remove all println statements in library (mostly in the windows library), 373 | replace with log macros. 374 | 375 | # 0.4.1 (2020-03-16) 376 | 377 | ## Features 378 | 379 | * Get BDAddr and UUID from String 380 | * More examples 381 | * Update dependencies 382 | 383 | # 0.4.0 (2020-01-20) 384 | 385 | ## Features 386 | 387 | * Added CoreBluetooth Support, using async-std with most of the async 388 | parts wrapped in block_on calls for now. Library now supports 389 | Win10/MacOS/Linux/Maybe iOS. 390 | * Brought code up to Rust 2018 standard 391 | * Added Characteristic UUID to ValueNotification struct, since 392 | only linux deals with Start/End/Value handles 393 | 394 | # 0.3.1 (2020-01-11) 395 | 396 | ## Features 397 | 398 | * Initial fork from rumble 399 | * Brought in winrt patch, as well as other PRs on that project. 400 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "btleplug" 3 | version = "0.11.8" 4 | authors = ["Nonpolynomial, LLC "] 5 | license = "MIT/Apache-2.0/BSD-3-Clause" 6 | repository = "https://github.com/deviceplug/btleplug" 7 | homepage = "https://github.com/deviceplug/btleplug" 8 | edition = "2021" 9 | description = """ 10 | A Cross-Platform Rust Bluetooth Low Energy (BLE) GATT 11 | library. 12 | """ 13 | readme = "README.md" 14 | keywords = ["bluetooth", "BLE", "bluez", "uwp", "corebluetooth"] 15 | categories = ["hardware-support"] 16 | 17 | [lib] 18 | name = "btleplug" 19 | path = "src/lib.rs" 20 | 21 | [features] 22 | serde = ["uuid/serde", "serde_cr", "serde_bytes"] 23 | 24 | [dependencies] 25 | async-trait = "0.1.88" 26 | log = "0.4.27" 27 | bitflags = "2.9.0" 28 | thiserror = "2.0.12" 29 | uuid = "1.16.0" 30 | serde_cr = { package = "serde", version = "1.0.219", features = ["derive"], default-features = false, optional = true } 31 | serde_bytes = { version = "0.11.17", optional = true } 32 | dashmap = "6.1.0" 33 | futures = "0.3.31" 34 | static_assertions = "1.1.0" 35 | # rt feature needed for block_on in macOS internal thread 36 | tokio = { version = "1.44.2", features = ["sync", "rt"] } 37 | tokio-stream = { version = "0.1.17", features = ["sync"] } 38 | 39 | [target.'cfg(target_os = "linux")'.dependencies] 40 | dbus = "0.9.7" 41 | bluez-async = "0.8.0" 42 | 43 | [target.'cfg(target_os = "android")'.dependencies] 44 | jni = "0.19.0" 45 | once_cell = "1.20.2" 46 | jni-utils = "0.1.1" 47 | 48 | [target.'cfg(target_vendor = "apple")'.dependencies] 49 | objc2 = "0.5.2" 50 | objc2-foundation = { version = "0.2.2", default-features = false, features = [ 51 | "std", 52 | "block2", 53 | "NSArray", 54 | "NSData", 55 | "NSDictionary", 56 | "NSEnumerator", 57 | "NSError", 58 | "NSObject", 59 | "NSString", 60 | "NSUUID", 61 | "NSValue", 62 | ] } 63 | objc2-core-bluetooth = { version = "0.2.2", default-features = false, features = [ 64 | "std", 65 | "CBAdvertisementData", 66 | "CBAttribute", 67 | "CBCentralManager", 68 | "CBCentralManagerConstants", 69 | "CBCharacteristic", 70 | "CBDescriptor", 71 | "CBManager", 72 | "CBPeer", 73 | "CBPeripheral", 74 | "CBService", 75 | "CBUUID", 76 | ] } 77 | 78 | [target.'cfg(target_os = "windows")'.dependencies] 79 | windows = { version = "0.61", features = ["Devices_Bluetooth", "Devices_Bluetooth_GenericAttributeProfile", "Devices_Bluetooth_Advertisement", "Devices_Radios", "Foundation_Collections", "Foundation", "Storage_Streams"] } 80 | windows-future = "0.2.0" 81 | 82 | [dev-dependencies] 83 | rand = "0.9" 84 | pretty_env_logger = "0.5.0" 85 | tokio = { version = "1.44.2", features = ["macros", "rt", "rt-multi-thread"] } 86 | serde_json = "1.0.140" 87 | toml = "0.8.20" 88 | anyhow = "1" 89 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | btleplug is covered under the following BSD-3-Clause license: 2 | 3 | Copyright (c) 2020-2021, Nonpolynomial 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are 8 | met: 9 | 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of btleplug nor the names of its contributors may 18 | be used to endorse or promote products derived from this software 19 | without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | --- 34 | 35 | Some parts of btleplug were forked from Rumble, which is dual-covered 36 | under the following MIT/Apache Licenses: 37 | 38 | Copyright (c) 2014 The Rust Project Developers 39 | 40 | Permission is hereby granted, free of charge, to any 41 | person obtaining a copy of this software and associated 42 | documentation files (the "Software"), to deal in the 43 | Software without restriction, including without 44 | limitation the rights to use, copy, modify, merge, 45 | publish, distribute, sublicense, and/or sell copies of 46 | the Software, and to permit persons to whom the Software 47 | is furnished to do so, subject to the following 48 | conditions: 49 | 50 | The above copyright notice and this permission notice 51 | shall be included in all copies or substantial portions 52 | of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 55 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 56 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 57 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 58 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 59 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 60 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 61 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 62 | DEALINGS IN THE SOFTWARE. 63 | 64 | - 65 | 66 | Apache License 67 | Version 2.0, January 2004 68 | http://www.apache.org/licenses/ 69 | 70 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 71 | 72 | 1. Definitions. 73 | 74 | "License" shall mean the terms and conditions for use, reproduction, 75 | and distribution as defined by Sections 1 through 9 of this document. 76 | 77 | "Licensor" shall mean the copyright owner or entity authorized by 78 | the copyright owner that is granting the License. 79 | 80 | "Legal Entity" shall mean the union of the acting entity and all 81 | other entities that control, are controlled by, or are under common 82 | control with that entity. For the purposes of this definition, 83 | "control" means (i) the power, direct or indirect, to cause the 84 | direction or management of such entity, whether by contract or 85 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 86 | outstanding shares, or (iii) beneficial ownership of such entity. 87 | 88 | "You" (or "Your") shall mean an individual or Legal Entity 89 | exercising permissions granted by this License. 90 | 91 | "Source" form shall mean the preferred form for making modifications, 92 | including but not limited to software source code, documentation 93 | source, and configuration files. 94 | 95 | "Object" form shall mean any form resulting from mechanical 96 | transformation or translation of a Source form, including but 97 | not limited to compiled object code, generated documentation, 98 | and conversions to other media types. 99 | 100 | "Work" shall mean the work of authorship, whether in Source or 101 | Object form, made available under the License, as indicated by a 102 | copyright notice that is included in or attached to the work 103 | (an example is provided in the Appendix below). 104 | 105 | "Derivative Works" shall mean any work, whether in Source or Object 106 | form, that is based on (or derived from) the Work and for which the 107 | editorial revisions, annotations, elaborations, or other modifications 108 | represent, as a whole, an original work of authorship. For the purposes 109 | of this License, Derivative Works shall not include works that remain 110 | separable from, or merely link (or bind by name) to the interfaces of, 111 | the Work and Derivative Works thereof. 112 | 113 | "Contribution" shall mean any work of authorship, including 114 | the original version of the Work and any modifications or additions 115 | to that Work or Derivative Works thereof, that is intentionally 116 | submitted to Licensor for inclusion in the Work by the copyright owner 117 | or by an individual or Legal Entity authorized to submit on behalf of 118 | the copyright owner. For the purposes of this definition, "submitted" 119 | means any form of electronic, verbal, or written communication sent 120 | to the Licensor or its representatives, including but not limited to 121 | communication on electronic mailing lists, source code control systems, 122 | and issue tracking systems that are managed by, or on behalf of, the 123 | Licensor for the purpose of discussing and improving the Work, but 124 | excluding communication that is conspicuously marked or otherwise 125 | designated in writing by the copyright owner as "Not a Contribution." 126 | 127 | "Contributor" shall mean Licensor and any individual or Legal Entity 128 | on behalf of whom a Contribution has been received by Licensor and 129 | subsequently incorporated within the Work. 130 | 131 | 2. Grant of Copyright License. Subject to the terms and conditions of 132 | this License, each Contributor hereby grants to You a perpetual, 133 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 134 | copyright license to reproduce, prepare Derivative Works of, 135 | publicly display, publicly perform, sublicense, and distribute the 136 | Work and such Derivative Works in Source or Object form. 137 | 138 | 3. Grant of Patent License. Subject to the terms and conditions of 139 | this License, each Contributor hereby grants to You a perpetual, 140 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 141 | (except as stated in this section) patent license to make, have made, 142 | use, offer to sell, sell, import, and otherwise transfer the Work, 143 | where such license applies only to those patent claims licensable 144 | by such Contributor that are necessarily infringed by their 145 | Contribution(s) alone or by combination of their Contribution(s) 146 | with the Work to which such Contribution(s) was submitted. If You 147 | institute patent litigation against any entity (including a 148 | cross-claim or counterclaim in a lawsuit) alleging that the Work 149 | or a Contribution incorporated within the Work constitutes direct 150 | or contributory patent infringement, then any patent licenses 151 | granted to You under this License for that Work shall terminate 152 | as of the date such litigation is filed. 153 | 154 | 4. Redistribution. You may reproduce and distribute copies of the 155 | Work or Derivative Works thereof in any medium, with or without 156 | modifications, and in Source or Object form, provided that You 157 | meet the following conditions: 158 | 159 | (a) You must give any other recipients of the Work or 160 | Derivative Works a copy of this License; and 161 | 162 | (b) You must cause any modified files to carry prominent notices 163 | stating that You changed the files; and 164 | 165 | (c) You must retain, in the Source form of any Derivative Works 166 | that You distribute, all copyright, patent, trademark, and 167 | attribution notices from the Source form of the Work, 168 | excluding those notices that do not pertain to any part of 169 | the Derivative Works; and 170 | 171 | (d) If the Work includes a "NOTICE" text file as part of its 172 | distribution, then any Derivative Works that You distribute must 173 | include a readable copy of the attribution notices contained 174 | within such NOTICE file, excluding those notices that do not 175 | pertain to any part of the Derivative Works, in at least one 176 | of the following places: within a NOTICE text file distributed 177 | as part of the Derivative Works; within the Source form or 178 | documentation, if provided along with the Derivative Works; or, 179 | within a display generated by the Derivative Works, if and 180 | wherever such third-party notices normally appear. The contents 181 | of the NOTICE file are for informational purposes only and 182 | do not modify the License. You may add Your own attribution 183 | notices within Derivative Works that You distribute, alongside 184 | or as an addendum to the NOTICE text from the Work, provided 185 | that such additional attribution notices cannot be construed 186 | as modifying the License. 187 | 188 | You may add Your own copyright statement to Your modifications and 189 | may provide additional or different license terms and conditions 190 | for use, reproduction, or distribution of Your modifications, or 191 | for any such Derivative Works as a whole, provided Your use, 192 | reproduction, and distribution of the Work otherwise complies with 193 | the conditions stated in this License. 194 | 195 | 5. Submission of Contributions. Unless You explicitly state otherwise, 196 | any Contribution intentionally submitted for inclusion in the Work 197 | by You to the Licensor shall be under the terms and conditions of 198 | this License, without any additional terms or conditions. 199 | Notwithstanding the above, nothing herein shall supersede or modify 200 | the terms of any separate license agreement you may have executed 201 | with Licensor regarding such Contributions. 202 | 203 | 6. Trademarks. This License does not grant permission to use the trade 204 | names, trademarks, service marks, or product names of the Licensor, 205 | except as required for reasonable and customary use in describing the 206 | origin of the Work and reproducing the content of the NOTICE file. 207 | 208 | 7. Disclaimer of Warranty. Unless required by applicable law or 209 | agreed to in writing, Licensor provides the Work (and each 210 | Contributor provides its Contributions) on an "AS IS" BASIS, 211 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 212 | implied, including, without limitation, any warranties or conditions 213 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 214 | PARTICULAR PURPOSE. You are solely responsible for determining the 215 | appropriateness of using or redistributing the Work and assume any 216 | risks associated with Your exercise of permissions under this License. 217 | 218 | 8. Limitation of Liability. In no event and under no legal theory, 219 | whether in tort (including negligence), contract, or otherwise, 220 | unless required by applicable law (such as deliberate and grossly 221 | negligent acts) or agreed to in writing, shall any Contributor be 222 | liable to You for damages, including any direct, indirect, special, 223 | incidental, or consequential damages of any character arising as a 224 | result of this License or out of the use or inability to use the 225 | Work (including but not limited to damages for loss of goodwill, 226 | work stoppage, computer failure or malfunction, or any and all 227 | other commercial damages or losses), even if such Contributor 228 | has been advised of the possibility of such damages. 229 | 230 | 9. Accepting Warranty or Additional Liability. While redistributing 231 | the Work or Derivative Works thereof, You may choose to offer, 232 | and charge a fee for, acceptance of support, warranty, indemnity, 233 | or other liability obligations and/or rights consistent with this 234 | License. However, in accepting such obligations, You may act only 235 | on Your own behalf and on Your sole responsibility, not on behalf 236 | of any other Contributor, and only if You agree to indemnify, 237 | defend, and hold each Contributor harmless for any liability 238 | incurred by, or claims asserted against, such Contributor by reason 239 | of your accepting any such warranty or additional liability. 240 | 241 | END OF TERMS AND CONDITIONS 242 | 243 | APPENDIX: How to apply the Apache License to your work. 244 | 245 | To apply the Apache License to your work, attach the following 246 | boilerplate notice, with the fields enclosed by brackets "[]" 247 | replaced with your own identifying information. (Don't include 248 | the brackets!) The text should be enclosed in the appropriate 249 | comment syntax for the file format. We also recommend that a 250 | file or class name and description of purpose be included on the 251 | same "printed page" as the copyright notice for easier 252 | identification within third-party archives. 253 | 254 | Copyright [yyyy] [name of copyright owner] 255 | 256 | Licensed under the Apache License, Version 2.0 (the "License"); 257 | you may not use this file except in compliance with the License. 258 | You may obtain a copy of the License at 259 | 260 | http://www.apache.org/licenses/LICENSE-2.0 261 | 262 | Unless required by applicable law or agreed to in writing, software 263 | distributed under the License is distributed on an "AS IS" BASIS, 264 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 265 | See the License for the specific language governing permissions and 266 | limitations under the License. 267 | 268 | --- 269 | 270 | Some parts of btleplug were forked from blurmac, which is covered 271 | under the following BSD 3-Clause License: 272 | 273 | Copyright (c) 2017 Akos Kiss. All rights reserved. 274 | 275 | Redistribution and use in source and binary forms, with or without 276 | modification, are permitted provided that the following conditions are 277 | met: 278 | 279 | * Redistributions of source code must retain the above copyright 280 | notice, this list of conditions and the following disclaimer. 281 | * Redistributions in binary form must reproduce the above copyright 282 | notice, this list of conditions and the following disclaimer in the 283 | documentation and/or other materials provided with the distribution. 284 | * Neither the name of the copyright holder nor the names of its 285 | contributors may be used to endorse or promote products derived from 286 | this software without specific prior written permission. 287 | 288 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 289 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 290 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 291 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 292 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 293 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 294 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 295 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 296 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 297 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 298 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 299 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # btleplug 2 | 3 | [![Crates.io Version](https://img.shields.io/crates/v/btleplug)](https://crates.io/crates/btleplug) 4 | [![docs.rs page](https://docs.rs/btleplug/badge.svg)](https://docs.rs/btleplug) 5 | [![Crates.io Downloads](https://img.shields.io/crates/d/btleplug)](https://crates.io/crates/btleplug) 6 | [![Crates.io License](https://img.shields.io/crates/l/btleplug)](https://crates.io/crates/btleplug) 7 | 8 | [![Discord](https://img.shields.io/discord/738080600032018443.svg?logo=discord)](https://discord.gg/QGhMFzR) 9 | 10 | [![Github donate button](https://img.shields.io/badge/github-donate-ff69b4.svg)](https://www.github.com/sponsors/qdot) 11 | 12 | btleplug is an async Rust BLE library, supporting Windows 10, macOS, Linux, iOS, and Android 13 | (including Flutter, see below for more info). 14 | 15 | It grew out of several earlier abandoned libraries for various platforms 16 | ([rumble](https://github.com/mwylde/rumble), [blurmac](https://github.com/servo/devices), etc...), 17 | with the goal of building a fully cross platform library. If you're curious about how the library grew, [you can read more on that in this blog post](https://nonpolynomial.com/2023/10/30/how-to-beg-borrow-steal-your-way-to-a-cross-platform-bluetooth-le-library/). 18 | 19 | btleplug is meant to be _host/central mode only_. If you are interested in peripheral BTLE (i.e. 20 | acting like a Bluetooth LE device instead of connecting to one), check out 21 | [bluster](https://github.com/dfrankland/bluster/) or [ble-peripheral-rust](https://github.com/rohitsangwan01/ble-peripheral-rust/). 22 | 23 | This library **DOES NOT SUPPORT BLUETOOTH 2/CLASSIC**. There are no plans to add BT2/Classic 24 | support. 25 | 26 | ## Platform Status 27 | 28 | - **Linux / Windows / macOS / iOS / Android** 29 | - Device enumeration and characteristic/services implemented and working. 30 | - Please file bugs and missing features if you find them. 31 | - **WASM/WebBluetooth** 32 | - WebBluetooth is possible, and a PR is in, but needs review. 33 | - [Tracking issue here](https://github.com/deviceplug/btleplug/issues/13) 34 | - Please hold off on filing more issues until base implementation is 35 | landed. 36 | 37 | ### Platform Feature Table 38 | 39 | - X: Completed and released 40 | - O: In development 41 | - Blank: Not started 42 | 43 | | Feature | Windows | MacOS / iOS | Linux | Android | 44 | | ------------------------------------- | ------- | ----------- | ----- | ------- | 45 | | Bring Up Adapter | X | X | X | X | 46 | | Handle Multiple Adapters | | | X | | 47 | | Discover Devices | X | X | X | X | 48 | | └ Discover Services | X | X | X | X | 49 | | └ Discover Characteristics | X | X | X | X | 50 | | └ Discover Descriptors | X | X | X | X | 51 | | └ Discover Name | X | X | X | X | 52 | | └ Discover Manufacturer Data | X | X | X | X | 53 | | └ Discover Service Data | X | X | X | X | 54 | | └ Discover MAC address | X | | X | X | 55 | | GATT Server Connect | X | X | X | X | 56 | | GATT Server Connect Event | X | X | X | X | 57 | | GATT Server Disconnect | X | X | X | X | 58 | | GATT Server Disconnect Event | X | X | X | X | 59 | | Write to Characteristic | X | X | X | X | 60 | | Read from Characteristic | X | X | X | X | 61 | | Subscribe to Characteristic | X | X | X | X | 62 | | Unsubscribe from Characteristic | X | X | X | X | 63 | | Get Characteristic Notification Event | X | X | X | X | 64 | | Read Descriptor | X | X | X | X | 65 | | Write Descriptor | X | X | X | X | 66 | | Retrieve MTU | | | | | 67 | | Retrieve Connection Interval | | | | | 68 | 69 | ## Library Features 70 | 71 | #### Serialization/Deserialization 72 | 73 | To enable implementation of serde's `Serialize` and `Deserialize` across some common types in the `api` module, use the `serde` feature. 74 | 75 | ```toml 76 | [dependencies] 77 | btleplug = { version = "0.11", features = ["serde"] } 78 | ``` 79 | 80 | ## Build/Installation Notes for Specific Platforms 81 | 82 | ### macOS 83 | 84 | To use Bluetooth on macOS Big Sur (11) or later, you need to either package your 85 | binary into an application bundle with an `Info.plist` including 86 | `NSBluetoothAlwaysUsageDescription`, or (for a command-line application such as 87 | the examples included with `btleplug`) enable the Bluetooth permission for your 88 | terminal. You can do the latter by going to _System Preferences_ → _Security & 89 | Privacy_ → _Privacy_ → _Bluetooth_, clicking the '+' button, and selecting 90 | 'Terminal' (or iTerm or whichever terminal application you use). 91 | 92 | ### Android 93 | 94 | Due to requiring a hybrid Rust/Java build, btleplug for Android requires a somewhat complicated 95 | setup. 96 | 97 | Some information on performing the build is available in the [original issue for Android support in btlplug](https://github.com/deviceplug/btleplug/issues/8). 98 | 99 | A quick overview of the build process: 100 | 101 | - For java, you will need the java portion of 102 | [jni-utils-rs](https://github.com/deviceplug/jni-utils-rs) available either in a Maven repository 103 | or locally (if locally, you'll need to check out btleplug and change the gradle file). 104 | - Either build the java portion of btleplug, in the `src/droidplug/java` directory, using the 105 | included gradle files, and them to a Maven repo, or have the Java portion of your android app point to that as a local implementation. 106 | - For Rust, the build should go as normal, though we recommend using `cargo-ndk` to build. Output 107 | the jniLibs and make sure they end up in the right place in your app. 108 | 109 | Proguard optimization can be an issue when using btleplug, as the .aar file generated by the java 110 | code in btleplug is only accessed by native code, and can be optimized out as part of dead code 111 | removal and resource shrinking. To fix this, changes will need to be made to your build.gradle file, and proguard rules will need to be defined. 112 | 113 | For build.gradle: 114 | ```groovy 115 | buildTypes { 116 | release { 117 | // TODO: Add your own signing config for the release build. 118 | // Signing with the debug keys for now, so `flutter run --release` works. 119 | signingConfig signingConfigs.debug 120 | 121 | shrinkResources true 122 | minifyEnabled true 123 | 124 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 125 | 126 | } 127 | } 128 | ``` 129 | 130 | proguard-rules.pro: 131 | ``` 132 | #Flutter Wrapper - Only needed if using flutter 133 | -keep class io.flutter.app.** { *; } 134 | -keep class io.flutter.plugin.** { *; } 135 | -keep class io.flutter.util.** { *; } 136 | -keep class io.flutter.view.** { *; } 137 | -keep class io.flutter.** { *; } 138 | -keep class io.flutter.plugins.** { *; } 139 | 140 | #btleplug resources 141 | -keep class com.nonpolynomial.** { *; } 142 | -keep class io.github.gedgygedgy.** { *; } 143 | ``` 144 | 145 | ### iOS 146 | 147 | As the Corebluetooth implementation is shared between macOS and iOS, btleplug on iOS should "just 148 | work", and seems to be stable. How this is built can vary based on your app setup and what language 149 | you're binding to, but sample instructions are as follows ([taken from 150 | here](https://github.com/deviceplug/btleplug/issues/12#issuecomment-1007671555)): 151 | 152 | - Write a rust library (static) that uses btleplug and exposes an FFI API to C 153 | - Use cbindgen to generate a C header file for that API 154 | - Use cargo-lipo to build a universal static lib 155 | - Drag the header file and the library into your Xcode project 156 | - Add NSBluetoothAlwaysUsageDescription to your Info.plist file 157 | 158 | There are also some examples in the Flutter shim listed below. 159 | 160 | ### Flutter 161 | 162 | While we don't specifically support Flutter, there's a template repo available at 163 | [https://github.com/trobanga/flutter_btleplug](https://github.com/trobanga/flutter_btleplug). This 164 | template has builds for both Android and iOS using btleplug. 165 | 166 | As flutter compilation tends to be complex across platforms, we cannot help with flutter build issues. 167 | 168 | ### Tauri 169 | 170 | While we don't specifically support Tauri, there's a plugin available at 171 | [https://github.com/MnlPhlp/tauri-plugin-blec](https://github.com/MnlPhlp/tauri-plugin-blec). Please note that all Tauri questions should go to the plugin repo before coming here, we cannot help with Tauri issues as none of this project's developers use Tauri. 172 | 173 | ## Alternative Libraries 174 | 175 | Everyone has different bluetooth needs, so if btleplug doesn't fit yours, try these other libraries by the rust community! 176 | 177 | - [Bluest](https://github.com/alexmoon/bluest) - Cross Platform BLE library (Windows/macOS/iOS/Linux) 178 | - [Bluey](https://github.com/rib/bluey) - Cross Platform BLE library (Windows/Android) 179 | - [Bluer](https://crates.io/crates/bluer) - Official Rust interface for Bluez on Linux, with more 180 | features since it only supports one platform (we use 181 | [Bluez-async](https://crates.io/crates/bluez-async) internally.) 182 | 183 | ## License 184 | 185 | BTLEPlug is covered under a BSD 3-Clause License, with some parts from 186 | Rumble/Blurmac covered under MIT/Apache dual license, and BSD 3-Clause 187 | licenses, respectively. See LICENSE.md for more info and copyright 188 | information. 189 | -------------------------------------------------------------------------------- /examples/discover_adapters_peripherals.rs: -------------------------------------------------------------------------------- 1 | // See the "macOS permissions note" in README.md before running this on macOS 2 | // Big Sur or later. 3 | 4 | use std::time::Duration; 5 | use tokio::time; 6 | 7 | use btleplug::api::{Central, Manager as _, Peripheral, ScanFilter}; 8 | use btleplug::platform::Manager; 9 | 10 | #[tokio::main] 11 | async fn main() -> anyhow::Result<()> { 12 | pretty_env_logger::init(); 13 | 14 | let manager = Manager::new().await?; 15 | let adapter_list = manager.adapters().await?; 16 | if adapter_list.is_empty() { 17 | eprintln!("No Bluetooth adapters found"); 18 | } 19 | 20 | for adapter in adapter_list.iter() { 21 | println!("Starting scan on {}...", adapter.adapter_info().await?); 22 | adapter 23 | .start_scan(ScanFilter::default()) 24 | .await 25 | .expect("Can't scan BLE adapter for connected devices..."); 26 | time::sleep(Duration::from_secs(10)).await; 27 | let peripherals = adapter.peripherals().await?; 28 | if peripherals.is_empty() { 29 | eprintln!("->>> BLE peripheral devices were not found, sorry. Exiting..."); 30 | } else { 31 | // All peripheral devices in range 32 | for peripheral in peripherals.iter() { 33 | let properties = peripheral.properties().await?; 34 | let is_connected = peripheral.is_connected().await?; 35 | let local_name = properties 36 | .unwrap() 37 | .local_name 38 | .unwrap_or(String::from("(peripheral name unknown)")); 39 | println!( 40 | "Peripheral {:?} is connected: {:?}", 41 | local_name, is_connected 42 | ); 43 | if !is_connected { 44 | println!("Connecting to peripheral {:?}...", &local_name); 45 | if let Err(err) = peripheral.connect().await { 46 | eprintln!("Error connecting to peripheral, skipping: {}", err); 47 | continue; 48 | } 49 | } 50 | let is_connected = peripheral.is_connected().await?; 51 | println!( 52 | "Now connected ({:?}) to peripheral {:?}...", 53 | is_connected, &local_name 54 | ); 55 | peripheral.discover_services().await?; 56 | println!("Discover peripheral {:?} services...", &local_name); 57 | for service in peripheral.services() { 58 | println!( 59 | "Service UUID {}, primary: {}", 60 | service.uuid, service.primary 61 | ); 62 | for characteristic in service.characteristics { 63 | println!(" {:?}", characteristic); 64 | } 65 | } 66 | if is_connected { 67 | println!("Disconnecting from peripheral {:?}...", &local_name); 68 | peripheral 69 | .disconnect() 70 | .await 71 | .expect("Error disconnecting from BLE peripheral"); 72 | } 73 | } 74 | } 75 | } 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /examples/event_driven_discovery.rs: -------------------------------------------------------------------------------- 1 | // See the "macOS permissions note" in README.md before running this on macOS 2 | // Big Sur or later. 3 | 4 | use btleplug::api::{ 5 | bleuuid::BleUuid, Central, CentralEvent, Manager as _, Peripheral, ScanFilter, 6 | }; 7 | use btleplug::platform::{Adapter, Manager}; 8 | use futures::stream::StreamExt; 9 | 10 | async fn get_central(manager: &Manager) -> Adapter { 11 | let adapters = manager.adapters().await.unwrap(); 12 | adapters.into_iter().nth(0).unwrap() 13 | } 14 | 15 | #[tokio::main] 16 | async fn main() -> anyhow::Result<()> { 17 | pretty_env_logger::init(); 18 | 19 | let manager = Manager::new().await?; 20 | 21 | // get the first bluetooth adapter 22 | // connect to the adapter 23 | let central = get_central(&manager).await; 24 | 25 | let central_state = central.adapter_state().await.unwrap(); 26 | println!("CentralState: {:?}", central_state); 27 | 28 | // Each adapter has an event stream, we fetch via events(), 29 | // simplifying the type, this will return what is essentially a 30 | // Future>>. 31 | let mut events = central.events().await?; 32 | 33 | // start scanning for devices 34 | central.start_scan(ScanFilter::default()).await?; 35 | 36 | // Print based on whatever the event receiver outputs. Note that the event 37 | // receiver blocks, so in a real program, this should be run in its own 38 | // thread (not task, as this library does not yet use async channels). 39 | while let Some(event) = events.next().await { 40 | match event { 41 | CentralEvent::DeviceDiscovered(id) => { 42 | let peripheral = central.peripheral(&id).await?; 43 | let properties = peripheral.properties().await?; 44 | let name = properties 45 | .and_then(|p| p.local_name) 46 | .map(|local_name| format!("Name: {local_name}")) 47 | .unwrap_or_default(); 48 | println!("DeviceDiscovered: {:?} {}", id, name); 49 | } 50 | CentralEvent::StateUpdate(state) => { 51 | println!("AdapterStatusUpdate {:?}", state); 52 | } 53 | CentralEvent::DeviceConnected(id) => { 54 | println!("DeviceConnected: {:?}", id); 55 | } 56 | CentralEvent::DeviceDisconnected(id) => { 57 | println!("DeviceDisconnected: {:?}", id); 58 | } 59 | CentralEvent::ManufacturerDataAdvertisement { 60 | id, 61 | manufacturer_data, 62 | } => { 63 | println!( 64 | "ManufacturerDataAdvertisement: {:?}, {:?}", 65 | id, manufacturer_data 66 | ); 67 | } 68 | CentralEvent::ServiceDataAdvertisement { id, service_data } => { 69 | println!("ServiceDataAdvertisement: {:?}, {:?}", id, service_data); 70 | } 71 | CentralEvent::ServicesAdvertisement { id, services } => { 72 | let services: Vec = 73 | services.into_iter().map(|s| s.to_short_string()).collect(); 74 | println!("ServicesAdvertisement: {:?}, {:?}", id, services); 75 | } 76 | _ => {} 77 | } 78 | } 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /examples/lights.rs: -------------------------------------------------------------------------------- 1 | // See the "macOS permissions note" in README.md before running this on macOS 2 | // Big Sur or later. 3 | 4 | use btleplug::api::{ 5 | bleuuid::uuid_from_u16, Central, Manager as _, Peripheral as _, ScanFilter, WriteType, 6 | }; 7 | use btleplug::platform::{Adapter, Manager, Peripheral}; 8 | use rand::{rng, Rng}; 9 | use std::time::Duration; 10 | use uuid::Uuid; 11 | 12 | const LIGHT_CHARACTERISTIC_UUID: Uuid = uuid_from_u16(0xFFE9); 13 | use tokio::time; 14 | 15 | async fn find_light(central: &Adapter) -> Option { 16 | for p in central.peripherals().await.unwrap() { 17 | if p.properties() 18 | .await 19 | .unwrap() 20 | .unwrap() 21 | .local_name 22 | .iter() 23 | .any(|name| name.contains("LEDBlue")) 24 | { 25 | return Some(p); 26 | } 27 | } 28 | None 29 | } 30 | 31 | #[tokio::main] 32 | async fn main() -> anyhow::Result<()> { 33 | pretty_env_logger::init(); 34 | 35 | let manager = Manager::new().await.unwrap(); 36 | 37 | // get the first bluetooth adapter 38 | let central = manager 39 | .adapters() 40 | .await 41 | .expect("Unable to fetch adapter list.") 42 | .into_iter() 43 | .nth(0) 44 | .expect("Unable to find adapters."); 45 | 46 | // start scanning for devices 47 | central.start_scan(ScanFilter::default()).await?; 48 | // instead of waiting, you can use central.events() to get a stream which will 49 | // notify you of new devices, for an example of that see examples/event_driven_discovery.rs 50 | time::sleep(Duration::from_secs(2)).await; 51 | 52 | // find the device we're interested in 53 | let light = find_light(¢ral).await.expect("No lights found"); 54 | 55 | // connect to the device 56 | light.connect().await?; 57 | 58 | // discover services and characteristics 59 | light.discover_services().await?; 60 | 61 | // find the characteristic we want 62 | let chars = light.characteristics(); 63 | let cmd_char = chars 64 | .iter() 65 | .find(|c| c.uuid == LIGHT_CHARACTERISTIC_UUID) 66 | .expect("Unable to find characterics"); 67 | 68 | // dance party 69 | let mut rng = rng(); 70 | for _ in 0..20 { 71 | let color_cmd = vec![ 72 | 0x56, 73 | rng.random(), 74 | rng.random(), 75 | rng.random(), 76 | 0x00, 77 | 0xF0, 78 | 0xAA, 79 | ]; 80 | light 81 | .write(&cmd_char, &color_cmd, WriteType::WithoutResponse) 82 | .await?; 83 | time::sleep(Duration::from_millis(200)).await; 84 | } 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /examples/subscribe_notify_characteristic.rs: -------------------------------------------------------------------------------- 1 | // See the "macOS permissions note" in README.md before running this on macOS 2 | // Big Sur or later. 3 | 4 | use btleplug::api::{Central, CharPropFlags, Manager as _, Peripheral, ScanFilter}; 5 | use btleplug::platform::Manager; 6 | use futures::stream::StreamExt; 7 | use std::time::Duration; 8 | use tokio::time; 9 | use uuid::Uuid; 10 | 11 | /// Only devices whose name contains this string will be tried. 12 | const PERIPHERAL_NAME_MATCH_FILTER: &str = "Neuro"; 13 | /// UUID of the characteristic for which we should subscribe to notifications. 14 | const NOTIFY_CHARACTERISTIC_UUID: Uuid = Uuid::from_u128(0x6e400002_b534_f393_67a9_e50e24dccA9e); 15 | 16 | #[tokio::main] 17 | async fn main() -> anyhow::Result<()> { 18 | pretty_env_logger::init(); 19 | 20 | let manager = Manager::new().await?; 21 | let adapter_list = manager.adapters().await?; 22 | if adapter_list.is_empty() { 23 | eprintln!("No Bluetooth adapters found"); 24 | } 25 | 26 | for adapter in adapter_list.iter() { 27 | println!("Starting scan..."); 28 | adapter 29 | .start_scan(ScanFilter::default()) 30 | .await 31 | .expect("Can't scan BLE adapter for connected devices..."); 32 | time::sleep(Duration::from_secs(2)).await; 33 | let peripherals = adapter.peripherals().await?; 34 | 35 | if peripherals.is_empty() { 36 | eprintln!("->>> BLE peripheral devices were not found, sorry. Exiting..."); 37 | } else { 38 | // All peripheral devices in range. 39 | for peripheral in peripherals.iter() { 40 | let properties = peripheral.properties().await?; 41 | let is_connected = peripheral.is_connected().await?; 42 | let local_name = properties 43 | .unwrap() 44 | .local_name 45 | .unwrap_or(String::from("(peripheral name unknown)")); 46 | println!( 47 | "Peripheral {:?} is connected: {:?}", 48 | &local_name, is_connected 49 | ); 50 | // Check if it's the peripheral we want. 51 | if local_name.contains(PERIPHERAL_NAME_MATCH_FILTER) { 52 | println!("Found matching peripheral {:?}...", &local_name); 53 | if !is_connected { 54 | // Connect if we aren't already connected. 55 | if let Err(err) = peripheral.connect().await { 56 | eprintln!("Error connecting to peripheral, skipping: {}", err); 57 | continue; 58 | } 59 | } 60 | let is_connected = peripheral.is_connected().await?; 61 | println!( 62 | "Now connected ({:?}) to peripheral {:?}.", 63 | is_connected, &local_name 64 | ); 65 | if is_connected { 66 | println!("Discover peripheral {:?} services...", local_name); 67 | peripheral.discover_services().await?; 68 | for characteristic in peripheral.characteristics() { 69 | println!("Checking characteristic {:?}", characteristic); 70 | // Subscribe to notifications from the characteristic with the selected 71 | // UUID. 72 | if characteristic.uuid == NOTIFY_CHARACTERISTIC_UUID 73 | && characteristic.properties.contains(CharPropFlags::NOTIFY) 74 | { 75 | println!("Subscribing to characteristic {:?}", characteristic.uuid); 76 | peripheral.subscribe(&characteristic).await?; 77 | // Print the first 4 notifications received. 78 | let mut notification_stream = 79 | peripheral.notifications().await?.take(4); 80 | // Process while the BLE connection is not broken or stopped. 81 | while let Some(data) = notification_stream.next().await { 82 | println!( 83 | "Received data from {:?} [{:?}]: {:?}", 84 | local_name, data.uuid, data.value 85 | ); 86 | } 87 | } 88 | } 89 | println!("Disconnecting from peripheral {:?}...", local_name); 90 | peripheral.disconnect().await?; 91 | } 92 | } else { 93 | println!("Skipping unknown peripheral {:?}", peripheral); 94 | } 95 | } 96 | } 97 | } 98 | Ok(()) 99 | } 100 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" -------------------------------------------------------------------------------- /src/api/bleuuid.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for dealing with BLE UUIDs, converting to and from their short formats. 2 | 3 | use uuid::Uuid; 4 | 5 | const BLUETOOTH_BASE_UUID: u128 = 0x00000000_0000_1000_8000_00805f9b34fb; 6 | const BLUETOOTH_BASE_MASK: u128 = 0x00000000_ffff_ffff_ffff_ffffffffffff; 7 | const BLUETOOTH_BASE_MASK_16: u128 = 0xffff0000_ffff_ffff_ffff_ffffffffffff; 8 | 9 | // TODO: Make these functions part of the `BleUuid` trait once const fn is allowed there. 10 | /// Convert a 32-bit BLE short UUID to a full 128-bit UUID by filling in the standard Bluetooth Base 11 | /// UUID. 12 | pub const fn uuid_from_u32(short: u32) -> Uuid { 13 | Uuid::from_u128(BLUETOOTH_BASE_UUID | ((short as u128) << 96)) 14 | } 15 | 16 | /// Convert a 16-bit BLE short UUID to a full 128-bit UUID by filling in the standard Bluetooth Base 17 | /// UUID. 18 | pub const fn uuid_from_u16(short: u16) -> Uuid { 19 | uuid_from_u32(short as u32) 20 | } 21 | 22 | /// An extension trait for `Uuid` which provides BLE-specific methods. 23 | pub trait BleUuid { 24 | /// If the UUID is a valid BLE short UUID then return its short form, otherwise return `None`. 25 | fn to_ble_u32(&self) -> Option; 26 | 27 | /// If the UUID is a valid 16-bit BLE short UUID then return its short form, otherwise return 28 | /// `None`. 29 | fn to_ble_u16(&self) -> Option; 30 | 31 | /// Convert the UUID to a string, using short format if applicable. 32 | fn to_short_string(&self) -> String; 33 | } 34 | 35 | impl BleUuid for Uuid { 36 | fn to_ble_u32(&self) -> Option { 37 | let value = self.as_u128(); 38 | if value & BLUETOOTH_BASE_MASK == BLUETOOTH_BASE_UUID { 39 | Some((value >> 96) as u32) 40 | } else { 41 | None 42 | } 43 | } 44 | 45 | fn to_ble_u16(&self) -> Option { 46 | let value = self.as_u128(); 47 | if value & BLUETOOTH_BASE_MASK_16 == BLUETOOTH_BASE_UUID { 48 | Some((value >> 96) as u16) 49 | } else { 50 | None 51 | } 52 | } 53 | 54 | fn to_short_string(&self) -> String { 55 | if let Some(uuid16) = self.to_ble_u16() { 56 | format!("{:#04x}", uuid16) 57 | } else if let Some(uuid32) = self.to_ble_u32() { 58 | format!("{:#06x}", uuid32) 59 | } else { 60 | self.to_string() 61 | } 62 | } 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use super::*; 68 | 69 | #[test] 70 | fn uuid_from_u32_test() { 71 | assert_eq!( 72 | uuid_from_u32(0x11223344), 73 | Uuid::parse_str("11223344-0000-1000-8000-00805f9b34fb").unwrap() 74 | ); 75 | } 76 | 77 | #[test] 78 | fn uuid_from_u16_test() { 79 | assert_eq!( 80 | uuid_from_u16(0x1122), 81 | Uuid::parse_str("00001122-0000-1000-8000-00805f9b34fb").unwrap() 82 | ); 83 | } 84 | 85 | #[test] 86 | fn uuid_to_from_u16_success() { 87 | let uuid = Uuid::parse_str("00001234-0000-1000-8000-00805f9b34fb").unwrap(); 88 | assert_eq!(uuid_from_u16(uuid.to_ble_u16().unwrap()), uuid); 89 | } 90 | 91 | #[test] 92 | fn uuid_to_from_u32_success() { 93 | let uuid = Uuid::parse_str("12345678-0000-1000-8000-00805f9b34fb").unwrap(); 94 | assert_eq!(uuid_from_u32(uuid.to_ble_u32().unwrap()), uuid); 95 | } 96 | 97 | #[test] 98 | fn uuid_to_u16_fail() { 99 | assert_eq!( 100 | Uuid::parse_str("12345678-0000-1000-8000-00805f9b34fb") 101 | .unwrap() 102 | .to_ble_u16(), 103 | None 104 | ); 105 | assert_eq!( 106 | Uuid::parse_str("12340000-0000-1000-8000-00805f9b34fb") 107 | .unwrap() 108 | .to_ble_u16(), 109 | None 110 | ); 111 | assert_eq!(Uuid::nil().to_ble_u16(), None); 112 | } 113 | 114 | #[test] 115 | fn uuid_to_u32_fail() { 116 | assert_eq!( 117 | Uuid::parse_str("12345678-9000-1000-8000-00805f9b34fb") 118 | .unwrap() 119 | .to_ble_u32(), 120 | None 121 | ); 122 | assert_eq!(Uuid::nil().to_ble_u32(), None); 123 | } 124 | 125 | #[test] 126 | fn to_short_string_u16() { 127 | let uuid = uuid_from_u16(0x1122); 128 | assert_eq!(uuid.to_short_string(), "0x1122"); 129 | } 130 | 131 | #[test] 132 | fn to_short_string_u32() { 133 | let uuid = uuid_from_u32(0x11223344); 134 | assert_eq!(uuid.to_short_string(), "0x11223344"); 135 | } 136 | 137 | #[test] 138 | fn to_short_string_long() { 139 | let uuid_str = "12345678-9000-1000-8000-00805f9b34fb"; 140 | let uuid = Uuid::parse_str(uuid_str).unwrap(); 141 | assert_eq!(uuid.to_short_string(), uuid_str); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | //! The `api` module contains the traits and types which make up btleplug's API. These traits have a 15 | //! different implementation for each supported platform, but only one implementation can be found 16 | //! on any given platform. These implementations are in the [`platform`](crate::platform) module. 17 | //! 18 | //! You will may want to import both the traits and their implementations, like: 19 | //! ``` 20 | //! use btleplug::api::{Central, Manager as _, Peripheral as _}; 21 | //! use btleplug::platform::{Adapter, Manager, Peripheral}; 22 | //! ``` 23 | 24 | pub(crate) mod bdaddr; 25 | pub mod bleuuid; 26 | 27 | use crate::Result; 28 | use async_trait::async_trait; 29 | use bitflags::bitflags; 30 | use futures::stream::Stream; 31 | #[cfg(feature = "serde")] 32 | use serde::{Deserialize, Serialize}; 33 | #[cfg(feature = "serde")] 34 | use serde_cr as serde; 35 | use std::{ 36 | collections::{BTreeSet, HashMap}, 37 | fmt::{self, Debug, Display, Formatter}, 38 | pin::Pin, 39 | }; 40 | use uuid::Uuid; 41 | 42 | pub use self::bdaddr::{BDAddr, ParseBDAddrError}; 43 | 44 | use crate::platform::PeripheralId; 45 | 46 | #[cfg_attr( 47 | feature = "serde", 48 | derive(Serialize, Deserialize), 49 | serde(crate = "serde_cr") 50 | )] 51 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Default)] 52 | pub enum AddressType { 53 | Random, 54 | #[default] 55 | Public, 56 | } 57 | 58 | impl AddressType { 59 | #[allow(clippy::should_implement_trait)] 60 | pub fn from_str(v: &str) -> Option { 61 | match v { 62 | "public" => Some(AddressType::Public), 63 | "random" => Some(AddressType::Random), 64 | _ => None, 65 | } 66 | } 67 | 68 | pub fn from_u8(v: u8) -> Option { 69 | match v { 70 | 1 => Some(AddressType::Public), 71 | 2 => Some(AddressType::Random), 72 | _ => None, 73 | } 74 | } 75 | 76 | pub fn num(&self) -> u8 { 77 | match *self { 78 | AddressType::Public => 1, 79 | AddressType::Random => 2, 80 | } 81 | } 82 | } 83 | 84 | /// A notification sent from a peripheral due to a change in a value. 85 | #[derive(Clone, Debug, Eq, PartialEq)] 86 | pub struct ValueNotification { 87 | /// UUID of the characteristic that fired the notification. 88 | pub uuid: Uuid, 89 | /// The new value of the characteristic. 90 | pub value: Vec, 91 | } 92 | 93 | bitflags! { 94 | /// A set of properties that indicate what operations are supported by a Characteristic. 95 | #[derive(Default, Debug, PartialEq, Eq, Ord, PartialOrd, Clone, Copy)] 96 | pub struct CharPropFlags: u8 { 97 | const BROADCAST = 0x01; 98 | const READ = 0x02; 99 | const WRITE_WITHOUT_RESPONSE = 0x04; 100 | const WRITE = 0x08; 101 | const NOTIFY = 0x10; 102 | const INDICATE = 0x20; 103 | const AUTHENTICATED_SIGNED_WRITES = 0x40; 104 | const EXTENDED_PROPERTIES = 0x80; 105 | } 106 | } 107 | 108 | /// A GATT service. Services are groups of characteristics, which may be standard or 109 | /// device-specific. 110 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] 111 | pub struct Service { 112 | /// The UUID for this service. 113 | pub uuid: Uuid, 114 | /// Whether this is a primary service. 115 | pub primary: bool, 116 | /// The characteristics of this service. 117 | pub characteristics: BTreeSet, 118 | } 119 | 120 | /// A Bluetooth characteristic. Characteristics are the main way you will interact with other 121 | /// bluetooth devices. Characteristics are identified by a UUID which may be standardized 122 | /// (like 0x2803, which identifies a characteristic for reading heart rate measurements) but more 123 | /// often are specific to a particular device. The standard set of characteristics can be found 124 | /// [here](https://www.bluetooth.com/specifications/gatt/characteristics). 125 | /// 126 | /// A characteristic may be interacted with in various ways depending on its properties. You may be 127 | /// able to write to it, read from it, set its notify or indicate status, or send a command to it. 128 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] 129 | pub struct Characteristic { 130 | /// The UUID for this characteristic. This uniquely identifies its behavior. 131 | pub uuid: Uuid, 132 | /// The UUID of the service this characteristic belongs to. 133 | pub service_uuid: Uuid, 134 | /// The set of properties for this characteristic, which indicate what functionality it 135 | /// supports. If you attempt an operation that is not supported by the characteristics (for 136 | /// example setting notify on one without the NOTIFY flag), that operation will fail. 137 | pub properties: CharPropFlags, 138 | /// The descriptors of this characteristic. 139 | pub descriptors: BTreeSet, 140 | } 141 | 142 | impl Display for Characteristic { 143 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 144 | write!( 145 | f, 146 | "uuid: {:?}, char properties: {:?}", 147 | self.uuid, self.properties 148 | ) 149 | } 150 | } 151 | 152 | /// Add doc 153 | #[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone)] 154 | pub struct Descriptor { 155 | /// The UUID for this descriptor. This uniquely identifies its behavior. 156 | pub uuid: Uuid, 157 | /// The UUID of the service this descriptor belongs to. 158 | pub service_uuid: Uuid, 159 | /// The UUID of the characteristic this descriptor belongs to. 160 | pub characteristic_uuid: Uuid, 161 | } 162 | 163 | impl Display for Descriptor { 164 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 165 | write!(f, "uuid: {:?}", self.uuid) 166 | } 167 | } 168 | 169 | /// The properties of this peripheral, as determined by the advertising reports we've received for 170 | /// it. 171 | #[cfg_attr( 172 | feature = "serde", 173 | derive(Serialize, Deserialize), 174 | serde(crate = "serde_cr") 175 | )] 176 | #[derive(Debug, Default, Clone)] 177 | pub struct PeripheralProperties { 178 | /// The address of this peripheral 179 | pub address: BDAddr, 180 | /// The type of address (either random or public) 181 | pub address_type: Option, 182 | /// The local name. This is generally a human-readable string that identifies the type of device. 183 | pub local_name: Option, 184 | /// The transmission power level for the device 185 | pub tx_power_level: Option, 186 | /// The most recent Received Signal Strength Indicator for the device 187 | pub rssi: Option, 188 | /// Advertisement data specific to the device manufacturer. The keys of this map are 189 | /// 'manufacturer IDs', while the values are arbitrary data. 190 | pub manufacturer_data: HashMap>, 191 | /// Advertisement data specific to a service. The keys of this map are 192 | /// 'Service UUIDs', while the values are arbitrary data. 193 | pub service_data: HashMap>, 194 | /// Advertised services for this device 195 | pub services: Vec, 196 | pub class: Option, 197 | } 198 | 199 | #[cfg_attr( 200 | feature = "serde", 201 | derive(Serialize, Deserialize), 202 | serde(crate = "serde_cr") 203 | )] 204 | /// The filter used when scanning for BLE devices. 205 | #[derive(Clone, Debug, Default, Eq, PartialEq)] 206 | pub struct ScanFilter { 207 | /// If the filter contains at least one service UUID, only devices supporting at least one of 208 | /// the given services will be available. 209 | pub services: Vec, 210 | } 211 | 212 | /// The type of write operation to use. 213 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 214 | pub enum WriteType { 215 | /// A write operation where the device is expected to respond with a confirmation or error. Also 216 | /// known as a request. 217 | WithResponse, 218 | /// A write-without-response, also known as a command. 219 | WithoutResponse, 220 | } 221 | 222 | /// Peripheral is the device that you would like to communicate with (the "server" of BLE). This 223 | /// struct contains both the current state of the device (its properties, characteristics, etc.) 224 | /// as well as functions for communication. 225 | #[async_trait] 226 | pub trait Peripheral: Send + Sync + Clone + Debug { 227 | /// Returns the unique identifier of the peripheral. 228 | fn id(&self) -> PeripheralId; 229 | 230 | /// Returns the MAC address of the peripheral. 231 | fn address(&self) -> BDAddr; 232 | 233 | /// Returns the set of properties associated with the peripheral. These may be updated over time 234 | /// as additional advertising reports are received. 235 | async fn properties(&self) -> Result>; 236 | 237 | /// The set of services we've discovered for this device. This will be empty until 238 | /// `discover_services` is called. 239 | fn services(&self) -> BTreeSet; 240 | 241 | /// The set of characteristics we've discovered for this device. This will be empty until 242 | /// `discover_services` is called. 243 | fn characteristics(&self) -> BTreeSet { 244 | self.services() 245 | .iter() 246 | .flat_map(|service| service.characteristics.clone().into_iter()) 247 | .collect() 248 | } 249 | 250 | /// Returns true iff we are currently connected to the device. 251 | async fn is_connected(&self) -> Result; 252 | 253 | /// Creates a connection to the device. If this method returns Ok there has been successful 254 | /// connection. Note that peripherals allow only one connection at a time. Operations that 255 | /// attempt to communicate with a device will fail until it is connected. 256 | async fn connect(&self) -> Result<()>; 257 | 258 | /// Terminates a connection to the device. 259 | async fn disconnect(&self) -> Result<()>; 260 | 261 | /// Discovers all services for the device, including their characteristics. 262 | async fn discover_services(&self) -> Result<()>; 263 | 264 | /// Write some data to the characteristic. Returns an error if the write couldn't be sent or (in 265 | /// the case of a write-with-response) if the device returns an error. 266 | async fn write( 267 | &self, 268 | characteristic: &Characteristic, 269 | data: &[u8], 270 | write_type: WriteType, 271 | ) -> Result<()>; 272 | 273 | /// Sends a read request to the device. Returns either an error if the request was not accepted 274 | /// or the response from the device. 275 | async fn read(&self, characteristic: &Characteristic) -> Result>; 276 | 277 | /// Enables either notify or indicate (depending on support) for the specified characteristic. 278 | async fn subscribe(&self, characteristic: &Characteristic) -> Result<()>; 279 | 280 | /// Disables either notify or indicate (depending on support) for the specified characteristic. 281 | async fn unsubscribe(&self, characteristic: &Characteristic) -> Result<()>; 282 | 283 | /// Returns a stream of notifications for characteristic value updates. The stream will receive 284 | /// a notification when a value notification or indication is received from the device. 285 | /// The stream will remain valid across connections and can be queried before any connection 286 | /// is made. 287 | async fn notifications(&self) -> Result + Send>>>; 288 | 289 | /// Write some data to the descriptor. Returns an error if the write couldn't be sent or (in 290 | /// the case of a write-with-response) if the device returns an error. 291 | async fn write_descriptor(&self, descriptor: &Descriptor, data: &[u8]) -> Result<()>; 292 | 293 | /// Sends a read descriptor request to the device. Returns either an error if the request 294 | /// was not accepted or the response from the device. 295 | async fn read_descriptor(&self, descriptor: &Descriptor) -> Result>; 296 | } 297 | 298 | #[cfg_attr( 299 | feature = "serde", 300 | derive(Serialize, Deserialize), 301 | serde(crate = "serde_cr") 302 | )] 303 | /// The state of the Central 304 | #[derive(Clone, Debug, Eq, PartialEq)] 305 | pub enum CentralState { 306 | Unknown = 0, 307 | PoweredOn = 1, 308 | PoweredOff = 2, 309 | } 310 | 311 | #[cfg_attr( 312 | feature = "serde", 313 | derive(Serialize, Deserialize), 314 | serde(crate = "serde_cr") 315 | )] 316 | #[derive(Debug, Clone)] 317 | pub enum CentralEvent { 318 | DeviceDiscovered(PeripheralId), 319 | DeviceUpdated(PeripheralId), 320 | DeviceConnected(PeripheralId), 321 | DeviceDisconnected(PeripheralId), 322 | /// Emitted when a Manufacturer Data advertisement has been received from a device 323 | ManufacturerDataAdvertisement { 324 | id: PeripheralId, 325 | manufacturer_data: HashMap>, 326 | }, 327 | /// Emitted when a Service Data advertisement has been received from a device 328 | ServiceDataAdvertisement { 329 | id: PeripheralId, 330 | service_data: HashMap>, 331 | }, 332 | /// Emitted when the advertised services for a device has been updated 333 | ServicesAdvertisement { 334 | id: PeripheralId, 335 | services: Vec, 336 | }, 337 | StateUpdate(CentralState), 338 | } 339 | 340 | /// Central is the "client" of BLE. It's able to scan for and establish connections to peripherals. 341 | /// A Central can be obtained from [`Manager::adapters()`]. 342 | #[async_trait] 343 | pub trait Central: Send + Sync + Clone { 344 | type Peripheral: Peripheral; 345 | 346 | /// Retrieve a stream of `CentralEvent`s. This stream will receive notifications when events 347 | /// occur for this Central module. See [`CentralEvent`] for the full set of possible events. 348 | async fn events(&self) -> Result + Send>>>; 349 | 350 | /// Starts a scan for BLE devices. This scan will generally continue until explicitly stopped, 351 | /// although this may depend on your Bluetooth adapter. Discovered devices will be announced 352 | /// to subscribers of `events` and will be available via `peripherals()`. 353 | /// The filter can be used to scan only for specific devices. While some implementations might 354 | /// ignore (parts of) the filter and make additional devices available, other implementations 355 | /// might require at least one filter for security reasons. Cross-platform code should provide 356 | /// a filter, but must be able to handle devices, which do not fit into the filter. 357 | async fn start_scan(&self, filter: ScanFilter) -> Result<()>; 358 | 359 | /// Stops scanning for BLE devices. 360 | async fn stop_scan(&self) -> Result<()>; 361 | 362 | /// Returns the list of [`Peripheral`]s that have been discovered so far. Note that this list 363 | /// may contain peripherals that are no longer available. 364 | async fn peripherals(&self) -> Result>; 365 | 366 | /// Returns a particular [`Peripheral`] by its address if it has been discovered. 367 | async fn peripheral(&self, id: &PeripheralId) -> Result; 368 | 369 | /// Add a [`Peripheral`] from a MAC address without a scan result. Not supported on all Bluetooth systems. 370 | async fn add_peripheral(&self, address: &PeripheralId) -> Result; 371 | 372 | /// Get information about the Bluetooth adapter being used, such as the model or type. 373 | /// 374 | /// The details of this are platform-specific andyou should not attempt to parse it, but it may 375 | /// be useful for debug logs. 376 | async fn adapter_info(&self) -> Result; 377 | 378 | /// Get information about the Bluetooth adapter state. 379 | async fn adapter_state(&self) -> Result; 380 | } 381 | 382 | /// The Manager is the entry point to the library, providing access to all the Bluetooth adapters on 383 | /// the system. You can obtain an instance from [`platform::Manager::new()`](crate::platform::Manager::new). 384 | /// 385 | /// ## Usage 386 | /// ``` 387 | /// use btleplug::api::Manager as _; 388 | /// use btleplug::platform::Manager; 389 | /// # use std::error::Error; 390 | /// 391 | /// # async fn example() -> Result<(), Box> { 392 | /// let manager = Manager::new().await?; 393 | /// let adapter_list = manager.adapters().await?; 394 | /// if adapter_list.is_empty() { 395 | /// eprintln!("No Bluetooth adapters"); 396 | /// } 397 | /// # Ok(()) 398 | /// # } 399 | /// ``` 400 | #[async_trait] 401 | pub trait Manager { 402 | /// The concrete type of the [`Central`] implementation. 403 | type Adapter: Central; 404 | 405 | /// Get a list of all Bluetooth adapters on the system. Each adapter implements [`Central`]. 406 | async fn adapters(&self) -> Result>; 407 | } 408 | -------------------------------------------------------------------------------- /src/bluez/adapter.rs: -------------------------------------------------------------------------------- 1 | use super::peripheral::{Peripheral, PeripheralId}; 2 | use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; 3 | use crate::{Error, Result}; 4 | use async_trait::async_trait; 5 | use bluez_async::{ 6 | AdapterEvent, AdapterId, BluetoothError, BluetoothEvent, BluetoothSession, DeviceEvent, 7 | DiscoveryFilter, Transport, 8 | }; 9 | use futures::stream::{self, Stream, StreamExt}; 10 | use std::pin::Pin; 11 | 12 | /// Implementation of [api::Central](crate::api::Central). 13 | #[derive(Clone, Debug)] 14 | pub struct Adapter { 15 | session: BluetoothSession, 16 | adapter: AdapterId, 17 | } 18 | 19 | impl Adapter { 20 | pub(crate) fn new(session: BluetoothSession, adapter: AdapterId) -> Self { 21 | Self { session, adapter } 22 | } 23 | } 24 | 25 | fn get_central_state(powered: bool) -> CentralState { 26 | match powered { 27 | true => CentralState::PoweredOn, 28 | false => CentralState::PoweredOff, 29 | } 30 | } 31 | 32 | #[async_trait] 33 | impl Central for Adapter { 34 | type Peripheral = Peripheral; 35 | 36 | async fn events(&self) -> Result + Send>>> { 37 | // There's a race between getting this event stream and getting the current set of devices. 38 | // Get the stream first, on the basis that it's better to have a duplicate DeviceDiscovered 39 | // event than to miss one. It's unlikely to happen in any case. 40 | let events = self.session.adapter_event_stream(&self.adapter).await?; 41 | 42 | // Synthesise `DeviceDiscovered' and `DeviceConnected` events for existing peripherals. 43 | let devices = self.session.get_devices().await?; 44 | let adapter_id = self.adapter.clone(); 45 | let initial_events = stream::iter( 46 | devices 47 | .into_iter() 48 | .filter(move |device| device.id.adapter() == adapter_id) 49 | .flat_map(|device| { 50 | let mut events = vec![CentralEvent::DeviceDiscovered(device.id.clone().into())]; 51 | if device.connected { 52 | events.push(CentralEvent::DeviceConnected(device.id.into())); 53 | } 54 | events.into_iter() 55 | }), 56 | ); 57 | 58 | let session = self.session.clone(); 59 | let adapter_id = self.adapter.clone(); 60 | let events = events 61 | .filter_map(move |event| central_event(event, session.clone(), adapter_id.clone())); 62 | 63 | Ok(Box::pin(initial_events.chain(events))) 64 | } 65 | 66 | async fn start_scan(&self, filter: ScanFilter) -> Result<()> { 67 | let filter = DiscoveryFilter { 68 | service_uuids: filter.services, 69 | duplicate_data: Some(true), 70 | transport: Some(Transport::Auto), 71 | ..Default::default() 72 | }; 73 | self.session 74 | .start_discovery_on_adapter_with_filter(&self.adapter, &filter) 75 | .await?; 76 | Ok(()) 77 | } 78 | 79 | async fn stop_scan(&self) -> Result<()> { 80 | self.session 81 | .stop_discovery_on_adapter(&self.adapter) 82 | .await?; 83 | Ok(()) 84 | } 85 | 86 | async fn peripherals(&self) -> Result> { 87 | let devices = self.session.get_devices_on_adapter(&self.adapter).await?; 88 | Ok(devices 89 | .into_iter() 90 | .map(|device| Peripheral::new(self.session.clone(), device)) 91 | .collect()) 92 | } 93 | 94 | async fn peripheral(&self, id: &PeripheralId) -> Result { 95 | let device = self.session.get_device_info(&id.0).await.map_err(|e| { 96 | if let BluetoothError::DbusError(_) = e { 97 | Error::DeviceNotFound 98 | } else { 99 | e.into() 100 | } 101 | })?; 102 | Ok(Peripheral::new(self.session.clone(), device)) 103 | } 104 | 105 | async fn add_peripheral(&self, _address: &PeripheralId) -> Result { 106 | Err(Error::NotSupported( 107 | "Can't add a Peripheral from a PeripheralId".to_string(), 108 | )) 109 | } 110 | 111 | async fn adapter_info(&self) -> Result { 112 | let adapter_info = self.session.get_adapter_info(&self.adapter).await?; 113 | Ok(format!("{} ({})", adapter_info.id, adapter_info.modalias)) 114 | } 115 | 116 | async fn adapter_state(&self) -> Result { 117 | let mut powered = false; 118 | if let Ok(info) = self.session.get_adapter_info(&self.adapter).await { 119 | powered = info.powered; 120 | } 121 | Ok(get_central_state(powered)) 122 | } 123 | } 124 | 125 | impl From for Error { 126 | fn from(error: BluetoothError) -> Self { 127 | Error::Other(Box::new(error)) 128 | } 129 | } 130 | 131 | async fn central_event( 132 | event: BluetoothEvent, 133 | session: BluetoothSession, 134 | adapter_id: AdapterId, 135 | ) -> Option { 136 | match event { 137 | BluetoothEvent::Device { 138 | id, 139 | event: device_event, 140 | } if id.adapter() == adapter_id => match device_event { 141 | DeviceEvent::Discovered => { 142 | let device = session.get_device_info(&id).await.ok()?; 143 | Some(CentralEvent::DeviceDiscovered(device.id.into())) 144 | } 145 | DeviceEvent::Connected { connected } => { 146 | let device = session.get_device_info(&id).await.ok()?; 147 | if connected { 148 | Some(CentralEvent::DeviceConnected(device.id.into())) 149 | } else { 150 | Some(CentralEvent::DeviceDisconnected(device.id.into())) 151 | } 152 | } 153 | DeviceEvent::Rssi { rssi: _ } => { 154 | let device = session.get_device_info(&id).await.ok()?; 155 | Some(CentralEvent::DeviceUpdated(device.id.into())) 156 | } 157 | DeviceEvent::ManufacturerData { manufacturer_data } => { 158 | let device = session.get_device_info(&id).await.ok()?; 159 | Some(CentralEvent::ManufacturerDataAdvertisement { 160 | id: device.id.into(), 161 | manufacturer_data, 162 | }) 163 | } 164 | DeviceEvent::ServiceData { service_data } => { 165 | let device = session.get_device_info(&id).await.ok()?; 166 | Some(CentralEvent::ServiceDataAdvertisement { 167 | id: device.id.into(), 168 | service_data, 169 | }) 170 | } 171 | DeviceEvent::Services { services } => { 172 | let device = session.get_device_info(&id).await.ok()?; 173 | Some(CentralEvent::ServicesAdvertisement { 174 | id: device.id.into(), 175 | services, 176 | }) 177 | } 178 | _ => None, 179 | }, 180 | BluetoothEvent::Adapter { 181 | id, 182 | event: adapter_event, 183 | } if id == adapter_id => match adapter_event { 184 | AdapterEvent::Powered { powered } => { 185 | let state = get_central_state(powered); 186 | Some(CentralEvent::StateUpdate(state)) 187 | } 188 | _ => None, 189 | }, 190 | _ => None, 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/bluez/manager.rs: -------------------------------------------------------------------------------- 1 | use super::adapter::Adapter; 2 | use crate::{api, Result}; 3 | use async_trait::async_trait; 4 | use bluez_async::BluetoothSession; 5 | 6 | /// Implementation of [api::Manager](crate::api::Manager). 7 | #[derive(Clone, Debug)] 8 | pub struct Manager { 9 | session: BluetoothSession, 10 | } 11 | 12 | impl Manager { 13 | pub async fn new() -> Result { 14 | let (_, session) = BluetoothSession::new().await?; 15 | Ok(Self { session }) 16 | } 17 | } 18 | 19 | #[async_trait] 20 | impl api::Manager for Manager { 21 | type Adapter = Adapter; 22 | 23 | async fn adapters(&self) -> Result> { 24 | let adapters = self.session.get_adapters().await?; 25 | Ok(adapters 26 | .into_iter() 27 | .map(|adapter| Adapter::new(self.session.clone(), adapter.id)) 28 | .collect()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/bluez/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod adapter; 2 | pub mod manager; 3 | pub mod peripheral; 4 | -------------------------------------------------------------------------------- /src/bluez/peripheral.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use bluez_async::{ 3 | BluetoothEvent, BluetoothSession, CharacteristicEvent, CharacteristicFlags, CharacteristicId, 4 | CharacteristicInfo, DescriptorInfo, DeviceId, DeviceInfo, MacAddress, ServiceInfo, 5 | WriteOptions, 6 | }; 7 | use futures::future::{join_all, ready}; 8 | use futures::stream::{Stream, StreamExt}; 9 | #[cfg(feature = "serde")] 10 | use serde::{Deserialize, Serialize}; 11 | #[cfg(feature = "serde")] 12 | use serde_cr as serde; 13 | use std::collections::{BTreeSet, HashMap}; 14 | use std::fmt::{self, Display, Formatter}; 15 | use std::pin::Pin; 16 | use std::sync::{Arc, Mutex}; 17 | use uuid::Uuid; 18 | 19 | use crate::api::{ 20 | self, AddressType, BDAddr, CharPropFlags, Characteristic, Descriptor, PeripheralProperties, 21 | Service, ValueNotification, WriteType, 22 | }; 23 | use crate::{Error, Result}; 24 | 25 | #[derive(Clone, Debug)] 26 | struct CharacteristicInternal { 27 | info: CharacteristicInfo, 28 | descriptors: HashMap, 29 | } 30 | 31 | impl CharacteristicInternal { 32 | fn new(info: CharacteristicInfo, descriptors: HashMap) -> Self { 33 | Self { info, descriptors } 34 | } 35 | } 36 | 37 | #[derive(Clone, Debug)] 38 | struct ServiceInternal { 39 | info: ServiceInfo, 40 | characteristics: HashMap, 41 | } 42 | 43 | #[cfg_attr( 44 | feature = "serde", 45 | derive(Serialize, Deserialize), 46 | serde(crate = "serde_cr") 47 | )] 48 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 49 | pub struct PeripheralId(pub(crate) DeviceId); 50 | 51 | impl Display for PeripheralId { 52 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 53 | self.0.fmt(f) 54 | } 55 | } 56 | 57 | /// Implementation of [api::Peripheral](crate::api::Peripheral). 58 | #[derive(Clone, Debug)] 59 | pub struct Peripheral { 60 | session: BluetoothSession, 61 | device: DeviceId, 62 | mac_address: BDAddr, 63 | services: Arc>>, 64 | } 65 | 66 | fn get_characteristic<'a>( 67 | services: &'a HashMap, 68 | service_uuid: &Uuid, 69 | characteristic_uuid: &Uuid, 70 | ) -> Result<&'a CharacteristicInternal> { 71 | services 72 | .get(service_uuid) 73 | .ok_or_else(|| { 74 | Error::Other(format!("Service with UUID {} not found.", service_uuid).into()) 75 | })? 76 | .characteristics 77 | .get(characteristic_uuid) 78 | .ok_or_else(|| { 79 | Error::Other( 80 | format!( 81 | "Characteristic with UUID {} not found.", 82 | characteristic_uuid 83 | ) 84 | .into(), 85 | ) 86 | }) 87 | } 88 | 89 | impl Peripheral { 90 | pub(crate) fn new(session: BluetoothSession, device: DeviceInfo) -> Self { 91 | Peripheral { 92 | session, 93 | device: device.id, 94 | mac_address: device.mac_address.into(), 95 | services: Arc::new(Mutex::new(HashMap::new())), 96 | } 97 | } 98 | 99 | fn characteristic_info(&self, characteristic: &Characteristic) -> Result { 100 | let services = self.services.lock().map_err(Into::::into)?; 101 | get_characteristic( 102 | &services, 103 | &characteristic.service_uuid, 104 | &characteristic.uuid, 105 | ) 106 | .map(|c| &c.info) 107 | .cloned() 108 | } 109 | 110 | fn descriptor_info(&self, descriptor: &Descriptor) -> Result { 111 | let services = self.services.lock().map_err(Into::::into)?; 112 | let characteristic = get_characteristic( 113 | &services, 114 | &descriptor.service_uuid, 115 | &descriptor.characteristic_uuid, 116 | )?; 117 | characteristic 118 | .descriptors 119 | .get(&descriptor.uuid) 120 | .ok_or_else(|| { 121 | Error::Other(format!("Descriptor with UUID {} not found.", descriptor.uuid).into()) 122 | }) 123 | .cloned() 124 | } 125 | 126 | async fn device_info(&self) -> Result { 127 | Ok(self.session.get_device_info(&self.device).await?) 128 | } 129 | } 130 | 131 | #[async_trait] 132 | impl api::Peripheral for Peripheral { 133 | fn id(&self) -> PeripheralId { 134 | PeripheralId(self.device.to_owned()) 135 | } 136 | 137 | fn address(&self) -> BDAddr { 138 | self.mac_address 139 | } 140 | 141 | async fn properties(&self) -> Result> { 142 | let device_info = self.device_info().await?; 143 | Ok(Some(PeripheralProperties { 144 | address: device_info.mac_address.into(), 145 | address_type: Some(device_info.address_type.into()), 146 | local_name: device_info.name, 147 | tx_power_level: device_info.tx_power, 148 | rssi: device_info.rssi, 149 | manufacturer_data: device_info.manufacturer_data, 150 | service_data: device_info.service_data, 151 | services: device_info.services, 152 | class: device_info.class, 153 | })) 154 | } 155 | 156 | fn services(&self) -> BTreeSet { 157 | self.services 158 | .lock() 159 | .unwrap() 160 | .values() 161 | .map(|service| service.into()) 162 | .collect() 163 | } 164 | 165 | async fn is_connected(&self) -> Result { 166 | let device_info = self.device_info().await?; 167 | Ok(device_info.connected) 168 | } 169 | 170 | async fn connect(&self) -> Result<()> { 171 | self.session.connect(&self.device).await?; 172 | Ok(()) 173 | } 174 | 175 | async fn disconnect(&self) -> Result<()> { 176 | self.session.disconnect(&self.device).await?; 177 | Ok(()) 178 | } 179 | 180 | async fn discover_services(&self) -> Result<()> { 181 | let mut services_internal = HashMap::new(); 182 | let services = self.session.get_services(&self.device).await?; 183 | for service in services { 184 | let characteristics = self.session.get_characteristics(&service.id).await?; 185 | let characteristics = join_all( 186 | characteristics 187 | .into_iter() 188 | .fold( 189 | // Only consider the first characteristic of each UUID 190 | // This "should" be unique, but of course it's not enforced 191 | HashMap::::new(), 192 | |mut map, characteristic| { 193 | if !map.contains_key(&characteristic.uuid) { 194 | map.insert(characteristic.uuid, characteristic); 195 | } 196 | map 197 | }, 198 | ) 199 | .into_iter() 200 | .map(|mapped_characteristic| async { 201 | let characteristic = mapped_characteristic.1; 202 | let descriptors = self 203 | .session 204 | .get_descriptors(&characteristic.id) 205 | .await 206 | .unwrap_or(Vec::new()) 207 | .into_iter() 208 | .map(|descriptor| (descriptor.uuid, descriptor)) 209 | .collect(); 210 | CharacteristicInternal::new(characteristic, descriptors) 211 | }), 212 | ) 213 | .await; 214 | services_internal.insert( 215 | service.uuid, 216 | ServiceInternal { 217 | info: service, 218 | characteristics: characteristics 219 | .into_iter() 220 | .map(|characteristic| (characteristic.info.uuid, characteristic)) 221 | .collect(), 222 | }, 223 | ); 224 | } 225 | *(self.services.lock().map_err(Into::::into)?) = services_internal; 226 | Ok(()) 227 | } 228 | 229 | async fn write( 230 | &self, 231 | characteristic: &Characteristic, 232 | data: &[u8], 233 | write_type: WriteType, 234 | ) -> Result<()> { 235 | let characteristic_info = self.characteristic_info(characteristic)?; 236 | let options = WriteOptions { 237 | write_type: Some(write_type.into()), 238 | ..Default::default() 239 | }; 240 | Ok(self 241 | .session 242 | .write_characteristic_value_with_options(&characteristic_info.id, data, options) 243 | .await?) 244 | } 245 | 246 | async fn read(&self, characteristic: &Characteristic) -> Result> { 247 | let characteristic_info = self.characteristic_info(characteristic)?; 248 | Ok(self 249 | .session 250 | .read_characteristic_value(&characteristic_info.id) 251 | .await?) 252 | } 253 | 254 | async fn subscribe(&self, characteristic: &Characteristic) -> Result<()> { 255 | let characteristic_info = self.characteristic_info(characteristic)?; 256 | Ok(self.session.start_notify(&characteristic_info.id).await?) 257 | } 258 | 259 | async fn unsubscribe(&self, characteristic: &Characteristic) -> Result<()> { 260 | let characteristic_info = self.characteristic_info(characteristic)?; 261 | Ok(self.session.stop_notify(&characteristic_info.id).await?) 262 | } 263 | 264 | async fn notifications(&self) -> Result + Send>>> { 265 | let device_id = self.device.clone(); 266 | let events = self.session.device_event_stream(&device_id).await?; 267 | let services = self.services.clone(); 268 | Ok(Box::pin(events.filter_map(move |event| { 269 | ready(value_notification(event, &device_id, services.clone())) 270 | }))) 271 | } 272 | 273 | async fn write_descriptor(&self, descriptor: &Descriptor, data: &[u8]) -> Result<()> { 274 | let descriptor_info = self.descriptor_info(descriptor)?; 275 | Ok(self 276 | .session 277 | .write_descriptor_value(&descriptor_info.id, data) 278 | .await?) 279 | } 280 | 281 | async fn read_descriptor(&self, descriptor: &Descriptor) -> Result> { 282 | let descriptor_info = self.descriptor_info(descriptor)?; 283 | Ok(self 284 | .session 285 | .read_descriptor_value(&descriptor_info.id) 286 | .await?) 287 | } 288 | } 289 | 290 | fn value_notification( 291 | event: BluetoothEvent, 292 | device_id: &DeviceId, 293 | services: Arc>>, 294 | ) -> Option { 295 | match event { 296 | BluetoothEvent::Characteristic { 297 | id, 298 | event: CharacteristicEvent::Value { value }, 299 | } if id.service().device() == *device_id => { 300 | let services = services.lock().unwrap(); 301 | let uuid = find_characteristic_by_id(&services, id)?.uuid; 302 | Some(ValueNotification { uuid, value }) 303 | } 304 | _ => None, 305 | } 306 | } 307 | 308 | fn find_characteristic_by_id( 309 | services: &HashMap, 310 | characteristic_id: CharacteristicId, 311 | ) -> Option<&CharacteristicInfo> { 312 | for service in services.values() { 313 | for characteristic in service.characteristics.values() { 314 | if characteristic.info.id == characteristic_id { 315 | return Some(&characteristic.info); 316 | } 317 | } 318 | } 319 | None 320 | } 321 | 322 | impl From for bluez_async::WriteType { 323 | fn from(write_type: WriteType) -> Self { 324 | match write_type { 325 | WriteType::WithoutResponse => bluez_async::WriteType::WithoutResponse, 326 | WriteType::WithResponse => bluez_async::WriteType::WithResponse, 327 | } 328 | } 329 | } 330 | 331 | impl From for BDAddr { 332 | fn from(mac_address: MacAddress) -> Self { 333 | <[u8; 6]>::into(mac_address.into()) 334 | } 335 | } 336 | 337 | impl From for PeripheralId { 338 | fn from(device_id: DeviceId) -> Self { 339 | PeripheralId(device_id) 340 | } 341 | } 342 | 343 | impl From for AddressType { 344 | fn from(address_type: bluez_async::AddressType) -> Self { 345 | match address_type { 346 | bluez_async::AddressType::Public => AddressType::Public, 347 | bluez_async::AddressType::Random => AddressType::Random, 348 | } 349 | } 350 | } 351 | 352 | fn make_descriptor( 353 | info: &DescriptorInfo, 354 | characteristic_uuid: Uuid, 355 | service_uuid: Uuid, 356 | ) -> Descriptor { 357 | Descriptor { 358 | uuid: info.uuid, 359 | characteristic_uuid, 360 | service_uuid, 361 | } 362 | } 363 | 364 | fn make_characteristic( 365 | characteristic: &CharacteristicInternal, 366 | service_uuid: Uuid, 367 | ) -> Characteristic { 368 | let CharacteristicInternal { info, descriptors } = characteristic; 369 | Characteristic { 370 | uuid: info.uuid, 371 | properties: info.flags.into(), 372 | descriptors: descriptors 373 | .iter() 374 | .map(|(_, descriptor)| make_descriptor(descriptor, info.uuid, service_uuid)) 375 | .collect(), 376 | service_uuid, 377 | } 378 | } 379 | 380 | impl From<&ServiceInternal> for Service { 381 | fn from(service: &ServiceInternal) -> Self { 382 | Service { 383 | uuid: service.info.uuid, 384 | primary: service.info.primary, 385 | characteristics: service 386 | .characteristics 387 | .values() 388 | .map(|characteristic| make_characteristic(characteristic, service.info.uuid)) 389 | .collect(), 390 | } 391 | } 392 | } 393 | 394 | impl From for CharPropFlags { 395 | fn from(flags: CharacteristicFlags) -> Self { 396 | let mut result = CharPropFlags::default(); 397 | if flags.contains(CharacteristicFlags::BROADCAST) { 398 | result.insert(CharPropFlags::BROADCAST); 399 | } 400 | if flags.contains(CharacteristicFlags::READ) { 401 | result.insert(CharPropFlags::READ); 402 | } 403 | if flags.contains(CharacteristicFlags::WRITE_WITHOUT_RESPONSE) { 404 | result.insert(CharPropFlags::WRITE_WITHOUT_RESPONSE); 405 | } 406 | if flags.contains(CharacteristicFlags::WRITE) { 407 | result.insert(CharPropFlags::WRITE); 408 | } 409 | if flags.contains(CharacteristicFlags::NOTIFY) { 410 | result.insert(CharPropFlags::NOTIFY); 411 | } 412 | if flags.contains(CharacteristicFlags::INDICATE) { 413 | result.insert(CharPropFlags::INDICATE); 414 | } 415 | if flags.contains(CharacteristicFlags::SIGNED_WRITE) { 416 | result.insert(CharPropFlags::AUTHENTICATED_SIGNED_WRITES); 417 | } 418 | if flags.contains(CharacteristicFlags::EXTENDED_PROPERTIES) { 419 | result.insert(CharPropFlags::EXTENDED_PROPERTIES); 420 | } 421 | result 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /src/common/adapter_manager.rs: -------------------------------------------------------------------------------- 1 | /// Implements common functionality for adapters across platforms. 2 | // btleplug Source Code File 3 | // 4 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 5 | // 6 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 7 | // for full license information. 8 | // 9 | // Some portions of this file are taken and/or modified from Rumble 10 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 11 | // following copyright: 12 | // 13 | // Copyright (c) 2014 The Rust Project Developers 14 | use crate::api::{CentralEvent, Peripheral}; 15 | use crate::platform::PeripheralId; 16 | use dashmap::{mapref::one::RefMut, DashMap}; 17 | use futures::stream::{Stream, StreamExt}; 18 | use log::trace; 19 | use std::pin::Pin; 20 | use tokio::sync::broadcast; 21 | use tokio_stream::wrappers::BroadcastStream; 22 | 23 | #[derive(Debug)] 24 | pub struct AdapterManager 25 | where 26 | PeripheralType: Peripheral, 27 | { 28 | peripherals: DashMap, 29 | events_channel: broadcast::Sender, 30 | } 31 | 32 | impl Default for AdapterManager { 33 | fn default() -> Self { 34 | let (broadcast_sender, _) = broadcast::channel(16); 35 | AdapterManager { 36 | peripherals: DashMap::new(), 37 | events_channel: broadcast_sender, 38 | } 39 | } 40 | } 41 | 42 | impl AdapterManager 43 | where 44 | PeripheralType: Peripheral + 'static, 45 | { 46 | pub fn emit(&self, event: CentralEvent) { 47 | if let CentralEvent::DeviceDisconnected(ref id) = event { 48 | self.peripherals.remove(id); 49 | } 50 | 51 | if let Err(lost) = self.events_channel.send(event) { 52 | trace!("Lost central event, while nothing subscribed: {:?}", lost); 53 | } 54 | } 55 | 56 | pub fn event_stream(&self) -> Pin + Send>> { 57 | let receiver = self.events_channel.subscribe(); 58 | Box::pin(BroadcastStream::new(receiver).filter_map(|x| async move { x.ok() })) 59 | } 60 | 61 | pub fn add_peripheral(&self, peripheral: PeripheralType) { 62 | assert!( 63 | !self.peripherals.contains_key(&peripheral.id()), 64 | "Adding a peripheral that's already in the map." 65 | ); 66 | self.peripherals.insert(peripheral.id(), peripheral); 67 | } 68 | 69 | pub fn peripherals(&self) -> Vec { 70 | self.peripherals 71 | .iter() 72 | .map(|val| val.value().clone()) 73 | .collect() 74 | } 75 | 76 | // Only used on windows and macOS/iOS, so turn off deadcode so we don't get warnings on android/linux. 77 | #[allow(dead_code)] 78 | pub fn peripheral_mut( 79 | &self, 80 | id: &PeripheralId, 81 | ) -> Option> { 82 | self.peripherals.get_mut(id) 83 | } 84 | 85 | pub fn peripheral(&self, id: &PeripheralId) -> Option { 86 | self.peripherals.get(id).map(|val| val.value().clone()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod adapter_manager; 2 | pub mod util; 3 | -------------------------------------------------------------------------------- /src/common/util.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | 8 | use crate::api::ValueNotification; 9 | use futures::stream::{Stream, StreamExt}; 10 | use std::pin::Pin; 11 | use tokio::sync::broadcast::Receiver; 12 | use tokio_stream::wrappers::BroadcastStream; 13 | 14 | pub fn notifications_stream_from_broadcast_receiver( 15 | receiver: Receiver, 16 | ) -> Pin + Send>> { 17 | Box::pin(BroadcastStream::new(receiver).filter_map(|x| async move { x.ok() })) 18 | } 19 | -------------------------------------------------------------------------------- /src/corebluetooth/adapter.rs: -------------------------------------------------------------------------------- 1 | use super::internal::{ 2 | run_corebluetooth_thread, CoreBluetoothEvent, CoreBluetoothMessage, CoreBluetoothReply, 3 | CoreBluetoothReplyFuture, 4 | }; 5 | use super::peripheral::{Peripheral, PeripheralId}; 6 | use crate::api::{Central, CentralEvent, CentralState, ScanFilter}; 7 | use crate::common::adapter_manager::AdapterManager; 8 | use crate::{Error, Result}; 9 | use async_trait::async_trait; 10 | use futures::channel::mpsc::{self, Sender}; 11 | use futures::sink::SinkExt; 12 | use futures::stream::{Stream, StreamExt}; 13 | use log::*; 14 | use objc2_core_bluetooth::CBManagerState; 15 | use std::pin::Pin; 16 | use std::sync::Arc; 17 | use tokio::task; 18 | 19 | /// Implementation of [api::Central](crate::api::Central). 20 | #[derive(Clone, Debug)] 21 | pub struct Adapter { 22 | manager: Arc>, 23 | sender: Sender, 24 | } 25 | 26 | fn get_central_state(state: CBManagerState) -> CentralState { 27 | match state { 28 | CBManagerState::PoweredOn => CentralState::PoweredOn, 29 | CBManagerState::PoweredOff => CentralState::PoweredOff, 30 | _ => CentralState::Unknown, 31 | } 32 | } 33 | 34 | impl Adapter { 35 | pub(crate) async fn new() -> Result { 36 | let (sender, mut receiver) = mpsc::channel(256); 37 | let adapter_sender = run_corebluetooth_thread(sender)?; 38 | // Since init currently blocked until the state update, we know the 39 | // receiver is dropped after that. We can pick it up here and make it 40 | // part of our event loop to update our peripherals. 41 | debug!("Waiting on adapter connect"); 42 | if !matches!( 43 | receiver.next().await, 44 | Some(CoreBluetoothEvent::DidUpdateState { state: _ }) 45 | ) { 46 | return Err(Error::Other( 47 | "Adapter failed to connect.".to_string().into(), 48 | )); 49 | } 50 | debug!("Adapter connected"); 51 | let manager = Arc::new(AdapterManager::default()); 52 | 53 | let manager_clone = manager.clone(); 54 | let adapter_sender_clone = adapter_sender.clone(); 55 | task::spawn(async move { 56 | while let Some(msg) = receiver.next().await { 57 | match msg { 58 | CoreBluetoothEvent::DeviceDiscovered { 59 | uuid, 60 | name, 61 | event_receiver, 62 | } => { 63 | manager_clone.add_peripheral(Peripheral::new( 64 | uuid, 65 | name, 66 | Arc::downgrade(&manager_clone), 67 | event_receiver, 68 | adapter_sender_clone.clone(), 69 | )); 70 | manager_clone.emit(CentralEvent::DeviceDiscovered(uuid.into())); 71 | } 72 | CoreBluetoothEvent::DeviceUpdated { uuid, name } => { 73 | let id = uuid.into(); 74 | if let Some(entry) = manager_clone.peripheral_mut(&id) { 75 | entry.value().update_name(&name); 76 | manager_clone.emit(CentralEvent::DeviceUpdated(id)); 77 | } 78 | } 79 | CoreBluetoothEvent::DeviceDisconnected { uuid } => { 80 | manager_clone.emit(CentralEvent::DeviceDisconnected(uuid.into())); 81 | } 82 | CoreBluetoothEvent::DidUpdateState { state } => { 83 | let central_state = get_central_state(state); 84 | manager_clone.emit(CentralEvent::StateUpdate(central_state)); 85 | } 86 | } 87 | } 88 | }); 89 | 90 | Ok(Adapter { 91 | manager, 92 | sender: adapter_sender, 93 | }) 94 | } 95 | } 96 | 97 | #[async_trait] 98 | impl Central for Adapter { 99 | type Peripheral = Peripheral; 100 | 101 | async fn events(&self) -> Result + Send>>> { 102 | Ok(self.manager.event_stream()) 103 | } 104 | 105 | async fn start_scan(&self, filter: ScanFilter) -> Result<()> { 106 | self.sender 107 | .to_owned() 108 | .send(CoreBluetoothMessage::StartScanning { filter }) 109 | .await?; 110 | Ok(()) 111 | } 112 | 113 | async fn stop_scan(&self) -> Result<()> { 114 | self.sender 115 | .to_owned() 116 | .send(CoreBluetoothMessage::StopScanning) 117 | .await?; 118 | Ok(()) 119 | } 120 | 121 | async fn peripherals(&self) -> Result> { 122 | Ok(self.manager.peripherals()) 123 | } 124 | 125 | async fn peripheral(&self, id: &PeripheralId) -> Result { 126 | self.manager.peripheral(id).ok_or(Error::DeviceNotFound) 127 | } 128 | 129 | async fn add_peripheral(&self, _address: &PeripheralId) -> Result { 130 | Err(Error::NotSupported( 131 | "Can't add a Peripheral from a PeripheralId".to_string(), 132 | )) 133 | } 134 | 135 | async fn adapter_info(&self) -> Result { 136 | // TODO: Get information about the adapter. 137 | Ok("CoreBluetooth".to_string()) 138 | } 139 | 140 | async fn adapter_state(&self) -> Result { 141 | let fut = CoreBluetoothReplyFuture::default(); 142 | self.sender 143 | .to_owned() 144 | .send(CoreBluetoothMessage::GetAdapterState { 145 | future: fut.get_state_clone(), 146 | }) 147 | .await?; 148 | 149 | match fut.await { 150 | CoreBluetoothReply::AdapterState(state) => { 151 | let central_state = get_central_state(state); 152 | return Ok(central_state.clone()); 153 | } 154 | _ => panic!("Shouldn't get anything but a AdapterState!"), 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/corebluetooth/ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | use std::os::raw::{c_char, c_void}; 3 | 4 | pub type dispatch_object_s = c_void; 5 | pub type dispatch_queue_t = *mut dispatch_object_s; 6 | pub type dispatch_queue_attr_t = *const dispatch_object_s; 7 | 8 | pub const DISPATCH_QUEUE_SERIAL: dispatch_queue_attr_t = 0 as dispatch_queue_attr_t; 9 | 10 | extern "C" { 11 | pub fn dispatch_queue_create( 12 | label: *const c_char, 13 | attr: dispatch_queue_attr_t, 14 | ) -> dispatch_queue_t; 15 | } 16 | 17 | // TODO: Do we need to link to AppKit here? 18 | #[cfg_attr(target_os = "macos", link(name = "AppKit", kind = "framework"))] 19 | extern "C" {} 20 | -------------------------------------------------------------------------------- /src/corebluetooth/future.rs: -------------------------------------------------------------------------------- 1 | use core::pin::Pin; 2 | use std::future::Future; 3 | use std::sync::{Arc, Mutex}; 4 | use std::task::{Context, Poll, Waker}; 5 | 6 | /// Struct used for waiting on replies from the server. 7 | /// 8 | /// When a BtlePlugMessage is sent to the server, it may take an indeterminate 9 | /// amount of time to get a reply. This struct holds the reply, as well as a 10 | /// [Waker] for the related future. Once the reply_msg is filled, the waker will 11 | /// be called to finish the future polling. 12 | #[derive(Debug, Clone)] 13 | pub struct BtlePlugFutureState { 14 | reply_msg: Option, 15 | waker: Option, 16 | } 17 | 18 | // For some reason, deriving default above doesn't work, but doing an explicit 19 | // derive here does work. 20 | impl Default for BtlePlugFutureState { 21 | fn default() -> Self { 22 | BtlePlugFutureState:: { 23 | reply_msg: None, 24 | waker: None, 25 | } 26 | } 27 | } 28 | 29 | impl BtlePlugFutureState { 30 | /// Sets the reply message in a message state struct, firing the waker. 31 | /// 32 | /// When a reply is received from (or in the in-process case, generated by) 33 | /// a server, this function takes the message, updates the state struct, and 34 | /// calls [Waker::wake] so that the corresponding future can finish. 35 | /// 36 | /// # Parameters 37 | /// 38 | /// - `msg`: Message to set as reply, which will be returned by the 39 | /// corresponding future. 40 | pub fn set_reply(&mut self, reply: T) { 41 | if self.reply_msg.is_some() { 42 | // TODO Can we stop multiple calls to set_reply_msg at compile time? 43 | panic!("set_reply_msg called multiple times on the same future."); 44 | } 45 | 46 | self.reply_msg = Some(reply); 47 | 48 | if self.waker.is_some() { 49 | self.waker.take().unwrap().wake(); 50 | } 51 | } 52 | } 53 | 54 | /// Shared [BtlePlugFutureState] type. 55 | /// 56 | /// [BtlePlugFutureState] is made to be shared across futures, and we'll 57 | /// never know if those futures are single or multithreaded. Only needs to 58 | /// unlock for calls to [BtlePlugFutureState::set_reply]. 59 | pub type BtlePlugFutureStateShared = Arc>>; 60 | 61 | /// [Future] implementation for [BtlePlugMessageUnion] types send to the server. 62 | /// 63 | /// A [Future] implementation that we can always expect to return a 64 | /// [BtlePlugMessageUnion]. Used to deal with getting server replies after 65 | /// sending [BtlePlugMessageUnion] types via the client API. 66 | #[derive(Debug)] 67 | pub struct BtlePlugFuture { 68 | /// State that holds the waker for the future, and the [BtlePlugMessageUnion] reply (once set). 69 | /// 70 | /// ## Notes 71 | /// 72 | /// This needs to be an [Arc]<[Mutex]> in order to make it mutable under 73 | /// pinning when dealing with being a future. There is a chance we could do 74 | /// this as a [Pin::get_unchecked_mut] borrow, which would be way faster, but 75 | /// that's dicey and hasn't been proven as needed for speed yet. 76 | waker_state: BtlePlugFutureStateShared, 77 | } 78 | 79 | impl Default for BtlePlugFuture { 80 | fn default() -> Self { 81 | BtlePlugFuture:: { 82 | waker_state: BtlePlugFutureStateShared::::default(), 83 | } 84 | } 85 | } 86 | 87 | impl BtlePlugFuture { 88 | /// Returns a clone of the state, used for moving the state across contexts 89 | /// (tasks/threads/etc...). 90 | pub fn get_state_clone(&self) -> BtlePlugFutureStateShared { 91 | self.waker_state.clone() 92 | } 93 | 94 | // TODO Should we implement drop on this, so it'll yell if its dropping and 95 | // the waker didn't fire? otherwise it seems like we could have quiet 96 | // deadlocks. 97 | } 98 | 99 | impl Future for BtlePlugFuture { 100 | type Output = T; 101 | 102 | /// Returns when the [BtlePlugMessageUnion] reply has been set in the 103 | /// [BtlePlugFutureStateShared]. 104 | fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { 105 | let mut waker_state = self.waker_state.lock().unwrap(); 106 | if waker_state.reply_msg.is_some() { 107 | let msg = waker_state.reply_msg.take().unwrap(); 108 | Poll::Ready(msg) 109 | } else { 110 | waker_state.waker = Some(cx.waker().clone()); 111 | Poll::Pending 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/corebluetooth/manager.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | 8 | use super::adapter::Adapter; 9 | use crate::{api, Result}; 10 | use async_trait::async_trait; 11 | 12 | /// Implementation of [api::Manager](crate::api::Manager). 13 | #[derive(Clone, Debug)] 14 | pub struct Manager {} 15 | 16 | impl Manager { 17 | pub async fn new() -> Result { 18 | Ok(Self {}) 19 | } 20 | } 21 | 22 | #[async_trait] 23 | impl api::Manager for Manager { 24 | type Adapter = Adapter; 25 | 26 | async fn adapters(&self) -> Result> { 27 | Ok(vec![Adapter::new().await?]) 28 | // TODO What do we do if there is no bluetooth adapter, like on an older 29 | // macbook pro? Will BluetoothAdapter::init() fail? 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/corebluetooth/mod.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | 8 | pub mod adapter; 9 | mod central_delegate; 10 | mod ffi; 11 | mod future; 12 | mod internal; 13 | pub mod manager; 14 | pub mod peripheral; 15 | mod utils; 16 | -------------------------------------------------------------------------------- /src/corebluetooth/utils/core_bluetooth.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from blurmac 9 | // (https://github.com/servo/devices), using a BSD 3-Clause license under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2017 Akos Kiss. 13 | // 14 | // Licensed under the BSD 3-Clause License 15 | // . 16 | // This file may not be copied, modified, or distributed except 17 | // according to those terms. 18 | 19 | use objc2::rc::Retained; 20 | use objc2_core_bluetooth::CBUUID; 21 | use objc2_foundation::NSString; 22 | use uuid::Uuid; 23 | 24 | /// Convert a CBUUID object to the standard Uuid type. 25 | pub fn cbuuid_to_uuid(cbuuid: &CBUUID) -> Uuid { 26 | // NOTE: CoreBluetooth tends to return uppercase UUID strings, and only 4 27 | // character long if the UUID is short (16 bits). It can also return 8 28 | // character strings if the rest of the UUID matches the generic UUID. 29 | let uuid = unsafe { cbuuid.UUIDString() }.to_string(); 30 | let long = if uuid.len() == 4 { 31 | format!("0000{}-0000-1000-8000-00805f9b34fb", uuid) 32 | } else if uuid.len() == 8 { 33 | format!("{}-0000-1000-8000-00805f9b34fb", uuid) 34 | } else { 35 | uuid 36 | }; 37 | let uuid_string = long.to_lowercase(); 38 | uuid_string.parse().unwrap() 39 | } 40 | 41 | /// Convert a `Uuid` to a `CBUUID`. 42 | pub fn uuid_to_cbuuid(uuid: Uuid) -> Retained { 43 | unsafe { CBUUID::UUIDWithString(&NSString::from_str(&uuid.to_string())) } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use objc2_foundation::ns_string; 49 | 50 | use super::*; 51 | 52 | #[test] 53 | fn parse_uuid_short() { 54 | let uuid_string = "1234"; 55 | let uuid_nsstring = NSString::from_str(uuid_string); 56 | let cbuuid = unsafe { CBUUID::UUIDWithString(&uuid_nsstring) }; 57 | let uuid = cbuuid_to_uuid(&*cbuuid); 58 | assert_eq!( 59 | uuid, 60 | Uuid::from_u128(0x00001234_0000_1000_8000_00805f9b34fb) 61 | ); 62 | } 63 | 64 | #[test] 65 | fn parse_uuid_long() { 66 | let uuid_nsstring = ns_string!("12345678-0000-1111-2222-333344445555"); 67 | let cbuuid = unsafe { CBUUID::UUIDWithString(uuid_nsstring) }; 68 | let uuid = cbuuid_to_uuid(&*cbuuid); 69 | assert_eq!( 70 | uuid, 71 | Uuid::from_u128(0x12345678_0000_1111_2222_333344445555) 72 | ); 73 | } 74 | 75 | #[test] 76 | fn cbuuid_roundtrip() { 77 | for uuid in [ 78 | Uuid::from_u128(0x00001234_0000_1000_8000_00805f9b34fb), 79 | Uuid::from_u128(0xabcd1234_0000_1000_8000_00805f9b34fb), 80 | Uuid::from_u128(0x12345678_0000_1111_2222_333344445555), 81 | ] { 82 | assert_eq!(cbuuid_to_uuid(&*uuid_to_cbuuid(uuid)), uuid); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/corebluetooth/utils/mod.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from blurmac 9 | // (https://github.com/servo/devices), using a BSD 3-Clause license under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2017 Akos Kiss. 13 | // 14 | // Licensed under the BSD 3-Clause License 15 | // . 16 | // This file may not be copied, modified, or distributed except 17 | // according to those terms. 18 | 19 | use std::ffi::CStr; 20 | 21 | use objc2_foundation::{NSString, NSUUID}; 22 | use uuid::Uuid; 23 | 24 | pub mod core_bluetooth; 25 | 26 | pub fn nsuuid_to_uuid(uuid: &NSUUID) -> Uuid { 27 | uuid.UUIDString().to_string().parse().unwrap() 28 | } 29 | 30 | pub unsafe fn nsstring_to_string(nsstring: *const NSString) -> Option { 31 | nsstring 32 | .as_ref() 33 | .and_then(|ns| CStr::from_ptr(ns.UTF8String()).to_str().ok()) 34 | .map(String::from) 35 | } 36 | -------------------------------------------------------------------------------- /src/droidplug/adapter.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | jni::{ 3 | global_jvm, 4 | objects::{JScanFilter, JScanResult}, 5 | }, 6 | peripheral::{Peripheral, PeripheralId}, 7 | }; 8 | use crate::{ 9 | api::{BDAddr, Central, CentralEvent, CentralState, PeripheralProperties, ScanFilter}, 10 | common::adapter_manager::AdapterManager, 11 | Error, Result, 12 | }; 13 | use async_trait::async_trait; 14 | use futures::stream::Stream; 15 | use jni::{ 16 | objects::{GlobalRef, JObject, JString}, 17 | strings::JavaStr, 18 | sys::jboolean, 19 | JNIEnv, 20 | }; 21 | use std::{ 22 | fmt::{Debug, Formatter}, 23 | pin::Pin, 24 | str::FromStr, 25 | sync::Arc, 26 | }; 27 | 28 | #[derive(Clone)] 29 | pub struct Adapter { 30 | manager: Arc>, 31 | internal: GlobalRef, 32 | } 33 | 34 | impl Debug for Adapter { 35 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 36 | f.debug_struct("Adapter") 37 | .field("manager", &self.manager) 38 | .finish() 39 | } 40 | } 41 | 42 | impl Adapter { 43 | pub(crate) fn new() -> Result { 44 | let env = global_jvm().get_env()?; 45 | 46 | let obj = env.new_object( 47 | "com/nonpolynomial/btleplug/android/impl/Adapter", 48 | "()V", 49 | &[], 50 | )?; 51 | let internal = env.new_global_ref(obj)?; 52 | let adapter = Self { 53 | manager: Arc::new(AdapterManager::default()), 54 | internal, 55 | }; 56 | env.set_rust_field(obj, "handle", adapter.clone())?; 57 | 58 | Ok(adapter) 59 | } 60 | 61 | pub fn report_scan_result(&self, scan_result: JObject) -> Result { 62 | use std::convert::TryInto; 63 | 64 | let env = global_jvm().get_env()?; 65 | let scan_result = JScanResult::from_env(&env, scan_result)?; 66 | 67 | let (addr, properties): (BDAddr, Option) = scan_result.try_into()?; 68 | 69 | match self.manager.peripheral(&PeripheralId(addr)) { 70 | Some(p) => match properties { 71 | Some(properties) => { 72 | self.report_properties(&p, properties, false); 73 | Ok(p) 74 | } 75 | None => { 76 | //self.manager.emit(CentralEvent::DeviceDisconnected(addr)); 77 | Err(Error::DeviceNotFound) 78 | } 79 | }, 80 | None => match properties { 81 | Some(properties) => { 82 | let p = self.add(addr)?; 83 | self.report_properties(&p, properties, true); 84 | Ok(p) 85 | } 86 | None => Err(Error::DeviceNotFound), 87 | }, 88 | } 89 | } 90 | 91 | fn add(&self, address: BDAddr) -> Result { 92 | let env = global_jvm().get_env()?; 93 | let peripheral = Peripheral::new(&env, self.internal.as_obj(), address)?; 94 | self.manager.add_peripheral(peripheral.clone()); 95 | Ok(peripheral) 96 | } 97 | 98 | fn report_properties( 99 | &self, 100 | peripheral: &Peripheral, 101 | properties: PeripheralProperties, 102 | new: bool, 103 | ) { 104 | peripheral.report_properties(properties.clone()); 105 | self.manager.emit(if new { 106 | CentralEvent::DeviceDiscovered(PeripheralId(properties.address)) 107 | } else { 108 | CentralEvent::DeviceUpdated(PeripheralId(properties.address)) 109 | }); 110 | self.manager 111 | .emit(CentralEvent::ManufacturerDataAdvertisement { 112 | id: PeripheralId(properties.address), 113 | manufacturer_data: properties.manufacturer_data, 114 | }); 115 | self.manager.emit(CentralEvent::ServiceDataAdvertisement { 116 | id: PeripheralId(properties.address), 117 | service_data: properties.service_data, 118 | }); 119 | self.manager.emit(CentralEvent::ServicesAdvertisement { 120 | id: PeripheralId(properties.address), 121 | services: properties.services, 122 | }); 123 | } 124 | } 125 | 126 | #[async_trait] 127 | impl Central for Adapter { 128 | type Peripheral = Peripheral; 129 | 130 | async fn adapter_info(&self) -> Result { 131 | // TODO: Get information about the adapter. 132 | Ok("Android".to_string()) 133 | } 134 | 135 | async fn events(&self) -> Result + Send>>> { 136 | Ok(self.manager.event_stream()) 137 | } 138 | 139 | async fn start_scan(&self, filter: ScanFilter) -> Result<()> { 140 | let env = global_jvm().get_env()?; 141 | let filter = JScanFilter::new(&env, filter)?; 142 | env.call_method( 143 | &self.internal, 144 | "startScan", 145 | "(Lcom/nonpolynomial/btleplug/android/impl/ScanFilter;)V", 146 | &[filter.into()], 147 | )?; 148 | Ok(()) 149 | } 150 | 151 | async fn stop_scan(&self) -> Result<()> { 152 | let env = global_jvm().get_env()?; 153 | env.call_method(&self.internal, "stopScan", "()V", &[])?; 154 | Ok(()) 155 | } 156 | 157 | async fn peripherals(&self) -> Result> { 158 | Ok(self.manager.peripherals()) 159 | } 160 | 161 | async fn peripheral(&self, address: &PeripheralId) -> Result { 162 | self.manager 163 | .peripheral(address) 164 | .ok_or(Error::DeviceNotFound) 165 | } 166 | 167 | async fn add_peripheral(&self, address: &PeripheralId) -> Result { 168 | self.add(address.0) 169 | } 170 | 171 | async fn adapter_state(&self) -> Result { 172 | Ok(CentralState::Unknown) 173 | } 174 | } 175 | 176 | pub(crate) fn adapter_report_scan_result_internal( 177 | env: &JNIEnv, 178 | obj: JObject, 179 | scan_result: JObject, 180 | ) -> crate::Result<()> { 181 | let adapter = env.get_rust_field::<_, _, Adapter>(obj, "handle")?; 182 | adapter.report_scan_result(scan_result)?; 183 | Ok(()) 184 | } 185 | 186 | pub(crate) fn adapter_on_connection_state_changed_internal( 187 | env: &JNIEnv, 188 | obj: JObject, 189 | addr: JString, 190 | connected: jboolean, 191 | ) -> crate::Result<()> { 192 | let adapter = env.get_rust_field::<_, _, Adapter>(obj, "handle")?; 193 | let addr_str = JavaStr::from_env(env, addr)?; 194 | let addr_str = addr_str.to_str().map_err(|e| Error::Other(e.into()))?; 195 | let addr = BDAddr::from_str(addr_str)?; 196 | adapter.manager.emit(if connected != 0 { 197 | CentralEvent::DeviceConnected(PeripheralId(addr)) 198 | } else { 199 | CentralEvent::DeviceDisconnected(PeripheralId(addr)) 200 | }); 201 | Ok(()) 202 | } 203 | -------------------------------------------------------------------------------- /src/droidplug/java/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /.classpath 17 | /.project 18 | /.settings 19 | -------------------------------------------------------------------------------- /src/droidplug/java/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:7.4.2' 8 | } 9 | } 10 | 11 | 12 | plugins { 13 | id 'com.android.library' 14 | } 15 | 16 | 17 | repositories { 18 | mavenLocal() 19 | google() 20 | mavenCentral() 21 | } 22 | 23 | android { 24 | compileSdkVersion 34 25 | 26 | defaultConfig { 27 | minSdkVersion 23 28 | versionCode 1 29 | versionName '0.7.3' 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation 'io.github.gedgygedgy.rust:jni-utils:0.1.1-SNAPSHOT' 40 | //implementation files('c:/Users/qdot/code/jni-utils-rs/java/build/libs/jni-utils-0.1.1-SNAPSHOT.jar') 41 | } 42 | -------------------------------------------------------------------------------- /src/droidplug/java/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deviceplug/btleplug/c227cc1adf885e143217f274758968afc02490fb/src/droidplug/java/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/droidplug/java/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/droidplug/java/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/droidplug/java/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /src/droidplug/java/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | } 5 | plugins { 6 | id 'com.android.library' version '7.4.2' 7 | } 8 | } 9 | 10 | rootProject.name = 'droidplug' 11 | 12 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/Adapter.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.bluetooth.BluetoothAdapter; 5 | import android.bluetooth.BluetoothManager; 6 | import android.bluetooth.le.BluetoothLeScanner; 7 | import android.bluetooth.le.ScanCallback; 8 | import android.bluetooth.le.ScanFilter.Builder; 9 | import android.bluetooth.le.ScanResult; 10 | import android.bluetooth.le.ScanSettings; 11 | import android.os.Build; 12 | import android.os.ParcelUuid; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | @SuppressWarnings("unused") // Native code uses this class. 18 | class Adapter { 19 | private long handle; 20 | private final Callback callback = new Callback(); 21 | 22 | public Adapter() {} 23 | 24 | @SuppressLint("MissingPermission") 25 | public void startScan(ScanFilter filter) { 26 | BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 27 | if (bluetoothAdapter == null) { 28 | throw new RuntimeException("No bluetooth adapter available."); 29 | } 30 | 31 | ArrayList filters = null; 32 | String[] uuids = filter.getUuids(); 33 | if (uuids.length > 0) { 34 | filters = new ArrayList<>(); 35 | for (String uuid : uuids) { 36 | filters.add(new Builder().setServiceUuid(ParcelUuid.fromString(uuid)).build()); 37 | } 38 | } 39 | ScanSettings settings; 40 | if (Build.VERSION.SDK_INT >= 26) { 41 | settings = new ScanSettings.Builder() 42 | .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 43 | .setLegacy(false) 44 | .build(); 45 | } else { 46 | settings = new ScanSettings.Builder() 47 | .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 48 | .build(); 49 | } 50 | BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner(); 51 | if (scanner == null) { 52 | throw new RuntimeException("No bluetooth scanner available for adapter"); 53 | } 54 | scanner.startScan(filters, settings, this.callback); 55 | } 56 | 57 | @SuppressLint("MissingPermission") 58 | public void stopScan() { 59 | BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 60 | if (bluetoothAdapter != null) { 61 | BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner(); 62 | if (scanner != null) { 63 | scanner.stopScan(this.callback); 64 | } 65 | } 66 | } 67 | 68 | private native void reportScanResult(ScanResult result); 69 | 70 | public native void onConnectionStateChanged(String address, boolean connected); 71 | 72 | private class Callback extends ScanCallback { 73 | @Override 74 | public void onScanResult(int callbackType, ScanResult result) { 75 | Adapter.this.reportScanResult(result); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/BluetoothException.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | class BluetoothException extends RuntimeException { 4 | public BluetoothException() { 5 | super(); 6 | } 7 | 8 | public BluetoothException(Throwable cause) { 9 | super(cause); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | class NoSuchCharacteristicException extends BluetoothException { 4 | } 5 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/NotConnectedException.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | class NotConnectedException extends BluetoothException { 4 | } 5 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/PermissionDeniedException.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | class PermissionDeniedException extends BluetoothException { 4 | public PermissionDeniedException() { 5 | super(); 6 | } 7 | 8 | public PermissionDeniedException(Throwable cause) { 9 | super(cause); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/ScanFilter.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | import java.util.Arrays; 4 | 5 | public class ScanFilter { 6 | private final String[] uuids; 7 | 8 | public ScanFilter(String uuids[]) { 9 | if (uuids == null) { 10 | this.uuids = new String[0]; 11 | } else { 12 | int len = uuids.length; 13 | this.uuids = Arrays.copyOf(uuids, len); 14 | } 15 | } 16 | 17 | public String[] getUuids() { 18 | int len = uuids.length; 19 | return Arrays.copyOf(uuids, len); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | class UnexpectedCallbackException extends BluetoothException { 4 | } 5 | -------------------------------------------------------------------------------- /src/droidplug/java/src/main/java/com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException.java: -------------------------------------------------------------------------------- 1 | package com.nonpolynomial.btleplug.android.impl; 2 | 3 | class UnexpectedCharacteristicException extends BluetoothException { 4 | } 5 | -------------------------------------------------------------------------------- /src/droidplug/jni/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod objects; 2 | 3 | use ::jni::{objects::JObject, JNIEnv, JavaVM, NativeMethod}; 4 | use jni::{objects::JString, sys::jboolean}; 5 | use once_cell::sync::OnceCell; 6 | use std::ffi::c_void; 7 | 8 | static GLOBAL_JVM: OnceCell = OnceCell::new(); 9 | 10 | pub fn init(env: &JNIEnv) -> crate::Result<()> { 11 | if let Ok(()) = GLOBAL_JVM.set(env.get_java_vm()?) { 12 | env.register_native_methods( 13 | "com/nonpolynomial/btleplug/android/impl/Adapter", 14 | &[ 15 | NativeMethod { 16 | name: "reportScanResult".into(), 17 | sig: "(Landroid/bluetooth/le/ScanResult;)V".into(), 18 | fn_ptr: adapter_report_scan_result as *mut c_void, 19 | }, 20 | NativeMethod { 21 | name: "onConnectionStateChanged".into(), 22 | sig: "(Ljava/lang/String;Z)V".into(), 23 | fn_ptr: adapter_on_connection_state_changed as *mut c_void, 24 | }, 25 | ], 26 | )?; 27 | jni_utils::classcache::find_add_class( 28 | env, 29 | "com/nonpolynomial/btleplug/android/impl/Peripheral", 30 | )?; 31 | jni_utils::classcache::find_add_class( 32 | env, 33 | "com/nonpolynomial/btleplug/android/impl/ScanFilter", 34 | )?; 35 | jni_utils::classcache::find_add_class( 36 | env, 37 | "com/nonpolynomial/btleplug/android/impl/NotConnectedException", 38 | )?; 39 | jni_utils::classcache::find_add_class( 40 | env, 41 | "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", 42 | )?; 43 | jni_utils::classcache::find_add_class( 44 | env, 45 | "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", 46 | )?; 47 | jni_utils::classcache::find_add_class( 48 | env, 49 | "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", 50 | )?; 51 | jni_utils::classcache::find_add_class( 52 | env, 53 | "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", 54 | )?; 55 | } 56 | Ok(()) 57 | } 58 | 59 | pub fn global_jvm() -> &'static JavaVM { 60 | GLOBAL_JVM.get().expect( 61 | "Droidplug has not been initialized. Please initialize it with btleplug::platform::init().", 62 | ) 63 | } 64 | 65 | impl From<::jni::errors::Error> for crate::Error { 66 | fn from(err: ::jni::errors::Error) -> Self { 67 | Self::Other(Box::new(err)) 68 | } 69 | } 70 | 71 | extern "C" fn adapter_report_scan_result(env: JNIEnv, obj: JObject, scan_result: JObject) { 72 | let _ = super::adapter::adapter_report_scan_result_internal(&env, obj, scan_result); 73 | } 74 | 75 | extern "C" fn adapter_on_connection_state_changed( 76 | env: JNIEnv, 77 | obj: JObject, 78 | addr: JString, 79 | connected: jboolean, 80 | ) { 81 | let _ = 82 | super::adapter::adapter_on_connection_state_changed_internal(&env, obj, addr, connected); 83 | } 84 | -------------------------------------------------------------------------------- /src/droidplug/manager.rs: -------------------------------------------------------------------------------- 1 | use super::adapter::Adapter; 2 | use crate::{api, Result}; 3 | use async_trait::async_trait; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct Manager; 7 | 8 | impl Manager { 9 | pub async fn new() -> Result { 10 | Ok(Manager) 11 | } 12 | } 13 | 14 | #[async_trait] 15 | impl api::Manager for Manager { 16 | type Adapter = Adapter; 17 | 18 | async fn adapters(&self) -> Result> { 19 | Ok(vec![super::global_adapter().clone()]) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/droidplug/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod adapter; 2 | pub mod manager; 3 | pub mod peripheral; 4 | 5 | use ::jni::JNIEnv; 6 | use once_cell::sync::OnceCell; 7 | 8 | mod jni; 9 | 10 | static GLOBAL_ADAPTER: OnceCell = OnceCell::new(); 11 | 12 | pub fn init(env: &JNIEnv) -> crate::Result<()> { 13 | self::jni::init(env)?; 14 | GLOBAL_ADAPTER.get_or_try_init(|| adapter::Adapter::new())?; 15 | Ok(()) 16 | } 17 | 18 | pub fn global_adapter() -> &'static adapter::Adapter { 19 | GLOBAL_ADAPTER.get().expect( 20 | "Droidplug has not been initialized. Please initialize it with btleplug::platform::init().", 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/droidplug/peripheral.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | api::{ 3 | self, BDAddr, Characteristic, Descriptor, PeripheralProperties, Service, ValueNotification, 4 | WriteType, 5 | }, 6 | Error, Result, 7 | }; 8 | use async_trait::async_trait; 9 | use futures::stream::Stream; 10 | use jni::{ 11 | objects::{GlobalRef, JList, JObject}, 12 | JNIEnv, 13 | }; 14 | use jni_utils::{ 15 | arrays::byte_array_to_vec, exceptions::try_block, future::JSendFuture, stream::JSendStream, 16 | task::JPollResult, uuid::JUuid, 17 | }; 18 | #[cfg(feature = "serde")] 19 | use serde::{Deserialize, Serialize}; 20 | #[cfg(feature = "serde")] 21 | use serde_cr as serde; 22 | use std::{ 23 | collections::BTreeSet, 24 | convert::TryFrom, 25 | fmt::{self, Debug, Display, Formatter}, 26 | pin::Pin, 27 | sync::{Arc, Mutex}, 28 | }; 29 | 30 | use super::jni::{ 31 | global_jvm, 32 | objects::{JBluetoothGattCharacteristic, JBluetoothGattService, JPeripheral}, 33 | }; 34 | use jni::objects::JClass; 35 | #[cfg_attr( 36 | feature = "serde", 37 | derive(Serialize, Deserialize), 38 | serde(crate = "serde_cr") 39 | )] 40 | #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 41 | pub struct PeripheralId(pub(super) BDAddr); 42 | impl Display for PeripheralId { 43 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 44 | Display::fmt(&self.0, f) 45 | } 46 | } 47 | 48 | fn get_poll_result<'a: 'b, 'b>( 49 | env: &'b JNIEnv<'a>, 50 | result: JPollResult<'a, 'b>, 51 | ) -> Result> { 52 | try_block(env, || Ok(Ok(result.get()?))) 53 | .catch( 54 | JClass::from( 55 | jni_utils::classcache::get_class( 56 | "io/github/gedgygedgy/rust/future/FutureException", 57 | ) 58 | .unwrap() 59 | .as_obj(), 60 | ), 61 | |ex| { 62 | let cause = env 63 | .call_method(ex, "getCause", "()Ljava/lang/Throwable;", &[])? 64 | .l()?; 65 | if env.is_instance_of( 66 | cause, 67 | JClass::from( 68 | jni_utils::classcache::get_class( 69 | "com/nonpolynomial/btleplug/android/impl/NotConnectedException", 70 | ) 71 | .unwrap() 72 | .as_obj(), 73 | ), 74 | )? { 75 | Ok(Err(Error::NotConnected)) 76 | } else if env.is_instance_of( 77 | cause, 78 | JClass::from( 79 | jni_utils::classcache::get_class( 80 | "com/nonpolynomial/btleplug/android/impl/PermissionDeniedException", 81 | ) 82 | .unwrap() 83 | .as_obj(), 84 | ), 85 | )? { 86 | Ok(Err(Error::PermissionDenied)) 87 | } else if env.is_instance_of( 88 | cause, 89 | JClass::from( 90 | jni_utils::classcache::get_class( 91 | "com/nonpolynomial/btleplug/android/impl/UnexpectedCallbackException", 92 | ) 93 | .unwrap() 94 | .as_obj(), 95 | ), 96 | )? { 97 | Ok(Err(Error::UnexpectedCallback)) 98 | } else if env.is_instance_of( 99 | cause, 100 | JClass::from( 101 | jni_utils::classcache::get_class( 102 | "com/nonpolynomial/btleplug/android/impl/UnexpectedCharacteristicException", 103 | ) 104 | .unwrap() 105 | .as_obj(), 106 | ), 107 | )? { 108 | Ok(Err(Error::UnexpectedCharacteristic)) 109 | } else if env.is_instance_of( 110 | cause, 111 | JClass::from( 112 | jni_utils::classcache::get_class( 113 | "com/nonpolynomial/btleplug/android/impl/NoSuchCharacteristicException", 114 | ) 115 | .unwrap() 116 | .as_obj(), 117 | ), 118 | )? { 119 | Ok(Err(Error::NoSuchCharacteristic)) 120 | } else if env.is_instance_of( 121 | cause, 122 | "java/lang/RuntimeException", 123 | )? { 124 | let msg = env 125 | .call_method(cause, "getMessage", "()Ljava/lang/String;", &[]) 126 | .unwrap() 127 | .l() 128 | .unwrap(); 129 | let msgstr:String = env.get_string(msg.into()).unwrap().into(); 130 | Ok(Err(Error::RuntimeError(msgstr))) 131 | } else { 132 | env.throw(ex)?; 133 | Err(jni::errors::Error::JavaException) 134 | } 135 | }, 136 | ) 137 | .result()? 138 | } 139 | 140 | #[derive(Debug)] 141 | struct PeripheralShared { 142 | services: BTreeSet, 143 | characteristics: BTreeSet, 144 | properties: Option, 145 | } 146 | 147 | #[derive(Clone)] 148 | pub struct Peripheral { 149 | addr: BDAddr, 150 | internal: GlobalRef, 151 | shared: Arc>, 152 | } 153 | 154 | impl Peripheral { 155 | pub(crate) fn new(env: &JNIEnv, adapter: JObject, addr: BDAddr) -> Result { 156 | let obj = JPeripheral::new(env, adapter, addr)?; 157 | Ok(Self { 158 | addr, 159 | internal: env.new_global_ref(obj)?, 160 | shared: Arc::new(Mutex::new(PeripheralShared { 161 | services: BTreeSet::new(), 162 | characteristics: BTreeSet::new(), 163 | properties: None, 164 | })), 165 | }) 166 | } 167 | 168 | pub(crate) fn report_properties(&self, properties: PeripheralProperties) { 169 | let mut guard = self.shared.lock().unwrap(); 170 | 171 | guard.properties = Some(properties); 172 | } 173 | 174 | fn with_obj( 175 | &self, 176 | f: impl FnOnce(&JNIEnv, JPeripheral) -> std::result::Result, 177 | ) -> std::result::Result 178 | where 179 | E: From<::jni::errors::Error>, 180 | { 181 | let env = global_jvm().get_env()?; 182 | let obj = JPeripheral::from_env(&env, self.internal.as_obj())?; 183 | f(&env, obj) 184 | } 185 | 186 | async fn set_characteristic_notification( 187 | &self, 188 | characteristic: &Characteristic, 189 | enable: bool, 190 | ) -> Result<()> { 191 | let future = self.with_obj(|env, obj| { 192 | let uuid_obj = JUuid::new(env, characteristic.uuid)?; 193 | JSendFuture::try_from(obj.set_characteristic_notification(uuid_obj, enable)?) 194 | })?; 195 | let result_ref = future.await?; 196 | self.with_obj(|env, _obj| { 197 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 198 | get_poll_result(env, result).map(|_| {}) 199 | }) 200 | } 201 | } 202 | 203 | impl Debug for Peripheral { 204 | fn fmt(&self, fmt: &mut Formatter) -> std::result::Result<(), std::fmt::Error> { 205 | write!(fmt, "{:?}", self.internal.as_obj()) 206 | } 207 | } 208 | 209 | #[async_trait] 210 | impl api::Peripheral for Peripheral { 211 | /// Returns the unique identifier of the peripheral. 212 | fn id(&self) -> PeripheralId { 213 | PeripheralId(self.addr) 214 | } 215 | 216 | fn address(&self) -> BDAddr { 217 | self.addr 218 | } 219 | 220 | async fn properties(&self) -> Result> { 221 | let guard = self.shared.lock().map_err(Into::::into)?; 222 | Ok((&guard.properties).clone()) 223 | } 224 | 225 | fn characteristics(&self) -> BTreeSet { 226 | let guard = self.shared.lock().unwrap(); 227 | (&guard.characteristics).clone() 228 | } 229 | 230 | async fn is_connected(&self) -> Result { 231 | self.with_obj(|_env, obj| Ok(obj.is_connected()?)) 232 | } 233 | 234 | async fn connect(&self) -> Result<()> { 235 | let future = self.with_obj(|_env, obj| JSendFuture::try_from(obj.connect()?))?; 236 | let result_ref = future.await?; 237 | self.with_obj(|env, _obj| { 238 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 239 | get_poll_result(env, result).map(|_| {}) 240 | }) 241 | } 242 | 243 | async fn disconnect(&self) -> Result<()> { 244 | let future = self.with_obj(|_env, obj| JSendFuture::try_from(obj.disconnect()?))?; 245 | let result_ref = future.await?; 246 | self.with_obj(|env, _obj| { 247 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 248 | get_poll_result(env, result).map(|_| {}) 249 | }) 250 | } 251 | 252 | /// The set of services we've discovered for this device. This will be empty until 253 | /// `discover_services` is called. 254 | fn services(&self) -> BTreeSet { 255 | let guard = self.shared.lock().unwrap(); 256 | (&guard.services).clone() 257 | } 258 | 259 | async fn discover_services(&self) -> Result<()> { 260 | let future = self.with_obj(|_env, obj| JSendFuture::try_from(obj.discover_services()?))?; 261 | let result_ref = future.await?; 262 | self.with_obj(|env, _obj| { 263 | use std::iter::FromIterator; 264 | 265 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 266 | let obj = get_poll_result(env, result)?; 267 | let list = JList::from_env(env, obj)?; 268 | let mut peripheral_services = Vec::new(); 269 | let mut peripheral_characteristics = Vec::new(); 270 | 271 | for service in list.iter()? { 272 | let service = JBluetoothGattService::from_env(env, service)?; 273 | let mut characteristics = BTreeSet::::new(); 274 | for characteristic in service.get_characteristics()? { 275 | let mut descriptors = BTreeSet::new(); 276 | for descriptor in characteristic.get_descriptors()? { 277 | descriptors.insert(Descriptor { 278 | uuid: descriptor.get_uuid()?, 279 | service_uuid: service.get_uuid()?, 280 | characteristic_uuid: characteristic.get_uuid()?, 281 | }); 282 | } 283 | let char = Characteristic { 284 | service_uuid: service.get_uuid()?, 285 | uuid: characteristic.get_uuid()?, 286 | properties: characteristic.get_properties()?, 287 | descriptors: descriptors.clone(), 288 | }; 289 | // Only consider the first characteristic of each UUID 290 | // This "should" be unique, but of course it's not enforced 291 | if characteristics 292 | .iter() 293 | .filter(|c| c.service_uuid == char.service_uuid && c.uuid == char.uuid) 294 | .count() 295 | == 0 296 | { 297 | characteristics.insert(char.clone()); 298 | peripheral_characteristics.push(char.clone()); 299 | } 300 | } 301 | peripheral_services.push(Service { 302 | uuid: service.get_uuid()?, 303 | primary: service.is_primary()?, 304 | characteristics, 305 | }) 306 | } 307 | let mut guard = self.shared.lock().map_err(Into::::into)?; 308 | guard.services = BTreeSet::from_iter(peripheral_services.clone()); 309 | guard.characteristics = BTreeSet::from_iter(peripheral_characteristics.clone()); 310 | Ok(()) 311 | }) 312 | } 313 | 314 | async fn write( 315 | &self, 316 | characteristic: &Characteristic, 317 | data: &[u8], 318 | write_type: WriteType, 319 | ) -> Result<()> { 320 | let future = self.with_obj(|env, obj| { 321 | let uuid = JUuid::new(env, characteristic.uuid)?; 322 | let data_obj = jni_utils::arrays::slice_to_byte_array(env, data)?; 323 | let write_type = match write_type { 324 | WriteType::WithResponse => 2, 325 | WriteType::WithoutResponse => 1, 326 | }; 327 | JSendFuture::try_from(obj.write(uuid, data_obj.into(), write_type)?) 328 | })?; 329 | let result_ref = future.await?; 330 | self.with_obj(|env, _obj| { 331 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 332 | get_poll_result(env, result).map(|_| {}) 333 | }) 334 | } 335 | 336 | async fn read(&self, characteristic: &Characteristic) -> Result> { 337 | let future = self.with_obj(|env, obj| { 338 | let uuid = JUuid::new(env, characteristic.uuid)?; 339 | JSendFuture::try_from(obj.read(uuid)?) 340 | })?; 341 | let result_ref = future.await?; 342 | self.with_obj(|env, _obj| { 343 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 344 | let bytes = get_poll_result(env, result)?; 345 | Ok(byte_array_to_vec(env, bytes.into_inner())?) 346 | }) 347 | } 348 | 349 | async fn subscribe(&self, characteristic: &Characteristic) -> Result<()> { 350 | self.set_characteristic_notification(characteristic, true) 351 | .await 352 | } 353 | 354 | async fn unsubscribe(&self, characteristic: &Characteristic) -> Result<()> { 355 | self.set_characteristic_notification(characteristic, false) 356 | .await 357 | } 358 | 359 | async fn notifications(&self) -> Result + Send>>> { 360 | use futures::stream::StreamExt; 361 | let stream = self.with_obj(|_env, obj| JSendStream::try_from(obj.get_notifications()?))?; 362 | let stream = stream 363 | .map(|item| match item { 364 | Ok(item) => { 365 | let env = global_jvm().get_env()?; 366 | let item = item.as_obj(); 367 | let characteristic = JBluetoothGattCharacteristic::from_env(&env, item)?; 368 | let uuid = characteristic.get_uuid()?; 369 | let value = characteristic.get_value()?; 370 | Ok(ValueNotification { uuid, value }) 371 | } 372 | Err(err) => Err(err), 373 | }) 374 | .filter_map(|item| async { item.ok() }); 375 | Ok(Box::pin(stream)) 376 | } 377 | 378 | async fn write_descriptor(&self, descriptor: &Descriptor, data: &[u8]) -> Result<()> { 379 | let future = self.with_obj(|env, obj| { 380 | let characteristic = JUuid::new(env, descriptor.characteristic_uuid)?; 381 | let uuid = JUuid::new(env, descriptor.uuid)?; 382 | let data_obj = jni_utils::arrays::slice_to_byte_array(env, data)?; 383 | JSendFuture::try_from(obj.write_descriptor(characteristic, uuid, data_obj.into())?) 384 | })?; 385 | let result_ref = future.await?; 386 | self.with_obj(|env, _obj| { 387 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 388 | get_poll_result(env, result).map(|_| {}) 389 | }) 390 | } 391 | 392 | async fn read_descriptor(&self, descriptor: &Descriptor) -> Result> { 393 | let future = self.with_obj(|env, obj| { 394 | let characteristic = JUuid::new(env, descriptor.characteristic_uuid)?; 395 | let uuid = JUuid::new(env, descriptor.uuid)?; 396 | JSendFuture::try_from(obj.read_descriptor(characteristic, uuid)?) 397 | })?; 398 | let result_ref = future.await?; 399 | self.with_obj(|env, _obj| { 400 | let result = JPollResult::from_env(env, result_ref.as_obj())?; 401 | let bytes = get_poll_result(env, result)?; 402 | Ok(byte_array_to_vec(env, bytes.into_inner())?) 403 | }) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | //! btleplug is a Bluetooth Low Energy (BLE) central module library for Rust. 15 | //! It currently supports Windows 10, macOS (and possibly iOS) and Linux 16 | //! (BlueZ). Android support is planned for the future. 17 | //! 18 | //! ## Usage 19 | //! 20 | //! An example of how to use the library to control some BLE smart lights: 21 | //! 22 | //! ```rust,no_run 23 | //! use btleplug::api::{bleuuid::uuid_from_u16, Central, Manager as _, Peripheral as _, ScanFilter, WriteType}; 24 | //! use btleplug::platform::{Adapter, Manager, Peripheral}; 25 | //! use rand::{Rng, rng}; 26 | //! use std::error::Error; 27 | //! use std::thread; 28 | //! use std::time::Duration; 29 | //! use tokio::time; 30 | //! use uuid::Uuid; 31 | //! 32 | //! const LIGHT_CHARACTERISTIC_UUID: Uuid = uuid_from_u16(0xFFE9); 33 | //! 34 | //! #[tokio::main] 35 | //! async fn main() -> Result<(), Box> { 36 | //! let manager = Manager::new().await.unwrap(); 37 | //! 38 | //! // get the first bluetooth adapter 39 | //! let adapters = manager.adapters().await?; 40 | //! let central = adapters.into_iter().nth(0).unwrap(); 41 | //! 42 | //! // start scanning for devices 43 | //! central.start_scan(ScanFilter::default()).await?; 44 | //! // instead of waiting, you can use central.events() to get a stream which will 45 | //! // notify you of new devices, for an example of that see examples/event_driven_discovery.rs 46 | //! time::sleep(Duration::from_secs(2)).await; 47 | //! 48 | //! // find the device we're interested in 49 | //! let light = find_light(¢ral).await.unwrap(); 50 | //! 51 | //! // connect to the device 52 | //! light.connect().await?; 53 | //! 54 | //! // discover services and characteristics 55 | //! light.discover_services().await?; 56 | //! 57 | //! // find the characteristic we want 58 | //! let chars = light.characteristics(); 59 | //! let cmd_char = chars.iter().find(|c| c.uuid == LIGHT_CHARACTERISTIC_UUID).unwrap(); 60 | //! 61 | //! // dance party 62 | //! let mut rng = rng(); 63 | //! for _ in 0..20 { 64 | //! let color_cmd = vec![0x56, rng.random(), rng.random(), rng.random(), 0x00, 0xF0, 0xAA]; 65 | //! light.write(&cmd_char, &color_cmd, WriteType::WithoutResponse).await?; 66 | //! time::sleep(Duration::from_millis(200)).await; 67 | //! } 68 | //! Ok(()) 69 | //! } 70 | //! 71 | //! async fn find_light(central: &Adapter) -> Option { 72 | //! for p in central.peripherals().await.unwrap() { 73 | //! if p.properties() 74 | //! .await 75 | //! .unwrap() 76 | //! .unwrap() 77 | //! .local_name 78 | //! .iter() 79 | //! .any(|name| name.contains("LEDBlue")) 80 | //! { 81 | //! return Some(p); 82 | //! } 83 | //! } 84 | //! None 85 | //! } 86 | //! ``` 87 | 88 | use crate::api::ParseBDAddrError; 89 | use std::result; 90 | use std::time::Duration; 91 | 92 | pub mod api; 93 | #[cfg(target_os = "linux")] 94 | mod bluez; 95 | #[cfg(not(target_os = "linux"))] 96 | mod common; 97 | #[cfg(target_vendor = "apple")] 98 | mod corebluetooth; 99 | #[cfg(target_os = "android")] 100 | mod droidplug; 101 | pub mod platform; 102 | #[cfg(feature = "serde")] 103 | pub mod serde; 104 | #[cfg(target_os = "windows")] 105 | mod winrtble; 106 | 107 | /// The main error type returned by most methods in btleplug. 108 | #[derive(Debug, thiserror::Error)] 109 | pub enum Error { 110 | #[error("Permission denied")] 111 | PermissionDenied, 112 | 113 | #[error("Device not found")] 114 | DeviceNotFound, 115 | 116 | #[error("Not connected")] 117 | NotConnected, 118 | 119 | #[error("Unexpected callback")] 120 | UnexpectedCallback, 121 | 122 | #[error("Unexpected characteristic")] 123 | UnexpectedCharacteristic, 124 | 125 | #[error("No such characteristic")] 126 | NoSuchCharacteristic, 127 | 128 | #[error("The operation is not supported: {}", _0)] 129 | NotSupported(String), 130 | 131 | #[error("Timed out after {:?}", _0)] 132 | TimedOut(Duration), 133 | 134 | #[error("Error parsing UUID: {0}")] 135 | Uuid(#[from] uuid::Error), 136 | 137 | #[error("Invalid Bluetooth address: {0}")] 138 | InvalidBDAddr(#[from] ParseBDAddrError), 139 | 140 | #[error("Runtime Error: {}", _0)] 141 | RuntimeError(String), 142 | 143 | #[error("{}", _0)] 144 | Other(Box), 145 | } 146 | 147 | /// Convert [`PoisonError`] to [`Error`] for replace `unwrap` to `map_err` 148 | impl From> for Error { 149 | fn from(e: std::sync::PoisonError) -> Self { 150 | Self::Other(format!("{:?}", e).into()) 151 | } 152 | } 153 | 154 | /// Convenience type for a result using the btleplug [`Error`] type. 155 | pub type Result = result::Result; 156 | -------------------------------------------------------------------------------- /src/platform.rs: -------------------------------------------------------------------------------- 1 | //! The `platform` module contains the platform-specific implementations of the various [`api`] 2 | //! traits. Refer for the `api` module for how to use them. 3 | 4 | #[cfg(target_os = "linux")] 5 | pub use crate::bluez::{ 6 | adapter::Adapter, manager::Manager, peripheral::Peripheral, peripheral::PeripheralId, 7 | }; 8 | #[cfg(target_vendor = "apple")] 9 | pub use crate::corebluetooth::{ 10 | adapter::Adapter, manager::Manager, peripheral::Peripheral, peripheral::PeripheralId, 11 | }; 12 | #[cfg(target_os = "android")] 13 | pub use crate::droidplug::{ 14 | adapter::Adapter, init, manager::Manager, peripheral::Peripheral, peripheral::PeripheralId, 15 | }; 16 | #[cfg(target_os = "windows")] 17 | pub use crate::winrtble::{ 18 | adapter::Adapter, manager::Manager, peripheral::Peripheral, peripheral::PeripheralId, 19 | }; 20 | 21 | use crate::api::{self, Central}; 22 | use static_assertions::assert_impl_all; 23 | use std::{ 24 | fmt::{Debug, Display}, 25 | hash::Hash, 26 | }; 27 | 28 | // Ensure that the exported types implement all the expected traits. 29 | assert_impl_all!(Adapter: Central, Clone, Debug, Send, Sized, Sync); 30 | assert_impl_all!(Manager: api::Manager, Clone, Debug, Send, Sized, Sync); 31 | assert_impl_all!(Peripheral: api::Peripheral, Clone, Debug, Send, Sized, Sync); 32 | assert_impl_all!( 33 | PeripheralId: Clone, 34 | Debug, 35 | Display, 36 | Hash, 37 | Eq, 38 | Ord, 39 | PartialEq, 40 | PartialOrd, 41 | Send, 42 | Sized, 43 | Sync 44 | ); 45 | -------------------------------------------------------------------------------- /src/serde.rs: -------------------------------------------------------------------------------- 1 | //! De-/Serialization with alternative formats. 2 | //! 3 | //! The various modules in here are intended to be used with `serde`'s [`with` annotation] to de-/serialize as something other than the default format. 4 | //! 5 | //! [`with` annotation]: https://serde.rs/attributes.html#field-attributes 6 | 7 | /// Different de-/serialization formats for [`crate::api::BDAddr`]. 8 | pub mod bdaddr { 9 | pub use crate::api::bdaddr::serde::*; 10 | } 11 | -------------------------------------------------------------------------------- /src/winrtble/adapter.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | use super::{ble::watcher::BLEWatcher, peripheral::Peripheral, peripheral::PeripheralId}; 15 | use crate::{ 16 | api::{BDAddr, Central, CentralEvent, CentralState, ScanFilter}, 17 | common::adapter_manager::AdapterManager, 18 | Error, Result, 19 | }; 20 | use async_trait::async_trait; 21 | use futures::stream::Stream; 22 | use std::convert::TryInto; 23 | use std::fmt::{self, Debug, Formatter}; 24 | use std::pin::Pin; 25 | use std::sync::{Arc, Mutex}; 26 | use windows::{ 27 | Devices::Radios::{Radio, RadioState}, 28 | Foundation::TypedEventHandler, 29 | }; 30 | 31 | /// Implementation of [api::Central](crate::api::Central). 32 | #[derive(Clone)] 33 | pub struct Adapter { 34 | watcher: Arc>, 35 | manager: Arc>, 36 | radio: Radio, 37 | } 38 | 39 | // https://github.com/microsoft/windows-rs/blob/master/crates/libs/windows/src/Windows/Devices/Radios/mod.rs 40 | fn get_central_state(radio: &Radio) -> CentralState { 41 | let state = radio.State().unwrap_or(RadioState::Unknown); 42 | match state { 43 | RadioState::On => CentralState::PoweredOn, 44 | RadioState::Off => CentralState::PoweredOff, 45 | _ => CentralState::Unknown, 46 | } 47 | } 48 | 49 | impl Adapter { 50 | pub(crate) fn new(radio: Radio) -> Result { 51 | let watcher = Arc::new(Mutex::new(BLEWatcher::new()?)); 52 | let manager = Arc::new(AdapterManager::default()); 53 | 54 | let radio_clone = radio.clone(); 55 | let manager_clone = manager.clone(); 56 | let handler = TypedEventHandler::new(move |_sender, _args| { 57 | let state = get_central_state(&radio_clone); 58 | manager_clone.emit(CentralEvent::StateUpdate(state.into())); 59 | Ok(()) 60 | }); 61 | if let Err(err) = radio.StateChanged(&handler) { 62 | eprintln!("radio.StateChanged error: {}", err); 63 | } 64 | 65 | Ok(Adapter { 66 | watcher, 67 | manager, 68 | radio, 69 | }) 70 | } 71 | } 72 | 73 | impl Debug for Adapter { 74 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 75 | f.debug_struct("Adapter") 76 | .field("manager", &self.manager) 77 | .finish() 78 | } 79 | } 80 | 81 | #[async_trait] 82 | impl Central for Adapter { 83 | type Peripheral = Peripheral; 84 | 85 | async fn events(&self) -> Result + Send>>> { 86 | Ok(self.manager.event_stream()) 87 | } 88 | 89 | async fn start_scan(&self, filter: ScanFilter) -> Result<()> { 90 | let watcher = self.watcher.lock().map_err(Into::::into)?; 91 | let manager = self.manager.clone(); 92 | watcher.start( 93 | filter, 94 | Box::new(move |args| { 95 | let bluetooth_address = args.BluetoothAddress()?; 96 | let address: BDAddr = bluetooth_address.try_into().unwrap(); 97 | if let Some(mut entry) = manager.peripheral_mut(&address.into()) { 98 | entry.value_mut().update_properties(args); 99 | manager.emit(CentralEvent::DeviceUpdated(address.into())); 100 | } else { 101 | let peripheral = Peripheral::new(Arc::downgrade(&manager), address); 102 | peripheral.update_properties(args); 103 | manager.add_peripheral(peripheral); 104 | manager.emit(CentralEvent::DeviceDiscovered(address.into())); 105 | } 106 | Ok(()) 107 | }), 108 | ) 109 | } 110 | 111 | async fn stop_scan(&self) -> Result<()> { 112 | let watcher = self.watcher.lock().map_err(Into::::into)?; 113 | watcher.stop()?; 114 | Ok(()) 115 | } 116 | 117 | async fn peripherals(&self) -> Result> { 118 | Ok(self.manager.peripherals()) 119 | } 120 | 121 | async fn peripheral(&self, id: &PeripheralId) -> Result { 122 | self.manager.peripheral(id).ok_or(Error::DeviceNotFound) 123 | } 124 | 125 | async fn add_peripheral(&self, _address: &PeripheralId) -> Result { 126 | Err(Error::NotSupported( 127 | "Can't add a Peripheral from a BDAddr".to_string(), 128 | )) 129 | } 130 | 131 | async fn adapter_info(&self) -> Result { 132 | // TODO: Get information about the adapter. 133 | Ok("WinRT".to_string()) 134 | } 135 | 136 | async fn adapter_state(&self) -> Result { 137 | Ok(get_central_state(&self.radio)) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/winrtble/ble/characteristic.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | use super::{super::utils::to_descriptor_value, descriptor::BLEDescriptor}; 15 | use crate::{ 16 | api::{Characteristic, WriteType}, 17 | winrtble::utils, 18 | Error, Result, 19 | }; 20 | 21 | use log::{debug, trace}; 22 | use std::{collections::HashMap, future::IntoFuture}; 23 | use uuid::Uuid; 24 | use windows::core::Ref; 25 | use windows::{ 26 | Devices::Bluetooth::{ 27 | BluetoothCacheMode, 28 | GenericAttributeProfile::{ 29 | GattCharacteristic, GattClientCharacteristicConfigurationDescriptorValue, 30 | GattCommunicationStatus, GattValueChangedEventArgs, GattWriteOption, 31 | }, 32 | }, 33 | Foundation::TypedEventHandler, 34 | Storage::Streams::{DataReader, DataWriter}, 35 | }; 36 | 37 | pub type NotifiyEventHandler = Box) + Send>; 38 | 39 | impl From for GattWriteOption { 40 | fn from(val: WriteType) -> Self { 41 | match val { 42 | WriteType::WithoutResponse => GattWriteOption::WriteWithoutResponse, 43 | WriteType::WithResponse => GattWriteOption::WriteWithResponse, 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug)] 49 | pub struct BLECharacteristic { 50 | characteristic: GattCharacteristic, 51 | pub descriptors: HashMap, 52 | notify_token: Option, 53 | } 54 | 55 | impl BLECharacteristic { 56 | pub fn new( 57 | characteristic: GattCharacteristic, 58 | descriptors: HashMap, 59 | ) -> Self { 60 | BLECharacteristic { 61 | characteristic, 62 | descriptors, 63 | notify_token: None, 64 | } 65 | } 66 | 67 | pub async fn write_value(&self, data: &[u8], write_type: WriteType) -> Result<()> { 68 | let writer = DataWriter::new()?; 69 | writer.WriteBytes(data)?; 70 | let operation = self 71 | .characteristic 72 | .WriteValueWithOptionAsync(&writer.DetachBuffer()?, write_type.into())?; 73 | let result = operation.into_future().await?; 74 | if result == GattCommunicationStatus::Success { 75 | Ok(()) 76 | } else { 77 | Err(Error::Other( 78 | format!("Windows UWP threw error on write: {:?}", result).into(), 79 | )) 80 | } 81 | } 82 | 83 | pub async fn read_value(&self) -> Result> { 84 | let result = self 85 | .characteristic 86 | .ReadValueWithCacheModeAsync(BluetoothCacheMode::Uncached)? 87 | .into_future() 88 | .await?; 89 | if result.Status()? == GattCommunicationStatus::Success { 90 | let value = result.Value()?; 91 | let reader = DataReader::FromBuffer(&value)?; 92 | let len = reader.UnconsumedBufferLength()? as usize; 93 | let mut input = vec![0u8; len]; 94 | reader.ReadBytes(&mut input[0..len])?; 95 | Ok(input) 96 | } else { 97 | Err(Error::Other( 98 | format!("Windows UWP threw error on read: {:?}", result).into(), 99 | )) 100 | } 101 | } 102 | 103 | pub async fn subscribe(&mut self, on_value_changed: NotifiyEventHandler) -> Result<()> { 104 | { 105 | let value_handler = TypedEventHandler::new( 106 | move |_: Ref, args: Ref| { 107 | if let Ok(args) = args.ok() { 108 | let value = args.CharacteristicValue()?; 109 | let reader = DataReader::FromBuffer(&value)?; 110 | let len = reader.UnconsumedBufferLength()? as usize; 111 | let mut input: Vec = vec![0u8; len]; 112 | reader.ReadBytes(&mut input[0..len])?; 113 | trace!("changed {:?}", input); 114 | on_value_changed(input); 115 | } 116 | Ok(()) 117 | }, 118 | ); 119 | let token = self.characteristic.ValueChanged(&value_handler)?; 120 | self.notify_token = Some(token); 121 | } 122 | let config = to_descriptor_value(self.characteristic.CharacteristicProperties()?); 123 | if config == GattClientCharacteristicConfigurationDescriptorValue::None { 124 | return Err(Error::NotSupported("Can not subscribe to attribute".into())); 125 | } 126 | 127 | let status = self 128 | .characteristic 129 | .WriteClientCharacteristicConfigurationDescriptorAsync(config)? 130 | .into_future() 131 | .await?; 132 | trace!("subscribe {:?}", status); 133 | if status == GattCommunicationStatus::Success { 134 | Ok(()) 135 | } else { 136 | Err(Error::Other( 137 | format!("Windows UWP threw error on subscribe: {:?}", status).into(), 138 | )) 139 | } 140 | } 141 | 142 | pub async fn unsubscribe(&mut self) -> Result<()> { 143 | if let Some(token) = &self.notify_token { 144 | self.characteristic.RemoveValueChanged(*token)?; 145 | } 146 | self.notify_token = None; 147 | let config = GattClientCharacteristicConfigurationDescriptorValue::None; 148 | let status = self 149 | .characteristic 150 | .WriteClientCharacteristicConfigurationDescriptorAsync(config)? 151 | .into_future() 152 | .await?; 153 | trace!("unsubscribe {:?}", status); 154 | if status == GattCommunicationStatus::Success { 155 | Ok(()) 156 | } else { 157 | Err(Error::Other( 158 | format!("Windows UWP threw error on unsubscribe: {:?}", status).into(), 159 | )) 160 | } 161 | } 162 | 163 | pub fn uuid(&self) -> Uuid { 164 | utils::to_uuid(&self.characteristic.Uuid().unwrap()) 165 | } 166 | 167 | pub fn to_characteristic(&self, service_uuid: Uuid) -> Characteristic { 168 | let uuid = self.uuid(); 169 | let properties = 170 | utils::to_char_props(&self.characteristic.CharacteristicProperties().unwrap()); 171 | let descriptors = self 172 | .descriptors 173 | .values() 174 | .map(|descriptor| descriptor.to_descriptor(service_uuid, uuid)) 175 | .collect(); 176 | Characteristic { 177 | uuid, 178 | service_uuid, 179 | descriptors, 180 | properties, 181 | } 182 | } 183 | } 184 | 185 | impl Drop for BLECharacteristic { 186 | fn drop(&mut self) { 187 | if let Some(token) = &self.notify_token { 188 | let result = self.characteristic.RemoveValueChanged(*token); 189 | if let Err(err) = result { 190 | debug!("Drop:remove_connection_status_changed {:?}", err); 191 | } 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/winrtble/ble/descriptor.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | use super::super::utils; 15 | use crate::{api::Descriptor, Error, Result}; 16 | use std::future::IntoFuture; 17 | use uuid::Uuid; 18 | use windows::{ 19 | Devices::Bluetooth::{ 20 | BluetoothCacheMode, 21 | GenericAttributeProfile::{GattCommunicationStatus, GattDescriptor}, 22 | }, 23 | Storage::Streams::{DataReader, DataWriter}, 24 | }; 25 | 26 | #[derive(Debug)] 27 | pub struct BLEDescriptor { 28 | descriptor: GattDescriptor, 29 | } 30 | 31 | impl BLEDescriptor { 32 | pub fn new(descriptor: GattDescriptor) -> Self { 33 | Self { descriptor } 34 | } 35 | 36 | pub fn uuid(&self) -> Uuid { 37 | utils::to_uuid(&self.descriptor.Uuid().unwrap()) 38 | } 39 | 40 | pub fn to_descriptor(&self, service_uuid: Uuid, characteristic_uuid: Uuid) -> Descriptor { 41 | let uuid = self.uuid(); 42 | Descriptor { 43 | uuid, 44 | service_uuid, 45 | characteristic_uuid, 46 | } 47 | } 48 | 49 | pub async fn write_value(&self, data: &[u8]) -> Result<()> { 50 | let writer = DataWriter::new()?; 51 | writer.WriteBytes(data)?; 52 | let operation = self.descriptor.WriteValueAsync(&writer.DetachBuffer()?)?; 53 | let result = operation.into_future().await?; 54 | if result == GattCommunicationStatus::Success { 55 | Ok(()) 56 | } else { 57 | Err(Error::Other( 58 | format!("Windows UWP threw error on write descriptor: {:?}", result).into(), 59 | )) 60 | } 61 | } 62 | 63 | pub async fn read_value(&self) -> Result> { 64 | let result = self 65 | .descriptor 66 | .ReadValueWithCacheModeAsync(BluetoothCacheMode::Uncached)? 67 | .into_future() 68 | .await?; 69 | if result.Status()? == GattCommunicationStatus::Success { 70 | let value = result.Value()?; 71 | let reader = DataReader::FromBuffer(&value)?; 72 | let len = reader.UnconsumedBufferLength()? as usize; 73 | let mut input = vec![0u8; len]; 74 | reader.ReadBytes(&mut input[0..len])?; 75 | Ok(input) 76 | } else { 77 | Err(Error::Other( 78 | format!("Windows UWP threw error on read: {:?}", result).into(), 79 | )) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/winrtble/ble/device.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | use crate::{api::BDAddr, winrtble::utils, Error, Result}; 15 | use log::{debug, trace}; 16 | use std::future::IntoFuture; 17 | use windows::{ 18 | core::Ref, 19 | Devices::Bluetooth::{ 20 | BluetoothCacheMode, BluetoothConnectionStatus, BluetoothLEDevice, 21 | GenericAttributeProfile::{ 22 | GattCharacteristic, GattCommunicationStatus, GattDescriptor, GattDeviceService, 23 | GattDeviceServicesResult, 24 | }, 25 | }, 26 | Foundation::TypedEventHandler, 27 | }; 28 | 29 | pub type ConnectedEventHandler = Box; 30 | 31 | pub struct BLEDevice { 32 | device: BluetoothLEDevice, 33 | connection_token: i64, 34 | services: Vec, 35 | } 36 | 37 | impl BLEDevice { 38 | pub async fn new( 39 | address: BDAddr, 40 | connection_status_changed: ConnectedEventHandler, 41 | ) -> Result { 42 | let async_op = BluetoothLEDevice::FromBluetoothAddressAsync(address.into()) 43 | .map_err(|_| Error::DeviceNotFound)?; 44 | let device = async_op 45 | .into_future() 46 | .await 47 | .map_err(|_| Error::DeviceNotFound)?; 48 | let connection_status_handler = 49 | TypedEventHandler::new(move |sender: Ref, _| { 50 | if let Ok(sender) = sender.ok() { 51 | let is_connected = sender 52 | .ConnectionStatus() 53 | .ok() 54 | .map_or(false, |v| v == BluetoothConnectionStatus::Connected); 55 | connection_status_changed(is_connected); 56 | trace!("state {:?}", sender.ConnectionStatus()); 57 | } 58 | 59 | Ok(()) 60 | }); 61 | let connection_token = device 62 | .ConnectionStatusChanged(&connection_status_handler) 63 | .map_err(|_| Error::Other("Could not add connection status handler".into()))?; 64 | 65 | Ok(BLEDevice { 66 | device, 67 | connection_token, 68 | services: vec![], 69 | }) 70 | } 71 | 72 | async fn get_gatt_services( 73 | &self, 74 | cache_mode: BluetoothCacheMode, 75 | ) -> Result { 76 | let winrt_error = |e| Error::Other(format!("{:?}", e).into()); 77 | let async_op = self 78 | .device 79 | .GetGattServicesWithCacheModeAsync(cache_mode) 80 | .map_err(winrt_error)?; 81 | let service_result = async_op.into_future().await.map_err(winrt_error)?; 82 | Ok(service_result) 83 | } 84 | 85 | pub async fn connect(&self) -> Result<()> { 86 | if self.is_connected().await? { 87 | return Ok(()); 88 | } 89 | 90 | let service_result = self.get_gatt_services(BluetoothCacheMode::Uncached).await?; 91 | let status = service_result.Status().map_err(|_| Error::DeviceNotFound)?; 92 | utils::to_error(status) 93 | } 94 | 95 | async fn is_connected(&self) -> Result { 96 | let winrt_error = |e| Error::Other(format!("{:?}", e).into()); 97 | let status = self.device.ConnectionStatus().map_err(winrt_error)?; 98 | 99 | Ok(status == BluetoothConnectionStatus::Connected) 100 | } 101 | 102 | pub async fn get_characteristics( 103 | service: &GattDeviceService, 104 | ) -> Result> { 105 | let async_result = service 106 | .GetCharacteristicsWithCacheModeAsync(BluetoothCacheMode::Uncached)? 107 | .into_future() 108 | .await?; 109 | 110 | match async_result.Status() { 111 | Ok(GattCommunicationStatus::Success) => { 112 | let results = async_result.Characteristics()?; 113 | debug!("characteristics {:?}", results.Size()); 114 | Ok(results.into_iter().collect()) 115 | } 116 | Ok(GattCommunicationStatus::ProtocolError) => Err(Error::Other( 117 | format!( 118 | "get_characteristics for {:?} encountered a protocol error", 119 | service 120 | ) 121 | .into(), 122 | )), 123 | Ok(status) => { 124 | debug!("characteristic read failed due to {:?}", status); 125 | Ok(vec![]) 126 | } 127 | Err(e) => Err(Error::Other( 128 | format!("get_characteristics for {:?} failed: {:?}", service, e).into(), 129 | )), 130 | } 131 | } 132 | 133 | pub async fn get_characteristic_descriptors( 134 | characteristic: &GattCharacteristic, 135 | ) -> Result> { 136 | let async_result = characteristic 137 | .GetDescriptorsWithCacheModeAsync(BluetoothCacheMode::Uncached)? 138 | .into_future() 139 | .await?; 140 | let status = async_result.Status(); 141 | if status == Ok(GattCommunicationStatus::Success) { 142 | let results = async_result.Descriptors()?; 143 | debug!("descriptors {:?}", results.Size()); 144 | Ok(results.into_iter().collect()) 145 | } else { 146 | Err(Error::Other( 147 | format!( 148 | "get_characteristic_descriptors for {:?} failed: {:?}", 149 | characteristic, status 150 | ) 151 | .into(), 152 | )) 153 | } 154 | } 155 | 156 | pub async fn discover_services(&mut self) -> Result<&[GattDeviceService]> { 157 | let winrt_error = |e| Error::Other(format!("{:?}", e).into()); 158 | let service_result = self.get_gatt_services(BluetoothCacheMode::Uncached).await?; 159 | let status = service_result.Status().map_err(winrt_error)?; 160 | if status == GattCommunicationStatus::Success { 161 | // We need to convert the IVectorView to a Vec, because IVectorView is not Send and so 162 | // can't be help past the await point below. 163 | let services: Vec<_> = service_result 164 | .Services() 165 | .map_err(winrt_error)? 166 | .into_iter() 167 | .collect(); 168 | self.services = services; 169 | debug!("services {:?}", self.services.len()); 170 | } 171 | Ok(self.services.as_slice()) 172 | } 173 | } 174 | 175 | impl Drop for BLEDevice { 176 | fn drop(&mut self) { 177 | let result = self 178 | .device 179 | .RemoveConnectionStatusChanged(self.connection_token); 180 | if let Err(err) = result { 181 | debug!("Drop:remove_connection_status_changed {:?}", err); 182 | } 183 | 184 | self.services.iter().for_each(|service| { 185 | if let Err(err) = service.Close() { 186 | debug!("Drop:remove_gatt_Service {:?}", err); 187 | } 188 | }); 189 | 190 | let result = self.device.Close(); 191 | if let Err(err) = result { 192 | debug!("Drop:close {:?}", err); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/winrtble/ble/mod.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | pub mod characteristic; 15 | pub mod descriptor; 16 | pub mod device; 17 | pub mod service; 18 | pub mod watcher; 19 | -------------------------------------------------------------------------------- /src/winrtble/ble/service.rs: -------------------------------------------------------------------------------- 1 | use super::characteristic::BLECharacteristic; 2 | use crate::api::Service; 3 | use std::collections::HashMap; 4 | use uuid::Uuid; 5 | 6 | #[derive(Debug)] 7 | pub struct BLEService { 8 | pub uuid: Uuid, 9 | pub characteristics: HashMap, 10 | } 11 | 12 | impl BLEService { 13 | pub fn to_service(&self) -> Service { 14 | let characteristics = self 15 | .characteristics 16 | .values() 17 | .map(|ble_characteristic| ble_characteristic.to_characteristic(self.uuid)) 18 | .collect(); 19 | Service { 20 | uuid: self.uuid, 21 | primary: true, 22 | characteristics, 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/winrtble/ble/watcher.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | use crate::{api::ScanFilter, Error, Result}; 15 | use windows::{core::Ref, Devices::Bluetooth::Advertisement::*, Foundation::TypedEventHandler}; 16 | 17 | pub type AdvertisementEventHandler = 18 | Box windows::core::Result<()> + Send>; 19 | 20 | #[derive(Debug)] 21 | pub struct BLEWatcher { 22 | watcher: BluetoothLEAdvertisementWatcher, 23 | } 24 | 25 | impl From for Error { 26 | fn from(err: windows::core::Error) -> Error { 27 | Error::Other(format!("{:?}", err).into()) 28 | } 29 | } 30 | 31 | impl BLEWatcher { 32 | pub fn new() -> Result { 33 | let ad = BluetoothLEAdvertisementFilter::new()?; 34 | let watcher = BluetoothLEAdvertisementWatcher::Create(&ad)?; 35 | Ok(BLEWatcher { watcher }) 36 | } 37 | 38 | pub fn start(&self, filter: ScanFilter, on_received: AdvertisementEventHandler) -> Result<()> { 39 | let ScanFilter { services } = filter; 40 | let ad = self.watcher.AdvertisementFilter()?.Advertisement()?; 41 | let ad_services = ad.ServiceUuids()?; 42 | ad_services.Clear()?; 43 | for service in services { 44 | ad_services.Append(windows::core::GUID::from(service.as_u128()))?; 45 | } 46 | self.watcher 47 | .SetScanningMode(BluetoothLEScanningMode::Active)?; 48 | let _ = self.watcher.SetAllowExtendedAdvertisements(true); 49 | let handler: TypedEventHandler< 50 | BluetoothLEAdvertisementWatcher, 51 | BluetoothLEAdvertisementReceivedEventArgs, 52 | > = TypedEventHandler::new( 53 | move |_sender, args: Ref| { 54 | if let Ok(args) = args.ok() { 55 | on_received(args)?; 56 | } 57 | Ok(()) 58 | }, 59 | ); 60 | 61 | self.watcher.Received(&handler)?; 62 | self.watcher.Start()?; 63 | Ok(()) 64 | } 65 | 66 | pub fn stop(&self) -> Result<()> { 67 | self.watcher.Stop()?; 68 | Ok(()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/winrtble/manager.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | use super::adapter::Adapter; 15 | use crate::{api, Result}; 16 | use async_trait::async_trait; 17 | use std::future::IntoFuture; 18 | use windows::Devices::Radios::{Radio, RadioKind}; 19 | 20 | /// Implementation of [api::Manager](crate::api::Manager). 21 | #[derive(Clone, Debug)] 22 | pub struct Manager {} 23 | 24 | impl Manager { 25 | pub async fn new() -> Result { 26 | Ok(Self {}) 27 | } 28 | } 29 | 30 | #[async_trait] 31 | impl api::Manager for Manager { 32 | type Adapter = Adapter; 33 | 34 | async fn adapters(&self) -> Result> { 35 | let radios = Radio::GetRadiosAsync()?.into_future().await?; 36 | radios 37 | .into_iter() 38 | .filter(|radio| radio.Kind() == Ok(RadioKind::Bluetooth)) 39 | .map(|radio| Adapter::new(radio)) 40 | .collect() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/winrtble/mod.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | pub mod adapter; 15 | mod ble; 16 | pub mod manager; 17 | pub mod peripheral; 18 | mod utils; 19 | 20 | /// Only some of the assigned numbers are populated here as needed from https://www.bluetooth.com/specifications/assigned-numbers/generic-access-profile/ 21 | mod advertisement_data_type { 22 | pub const SERVICE_DATA_16_BIT_UUID: u8 = 0x16; 23 | pub const SERVICE_DATA_32_BIT_UUID: u8 = 0x20; 24 | pub const SERVICE_DATA_128_BIT_UUID: u8 = 0x21; 25 | } 26 | -------------------------------------------------------------------------------- /src/winrtble/utils.rs: -------------------------------------------------------------------------------- 1 | // btleplug Source Code File 2 | // 3 | // Copyright 2020 Nonpolynomial Labs LLC. All rights reserved. 4 | // 5 | // Licensed under the BSD 3-Clause license. See LICENSE file in the project root 6 | // for full license information. 7 | // 8 | // Some portions of this file are taken and/or modified from Rumble 9 | // (https://github.com/mwylde/rumble), using a dual MIT/Apache License under the 10 | // following copyright: 11 | // 12 | // Copyright (c) 2014 The Rust Project Developers 13 | 14 | use crate::{api::CharPropFlags, Error, Result}; 15 | use std::str::FromStr; 16 | use uuid::Uuid; 17 | use windows::core::GUID; 18 | use windows::{ 19 | Devices::Bluetooth::GenericAttributeProfile::{ 20 | GattCharacteristicProperties, GattClientCharacteristicConfigurationDescriptorValue, 21 | GattCommunicationStatus, 22 | }, 23 | Storage::Streams::{DataReader, IBuffer}, 24 | }; 25 | 26 | pub fn to_error(status: GattCommunicationStatus) -> Result<()> { 27 | if status == GattCommunicationStatus::AccessDenied { 28 | Err(Error::PermissionDenied) 29 | } else if status == GattCommunicationStatus::Unreachable { 30 | Err(Error::NotConnected) 31 | } else if status == GattCommunicationStatus::Success { 32 | Ok(()) 33 | } else if status == GattCommunicationStatus::ProtocolError { 34 | Err(Error::NotSupported("ProtocolError".to_string())) 35 | } else { 36 | Err(Error::Other("Communication Error:".to_string().into())) 37 | } 38 | } 39 | 40 | pub fn to_descriptor_value( 41 | properties: GattCharacteristicProperties, 42 | ) -> GattClientCharacteristicConfigurationDescriptorValue { 43 | let notify = GattCharacteristicProperties::Notify; 44 | let indicate = GattCharacteristicProperties::Indicate; 45 | if properties & indicate == indicate { 46 | GattClientCharacteristicConfigurationDescriptorValue::Indicate 47 | } else if properties & notify == notify { 48 | GattClientCharacteristicConfigurationDescriptorValue::Notify 49 | } else { 50 | GattClientCharacteristicConfigurationDescriptorValue::None 51 | } 52 | } 53 | 54 | pub fn to_uuid(uuid: &GUID) -> Uuid { 55 | let guid_s = format!("{:?}", uuid); 56 | Uuid::from_str(&guid_s).unwrap() 57 | } 58 | 59 | pub fn to_vec(buffer: &IBuffer) -> Vec { 60 | let reader = DataReader::FromBuffer(buffer).unwrap(); 61 | let len = reader.UnconsumedBufferLength().unwrap() as usize; 62 | let mut data = vec![0u8; len]; 63 | reader.ReadBytes(&mut data).unwrap(); 64 | data 65 | } 66 | 67 | #[allow(dead_code)] 68 | pub fn to_guid(uuid: &Uuid) -> GUID { 69 | let (data1, data2, data3, data4) = uuid.as_fields(); 70 | GUID::from_values(data1, data2, data3, data4.to_owned()) 71 | } 72 | 73 | pub fn to_char_props(props: &GattCharacteristicProperties) -> CharPropFlags { 74 | let mut flags = CharPropFlags::default(); 75 | if *props & GattCharacteristicProperties::Broadcast != GattCharacteristicProperties::None { 76 | flags |= CharPropFlags::BROADCAST; 77 | } 78 | if *props & GattCharacteristicProperties::Read != GattCharacteristicProperties::None { 79 | flags |= CharPropFlags::READ; 80 | } 81 | if *props & GattCharacteristicProperties::WriteWithoutResponse 82 | != GattCharacteristicProperties::None 83 | { 84 | flags |= CharPropFlags::WRITE_WITHOUT_RESPONSE; 85 | } 86 | if *props & GattCharacteristicProperties::Write != GattCharacteristicProperties::None { 87 | flags |= CharPropFlags::WRITE; 88 | } 89 | if *props & GattCharacteristicProperties::Notify != GattCharacteristicProperties::None { 90 | flags |= CharPropFlags::NOTIFY; 91 | } 92 | if *props & GattCharacteristicProperties::Indicate != GattCharacteristicProperties::None { 93 | flags |= CharPropFlags::INDICATE; 94 | } 95 | if *props & GattCharacteristicProperties::AuthenticatedSignedWrites 96 | != GattCharacteristicProperties::None 97 | { 98 | flags |= CharPropFlags::AUTHENTICATED_SIGNED_WRITES; 99 | } 100 | if *props & GattCharacteristicProperties::ExtendedProperties 101 | != GattCharacteristicProperties::None 102 | { 103 | flags |= CharPropFlags::EXTENDED_PROPERTIES; 104 | } 105 | flags 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | use super::*; 111 | 112 | #[test] 113 | fn check_uuid_to_guid_conversion() { 114 | let uuid_str = "10B201FF-5B3B-45A1-9508-CF3EFCD7BBAF"; 115 | let uuid = Uuid::from_str(uuid_str).unwrap(); 116 | 117 | let guid_converted = to_guid(&uuid); 118 | 119 | let guid_expected = GUID::try_from(uuid_str).unwrap(); 120 | assert_eq!(guid_converted, guid_expected); 121 | } 122 | 123 | #[test] 124 | fn check_guid_to_uuid_conversion() { 125 | let uuid_str = "10B201FF-5B3B-45A1-9508-CF3EFCD7BBAF"; 126 | let guid = GUID::try_from(uuid_str).unwrap(); 127 | 128 | let uuid_converted = to_uuid(&guid); 129 | 130 | let uuid_expected = Uuid::from_str(uuid_str).unwrap(); 131 | assert_eq!(uuid_converted, uuid_expected); 132 | } 133 | } 134 | --------------------------------------------------------------------------------