btleplug: one async Rust API over BlueZ, CoreBluetooth, WinRT and Android
Rust Cross-Platform Host-Side Bluetooth LE Access Library
At a glance
- What is it?
- btleplug is a host-side Bluetooth LE library for Rust that presents a single API over four platform stacks, and it is explicit about the gaps between them, including a Linux scan filter that returns the union of every application's filters. It supports BLE only, never Classic Bluetooth.
- Who is it for?
- btleplug is the right choice for a Rust project that needs Bluetooth LE on more than one operating system and does not want to write four backends, because the platform caveat documentation is candid enough to plan around, particularly the BlueZ filter behaviour; it is the wrong choice if you need Classic Bluetooth, if you need peripheral mode, or if your target is a WebAssembly browser build, where the support is still a pull request awaiting review, so before you commit you should read the platform feature table rather than assuming the four platforms behave alike, because MAC address discovery, multiple adapter handling and adapter state are marked complete only on some of them.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 13 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Host side only, Bluetooth LE only
btleplug is described as a cross-platform host-side Bluetooth LE access library, and the project documentation is unusually precise about what that excludes.
It is host and central mode only. If you want to act as a Bluetooth LE device rather than connect to one, the project points you to bluster or ble-peripheral-rust instead. And the README states in capitals that the library does not support Bluetooth 2 and Classic, with no plans to add it.
Those two exclusions define the use case. If you are building a tool that talks to a heart rate strap, a sensor, a beacon or a lock, this library is aimed straight at you. If you are building something that needs Classic Bluetooth, an audio profile, or a serial port over RFCOMM, you need a different dependency.
The origin story is worth a line because it explains the design. The library grew out of several earlier abandoned platform libraries, including rumble and blurmac, with the goal of building a fully cross-platform library, and the author has written a blog post about how it grew. The abandoned predecessors were usually platform-specific, and the reason they died is a good hint at the risk here: Bluetooth host stacks are large, slow-moving platform APIs, and keeping four of them working is a long-term commitment rather than a one-off task.
The crate is published on crates.io, documented on docs.rs, and the Cargo.toml version is 0.13.5 on the 2024 edition.
Four platform stacks behind one async API
The platform support is listed as Windows 10, macOS, Linux, iOS and Android, including Flutter, and the dependency list in Cargo.toml shows how each is reached.
On Linux the library talks to BlueZ over D-Bus, through the dbus and bluez-async crates. On Apple platforms it uses objc2 and objc2-foundation, which is the modern Objective-C binding layer over CoreBluetooth. On Windows the keyword list names uwp, pointing at the Windows Runtime APIs. On Android it uses jni, since the platform stack is Java and a native library has to cross the JNI boundary.
The shared layer above those is built on async-trait, tokio for the sync, runtime and time features, tokio-stream, futures, dashmap for concurrent maps, uuid for identifiers, bitflags, thiserror for error types, and log for diagnostics. The tokio comment in the manifest explains one dependency: the runtime feature is needed for block_on in the macOS internal thread.
Two optional feature flags are defined. The serde feature turns on Serialize and Deserialize for some of the common types in the api module, and the jni-host-tests feature pulls in the JNI invocation pieces and once_cell for running host tests on Android.
For a project that wants serialisable Bluetooth state, the feature declaration is short enough to quote:
[dependencies]
btleplug = { version = "0.12", features = ["serde"] }The version in that example is 0.12 while the crate itself is at 0.13.5, so the snippet is behind the current release, which is a small sign that the documentation examples are not always regenerated on every version bump.
The feature table is the honest part of the documentation
Most projects would write a sentence saying it works on Windows, macOS, Linux, iOS and Android. btleplug instead publishes a matrix, with X for completed and released, O for in development, and blank for not started.
The core GATT path is complete everywhere. Bring up adapter, discover devices, then services, characteristics, descriptors, name, manufacturer data and service data, GATT server connect and disconnect with their events, write to and read from a characteristic, subscribe and unsubscribe, the notification event, read and write descriptors, retrieve MTU, and read RSSI. Every one of those rows shows X in all four platform columns.
The blanks are where the platforms diverge, and they are the interesting part. Handle multiple adapters is complete only on Linux. Discover MAC address is complete on Windows, Linux and Android but blank on macOS and iOS. Retrieve local adapter address is complete on Windows and Linux only. Retrieve adapter state is complete on Windows, macOS and Linux but not Android. Retrieve connection parameters and request connection parameters are complete on Windows and Android only. Add peripheral by address exists only on Android.
That last row is worth reading as a design consequence rather than a gap. On Android the platform lets you ask for a specific peripheral by address, and on the other platforms it does not, so a cross-platform application that wants to reconnect to a known device has to rely on whatever identifier the platform does expose.
The practical advice is to treat the table as the API contract and not the prose. Anything marked X on one platform and blank on another will surface as a compile error or a runtime difference, and the table tells you which.
BlueZ scan filters return the union of every client's filters
This is the most important caveat in the project, and it is the kind of problem that would otherwise cost a day of debugging on Linux.
The ScanFilter you pass to start_scan() behaves differently on Linux than on other platforms. btleplug forwards service UUID filters to BlueZ, but BlueZ merges discovery filters across all D-Bus clients. The consequence, stated directly, is that your application receives advertisements matching the union of all applications' filters, not just your own. On a Linux desktop running other Bluetooth tools, that means you see advertisements you did not ask for.
There is a second problem on the same path. BlueZ's UUID filter can drop some advertisement types, such as service data, that it should not drop. So the filter is simultaneously too permissive in what it admits and lossy in what it keeps.
The guidance is to post-filter scan results yourself rather than relying solely on ScanFilter. In practice that means your Linux code path has to do the service UUID and service data matching that you assumed the stack would do, and it means the same application behaves differently on Linux and on Windows, macOS or Android, where the filter is honoured as written.
A second Linux caveat concerns the adapter address. The Central::adapter_address() API returns a local Bluetooth adapter address only where the platform exposes one, currently Linux and Windows. CoreBluetooth and ordinary Android applications return Ok(None) because their public APIs give opaque or privacy-restricted adapter identities. The documentation also makes the important distinction that this adapter address is not a discovered peripheral address, and that PeripheralId remains the portable identity for an adapter's peripherals.
macOS needs an Info.plist, Android needs API 24
The platform setup notes are where a build breaks, so it is worth reading them before writing any code.
On macOS Big Sur and later, Bluetooth access requires either packaging the binary into an application bundle with an Info.plist that includes NSBluetoothAlwaysUsageDescription, or, for a command line application such as the examples shipped with btleplug, enabling the Bluetooth permission for your terminal. The instructions for the second route are to go to System Preferences, then Security and Privacy, then Privacy, then Bluetooth, click the plus button and select Terminal, or iTerm, or whichever terminal you use. In other words, the examples need a manual permission grant before they will scan, and the failure without it is an empty result rather than an error message.
On Android the situation is different in kind. The project notes that btleplug for Android requires a somewhat complicated setup, because it needs a hybrid Rust and Java build. The Android library and its test application require Android API 24, Android 7.0, or newer, because the JNI support classes use Java functions that did not exist before that level. The workspace excludes tests/android/rust in Cargo.toml, which is how the separate Android build is kept out of the normal workspace build.
The macOS note is a five-minute fix. The Android one is a genuine integration project, and for a cross-platform Flutter or native mobile product it is the part of the effort that should be budgeted separately rather than discovered at the end.
Examples, tests, and how the project is laid out
The examples directory shows the shape of the API through four programs rather than a large sample application. There is discover_adapters_peripherals.rs, which enumerates both, and event_driven_discovery.rs, which shows the asynchronous pattern. There is lights.rs, which writes to a characteristic, and subscribe_notify_characteristic.rs, which demonstrates the subscription and notification path.
That selection matches the feature table. The rows that are complete on every platform are the ones with examples, so the sample code covers the part of the API that behaves identically everywhere, and the divergent rows are documented in prose instead.
The repository layout is a single crate with a workspace, and the Cargo.toml include list ships src, examples, README.md, CHANGELOG.md and LICENSE.md, which is a deliberate trimming that keeps the published package small. The rest of the top level is standard: an AGENTS.md for agent tooling, a CHANGELOG.md, a Cargo.lock, a rustfmt.toml, a scripts/ directory, a src/ directory, a test-peripheral/ directory and a tests/ directory. The test-peripheral/ directory is notable, since a host-side BLE library needs something to act as the device, and shipping one in-tree means the test suite does not depend on a physical peripheral.
The licence deserves a note because the metadata and the manifest disagree in a way that is easy to misread. The repository's licence field is listed as a custom licence that GitHub cannot classify, while the Cargo.toml declares license = MIT/Apache-2.0/BSD-3-Clause, and the file on disk is LICENSE.md. The Cargo.toml form is the triple-licence standard used by Rust projects, and reading LICENSE.md is the way to confirm what each licence requires, in particular for the notice obligations in the BSD variant.
Editorial conclusion
btleplug is the right choice for a Rust project that needs Bluetooth LE on more than one operating system and does not want to write four backends, because the platform caveat documentation is candid enough to plan around, particularly the BlueZ filter behaviour; it is the wrong choice if you need Classic Bluetooth, if you need peripheral mode, or if your target is a WebAssembly browser build, where the support is still a pull request awaiting review, so before you commit you should read the platform feature table rather than assuming the four platforms behave alike, because MAC address discovery, multiple adapter handling and adapter state are marked complete only on some of them.
Frequently asked questions
Does btleplug support Classic Bluetooth?
No. The project states that the library does not support Bluetooth 2 or Classic and that there are no plans to add it. It covers Bluetooth LE only, in host and central mode.
Which platforms does btleplug support?
Windows 10, macOS 10.15 and later, Linux, iOS and Android, including Flutter. Device enumeration, service and characteristic discovery, GATT connections, reads, writes and notifications are listed as complete and released on all of them.
Why does my Linux scan return devices I did not filter for?
Because BlueZ merges discovery filters across all D-Bus clients, so your application receives advertisements matching the union of all applications' filters. The project advises post-filtering scan results yourself on Linux rather than relying solely on ScanFilter, and notes that BlueZ's UUID filter can also drop advertisement types such as service data.
Why does my macOS program find no Bluetooth devices?
On macOS Big Sur and later, Bluetooth use requires either an application bundle with an Info.plist containing NSBluetoothAlwaysUsageDescription, or a manual grant of Bluetooth permission for your terminal under System Preferences, Security and Privacy, then Privacy, then Bluetooth.
Can I use btleplug in peripheral mode or in a web browser?
Peripheral mode is out of scope, since the library is host and central only and the project points to bluster and ble-peripheral-rust for that. For WebAssembly and WebBluetooth, support is described as possible with a pull request in review, tracked under issue 13, with the request to hold off on filing more issues until the base implementation lands.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/deviceplug-btleplug)Was this useful?