# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). [1.1.1] - 2026-XX-XX [#111---2026-xx-xx] **Notes** * **Added** * (Dongl) Added pairing and bonded-peripheral management with typed passkey callbacks. * (Dongl) Added service data, descriptor I/O, and negotiated MTU reporting. * (Linux) Added Dongl backend support. * (SimpleBluez) Added device connection-state change callbacks. * (Linux) Added local peripheral-mode support. * (MacOS) Added local peripheral-mode support. * (Windows) Added local peripheral-mode support. * (Android) Added local peripheral-mode support. * (SimpleDroidBLE) Added Kotlin local peripheral, service, characteristic, advertising, permission, and event APIs. * (SimpleBLE) Added initial local peripheral-mode API scaffolding. **Changed** * **API CHANGE**: (SimpleBLE) Replaced `Local::Advertisement` with additive advertised-service APIs on local peripherals. * **API CHANGE**: (SimpleDroidBLE) Replaced `LocalAdvertisement` with additive advertised-service APIs on local peripherals. * (Dongl) Consolidated UUID conversion and routed diagnostics through SimpleBLE logging. **Fixed** * (Dongl) Fixed scan callbacks, advertising-data merging, and disconnect notifications. * (SimpleJavaBLE) Fixed JPMS compatibility with a stable automatic module name. **Removed** * [1.1.0] - 2026-08-09 [#110---2026-08-09] **Notes** * The deprecated legacy BlueZ backend has been removed. Linux now exclusively uses SimpleBluez; applications should remove any C++ or C backend-selection configuration. **Added** * (Android) Added support for scanning extended advertisements. * (MacOS/iOS) Added advanced retrieval of cached CoreBluetooth peripherals by UUID. * (Python) Completed configuration bindings and exposed backend enumeration and advanced cached CoreBluetooth peripheral retrieval. * (SimpleDroidBLE) Added permission helpers, paired peripherals, write operations, and descriptor access. * (SimpleDroidBLE) Added the SimpleBLE Explorer example and a PLAIN test build for simulation. * (SimpleDroidBLE) Published SimpleDroidBLE and SimpleDroidBridge on Maven Central with native libraries for all four supported Android ABIs. * (SimpleJavaBLE) Published SimpleJavaBLE on Maven Central with bundled native libraries for Windows, macOS, and Linux. * (Windows) Added support for scanning extended advertisements. **Changed** * (Python) Delegated runtime log filtering to the standard `logging` module without exposing native logging controls. * (SimpleJNI) Shared Java collection wrappers across JNI bindings. * (SimpleDroidBLE) Replaced manual JNI plumbing with SimpleJNI. * **API CHANGE**: (SimpleDroidBLE) Strongly typed Bluetooth addresses and discovered GATT UUIDs. * **API CHANGE**: (SimpleDroidBLE) Exposed callbacks as read-only flows and notification streams as cancellation-aware `Flow`s. * **API CHANGE**: (SimpleDroidBLE) Made blocking GATT operations main-safe suspending calls. * (Windows) Ignored synthetic WinRT out-of-range scan events. **Fixed** * (Android) Fixed oversized write commands being silently truncated. * (MacOS) Fixed peripheral identifiers to prefer advertised local names over cached GAP names. * (Python) Corrected async callback type hints and modernized annotations. **(Thanks, josmithua!)** * (Python) Released the GIL while disconnecting so concurrent blocking disconnects can overlap. * (SimpleBluez) Fixed discovery filters dropping non-string values. * (SimpleBluez) Fixed UUID lookups returning invalid service and characteristic proxies. * (SimpleDroidBLE) Fixed coroutine dependency exposure and silent notification buffer overflows. * (SimpleDroidBLE) Fixed JNI error propagation, byte-array conversion, and callback cleanup. * (Linux/Windows) Fixed notification callbacks remaining retained after unsubscribe. * (Windows) Fixed restricted GATT services preventing discovery of subsequent services. * (SimpleJavaBLE) Fixed notification callbacks being garbage-collected while subscriptions are active. * (SimpleJavaBLE) Fixed JVM crashes while translating Java callback exceptions. **Removed** * **API CHANGE**: (Linux/SimpleCBLE) Removed the legacy BlueZ backend and the C++ and C configuration APIs that selected it. [1.0.0] - 2026-07-15 [#100---2026-07-15] **Added** * (SimpleAIBLE) Added a `simpleaible` command with `install` and `uninstall` subcommands for the agent skill. * (SimpleBluez) Added support for setting LE advertisement secondary channels, including `2M`, and querying supported secondary channels. * (SimpleBluez) Added fd-backed `AcquireNotify` callbacks for local GATT characteristics. * (SimpleBluez) Added runtime control for the optional `NotifyAcquired` property on local GATT characteristics. * (SimpleBluez) Added callback registration for adapter `Powered` property changes. * (SimpleCBLE) Added C API accessors for SimpleBLE configuration options. * (SimpleDBus) Added a Unix socket RAII primitive for D-Bus APIs that exchange file descriptors. * (SimpleDBus) Added support for invalidating optional exported properties. * (SimpleDBus) Formalized `org.freedesktop.DBus.Introspectable` as a dedicated interface for exported proxy objects. * (Linux) Added adapter power callbacks for BlueZ `Powered` changes. * (MacOS) Added adapter power callbacks for CoreBluetooth state changes. * (Python) Forwarded SimpleBLE logs to Python's `logging` module under the `simplepyble` logger. * (Windows) Added support for `Peripheral::is_paired`. **Changed** * (SimpleAIBLE) Bundled the agent skill into the Python package. * **API CHANGE**: (SimpleBluez) Updated `ReadValue` and `WriteValue` callbacks to expose BlueZ server-side options. * (SimpleBluez) Preloaded the D-Bus `Properties` interface on Device proxies to support refreshed BlueZ device properties. * (Linux) Switched the default BlueZ implementation from legacy to SimpleBluez. * (Linux) Start the BlueZ dispatch loop before registering the pairing agent. * (Windows) Build the SimpleBLE target as C++20 when using MSVC, and require Windows SDK 10.0.22621.0 or newer for compatible C++/WinRT coroutine headers. * (Windows) Deprecated `SimpleBLE::Config::WinRT::experimental_use_own_mta_apartment`; SimpleBLE now uses its own WinRT MTA apartment by default and the flag remains only for compatibility. **Fixed** * (Android) Fixed GATT read, write, service discovery and MTU-change callbacks to honor the reported GATT status instead of always reporting success. * (Android) Fixed stale service discovery state not being reset on disconnect, which broke reconnection. * (Linux) Fixed connection state reporting to wait for locally-dispatched service resolution. * (Linux) Fixed connection timeouts when services were already locally resolved. * (Linux) Fixed `Adapter::power_on` and `Adapter::power_off`. * (Linux) Fixed BlueZ backend selection to avoid constructing both SimpleBluez and legacy implementations. * (Linux) Fixed concurrent access to scan result maps. * (Linux) Fixed notification cleanup after abrupt BlueZ peripheral disconnects. * (Linux) Fixed adapter and peripheral callbacks being cleared when `Adapter::get_adapters` or `Adapter::get_paired_peripherals` is called again. * (Linux) Fixed `Peripheral::services()` to include advertised service data payloads. * (Linux) Fixed random address types being reported as unspecified. * (MacOS) Fixed characteristic and descriptor operations with standard UUIDs. * (MacOS) Fixed notification callbacks to snapshot payload data before invoking the user callback. * (MacOS) Fixed concurrent access to peripheral advertising data while scan updates are being processed. * (MacOS) Fixed concurrent access to adapter peripheral maps while scan and connection events are processed. * (MacOS) Fixed out-of-bounds read when parsing advertised manufacturer data shorter than 2 bytes. * (MacOS) Fixed advertised service UUIDs overwriting service data payloads from the same advertisement. * (MacOS) Fixed crash when reading descriptors whose values CoreBluetooth returns as numbers or strings, such as the CCCD or User Description. * (SimpleBLE) Cleaned up `kvn::bytearray` slicing to avoid an unnecessary temporary allocation and resolve a Coverity iterator-container warning. * (SimpleBLE) Fixed installed CMake package configs to include version files and separate target exports. * (SimpleCBLE) Fixed allocation-failure handling in C string getters for adapter and peripheral identifiers and addresses. * (SimpleBluez) Fixed custom peripheral D-Bus objects to use the connection's unique bus name instead of the unowned `org.simplebluez` name. * (SimpleBluez) Fixed device update notifications to avoid retaining discovered devices. * (SimpleBluez) Fixed concurrent access to adapter children while listing paired or bonded devices. * (SimpleDBus) Fixed local `ObjectManager::GetManagedObjects()` calls to return the local object tree directly instead of blocking on a D-Bus self-call. * (SimpleDBus) Fixed descendant proxies staying valid after an ancestor D-Bus object path is removed. * (SimpleDBus) Fixed retained proxies remaining invalid after their D-Bus object path is removed and later added again. * (SimpleDBus) Fixed exported proxies to return a D-Bus `UnknownInterface` error for method calls targeting unsupported interfaces. * (SimpleDBus) Fixed a stack overwrite when extracting booleans from D-Bus messages. * (SimpleDBus) Fixed `Holder::create` silently returning an empty holder when the given value was already a `Holder`. * (SimpleJavaBLE) Fixed Windows JVM crashes when loading the native bindings with JDKs that bundle older MSVC runtime DLLs. * (SimpleJavaBLE) Fixed duplicate Java peripheral wrappers replacing connect and disconnect callbacks for the same native peripheral. * (Windows) Fixed `Adapter::get_paired_peripherals` and `Adapter::get_connected_peripherals` to avoid materializing invalid WinRT Bluetooth LE device IDs during enumeration. * (Windows) Fixed `Peripheral::set_callback_on_disconnected` so user-initiated disconnects trigger the callback once without relying solely on WinRT's connection-status event. * (Windows) Fixed `Peripheral::connect` to fail cleanly when Bluetooth is disabled after scanning but before connecting. * (Windows) Fixed notification callbacks to safely log user callback exceptions instead of letting them escape the WinRT event handler. **Removed** * (Python) Removed support for Python 3.8 and 3.9. SimplePyBLE and SimpleAIBLE now require Python 3.10 or newer, and CI no longer builds Python 3.8/3.9 wheels. [0.14.0] - 2026-05-02 [#0140---2026-05-02] **Notes** This release introduces the concept of `Backend` to the SimpleBLE API. Backends represent the underlying BLE implementation, such as BlueZ on Linux, CoreBluetooth on macOS, and WinRT on Windows. These entities will become more relevant in future releases as we move towards more elaborate workstreams, the first of which is the introduction of the Dongl backend. The SimpleBLE Dongl is a suite of hardware and firmware products that bypass the host's BLE stack entirely, providing an extremely consistent BLE experience across all platforms. Visit the [SimpleBLE Dongl](https://simpleble.org/dongl) website to learn more. **Added** * (SimpleJavaBLE) Added `CompletableFuture` wrappers for blocking operations to allow clean asynchronous programming without blocking threads. * (SimpleBLE) Elevated `Backend` class to the public API, enabling programmatical control of all available backends. * (SimpleBLE) Introduced the Dongl backend for custom SimpleBLE hardware and firmware products. **Changed** * (SimpleJavaBLE) JNI API calls are now safely wrapped to catch C++ exceptions and bubble them up as Java `RuntimeException` instead of crashing the JVM. * (SimpleJavaBLE) Updated `EventListener` interfaces in `Adapter` and `Peripheral` to provide default empty implementations. * (Windows) Added configurable workaround for 3-second delay in disconnecting from a peripheral. **(Thanks, josmithua!)** * (Android) Updated NDK version to r29. **Fixed** * (SimpleRsBLE) Fixed linker errors on some 64-bit Linux systems. **(Thanks, davidedellagiustina!)** * (SimpleDBus) Fixed `bad_any_cast` exception in dictionaries with `ObjectPath` or `Signature` keys. **(Thanks, AndreiSvatko!)** * (SimpleDBus) Fixed D-Bus parameter errors and strict validation rejections on BlueZ 5.83+ by properly skipping uninitialized optional properties. **(Thanks, piranna!)** * (SimpleJavaBLE) Fixed missing macOS x64 `.dylib` artifact in the release package due to an omitted CI architecture target. * (SimpleJavaBLE) Fixed JNI symbol resolution errors (`UnsatisfiedLinkError`) on `Adapter.isBluetoothEnabled()` and `isPaired` flags. * (SimpleJavaBLE) Fixed `jvm.dll` initialization conflicts on Windows that prevented the native bindings from loading smoothly. * (SimpleCBLE) Fixed remotely triggerable stack buffer overflows when handling manufacturer data and advertised services (EVE-2026-002, EVE-2026-003). **(Thanks, Mr-IoT!)** * (SimpleBLE) Fixed stack buffer overflow in Dongl write function. (EVE-2026-001). **(Thanks, Mr-IoT!)** * (SimpleBLE) Added missing export headers to `Config.h`. **Special Acknowledgements** We'd like to thank [Mr-IoT](https://github.com/V33RU) for reporting and providing patches for multiple security vulnerabilities (EVE-2026-001, EVE-2026-002, EVE-2026-003). [0.12.1] - 2026-02-12 [#0121---2026-02-12] **Added** * (SimpleAIBLE) Added AI capabilities to SimpleBLE with an MCP and an HTTP server. * (SimplePyBLE) Added `simplepyble.aio` module, providing an `asyncio`-compatible API with automatic callback cleanup and context manager support. * (SimplePyBLE) Exposed `Adapter.set_callback_on_power_on`, `Adapter.set_callback_on_power_off`, and `Adapter.get_connected_peripherals` in Python bindings. * (SimplePyBLE) Added `initialized()` method to `Service`, `Characteristic`, and `Descriptor` Python bindings. **Changed** * (SimpleCBLE) Modernized the library to match the C++ API, renamed internal files to `simplecble`, and migrated to standard classes with internal exception handling, including a fix for `Peripheral::underlying()`. * (SimpleRsBLE) Modernized the library to match the C++ API. * (SimpleRsBLE) Updated Minimum Supported Rust Version (MSRV) to 1.82.0 to resolve compatibility warnings with recent Rust compilers. **Fixed** * (SimpleBLE) Implemented `Adapter::get_connected_peripherals` in the C++ base frontend. * (SimpleBLE) Improved `OperationNotSupported` exceptions to include the requested operation and characteristic UUID. * (Linux) Added explicit capability checks for read/write/notify operations before invoking BlueZ operations. * (Linux) Fixed `_cleanup_characteristics()` to handle exceptions per characteristic callback cleanup, preventing incomplete cleanup. * (Windows) Fixed scan stop handling so `scan_is_active_` is updated correctly when WinRT reports scanning has stopped. **Removed** * (SimpleBLE) Removed the `simpleble-c` target and its associated source and header files from the `simpleble` package. Users should migrate to the `simplecble` library, which provides a compatible C interface. [0.11.0] - 2026-01-31 [#0110---2026-01-31] **Added** * (SimpleDBus) Connection has a new `send_with_reply` method that doesn't block the underlying DBus event loop. * (SimpleBluez) Added support for advertising as a peripheral. * (SimpleBluez) Added support for exposing services and characteristics as a peripheral. * (Linux) Added support for powering on and off the adapter. * (Linux) Added configuration option to select between the system and the user DBus bus. * (Python) Added type annotations. * (SimplePyBLE) Added a REST API server. **Changed** * (SimpleDBus) Interfaces now host their own property objects. * (SimpleDBus) Property change callbacks are now handled by the individual property objects. * (SimpleBluez) Moved some files around, which might change the include paths for some users. * **API CHANGE**: (SimpleBluez) API for accessing internal BlueZ objects has been made more flexible. * (SimpleBluez) Migrated all internal classes to use `send_with_reply`. * (SimpleDBus) `Holder` class now has templated methods for creating and getting values. **Fixed** * (SimpleDBus) The Holder class now has appropriate getters, setters and defaults for object paths and signatures. * (SimpleDBus) Fixed several issues around handling of Signature and ObjectPath data types. **Removed** * (SimpleDBus) Removed individual create/get functions in favor of templated versions. * **API CHANGE**: (SimpleBluez) Deprecated the use of the `SIMPLEBLUEZ_USE_SESSION_DBUS` environment variable in favor of a configuration option. [0.10.4] - 2025-12-29 [#0104---2025-12-29] **Added** * Introduced scaffolding for advanced low-level features. * Configuration option to select which BlueZ backend to use. * (Android) Added support to set and retrieve the JavaVM pointer. * (Linux) Added frozen BlueZ backend in preparation for upcoming changes. * (SimpleDBus) Added dedicated Properties interface. * (Dongl) Added scaffolding for Dongl backend. **Changed** * (MacOS) Use a single Adapter object across all users of the CoreBluetooth backend. * (Android) Use a single Adapter object across all users of the Android backend. * (SimpleDBus) Messages are now directly forwarded to the appropriate proxy object, no more chaining required. * (SimpleDBus) Require Proxy factory method to handle proxy creation and registration. * (SimpleDBus) Interface objects now store a weak reference to their proxy. **Fixed** * Improper handling of configuration settings when consuming SimpleBLE as a shared library. * (MacOS) Freeze when attempting a double disconnection. * (Android) Solved "local reference table overflow" error. *(Thanks Nicole S.!)* * (Android) Fixed unexpected initialization of SimpleJNI. * (SimpleDBus) Fixed race condition when handling property updates of DBus objects. * (Linux) Fixed potential race condition when handling disconnection events. * (Python) Fixed missing files in source distribution. **Removed** * (MacOS) Removed support for MacOS Catalina, Big Sur and Monterey. [0.10.3] - 2025-06-24 [#0103---2025-06-24] **Changed** * (SimpleDBus) Interface creation is now done via a registry. **Fixed** * (Python) Fixed GIL issues introduced in v0.10.2. [0.10.2] - 2025-06-20 [#0102---2025-06-20] **Changed** * (Windows) Experimental option to skip reinitializing the WinRT apartment on the main thread. Default is now to not reinitialize. **Fixed** * (Python) Fixed several deadlocks in the Python bindings related to new WinRT threading model. [0.10.1] - 2025-05-30 [#0101---2025-05-30] **Notes** * The Rust bindings have undergone a major rewrite of their API, towards a more idiomatic Rust style and stream-based API. * iOS and Android do not support powering on and off the adapter. Calling these methods will not have any effect on the adapter. * Linux does have support for powering on and off the adapter, but further architecture changes are needed to properly expose this. * Callbacks for power on and off events are currently only supported on Windows. * Retrieving connected peripherals is currently only supported on Windows. More backends coming soon. **Added** * Functions for powering adapters on, off and querying their power state. * Callbacks to monitor adapter power on and off events. * (Windows) Added support for powering adapters on and off. * (Windows) Added support for retrieving paired peripherals. * (Windows) Added support for retrieving connected peripherals. * (Python) Exposed the `Adapter::power_on()`, `Adapter::power_off()` and `Adapter::is_powered()` methods. * (Android) Calls to Java methods are now checked for exceptions. * (Java) Calls to Java methods are now checked for exceptions. * (Android) Added support for requesting a specific connection priority via configuration. *(Thanks Nicole S.!)* * (MacOS) Added support for powering adapters on and off. * (Linux) Added configurable connection and disconnection timeouts. *(Thanks Kober Engineering!)* **Changed** * (Windows) Calls to the WinRT backend are now executed in a separate MTA apartment by default. * (Android) Migrated to using the `simplejni` library for JNI bindings. * **API CHANGE**: (Rust) Migrated to streams for scan and connection events. * **API CHANGE**: (Rust) Migrated to streams for peripheral notifications and indications. **Fixed** * (Android) Fixed a bug where the GATT object would not be closed if a connection was lost. *(Thanks Nicole S.!)* * (Rust) Fixed a race condition in the Rust bindings that would cause a crash if the adapter was deleted while a callback was in progress. * Added missing operating system definitions for utils. * (Linux) Use steady\_clock instead of system\_clock for timeout calculations. *(Thanks Kober Engineering!)* **Removed** * Source code of the `simpleble-bridge` project in favor of `simpledroidbridge`. [0.9.1] - 2025-04-24 [#091---2025-04-24] **Important:** * In the near future we will deprecate the `simpleble-c` target in favor of `simplecble`, which will be a drop-in replacement for the existing C bindings. * The `simpleble-bridge` project has been renamed to `simpledroidbridge` and can be found in the root directory of the repository. **Added** * (Android) Implemented the following API functions: * `Adapter::scan_get_results()` * `Adapter::get_paired_peripherals()` * `Peripheral::rssi()` * `Peripheral::tx_power()` * `Peripheral::is_connectable()` * `Peripheral::is_paired()` * `Peripheral::manufacturer_data()` * `Peripheral::advertised_services()` * (Java) Early preview of Java bindings. * Configuration class to control the behavior of SimpleBLE internals as well as experimental features. * SimpleCBLE: Moved SimpleBLE C bindings into a separate library. **Changed** * `Adapter::identifier()` method is non-const, as underlying const conditions can't be guaranteed. * (Android) Callback functions are not handled on a separate, dedicated thread. * (Windows) **(Experimental)** Calls to the WinRT backend can now be executed in a separate MTA apartment via feature flag. * (Android) The `simpleble-bridge` project has been renamed to `simpledroidbridge`. * Upgraded `fmt` dependency to version 11.1.4 and vendorized into the repository. **Fixed** * (Android) Some potential race conditions in the Android backend. * (Android) Fixed handling of null objects. * (Android) `Peripheral::address_type()` and `Peripheral::unpair()` had to be removed due to API level limitations. * (Android) Potential duplicate callback invocations on builds with newer Android API levels. [0.9.0] - 2025-01-20 [#090---2025-01-20] **Important: License has changed, please review the new license terms.** **Changed** * Removed unnecessary log print in MacOS backend. *(Thanks will-tm!)* * Remove builders in favor of templated approach. *(Thanks jcarrano!)* * Refactor code to use abstract classes and PIMPL idiom. *(Thanks jcarrano!)* [0.8.1] - 2024-11-05 [#081---2024-11-05] **Added** * (Android) Alpha preview of Android support. * (SimpleDBus) Added templated version of creation and getter functions for Holder class. *(Thanks lorsi96!)* **Changed** * Implemented standalone ByteArray class derived from `kvn::bytearray`. *(Thanks tlifschitz!)* * **API CHANGE**: Notify and Indicate callback in C bindings now receive the peripheral handle as the first argument. **Fixed** * (SimpleBluez) Fixed improper handling of non `org.Bluez.Service1` objects within a `org.bluez.Device1` object. *(Thanks Kober Engineering!)* * (MacOS) Fixed incorrect storage and retrieval with standard Bluetooth UUIDs inside the peripheral class. *(Thanks TellowKrinkle!)* * (Python) Fixed incorrect handling of the GIL in certain functions. *(Thanks nomenquis and Medra AI!)* [0.7.X] [#07x] This entire series is dedicated to reviewing and updating the license terms of the project. [0.7.0] - 2024-02-15 [#070---2024-02-15] **Added** * Function to query the version of SimpleBLE at runtime. * (Python) Missing API from SimpleBLE::Characteristic. **Changed** * (MacOS) Main adapter address is now hardcoded to allow caching based on adapter address. *(Thanks BlissChapman!)* * (Python) Release GIL when calling `Peripheral.write_request` and `Peripheral.write_command`. * (MacOS) Rewrote the entire backend. * (MacOS) OperationFailed exception now contains the error message provided by the OS. **Fixed** * (MacOS) Remove unnecessary timeout during service discovery. *(Thanks BlissChapman!)* * (MacOS) Return correct list of devices when scanning. *(Thanks roozmahdavian!)* * (MacOS) Remove unnecessary timeout during characteristic notification. *(Thanks BlissChapman!)* * (MacOS) Remove unnecessary timeout during operations on characteristics. * (Windows) Failed connection attempt would not trigger an exception. *(Thanks eriklins!)* * (Linux) Use correct UUIDs for advertized services. *(Thanks Symbitic!)* [0.6.1] - 2023-03-14 [#061---2023-03-14] **Added** * (Python) Generate source distribution packages. * (SimpleDBus) Proxy objects keep track of their existence on the DBus object tree. **Changed** * Bluetooth enabled check was moved into the frontend modules. *(Thanks felixdollack!)* * (Windows) Use the standard C++ exception handling model. *(Thanks TheFrankyJoe!)* **Fixed** * CI artifacts for non-standard architectures are now properly built. * (SimpleBluez) Fixed incorrect handling of invalidated children objects. [0.6.0] - 2023-02-23 [#060---2023-02-23] **Added** * Option to build SimpleBLE plain-flavored (without any BLE code) for testing and debugging purposes. * Support for advertized services. * Support for GATT Characteristic properties. * Retrieve the MTU value of an established connection. *(Thanks Marco Cruz!)* * Peripheral addresses can now be queried for their type. *(Thanks camm73!)* * Tx Power is decoded from advertising data if available. *(Thanks camm73!)* * Logger now provides default functions to log to a file or to stdout. * Support for exposing advertized service data. *(Thanks Symbitic!)* * (Rust) Preliminary implementation of Rust bindings. * (Windows) Logging of WinRT initialization behavior. * (SimpleBluez) Support for GATT characteristic flags. * (SimpleBluez) Support for GATT characteristic MTUs. *(Thanks Marco Cruz!)* * (SimpleBluez) Support for advertized services. * (SimpleBluez) Mechanism to select the default DBus bus type during compilation-time. *(Thanks MrMinos!)* **Changed** * Debug, MinSizeRel and RelWithDebInfo targets now contain their appropriate suffix. *(Thanks kutij!)* * **API CHANGE**: Log level convention changed from uppercase to capitalizing the first letter. * Updated `libfmt` dependency to version 9.1.0. * Unused `libfmt` targets removed from the build process. * (MacOS) More explicit exception messages. * (MacOS) 16-bit UUIDs are now presented in their 128-bit form. * (MacOS) Adapter address now swapped for a random UUID. *(Thanks nothingisdead!)* * (Windows) Reinitialize the WinRT backend if a single-threaded apartment is detected. *(Thanks jferdelyi!)* * (Windows) Callbacks for indications and notifications are now swapped if one already exists. **Fixed** * Incorrect handling of services and characteristics in the Python examples. *(Thanks Carl-CWX!)* * Minor potential race condition in the safe callback. * Compilation-time log levels were not being set correctly. *(Thanks chen3496!)* * Missing function definition in C-bindings. *(Thanks eriklins!)* * (Linux) Peripheral would still issue callbacks after deletion. * (MacOS) Increased priority of the dispatch queue to prevent jitter in the incoming data. * (MacOS) Incorrect listing of advertized services. *(Thanks eriklins & Symbitic!)* * (Windows) Missing peripheral identifier data. *(Thanks eriklins!)* * (Windows) Multiple initializations of the WinRT backend. * (Windows) Incorrect initialization of the WinRT backend. *(Thanks ChatGPT & Andrey1994!)* * (Windows) Scan callbacks would continue after scan stopped. * (Windows) Disconnecting would prevent the user from connecting again. *(Thanks klaff, felixdollack & lairdrt!)* * (Windows) Uncleared callbacks when unsubscribe is called. * (Windows) Incorrect handling of non-english locale by MSVC. *(Thanks felixdollack!)* * (Windows) Disconnection callback would not be triggered on a manual disconnect. *(Thanks crashtua!)* * (Python) Type returned by `simplepyble.get_operating_system()` was not defined. * (SimpleBluez) Incorrect attempt to operate on an uninitialized DBus connection. *(Thanks jacobbreen25!)* [0.5.0] - 2022-09-25 [#050---2022-09-25] **Important:** * From this version onwards, the CMake target that should be consumed by downstream projects is `simpleble::simpleble`. * This version includes a breaking API change in the enumeration of services and characteristics. * This version has brought in the files from SimpleBluez and SimpleDBus into the repository as subpackages. **Added** * Multiple connection example. * Installation interface. * Logger level and callback can now be queried. * Characteristics can now list their descriptors. *(Thanks Symbitic!)* * Peripherals can now read and write characteristic descriptors. *(Thanks Symbitic!)* * Adapter object can now be queried to see if Bluetooth is enabled. * (Windows) WinRT exception handling. * (Windows) Accessor function to underlying OS objects of `Adapter` and `Peripheral`. * (MacOS) Failures will now throw corresponding exception. * (SimpleBluez) Support for characteristic descriptors. *(Thanks Symbitic!)* * (SimpleBluez) Full support for all discovery filters. *(Thanks Symbitic!)* **Changed** * Clearer layout of examples. *(Thanks Yohannfra!)* * `AdapterSafe` and `PeripheralSafe` will now catch all exceptions. * Selection of build type is now based on the `BUILD_SHARED_LIBS` setting. * Consumable CMake target is now `simpleble::simpleble`. * **API CHANGE**: `BluetoothService` class was replaced by the `Service` class. * Updated CMake minimum version to 3.21 * Symbols are now hidden by default and use proper export mechanics. * Logger will print to std::out by default. * (MacOS) Stop throwing exceptions if Bluetooth not enabled. Print warning and no-op instead. * (Linux) Default scanning behavior switched to all devices. **Fixed** * Made user callback invocations exception-safe. * Attempting to scan while connected will erase references to all existing peripherals. * CMake target `simpleble::simpleble` was removed in favour of `BUILD_SHARED_LIBS`. * CMake target `simpleble::simpleble-c` was removed in favour of `BUILD_SHARED_LIBS`. * Using the correct CMake functionality to export headers for all targets. * Corrected maximum length of manufacturer data on the C-api to 27 bytes. *(Thanks DrSegatron!)* * (Windows) Peripheral reads are now uncached. *(Thanks piotromt!)* * (Linux) Failure to set agent would trigger a crash. * (Linux) Spurious disconnection events during connection retries have been fully removed. * (Linux) Exceptions thrown during the deletion phase of a peripheral would not be captured. * (Linux) Characteristic cleanup function has been made exception-safe. * (SimpleBluez) Accessing the `Paired` property of `Device1` would only use the cached value. [0.4.0] - 2022-06-12 [#040---2022-06-12] **Added** * Expose RSSI as a property of `Peripheral`. * Utils function to identify the current platform. * (Linux) `Peripheral::is_paired` method to check if a peripheral is paired. * (Linux) `Adapter::get_paired_peripherals` method to list all paired peripherals. * Function to validate whether an `Adapter` or `Peripheral` object is initialized. * Logging hooks to capture logs from SimpleBLE and internal components. * Accessor function to underlying OS objects of `Adapter` and `Peripheral`. * (Python) Python's Global Interpreter Lock (GIL) will be released during `Peripheral.connect()`. * (Python) Keep-alive policies for function objects passed into SimplePyBLE. **Changed** * Updated Linux implementation to use SimpleBluez v0.5.0. * Added support for Windows SDK 10.0.22000.0 * Updated `libfmt` to version 8.1.1. * Cleaned up dependency management for `libfmt` and SimpleBluez. * `Adapter::get_paired_peripherals` will return an empty list on Windows and MacOS. * (Linux) **(Experimental)** Exceptions thrown inside the Bluez async thread are now caught to prevent lockups. * `NotConnected` exception will be thrown instead of `OperationFailed` when peripheral not connected. **Fixed** * (MacOS) Known peripherals would not get cleared at the beginning of a scanning session. * (Windows) Known peripherals would not get cleared at the beginning of a scanning session. * Calling functions of uninitialized objects will now throw an exception instead of crashing. * (MacOS) Thread synchronization issues would cause certain peripheral actions to report failure. * (Windows) Behavior of `write_request` and `write_command` was flipped. * (MacOS) Behavior of `write_request` and `write_command` was flipped. * (Linux) `on_connected` callback was not being called. * (Linux) Spurious disconnection events during connection retries have been removed. * (Linux) Existing characteristic callbacks were not being cleared on disconnection. * (Linux) Characteristics are unsubscribed on disconnection. * (Linux) Missing agent registration that would prevent pairing from working. [0.3.0] - 2022-04-03 [#030---2022-04-03] **Added** * Pairing functionality has been validated on all supported operating systems. In the case of Windows and MacOS, the user will be required to interact with an operating system popup to pair the device, while on Linux all pairing requests will automatically be accepted, with passcodes `abc123` or `123456`. * Unpair command has been added, although the only working implementation will be the Linux one. Both Windows and MacOS require the user to manually unpair a device from the corresponding OS settings page. **Changed** * Updated Linux implementation to use SimpleBluez v0.3.1. * Migrated to using safe callbacks from external vendor (kvn::safe\_callback). [0.2.0] - 2022-02-13 [#020---2022-02-13] **Added** * (Linux) Support for emulated battery service. *(Thanks ptenbrock!)* **Fixed** * (Windows) Proper cleanup of callbacks during destruction. * (Windows) Async timeout reduced to 10 seconds. * (Linux) Returned characteristic value would be empty or outdated. *(Thanks ptenbrock!)* * (MacOS) Fixed a bunch of memory leaks and enabled automatic reference counting. * (MacOS) Fixed race condition. * (Python) `write_request` and `write_command` functions would accept strings instead of bytes as payloads. *(Thanks kaedenbrinkman!)* **Changed** * Updated Linux implementation to use SimpleBluez v0.2.1. [0.1.0] - 2021-12-28 [#010---2021-12-28] **Changed** * Referenced specific version of SimpleBluez to avoid breaking changes as those libraries evolve. * (Linux) When `scan_stop` is called, it is now guaranteed that no more scan results will be received. * Updated Linux implementation to use SimpleBluez v0.1.1. **Fixed** * (Linux) Scan will never stop sleeping. [0.0.2] - 2021-10-09 [#002---2021-10-09] **Added** * Safe implementation of `Adapter` and `Peripheral` classes. * CppCheck and ClangFormat CI checks. *(Thanks Andrey1994!)* * C-style API with examples. * Access to manufacturer data in the `Peripheral` class, for Windows and MacOS. **Fixed** * Compilation errors that came up during development. *(Thanks fidoriel!)* * WinRT buffer allocation would fail. *(Thanks PatrykSajdok!)* * `Adapter` would fail to stop scanning. *(Thanks PatrykSajdok!)* * Switched WinRT initialization to single-threaded. **Changed** * SimpleBluez dependency migrated to OpenBluetoothToolbox. [0.0.1] - 2021-09-06 [#001---2021-09-06] **Added** * Initial definition of the full API. * Usage examples of the library. # Extras Building documentation [#building-documentation] The documentation for SimpleBLE is built using [Fumadocs](https://fumadocs.vercel.app/), a Next.js-based documentation framework. Prerequisites [#prerequisites] * Node.js 20.9+ or Bun * npm, pnpm, or bun package manager Installation [#installation] First, install the dependencies: ```bash cd /docs npm install # or bun install ``` Development [#development] To run the documentation site in development mode with hot reload: ```bash npm run dev # or bun dev ``` The documentation will be available at `http://localhost:3000`. Building for Production [#building-for-production] To build the documentation for production: ```bash npm run build # or bun run build ``` To preview the production build locally: ```bash npm run start # or bun start ``` Content Structure [#content-structure] * **Content files**: Located in `docs/content/docs/` * **Components**: Located in `docs/src/components/` * **API documentation**: Generated using custom React components in `docs/src/components/api/` Documentation is written in MDX (Markdown with JSX), allowing you to use React components within your documentation. Release checklist [#release-checklist] Before releasing a new version of the project, perform the following steps in order. Prepare the release [#prepare-the-release] 1. Ensure content parity between all readmes and the documentation. * `README.rst` * `LICENSE.md` * `simpleaible/README.rst` * `simplepyble/README.rst` * `simplersble/README.md` 2. Update and review the release version in the following files: * `VERSION` * `Cargo.toml` (remove the `-dev1` suffix) * `Cargo.lock` * `docs/content/docs/changelog.mdx` (add the actual release date) * `simpleaible/pyproject.toml` (remove the `-dev1` suffix) 3. Run the Hardware In The Loop tests listed below. 4. Run the pre-release checks from the repository root: ```bash ./scripts/check_version.sh cargo build (cd docs && npm run types:check && npm run build) ``` Review the resulting diff and confirm that `Cargo.lock` contains the release version. 5. Commit the release changes to the repository. 6. Push the release commit to the remote repository without a tag. 7. Wait for the branch-triggered `CI Main` workflow to finish successfully. A `main` push publishes development packages: `X.Y.Z-devN` to crates.io and Maven Central, and `X.Y.Z.devN` to PyPI. Confirm that SimpleRsBLE, SimplePyBLE, SimpleAIBLE, SimpleJavaBLE, SimpleDroidBridge, and SimpleDroidBLE all use the expected development version. If a fix is required, push the fix and wait for its workflow to finish before continuing. Tag and publish the release [#tag-and-publish-the-release] 8. Confirm that the latest commit on `main` is the exact commit whose `CI Main` workflow succeeded, then create an annotated tag: ```bash git tag -a vX.Y.Z -m "SimpleBLE vX.Y.Z release" ``` 9. Push the tag to the remote repository. Tag-only pushes intentionally do not start `CI Main`; there is no workflow to cancel. 10. Create and publish a stable GitHub release for that tag. A draft does not trigger publication, and the release must not be marked as a prerelease. 11. Wait for the release-triggered `CI Main` workflow to finish. Do not cancel it. This workflow uploads the GitHub release artifacts and publishes the stable packages. 12. Verify the published release: * The GitHub release contains the expected artifacts and no filenames with a development suffix. * SimplePyBLE `X.Y.Z` is available on PyPI. * SimpleAIBLE `X.Y.Z` is available on PyPI. * SimpleRsBLE `X.Y.Z` is available and not yanked on crates.io. * SimpleJavaBLE `X.Y.Z` is available on Maven Central. * SimpleDroidBLE and SimpleDroidBridge `X.Y.Z` are available on Maven Central. Start the next development version [#start-the-next-development-version] 13. Advance to the next development version in the following files: * `VERSION` (next version without a development suffix) * `Cargo.toml` (next version with a `-dev1` suffix) * `Cargo.lock` (next version with a `-dev1` suffix) * `docs/content/docs/changelog.mdx` (add an empty section for the next version) * `simpleaible/pyproject.toml` (next version with a `-dev1` suffix) 14. Validate the development bump and documentation: ```bash cargo build (cd docs && npm run types:check && npm run build) ``` 15. Commit and push the development-version bump. 16. Wait for the branch-triggered `CI Main` workflow to finish successfully and confirm that the new development packages were published to crates.io, PyPI, and Maven Central with the expected `-devN`/`.devN` versions. 17. From the documentation root, run `berth app deploy` to deploy the documentation. Hardware In The Loop - Recommended Tests [#hardware-in-the-loop---recommended-tests] * Attempt connecting while already connected. * Attempt disconnecting while not connected. Documentation Structure Guide [#documentation-structure-guide] When adding documentation for new components or language bindings, follow this standardized structure to ensure consistency and a high-quality onboarding experience: 1. **Usage**: Detail installation, system requirements, and how to include the library in a project (CMake, package managers, etc.). 2. **Platform Notes**: Document OS-specific behaviors, limitations, and quirks. This is critical for Bluetooth-related components. 3. **Tutorial**: Provide a narrative, step-by-step guide that takes the user from a basic "Hello World" (e.g., listing adapters) to more advanced interactions (scanning, connecting, reading/writing). 4. **API Reference**: Use the ``, ``, and `` components to provide technical details for every public class and method. 5. **Examples**: Provide a curated list of use-cases with descriptions and direct links to the source code in the repository. 6. **FAQ**: Address common questions, troubleshooting steps, and platform-specific nuances that don't fit in the tutorial. # Installation import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; SimpleBLE is a family of libraries. Start by choosing the binding that matches your application, then install the platform dependencies that binding needs. Choose a binding [#choose-a-binding] | Binding | Language | Status | Best fit | Install path | | ----------------------------------------------- | ------------------------------- | ------------------- | ---------------------------------------------------- | -------------------------------------- | | [SimpleBLE](./simpleble/usage) | C++ | Production-ready | Native desktop, mobile native code, shared libraries | CMake from source | | [SimpleCBLE](./simplecble/usage) | C | Production-ready | C applications, FFI, language bridges | CMake from source | | [SimplePyBLE](./simplepyble/usage) | Python | Production-ready | Scripts, tools, data pipelines, tests | PyPI or local source | | [SimpleJavaBLE](./simplejavable/usage) | Java | Production-ready | JVM desktop apps and integrations | Maven Central or local source | | [SimpleRsBLE](./simplersble/usage) | Rust | Production-ready | Rust applications and async workflows | crates.io or local path | | [SimpleDroidBLE](./simpledroidble/usage) | Kotlin / Android | Beta | Android-only applications | Maven Central or local composite build | | [SimpleBLE for Unreal](./simpleunrealble/usage) | Unreal Engine / Blueprint / C++ | Fab plugin | Unreal projects that need BLE device integration | Fab or project plugin | | [SimpleBluez](./simplebluez/usage) | C++ | Lower-level utility | Direct BlueZ integration on Linux | CMake from source | | [SimpleDBus](./simpledbus/usage) | C++ | Lower-level utility | DBus object model work | CMake from source | | [SimpleAIBLE](./simpleaible/mcp) | Python / AI agents | Utility package | Local BLE tools for agents and HTTP/MCP workflows | `uv` or PyPI | Platform prerequisites [#platform-prerequisites] | Platform | Required setup | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Linux | BlueZ, DBus development headers, and permission to access the system Bluetooth stack. On Ubuntu, install `libdbus-1-dev`. | | Windows | Windows 10 or newer, Windows SDK `10.0.22621.0` or newer, and a C++20-capable MSVC toolchain such as Visual Studio 2019 16.11 or newer. Build and run 64-bit applications on 64-bit Windows. | | macOS | macOS 13.0 or newer and Xcode Command Line Tools. Packaged apps need Bluetooth usage strings in `Info.plist`. | | iOS | iOS 15.8 or newer and the normal CoreBluetooth permission flow. | | Android | API 31 or newer, Android Studio, Android NDK, manifest permissions, and runtime Bluetooth permission requests. | See [Bluetooth Permissions](./fundamentals/permissions) for the OS-specific permission details that are easy to miss on first adoption. Install commands [#install-commands] Build and install the C++ library from source: ```bash git clone https://github.com/simpleble/simpleble.git cd simpleble cmake -S simpleble -B build_simpleble cmake --build build_simpleble -j cmake --install build_simpleble --prefix "$PWD/install_simpleble" ``` Consume the installed package from your own CMake project: ```cmake find_package(simpleble REQUIRED CONFIG) target_link_libraries(your_app PRIVATE simpleble::simpleble) ``` For a local vendored build, add the library directly: ```cmake add_subdirectory(path/to/simpleble/simpleble ${CMAKE_BINARY_DIR}/simpleble) target_link_libraries(your_app PRIVATE simpleble::simpleble) ``` Build and install the C wrapper from source: ```bash git clone https://github.com/simpleble/simpleble.git cd simpleble cmake -S simplecble -B build_simplecble cmake --build build_simplecble -j cmake --install build_simplecble --prefix "$PWD/install_simplecble" ``` Consume the installed package from CMake: ```cmake find_package(simplecble REQUIRED CONFIG) target_link_libraries(your_app PRIVATE simplecble::simplecble) ``` If you are compiling manually, include `simplecble` headers and link both `simplecble` and the platform Bluetooth dependencies. Install the Python binding from PyPI: ```bash pip install simplepyble ``` Linux users also need DBus headers: ```bash sudo apt-get install libdbus-1-dev ``` Install from a local checkout when developing SimplePyBLE itself: ```bash git clone https://github.com/simpleble/simpleble.git cd simpleble pip install . ``` Add SimpleJavaBLE to your Gradle dependencies: ```kotlin dependencies { implementation("org.simpleble:simplejavable:") } ``` The Maven artifact bundles the supported Windows, macOS, and Linux native libraries. See [SimpleJavaBLE Usage](./simplejavable/usage) for source builds and native library loading options. Add the Rust crate: ```bash cargo add simplersble ``` Linux users need DBus headers before building: ```bash sudo apt-get install libdbus-1-dev ``` If you want to use a local checkout while developing: ```toml [dependencies] simplersble = { path = "path/to/simpleble" } ``` Add SimpleDroidBLE to your app module: ```kotlin dependencies { implementation("org.simpleble:simpledroidble:") } ``` For a local checkout, include the library before `include(":app")`: ```kotlin includeBuild("path/to/simpleble/simpledroidble") { dependencySubstitution { substitute(module("org.simpleble:simpledroidble")).using(project(":simpledroidble")) } } include(":app") ``` The app must also declare and request Bluetooth permissions at runtime. Install with `uv`: ```bash uv tool install simpleaible ``` Or with pip: ```bash pip install simpleaible ``` Then run the MCP server: ```bash simpleaible ``` See [MCP Server](./simpleaible/mcp) and [HTTP Server](./simpleaible/http) for agent and HTTP workflows. Verify the install [#verify-the-install] The fastest smoke test is to scan for nearby devices: * C++, C, Python, Java, Rust, and Android users can start with [Quickstart](./quickstart). * CMake users can build the repository examples from [SimpleBLE Examples](./simpleble/examples) or [SimpleCBLE Examples](./simplecble/examples). * Python users can run the examples under `examples/simplepyble`. * Rust users can run the examples under `examples/simplersble`. * Android users can run the example app under `examples/simpleble-android`. # Licensing import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; Since January 20th 2025, SimpleBLE is now available under the Business Source License 1.1 (BUSL-1.1). Each version of SimpleBLE will convert to the GNU General Public License version 3 after four years of its initial release. Qualifying non-commercial users may instead continue using and distributing that version under the original BUSL-1.1 terms under the Non-Commercial Perpetual Use Grant in `LICENSE.md`. The project is free to use for non-commercial purposes, but requires a commercial license for commercial use. We also offer FREE commercial licenses for small projects and early-stage companies - reach out to discuss your use case! **Why purchase a commercial license?** * Build and deploy unlimited commercial applications * Use across your entire development team * Zero revenue sharing or royalty payments * Choose features that match your needs and budget * Priority technical support included * Clear terms for integrating into MIT-licensed projects **Looking for information on pricing and commercial terms of service?** Visit [our website](https://simpleble.org) for more details. For further enquiries, please [email us](mailto:contact@simpleble.org) or [leave us a message on our website](https://www.simpleble.org/contact) and we can discuss the specifics of your situation. Business Source License 1.1 [#business-source-license-11] License text copyright © 2017 MariaDB Corporation Ab, All Rights Reserved. "Business Source License" is a trademark of MariaDB Corporation Ab. SimpleBLE's `LICENSE.md` includes a Non-Commercial Perpetual Use Grant. After the Change Date, qualifying non-commercial users may continue using and distributing that version under the original BUSL-1.1 terms, notwithstanding its automatic conversion to GPL-3.0. Terms [#terms] The Licensor hereby grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work. The Licensor may make an Additional Use Grant, above, permitting limited production use. Effective on the Change Date, or the fourth anniversary of the first publicly available distribution of a specific version of the Licensed Work under this License, whichever comes first, the Licensor hereby grants you rights under the terms of the Change License, and the rights granted in the paragraph above terminate. If your use of the Licensed Work does not comply with the requirements currently in effect as described in this License, you must purchase a commercial license from the Licensor, its affiliated entities, or authorized resellers, or you must refrain from using the Licensed Work. All copies of the original and modified Licensed Work, and derivative works of the Licensed Work, are subject to this License. This License applies separately for each version of the Licensed Work and the Change Date may vary for each version of the Licensed Work released by Licensor. You must conspicuously display this License on each original or modified copy of the Licensed Work. If you receive the Licensed Work in original or modified form from a third party, the terms and conditions set forth in this License apply to your use of that work. Any use of the Licensed Work in violation of this License will automatically terminate your rights under this License for the current and all other versions of the Licensed Work. This License does not grant you any right in any trademark or logo of Licensor or its affiliates (provided that you may use a trademark or logo of Licensor as expressly required by this License). TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE. Licensing FAQ [#licensing-faq] Please [email us](mailto:contact@simpleble.org) or [leave us a message on our website](https://www.simpleble.org/contact) and we can discuss the specifics of your situation. We have a free commercial license option for small projects and early-stage companies and very flexible commercial licensing options for larger projects. Nothing changes. You can continue using SimpleBLE under the terms of your commercial license. The Business Source License allows non-production use, such as development and testing in isolated environments. However, any use that could be considered production (including internal tools, revenue-generating projects, or customer-facing deployments) requires purchasing a commercial license. After four years of the initial release of a given version of SimpleBLE, the license automatically converts to the GNU General Public License version 3. Qualifying non-commercial users may instead continue using and distributing that version under the original BUSL-1.1 terms under the Non-Commercial Perpetual Use Grant. Commercial use in the context of SimpleBLE refers to any usage of the library or its derivatives in a commercial setting where it contributes, either directly or indirectly, to financial gain. This includes, but is not limited to, incorporating SimpleBLE into commercial software products, using it in the development of services offered for a fee, or deploying it within internal tools or software in a for-profit organization where it adds business value. Commercial licensing enables us to sustainably maintain and enhance SimpleBLE, particularly given its growing adoption in critical sectors like medical and industrial applications. The revenue from commercial licenses allows us to provide robust support, maintain compatibility with evolving underlying APIs, and invest in improvements that benefit all users. This includes expanded language support, better testing infrastructure, and more comprehensive documentation. While SimpleBLE remains freely available for non-commercial use, the Business Source License ensures we can continue providing the level of reliability and support that commercial applications require. # SimpleBLE SimpleBLE is the cross-platform Bluetooth Low Energy stack for Windows, macOS, Linux, iOS, and Android. Use it from C++, C, Python, Java, or Rust, with dedicated APIs for Kotlin on Android and Unreal Engine. > **New in v1.1** > > Java and Android developers no longer need a source checkout. Install **SimpleJavaBLE** or the Android-only **SimpleDroidBLE** from Maven Central, with the supported native libraries included. Why SimpleBLE [#why-simpleble] * **One cross-platform API:** Keep application-facing BLE logic consistent across desktop and mobile. * **Native underneath:** Use WinRT, CoreBluetooth, BlueZ, and Android Bluetooth through one maintained stack. * **Built for real products:** Central- and peripheral-role support across all supported platforms. * **Commercially supported:** Ship proprietary applications with licensing and direct technical support available. Language Bindings & Plugins [#language-bindings--plugins] * **C++ (SimpleBLE):** Core implementation with full platform support. * **C (SimpleCBLE):** C-wrapped version of the library. * **Python (SimplePyBLE):** Python bindings available from [PyPI](https://pypi.org/project/simplepyble/). * **Java (SimpleJavaBLE):** Java bindings available from Maven Central with bundled desktop native libraries. * **Rust (SimpleRsBLE):** Rust bindings available from [crates.io](https://crates.io/crates/simplersble). * **Android (SimpleDroidBLE) - Beta:** Android-only Kotlin bindings available from Maven Central. * **Unreal Engine (SimpleBLE for Unreal):** Unreal Engine plugin with Blueprint and C++ APIs. SimplePyBLE has surpassed 3.5 million downloads on PyPI. Getting Started [#getting-started] New to SimpleBLE or Bluetooth LE? Start with the shortest path that matches where you are: * **Need to install a binding?** Use the [Installation](./installation) guide to choose the right package or source build path for C++, C, Python, Java, Rust, Android, Unreal Engine, and agent tooling. * **Need a first working program?** Use the [Quickstart](./quickstart) to scan for nearby BLE devices with copy/paste snippets for each supported binding. * **Need to build a real workflow?** Use the [Recipes](./recipes/scan) for scanning, connecting, and GATT operations like read, write, and notify. * **New to BLE concepts?** Read [Fundamentals](./fundamentals) for Bluetooth LE basics, permissions, concurrency, and CMake setup. Support & Resources [#support--resources] We're here to help you succeed with SimpleBLE: * **Documentation**: Start with [Installation](./installation), [Quickstart](./quickstart), and the task-based [Recipes](./recipes/scan). * **Community**: Join our [Discord](https://discord.gg/N9HqNEcvP3) server for discussions and help. * **Commercial Support**: Check out [our website](https://simpleble.org?utm_source=docs\&utm_medium=referral\&utm_campaign=readme) or [email us](mailto:contact@simpleble.org) for licensing and professional services. * **Extension Libraries**: Learn about [SimpleBluez](https://github.com/simpleble/simpleble/tree/main/simplebluez), our BlueZ abstraction layer, and [SimpleDBus](https://github.com/simpleble/simpleble/tree/main/simpledbus), our DBus object hierarchy handler. **Don't hesitate to reach out if you need assistance - we're happy to help!** *** **SimpleBLE** is a project powered by **[The California Open Source Company](https://californiaopensource.com/?utm_source=docs\&utm_medium=referral\&utm_campaign=readme)**. # Quickstart import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; This quickstart gets you to the first adoption milestone: your application can see nearby BLE devices. After that, move to the [Connect](./recipes/connect) and [Read, Write, Notify](./recipes/read-write-notify) recipes, or [Advertise](./recipes/advertise) to host a local peripheral. Before you run [#before-you-run] 1. Install the binding or enable the plugin for your target environment. 2. Turn on Bluetooth on the host device. 3. Grant OS permissions when required. On macOS, iOS, and Android this usually means app-level permission prompts or manifest entries. 4. Keep at least one BLE peripheral nearby and advertising. Scan for nearby devices [#scan-for-nearby-devices] ```cpp #include #include #include int main() { if (!SimpleBLE::Adapter::bluetooth_enabled()) { std::cerr << "Bluetooth is not enabled or permission is missing." << std::endl; return EXIT_FAILURE; } auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { std::cerr << "No Bluetooth adapters found." << std::endl; return EXIT_FAILURE; } auto adapter = adapters.front(); std::cout << "Using adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl; adapter.set_callback_on_scan_found([](SimpleBLE::Peripheral peripheral) { std::cout << "Found: " << peripheral.identifier() << " [" << peripheral.address() << "] " << peripheral.rssi() << " dBm" << std::endl; }); adapter.scan_for(5000); std::cout << "Scan results:" << std::endl; for (auto& peripheral : adapter.scan_get_results()) { std::cout << "- " << peripheral.identifier() << " [" << peripheral.address() << "] " << (peripheral.is_connectable() ? "connectable" : "not connectable") << std::endl; } return EXIT_SUCCESS; } ``` ```c #include #include #include #include int main(void) { size_t adapter_count = simpleble_adapter_get_count(); if (adapter_count == 0) { printf("No Bluetooth adapters found.\n"); return 1; } simpleble_adapter_t adapter = simpleble_adapter_get_handle(0); if (adapter == NULL) { printf("Unable to open adapter 0.\n"); return 1; } simpleble_adapter_scan_for(adapter, 5000); size_t peripheral_count = simpleble_adapter_scan_get_results_count(adapter); printf("Found %zu peripherals:\n", peripheral_count); for (size_t i = 0; i < peripheral_count; i++) { simpleble_peripheral_t peripheral = simpleble_adapter_scan_get_results_handle(adapter, i); char* identifier = simpleble_peripheral_identifier(peripheral); char* address = simpleble_peripheral_address(peripheral); bool connectable = false; simpleble_peripheral_is_connectable(peripheral, &connectable); printf("[%zu] %s [%s] %s\n", i, identifier, address, connectable ? "connectable" : "not connectable"); simpleble_free(identifier); simpleble_free(address); simpleble_peripheral_release_handle(peripheral); } simpleble_adapter_release_handle(adapter); return 0; } ``` ```python import simplepyble adapters = simplepyble.Adapter.get_adapters() if not adapters: raise SystemExit("No Bluetooth adapters found.") adapter = adapters[0] print(f"Using adapter: {adapter.identifier()} [{adapter.address()}]") adapter.set_callback_on_scan_found( lambda peripheral: print( f"Found: {peripheral.identifier()} [{peripheral.address()}] {peripheral.rssi()} dBm" ) ) adapter.scan_for(5000) print("Scan results:") for peripheral in adapter.scan_get_results(): state = "connectable" if peripheral.is_connectable() else "not connectable" print(f"- {peripheral.identifier()} [{peripheral.address()}] {state}") ``` ```java import org.simplejavable.Adapter; import org.simplejavable.Peripheral; import java.util.List; public class ScanExample { public static void main(String[] args) throws Exception { if (!Adapter.isBluetoothEnabled()) { System.err.println("Bluetooth is not enabled or permission is missing."); return; } List adapters = Adapter.getAdapters(); if (adapters.isEmpty()) { System.err.println("No Bluetooth adapters found."); return; } Adapter adapter = adapters.get(0); System.out.println("Using adapter: " + adapter.getIdentifier() + " [" + adapter.getAddress() + "]"); adapter.setEventListener(new Adapter.EventListener() { @Override public void onScanFound(Peripheral peripheral) { System.out.println("Found: " + peripheral.getIdentifier() + " [" + peripheral.getAddress() + "] " + peripheral.getRssi() + " dBm"); } }); adapter.scanFor(5000); System.out.println("Scan results:"); for (Peripheral peripheral : adapter.scanGetResults()) { String state = peripheral.isConnectable() ? "connectable" : "not connectable"; System.out.println("- " + peripheral.getIdentifier() + " [" + peripheral.getAddress() + "] " + state); } } } ``` Add the dependency: ```toml [dependencies] simplersble = "0.14" tokio = { version = "1", features = ["full"] } futures = "0.3" ``` Then scan: ```rust use futures::stream::StreamExt; #[tokio::main] async fn main() { let mut adapters = simplersble::Adapter::get_adapters().unwrap(); if adapters.is_empty() { println!("No Bluetooth adapters found."); return; } let adapter = adapters.remove(0); let mut events = adapter.on_scan_event(); tokio::spawn(async move { while let Some(Ok(event)) = events.next().await { if let simplersble::ScanEvent::Found(peripheral) = event { println!( "Found: {} [{}] {} dBm", peripheral.identifier().unwrap(), peripheral.address().unwrap(), peripheral.rssi().unwrap() ); } } }); adapter.scan_for(5000).unwrap(); println!("Scan results:"); for peripheral in adapter.scan_get_results().unwrap() { println!( "- {} [{}]", peripheral.identifier().unwrap(), peripheral.address().unwrap() ); } } ``` Declare Bluetooth permissions and request them at runtime before calling the adapter APIs. A minimal Compose scan flow looks like this: ```kotlin import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import kotlinx.coroutines.launch import org.simpleble.android.Adapter @Composable fun ScanScreen() { val adapter = remember { Adapter.getAdapters().first() } val results = remember { mutableStateOf(emptyList()) } LaunchedEffect(Unit) { launch { adapter.onScanFound.collect { peripheral -> val line = "${peripheral.identifier} [${peripheral.address}] ${peripheral.rssi} dBm" Log.d("SimpleBLE", line) results.value = results.value + line } } adapter.scanFor(5000) } } ``` The full Android example app shows permission setup, adapter ownership, scanning, connection, GATT read/write, notification, and cleanup flows. Use the Unreal plugin nodes to run a short scan from Blueprint: 1. Create a `Simple BLE Manager` object and keep it referenced while the scan is active. 2. Bind `On Device Found` to collect or display discovered device names. 3. Bind `On Scan Finished` to update your UI when discovery ends. 4. Call `Initialize`. 5. If initialization succeeds, call `Start Scan` with a timeout such as `5000`. 6. Use `Get Found Devices` to inspect the discovered device names, identifiers, or addresses. For C++ Unreal projects, create and keep a `USimpleBLEManager` reference on a UObject that outlives the scan: ```cpp #include "SimpleBLE/Manager.h" void UMyBluetoothController::StartBluetoothScan() { Manager = NewObject(this); Manager->OnDeviceFound.AddDynamic(this, &UMyBluetoothController::HandleDeviceFound); Manager->OnScanFinished.AddDynamic(this, &UMyBluetoothController::HandleScanFinished); if (!Manager->Initialize()) { UE_LOG(LogTemp, Warning, TEXT("SimpleBLE could not initialize.")); return; } Manager->StartScan(5000); } ``` See the [SimpleBLE for Unreal quickstart](./simpleunrealble/quickstart) for connect, read, write, notify, payload helpers, and the lower-level adapter/peripheral API. What this proves [#what-this-proves] If the scan prints devices or the Unreal scan callback reports devices, the binding or plugin is wired up, the OS backend is available, Bluetooth is enabled, and permissions are good enough for discovery. The next step is to connect to one of the connectable peripherals and inspect its GATT services. Next steps [#next-steps] * [Scan recipe](./recipes/scan): scan lifecycle, callbacks, and result handling. * [Connect recipe](./recipes/connect): choose a device, connect, list services and characteristics. * [Read, Write, Notify](./recipes/read-write-notify): perform GATT operations once connected. * [SimpleBLE for Unreal quickstart](./simpleunrealble/quickstart): Blueprint and C++ flows for Unreal Engine projects. # Bluetooth LE Basics SimpleBLE abstracts away much of the complexity of Bluetooth Low Energy (BLE), but having a basic understanding of how BLE works will help you use the library more effectively. Device Roles [#device-roles] In BLE, there are two primary roles when two devices are communicating: * **Central:** Typically a smartphone or computer (like the one running SimpleBLE). The Central scans for advertisements and initiates connections. * **Peripheral:** Typically a small, low-power device (like a heart rate monitor or a smart bulb). The Peripheral advertises its presence and waits for a Central to connect. SimpleBLE can fill either role on every supported platform. Use `SimpleBLE::Adapter` and `SimpleBLE::Peripheral` in the central role, or `SimpleBLE::Local::Peripheral` to host a local peripheral. Android applications can use either the C++ API or SimpleDroidBLE's Kotlin API. See [Advertise, Serve, Publish](../simpleble/peripheral/advertise-serve-publish). Advertising and Scanning [#advertising-and-scanning] Peripherals send out small packets of data called **Advertisements** to announce their presence. These packets can contain: * The device name. * List of supported services. * Manufacturer-specific data. * TX Power level. Centrals perform **Scanning** to listen for these advertisements. Once a Central finds a Peripheral it's interested in, it can initiate a **Connection**. Services and Characteristics (GATT) [#services-and-characteristics-gatt] Once connected, devices communicate using the **Generic Attribute Profile (GATT)**. Data is organized into a hierarchy: Services [#services] A **Service** is a collection of data and associated behaviors to accomplish a particular function. For example, a "Heart Rate Service" would group together all the features related to heart rate monitoring. Characteristics [#characteristics] A **Characteristic** is the smallest logical unit of data within a service. It contains a single value and any number of **Descriptors** that describe the characteristic's value. Characteristics can have different properties, such as: * **Read:** The Central can read the value from the Peripheral. * **Write:** The Central can write a new value to the Peripheral. * **Notify/Indicate:** The Peripheral can push updates to the Central when the value changes. UUIDs [#uuids] Every Service and Characteristic is identified by a **Universally Unique Identifier (UUID)**. * Standardized UUIDs (defined by the Bluetooth SIG) are 16-bit (e.g., `0x180D` for Heart Rate Service). * Custom UUIDs are 128-bit (e.g., `00001234-0000-1000-8000-00805f9b34fb`). Connections and Pairing [#connections-and-pairing] * **Connection:** A state where two devices are synchronized and can exchange data. * **Pairing:** The process of exchanging security keys to encrypt the connection. * **Bonding:** The process of storing those keys for future use, so the devices don't have to pair again next time they connect. # CMake Primer This guide introduces the core concepts of CMake for beginners, focusing on the essential commands for configuring and building projects, the difference between building and installing, and how to use installation prefixes. It aims to help you understand CMake's workflow and apply it to any project. What is CMake? [#what-is-cmake] CMake is a cross-platform tool that automates the process of building software. Instead of writing platform-specific build scripts (e.g., Makefiles or Visual Studio project files), you write a single `CMakeLists.txt` file. CMake uses this file to generate build files tailored to your system, making your project portable across Windows, Linux, MacOS, and more. Prerequisites [#prerequisites] * **CMake**: Version 3.21 or higher. Download and install from [cmake.org](https://cmake.org/). * A C++ compiler (e.g., GCC, Clang, MSVC) or another compiler supported by your project. * A project with a `CMakeLists.txt` file (the configuration file for CMake). Core CMake Workflow [#core-cmake-workflow] CMake operates in two main phases: **configuration** and **building**. A third optional phase, **installation**, makes the built software available for use. Let's break these down. 1. Configuring the Project [#1-configuring-the-project] Configuration is where CMake reads the `CMakeLists.txt` file and generates build files (e.g., Makefiles, Ninja files, or IDE project files) based on your system and preferences. Run the following command from your project's root directory: ```bash cmake -S -B ``` * `-S `: Specifies the **source directory**, where the `CMakeLists.txt` file is located (e.g., `/home/user/project`). * `-B `: Specifies the **build directory** (e.g., `/home/user/project-build`), where CMake will store generated build files and temporary files. **Why a separate build directory?** Keeping build files separate from source files avoids cluttering your project and makes it easy to delete build artifacts for a fresh start. After running this command, CMake creates `` containing all the files needed to build your project. 2. Building the Project [#2-building-the-project] Building compiles the source code into executables, libraries, or other artifacts using the generated build files. Run: ```bash cmake --build -j4 ``` * `--build `: Tells CMake to build the project in the specified directory. * `-j4`: Uses 4 parallel jobs to speed up compilation (adjust based on your CPU cores, e.g., `-j8` for 8 cores). This produces the final output (e.g., `.exe` files, `.a` or `.so` libraries) in ``. At this stage, the project is built but not yet installed. **Key Point**: Building creates the software in the build directory, but it's not yet integrated into your system or ready for use by other projects. 3. Installing the Project [#3-installing-the-project] Installation copies the built files (executables, libraries, headers, etc.) to a location where they can be used by your system or other projects. This is distinct from building because it makes the software accessible outside the build directory. Run: ```bash cmake --install ``` This installs files to a default location, typically: * Linux/MacOS: `/usr/local` (e.g., binaries in `/usr/local/bin`, libraries in `/usr/local/lib`). * Windows: `C:\Program Files\`. **Why install?** Installation is necessary if: * You want to run the program from anywhere on your system (e.g., via the command line). * You're building a library that other projects need to link against. * You want to package the software for distribution. Customizing the Installation Location with CMAKE_INSTALL_PREFIX [#customizing-the-installation-location-with-cmake_install_prefix] You can specify where to install the project using the `CMAKE_INSTALL_PREFIX` variable. This is useful for installing to non-standard locations or for testing without affecting system directories. Set the prefix during installation: ```bash cmake --install --prefix ``` Or, set it during configuration to make it persistent: ```bash cmake -S -B -DCMAKE_INSTALL_PREFIX= cmake --build -j4 cmake --install ``` Example: * `` could be `/home/user/my-install` or `C:\CustomInstall`. **Why is a prefix needed?** The prefix defines the root directory for installation, ensuring files are organized predictably (e.g., binaries in `/bin`, libraries in `/lib`). Without a prefix, CMake uses a default that may require administrative privileges or conflict with system files. **Note for Linux/MacOS**: Installing to system directories (e.g., `/usr/local`) often requires `sudo`: ```bash sudo cmake --install ``` Building Shared vs. Static Libraries [#building-shared-vs-static-libraries] If your project includes libraries, you can choose between **shared** (dynamically linked) and **static** (statically linked) libraries using the `BUILD_SHARED_LIBS` variable. * **Shared Library** (e.g., `.so` on Linux, `.dll` on Windows): ```bash cmake -S -B -DBUILD_SHARED_LIBS=TRUE cmake --build -j4 ``` * **Static Library** (e.g., `.a` on Linux, `.lib` on Windows): ```bash cmake -S -B -DBUILD_SHARED_LIBS=FALSE cmake --build -j4 ``` **What's the difference?** * **Shared libraries** are loaded at runtime, reducing executable size but requiring the library to be present on the system. * **Static libraries** are embedded in the executable, increasing its size but making it self-contained. If `BUILD_SHARED_LIBS` is not set, the project's `CMakeLists.txt` determines the default behavior. Consuming CMake Projects from Xcode [#consuming-cmake-projects-from-xcode] When using a CMake-based library from Xcode, create a small CMake consumer project instead of manually adding the built library file to a native Xcode project. Find the installed package and link against its exported imported target, such as `simpleble::simpleble`, in your `CMakeLists.txt`: ```cmake cmake_minimum_required(VERSION 3.21) project(MyApp LANGUAGES CXX) find_package(simpleble REQUIRED CONFIG) add_executable(MyApp main.cpp) target_link_libraries(MyApp PRIVATE simpleble::simpleble) ``` Then generate an Xcode project from that consumer project: ```bash cmake -S . -B build-xcode -G Xcode open build-xcode/MyApp.xcodeproj ``` This keeps CMake in charge of the dependency information attached to the imported target, including include paths, compile options, libraries, and Apple framework link flags. Manually adding a naked static library to a native Xcode project skips those transitive usage requirements and often leads to missing headers or linker errors. Passing Configuration Options [#passing-configuration-options] CMake allows customization through variables set with the `-D` flag during configuration. These variables control project-specific settings defined in the `CMakeLists.txt`. For example: ```bash cmake -S -B -DMY_PROJECT_FEATURE=ON -DMY_LOG_LEVEL=DEBUG ``` * `-DMY_PROJECT_FEATURE=ON`: Enables a project-specific feature (if supported). * `-DMY_LOG_LEVEL=DEBUG`: Sets a custom log level (if defined). Check your project's documentation for available variables. Common ones include: * `CMAKE_BUILD_TYPE`: Sets the build type (e.g., `Debug`, `Release`): ```bash cmake -S -B -DCMAKE_BUILD_TYPE=Release ``` Troubleshooting Tips [#troubleshooting-tips] * **Verbose Output**: To see detailed build steps, add `--verbose`: ```bash cmake --build --verbose ``` * **Clean Build**: To start fresh, delete `` and re-run configuration: ```bash rm -rf cmake -S -B ``` * **Reconfiguration**: If you change options, re-run the configuration command to update the build files. * **Check CMake Cache**: The `/CMakeCache.txt` file stores configuration settings. If issues persist, delete it or the entire build directory. # Concurrency within SimpleBLE import { Callout } from 'fumadocs-ui/components/callout'; SimpleBLE deliberately exposes a **blocking, synchronous API**. Calls like `connect()`, `read()`, or `scan_for()` do not return until the operation has completed, failed, or timed out. This is a design choice: control flow stays linear and understandable, there is no async runtime to integrate against, and the same mental model works identically from C++, Python, Java, or any other binding. The cost of that choice is that the threading rules become part of the API contract. This page is that contract: which threads your callbacks run on, what you are allowed to call from inside a callback, and how long every operation can block. Which thread runs your callbacks [#which-thread-runs-your-callbacks] SimpleBLE never invokes callbacks on your application's main thread. Every backend delivers callbacks — `set_callback_on_scan_found`, `set_callback_on_scan_updated`, notification/indication callbacks, connect/disconnect callbacks — from an internal thread owned by the library or the operating system: | Platform | Callback thread | Notes | | ------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Linux (BlueZ) | A single DBus dispatch thread owned by SimpleBLE | Spawned when the backend initializes; it pumps all DBus messages, signals, and method replies for the whole process. All callbacks are serialized on it. | | macOS / iOS | A serial CoreBluetooth dispatch queue | One serial queue per adapter. CoreBluetooth delegate events and your callbacks are serialized on it. | | Windows | WinRT threadpool threads | Advertisement and GATT events arrive on arbitrary threadpool threads belonging to the multithreaded apartment (MTA). Callbacks are **not** guaranteed to arrive on the same thread every time. | | Android | A single SimpleJNI runner thread owned by SimpleBLE | JNI callbacks from the Android stack are enqueued onto this thread; all callbacks are serialized on it. | Two consequences apply on every platform: 1. **Thread safety is your responsibility.** Any data your callback touches must be protected (mutex, atomic, thread-safe queue) if the rest of your application also touches it. 2. **Callbacks block the delivery pipeline.** On Linux, macOS, and Android a single thread (or serial queue) delivers *all* events. While your callback runs, no other scan result, notification, or disconnection event can be delivered. Keep callbacks short. Calling SimpleBLE from inside a callback [#calling-simpleble-from-inside-a-callback] Because most SimpleBLE operations block until an event is delivered — and callbacks run on the very thread that delivers events — calling back into SimpleBLE from inside a callback is unsafe on most platforms, with consequences that range from delayed events to a deadlock depending on the backend. **The portable rule: treat callbacks like interrupt handlers.** Copy the data you need, signal another thread (queue, condition variable, channel), and return. Do all SimpleBLE calls from your own threads. The per-platform details: Linux [#linux] Method calls to BlueZ — `connect()`, `disconnect()`, `unpair()`, `read()`, `write_request()`, `write_command()`, `notify()`, `indicate()`, `unsubscribe()`, `scan_start()`, `scan_stop()` — are sent asynchronously and their replies are pumped by the same dispatch thread that runs your callbacks. Calling any of them from inside a callback therefore stalls: the reply can never be processed, the call blocks for the full 30-second DBus reply timeout, and then throws. Property reads — `rssi()`, `is_connected()`, `is_paired()`, `identifier()`, `address()` and similar — use blocking DBus calls that do not depend on the dispatch thread and are safe to call from callbacks. macOS / iOS [#macos--ios] GATT operations block the calling thread in a poll loop until the corresponding CoreBluetooth delegate event fires — and delegate events are delivered on the same serial queue your callback is running on. Calling `read()`, `write_request()`, `notify()`, `unsubscribe()`, or any other peripheral operation from inside a notification or scan callback **deadlocks permanently**: most of these waits are unbounded, so no timeout will unblock the call. Do not call peripheral methods from callbacks on macOS. Windows [#windows] SimpleBLE marshals its own WinRT calls onto a dedicated internal MTA thread, which is separate from the threadpool threads that deliver your callbacks. Calling SimpleBLE methods from inside a callback therefore does not self-deadlock, but it blocks a threadpool event thread and can delay other events; short calls are tolerable, long blocking calls (like `connect()` or a blocking `disconnect()`) should still be moved to your own thread. Android [#android] Operation completions are delivered through the same single runner thread that executes your callbacks. Calling a blocking operation from inside a callback stalls until its timeout expires and then throws: 5 seconds for GATT reads/writes, 8 seconds for `connect()`/`disconnect()`. The most frequent mistake on every platform is issuing a `read()` or `write_request()` from inside a notification callback — for example, responding to a device's notification with a command. On macOS this deadlocks forever; on Linux it stalls 30 seconds and throws; on Android it stalls 5 seconds and throws. Push the notification payload into a queue and let a worker thread issue the response instead. Timeout reference [#timeout-reference] Every blocking operation in SimpleBLE eventually gives up. The table below lists every wait in the library, per backend. "Hardcoded" means the value cannot be changed through `SimpleBLE::Config`. Linux (BlueZ backend) [#linux-bluez-backend] | Operation | Timeout | Configurable | | ---------------------------------------------------------------------- | --------------------------- | -------------------------------------------- | | Any BlueZ method reply (connect, read, write, notify, scan control, …) | 30 s | Hardcoded (internal DBus reply timeout) | | Connect: wait for connection + service resolution | 2 s per attempt, 5 attempts | `Config::SimpleBluez::connection_timeout` | | Disconnect: wait for disconnection | 1 s per attempt, 5 attempts | `Config::SimpleBluez::disconnection_timeout` | | Unsubscribe: wait for notification session to stop | 5 s (50 ms poll) | Hardcoded | On failure after all attempts, `connect()`/`disconnect()` throw `SimpleBLE::Exception::OperationFailed`. macOS / iOS (CoreBluetooth backend) [#macos--ios-corebluetooth-backend] | Operation | Timeout | Configurable | | ---------------------------------------------------------- | --------- | ------------ | | Connect | 5 s | Hardcoded | | Adapter power on/off, central manager startup | 5 s | Hardcoded | | Disconnect | Unbounded | — | | Service / characteristic / descriptor discovery | Unbounded | — | | Read, write, notify, indicate, unsubscribe, descriptor ops | Unbounded | — | The unbounded waits poll every 10 ms until the corresponding CoreBluetooth delegate event arrives. In practice CoreBluetooth reports errors promptly, but if the OS never delivers an event (for example, the peripheral vanished mid-operation) the call can hang indefinitely. Windows (WinRT backend) [#windows-winrt-backend] | Operation | Timeout | Configurable | | ------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------ | | Disconnect (blocking mode) | 10 s | Only reached when `Config::WinRT::use_deferred_disconnect = false` | | Scan start: wait for pending deferred disconnects | 5 s (100 ms poll) | Only when `use_deferred_disconnect = true` (default) | | Scan stop acknowledgment | 1 s (silent on timeout) | Hardcoded | | GATT operations | Unbounded (WinRT async completion) | — | Android [#android-1] | Operation | Timeout | Configurable | | ------------------------------------- | ------- | ------------ | | Connect (including service discovery) | 8 s | Hardcoded | | Disconnect | 8 s | Hardcoded | | Characteristic read / write | 5 s | Hardcoded | | Descriptor read / write | 5 s | Hardcoded | Configuration knobs [#configuration-knobs] `SimpleBLE::Config` (in `simpleble/Config.h`) exposes the tunable subset of the behavior above. **All values must be set before calling `Adapter::get_adapters()`** — changes made after the backend is instantiated may not take effect. | Knob | Default | Effect | | --------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- | | `Config::SimpleBluez::connection_timeout` | 2 s | Per-attempt wait for connection + service resolution on Linux (5 attempts). | | `Config::SimpleBluez::disconnection_timeout` | 1 s | Per-attempt wait for disconnection on Linux (5 attempts). | | `Config::SimpleBluez::use_system_bus` | `true` | Connect the Linux BlueZ backend to the DBus system bus. | | `Config::WinRT::use_deferred_disconnect` | `true` | Disconnects return immediately and clean up in the background instead of blocking up to 10 s. | | `Config::WinRT::experimental_use_own_mta_apartment` | `true` | SimpleBLE runs its own MTA thread for WinRT calls. Disabling runs WinRT calls on your calling thread. | | `Config::Android::connection_priority_request` | `DISABLED` | Request a connection priority (`BALANCED`, `HIGH`, `LOW_POWER`, `DCK`) after connecting. | The 30-second DBus reply timeout on Linux is internal to the DBus layer and is not exposed through `SimpleBLE::Config`. UI applications and event loops [#ui-applications-and-event-loops] Using SimpleBLE with UI frameworks (Qt, WxWidgets, WinForms, Unity, …) requires special care. Most UI frameworks are not thread-safe and require all UI updates to happen on the main/UI thread — and as established above, SimpleBLE callbacks never run there. If you attempt to update a UI element directly from a SimpleBLE callback, your application might crash or freeze. Instead: * Use the UI framework's message passing or "invoke on main thread" mechanism (`QMetaObject::invokeMethod`, `Control.BeginInvoke`, Unity's main-thread dispatcher patterns, …). * Or signal an event that the main thread's loop picks up and processes. The same applies in reverse: don't run blocking SimpleBLE operations (`connect()`, `scan_for()`) on the UI thread, or your interface will freeze for the duration. A dedicated BLE worker thread that owns all SimpleBLE calls, fed by a message queue and feeding results back to the UI thread, is the pattern that works in every framework. Rules of thumb [#rules-of-thumb] 1. Keep callbacks lightweight: copy data out and return. 2. Never call SimpleBLE operations from inside a callback; hand the work to another thread. 3. Own all blocking SimpleBLE calls from a dedicated worker thread, not the UI thread. 4. Protect any state shared between callbacks and the rest of your application. 5. Set `Config` values before touching `Adapter::get_adapters()`. # Error Handling import { Callout } from 'fumadocs-ui/components/callout'; SimpleBLE reports errors by throwing exceptions. All library-defined exceptions live in the `SimpleBLE::Exception` namespace (declared in `simpleble/Exceptions.h`) and derive from `SimpleBLE::Exception::BaseException`, which itself derives from `std::runtime_error`. In Python, exceptions surface as `RuntimeError` with the same message text. The C bindings translate exceptions into return codes instead. Exception taxonomy [#exception-taxonomy] | Exception | Thrown when | Typical cause | | ------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | `NotInitialized` | Any method is called on a default-constructed `Adapter`, `Peripheral`, or `SimpleBLE::Local::*` object | Programming error — obtain objects from `get_adapters()`, scan results, or `create_local_peripheral()` | | `InvalidReference` | The internal object behind a handle is no longer valid | Programming error — a stale handle outlived its backend object | | `NotConnected` | A GATT operation is called while the peripheral is not connected | Device disconnected, or `connect()` was never called | | `ServiceNotFound` | The requested service UUID isn't in the discovered GATT database | Wrong UUID, or the device doesn't expose that service | | `CharacteristicNotFound` | The requested characteristic UUID isn't under that service | Wrong UUID pair | | `DescriptorNotFound` | The requested descriptor UUID isn't under that characteristic | Wrong UUID triple | | `OperationNotSupported` | The characteristic doesn't have the required capability, or the backend doesn't support the operation | e.g. calling `read()` on a write-only characteristic | | `OperationFailed` | The operation was attempted but did not complete successfully | Timeouts, stack-level errors, connection failures, mutating a local peripheral while it is started | | `WinRTException` | A WinRT API call failed (Windows only) | Carries the WinRT error code and message | | `CoreBluetoothException` | A CoreBluetooth operation failed (macOS/iOS only) | Carries the `NSError` description | Recoverable vs. fatal [#recoverable-vs-fatal] As a rule of thumb: * **Programming errors — fix your code, don't retry:** `NotInitialized`, `InvalidReference`, `OperationNotSupported`, and the three `*NotFound` exceptions. If the UUIDs and capabilities are right, these should never fire in production. The one nuance: `*NotFound` can also occur when talking to a fleet of devices with different firmware revisions — in that case treat it as "this device doesn't support the feature," not as a retry candidate. * **Runtime errors — retry or reconnect:** `NotConnected`, `OperationFailed`, `WinRTException`, `CoreBluetoothException`. These reflect transient radio and stack conditions. The standard recovery is: check `is_connected()`, reconnect if needed, re-establish subscriptions, and retry the operation. See the [reconnect loop recipe](../recipes/reconnect-loop) for a production-ready pattern. Catch std::exception, not just BaseException [#catch-stdexception-not-just-baseexception] On some platforms, lower-layer errors surface through the public API without being translated into `SimpleBLE::Exception` types. On Linux, a failed DBus call inside a GATT operation can surface as a `SimpleDBus::Exception::SendFailed` (which derives from `std::exception` but **not** from `BaseException`), and the internal 30-second DBus reply timeout throws a plain `std::runtime_error`. On Android, GATT operation timeouts throw plain `std::runtime_error` as well. To cover both the classified exceptions and platform-layer errors, catch at two levels: ```cpp try { auto value = peripheral.read(service_uuid, characteristic_uuid); } catch (const SimpleBLE::Exception::BaseException& e) { // Classified SimpleBLE error: inspect the concrete type if needed. std::cerr << "BLE error: " << e.what() << std::endl; } catch (const std::exception& e) { // Lower-layer error that escaped translation (DBus failure, timeout, ...). std::cerr << "Unclassified error: " << e.what() << std::endl; } ``` In Python everything arrives as `RuntimeError`, so a single `except RuntimeError` handler covers both cases: ```python try: value = peripheral.read(service_uuid, characteristic_uuid) except RuntimeError as e: print(f"BLE error: {e}") ``` Timeout semantics [#timeout-semantics] Every blocking SimpleBLE call eventually gives up; the complete per-platform timeout table lives in [Concurrency](./concurrency#timeout-reference). What a timeout *means* is worth spelling out: * **A timeout is not a rollback.** When `connect()` or a GATT operation throws after its timeout, the underlying OS operation may still be in flight and may even complete afterwards. SimpleBLE does not attempt to cancel it on every platform. * **State is uncertain after a timeout.** The only reliable move is to re-check observable state: call `is_connected()`, and if you were mid-transaction with the device, verify the device-side effect before repeating a non-idempotent write. * **Timeouts usually surface as `OperationFailed`** (connect/disconnect on most platforms), with the Linux/Android caveats described above. What happens to in-flight operations on disconnect [#what-happens-to-in-flight-operations-on-disconnect] When a device drops the connection while an operation is blocked waiting for its result, behavior differs by platform: | Platform | In-flight operation outcome on disconnect | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Linux | BlueZ fails the pending DBus call; the error escapes as a DBus-layer exception (see callout above). The `on_disconnected` callback fires after internal cleanup. | | Windows | The WinRT operation completes with a failure status and SimpleBLE throws `OperationFailed`. | | macOS / iOS | Pending operations are unblocked without an error being recorded: an in-flight `read()` can return empty or stale data without throwing, and an in-flight `write_request()` can return as if it succeeded. A `read()` on a characteristic that is actively notifying waits for the next notification — if the device disconnects instead, that call can hang indefinitely. | | Android | The completion callback never arrives, so the operation stalls for its full 5-second timeout and then throws. | Treat any result that races a disconnect as suspect. Use `set_callback_on_disconnected` as the source of truth for link state, prefer idempotent device commands where the protocol allows, and validate critical reads (e.g. sequence numbers or checksums in the payload) rather than trusting that a returned buffer implies a healthy link. Errors inside callbacks [#errors-inside-callbacks] Exceptions thrown from *your* callbacks do not propagate into SimpleBLE: the library invokes every user callback inside a try/catch and logs the failure. A throwing callback will not crash your application, but it also won't be reported anywhere except the log — do your own error handling inside callbacks that matter. # Fundamentals Welcome to the Fundamentals section. Whether you are new to Bluetooth Low Energy or an experienced developer looking to understand SimpleBLE's internals, these guides cover the essential knowledge required to build robust applications. In this section [#in-this-section] Bluetooth LE Basics [#bluetooth-le-basics] A high-level introduction to how Bluetooth Low Energy works, including device roles (Central/Peripheral), the GATT hierarchy (Services/Characteristics), and UUIDs. CMake Primer [#cmake-primer] A beginner-friendly guide to using CMake to configure, build, and install SimpleBLE in your own projects. Permissions [#permissions] A guide to the various permissions required by different operating systems (Windows, macOS, Linux, Android, and iOS) to access Bluetooth functionality. Error Handling [#error-handling] SimpleBLE's exception taxonomy, which errors are worth retrying, what timeouts actually mean, and how in-flight operations behave when a device disconnects. Concurrency [#concurrency] The threading and callback contract for SimpleBLE's synchronous API: which threads run your callbacks on each platform, what you can safely call from inside a callback, every timeout in the library, and how to integrate with UI frameworks like Qt, WinForms, or Unity. # Bluetooth Permissions Bluetooth is a sensitive capability that requires explicit permissions from the operating system and, in many cases, the user. SimpleBLE attempts to simplify this, but you still need to configure your application correctly. Desktop Platforms [#desktop-platforms] Windows [#windows] Generally, Windows doesn't require special manifest entries for Bluetooth if you're building a standard Win32 application. However, users must have Bluetooth enabled in the system settings. Note that if you attempt to pair with a device, Windows will typically show a system notification or pop-up asking the user to confirm the pairing request. macOS [#macos] Starting with macOS 10.15 (Catalina), applications must include the `NSBluetoothAlwaysUsageDescription` key in their `Info.plist` file. This key should contain a string explaining why the app needs Bluetooth access. ```xml NSBluetoothAlwaysUsageDescription This app needs Bluetooth access to connect to your smart device. ``` The first time your application attempts to use Bluetooth, the operating system will automatically prompt the user to grant access using the description provided above. If your app targets older versions of macOS, you might also need `NSBluetoothPeripheralUsageDescription`. Similar to Windows, any pairing requests will trigger a macOS system UI that the user must interact with to complete the process. Linux [#linux] On Linux, permissions are usually handled by the `bluetooth` group or via D-Bus policies. Users might need to be part of the `lp` or `bluetooth` group depending on the distribution. Mobile Platforms [#mobile-platforms] Android [#android] Android has the most complex permission model for Bluetooth. Depending on the API level, you need different permissions in your `AndroidManifest.xml`: **For all versions:** ```xml ``` **For Android 12 (API 31) and above:** ```xml ``` Additionally, you must **request these permissions at runtime**. SimpleDroidBLE provides helpers for this. **Note on Location:** On many Android versions, `ACCESS_FINE_LOCATION` or `ACCESS_COARSE_LOCATION` is required to perform Bluetooth scans because beacons can be used to infer the user's location. iOS [#ios] Similar to macOS, iOS requires the `NSBluetoothAlwaysUsageDescription` in the `Info.plist`. You must also request permission at runtime, which typically happens automatically the first time you try to use the Bluetooth adapter. Checking Permission State [#checking-permission-state] In SimpleBLE, you can check if Bluetooth is available and permissions are granted using: ```cpp if (!SimpleBLE::Adapter::bluetooth_enabled()) { // Bluetooth is off or permissions are missing } ``` On Android, you should use the platform-specific helpers to request permissions before calling any SimpleBLE functions. # Migrating from Bleak import { Callout } from 'fumadocs-ui/components/callout'; [Bleak](https://github.com/hbldh/bleak) is an asyncio-based BLE client library for Python; SimplePyBLE is the Python binding for SimpleBLE's synchronous C++ core. Both cover the central role (scan, connect, GATT operations, notifications) on Windows, macOS, and Linux. Migrating is mostly a mechanical translation: remove the `async`/`await` scaffolding and swap the method names. The mental-model shift [#the-mental-model-shift] Bleak structures your program around an event loop: every operation is a coroutine, and callbacks are delivered on the loop. SimplePyBLE structures it around plain blocking calls: `connect()` returns when the connection is up, `scan_for(5000)` returns when the scan is done, and callbacks are delivered on a native background thread. SimplePyBLE releases the GIL during every blocking operation (`scan_for`, `connect`, `read`, `write_request`, …) — the actual BLE work happens on native OS threads, not in the Python interpreter. A blocking `connect()` does not stall your other Python threads. This means wrapping SimplePyBLE in asyncio executors gains you little unless the rest of your application is already async; plain threads work just as well. API mapping [#api-mapping] | Task | bleak (asyncio) | SimplePyBLE (sync) | | --------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | Pick an adapter | implicit (or `adapter=` kwarg on some backends) | `simplepyble.Adapter.get_adapters()[0]` | | One-shot scan | `await BleakScanner.discover(timeout=5.0)` | `adapter.scan_for(5000)` then `adapter.scan_get_results()` | | Live scan callback | `BleakScanner(detection_callback)` | `adapter.set_callback_on_scan_found(cb)` + `adapter.scan_start()` / `scan_stop()` | | Connect | `async with BleakClient(device) as client:` | `peripheral.connect()` … `peripheral.disconnect()` | | Connection state | `client.is_connected` | `peripheral.is_connected()` | | Disconnect hook | `BleakClient(device, disconnected_callback=cb)` | `peripheral.set_callback_on_disconnected(cb)` | | Enumerate GATT | `client.services` | `peripheral.services()` | | Read | `await client.read_gatt_char(char_uuid)` | `peripheral.read(service_uuid, char_uuid)` | | Write (with response) | `await client.write_gatt_char(char_uuid, data, response=True)` | `peripheral.write_request(service_uuid, char_uuid, data)` | | Write (no response) | `await client.write_gatt_char(char_uuid, data, response=False)` | `peripheral.write_command(service_uuid, char_uuid, data)` | | Subscribe | `await client.start_notify(char_uuid, cb)` | `peripheral.notify(service_uuid, char_uuid, cb)` | | Unsubscribe | `await client.stop_notify(char_uuid)` | `peripheral.unsubscribe(service_uuid, char_uuid)` | | MTU | `client.mtu_size` | `peripheral.mtu()` | | Pairing state | `await client.pair()` | `peripheral.is_paired()` / `peripheral.unpair()` (pairing is triggered by the OS when needed) | Two structural differences to note while translating: 1. **Characteristics are addressed by (service, characteristic) pairs.** Bleak lets you pass just a characteristic UUID; SimplePyBLE always wants the service UUID too. If your bleak code only stored characteristic UUIDs, recover the service UUID from `peripheral.services()` once after connecting. 2. **Peripherals come from scan results, not addresses.** Bleak can construct a `BleakClient("AA:BB:CC:…")` directly from an address. In SimplePyBLE you obtain `Peripheral` objects from `adapter.scan_get_results()` (or `adapter.get_paired_peripherals()` on Linux/Windows/Android) and filter by `address()` or `identifier()`. Plan a scan step in flows that used hardcoded addresses; note that address-based connection was never portable to macOS, where the OS exposes host-local identifiers instead of MAC addresses. Side-by-side: scan, connect, notify [#side-by-side-scan-connect-notify] **bleak:** ```python import asyncio from bleak import BleakClient, BleakScanner HR_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb" HR_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb" async def main(): device = await BleakScanner.find_device_by_name("Polar H10", timeout=10.0) def on_data(characteristic, data: bytearray): print(f"Received: {data.hex()}") async with BleakClient(device) as client: await client.start_notify(HR_MEASUREMENT, on_data) await asyncio.sleep(30) await client.stop_notify(HR_MEASUREMENT) asyncio.run(main()) ``` **SimplePyBLE:** ```python import time import simplepyble HR_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb" HR_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb" adapter = simplepyble.Adapter.get_adapters()[0] adapter.scan_for(10000) device = next(p for p in adapter.scan_get_results() if p.identifier() == "Polar H10") def on_data(data): print(f"Received: {data.hex()}") device.connect() try: device.notify(HR_SERVICE, HR_MEASUREMENT, on_data) time.sleep(30) device.unsubscribe(HR_SERVICE, HR_MEASUREMENT) finally: device.disconnect() ``` Note the callback signature: bleak passes `(characteristic, data)`, SimplePyBLE passes just the payload. Callback threading differences [#callback-threading-differences] Bleak delivers detection and notification callbacks on the asyncio event loop, so they can safely touch your async state. SimplePyBLE delivers callbacks on a native background thread owned by the OS Bluetooth stack: * Protect shared state with locks or `queue.Queue`. * Don't call blocking SimplePyBLE operations from inside a callback — on some platforms that deadlocks. Signal a worker thread instead. The full rules are in [Concurrency](../fundamentals/concurrency). * If you're bridging into an asyncio application, hop back onto the loop with `loop.call_soon_threadsafe(...)` from the callback. Keeping an async codebase [#keeping-an-async-codebase] If the rest of your application is asyncio and you just want SimplePyBLE underneath, wrap the blocking calls with `asyncio.to_thread` — since the GIL is released, this behaves well: ```python peripheral = await asyncio.to_thread(find_and_connect) # blocking scan + connect value = await asyncio.to_thread(peripheral.read, HR_SERVICE, SOME_CHAR) ``` But if you're starting fresh, consider whether you need asyncio at all: for most BLE tools — CLI utilities, test rigs, data loggers — a plain synchronous script with one worker thread is simpler to write and debug. What bleak has that SimpleBLE doesn't (and vice versa) [#what-bleak-has-that-simpleble-doesnt-and-vice-versa] * Bleak exposes advertisement data through a dedicated `AdvertisementData` object in scan callbacks; SimplePyBLE exposes the equivalents as peripheral methods (`manufacturer_data()`, `rssi()`, `tx_power()`, and advertised services via `services()` while unconnected). * SimpleBLE supports Android (via the same C++ core) and ships bindings for C, C++, Rust, and Java from the same codebase — useful if your Python prototype later needs to become an embedded or mobile product. * Licensing differs: bleak is MIT, while SimpleBLE is available under the Business Source License 1.1 — free for non-commercial use, with commercial licenses (including free ones for small projects) described in [Licensing](../licensing). # Migrating from noble (Node.js) import { Callout } from 'fumadocs-ui/components/callout'; [noble](https://github.com/abandonware/noble) was the de-facto BLE central library for Node.js for a decade. Its maintained fork was archived in July 2025, leaving Node BLE projects without an actively maintained stack. This guide maps noble's concepts onto SimpleBLE for teams evaluating a migration. SimpleBLE does not currently ship a Node.js binding. The practical migration paths are the C++ library ([SimpleBLE](../simpleble/usage)), the C bindings ([SimpleCBLE](../simplecble/usage)) — which can back a native Node addon via N-API if you want to stay in JavaScript — or a sidecar process in Python ([SimplePyBLE](../simplepyble/usage)). If a first-party Node binding matters to you, [let us know](mailto:contact@simpleble.org) — user demand is a key input to the roadmap. Concept mapping [#concept-mapping] noble is event-driven and callback-based; SimpleBLE is synchronous and blocking. Most noble state machines collapse into straight-line code. | noble | SimpleBLE (C++) | SimplePyBLE (Python) | | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------- | | `noble.on('stateChange', ...)` → wait for `poweredOn` | `Adapter::bluetooth_enabled()` / `adapter.is_powered()` | `simplepyble.Adapter.bluetooth_enabled()` | | `noble.startScanning([serviceUuids])` | `adapter.scan_start()` or `adapter.scan_for(ms)` (filter in your callback) | `adapter.scan_start()` / `adapter.scan_for(ms)` | | `noble.on('discover', peripheral => ...)` | `adapter.set_callback_on_scan_found(...)` | `adapter.set_callback_on_scan_found(...)` | | `peripheral.connect(cb)` | `peripheral.connect()` (blocking; throws on failure) | `peripheral.connect()` | | `peripheral.discoverServices(...)` / `discoverAllServicesAndCharacteristics` | automatic during `connect()`; enumerate with `peripheral.services()` | `peripheral.services()` | | `characteristic.read(cb)` | `peripheral.read(service, characteristic)` | `peripheral.read(service, characteristic)` | | `characteristic.write(data, withoutResponse, cb)` | `write_request(...)` (with response) / `write_command(...)` (without) | `write_request(...)` / `write_command(...)` | | `characteristic.subscribe()` + `characteristic.on('data', ...)` | `peripheral.notify(service, characteristic, callback)` | `peripheral.notify(...)` | | `characteristic.unsubscribe()` | `peripheral.unsubscribe(service, characteristic)` | `peripheral.unsubscribe(...)` | | `peripheral.once('disconnect', ...)` | `peripheral.set_callback_on_disconnected(...)` | `peripheral.set_callback_on_disconnected(...)` | | `peripheral.rssi` / `peripheral.advertisement` | `peripheral.rssi()`, `manufacturer_data()`, `services()` while unconnected | same names | Differences worth planning around: * **No `poweredOn` dance.** SimpleBLE initializes the adapter when you call `Adapter::get_adapters()`; there's no state-machine event to wait for. * **Service discovery is implicit.** `connect()` blocks until services are resolved, so there is no separate `discoverServices` step or its nested callbacks. * **Characteristics are addressed by UUID pair**, not by object handles held from a discovery callback: `read(service_uuid, characteristic_uuid)`. * **Callbacks run on internal threads**, not on a JavaScript event loop — read [Concurrency](../fundamentals/concurrency) before porting event-handler logic. * **Licensing:** noble is MIT; SimpleBLE is BUSL-1.1 — free for non-commercial use, commercial licenses described in [Licensing](../licensing). Skeleton: noble's canonical example, translated [#skeleton-nobles-canonical-example-translated] The noble README's heart-rate example — scan for `180d`, connect, subscribe — becomes, in Python: ```python import time import simplepyble adapter = simplepyble.Adapter.get_adapters()[0] adapter.scan_for(5000) peripheral = next( p for p in adapter.scan_get_results() if "0000180d-0000-1000-8000-00805f9b34fb" in [s.uuid() for s in p.services()] ) peripheral.connect() peripheral.notify( "0000180d-0000-1000-8000-00805f9b34fb", "00002a37-0000-1000-8000-00805f9b34fb", lambda data: print(f"Heart rate payload: {data.hex()}"), ) time.sleep(30) peripheral.disconnect() ``` The same flow in C++ is shown step-by-step in the [heart rate monitor recipe](../recipes/heart-rate-monitor), which also covers parsing the measurement payload. Staying in JavaScript [#staying-in-javascript] If a rewrite is off the table, two architectures keep your Node code intact: 1. **Native addon over SimpleCBLE.** The [C API](../simplecble/api) is a stable, flat surface designed for FFI; wrapping the handful of calls your app uses with N-API is a bounded task. 2. **Sidecar process.** Run BLE in a small SimplePyBLE (or C++) process and talk to it over stdio/IPC/HTTP from Node. This also isolates BLE stack crashes from your main process. # Advertise import { Callout } from 'fumadocs-ui/components/callout'; Advertising is how this host is found. A local peripheral serves GATT, sends advertisements, and waits for a central to connect. `SimpleBLE::Peripheral` is a remote device. `SimpleBLE::Local::Peripheral` is this host. Create the local peripheral from an `Adapter`, the same object used to scan. The C++ API can host a local peripheral on every supported platform. Native Android applications must provide their application context with `SimpleBLE::Advanced::Android::set_context()` before creating the local peripheral. SimpleDroidBLE exposes the same flow directly in Kotlin. Flow [#flow] 1. Get an adapter. 2. Create a local peripheral. 3. Select the advertised service UUIDs, if needed. 4. Add services and characteristics. 5. Register client or write callbacks if you need them. 6. Call `start()`, keep the process alive, then `stop()`. Advertise a readable characteristic [#advertise-a-readable-characteristic] ```cpp auto adapters = SimpleBLE::Adapter::get_adapters(); auto adapter = adapters.front(); auto peripheral = adapter.create_local_peripheral(); peripheral.add_advertised_service("12345678-1234-5678-1234-56789abcdef0"); auto service = peripheral.add_service("12345678-1234-5678-1234-56789abcdef0"); auto characteristic = service.add_characteristic( "12345678-1234-5678-1234-56789abcdef1", {SimpleBLE::Local::CharacteristicCapability::READ, SimpleBLE::Local::CharacteristicCapability::WRITE_REQUEST, SimpleBLE::Local::CharacteristicCapability::NOTIFY}); characteristic.set_value(SimpleBLE::ByteArray("ready")); characteristic.set_callback_on_write([&characteristic](SimpleBLE::ByteArray value) { // Echo the written value to subscribed clients. characteristic.set_value(std::move(value)); }); peripheral.set_callback_on_client_connected( [](SimpleBLE::BluetoothAddress address) { std::cout << "Client connected: " << address << std::endl; }); peripheral.start(); ``` Configure services, characteristics, and advertising before `start()`. While started, update data with `set_value()` rather than rebuilding the GATT table. `set_value()` also publishes the latest value to subscribed clients when the characteristic has `NOTIFY` or `INDICATE` capability. SimpleBLE permits the same adapter to scan while the local peripheral is started when the platform Bluetooth stack and controller support concurrent scanning and advertising. On Linux, BlueZ and the controller must support both operations at the same time. Practical notes [#practical-notes] * Keep the process alive after `start()`. Advertising stops when the object is destroyed or `stop()` is called. * Pass capabilities as a `std::set` (an initializer list is enough). Each characteristic needs at least one capability. * On Linux, BlueZ must be running and the process must have D-Bus access, as with scanning. * On macOS, CoreBluetooth reports client connection and disconnection callbacks when the client first subscribes and finally unsubscribes. * On Windows, the adapter and driver must support the peripheral role. Applications distributed as AppX/MSIX packages should declare `` in `Package.appxmanifest`; ordinary unpackaged executables do not need this manifest declaration. Windows controls the advertised Bluetooth name. * On Android, SimpleBLE includes the adapter's system Bluetooth name. Android's public advertising API cannot set an arbitrary per-advertisement name. * Linux, macOS, and iOS callers that need a platform-specific advertised name can use `set_advertisement_local_name()` from the corresponding `SimpleBLE::Advanced` platform namespace before `start()`. For the full C++ walkthrough, see [Advertise, Serve, Publish](../simpleble/peripheral/advertise-serve-publish). For scanning from the other side of the link, see [Scan](./scan). # Connect import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Connecting moves from BLE advertisements into the GATT database. Once connected, SimpleBLE resolves services and characteristics so you can decide which characteristic to read, write, notify, or indicate. Flow [#flow] 1. Scan for nearby devices. 2. Keep only peripherals that are connectable. 3. Let the user or application logic pick one. 4. Connect. 5. Enumerate services, characteristics, capabilities, and descriptors. 6. Disconnect when finished. Connect and list GATT objects [#connect-and-list-gatt-objects] ```cpp SimpleBLE::Peripheral peripheral = peripherals[selected_index]; peripheral.connect(); std::cout << "MTU: " << peripheral.mtu() << std::endl; for (auto& service : peripheral.services()) { std::cout << "Service: " << service.uuid() << std::endl; for (auto& characteristic : service.characteristics()) { std::cout << " Characteristic: " << characteristic.uuid() << std::endl; std::cout << " read=" << characteristic.can_read() << " notify=" << characteristic.can_notify() << " write_request=" << characteristic.can_write_request() << " write_command=" << characteristic.can_write_command() << std::endl; } } peripheral.disconnect(); ``` ```c simpleble_peripheral_t peripheral = peripheral_list[selected_index]; if (simpleble_peripheral_connect(peripheral) != SIMPLEBLE_SUCCESS) { printf("Failed to connect.\n"); return 1; } size_t service_count = simpleble_peripheral_services_count(peripheral); for (size_t i = 0; i < service_count; i++) { simpleble_service_t service; simpleble_peripheral_services_get(peripheral, i, &service); printf("Service: %s\n", service.uuid.value); for (size_t j = 0; j < service.characteristic_count; j++) { printf(" Characteristic: %s\n", service.characteristics[j].uuid.value); } } simpleble_peripheral_disconnect(peripheral); ``` ```python peripheral = peripherals[selected_index] peripheral.connect() print(f"MTU: {peripheral.mtu()}") for service in peripheral.services(): print(f"Service: {service.uuid()}") for characteristic in service.characteristics(): print(f" Characteristic: {characteristic.uuid()}") print(f" Capabilities: {' '.join(characteristic.capabilities())}") peripheral.disconnect() ``` ```java Peripheral peripheral = peripherals.get(selectedIndex); peripheral.connect(); System.out.println("MTU: " + peripheral.getMtu()); for (Service service : peripheral.services()) { System.out.println("Service: " + service.uuid()); for (Characteristic characteristic : service.characteristics()) { System.out.println(" Characteristic: " + characteristic.uuid()); System.out.println(" read=" + characteristic.canRead() + " notify=" + characteristic.canNotify() + " writeRequest=" + characteristic.canWriteRequest() + " writeCommand=" + characteristic.canWriteCommand()); } } peripheral.disconnect(); ``` ```rust let peripheral = adapter.scan_get_results().unwrap().remove(selected_index); peripheral.connect().unwrap(); println!("MTU: {}", peripheral.mtu().unwrap()); for service in peripheral.services().unwrap() { println!("Service: {}", service.uuid()); for characteristic in service.characteristics() { println!(" Characteristic: {}", characteristic.uuid()); println!(" Capabilities: {:?}", characteristic.capabilities()); } } peripheral.disconnect().unwrap(); ``` ```kotlin val peripheral = selectedPeripheral peripheral.connect() Log.d("SimpleBLE", "MTU: ${peripheral.mtu}") val services = peripheral.services() for (service in services) { Log.d("SimpleBLE", "Service: ${service.uuid}") for (characteristic in service.characteristics) { Log.d("SimpleBLE", " Characteristic: ${characteristic.uuid}") Log.d("SimpleBLE", " notify=${characteristic.canNotify} read=${characteristic.canRead}") } } peripheral.disconnect() ``` Selection tips [#selection-tips] * Prefer filtering by advertised service UUID when your peripheral advertises it. * If several peripherals use the same name, show the address/identifier and RSSI to the user. * Treat macOS/iOS identifiers as host-local UUIDs, not stable global MAC addresses. * Always disconnect when your workflow is done, especially in command-line tools and tests. After you can connect and list characteristics, continue to [Read, Write, Notify](./read-write-notify). # Heart Rate Monitor import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Callout } from 'fumadocs-ui/components/callout'; The Heart Rate Profile is the "hello world" of real BLE integrations: it's a standardized service implemented identically by chest straps, watches, and fitness equipment, and it exercises the full SimpleBLE workflow — scan, filter by advertised service, connect, subscribe, parse binary payloads, and handle disconnects. The two UUIDs involved are assigned by the Bluetooth SIG: | Role | 16-bit ID | Full UUID | | ------------------------------------- | --------- | -------------------------------------- | | Heart Rate service | `0x180D` | `0000180d-0000-1000-8000-00805f9b34fb` | | Heart Rate Measurement characteristic | `0x2A37` | `00002a37-0000-1000-8000-00805f9b34fb` | The measurement format [#the-measurement-format] The Heart Rate Measurement characteristic is notify-only (you cannot read it) and packs its payload as: one **flags** byte, then the heart rate value, then optional fields: | Flags bit | Meaning | | --------- | ----------------------------------------------------------------------------- | | Bit 0 | `0` → heart rate is a `uint8`; `1` → heart rate is a `uint16` (little-endian) | | Bits 1–2 | Sensor contact status (bit 2 = feature supported, bit 1 = contact detected) | | Bit 3 | Energy Expended field present (`uint16`, kJ) | | Bit 4 | RR-Interval fields present (one or more `uint16`, units of 1/1024 s) | Implementation [#implementation] ```cpp #include #include #include #include #include #include #include const std::string HR_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb"; const std::string HR_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb"; struct HeartRateMeasurement { uint16_t bpm = 0; bool contact_supported = false; bool contact_detected = false; }; HeartRateMeasurement parse_heart_rate(const SimpleBLE::ByteArray& payload) { HeartRateMeasurement hr; if (payload.size() < 2) return hr; uint8_t flags = static_cast(payload[0]); if (flags & 0x01) { // 16-bit heart rate value, little-endian. hr.bpm = static_cast(payload[1]) | (static_cast(payload[2]) << 8); } else { // 8-bit heart rate value. hr.bpm = static_cast(payload[1]); } hr.contact_supported = (flags & 0x04) != 0; hr.contact_detected = (flags & 0x02) != 0; return hr; } int main() { auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { std::cerr << "No Bluetooth adapters found." << std::endl; return 1; } auto adapter = adapters.front(); // 1. Scan and keep the first device advertising the Heart Rate service. SimpleBLE::Peripheral monitor; adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { for (auto& service : peripheral.services()) { if (service.uuid() == HR_SERVICE && !monitor.initialized()) { monitor = peripheral; std::cout << "Found monitor: " << peripheral.identifier() << std::endl; } } }); adapter.scan_for(5000); if (!monitor.initialized()) { std::cerr << "No heart rate monitor found." << std::endl; return 1; } // 2. Watch for disconnects. Only signal from the callback; act elsewhere. std::mutex mutex; std::condition_variable cv; bool disconnected = false; monitor.set_callback_on_disconnected([&]() { std::lock_guard lock(mutex); disconnected = true; cv.notify_all(); }); // 3. Connect and subscribe. monitor.connect(); monitor.notify(HR_SERVICE, HR_MEASUREMENT, [](SimpleBLE::ByteArray payload) { HeartRateMeasurement hr = parse_heart_rate(payload); std::cout << "Heart rate: " << hr.bpm << " bpm"; if (hr.contact_supported && !hr.contact_detected) { std::cout << " (no skin contact)"; } std::cout << std::endl; }); // 4. Stream for 30 seconds or until the device disconnects. { std::unique_lock lock(mutex); cv.wait_for(lock, std::chrono::seconds(30), [&]() { return disconnected; }); } if (disconnected) { std::cout << "Device disconnected." << std::endl; } else { monitor.unsubscribe(HR_SERVICE, HR_MEASUREMENT); monitor.disconnect(); } return 0; } ``` ```python import threading import simplepyble HR_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb" HR_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb" def parse_heart_rate(payload: bytes) -> dict: flags = payload[0] if flags & 0x01: # 16-bit heart rate value, little-endian. bpm = int.from_bytes(payload[1:3], "little") offset = 3 else: # 8-bit heart rate value. bpm = payload[1] offset = 2 result = { "bpm": bpm, "contact_supported": bool(flags & 0x04), "contact_detected": bool(flags & 0x02), } if flags & 0x08: # Energy Expended present. result["energy_kj"] = int.from_bytes(payload[offset : offset + 2], "little") offset += 2 if flags & 0x10: # RR intervals present (1/1024 s units). rr = [] while offset + 1 < len(payload): rr.append(int.from_bytes(payload[offset : offset + 2], "little") / 1024.0) offset += 2 result["rr_intervals_s"] = rr return result def main(): adapter = simplepyble.Adapter.get_adapters()[0] # 1. Scan for a device advertising the Heart Rate service. adapter.scan_for(5000) monitor = None for peripheral in adapter.scan_get_results(): service_uuids = [service.uuid() for service in peripheral.services()] if HR_SERVICE in service_uuids: monitor = peripheral print(f"Found monitor: {peripheral.identifier()}") break if monitor is None: print("No heart rate monitor found.") return # 2. Watch for disconnects. Only signal from the callback; act elsewhere. disconnected = threading.Event() monitor.set_callback_on_disconnected(disconnected.set) # 3. Connect and subscribe. monitor.connect() def on_measurement(payload): hr = parse_heart_rate(bytes(payload)) contact = "" if hr.get("contact_detected", True) else " (no skin contact)" print(f"Heart rate: {hr['bpm']} bpm{contact}") monitor.notify(HR_SERVICE, HR_MEASUREMENT, on_measurement) # 4. Stream for 30 seconds or until the device disconnects. if disconnected.wait(timeout=30): print("Device disconnected.") else: monitor.unsubscribe(HR_SERVICE, HR_MEASUREMENT) monitor.disconnect() if __name__ == "__main__": main() ``` Walkthrough [#walkthrough] 1. **Scan filtering.** While not connected, `peripheral.services()` returns the service UUIDs found in the advertisement, which is how we detect `0x180D` without connecting. Many monitors only start advertising when worn or when a session is active. 2. **Disconnect handling.** The disconnect callback fires on SimpleBLE's internal event thread, so it only sets a flag/event that the main thread waits on. Chest straps disconnect aggressively when taken off — treat it as a normal exit path, not an error. 3. **Subscription.** `0x2A37` supports notifications only; there is nothing to `read()`. After `notify()` returns, measurements arrive roughly once per second on the callback thread. 4. **Parsing.** Always branch on bit 0 of the flags byte: most devices send `uint8` heart rates, but a device may switch to the `uint16` format (for example at high rates), so hardcoding one format eventually breaks. Practical notes [#practical-notes] * On macOS/iOS, the peripheral you get back is identified by a host-local UUID, not a MAC address — filter by advertised service (as above) or name, not by address. * If you need this to survive drops for a long-running session, combine it with the [reconnect loop](./reconnect-loop) pattern and re-subscribe after each reconnect. * The sensor-contact bits are worth surfacing in real products: a valid link with no skin contact produces readings that look plausible but are not meaningful. # Read, Write, Notify import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; GATT operations happen after a peripheral is connected and services have been discovered. Most applications use the same shape: 1. Select a service UUID and characteristic UUID. 2. Check the characteristic capability. 3. Read, write, subscribe, or unsubscribe. 4. Keep the process, activity, or coroutine alive long enough for notifications or indications to arrive. The snippets below assume you already have a connected `peripheral`, a `service_uuid`, and a `characteristic_uuid`. Read [#read] ```cpp auto value = peripheral.read(service_uuid, characteristic_uuid); std::cout << "Read: " << value << std::endl; ``` ```c uint8_t* data = NULL; size_t data_length = 0; simpleble_peripheral_read(peripheral, service_uuid, characteristic_uuid, &data, &data_length); for (size_t i = 0; i < data_length; i++) { printf("%02X ", data[i]); } printf("\n"); simpleble_free(data); ``` ```python value = peripheral.read(service_uuid, characteristic_uuid) print(f"Read: {value}") ``` ```java BluetoothUUID service = new BluetoothUUID(serviceUuid); BluetoothUUID characteristic = new BluetoothUUID(characteristicUuid); byte[] value = peripheral.read(service, characteristic); System.out.println("Read: " + bytesToHex(value)); ``` ```rust let value = peripheral.read(&service_uuid, &characteristic_uuid).unwrap(); println!("Read: {:?}", value); ``` ```kotlin val value = peripheral.read(serviceUuid, characteristicUuid) Log.d("SimpleBLE", "Read: ${value.joinToString(" ") { "%02x".format(it) }}") ``` Write [#write] ```cpp SimpleBLE::ByteArray payload = SimpleBLE::ByteArray::fromHex("010203"); // Acknowledged write. peripheral.write_request(service_uuid, characteristic_uuid, payload); // Unacknowledged write. peripheral.write_command(service_uuid, characteristic_uuid, payload); ``` ```c uint8_t payload[] = {0x01, 0x02, 0x03}; simpleble_peripheral_write_request( peripheral, service_uuid, characteristic_uuid, payload, sizeof(payload)); simpleble_peripheral_write_command( peripheral, service_uuid, characteristic_uuid, payload, sizeof(payload)); ``` ```python payload = bytes([0x01, 0x02, 0x03]) # Acknowledged write. peripheral.write_request(service_uuid, characteristic_uuid, payload) # Unacknowledged write. peripheral.write_command(service_uuid, characteristic_uuid, payload) ``` ```java byte[] payload = new byte[] {0x01, 0x02, 0x03}; peripheral.writeRequest(service, characteristic, payload); peripheral.writeCommand(service, characteristic, payload); ``` ```rust let payload = vec![0x01, 0x02, 0x03]; peripheral.write_request(&service_uuid, &characteristic_uuid, &payload).unwrap(); peripheral.write_command(&service_uuid, &characteristic_uuid, &payload).unwrap(); ``` ```kotlin val payload = byteArrayOf(0x01, 0x02, 0x03) peripheral.writeRequest(serviceUuid, characteristicUuid, payload) peripheral.writeCommand(serviceUuid, characteristicUuid, payload) ``` Notify [#notify] ```cpp peripheral.notify(service_uuid, characteristic_uuid, [](SimpleBLE::ByteArray payload) { std::cout << "Notification: " << payload << std::endl; }); std::this_thread::sleep_for(std::chrono::seconds(10)); peripheral.unsubscribe(service_uuid, characteristic_uuid); ``` ```c static void on_notify(simpleble_peripheral_t peripheral, simpleble_uuid_t service, simpleble_uuid_t characteristic, const uint8_t* data, size_t data_length, void* userdata) { for (size_t i = 0; i < data_length; i++) { printf("%02X ", data[i]); } printf("\n"); } simpleble_peripheral_notify(peripheral, service_uuid, characteristic_uuid, on_notify, NULL); /* Keep the process alive while notifications arrive. */ simpleble_peripheral_unsubscribe(peripheral, service_uuid, characteristic_uuid); ``` ```python import time peripheral.notify( service_uuid, characteristic_uuid, lambda data: print(f"Notification: {data}"), ) time.sleep(10) peripheral.unsubscribe(service_uuid, characteristic_uuid) ``` ```java peripheral.notify(service, characteristic, data -> { System.out.println("Notification: " + bytesToHex(data)); }); Thread.sleep(10_000); peripheral.unsubscribe(service, characteristic); ``` ```rust let mut stream = peripheral.notify(&service_uuid, &characteristic_uuid).unwrap(); tokio::spawn(async move { while let Some(Ok(event)) = stream.next().await { if let simplersble::ValueChangedEvent::ValueUpdated(data) = event { println!("Notification: {:?}", data); } } }); std::thread::sleep(std::time::Duration::from_secs(10)); peripheral.unsubscribe(&service_uuid, &characteristic_uuid).unwrap(); ``` ```kotlin withTimeoutOrNull(10_000) { peripheral.notify(serviceUuid, characteristicUuid).collect { payload -> Log.d("SimpleBLE", "Notification: ${payload.joinToString(" ") { "%02x".format(it) }}") } } ``` On Android, collecting the flow subscribes to the characteristic. When collection is cancelled or completes, SimpleDroidBLE unsubscribes and waits for native cleanup to finish. The timeout above is only a compact way to demonstrate a ten-second subscription. Production Android apps [#production-android-apps] Choose the subscription lifetime based on what the app promises: * For data needed only while a screen is visible, collect inside [`repeatOnLifecycle`](https://developer.android.com/topic/libraries/architecture/views/coroutines-views). Leaving the screen cancels collection and unsubscribes; returning starts a new subscription. * For a connection that must survive screen changes, let a ViewModel or connection controller own the collection job and expose parsed values as UI state. Cancel and join that job before disconnecting, selecting another peripheral, or subscribing again. * A notification flow does not reconnect automatically. After reconnecting, refresh services and collect a new flow. * Serialize connect, read, write, subscribe, and disconnect actions so taps and lifecycle events cannot start conflicting GATT operations. * Surface subscription failures through UI state. Cancellation is normal lifecycle cleanup and should not be shown as an error. A screen-scoped subscription can stay small: ```kotlin lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { peripheral.notify(serviceUuid, characteristicUuid).collect { payload -> viewModel.onNotification(payload) } } } ``` If notifications must continue while the app is not visible, a ViewModel is not sufficient. Follow Android's [background BLE guidance](https://developer.android.com/develop/connectivity/bluetooth/ble/background) and use a companion-device service or a `connectedDevice` foreground service as appropriate. Common pitfalls [#common-pitfalls] * Use `write_request` / `writeRequest` when you need the peripheral to acknowledge the write. * Use `write_command` / `writeCommand` only when the characteristic supports write-without-response. * Subscribe only to characteristics that advertise notify or indicate capability. * Keep the process, activity, or coroutine alive while notifications are active. * Unsubscribe before disconnecting when your workflow has a clear end. On Android, cancel and join the collector to unsubscribe automatically. * On Android, only one notify or indicate collector may be active for a characteristic. Cancel and join it before resubscribing. # Reconnect Loop import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Callout } from 'fumadocs-ui/components/callout'; BLE links drop. Devices go out of range, sleep to save power, or reset mid-session. A production application treats disconnection as a normal event, not an error: it detects the drop, reconnects with backoff, and re-establishes its notification subscriptions. Flow [#flow] 1. Register `set_callback_on_disconnected` **before** connecting. 2. From the callback, only *signal* a reconnect worker — never reconnect inside the callback itself. 3. In the worker, retry `connect()` with exponential backoff (plus a cap). 4. After every successful connect, re-subscribe all notifications. Subscriptions do not survive a disconnect. 5. Stop cleanly: a user-initiated `disconnect()` should not trigger the reconnect machinery. The disconnect callback runs on SimpleBLE's internal event thread. Calling `connect()` there stalls or deadlocks on most platforms — see [Concurrency](../fundamentals/concurrency#calling-simpleble-from-inside-a-callback). The pattern below signals a dedicated worker thread instead. Implementation [#implementation] ```cpp #include #include #include #include #include #include #include #include class ReconnectingPeripheral { public: ReconnectingPeripheral(SimpleBLE::Peripheral peripheral) : peripheral_(peripheral) { peripheral_.set_callback_on_disconnected([this]() { // Runs on SimpleBLE's event thread: only signal, never reconnect here. std::lock_guard lock(mutex_); link_up_ = false; cv_.notify_all(); }); } void subscribe(const SimpleBLE::BluetoothUUID& service, const SimpleBLE::BluetoothUUID& characteristic, std::function callback) { // Remember the subscription so it can be re-established after a reconnect. std::lock_guard lock(mutex_); subscriptions_.push_back({service, characteristic, std::move(callback)}); } void run() { running_ = true; worker_ = std::thread([this]() { maintain_link(); }); } void stop() { { std::lock_guard lock(mutex_); running_ = false; cv_.notify_all(); } worker_.join(); if (peripheral_.is_connected()) { peripheral_.disconnect(); } } private: struct Subscription { SimpleBLE::BluetoothUUID service; SimpleBLE::BluetoothUUID characteristic; std::function callback; }; void maintain_link() { auto backoff = std::chrono::seconds(1); const auto max_backoff = std::chrono::seconds(30); while (true) { { std::unique_lock lock(mutex_); // Sleep until the link drops or we're told to stop. cv_.wait(lock, [this]() { return !running_ || !link_up_; }); if (!running_) return; } try { peripheral_.connect(); std::lock_guard lock(mutex_); for (auto& sub : subscriptions_) { peripheral_.notify(sub.service, sub.characteristic, sub.callback); } link_up_ = true; backoff = std::chrono::seconds(1); // Reset after success. std::cout << "Link established." << std::endl; } catch (const std::exception& e) { std::cerr << "Connect failed: " << e.what() << " — retrying in " << backoff.count() << "s" << std::endl; std::unique_lock lock(mutex_); cv_.wait_for(lock, backoff, [this]() { return !running_; }); if (!running_) return; backoff = std::min(backoff * 2, max_backoff); } } } SimpleBLE::Peripheral peripheral_; std::vector subscriptions_; std::mutex mutex_; std::condition_variable cv_; bool link_up_ = false; bool running_ = false; std::thread worker_; }; int main() { auto adapter = SimpleBLE::Adapter::get_adapters().front(); adapter.scan_for(5000); for (auto& peripheral : adapter.scan_get_results()) { if (peripheral.identifier() != "MyDevice") continue; ReconnectingPeripheral link(peripheral); link.subscribe("0000180d-0000-1000-8000-00805f9b34fb", "00002a37-0000-1000-8000-00805f9b34fb", [](SimpleBLE::ByteArray payload) { std::cout << "Data: " << payload << std::endl; }); link.run(); std::this_thread::sleep_for(std::chrono::minutes(5)); link.stop(); break; } return 0; } ``` ```python import threading import time import simplepyble class ReconnectingPeripheral: def __init__(self, peripheral): self.peripheral = peripheral self.subscriptions = [] self.link_down = threading.Event() self.stop_requested = threading.Event() self.worker = None # Runs on SimpleBLE's event thread: only signal, never reconnect here. peripheral.set_callback_on_disconnected(self.link_down.set) def subscribe(self, service_uuid, characteristic_uuid, callback): """Remember the subscription so it can be re-established after a reconnect.""" self.subscriptions.append((service_uuid, characteristic_uuid, callback)) def run(self): self.link_down.set() # Not connected yet: trigger the first connect. self.worker = threading.Thread(target=self._maintain_link, daemon=True) self.worker.start() def stop(self): self.stop_requested.set() self.link_down.set() # Wake the worker so it can exit. self.worker.join() if self.peripheral.is_connected(): self.peripheral.disconnect() def _maintain_link(self): backoff = 1.0 max_backoff = 30.0 while not self.stop_requested.is_set(): self.link_down.wait() if self.stop_requested.is_set(): return try: self.peripheral.connect() for service_uuid, characteristic_uuid, callback in self.subscriptions: self.peripheral.notify(service_uuid, characteristic_uuid, callback) self.link_down.clear() backoff = 1.0 # Reset after success. print("Link established.") except RuntimeError as e: print(f"Connect failed: {e} — retrying in {backoff:.0f}s") if self.stop_requested.wait(timeout=backoff): return backoff = min(backoff * 2, max_backoff) if __name__ == "__main__": adapter = simplepyble.Adapter.get_adapters()[0] adapter.scan_for(5000) peripheral = next(p for p in adapter.scan_get_results() if p.identifier() == "MyDevice") link = ReconnectingPeripheral(peripheral) link.subscribe( "0000180d-0000-1000-8000-00805f9b34fb", "00002a37-0000-1000-8000-00805f9b34fb", lambda data: print(f"Data: {data}"), ) link.run() time.sleep(300) link.stop() ``` Practical notes [#practical-notes] * **Register the disconnect callback before `connect()`**, or a drop during setup can go unnoticed. * **Re-subscribe after every reconnect.** The GATT session — including notification state — is gone after a disconnect on every platform. * **Reset the backoff on success** so a healthy device that drops once doesn't inherit a 30-second penalty. * **`connect()` itself retries and times out internally** (per-platform values in the [timeout reference](../fundamentals/concurrency#timeout-reference)), so each loop iteration can already take several seconds; keep your initial backoff short. * **If reconnects keep failing for a long time, rescan.** Some stacks eventually drop cached knowledge of the device; discovering it again with a fresh scan restores a usable `Peripheral`. * A user-initiated shutdown also fires the disconnect callback on some platforms — the `stop_requested` / `running_` flag is what prevents the worker from reconnecting after your own `disconnect()`. # Scan import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; Scanning is the first real interaction with the Bluetooth stack. A scan listens for advertisements, reports devices as they appear, and stores the latest result set so your app can choose what to connect to. Flow [#flow] 1. Check that Bluetooth is enabled and permissions are available. 2. Get the available adapters. 3. Pick an adapter. 4. Register scan callbacks or event streams when your binding supports them. 5. Scan for a bounded amount of time. 6. Read the scan results and filter for connectable peripherals. Core scan call [#core-scan-call] ```cpp auto adapters = SimpleBLE::Adapter::get_adapters(); auto adapter = adapters.front(); adapter.set_callback_on_scan_found([](SimpleBLE::Peripheral peripheral) { std::cout << "Found " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; }); adapter.scan_for(5000); for (auto& peripheral : adapter.scan_get_results()) { if (peripheral.is_connectable()) { std::cout << peripheral.identifier() << " is connectable." << std::endl; } } ``` ```c simpleble_adapter_t adapter = simpleble_adapter_get_handle(0); simpleble_adapter_scan_for(adapter, 5000); size_t count = simpleble_adapter_scan_get_results_count(adapter); for (size_t i = 0; i < count; i++) { simpleble_peripheral_t peripheral = simpleble_adapter_scan_get_results_handle(adapter, i); char* identifier = simpleble_peripheral_identifier(peripheral); char* address = simpleble_peripheral_address(peripheral); printf("%s [%s]\n", identifier, address); simpleble_free(identifier); simpleble_free(address); simpleble_peripheral_release_handle(peripheral); } simpleble_adapter_release_handle(adapter); ``` ```python adapter = simplepyble.Adapter.get_adapters()[0] adapter.set_callback_on_scan_found( lambda peripheral: print(f"Found {peripheral.identifier()} [{peripheral.address()}]") ) adapter.scan_for(5000) for peripheral in adapter.scan_get_results(): if peripheral.is_connectable(): print(f"{peripheral.identifier()} is connectable") ``` ```java Adapter adapter = Adapter.getAdapters().get(0); adapter.setEventListener(new Adapter.EventListener() { @Override public void onScanFound(Peripheral peripheral) { System.out.println("Found " + peripheral.getIdentifier() + " [" + peripheral.getAddress() + "]"); } }); adapter.scanFor(5000); for (Peripheral peripheral : adapter.scanGetResults()) { if (peripheral.isConnectable()) { System.out.println(peripheral.getIdentifier() + " is connectable"); } } ``` ```rust let mut adapters = simplersble::Adapter::get_adapters().unwrap(); let adapter = adapters.remove(0); adapter.scan_for(5000).unwrap(); for peripheral in adapter.scan_get_results().unwrap() { println!( "{} [{}]", peripheral.identifier().unwrap(), peripheral.address().unwrap() ); } ``` ```kotlin val adapter = Adapter.getAdapters().first() coroutineScope { val events = launch { adapter.onScanFound.collect { peripheral -> Log.d("SimpleBLE", "Found ${peripheral.identifier} [${peripheral.address}]") } } try { adapter.scanFor(5000) } finally { events.cancelAndJoin() } val results = adapter.scanGetResults() } ``` Practical notes [#practical-notes] * Scans should be bounded. Use `scan_for` / `scanFor` when you want a simple blocking scan with predictable duration. * Event callbacks are useful for live UI updates, but the final result list is usually the easiest thing to feed into a selection flow. * macOS and iOS expose per-host UUIDs instead of hardware MAC addresses. * On Linux, make sure the process has access to BlueZ over DBus. * On Android, request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` before scanning. For a complete first scan program, see [Quickstart](../quickstart). To advertise from this host instead, see [Advertise](./advertise). # HTTP Server SimpleAIBLE includes a built-in HTTP server that allows you to control BLE devices using a REST API. This is particularly useful for controlling BLE devices remotely or for providing an interface for AI agents. Installation [#installation] Install the `simpleaible` package: ```bash pip install simpleaible ``` Running the Server [#running-the-server] Once the dependencies are installed, you can run the server using the following command: ```bash python3 -m simpleaible.http --host 127.0.0.1 --port 8000 ``` By default, the server runs on `http://127.0.0.1:8000`. API Reference [#api-reference] General Endpoints [#general-endpoints] GET / [#get-] Check if the API is running and healthy. * **Response**: ```json {"message": "SimpleAIBLE API is running"} ``` GET /adapters [#get-adapters] List all available Bluetooth adapters on the system. * **Response**: List of objects containing `identifier` and `address`. ```json [ { "identifier": "hci0", "address": "AA:BB:CC:DD:EE:FF" } ] ``` POST /scan [#post-scan] Scan for nearby BLE devices. * **Parameters**: `timeout_ms` (query, default: `5000`) - Duration of the scan in milliseconds. * **Response**: List of found devices. ```json [ { "identifier": "Nordic_HRM", "address": "AA:BB:CC:DD:EE:FF", "rssi": -55, "connectable": true, "manufacturer_data": { "76": "0215..." } } ] ``` Device Connection [#device-connection] POST /connect/{address} [#post-connectaddress] Establish a connection to a specific device. * **Path Parameters**: `address` (Device MAC address or UUID). * **Response**: ```json {"message": "Connected to Nordic_HRM", "address": "AA:BB:CC:DD:EE:FF"} ``` POST /disconnect/{address} [#post-disconnectaddress] Disconnect from a connected device. * **Path Parameters**: `address` * **Response**: ```json {"message": "Disconnected from AA:BB:CC:DD:EE:FF"} ``` GET /device/{address} [#get-deviceaddress] Retrieve detailed information about a connected device, including its services and characteristics. * **Path Parameters**: `address` * **Response**: ```json { "identifier": "Nordic_HRM", "address": "AA:BB:CC:DD:EE:FF", "connected": true, "mtu": 23, "services": [ { "uuid": "0000180d-0000-1000-8000-00805f9b34fb", "characteristics": ["00002a37-0000-1000-8000-00805f9b34fb"] } ] } ``` Characteristic Interaction [#characteristic-interaction] POST /device/{address}/read/{service_uuid}/{char_uuid} [#post-deviceaddressreadservice_uuidchar_uuid] Read the current value of a characteristic. * **Path Parameters**: `address`, `service_uuid`, `char_uuid` * **Response**: ```json {"data_hex": "00aabb", "data_utf8": "..."} ``` POST /device/{address}/write/{service_uuid}/{char_uuid} [#post-deviceaddresswriteservice_uuidchar_uuid] Write a value to a characteristic (Write with Response). * **Path Parameters**: `address`, `service_uuid`, `char_uuid` * **Body**: ```json {"data": "001122"} ``` * **Response**: `{"message": "Write successful"}` POST /device/{address}/write_command/{service_uuid}/{char_uuid} [#post-deviceaddresswrite_commandservice_uuidchar_uuid] Write a value to a characteristic without expecting a response (Write Command). * **Path Parameters**: `address`, `service_uuid`, `char_uuid` * **Body**: ```json {"data": "001122"} ``` * **Response**: `{"message": "Write command successful"}` Notifications and Indications [#notifications-and-indications] POST /device/{address}/notify/{service_uuid}/{char_uuid} [#post-deviceaddressnotifyservice_uuidchar_uuid] Subscribe to notifications for a specific characteristic. * **Response**: `{"message": "Subscribed to notifications"}` POST /device/{address}/indicate/{service_uuid}/{char_uuid} [#post-deviceaddressindicateservice_uuidchar_uuid] Subscribe to indications for a specific characteristic. * **Response**: `{"message": "Subscribed to indications"}` POST /device/{address}/unsubscribe/{service_uuid}/{char_uuid} [#post-deviceaddressunsubscribeservice_uuidchar_uuid] Stop receiving notifications or indications for a characteristic. * **Response**: `{"message": "Unsubscribed from notifications"}` GET /device/{address}/notifications [#get-deviceaddressnotifications] Retrieve all received notifications and indications for a device. **Note**: This call clears the internal notification buffer for that device. * **Response**: List of notification objects. ```json [ { "service": "0000180d-0000-1000-8000-00805f9b34fb", "characteristic": "00002a37-0000-1000-8000-00805f9b34fb", "data_hex": "163c00", "data_utf8": "...", "type": "notification" } ] ``` Interactive Documentation [#interactive-documentation] One of the benefits of using FastAPI is the built-in interactive documentation. When the server is running, you can explore the API and test endpoints directly from your browser: * **Swagger UI**: `http://127.0.0.1:8000/docs` * **ReDoc**: `http://127.0.0.1:8000/redoc` # MCP Server import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; import { Callout } from 'fumadocs-ui/components/callout'; The SimpleAIBLE MCP server provides local BLE tools to MCP-capable clients. It is designed for development, debugging, and AI-assisted workflows that need direct access to BLE hardware on the host machine. This server runs locally using STDIO transport. The local setup uses [uv](https://docs.astral.sh/uv/) (recommended) or Python/pip. Installation [#installation] ```bash uv tool install simpleaible ``` ```bash pip install simpleaible ``` On Python 3.11+ (most modern macOS and Linux systems), `pip install` may fail with an *"externally-managed-environment"* error ([PEP 668](https://peps.python.org/pep-0668/)). Add the `--break-system-packages` flag: ```bash pip install --break-system-packages simpleaible # or python3 -m pip install --break-system-packages simpleaible ```
Install in Cursor Go to: `Settings` -> `Cursor Settings` -> `MCP` -> `Add new global MCP server` Pasting the following configuration into your Cursor `~/.cursor/mcp.json` file is the recommended approach. You may also install in a specific project by creating `.cursor/mcp.json` in your project folder. See [Cursor MCP docs](https://cursor.com/docs/context/mcp#installing-mcp-servers) for more info. Cursor Local Server Connection [#cursor-local-server-connection] [ Install MCP Server](https://cursor.com/en/install-mcp?name=simpleaible\&config=eyJjb21tYW5kIjoic2ltcGxlYWlibGUifQ==) ```json { "mcpServers": { "simpleaible": { "command": "simpleaible" } } } ```
Install in Claude Code Run this command. See [Claude Code MCP docs](https://code.claude.com/docs/en/mcp) for more info. Claude Code Local Server Connection [#claude-code-local-server-connection] ```sh claude mcp add simpleaible -- simpleaible ```
Install in Opencode Add this to your Opencode configuration file. See [Opencode MCP docs](https://opencode.ai/docs/mcp-servers) for more info. Opencode Local Server Connection [#opencode-local-server-connection] ```json { "mcp": { "simpleaible": { "type": "local", "command": ["simpleaible"], "enabled": true } } } ```
Tool catalog [#tool-catalog] All tool inputs/outputs are JSON-serializable. General Tools [#general-tools] bluetooth_enabled [#bluetooth_enabled] Check if Bluetooth is enabled on the host system. By default, assume Bluetooth is enabled. Use this tool only when troubleshooting failed operations. * **Response**: ```json {"enabled": true} ``` get_adapters [#get_adapters] List all available Bluetooth adapters on the system. * **Response**: List of objects containing `identifier` and `address`. ```json [ { "identifier": "hci0", "address": "AA:BB:CC:DD:EE:FF" } ] ``` scan_for [#scan_for] Scan for nearby BLE devices using the first available adapter. * **Parameters**: `timeout_ms` (default: `5000`) - Duration of the scan in milliseconds. * **Response**: List of found devices. ```json [ { "identifier": "Nordic_HRM", "address": "AA:BB:CC:DD:EE:FF", "rssi": -55, "connectable": true, "manufacturer_data": {"76": "0215..."} } ] ``` Device Connection [#device-connection] connect [#connect] Establish a connection to a specific device found in the last scan. * **Parameters**: `address` - Device address (UUID on macOS, MAC on Linux/Windows). * **Response**: ```json {"message": "Connected to Nordic_HRM", "address": "AA:BB:CC:DD:EE:FF"} ``` disconnect [#disconnect] Disconnect from a connected device. * **Parameters**: `address` * **Response**: ```json {"message": "Disconnected from AA:BB:CC:DD:EE:FF"} ``` services [#services] Retrieve services and characteristics for a connected device. * **Parameters**: `address` * **Response**: ```json { "identifier": "Nordic_HRM", "address": "AA:BB:CC:DD:EE:FF", "connected": true, "mtu": 23, "services": [ { "uuid": "0000180d-0000-1000-8000-00805f9b34fb", "characteristics": ["00002a37-0000-1000-8000-00805f9b34fb"] } ] } ``` Characteristic Interaction [#characteristic-interaction] read [#read] Read the current value of a characteristic. * **Parameters**: `address`, `service_uuid`, `char_uuid` * **Response**: ```json {"service_uuid": "0000180d-0000-1000-8000-00805f9b34fb", "char_uuid": "00002a37-0000-1000-8000-00805f9b34fb", "data_hex": "00aabb", "data_utf8": "..."} ``` * **Note**: `data_utf8` is a convenience field. If the data is not valid UTF-8, invalid bytes are silently skipped, so it may be incomplete or empty for binary data. Use `data_hex` for reliable binary representation. write_request [#write_request] Write a value to a characteristic (Write with Response). * **Parameters**: `address`, `service_uuid`, `char_uuid`, `data` (hex string, e.g. `"001122"`) * **Response**: `{"message": "Write successful"}` write_command [#write_command] Write a value to a characteristic without expecting a response (Write Command). * **Parameters**: `address`, `service_uuid`, `char_uuid`, `data` (hex string, e.g. `"001122"`) * **Response**: `{"message": "Write command successful"}` Notifications and Indications [#notifications-and-indications] notify [#notify] Subscribe to notifications for a specific characteristic. Data is buffered in the background. * **Parameters**: `address`, `service_uuid`, `char_uuid` * **Response**: `{"message": "Subscribed to notifications"}` indicate [#indicate] Subscribe to indications for a specific characteristic. Data is buffered in the background. * **Parameters**: `address`, `service_uuid`, `char_uuid` * **Response**: `{"message": "Subscribed to indications"}` unsubscribe [#unsubscribe] Stop receiving notifications or indications for a characteristic. * **Parameters**: `address`, `service_uuid`, `char_uuid` * **Response**: `{"message": "Unsubscribed"}` get_notifications [#get_notifications] Retrieve all received notifications and indications for a device. This call clears the internal notification buffer for that device. * **Parameters**: `address` * **Response**: List of notification objects. ```json [ { "service": "0000180d-0000-1000-8000-00805f9b34fb", "characteristic": "00002a37-0000-1000-8000-00805f9b34fb", "data_hex": "163c00", "data_utf8": "...", "type": "notification" } ] ``` * **Note**: `data_utf8` is a convenience field. If the data is not valid UTF-8, invalid bytes are silently skipped, so it may be incomplete or empty for binary data. Use `data_hex` for reliable binary representation. Recommended flow [#recommended-flow] 1. `scan_for` 2. `connect` 3. `services` 4. `read` or `write_request`/`write_command` for one-time operations, or `notify`/`indicate` + `get_notifications` + `unsubscribe` for streaming data 5. `disconnect` `get_adapters` is only needed when you want to list available Bluetooth adapters, not as a prerequisite for other operations. Platform notes [#platform-notes] * On macOS/iOS, device `address` is a randomized UUID (e.g., `5E2A...`) instead of a hardware MAC address. These UUIDs are temporary and may change between sessions or device restarts. On Linux/Windows, standard MAC addresses are used (e.g., `AA:BB:CC:DD:EE:FF`). * Advertisement fields (RSSI, manufacturer data, service UUIDs) can vary by OS and device. * Some characteristics require pairing or may be restricted by the OS. MCP client example (FastMCP via HTTP) [#mcp-client-example-fastmcp-via-http] If you are running the server manually using the HTTP transport (`simpleaible --transport http`), you can connect to it using a FastMCP client: ```python import asyncio from fastmcp import Client async def main(): # Connect to the server running on the default port 8000 async with Client("http://127.0.0.1:8000/mcp") as client: adapters = await client.call_tool("get_adapters", {}) # ... ``` Note: FastMCP returns structured results under `structured_content` rather than `data` for some tool outputs. Running the server manually [#running-the-server-manually] While MCP clients handle starting the server automatically, you can also run it manually for testing. **STDIO Transport (Default):** ```bash simpleaible ``` **HTTP Transport:** ```bash simpleaible --transport http --host 127.0.0.1 --port 8000 ``` # Agent Skills SimpleAIBLE provides a set of **Agent Skills** that teach your AI assistant (Cursor, Claude Code, Windsurf, etc.) how to effectively use the SimpleAIBLE MCP server and debug Bluetooth Low Energy connections. What are Agent Skills? [#what-are-agent-skills] Agent skills are reusable instruction sets that provide domain-specific knowledge to your AI. The `simpleaible` skill includes\*\*:\*\* * **Recommended Workflows**: Step-by-step guidance on scanning, connecting, and interacting with devices. * **Platform Knowledge**: Awareness of differences between macOS UUIDs and Linux MAC addresses. * **Debugging Patterns**: Pre-defined strategies for troubleshooting connection and discovery issues. Installation [#installation] Using the SimpleAIBLE CLI (Recommended) [#using-the-simpleaible-cli-recommended] If you have already [installed the `simpleaible` package](./mcp#installation), the bundled installer detects supported agents on your machine and installs the skill for each of them: ```bash simpleaible install ``` The command currently detects **Claude Code** (`~/.claude`) and **Codex** (`~/.codex`) and copies the skill into each agent's `skills/` directory. To target a specific agent, pass `--agent`: ```bash simpleaible install --agent claude simpleaible install --agent codex ``` To remove the skill again: ```bash simpleaible uninstall ``` Because the skill ships inside the `simpleaible` package, re-running `simpleaible install` after upgrading the package updates the installed skill to match. Using npx [#using-npx] For agents not covered by the installer, the `skills` CLI works across Cursor, Windsurf, and many others: ```bash npx skills add https://github.com/simpleble/simpleble --skill simpleaible ``` * **Target specific agents**: Use `-a` or `--agent` (e.g., `-a cursor -a claude-code`). * **Global installation**: Use `-g` or `--global` to make the skill available across all your projects. Usage in Agents [#usage-in-agents] Once installed, your agent will automatically "activate" the skill when it detects you are working on Bluetooth-related tasks. You can trigger it by asking questions like: * *"I'm debugging my peripheral, please ensure it is connectable and I can read some data."* * *"I'm not finding any device, please see if you can find any BLE device."* * *"Read the firmware version from the device at address AA:BB:CC:DD:EE:FF."* Supported Agents [#supported-agents] The skill follows the [Agent Skills specification](https://agentskills.io) and is compatible with: * **Cursor** * **Claude Code** * **Windsurf** * **Cline** * **Roo Code** * ... and many more. # Tutorial: Drive a BLE Device from an AI Agent import { Callout } from 'fumadocs-ui/components/callout'; This tutorial takes you from zero to an AI agent that can autonomously discover a nearby BLE device, connect to it, explore its GATT database, and stream live data — using the SimpleAIBLE MCP server and the bundled agent skill. A heart rate monitor is used as the example device, but any BLE peripheral you own works the same way. What you'll build [#what-youll-build] An agent session where you can type *"find my heart rate strap and show me my current heart rate"* and the agent handles the rest: scan, identify the device by its advertised service, connect, subscribe to measurements, interpret the bytes, and disconnect cleanly. Prerequisites [#prerequisites] * An MCP-capable agent client (Claude Code, Claude Desktop, Cursor, …). * Bluetooth hardware on the host machine, with the OS-level permissions described in [Permissions](../fundamentals/permissions). * A BLE device to talk to — a heart rate strap, a smart bulb, a dev kit, anything that advertises. Step 1 — Install and register the MCP server [#step-1--install-and-register-the-mcp-server] Install the server (full options, including per-client configuration snippets, are in [MCP Server](./mcp)): ```bash uv tool install simpleaible ``` Then register it with your client. For Claude Code: ```bash claude mcp add simpleaible -- simpleaible ``` For Claude Desktop or Cursor, add `{"command": "simpleaible"}` to the client's MCP configuration as shown on the [MCP Server](./mcp) page. Step 2 — Verify the toolset [#step-2--verify-the-toolset] Once connected, the agent has access to these tools: | Tool | Purpose | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | `bluetooth_enabled`, `get_adapters` | Check the radio and enumerate adapters | | `scan_for` | Scan for nearby peripherals (default 5 s) and return their advertisements | | `connect` / `disconnect` | Manage the connection by device address | | `services` | List the GATT services and characteristics of a connected device | | `read`, `write_request`, `write_command` | One-shot GATT operations | | `notify`, `indicate`, `get_notifications`, `unsubscribe` | Subscribe to streaming data; notifications are buffered and retrieved by polling `get_notifications` | A quick smoke test: ask the agent *"scan for BLE devices for five seconds and list what you find."* You should see it call `scan_for` and report identifiers, addresses, and RSSI values. Step 3 — The walkthrough [#step-3--the-walkthrough] Ask the agent for the end goal, not the individual steps — e.g. *"Connect to my heart rate monitor and stream my heart rate for 30 seconds."* A well-behaved session looks like: 1. **Scan:** the agent calls `scan_for` and picks the peripheral advertising the Heart Rate service (`0x180D`) from the results. 2. **Connect:** it calls `connect` with the device's `address` field from the scan results. (On macOS this is a host-local UUID, not a MAC address — the agent should use whatever the scan returned, never a hardcoded MAC.) 3. **Explore:** `services` reveals the GATT database, confirming the Heart Rate Measurement characteristic (`0x2A37`) supports notifications. 4. **Stream:** the agent calls `notify`, then polls `get_notifications` to drain buffered measurements, decoding the payload from the `data_hex` field (flags byte first — see the [heart rate recipe](../recipes/heart-rate-monitor#the-measurement-format) for the exact format). 5. **Clean up:** `unsubscribe`, then `disconnect`. If the agent skips steps (a common failure is connecting without a fresh scan, or forgetting to disconnect), that's what the skill in the next step fixes. Step 4 — Add the agent skill [#step-4--add-the-agent-skill] SimpleAIBLE ships an agent skill — a `SKILL.md` plus reference files — that encodes the correct workflow so the agent doesn't have to rediscover it every session: always scan before connecting, use `data_hex` for protocol work, poll `get_notifications` while subscribed, always disconnect when done. The skill is bundled with the `simpleaible` package. The installer detects Claude Code and Codex on your machine and installs the skill for each: ```bash simpleaible install ``` Run `simpleaible uninstall` to remove it again. For other agents, or for per-project installation, see [Agent Skills](./skills) for the `npx skills` alternative. With the skill installed, the agent automatically follows the scan → connect → services → interact → disconnect discipline and knows the platform quirks (macOS UUID addressing, UTF-8 vs hex payload fields) without being told. Where to go next [#where-to-go-next] * **Custom agents and non-MCP stacks:** the [HTTP bridge](./http) exposes the same capabilities as a REST API, which suits agent frameworks that prefer function-calling over MCP. * **Beyond one device:** the MCP tools operate on addresses, so multi-device sessions (e.g. "compare the RSSI of all my beacons") work naturally — just watch connection limits on your OS. * **Understanding failures:** when a tool call errors, the taxonomy in [Error Handling](../fundamentals/error-handling) explains what's retryable; timeouts and platform limits are in [Concurrency](../fundamentals/concurrency). # Examples To learn how to use SimpleBluez, please refer to the [examples](https://github.com/simpleble/simpleble/tree/main/examples/simplebluez) provided in the repository. For controllers that support extended advertising, the advertisement secondary channel can be selected before registering the advertisement: ```cpp auto advertisement = bluez.root_custom()->advertisement_add("reader"); advertisement->secondary_channel("2M"); adapter->register_advertisement(advertisement); ``` Use `adapter->supported_secondary_channels()` to check whether BlueZ reports support for `"2M"` on the selected adapter. Local GATT read/write options [#local-gatt-readwrite-options] When exposing local GATT server objects, BlueZ passes per-client context in the `ReadValue` and `WriteValue` options dictionary. The callbacks take `Characteristic::ValueOptions`, which exposes fields such as `device`, `mtu`, `offset`, `link`, `type`, and `prepare-authorize` as `prepare_authorize` in C++: ```cpp characteristic->set_on_read_value([characteristic](SimpleBluez::Characteristic::ValueOptions options) { if (options.mtu.has_value()) { std::cout << "Read MTU: " << *options.mtu << std::endl; } characteristic->value(SimpleBluez::ByteArray("hello")); }); characteristic->set_on_write_value( [](SimpleBluez::ByteArray value, SimpleBluez::Characteristic::ValueOptions options) { if (options.mtu.has_value()) { std::cout << "Write MTU: " << *options.mtu << std::endl; } }); ``` For user-generated/local GATT server characteristics, `characteristic->mtu()` reflects the BlueZ `GattCharacteristic1.MTU` property and should not be treated as a per-device server-side MTU. BlueZ provides the per-request server MTU in `ValueOptions::mtu` for `ReadValue` and `WriteValue` callbacks when that field is present. Local GATT notify acquisition capability [#local-gatt-notify-acquisition-capability] By default, local GATT characteristics do not expose BlueZ's optional `NotifyAcquired` property, so BlueZ uses the `StartNotify` and `StopNotify` flow: ```cpp characteristic->set_on_notify([](bool notifying) { std::cout << "Notifying: " << notifying << std::endl; }); ``` The `NotifyAcquired` property can be exported or removed at runtime: ```cpp struct NotifyClient { SimpleDBus::UnixSocket socket; SimpleBluez::Characteristic::ValueOptions options; }; std::vector notify_clients; characteristic->set_on_acquire_notify( [¬ify_clients](SimpleDBus::UnixSocket socket, SimpleBluez::Characteristic::ValueOptions options) { std::cout << "Notify MTU: " << options.mtu.value_or(0) << std::endl; notify_clients.emplace_back(NotifyClient{std::move(socket), options}); }); characteristic->enable_acquire_notify(); // Future BlueZ notification subscriptions can now discover NotifyAcquired. characteristic->disable_acquire_notify(); ``` `enable_acquire_notify()` exports `NotifyAcquired` with a value of `false`. `disable_acquire_notify()` invalidates the property, allowing BlueZ to fall back to `StartNotify` and `StopNotify` for future subscriptions. Existing acquired sockets are not closed automatically. Acquired notify socket ownership [#acquired-notify-socket-ownership] Each acquired socket is per BlueZ `AcquireNotify` call. SimpleBluez creates a connected Unix socket pair, returns one end to BlueZ, and passes the application-owned end to the callback. The application must move and store that socket if it wants the acquired notify stream to remain alive after the callback returns. If the socket is destroyed at the end of the callback, its file descriptor is closed and BlueZ treats the acquired stream as released. The socket passed to the callback is move-only. `socket.fd()` returns a borrowed descriptor for polling or integration with an event loop; do not close that raw descriptor directly while `UnixSocket` still owns it. Use `socket.close()` to release the acquired stream, or `socket.release()` only when transferring the raw descriptor to another owner that will close it. Writing to the socket sends notification or indication payloads through BlueZ. SimpleBluez creates acquired notify sockets in non-blocking mode, so `send()` can write fewer bytes than requested or fail with `EAGAIN` / `EWOULDBLOCK`. User code should poll `socket.fd()` for writability and retry as needed. Use `options.mtu` as the negotiated MTU context for that acquired stream. SimpleBluez does not chunk payloads, enforce MTU limits, remove closed sockets from user containers, or track the lifetime of user-owned sockets after the callback returns. For indications, poll `socket.fd()` for readability and call `socket.receive()` to read confirmation bytes from BlueZ. A hangup or unrecoverable read/write error means the acquired notify stream is gone; remove that socket from your container and close it if it is still valid. Disabling acquire notify only removes the optional `NotifyAcquired` property for future subscriptions; it does not close sockets already handed to the application. The MTU in `options.mtu` belongs to the acquired socket delivered in the same callback. It is stable for that socket's lifetime. Store the MTU alongside the socket, use it while writing to that socket, and discard both when the socket is closed or reports an unrecoverable I/O error. Do not expect SimpleBluez to update the MTU for a socket that has already been handed to application code. When a central stops notifications or indications, BlueZ closes its end of the acquired notify socket. SimpleBluez does not receive a separate `StopNotify` callback for that fd-backed path. The application should detect the end of the stream by polling the socket fd or by handling `send()` / `receive()` failures: ```cpp // Requires . void notify_all(std::vector& clients, const SimpleBluez::ByteArray& payload) { for (auto it = clients.begin(); it != clients.end();) { auto& client = *it; uint16_t mtu = client.options.mtu.value_or(23); size_t max_payload = mtu > 3 ? mtu - 3 : 0; if (payload.size() > max_payload) { // Split or drop oversized payloads; SimpleBluez does not chunk them. ++it; continue; } ssize_t bytes_sent = client.socket.send(payload.data(), payload.size()); if (bytes_sent == static_cast(payload.size())) { ++it; continue; } if (bytes_sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { // The non-blocking socket is not ready yet. Poll for POLLOUT and retry later. ++it; continue; } // HUP, disconnect, central unsubscribe, or another hard socket error. it = clients.erase(it); } } ``` For event-loop based code, monitor each `socket.fd()` for `POLLHUP`, `POLLERR`, or `POLLNVAL` and remove that `NotifyClient` when any of those flags appear. Monitor for `POLLOUT` before retrying a payload that previously failed with `EAGAIN` / `EWOULDBLOCK`. For indications, also monitor for `POLLIN` and call `socket.receive()` to read BlueZ's confirmation byte. # Usage SimpleBluez should work on any Linux environment supporting DBus and Bluez. Please follow the instructions below to build and run SimpleBluez in your specific environment. System Requirements [#system-requirements] When building SimpleBluez from source, you will need some dependencies based on your current operating system. General Requirements [#general-requirements] * [CMake](https://cmake.org) (Version 3.21 or higher) Linux [#linux] APT-based Distros [#apt-based-distros] * `libdbus-1-dev` (install via `sudo apt install libdbus-1-dev`) RPM-based Distros [#rpm-based-distros] * `dbus-devel` * On Fedora, install via `sudo dnf install dbus-devel` * On CentOS, install via `sudo yum install dbus-devel` Building and Installing the Library (Source) [#building-and-installing-the-library-source] Compiling the library is done using [CMake](https://cmake.org) and relies heavily on plenty of CMake functionality. It is strongly suggested that you get familiarized with CMake before blindly following the instructions below. Building SimpleBluez [#building-simplebluez] You can use the following commands to build SimpleBluez: ```bash cmake -S -B build_simplebluez cmake --build build_simplebluez -j7 ``` Note that if you want to modify the build configuration, you can do so by passing additional arguments to the `cmake` command. For example, to build a shared library set the `BUILD_SHARED_LIBS` CMake variable to `TRUE`: ```bash cmake -S -B build_simplebluez -DBUILD_SHARED_LIBS=TRUE ``` To modify the log level, set the `SIMPLEBLUEZ_LOG_LEVEL` CMake variable to one of the following values: `VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`: ```bash cmake -S -B build_simplebluez -DSIMPLEBLUEZ_LOG_LEVEL=DEBUG ``` To force the usage of the DBus session bus, enable the `SIMPLEBLUEZ_USE_SESSION_DBUS` flag: ```bash cmake -S -B build_simplebluez -DSIMPLEBLUEZ_USE_SESSION_DBUS=TRUE ``` Installing SimpleBluez [#installing-simplebluez] To install SimpleBluez, you can use the following commands: ```bash cmake --install build_simplebluez ``` Note that if you want to modify the installation configuration, you can do so by passing additional arguments to the `cmake` command. For example, to install the library to a specific location, set the `CMAKE_INSTALL_PREFIX` CMake variable to the desired location: ```bash cmake --install build_simplebluez --prefix /usr/local ``` Note that on Linux and MacOS, you will need to run the `cmake --install` command with `sudo` privileges: ```bash sudo cmake --install build_simplebluez ``` Usage with CMake (Installed) [#usage-with-cmake-installed] Once SimpleBluez has been installed, it can be consumed from within CMake: ```cmake find_package(simplebluez REQUIRED CONFIG) target_link_libraries( simplebluez::simplebluez) ``` Usage with CMake (Local) [#usage-with-cmake-local] You can add the `simplebluez` library directory into your project and include it in your `CMakeLists.txt` file: ```cmake add_subdirectory( ${CMAKE_BINARY_DIR}/simplebluez) target_link_libraries( simplebluez::simplebluez) ``` Set `SIMPLEBLUEZ_INSTALL=OFF` before `add_subdirectory` if you do not want SimpleBluez to add install rules to the parent project. Usage with CMake (Vendorized) [#usage-with-cmake-vendorized] If you want to use a vendorized copy of SimpleBluez, you can do so by using FetchContent and specifying the location from where SimpleBluez should be consumed from: ```cmake include(FetchContent) FetchContent_Declare( simplebluez GIT_REPOSITORY GIT_TAG GIT_SHALLOW YES ) # Note that here we manually do what FetchContent_MakeAvailable() would do, # except to ensure that the dependency can also get what it needs, we add # custom logic between the FetchContent_Populate() and add_subdirectory() # calls. FetchContent_GetProperties(simplebluez) if(NOT simplebluez_POPULATED) FetchContent_Populate(simplebluez) list(APPEND CMAKE_MODULE_PATH "${simplebluez_SOURCE_DIR}/cmake/find") add_subdirectory("${simplebluez_SOURCE_DIR}/simplebluez" "${simplebluez_BINARY_DIR}") endif() set(simplebluez_FOUND 1) ``` You can put this code inside `Findsimplebluez.cmake` and add it to your CMake module path, as depicted in [cmake-init-fetchcontent](https://github.com/friendlyanon/cmake-init-fetchcontent). Once vendorized using the above approach, you can consume SimpleBluez from within CMake as you'd normally do: ```cmake find_package(simplebluez REQUIRED) target_link_libraries( simplebluez::simplebluez) ``` One key security feature of SimpleBluez is that it allows the user to specify the URLs and tags of all internal dependencies, thus allowing compilation from internal or secure sources without the risk of those getting compromised. Currently, the following libraries are included as part of SimpleBluez, with the following CMake options available: * [fmtlib](https://github.com/fmtlib/fmt) * `LIBFMT_VENDORIZE`: Enable vendorization of fmtlib. *(Default: True)* * `LIBFMT_GIT_REPOSITORY`: The git repository to use for fmtlib. * `LIBFMT_GIT_TAG`: The git tag to use for fmtlib. *(Default: v8.1.1)* * `LIBFMT_LOCAL_PATH`: The local path to use for fmtlib. *(Default: None)* Build Examples [#build-examples] Use the following instructions to build the provided SimpleBluez examples: ```bash cmake -S /examples/simplebluez -B build_simplebluez_examples -DSIMPLEBLUEZ_LOCAL=ON cmake --build build_simplebluez_examples -j7 ``` Testing [#testing] To build and run unit and integration tests, the following packages are required: ```bash sudo apt install libgtest-dev libgmock-dev python3-dev pip3 install -r /test/requirements.txt ``` Unit Tests [#unit-tests] To run the unit tests, run the following command: ```bash cmake -S -B build_simplebluez_test -DSIMPLEBLUEZ_TEST=ON cmake --build build_simplebluez_test -j7 ./build_simplebluez_test/bin/simplebluez_test ``` Address Sanitizer Tests [#address-sanitizer-tests] To run the address sanitizer tests, run the following command: ```bash cmake -S -B build_simplebluez_test -DSIMPLEBLUEZ_SANITIZE=Address -DSIMPLEBLUEZ_TEST=ON cmake --build build_simplebluez_test -j7 PYTHONMALLOC=malloc ./build_simplebluez_test/bin/simplebluez_test ``` It's important for `PYTHONMALLOC` to be set to `malloc`, otherwise the tests will fail due to Python's memory allocator from triggering false positives. Thread Sanitizer Tests [#thread-sanitizer-tests] To run the thread sanitizer tests, run the following command: ```bash cmake -S -B build_simplebluez_test -DSIMPLEBLUEZ_SANITIZE=Thread -DSIMPLEBLUEZ_TEST=ON cmake --build build_simplebluez_test -j7 ./build_simplebluez_test/bin/simplebluez_test ``` # API Reference import { ApiClass, ApiMethod, ApiSection } from "@/components/api"; import { Callout } from "fumadocs-ui/components/callout"; The following notes provide an overview of the architecture of some of the higher-level classes in the library, as to facilitate their understanding. SimpleBLE::Backend [#simpleble-backend] [toc] Public Functions [!toc] [#public-functions-toc] Public Static Functions [!toc] [#public-static-functions-toc] SimpleBLE::Adapter [#simpleble-adapter] [toc] This class is intended to be used by the user only. Library developers should use shared pointers to `AdapterBase` instead. Public Functions [!toc] [#public-functions-toc-1] "}]} /> "}]} /> "}]} /> "}]} /> "}]} /> "}]} /> Public Static Functions [!toc] [#public-static-functions-toc-1] SimpleBLE::Peripheral [#simpleble-peripheral] [toc] Public Functions [!toc] [#public-functions-toc-2] "}]} /> "}]} /> "}]} /> "}]} /> SimpleBLE::Service [#simpleble-service] [toc] Public Functions [!toc] [#public-functions-toc-3] SimpleBLE::Characteristic [#simpleble-characteristic] [toc] Public Functions [!toc] [#public-functions-toc-4] SimpleBLE::Descriptor [#simpleble-descriptor] [toc] Public Functions [!toc] [#public-functions-toc-5] The Local API is how this host advertises and serves GATT. `SimpleBLE::Peripheral` is a remote device; `SimpleBLE::Local::Peripheral` is this host. Create a local peripheral with [`Adapter::create_local_peripheral()`](#simpleble-adapter). SimpleBLE::Local::CharacteristicCapability [#simpleble-local-characteristiccapability] [toc] Capabilities are passed to `add_characteristic` as a `std::set`. Each characteristic needs at least one. | Enumerator | Meaning | | --------------- | ---------------------------------------------- | | `READ` | Remote centrals can read | | `WRITE_REQUEST` | Remote centrals can write with a response | | `WRITE_COMMAND` | Remote centrals can write without a response | | `NOTIFY` | Remote centrals can subscribe to notifications | | `INDICATE` | Remote centrals can subscribe to indications | SimpleBLE::Local::Peripheral [#simpleble-local-peripheral] [toc] `SimpleBLE::Peripheral` is a remote device discovered by this host. `SimpleBLE::Local::Peripheral` is this host exposing local GATT services. Public Functions [!toc] [#public-functions-toc-6] "}]} detailed="Add several service UUIDs to the advertisement. Must be called before `start()`." notes={[{ content: "Throws `OperationFailed` if the peripheral is already started.", type: "warn" }]} /> The portable API does not select a local name. Linux, macOS, and iOS expose their native advertisement-specific name through `SimpleBLE::Advanced::Linux::set_advertisement_local_name()`, `SimpleBLE::Advanced::MacOS::set_advertisement_local_name()`, and `SimpleBLE::Advanced::iOS::set_advertisement_local_name()`. Each function accepts a `Local::Peripheral&` and an `std::optional`, must be called before `start()`, and changes only the advertisement payload. Passing `std::nullopt` removes the override. "}]} detailed="Observe clients connecting to this peripheral. On platforms that do not expose a Bluetooth address, the value is a platform-specific identifier for the remote client." /> "}]} /> SimpleBLE::Local::Service [#simpleble-local-service] [toc] Public Functions [!toc] [#public-functions-toc-7] "} ]} detailed="Add a characteristic with a unique set of capabilities. At least one capability is required." notes={[{ content: "Throws `OperationFailed` if the service's peripheral is already started.", type: "warn" }]} /> SimpleBLE::Local::Characteristic [#simpleble-local-characteristic] [toc] Public Functions [!toc] [#public-functions-toc-8] "}]} detailed="Optional dynamic value callback. Without this callback, reads return `value()`." /> "}]} detailed="Optional write callback invoked after an incoming write updates `value()`. Call `set_value()` from the callback to publish or echo that value to subscribed clients." /> "}]} detailed="Invoked when the first client subscribes." /> "}]} detailed="Invoked when the last client unsubscribes." /> SimpleBLE::Safe::Adapter [#simpleble-safe-adapter] [toc] Public Functions [!toc] [#public-functions-toc-9] "}]} /> "}]} /> "}]} /> "}]} /> Public Static Functions [!toc] [#public-static-functions-toc-2] SimpleBLE::Safe::Peripheral [#simpleble-safe-peripheral] [toc] Public Functions [!toc] [#public-functions-toc-10] "}]} /> "}]} /> "}]} /> "}]} /> kvn::bytearray [#kvn-bytearray] [toc] Public Functions [!toc] [#public-functions-toc-11] &"}]} /> "}]} /> Public Static Functions [!toc] [#public-static-functions-toc-3] # Examples To learn how to use SimpleBLE, please refer to the [examples](https://github.com/simpleble/simpleble/tree/main/examples/simpleble) provided in the repository. Those examples with a `_safe` suffix use the *noexcept* version of the library. The following list briefly describes each example provided: Central [#central] List Adapters [#list-adapters] [list\_adapters](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/list_adapters.cpp) This example shows how to list all available Bluetooth adapters on the system. It prints the identifier and MAC address of each adapter. Scan [#scan] [scan](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/scan.cpp) This example demonstrates how to scan for nearby BLE devices. It sets up callbacks to print information about devices as they are found and updated. Connect [#connect] [connect](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/connect.cpp) & [connect\_safe](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/connect_safe.cpp) This example connects to a specific BLE device (or the first one found) and lists its services and characteristics. It demonstrates the complete flow of scanning, connecting, and service discovery. Read [#read] [read](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/read.cpp) This example shows how to read the value of a specific characteristic. Write [#write] [write](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/write.cpp) This example shows how to write a value to a specific characteristic. Notify [#notify] [notify](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/notify.cpp) This example demonstrates how to subscribe to notifications on a characteristic and receive updates asynchronously. Peripheral [#peripheral] Peripheral [#peripheral-1] [peripheral](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/peripheral.cpp) This example hosts a local BLE peripheral: it advertises a custom service, accepts reads and writes, publishes notifications, and prints client connect, disconnect, and subscribe events. See [Advertise, Serve, Publish](./peripheral/advertise-serve-publish) for the matching walkthrough. # FAQ import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; Right now, the best way to have your questions answered is on our [Discord](https://discord.gg/N9HqNEcvP3) server. More information will be made available here soon. SimpleBLE supports Windows 10+, Linux (Ubuntu 20.04+ and other distros using Bluez), MacOS 13.0+ (Ventura and newer), iOS 15.0+, and Android API 31+. Central-role APIs (scan, connect, GATT client) and the C++ local peripheral API are available on all supported platforms. Android applications can also use SimpleDroidBLE's Kotlin API. Please check the overview page on more information about platform-specific limitations. All configuration values must be set prior to any other interaction with a SimpleBLE component. This includes calling `SimpleBLE::Adapter::get_adapters()` or any other function that interacts with the underlying Bluetooth stack. Changes made after initialization may not take effect or could lead to undefined behavior. Yes. Call `Adapter::create_local_peripheral()` to get a `SimpleBLE::Local::Peripheral`, configure services and advertising, then `start()`. Android applications can also use the equivalent SimpleDroidBLE Kotlin API. See [Advertise, Serve, Publish](./peripheral/advertise-serve-publish) and the [Advertise](../recipes/advertise) recipe. Peripheral mode still depends on the host adapter and driver supporting the role. Applications distributed as AppX/MSIX packages should declare `` in `Package.appxmanifest`; ordinary unpackaged executables do not need this manifest declaration. MacOS has a policy of not allowing applications to access the Bluetooth address of peripherals. Instead, it provides a UUID that is unique to the peripheral. This UUID is not persistent across reboots, so you should not use it to identify a peripheral. Instead, you should use the name of the peripheral, which is persistent across reboots. On Linux (BlueZ), SimpleBLE waits until all services have been fully resolved before considering the device "connected". This ensures that when `connect()` returns, the device is fully ready for interaction, but it adds the time required for service discovery to the connection process. Retrieving the address type of a peripheral is only supported on Android API level 35 and newer. On older versions of Android, the OS does not expose this information to applications. This is a known issue when running a version of SimpleBLE built for a 32-bit architecture on a 64-bit Windows machine. The issue is that the underlying Windows API will not allow us to query the state of the Bluetooth adapter when running in 32-bit mode. The solution is to consume a 64-bit version of SimpleBLE instead. Building SimpleBLE with the plain-flavored interface allows you to use a version of the library that is independent of the operating system, which is achieved by hardcoding all behaviors and responses. This is useful for testing and debugging, especially when you don't need to deal with operating-system shenanigans and just want to test the API. Coming soon. Coming soon. Most unsupported operations are a no-op. Some throw `SimpleBLE::Exception::OperationNotSupported` instead, including `Adapter::create_local_peripheral()` on backends that cannot host a local peripheral. # Platform Notes SimpleBLE aims to provide a unified API across all platforms, but due to differences in the underlying operating system Bluetooth stacks, there are some platform-specific behaviors and limitations you should be aware of. Windows [#windows] Architecture Mismatch [#architecture-mismatch] If you are running a 32-bit application on a 64-bit Windows machine, you might encounter issues where `SimpleBLE::Adapter::bluetooth_enabled()` returns false even if Bluetooth is enabled. This is a known limitation of the Windows API. It is recommended to build and run your application as 64-bit on 64-bit systems. Adapter Selection [#adapter-selection] The Windows implementation currently has a limitation where `initialized()` might return true if *any* adapter is enabled, not necessarily the specific instance you are inspecting. This is a known issue being tracked. Local Peripheral Mode [#local-peripheral-mode] Windows hosts local services with WinRT `GattServiceProvider`. The Bluetooth adapter and driver must report peripheral-role support. Applications distributed as AppX/MSIX packages should declare `` in `Package.appxmanifest`; ordinary unpackaged executables do not need this manifest declaration. Windows controls the friendly name placed in a discoverable advertisement. WinRT requires one provider per primary service; SimpleBLE starts every configured provider as one peripheral operation and prioritizes providers added with `add_advertised_service()`. Windows owns the shared advertisement payload and may omit UUIDs that do not fit. Explicitly selected UUIDs must belong to configured services, and `start()` throws if the adapter or driver lacks the resources to publish every provider. Windows reports a client session after the client first interacts with or subscribes to a hosted characteristic; a bare link that performs no GATT operation may not produce a client-connected callback. MacOS / iOS [#macos--ios] Peripheral Identifiers (UUIDs) [#peripheral-identifiers-uuids] On macOS and iOS, the operating system does not expose the raw MAC address of Bluetooth peripherals to applications. Instead, it assigns a unique UUID to each peripheral. * **Persistent UUIDs:** These UUIDs are generally persistent for a given device on a given host, meaning if you restart your app, the UUID for a specific peripheral should remain the same. * **Not Global:** The UUID for a specific peripheral will be different on different macOS/iOS devices. You cannot use this UUID to identify a peripheral across different host devices. Connection Management [#connection-management] macOS and iOS manage Bluetooth connections at the OS level. * **Unpairing:** Applications cannot programmatically "unpair" or "forget" a device. This must be done by the user in the system settings. * **Connection State:** The OS might keep a connection alive even if your application disconnects, especially if other apps or the system itself are using the device. Local Peripheral Mode [#local-peripheral-mode-1] macOS and iOS can host a local peripheral through CoreBluetooth. Because CoreBluetooth does not expose peripheral-role connection events, SimpleBLE reports a client as connected when it first subscribes to a characteristic and disconnected when its final subscription ends. Client addresses are CoreBluetooth identifiers rather than Bluetooth MAC addresses. CoreBluetooth supports an advertisement-specific local name distinct from the system Bluetooth and GAP device names. Set it before `start()` with `SimpleBLE::Advanced::MacOS::set_advertisement_local_name()` or `SimpleBLE::Advanced::iOS::set_advertisement_local_name()`. Advertising is best-effort and space-limited; iOS omits the local name while the app is in the background. Linux (BlueZ) [#linux-bluez] Scanning Delays [#scanning-delays] When calling `scan_stop()`, there might be a slight delay (a few seconds) before the underlying adapter actually reports that discovery has stopped. This is a behavior of the BlueZ stack. LE Privacy [#le-privacy] On Linux, LE privacy is controlled by BlueZ and the Bluetooth controller, not by SimpleBLE. SimpleBLE uses BlueZ over D-Bus and follows the privacy policy configured for `bluetoothd`. To enable private device addresses on BlueZ-based systems, configure `/etc/bluetooth/main.conf`: ```ini [General] Privacy = device ``` Restart BlueZ after changing this setting: ```bash sudo systemctl restart bluetooth ``` `Privacy = device` enables private addressing while still accepting peer devices that use either identity addresses or private addresses. Support depends on the BlueZ version, Linux kernel, and Bluetooth adapter/controller. If you need to verify the address used over the air, use BlueZ tools such as `btmon` or an external BLE sniffer. `SimpleBLE::Adapter::address()` may still report the adapter identity address. Adapter Removal [#adapter-removal] On Linux, BlueZ exposes adapters, peripherals, services, and characteristics as D-Bus object paths. If a Bluetooth adapter is disabled, unplugged, or otherwise removed, BlueZ removes the adapter object path and any child device paths below it. SimpleBLE objects retained by the application may still exist as C++ objects, but the underlying BlueZ objects they refer to are no longer present. Applications that support adapter hot-plug or adapter removal should treat existing adapters, remote peripherals, and local peripherals as stale after the adapter disappears. Stop active work, release retained objects when possible, and call `SimpleBLE::Adapter::get_adapters()` again to discover the current adapter set before scanning, connecting, or advertising again. Local Peripheral Mode [#local-peripheral-mode-2] On Linux, `Adapter::create_local_peripheral()` registers a BlueZ GATT application and an LE advertisement through D-Bus, so `bluetoothd` must be running and the process must have the same D-Bus access used for scanning. SimpleBLE permits the same adapter to scan and advertise at the same time, but availability depends on BlueZ and the Bluetooth controller supporting concurrent operation and having sufficient resources. Incoming clients of the local peripheral are reported through `SimpleBLE::Local::Peripheral` client callbacks, not as remote `SimpleBLE::Peripheral` objects to `connect()` to. BlueZ supports a per-advertisement local name independent of the adapter alias. Set it before `start()` with `SimpleBLE::Advanced::Linux::set_advertisement_local_name()`. BlueZ may shorten the name to fit the advertising data. Android [#android] API Level Limitations [#api-level-limitations] Android's Bluetooth API has evolved significantly over time, leading to some version-specific limitations: * **Address Type:** The `address_type()` method is only available on devices running Android API level 35 or newer. On older devices, this may return an unspecified or default value. * **Bluetooth Enable/Disable:** Programmatically enabling or disabling Bluetooth (e.g., `power_on()`, `power_off()`) uses APIs that were deprecated in API level 33. These functions may not work or might throw exceptions on newer Android versions. Bond Management [#bond-management] Similar to iOS, Android (especially newer versions) restricts the ability of applications to remove bonds (unpair) programmatically. While SimpleBLE attempts to use hidden APIs to achieve this where possible, it is not guaranteed to work on all devices or future Android versions. Local Peripheral Mode [#local-peripheral-mode-3] Android API 31 and newer can host local GATT services and legacy BLE advertisements. Add `BLUETOOTH_CONNECT` and `BLUETOOTH_ADVERTISE` to the application manifest and request both at runtime. Hardware support still depends on `BluetoothAdapter.isMultipleAdvertisementSupported()` and the vendor Bluetooth stack. Native C++ applications must call `SimpleBLE::Advanced::Android::set_context(application_context)` before `Adapter::create_local_peripheral()`. Pass an application context, not an Activity; SimpleBLE retains a global JNI reference while the process is alive. `Adapter.createLocalPeripheral(context)` in SimpleDroidBLE extracts and configures the application context automatically. Android's public advertising API can include the adapter's system Bluetooth name but cannot set an arbitrary per-advertisement name. SimpleBLE includes the system name in the scan response. The backend uses legacy advertising, so the normal 31-byte advertising and scan-response limits apply. Prepared writes are rejected; clients should use regular write requests or write commands within the negotiated GATT payload size. Android reports real GATT server connection and disconnection events. Client addresses are the addresses exposed by `BluetoothDevice`; privacy-enabled centrals may use a rotating address. # Getting Started import { Callout } from 'fumadocs-ui/components/callout'; SimpleBLE is a cross-platform C++ library for Bluetooth Low Energy. An adapter can be used in two roles: * **Central:** scan for nearby devices and connect to them as a GATT client. That remote device is a `SimpleBLE::Peripheral`. * **Peripheral:** advertise and serve local GATT services. This host is a `SimpleBLE::Local::Peripheral`. This page gets you to a first success in each role. [Scan, Connect, Read](./central/scan-connect-read) and [Advertise, Serve, Publish](./peripheral/advertise-serve-publish) go deeper. If you are new to BLE or platform-specific permissions, it is worth taking a quick look at [Bluetooth LE Basics](../fundamentals/ble_basics) and [Bluetooth Permissions](../fundamentals/permissions) before you begin. Prerequisites [#prerequisites] Before we start building the sample application, make sure your machine and development environment are ready: * A C++17-compatible compiler * [CMake](https://cmake.org) 3.21 or newer * A machine with Bluetooth Low Energy support enabled * The platform dependencies described in the [usage](./usage) guide * On Windows with MSVC, a C++20-capable toolchain such as Visual Studio 2019 16.11 or newer If you are developing on macOS, iOS, or Android, make sure your app has the required Bluetooth permission entries before testing. Install SimpleBLE [#install-simpleble] Start by cloning the SimpleBLE repository locally: ```bash git clone https://github.com/simpleble/simpleble.git cd simpleble ``` With the source available locally, the next step is to build and install the library: ```bash cmake -S . -B build_simpleble cmake --build build_simpleble -j7 cmake --install build_simpleble ``` On Linux and macOS, you may need elevated privileges for the install step: ```bash sudo cmake --install build_simpleble ``` Once that finishes, SimpleBLE is installed on your machine and ready to be used from your own CMake project. If you need platform-specific setup details, refer to the [usage](./usage) page and [platform notes](./platform_notes). Create your application [#create-your-application] Now that SimpleBLE is installed, we can create a small sample project that links against it. Start with this structure: ```text my-simpleble-app/ |-- CMakeLists.txt |-- src/ |-- main.cpp ``` Add CMakeLists.txt [#add-cmakeliststxt] In the project root, create `CMakeLists.txt`: ```cmake cmake_minimum_required(VERSION 3.21) project(simpleble_quickstart LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) find_package(simpleble REQUIRED CONFIG) add_executable(simpleble_quickstart src/main.cpp) target_link_libraries(simpleble_quickstart PRIVATE simpleble::simpleble) ``` This gives your sample project a standard CMake setup and tells it to locate the installed SimpleBLE package and link it to your executable. Get a Bluetooth adapter [#get-a-bluetooth-adapter] Every SimpleBLE program starts the same way: confirm Bluetooth is available, then pick an adapter. SimpleBLE manages multiple backends representing different under-the-hood implementations (such as BlueZ, CoreBluetooth, WinRT, or Dongl), but provides a consolidated method to fetch all available adapters. ```cpp #include #include int main() { if (!SimpleBLE::Adapter::bluetooth_enabled()) { std::cerr << "Bluetooth is not enabled or permission has not been granted." << std::endl; return EXIT_FAILURE; } auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { std::cerr << "No Bluetooth adapters found." << std::endl; return EXIT_FAILURE; } auto adapter = adapters.front(); std::cout << "Using adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl; return EXIT_SUCCESS; } ``` Act as a central [#act-as-a-central] With an adapter, scan for nearby devices. This listens for advertisements and prints each peripheral as it is found: ```cpp adapter.set_callback_on_scan_found([](SimpleBLE::Peripheral peripheral) { std::cout << "Found: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; }); adapter.scan_for(5000); ``` `SimpleBLE::Peripheral` here is a **remote** device. Connecting to one, listing its GATT table, and reading a characteristic is covered in [Scan, Connect, Read](./central/scan-connect-read). Act as a peripheral [#act-as-a-peripheral] The C++ API can host a local peripheral on every supported platform. Native Android applications must provide their application context with `SimpleBLE::Advanced::Android::set_context()` before creating the local peripheral. SimpleDroidBLE does this automatically. `SimpleBLE::Local::Peripheral` is **this host** advertising and serving GATT. Configure the advertisement and at least one characteristic, then call `start()`: ```cpp auto peripheral = adapter.create_local_peripheral(); peripheral.add_advertised_service("12345678-1234-5678-1234-56789abcdef0"); auto service = peripheral.add_service("12345678-1234-5678-1234-56789abcdef0"); service.add_characteristic("12345678-1234-5678-1234-56789abcdef1", {SimpleBLE::Local::CharacteristicCapability::READ}); peripheral.start(); std::cout << "Local peripheral is advertising." << std::endl; ``` The portable advertisement does not select a local name. Windows and Android use their system-owned Bluetooth name. Linux, macOS, and iOS expose an optional advertised-name override through their `SimpleBLE::Advanced` namespaces. Keep the process alive after `start()`, then call `peripheral.stop()` when you are done. Writes, notifications, and client connection callbacks are covered in [Advertise, Serve, Publish](./peripheral/advertise-serve-publish). Build and run [#build-and-run] From your project directory: ```bash cmake -S . -B build cmake --build build ./build/simpleble_quickstart ``` Where to next? [#where-to-next] * [Scan, Connect, Read](./central/scan-connect-read) to pick a device, connect, and read a characteristic * [Advertise, Serve, Publish](./peripheral/advertise-serve-publish) to serve GATT and publish value updates * [Recipes](../recipes/scan) for task-based snippets across the supported bindings * [API reference](./api) for the complete C++ surface area * [Examples](./examples) for the programs in the repository * [FAQ](./faq) for common questions and troubleshooting tips # Usage Please follow the instructions below to build and run SimpleBLE in your specific environment. System Requirements [#system-requirements] To build SimpleBLE from source, ensure your system meets the following requirements, which vary by operating system. These dependencies and version constraints ensure compatibility and optimal performance. General Requirements [#general-requirements] To build SimpleBLE, you need: * [CMake](https://cmake.org) (Version 3.21 or higher). Refer to our [CMake Primer](../cmake_primer) for setup guidance. Linux [#linux] SimpleBLE is designed to work on Linux distributions using BlueZ as the Bluetooth stack. **Supported Distributions** * Primary: Ubuntu 20.04 and newer * Other major distributions using BlueZ may work but are not officially supported. **Dependencies** * **APT-based Distributions** (e.g., Ubuntu): Install `libdbus-1-dev` using: ```bash sudo apt install libdbus-1-dev ``` * **RPM-based Distributions** (e.g., Fedora, CentOS): Install `dbus-devel` using: ```bash # On Fedora sudo dnf install dbus-devel # On CentOS sudo yum install dbus-devel ``` **Notes** * BlueZ compatibility should ensure broad support, but Ubuntu is the primary tested platform. Windows [#windows] **Supported Versions** * Windows 10 and newer **Dependencies** * [Windows SDK](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/) (Version 10.0.22621.0 or higher) * A C++20-capable MSVC toolchain when building with Visual Studio, such as Visual Studio 2019 16.11 or newer. **Notes** * Only a single Bluetooth adapter is supported by the OS backend. * WSL does not support Bluetooth. MacOS [#macos] **Supported Versions** * macOS 13.0 (Ventura) and newer **Dependencies** * [Xcode Command Line Tools](https://developer.apple.com/xcode/resources/) (install via `xcode-select --install`) **Exceptions** * macOS 12.0, 12.1, and 12.2 have a known bug where the adapter fails to return peripherals after scanning. **Notes** * Only a single Bluetooth adapter is supported by the OS backend. Android [#android] **Supported Versions** * API 31 and newer **Dependencies** * [Android Studio](https://developer.android.com/studio) * [Android NDK](https://developer.android.com/ndk) (Version 25 or higher; older versions may work but are untested) **Notes** * Older APIs lack certain JVM API features required by SimpleBLE. * Removing bonds is not supported due to limitations in the public API; non-public API workarounds are needed. * Address type is unavailable, as it requires API 35 or newer. iOS [#ios] **Supported Versions** * iOS 15.8 and newer **Notes** * Older iOS versions may work but lack formal testing. Building and Installing SimpleBLE (Source) [#building-and-installing-simpleble-source] Compiling the library is done using [CMake](https://cmake.org) and relies heavily on plenty of CMake functionality. It is strongly suggested that you get familiarized with CMake before blindly following the instructions below. Building SimpleBLE [#building-simpleble] You can use the following commands to build SimpleBLE: ```bash cmake -S -B build_simpleble cmake --build build_simpleble -j7 ``` Note that if you want to modify the build configuration, you can do so by passing additional arguments to the `cmake` command. For example, to build a shared library set the `BUILD_SHARED_LIBS` CMake variable to `TRUE`: ```bash cmake -S -B build_simpleble -DBUILD_SHARED_LIBS=TRUE ``` To build a plain-flavored version of the library, set the `SIMPLEBLE_PLAIN` CMake variable to `TRUE`: ```bash cmake -S -B build_simpleble -DSIMPLEBLE_PLAIN=TRUE ``` To modify the log level, set the `SIMPLEBLE_LOG_LEVEL` CMake variable to one of the following values: `VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`: ```bash cmake -S -B build_simpleble -DSIMPLEBLE_LOG_LEVEL=DEBUG ``` **(Linux only)** To force the usage of the DBus session bus, enable the `SIMPLEBLE_USE_SESSION_DBUS` flag: ```bash cmake -S -B build_simplebluez -DSIMPLEBLE_USE_SESSION_DBUS=TRUE ``` Installing SimpleBLE [#installing-simpleble] To install SimpleBLE, you can use the following commands: ```bash cmake --install build_simpleble ``` Note that if you want to modify the installation configuration, you can do so by passing additional arguments to the `cmake` command. For example, to install the library to a specific location, set the `CMAKE_INSTALL_PREFIX` CMake variable to the desired location: ```bash cmake --install build_simpleble --prefix /usr/local ``` Note that on Linux and MacOS, you will need to run the `cmake --install` command with `sudo` privileges. ```bash sudo cmake --install build_simpleble ``` Dongl permissions on Linux [#dongl-permissions-on-linux] The Dongl backend opens its USB CDC ACM device directly. If opening the device fails with `EACCES`, add your user to the serial-device group and then sign out and back in: ```bash sudo usermod -aG dialout "$USER" ``` Some distributions use a different group; check the group assigned to the Dongl's `/dev/ttyACM*` device when necessary. Distributors can instead grant device-specific access with a udev rule: ```text SUBSYSTEM=="tty", ATTRS{idVendor}=="3918", ATTRS{idProduct}=="0001", MODE="0660", GROUP="dialout", TAG+="uaccess" ``` Install the rule under `/etc/udev/rules.d/`, reload udev, and reconnect the Dongl. Usage with CMake (Installed) [#usage-with-cmake-installed] Once SimpleBLE has been installed, it can be consumed from within CMake: ```cmake find_package(simpleble REQUIRED CONFIG) target_link_libraries( simpleble::simpleble) ``` Note that this example assumes that SimpleBLE has been installed to a location that is part of the default CMake module path. Usage with CMake (Local) [#usage-with-cmake-local] You can add the `simpleble` library directory into your project and include it in your `CMakeLists.txt` file: ```cmake add_subdirectory( ${CMAKE_BINARY_DIR}/simpleble) target_link_libraries( simpleble::simpleble) ``` Set `SIMPLEBLE_INSTALL=OFF` before `add_subdirectory` if you do not want SimpleBLE to add install rules to the parent project. Usage with CMake (Vendorized) [#usage-with-cmake-vendorized] If you want to use a vendorized copy of SimpleBLE, you can do so by using FetchContent and specifying the location from where SimpleBLE should be consumed from. ```cmake include(FetchContent) FetchContent_Declare( simpleble GIT_REPOSITORY GIT_TAG GIT_SHALLOW YES ) # Note that here we manually do what FetchContent_MakeAvailable() would do, # except to ensure that the dependency can also get what it needs, we add # custom logic between the FetchContent_Populate() and add_subdirectory() # calls. FetchContent_GetProperties(simpleble) if(NOT simpleble_POPULATED) FetchContent_Populate(simpleble) list(APPEND CMAKE_MODULE_PATH "${simpleble_SOURCE_DIR}/cmake/find") add_subdirectory("${simpleble_SOURCE_DIR}/simpleble" "${simpleble_BINARY_DIR}") endif() set(simpleble_FOUND 1) ``` You can put this code inside `Findsimpleble.cmake` and add it to your CMake module path, as depicted in [cmake-init-fetchcontent](https://github.com/friendlyanon/cmake-init-fetchcontent). Once vendorized using the above approach, you can consume SimpleBLE from within CMake as you'd normally do: ```cmake find_package(simpleble REQUIRED) target_link_libraries( simpleble::simpleble) ``` One key security feature of SimpleBLE is that it allows the user to specify the URLs and tags of all internal dependencies, thus allowing compilation from internal or secure sources without the risk of those getting compromised. Usage alongside native code in Android [#usage-alongside-native-code-in-android] When using SimpleBLE alongside native code in Android, add the bridge module that provides the JVM callback classes: ```kotlin dependencies { implementation("org.simpleble:simpledroidbridge:") } ``` For a local checkout, add the following to your `settings.gradle` file: ```groovy includeBuild("path/to/simpleble/simpledroidbridge") { dependencySubstitution { substitute module("org.simpleble:simpledroidbridge") with project(":") } } ``` ```kotlin includeBuild("path/to/simpleble/simpledroidbridge") { dependencySubstitution { substitute(module("org.simpleble:simpledroidbridge")).using(project(":")) } } ``` Build Examples [#build-examples] Use the following instructions to build the provided SimpleBLE examples: ```bash cmake -S /examples/simpleble -B build_simpleble_examples -DSIMPLEBLE_LOCAL=ON cmake --build build_simpleble_examples -j7 ``` Testing [#testing] To build and run unit and integration tests, the following packages are required: ```bash sudo apt install libgtest-dev libgmock-dev python3-dev pip3 install -r /test/requirements.txt ``` Unit Tests [#unit-tests] To run the unit tests, run the following command: ```bash cmake -S -B build_simpleble_test -DSIMPLEBLE_TEST=ON cmake --build build_simpleble_test -j7 ./build_simpleble_test/bin/simpleble_test ``` Address Sanitizer Tests [#address-sanitizer-tests] To run the address sanitizer tests, run the following command: ```bash cmake -S -B build_simpleble_test -DSIMPLEBLE_SANITIZE=Address -DSIMPLEBLE_TEST=ON cmake --build build_simpleble_test -j7 PYTHONMALLOC=malloc ./build_simpleble_test/bin/simpleble_test ``` It's important for `PYTHONMALLOC` to be set to `malloc`, otherwise the tests will fail due to Python's memory allocator from triggering false positives. Thread Sanitizer Tests [#thread-sanitizer-tests] To run the thread sanitizer tests, run the following command: ```bash cmake -S -B build_simpleble_test -DSIMPLEBLE_SANITIZE=Thread -DSIMPLEBLE_TEST=ON cmake --build build_simpleble_test -j7 ./build_simpleble_test/bin/simpleble_test ``` # API Reference import { ApiClass, ApiMethod, ApiSection } from "@/components/api"; The SimpleCBLE API provides a C-compatible interface for SimpleBLE. It relies on handles (`simpleble_adapter_t`, `simpleble_peripheral_t`) to manage objects. Memory Management [#memory-management] [toc] Configuration [#configuration] [toc] All configuration values must be set before calling adapter functions such as `simpleble_adapter_get_count` or `simpleble_adapter_get_handle`. Logging [#logging] [toc] simpleble_adapter_t [#simpleble_adapter_t] [toc] Lifecycle and Info [!toc] [#lifecycle-and-info-toc] Scanning [!toc] [#scanning-toc] Callbacks [!toc] [#callbacks-toc] simpleble_peripheral_t [#simpleble_peripheral_t] [toc] Lifecycle and Info [!toc] [#lifecycle-and-info-toc-1] Connection [!toc] [#connection-toc] Services and Data [!toc] [#services-and-data-toc] GATT Operations [!toc] [#gatt-operations-toc] The `service` and `characteristic` arguments are fixed-size `simpleble_uuid_t` structures passed by value. FFI bindings must model them as by-value structures, not as `char*` pointers. # Examples You can find the official examples in the `examples/simplecble` directory of the repository. * [Scan](https://github.com/simpleble/simpleble/blob/main/examples/simplecble/c/scan.c): Scans for peripherals and prints their details. * [Connect](https://github.com/simpleble/simpleble/blob/main/examples/simplecble/c/connect.c): Connects to a selected peripheral and lists its services. * [Notify](https://github.com/simpleble/simpleble/blob/main/examples/simplecble/c/notify.c): Subscribes to notifications from a characteristic. Building Examples [#building-examples] To build the examples: ```bash cmake -S examples/simplecble/c -B build_examples cmake --build build_examples ``` # FAQ import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; In the C API (SimpleCBLE), you are responsible for freeing memory allocated by the library. 1. **Handles**: Objects like `simpleble_adapter_t` and `simpleble_peripheral_t` are handles. When you obtain one (e.g., via `simpleble_adapter_get_handle` or `simpleble_adapter_scan_get_results_handle`), you *must* release it when you are done using `simpleble_adapter_release_handle` or `simpleble_peripheral_release_handle`. 2. **Strings**: Functions that return `char*` (e.g., `simpleble_adapter_identifier`) allocate memory. You *must* free this memory using `simpleble_free()`. 3. **Data Buffers**: Data buffers passed to callbacks are usually owned by the library and valid only for the duration of the callback. If you need to keep the data, copy it. The C API does not use vectors directly. Instead, it uses a count/get pattern. 1. Call `_count()` to get the number of items. 2. Loop from `0` to `count - 1`. 3. Call `_get_handle(index)` or `_get(index, &struct)` to retrieve the item. For example, `simpleble_adapter_scan_get_results_count` and `simpleble_adapter_scan_get_results_handle`. SimpleCBLE supports the same platforms as SimpleBLE: Windows 10+, Linux (BlueZ), MacOS 13+, iOS 15+, and Android API 31+. # Platform Notes SimpleCBLE wraps the SimpleBLE C++ library. Therefore, all platform-specific notes, limitations, and behaviors documented for SimpleBLE apply to SimpleCBLE as well. Please refer to the [SimpleBLE Platform Notes](../simpleble/platform_notes) for detailed information regarding: * **Windows**: Architecture mismatch (32-bit vs 64-bit) and adapter selection issues. * **MacOS / iOS**: UUIDs instead of MAC addresses, and connection management limitations. * **Linux**: Scanning delays and adapter removal behavior. * **Android**: API level limitations and bond management. # Tutorial This tutorial will guide you through the basics of using SimpleCBLE to scan for and connect to Bluetooth Low Energy devices. Header Files [#header-files] To use SimpleCBLE, you need to include the main header file: ```c #include ``` Initialization [#initialization] There is no global initialization function, but you should usually start by getting the list of adapters. Adapters [#adapters] Everything in SimpleBLE starts with an adapter. You can check if Bluetooth is enabled and list available adapters. ```c #include #include int main() { // Check if Bluetooth is enabled if (!simpleble_adapter_is_bluetooth_enabled()) { printf("Bluetooth is not enabled. "); return 1; } // Get the number of adapters size_t adapter_count = simpleble_adapter_get_count(); if (adapter_count == 0) { printf("No adapters found. "); return 1; } // Get the handle for the first adapter // Note: You must release this handle when done! simpleble_adapter_t adapter = simpleble_adapter_get_handle(0); if (adapter == NULL) { printf("Failed to get adapter handle. "); return 1; } // Get adapter details char* identifier = simpleble_adapter_identifier(adapter); char* address = simpleble_adapter_address(adapter); printf("Adapter: %s [%s] ", identifier, address); // Clean up strings simpleble_free(identifier); simpleble_free(address); // Clean up adapter handle simpleble_adapter_release_handle(adapter); return 0; } ``` Scanning [#scanning] To scan for peripherals, you use `simpleble_adapter_scan_for` or the async methods. ```c // Start scanning for 5 seconds (5000 ms) simpleble_adapter_scan_for(adapter, 5000); // Get results size_t results_count = simpleble_adapter_scan_get_results_count(adapter); for (size_t i = 0; i < results_count; i++) { // Get peripheral handle // Note: You must release this handle when done! simpleble_peripheral_t peripheral = simpleble_adapter_scan_get_results_handle(adapter, i); char* identifier = simpleble_peripheral_identifier(peripheral); char* address = simpleble_peripheral_address(peripheral); int16_t rssi = simpleble_peripheral_rssi(peripheral); printf("Found: %s [%s] RSSI: %d ", identifier, address, rssi); simpleble_free(identifier); simpleble_free(address); simpleble_peripheral_release_handle(peripheral); } ``` Connecting [#connecting] To connect, you need a peripheral handle. ```c simpleble_peripheral_t peripheral = simpleble_adapter_scan_get_results_handle(adapter, 0); if (simpleble_peripheral_connect(peripheral) == SIMPLEBLE_SUCCESS) { printf("Connected! "); // Do something... simpleble_peripheral_disconnect(peripheral); } else { printf("Failed to connect. "); } simpleble_peripheral_release_handle(peripheral); ``` Services and Characteristics [#services-and-characteristics] Once connected, you can explore services and characteristics. ```c size_t services_count = simpleble_peripheral_services_count(peripheral); for (size_t i = 0; i < services_count; i++) { simpleble_service_t service; if (simpleble_peripheral_services_get(peripheral, i, &service) == SIMPLEBLE_SUCCESS) { printf("Service UUID: %s ", service.uuid.value); for (size_t j = 0; j < service.characteristic_count; j++) { printf(" Characteristic UUID: %s ", service.characteristics[j].uuid.value); } } } ``` Reading and Writing [#reading-and-writing] (Add examples for reading/writing using `simpleble_peripheral_read`, `simpleble_peripheral_write_request`, etc.) Notifications [#notifications] (Add example for `simpleble_peripheral_notify`) Memory Management [#memory-management] It is critical to release handles and free strings returned by the library. * Use `simpleble_free()` for any string (`char*`) returned by the API. * Use `simpleble_adapter_release_handle()` for `simpleble_adapter_t`. * Use `simpleble_peripheral_release_handle()` for `simpleble_peripheral_t`. Failure to do so will result in memory leaks. # Usage SimpleCBLE is the C interface for SimpleBLE, allowing you to use the library in pure C environments or bind it to other languages. System Requirements [#system-requirements] Since SimpleCBLE is a wrapper around SimpleBLE, it shares the same system requirements. Please refer to the [SimpleBLE Usage](../simpleble/usage#system-requirements) page for a detailed list of requirements for your operating system. Building SimpleCBLE [#building-simplecble] SimpleCBLE is built as part of the SimpleBLE project using CMake. ```bash # Clone the repository (if you haven't already) git clone https://github.com/simpleble/simpleble.git cd simpleble # Build SimpleCBLE cmake -S simplecble -B build_simplecble cmake --build build_simplecble ``` This will produce the `simplecble` library (and `simpleble` which it depends on). Build Options [#build-options] * `BUILD_SHARED_LIBS`: Set to `TRUE` to build shared libraries (default is usually static). * `SIMPLEBLE_LOG_LEVEL`: Set the log level (`VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`). Example: ```bash cmake -S simplecble -B build_simplecble -DBUILD_SHARED_LIBS=TRUE -DSIMPLEBLE_LOG_LEVEL=DEBUG ``` Installing SimpleCBLE [#installing-simplecble] To install the library: ```bash sudo cmake --install build_simplecble ``` This will install the headers to your system's include directory (e.g., `/usr/local/include/simplecble`) and the libraries to the lib directory. Usage in your Project [#usage-in-your-project] Runtime Configuration [#runtime-configuration] Set configuration values before the first adapter call. For example, Unity or other P/Invoke users on Android can request high connection priority with: ```c #include int main(void) { simpleble_config_set_android_connection_priority(1); size_t adapter_count = simpleble_adapter_get_count(); /* Continue with adapter selection and scanning. */ } ``` Using CMake [#using-cmake] If you are using CMake, you can use `find_package` to locate SimpleCBLE. ```cmake find_package(simplecble REQUIRED) target_link_libraries(your_target PRIVATE simplecble::simplecble) ``` Using pkg-config [#using-pkg-config] (Note: Check if pkg-config files are generated. If not, maybe skip this or say "Manual linking") Manual Linking [#manual-linking] If you are not using CMake, you need to link against `simplecble` and `simpleble`. You also need to link against the necessary system libraries (e.g., `dbus-1` on Linux, `CoreBluetooth` on macOS). **Linux Example:** ```bash gcc main.c -o main -lsimplecble -lsimpleble $(pkg-config --libs --cflags dbus-1) -lstdc++ ``` (Note: Since SimpleBLE is C++, you need to link with the C++ standard library usually, or use `g++` to link even if compiling C source). **MacOS Example:** ```bash clang main.c -o main -lsimplecble -lsimpleble -framework CoreBluetooth -framework Foundation -lc++ ``` # Architecture The following notes provide an overview of the architecture of some of the higher-level classes in the library, as to facilitate their understanding. Interface [#interface] * In order to simplify the routing of messages, all interfaces are assumed to have properties, thus skipping the need to have a special implementation of `org.freedesktop.DBus.Properties`. * All properties are stored in the holder in which they came from. This is not the most efficient way of handling properties, but it is the one that minimizes the necessary code for children of the `Interface` class. Proxy [#proxy] * Messages for `org.freedesktop.DBus.Properties` are automatically handled by the `Proxy` class. * `Proxy::valid()` tracks whether the proxy's object path is currently present in the D-Bus object tree. When an object path is removed, the corresponding proxy and all retained descendant proxies are marked invalid. If the path is later added again, the same retained proxy can become valid again. * A retained `Proxy` object should not be treated as proof that the remote D-Bus object still exists. Callers that keep proxy references across `InterfacesRemoved` events should check `valid()` before issuing methods or refreshing state. # Examples To learn how to use SimpleDBus, please refer to the [examples](https://github.com/simpleble/simpleble/tree/main/examples/simpledbus) provided in the repository. # Usage SimpleDBus should work on any Linux environment supporting DBus. Please follow the instructions below to build and run SimpleDBus in your specific environment. System Requirements [#system-requirements] When building SimpleDBus from source, you will need some dependencies based on your current operating system. General Requirements [#general-requirements] * [CMake](https://cmake.org) (Version 3.21 or higher) Linux [#linux] APT-based Distros [#apt-based-distros] * `libdbus-1-dev` (install via `sudo apt install libdbus-1-dev`) RPM-based Distros [#rpm-based-distros] * `dbus-devel` * On Fedora, install via `sudo dnf install dbus-devel` * On CentOS, install via `sudo yum install dbus-devel` Building and Installing the Library (Source) [#building-and-installing-the-library-source] Compiling the library is done using [CMake](https://cmake.org) and relies heavily on plenty of CMake functionality. It is strongly suggested that you get familiarized with CMake before blindly following the instructions below. Building SimpleDBus [#building-simpledbus] You can use the following commands to build SimpleDBus: ```bash cmake -S -B build_simpledbus cmake --build build_simpledbus -j7 ``` Note that if you want to modify the build configuration, you can do so by passing additional arguments to the `cmake` command. For example, to build a shared library set the `BUILD_SHARED_LIBS` CMake variable to `TRUE`: ```bash cmake -S -B build_simpledbus -DBUILD_SHARED_LIBS=TRUE ``` To modify the log level, set the `SIMPLEDBUS_LOG_LEVEL` CMake variable to one of the following values: `VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`: ```bash cmake -S -B build_simpledbus -DSIMPLEDBUS_LOG_LEVEL=DEBUG ``` To force the usage of the DBus session bus, enable the `SIMPLEDBUS_USE_SESSION_DBUS` flag: ```bash cmake -S -B build_simpledbus -DSIMPLEDBUS_USE_SESSION_DBUS=TRUE ``` Installing SimpleDBus [#installing-simpledbus] To install SimpleDBus, you can use the following commands: ```bash cmake --install build_simpledbus ``` Note that if you want to modify the installation configuration, you can do so by passing additional arguments to the `cmake` command. For example, to install the library to a specific location, set the `CMAKE_INSTALL_PREFIX` CMake variable to the desired location: ```bash cmake --install build_simpledbus --prefix /usr/local ``` Note that on Linux and MacOS, you will need to run the `cmake --install` command with `sudo` privileges: ```bash sudo cmake --install build_simpledbus ``` Usage with CMake (Installed) [#usage-with-cmake-installed] Once SimpleDBus has been installed, it can be consumed from within CMake: ```cmake find_package(simpledbus REQUIRED CONFIG) target_link_libraries( simpledbus::simpledbus) ``` Usage with CMake (Local) [#usage-with-cmake-local] You can add the `simpledbus` library directory into your project and include it in your `CMakeLists.txt` file: ```cmake add_subdirectory( ${CMAKE_BINARY_DIR}/simpledbus) target_link_libraries( simpledbus::simpledbus) ``` Set `SIMPLEDBUS_INSTALL=OFF` before `add_subdirectory` if you do not want SimpleDBus to add install rules to the parent project. Usage with CMake (Vendorized) [#usage-with-cmake-vendorized] If you want to use a vendorized copy of SimpleDBus, you can do so by using FetchContent and specifying the location from where SimpleDBus should be consumed from: ```cmake include(FetchContent) FetchContent_Declare( simpledbus GIT_REPOSITORY GIT_TAG GIT_SHALLOW YES ) # Note that here we manually do what FetchContent_MakeAvailable() would do, # except to ensure that the dependency can also get what it needs, we add # custom logic between the FetchContent_Populate() and add_subdirectory() # calls. FetchContent_GetProperties(simpledbus) if(NOT simpledbus_POPULATED) FetchContent_Populate(simpledbus) list(APPEND CMAKE_MODULE_PATH "${simpledbus_SOURCE_DIR}/cmake/find") add_subdirectory("${simpledbus_SOURCE_DIR}/simpledbus" "${simpledbus_BINARY_DIR}") endif() set(simpledbus_FOUND 1) ``` You can put this code inside `Findsimpledbus.cmake` and add it to your CMake module path, as depicted in [cmake-init-fetchcontent](https://github.com/friendlyanon/cmake-init-fetchcontent). Once vendorized using the above approach, you can consume SimpleDBus from within CMake as you'd normally do: ```cmake find_package(simpledbus REQUIRED) target_link_libraries( simpledbus::simpledbus) ``` One key security feature of SimpleDBus is that it allows the user to specify the URLs and tags of all internal dependencies, thus allowing compilation from internal or secure sources without the risk of those getting compromised. Currently, the following libraries are included as part of SimpleDBus, with the following CMake options available: * [fmtlib](https://github.com/fmtlib/fmt) * `LIBFMT_VENDORIZE`: Enable vendorization of fmtlib. *(Default: True)* * `LIBFMT_GIT_REPOSITORY`: The git repository to use for fmtlib. * `LIBFMT_GIT_TAG`: The git tag to use for fmtlib. *(Default: v8.1.1)* * `LIBFMT_LOCAL_PATH`: The local path to use for fmtlib. *(Default: None)* Build Examples [#build-examples] Use the following instructions to build the provided SimpleDBus examples: ```bash cmake -S /examples/simpledbus -B build_simpledbus_examples -DSIMPLEDBUS_LOCAL=ON cmake --build build_simpledbus_examples -j7 ``` Testing [#testing] To build and run unit and integration tests, the following packages are required: ```bash sudo apt install libgtest-dev libgmock-dev python3-dev pip3 install -r /test/requirements.txt ``` Unit Tests [#unit-tests] To run the unit tests, run the following command: ```bash cmake -S -B build_simpledbus_test -DSIMPLEDBUS_TEST=ON cmake --build build_simpledbus_test -j7 ./build_simpledbus_test/bin/simpledbus_test ``` Address Sanitizer Tests [#address-sanitizer-tests] To run the address sanitizer tests, run the following command: ```bash cmake -S -B build_simpledbus_test -DSIMPLEDBUS_SANITIZE=Address -DSIMPLEDBUS_TEST=ON cmake --build build_simpledbus_test -j7 PYTHONMALLOC=malloc ./build_simpledbus_test/bin/simpledbus_test ``` It's important for `PYTHONMALLOC` to be set to `malloc`, otherwise the tests will fail due to Python's memory allocator from triggering false positives. Thread Sanitizer Tests [#thread-sanitizer-tests] To run the thread sanitizer tests, run the following command: ```bash cmake -S -B build_simpledbus_test -DSIMPLEDBUS_SANITIZE=Thread -DSIMPLEDBUS_TEST=ON cmake --build build_simpledbus_test -j7 ./build_simpledbus_test/bin/simpledbus_test ``` # Examples The **SimpleBLE Explorer** Android app lives in [`examples/simpleble-android`](https://github.com/simpleble/simpleble/tree/main/examples/simpleble-android). It demonstrates: * requesting Android Bluetooth permissions with the Activity Result API, * explaining Android adapter selection, * scanning and showing discovered peripherals, * connecting and disconnecting cleanly, * listing services and characteristics with capabilities, * reading a selected characteristic, * writing with request or command, * subscribing to notify or indicate characteristics, * showing errors in the UI, * stopping scans on activity pause and cleaning up notification/connection work from the ViewModel. Run it from Android Studio by opening `examples/simpleble-android`, or from the command line: ```bash cd examples/simpleble-android ./gradlew :app:assembleDebug ``` Install the APK on an Android API 31+ device with BLE support. For deterministic emulator development, build the PLAIN test build instead: ```bash ./gradlew :app:assemblePlain ``` The repository-only PLAIN build uses the same Kotlin and JNI layers with SimpleBLE's simulated backend. It supports the Explorer scan, connect, service, read, and notification workflow without Bluetooth hardware. Use the regular build on a physical phone before claiming real scan, connection, reconnection, or GATT behavior is verified. # Usage SimpleDroidBLE is the Android/Kotlin wrapper around the SimpleBLE Android backend. Use it when your application is a normal Android app and you want Kotlin APIs, Android runtime permission helpers, coroutine-friendly operations, and flows for scan, connection, and notification events. If you are already writing native C++ inside an Android app, use SimpleBLE directly and include `simpledroidbridge`. SimpleDroidBLE is for Android-first app code. Requirements [#requirements] * Android API 31 or newer. * Android Studio. * Android NDK r29 when building SimpleDroidBLE from source. * Bluetooth permissions declared in `AndroidManifest.xml` and requested at runtime. Install from Maven Central [#install-from-maven-central] Add SimpleDroidBLE to your app module: ```kotlin dependencies { implementation("org.simpleble:simpledroidble:") } ``` The artifact includes the native libraries for `arm64-v8a`, `armeabi-v7a`, `x86`, and `x86_64`. Gradle resolves SimpleDroidBridge, AndroidX Core, and Kotlin coroutines transitively. Make sure AndroidX is enabled in `gradle.properties`: ```properties android.useAndroidX=true ``` Consuming Locally [#consuming-locally] From a local checkout, use a Gradle composite build before `include(":app")`: ```kotlin includeBuild("path/to/simpleble/simpledroidble") { dependencySubstitution { substitute(module("org.simpleble:simpledroidble")).using(project(":simpledroidble")) } } include(":app") ``` Then add the dependency: ```kotlin dependencies { implementation("org.simpleble:simpledroidble:") } ``` Consumer builds always use the Android Bluetooth backend. The PLAIN backend is available only as a repository test build. Android Permissions [#android-permissions] Declare Android 12+ Bluetooth permissions in your app manifest: ```xml ``` If your app uses BLE scans to infer physical location, review Android location policy before using `neverForLocation`. Request the runtime permissions before calling adapter APIs. The permission helpers are stateless; the native library loads automatically on the first adapter call. ```kotlin class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!SimpleDroidBle.hasPermissions(this)) { SimpleDroidBle.requestPermissions(this) return } // Safe to create adapters after permissions are available. } } ``` In modern Android apps, you can also use `ActivityResultContracts.RequestMultiplePermissions()` with `SimpleDroidBle.requiredPermissions`. Peripheral-only applications should use `requiredPeripheralPermissions`, `hasPeripheralPermissions()`, and `requestPeripheralPermissions()` so they do not request scan access. Applications that support both roles can combine the two permission arrays and remove duplicates. Quickstart [#quickstart] SimpleDroidBLE exposes scan and connection events as flows. Operations that wait for Android Bluetooth callbacks are suspending and safe to call from the main thread: ```kotlin val adapter = Adapter.getAdapters().first() val scanJob = lifecycleScope.launch { adapter.onScanFound.collect { peripheral -> Log.d("SimpleBLE", "Found ${peripheral.identifier} [${peripheral.address}]") } } lifecycleScope.launch { adapter.scanFor(5_000) scanJob.cancelAndJoin() } ``` Connect and inspect GATT services: ```kotlin val peripheral = adapter.scanGetResults().first { it.isConnectable } peripheral.connect() val services = peripheral.services() services.forEach { service -> service.characteristics.forEach { characteristic -> Log.d("SimpleBLE", "${service.uuid} / ${characteristic.uuid}") } } ``` Service, characteristic, and descriptor UUIDs are `BluetoothUUID` values and can be passed directly to GATT operations. Bluetooth UUIDs and addresses expose their underlying string through `value` when needed. Read, write, and subscribe: ```kotlin val service = BluetoothUUID("0000180f-0000-1000-8000-00805f9b34fb") val characteristic = BluetoothUUID("00002a19-0000-1000-8000-00805f9b34fb") val value = peripheral.read(service, characteristic) peripheral.writeRequest(service, characteristic, byteArrayOf(0x01, 0x02)) val notifyJob = lifecycleScope.launch { peripheral.notify(service, characteristic).collect { payload -> Log.d("SimpleBLE", "Notification bytes: ${payload.size}") } } notifyJob.cancelAndJoin() // Waits for automatic unsubscribe. peripheral.disconnect() ``` Use `writeRequest` for acknowledged writes and `writeCommand` for write-without-response characteristics. Descriptor `read` and `write` overloads are also available. Host a peripheral [#host-a-peripheral] Create the peripheral from an adapter and pass any Android `Context`; SimpleDroidBLE retains only its application context. Configure the complete GATT table before `start()`: ```kotlin val adapter = Adapter.getAdapters().first() val peripheral = adapter.createLocalPeripheral(applicationContext) peripheral.addAdvertisedService( BluetoothUUID("12345678-1234-5678-1234-56789abcdef0") ) val characteristic = peripheral .addService(BluetoothUUID("12345678-1234-5678-1234-56789abcdef0")) .addCharacteristic( BluetoothUUID("12345678-1234-5678-1234-56789abcdef1"), LocalCharacteristicCapability.Read, LocalCharacteristicCapability.WriteRequest, LocalCharacteristicCapability.WriteCommand, LocalCharacteristicCapability.Notify, LocalCharacteristicCapability.Indicate ) characteristic.value = "ready".encodeToByteArray() characteristic.setWriteHandler { value -> characteristic.value = value // Store and publish the echo to subscribers. } lifecycleScope.launch { peripheral.start() } ``` `setWriteHandler` receives every accepted write on SimpleDroidBLE's callback thread and is the reliable place for application behavior. Keep it short. `onWrite` is a bounded hot flow intended for UI and telemetry-style observation. `setReadHandler` provides a dynamic value; without it, reads return `value` directly. Assigning `value` publishes the new bytes to subscribed clients when the characteristic supports notify or indicate. `onSubscribed` and `onUnsubscribed` expose subscription transitions, while `onClientConnected` and `onClientDisconnected` expose Android's GATT server connection events. Call `stop()` from your lifecycle cleanup. Advertising data, services, and characteristics cannot be changed while started; values and handlers can. SimpleDroidBLE includes the phone's system Bluetooth name. Android's public advertising API cannot set an arbitrary per-advertisement name. Peripheral mode currently uses legacy advertising, including its 31-byte payload limits, and rejects prepared writes. Use regular write requests or write commands within the negotiated GATT payload size. Lifecycle And Threading [#lifecycle-and-threading] * Create adapters only after runtime permissions have been granted. * Stop active scans from `onPause()` or an equivalent lifecycle callback. * Keep notification collection in a lifecycle-aware coroutine. Cancelling the `notify` or `indicate` flow waits for native unsubscribe before collection finishes. * Stop a local peripheral when its owning lifecycle ends. `start()` and `stop()` dispatch blocking Bluetooth setup and teardown to `Dispatchers.IO`. * Only one `notify` or `indicate` collector may be active for a characteristic. Cancel and join the old collector before starting another. * Notification and indication flows use bounded buffers and fail with `SimpleDroidBleException` if a collector cannot keep up. Place `buffer(256)` directly after `notify` or `indicate` to increase capacity, or explicitly opt into dropping with `buffer(256, onBufferOverflow = BufferOverflow.DROP_OLDEST)` or `BufferOverflow.DROP_LATEST`. * Flow collectors resume in their own coroutine context. Keep payload parsing short; blocking GATT operations dispatch to `Dispatchers.IO` internally. * Android exposes one active Bluetooth adapter through the backend today. `Adapter.getAdapters()` returns a list for API consistency. Error Handling [#error-handling] Native SimpleBLE operation failures are surfaced as `SimpleDroidBleException`, an unchecked Kotlin/Java exception. Wrap scan, connect, read, write, notify, unsubscribe, and disconnect operations in normal Kotlin `try`/`catch` blocks and show errors to users. Bluetooth can fail because permissions are missing, Bluetooth is off, a device disappeared, a GATT operation timed out, or a characteristic does not support the requested operation. Current Limitations [#current-limitations] * The JNI layer still keeps process-local native caches for adapters, peripherals, and active notification callbacks. Treat adapter and peripheral objects as app-process objects, not serializable handles. * Reconnection works through the backend after disconnect, but apps should refresh services after each new connection. * Android address type is reported as unspecified on API levels where Android does not expose it. * Programmatic unpairing is intentionally not relied on because Android restricts bond removal. # Examples To learn how to use SimpleJavaBLE, please refer to the [examples](https://github.com/simpleble/simpleble/tree/main/examples/simplejavable) provided in the repository. The example project includes: * **ScanExample**: Scan for nearby peripherals and print advertisement details. * **ConnectExample**: Scan, choose a connectable peripheral, connect, and list services and characteristics. * **NotifyExample**: Connect to a peripheral, choose a characteristic, subscribe to notifications, then unsubscribe. For setup details, see [Usage](./usage), [Installation](../installation), and [Quickstart](../quickstart). # Usage SimpleJavaBLE exposes SimpleBLE to JVM applications through a Java API backed by the native SimpleBLE library. Requirements [#requirements] * JDK 17 or newer. * CMake 3.21 or newer. * The platform requirements from [Installation](../installation#platform-prerequisites). * A native library artifact for the platform and architecture where the JVM runs. Install from Maven Central [#install-from-maven-central] Add SimpleJavaBLE to your Gradle dependencies: ```kotlin dependencies { implementation("org.simpleble:simplejavable:") } ``` The Maven artifact bundles the supported Windows, macOS, and Linux native libraries in the JAR. Build from source [#build-from-source] From a local checkout: ```bash cd simplejavable/java ../../utils/gradle/gradlew build -PbuildFromCMake ``` This builds the Java classes and asks Gradle to build the native library through the CMake project under `simplejavable/cpp`. The generated JAR is written under: ```text simplejavable/java/build/libs ``` Native library loading [#native-library-loading] SimpleJavaBLE loads the `simplejavable` native library at runtime. The Gradle build can package native libraries into the JAR, or you can provide native libraries separately and make them visible to the JVM. If you already built native libraries elsewhere, pass their location to Gradle: ```bash ../../utils/gradle/gradlew build -PnativeLibPath=/path/to/native/libs ``` First scan [#first-scan] ```java import org.simplejavable.Adapter; import org.simplejavable.Peripheral; import java.util.List; public class ScanExample { public static void main(String[] args) throws Exception { List adapters = Adapter.getAdapters(); if (adapters.isEmpty()) { System.err.println("No Bluetooth adapters found."); return; } Adapter adapter = adapters.get(0); adapter.scanFor(5000); for (Peripheral peripheral : adapter.scanGetResults()) { System.out.println(peripheral.getIdentifier() + " [" + peripheral.getAddress() + "]"); } } } ``` For a fuller walkthrough, see [Quickstart](../quickstart) and the [SimpleJavaBLE Examples](./examples). # Examples To learn how to use SimplePyBLE, please refer to the [examples](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble) provided in the repository. The following list briefly describes each example provided: * **[list\_adapters](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/list_adapters.py)**: List all available adapters. * **[scan](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/scan.py)**: Scan for nearby BLE devices. * **[connect](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/connect.py)**: Connect to a BLE device and list its services and characteristics. * **[read](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/read.py)**: Read a characteristic's value. * **[write](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/write.py)**: Write a characteristic's value. * **[notify](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/notify.py)**: Enable notifications on a characteristic. Asynchronous API [#asynchronous-api] * **[scan\_async](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/scan_async.py)**: Scan for nearby BLE devices using `asyncio`. * **[notify\_async](https://github.com/simpleble/simpleble/tree/main/examples/simplepyble/notify_async.py)**: Enable notifications on a characteristic using `asyncio` and `async def` callbacks. # Usage SimpleBLE works on Windows, Linux and MacOS. Please follow the instructions below to build and run SimplePyBLE in your specific environment. Installing from PyPI [#installing-from-pypi] The fastest way to get going with SimplePyBLE is to install it from PyPI: ```bash pip install simplepyble ``` Please take into consideration that when using this library on Linux, you will need to have the following dependencies installed: ```bash sudo apt-get install libdbus-1-dev ``` Installing locally [#installing-locally] If you wish to install SimplePyBLE locally, either for development purposes or to access the latest features, you can do so by cloning the repository and installing it with pip: ```bash cd /simplepyble pip install . ``` Asynchronous API [#asynchronous-api] SimplePyBLE provides an asynchronous API via the `simplepyble.aio` module. This module is designed to work with `asyncio` and provides a more idiomatic way to handle asynchronous operations in Python. To use the asynchronous API, simply import from `simplepyble.aio`: ```python import asyncio from simplepyble.aio import Adapter async def main(): adapters = Adapter.get_adapters() if not adapters: return adapter = adapters[0] async with adapter: await adapter.scan_for(5000) peripherals = adapter.scan_get_results() for peripheral in peripherals: print(f"Found: {peripheral.identifier()} [{peripheral.address()}]") if __name__ == "__main__": asyncio.run(main()) ``` All blocking calls from the synchronous API are available as coroutines in the `aio` module, and callbacks can be either regular functions or coroutines. For more examples, check out the [Examples](./examples) section. # Examples To learn how to use SimpleRsBLE, please refer to the [examples](https://github.com/simpleble/simpleble/tree/main/examples/simplersble) provided in the repository. The example crate includes: * **list\_adapters**: Print the adapters visible to SimpleRsBLE. * **scan**: Scan for nearby peripherals and print advertisement details. * **connect**: Scan, choose a peripheral, connect, and list services and characteristics. * **notify**: Connect to a peripheral, subscribe to notifications, then unsubscribe. Run an example from the repository root: ```bash cargo run --manifest-path examples/simplersble/Cargo.toml --bin scan ``` For setup details, see [Usage](./usage), [Installation](../installation), and [Quickstart](../quickstart). # Usage SimpleRsBLE exposes SimpleBLE to Rust applications with Rust-facing types and async event streams. Requirements [#requirements] * Rust 1.82 or newer. * CMake 3.21 or newer. * The platform requirements from [Installation](../installation#platform-prerequisites). * On Linux, DBus headers: ```bash sudo apt-get install libdbus-1-dev ``` Install from crates.io [#install-from-cratesio] ```bash cargo add simplersble ``` The crate builds the native SimpleBLE components during the Cargo build. Use a local checkout [#use-a-local-checkout] When developing SimpleRsBLE or testing unreleased changes: ```toml [dependencies] simplersble = { path = "path/to/simpleble" } ``` First scan [#first-scan] ```rust #[tokio::main] async fn main() { let mut adapters = simplersble::Adapter::get_adapters().unwrap(); if adapters.is_empty() { println!("No Bluetooth adapters found."); return; } let adapter = adapters.remove(0); adapter.scan_for(5000).unwrap(); for peripheral in adapter.scan_get_results().unwrap() { println!( "{} [{}]", peripheral.identifier().unwrap(), peripheral.address().unwrap() ); } } ``` For event streams and notification examples, see [Quickstart](../quickstart), [Read, Write, Notify](../recipes/read-write-notify), and the [SimpleRsBLE Examples](./examples). # Installation SimpleBLE for Unreal can be installed from Fab or copied into an Unreal project as a source plugin. Install From Fab [#install-from-fab] 1. Install the SimpleBLE plugin for your Unreal Engine version from Fab or the Epic Games Launcher. 2. Open your Unreal project. 3. Go to `Edit > Plugins`. 4. Search for `SimpleBLE`. 5. Enable the plugin. 6. Restart the editor when prompted. After restart, Blueprint nodes are available under the `SimpleBLE`, `SimpleBLE|Adapter`, `SimpleBLE|Peripheral`, `SimpleBLE|Backend`, and `SimpleBLE|Payload` categories. Install From Source [#install-from-source] Copy the `SimpleBLE` plugin folder into your project: ```text YourProject/ Plugins/ SimpleBLE/ SimpleBLE.uplugin Source/ Config/ Docs/ ``` Then regenerate project files if you use an IDE, build your project, and enable the plugin from `Edit > Plugins`. C++ Module Setup [#c-module-setup] To call the plugin from a game module, add `SimpleBLE` as a dependency in your module `.Build.cs` file: ```csharp PublicDependencyModuleNames.AddRange( new string[] { "Core", "CoreUObject", "Engine", "SimpleBLE" } ); ``` Include the public headers you need: ```cpp #include "SimpleBLE/Manager.h" #include "SimpleBLE/Adapter.h" #include "SimpleBLE/Peripheral.h" #include "SimpleBLE/PayloadLibrary.h" ``` Verify The Plugin [#verify-the-plugin] The quickest smoke test is to create a `Simple BLE Manager` object in Blueprint, call `Initialize`, and run a short scan. If initialization fails, check that Bluetooth is enabled, the device has a BLE-capable adapter, the packaged plugin includes `Source/ThirdParty/SimpleBLE/Lib` for the target platform, and mobile runtime permissions have been granted. # Platform Notes The SimpleBLE Unreal plugin is configured for Windows 64-bit, macOS, iOS, Android, and Linux. Platform Bluetooth permissions and system services are configured by the consuming Unreal project. Android [#android] The plugin adds these manifest entries through `Source/SimpleBLE/Private/Android/SimpleBLE_APL.xml`: * `android.permission.BLUETOOTH` * `android.permission.BLUETOOTH_ADMIN` * `android.permission.BLUETOOTH_SCAN` * `android.permission.BLUETOOTH_CONNECT` * `android.hardware.bluetooth_le` For Android 12 and newer, request runtime permission for `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` before scanning or connecting. Blueprint projects commonly handle this with Unreal's Android Permission plugin. The plugin marks `BLUETOOTH_SCAN` with `neverForLocation`. If your application uses BLE scanning for location-like behavior, review Android Bluetooth and location policy requirements before shipping. iOS [#ios] The plugin links `CoreBluetooth`. Add Bluetooth usage text to your app plist, for example through `Project Settings > iOS > Additional Plist Data`: ```xml NSBluetoothAlwaysUsageDescription This app uses Bluetooth to connect to nearby BLE devices. ``` If you need background BLE behavior, configure the relevant iOS background modes in your Unreal project. The plugin does not enable background modes by default. macOS [#macos] Recent macOS versions may require Bluetooth privacy usage text in the packaged application plist. Add a clear `NSBluetoothAlwaysUsageDescription` string when packaging for macOS distribution. Linux [#linux] Linux runtime targets need the normal BlueZ and D-Bus Bluetooth stack installed and running. The user running the application must have permission to access Bluetooth through the host distribution's Bluetooth configuration. Windows [#windows] Bluetooth must be enabled and the target device must be visible to the Windows Bluetooth stack. Some BLE operations depend on adapter and device driver support exposed by the operating system. Troubleshooting [#troubleshooting] If `Initialize` returns false, check that Bluetooth is enabled, the host has a BLE-capable adapter, native SimpleBLE binaries are packaged for the current platform, and required mobile permissions have been granted. If no devices are found, confirm the peripheral is advertising, increase the scan timeout, move the peripheral closer, and verify Android permissions or Linux BlueZ/D-Bus services as appropriate. If read, write, or notification operations fail, confirm the service and characteristic UUIDs, check characteristic capabilities, use the write mode supported by the device, and keep the manager or peripheral UObject referenced while subscribed. For binary protocols, prefer the lower-level peripheral API and `USimpleBLEPayloadLibrary` over the convenience manager's UTF-8 string helpers. # Quickstart This quickstart covers the two main ways to use SimpleBLE from Unreal: the convenience manager and the lower-level adapter/peripheral API. Blueprint Manager Flow [#blueprint-manager-flow] Use `USimpleBLEManager` when you want the shortest Blueprint path from scan to connect: 1. Create a `Simple BLE Manager` object and keep it referenced while Bluetooth work is active. 2. Bind `On Device Found`, `On Scan Finished`, and `On Notify Data`. 3. Call `Initialize`. 4. If `Initialize` returns `true`, call `Start Scan` with a timeout in milliseconds. 5. Use `Get Found Devices` to show names or addresses to the player. 6. Call `Connect To Device` with the selected display name, identifier, or address. 7. Call `Read Characteristic`, `Write Characteristic`, `Subscribe Notify`, or `Unsubscribe Notify`. 8. Call `Disconnect` when finished. The manager read and write helpers treat characteristic values as UTF-8 strings. For binary protocols, use the lower-level peripheral API and `USimpleBLEPayloadLibrary`. Blueprint Adapter Flow [#blueprint-adapter-flow] Use `USimpleBLEBackend`, `USimpleBLEAdapter`, and `USimpleBLEPeripheral` when you need complete BLE metadata, byte arrays, or structured error information: 1. Call `Get Backends` or `Get Adapters`. 2. Choose an adapter. 3. Bind `On Peripheral Found`, `On Peripheral Updated`, and `On Scan Finished`. 4. Call `Scan For`, `Start Scan`, or `Stop Scan`. 5. Use `Get Scan Results` to inspect `USimpleBLEPeripheral` objects. 6. Call `Get Info` on each peripheral to read identifier, address, RSSI, MTU, connectable state, paired state, and manufacturer data. 7. Call `Connect`. 8. Call `Get Services`. 9. Inspect characteristic capabilities such as `bCanRead`, `bCanWriteRequest`, `bCanWriteCommand`, `bCanNotify`, and `bCanIndicate`. 10. Call `Read Characteristic`, `Write Request`, `Write Command`, `Notify`, `Indicate`, or `Unsubscribe`. C++ Manager Flow [#c-manager-flow] Create and keep a `USimpleBLEManager` reference on a UObject that outlives the scan and connection. ```cpp #include "SimpleBLE/Manager.h" void UMyBluetoothController::StartBluetoothScan() { Manager = NewObject(this); Manager->OnDeviceFound.AddDynamic(this, &UMyBluetoothController::HandleDeviceFound); Manager->OnScanFinished.AddDynamic(this, &UMyBluetoothController::HandleScanFinished); Manager->OnNotifyData.AddDynamic(this, &UMyBluetoothController::HandleNotifyData); if (!Manager->Initialize()) { UE_LOG(LogTemp, Warning, TEXT("SimpleBLE could not initialize.")); return; } Manager->StartScan(5000); } ``` Delegate handlers must be `UFUNCTION` methods: ```cpp UFUNCTION() void HandleDeviceFound(const FString& DeviceName); UFUNCTION() void HandleScanFinished(); UFUNCTION() void HandleNotifyData( const FString& ServiceUUID, const FString& CharacteristicUUID, const TArray& Data); ``` Connect by a discovered display name, identifier, or address: ```cpp if (Manager->ConnectToDevice(DeviceName)) { const FString Value = Manager->ReadCharacteristic(ServiceUUID, CharacteristicUUID); Manager->WriteCharacteristic(ServiceUUID, CharacteristicUUID, TEXT("hello")); Manager->SubscribeNotify(ServiceUUID, CharacteristicUUID); } ``` C++ Lower-Level API [#c-lower-level-api] The lower-level API returns structured result types and byte payloads: ```cpp #include "SimpleBLE/Adapter.h" #include "SimpleBLE/Manager.h" #include "SimpleBLE/PayloadLibrary.h" #include "SimpleBLE/Peripheral.h" void UMyBluetoothController::ScanWithAdapter() { Manager = NewObject(this); const TArray Adapters = Manager->GetAdapters(); if (Adapters.IsEmpty()) { return; } Adapter = Adapters[0]; Adapter->OnPeripheralFound.AddDynamic(this, &UMyBluetoothController::HandlePeripheralFound); Adapter->OnScanFinished.AddDynamic(this, &UMyBluetoothController::HandleAdapterScanFinished); Adapter->ScanFor(5000); } ``` Read and write bytes through `USimpleBLEPeripheral`: ```cpp void UMyBluetoothController::UsePeripheral(USimpleBLEPeripheral* Peripheral) { const FSimpleBLEOperationResult ConnectResult = Peripheral->Connect(); if (!ConnectResult.bSuccess) { UE_LOG(LogTemp, Warning, TEXT("%s"), *ConnectResult.Error.Message); return; } const FSimpleBLEDataResult ReadResult = Peripheral->ReadCharacteristic(ServiceUUID, CharacteristicUUID); if (ReadResult.bSuccess) { const FString Hex = USimpleBLEPayloadLibrary::BytesToHex(ReadResult.Data, true); UE_LOG(LogTemp, Log, TEXT("Read bytes: %s"), *Hex); } const TArray Payload = USimpleBLEPayloadLibrary::Utf8StringToBytes(TEXT("hello")); const FSimpleBLEOperationResult WriteResult = Peripheral->WriteRequest(ServiceUUID, CharacteristicUUID, Payload); if (!WriteResult.bSuccess) { UE_LOG(LogTemp, Warning, TEXT("%s"), *WriteResult.Error.Message); } } ``` Payload Helpers And Results [#payload-helpers-and-results] `USimpleBLEPayloadLibrary` provides helpers for UTF-8 strings and hex-encoded device payloads: * `Utf8 String To Bytes` * `Bytes To Utf8 String` * `Hex To Bytes` * `Bytes To Hex` Lower-level operations return `FSimpleBLEOperationResult`, `FSimpleBLEBoolResult`, or `FSimpleBLEDataResult`. Check `bSuccess` first, then inspect `Error.Code` and `Error.Message` when an operation fails. Keep manager, adapter, and peripheral UObjects referenced while operations are active. Scan callbacks and notification callbacks are delivered back to the game thread. # Usage SimpleBLE for Unreal Engine provides Blueprint and C++ access to Bluetooth Low Energy adapters, peripherals, services, characteristics, descriptors, notifications, and payload conversion helpers. Use it when an Unreal project needs to scan for nearby BLE peripherals, connect to a device, inspect services and characteristics, read or write characteristic values, or subscribe to notifications. API Shape [#api-shape] The plugin exposes Unreal-native UObject and USTRUCT types over the native SimpleBLE runtime: * `USimpleBLEManager` is the convenience entry point for common Blueprint workflows such as initialize, scan, connect by display name, read and write UTF-8 text, and subscribe to notifications. * `USimpleBLEBackend`, `USimpleBLEAdapter`, and `USimpleBLEPeripheral` provide lower-level access to adapters, peripheral metadata, byte payloads, services, characteristics, descriptors, and connection events. * Services, characteristics, descriptors, manufacturer data, errors, and operation results are reflected as Unreal `USTRUCT` snapshots. * Characteristic and descriptor payloads use `TArray`. `USimpleBLEPayloadLibrary` provides UTF-8 and hex conversion helpers. Public Headers [#public-headers] The public C++ headers are grouped under `SimpleBLE/`: ```cpp #include "SimpleBLE/Manager.h" #include "SimpleBLE/Adapter.h" #include "SimpleBLE/Peripheral.h" #include "SimpleBLE/PayloadLibrary.h" ``` Blueprint Categories [#blueprint-categories] After enabling the plugin and restarting the editor, Blueprint nodes appear under: * `SimpleBLE` * `SimpleBLE|Adapter` * `SimpleBLE|Peripheral` * `SimpleBLE|Backend` * `SimpleBLE|Payload` Choosing An API [#choosing-an-api] Use `USimpleBLEManager` for the shortest path from scan to connection, especially in Blueprint projects. Use the lower-level adapter and peripheral API when you need byte arrays, full service metadata, structured operation errors, or capabilities such as write-with-response versus write-without-response. Package Contents [#package-contents] The Fab package includes the plugin descriptor, source module, config, third-party native binaries, documentation, icon resources, and third-party notices. The `Content/` directory is included for the Fab code plugin package structure; the plugin does not require Unreal assets to function. # Advertise, Serve, Publish import { Callout } from 'fumadocs-ui/components/callout'; This is the peripheral-mode walkthrough: this host advertises, serves a GATT table, and publishes characteristic updates to connected clients. `SimpleBLE::Peripheral` is a remote device discovered while acting as a central. `SimpleBLE::Local::Peripheral` is this host. Create the local peripheral from the same `Adapter` you would use to scan. The C++ API can host a local peripheral on every supported platform. Native Android applications must provide their application context with `SimpleBLE::Advanced::Android::set_context()` before creating the local peripheral. SimpleDroidBLE handles the context automatically and provides a Kotlin API. If you still need to install SimpleBLE or create the CMake project, start with [Getting Started](../tutorial). The [Advertise](../../recipes/advertise) recipe is the short form of this flow. Lifecycle [#lifecycle] Configure first, then start: 1. Create a local peripheral from an adapter. 2. Set advertisement data and add services and characteristics. 3. Call `start()` to serve the GATT application and begin advertising. 4. Update characteristic values and observe clients while started. 5. Call `stop()` when finished. Services, characteristics, and advertising data must be configured before `start()` and cannot be changed while the peripheral is started. `set_value()` and the characteristic callbacks are the supported way to change data after that. Starting twice is a no-op; `stop()` while already stopped is also a no-op. Destroying the object stops it. ```cpp auto adapters = SimpleBLE::Adapter::get_adapters(); auto adapter = adapters.front(); auto peripheral = adapter.create_local_peripheral(); peripheral.add_advertised_service("12345678-1234-5678-1234-56789abcdef0"); auto service = peripheral.add_service("12345678-1234-5678-1234-56789abcdef0"); auto characteristic = service.add_characteristic( "12345678-1234-5678-1234-56789abcdef1", {SimpleBLE::Local::CharacteristicCapability::READ, SimpleBLE::Local::CharacteristicCapability::WRITE_REQUEST, SimpleBLE::Local::CharacteristicCapability::NOTIFY}); characteristic.set_value(SimpleBLE::ByteArray("ready")); peripheral.start(); ``` `is_started()` reports whether the GATT application is registered. `is_advertising()` reports whether the advertisement is currently active. Advertised services [#advertised-services] Add configured service UUIDs to the advertisement directly on the local peripheral: ```cpp peripheral.add_advertised_service("12345678-1234-5678-1234-56789abcdef0"); ``` If no advertised services are added explicitly, SimpleBLE advertises the service UUIDs configured on the local peripheral when the platform supports doing so. Pass a vector to add several UUIDs in one call. The portable advertisement does not select a local name. Windows controls the name in its discoverable advertisement, and Android includes the adapter's system Bluetooth name. Neither platform exposes an arbitrary per-advertisement name through its public GATT advertising API. BlueZ and CoreBluetooth do support an advertisement-specific name. Configure that platform capability before `start()` without changing the adapter alias, system Bluetooth name, or GAP Device Name: ```cpp #include #if defined(__linux__) && !defined(__ANDROID__) SimpleBLE::Advanced::Linux::set_advertisement_local_name(peripheral, "SimpleBLE Peripheral"); #elif TARGET_OS_OSX SimpleBLE::Advanced::MacOS::set_advertisement_local_name(peripheral, "SimpleBLE Peripheral"); #elif TARGET_OS_IOS SimpleBLE::Advanced::iOS::set_advertisement_local_name(peripheral, "SimpleBLE Peripheral"); #endif ``` CoreBluetooth advertises data on a best-effort basis, and iOS omits the local name while the app is in the background. BlueZ may shorten a name that does not fit. Pass `std::nullopt` to remove an override. Services and characteristics [#services-and-characteristics] `add_service()` adds a primary GATT service. `add_characteristic()` takes a `std::set` of capabilities, so each flag is unique: | Capability | Remote centrals can | | --------------- | -------------------------- | | `READ` | Read the characteristic | | `WRITE_REQUEST` | Write with a response | | `WRITE_COMMAND` | Write without a response | | `NOTIFY` | Subscribe to notifications | | `INDICATE` | Subscribe to indications | A characteristic needs at least one capability. Pass the set as an initializer list: ```cpp auto characteristic = service.add_characteristic( CHARACTERISTIC_UUID, {SimpleBLE::Local::CharacteristicCapability::READ, SimpleBLE::Local::CharacteristicCapability::WRITE_REQUEST, SimpleBLE::Local::CharacteristicCapability::WRITE_COMMAND, SimpleBLE::Local::CharacteristicCapability::NOTIFY, SimpleBLE::Local::CharacteristicCapability::INDICATE}); ``` Values and callbacks [#values-and-callbacks] Without a read callback, reads return `value()`. Incoming writes always update `value()` before the write callback runs. Optional callbacks customize or observe that behavior: * `set_callback_on_read` — return the bytes to send for this read * `set_callback_on_write` — observe the bytes after they have been stored. Call `set_value()` from the callback only when you want to publish or echo that value to subscribed clients. ```cpp characteristic.set_callback_on_write([&characteristic](SimpleBLE::ByteArray value) { std::cout << "Write: " << value << std::endl; // Echo the written value to subscribed clients. characteristic.set_value(std::move(value)); }); ``` Publish updates [#publish-updates] `set_value()` is how this host publishes a new characteristic value. When the characteristic has `NOTIFY` or `INDICATE` capability, SimpleBLE also pushes that value to subscribed clients on a best-effort basis. ```cpp characteristic.set_value(SimpleBLE::ByteArray("tick")); ``` `set_callback_on_subscribed` runs when the first client subscribes. `set_callback_on_unsubscribed` runs when the last client unsubscribes. ```cpp characteristic.set_callback_on_subscribed([]() { std::cout << "Client subscribed." << std::endl; }); characteristic.set_callback_on_unsubscribed([]() { std::cout << "Client unsubscribed." << std::endl; }); ``` Use **publish** for this path. `start()` serves the GATT application and begins advertising; it is not the same as publishing a characteristic value. Observe clients [#observe-clients] Register callbacks before `start()` if you want the first connection: ```cpp peripheral.set_callback_on_client_connected( [](SimpleBLE::BluetoothAddress address) { std::cout << "Client connected: " << address << std::endl; }); peripheral.set_callback_on_client_disconnected( [](SimpleBLE::BluetoothAddress address) { std::cout << "Client disconnected: " << address << std::endl; }); ``` On platforms that do not expose a Bluetooth address, the value is a platform-specific identifier for the remote client. CoreBluetooth does not expose peripheral-role connection events on macOS. SimpleBLE reports a client as connected when it first subscribes to a characteristic and disconnected when its final subscription ends. On Windows, WinRT exposes the client session after a client first reads, writes, or subscribes to a hosted characteristic. A connection that performs no GATT operation may not produce a client-connected callback. Callbacks follow the same threading rules as the rest of SimpleBLE. See [Concurrency](../../fundamentals/concurrency). Scan and advertise together [#scan-and-advertise-together] SimpleBLE permits the same adapter to scan while a local peripheral is started when the platform Bluetooth stack and controller support concurrent scanning and advertising. On Linux, BlueZ and the controller must support both operations at the same time. On Android, support depends on the phone's controller and vendor Bluetooth stack. Incoming clients of the local peripheral are reported through the client callbacks above. They are not devices you should `connect()` to as a central. Full application [#full-application] The repository example [`examples/simpleble/src/peripheral.cpp`](https://github.com/simpleble/simpleble/blob/main/examples/simpleble/src/peripheral.cpp) is a complete program. A compact version: ```cpp #include #include #include #include #include #include #include #include namespace { constexpr auto SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0"; constexpr auto CHARACTERISTIC_UUID = "12345678-1234-5678-1234-56789abcdef1"; std::atomic_bool running{true}; void signal_handler(int) { running = false; } } // namespace int main() { std::signal(SIGINT, signal_handler); auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { std::cerr << "No Bluetooth adapters found." << std::endl; return EXIT_FAILURE; } auto adapter = adapters.front(); auto peripheral = adapter.create_local_peripheral(); peripheral.add_advertised_service(SERVICE_UUID); auto service = peripheral.add_service(SERVICE_UUID); auto characteristic = service.add_characteristic( CHARACTERISTIC_UUID, {SimpleBLE::Local::CharacteristicCapability::READ, SimpleBLE::Local::CharacteristicCapability::WRITE_REQUEST, SimpleBLE::Local::CharacteristicCapability::NOTIFY}); characteristic.set_value(SimpleBLE::ByteArray("ready")); characteristic.set_callback_on_write([&characteristic](SimpleBLE::ByteArray value) { std::cout << "Write: " << value << std::endl; // Echo the written value to subscribed clients. characteristic.set_value(std::move(value)); }); peripheral.set_callback_on_client_connected( [](SimpleBLE::BluetoothAddress address) { std::cout << "Client connected: " << address << std::endl; }); peripheral.start(); std::cout << "Local peripheral is advertising. Press Ctrl+C to stop." << std::endl; while (running) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } characteristic.set_callback_on_write({}); peripheral.stop(); return EXIT_SUCCESS; } ``` Build it with the other SimpleBLE examples: ```bash cmake -S /examples/simpleble -B build_simpleble_examples -DSIMPLEBLE_LOCAL=ON cmake --build build_simpleble_examples -j7 ``` Where to next? [#where-to-next] * [Advertise](../../recipes/advertise) for a shorter recipe form of this flow * [Scan, Connect, Read](../central/scan-connect-read) to use the same adapter as a central * [API reference](../api#local-api) for `SimpleBLE::Local` types * [Platform notes](../platform_notes) for platform-specific behavior * [Examples](../examples) for the programs in the repository # Scan, Connect, Read This is the central-mode walkthrough: this host scans, connects to a remote `SimpleBLE::Peripheral`, and reads a characteristic. By the end, your app will: * Scan for nearby peripherals * Select a device to connect to * List readable characteristics * Read a characteristic value multiple times If you still need to install SimpleBLE or create the CMake project, start with [Getting Started](../tutorial). The snippets below assume that project, with `src/main.cpp` as the application. If you are new to BLE or platform-specific permissions, it is worth taking a quick look at [Bluetooth LE Basics](../../fundamentals/ble_basics) and [Bluetooth Permissions](../../fundamentals/permissions) before you begin. Build a BLE reader [#build-a-ble-reader] We will implement the BLE reader step by step, starting with basic Bluetooth access and moving through device discovery, connection, and finally reading data. Step 1: Initialize the Bluetooth adapter [#step-1-initialize-the-bluetooth-adapter] We will begin by verifying that Bluetooth is available and that the operating system has granted your app access to it: ```cpp #include #include int main() { if (!SimpleBLE::Adapter::bluetooth_enabled()) { std::cerr << "Bluetooth is not enabled or permission has not been granted." << std::endl; return EXIT_FAILURE; } std::cout << "Bluetooth is available." << std::endl; return EXIT_SUCCESS; } ``` Step 2: Get a Bluetooth adapter [#step-2-get-a-bluetooth-adapter] With Bluetooth access confirmed, we need to discover the adapters available on the machine. SimpleBLE manages multiple backends representing different under-the-hood implementations (such as BlueZ, CoreBluetooth, WinRT, or Dongl), but provides a consolidated method to easily fetch all available adapters across every active backend. To keep things simple, we will just use the first adapter that we find: ```cpp auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { std::cerr << "No Bluetooth adapters found." << std::endl; return EXIT_FAILURE; } auto adapter = adapters[0]; std::cout << "Using adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl; ``` Step 3: Scan for peripherals [#step-3-scan-for-peripherals] Now that we have an active adapter, we can use it to scan for nearby devices. We'll use callbacks to track discovered devices and store connectable ones in a vector: ```cpp std::vector peripherals; adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; }); adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; }); adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; if (peripheral.is_connectable()) { peripherals.push_back(peripheral); } }); adapter.scan_for(5000); ``` Step 4: Select a device [#step-4-select-a-device] Once the scan is complete, we display the discovered connectable devices and ask the user to select one: ```cpp if (peripherals.empty()) { std::cerr << "No connectable peripherals found." << std::endl; return EXIT_FAILURE; } std::cout << "Connectable devices:" << std::endl; for (std::size_t i = 0; i < peripherals.size(); i++) { std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]" << std::endl; } std::size_t peripheral_index = 0; std::cout << "Select a device to connect to (0-" << peripherals.size() - 1 << "): "; std::cin >> peripheral_index; if (!std::cin || peripheral_index >= peripherals.size()) { std::cerr << "Invalid peripheral selection." << std::endl; return EXIT_FAILURE; } auto peripheral = peripherals[peripheral_index]; ``` Step 5: Connect and discover characteristics [#step-5-connect-and-discover-characteristics] Once a peripheral has been selected, the next step is to establish a connection and inspect its services to find characteristics that support data reads: ```cpp std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; peripheral.connect(); std::vector> readable_characteristics; for (const auto& service : peripheral.services()) { for (const auto& characteristic : service.characteristics()) { if (characteristic.can_read()) { readable_characteristics.emplace_back(service.uuid(), characteristic.uuid()); } } } if (readable_characteristics.empty()) { std::cerr << "The peripheral has no readable characteristics." << std::endl; peripheral.disconnect(); return EXIT_FAILURE; } std::cout << "Readable characteristics:" << std::endl; for (std::size_t i = 0; i < readable_characteristics.size(); i++) { std::cout << "[" << i << "] " << readable_characteristics[i].first << " " << readable_characteristics[i].second << std::endl; } ``` Step 6: Read data from the device [#step-6-read-data-from-the-device] For the final step, we will let the user choose one of the discovered readable characteristics and read its value five times: ```cpp std::size_t characteristic_index = 0; std::cout << "Select a characteristic to read (0-" << readable_characteristics.size() - 1 << "): "; std::cin >> characteristic_index; if (!std::cin || characteristic_index >= readable_characteristics.size()) { std::cerr << "Invalid characteristic selection." << std::endl; peripheral.disconnect(); return EXIT_FAILURE; } const auto& [service_uuid, characteristic_uuid] = readable_characteristics[characteristic_index]; for (std::size_t i = 0; i < 5; i++) { SimpleBLE::ByteArray data = peripheral.read(service_uuid, characteristic_uuid); std::cout << "Read " << i + 1 << ": " << data << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1)); } peripheral.disconnect(); ``` Full Application Code [#full-application-code] Here is the complete `src/main.cpp` file combining all the steps above: ```cpp #include #include #include #include #include #include using namespace std::chrono_literals; int main() { if (!SimpleBLE::Adapter::bluetooth_enabled()) { std::cerr << "Bluetooth is not enabled or permission has not been granted." << std::endl; return EXIT_FAILURE; } auto adapters = SimpleBLE::Adapter::get_adapters(); if (adapters.empty()) { std::cerr << "No Bluetooth adapters found." << std::endl; return EXIT_FAILURE; } auto adapter = adapters[0]; std::cout << "Using adapter: " << adapter.identifier() << " [" << adapter.address() << "]" << std::endl; std::vector peripherals; adapter.set_callback_on_scan_start([]() { std::cout << "Scan started." << std::endl; }); adapter.set_callback_on_scan_stop([]() { std::cout << "Scan stopped." << std::endl; }); adapter.set_callback_on_scan_found([&](SimpleBLE::Peripheral peripheral) { std::cout << "Found device: " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; if (peripheral.is_connectable()) { peripherals.push_back(peripheral); } }); adapter.scan_for(5000); if (peripherals.empty()) { std::cerr << "No connectable peripherals found." << std::endl; return EXIT_FAILURE; } std::cout << "Connectable devices:" << std::endl; for (std::size_t i = 0; i < peripherals.size(); i++) { std::cout << "[" << i << "] " << peripherals[i].identifier() << " [" << peripherals[i].address() << "]" << std::endl; } std::size_t peripheral_index = 0; std::cout << "Select a device to connect to (0-" << peripherals.size() - 1 << "): "; std::cin >> peripheral_index; if (!std::cin || peripheral_index >= peripherals.size()) { std::cerr << "Invalid peripheral selection." << std::endl; return EXIT_FAILURE; } auto peripheral = peripherals[peripheral_index]; std::cout << "Connecting to " << peripheral.identifier() << " [" << peripheral.address() << "]" << std::endl; peripheral.connect(); std::vector> readable_characteristics; for (const auto& service : peripheral.services()) { for (const auto& characteristic : service.characteristics()) { if (characteristic.can_read()) { readable_characteristics.emplace_back(service.uuid(), characteristic.uuid()); } } } if (readable_characteristics.empty()) { std::cerr << "The peripheral has no readable characteristics." << std::endl; peripheral.disconnect(); return EXIT_FAILURE; } std::cout << "Readable characteristics:" << std::endl; for (std::size_t i = 0; i < readable_characteristics.size(); i++) { std::cout << "[" << i << "] " << readable_characteristics[i].first << " " << readable_characteristics[i].second << std::endl; } std::size_t characteristic_index = 0; std::cout << "Select a characteristic to read (0-" << readable_characteristics.size() - 1 << "): "; std::cin >> characteristic_index; if (!std::cin || characteristic_index >= readable_characteristics.size()) { std::cerr << "Invalid characteristic selection." << std::endl; peripheral.disconnect(); return EXIT_FAILURE; } const auto& [service_uuid, characteristic_uuid] = readable_characteristics[characteristic_index]; for (std::size_t i = 0; i < 5; i++) { SimpleBLE::ByteArray data = peripheral.read(service_uuid, characteristic_uuid); std::cout << "Read " << i + 1 << ": " << data << std::endl; std::this_thread::sleep_for(1s); } peripheral.disconnect(); return EXIT_SUCCESS; } ``` How the example works [#how-the-example-works] Now that the full sample is in place, let's walk through the complete flow once more: 1. `SimpleBLE::Adapter::bluetooth_enabled()` verifies that Bluetooth is available and that the application has the permissions it needs. 2. `SimpleBLE::Adapter::get_adapters()` aggregates and returns the Bluetooth adapters available across all native backends on the system. To keep things simple, the program uses the first one it finds. 3. `adapter.scan_for(5000)` scans for nearby peripherals for five seconds. The `set_callback_on_scan_found()` callback prints each discovered device and stores connectable ones. 4. After selecting a peripheral, `peripheral.connect()` establishes a connection and allows service discovery. 5. `peripheral.services()` returns the GATT services exposed by the device. The example walks through each service and characteristic, keeping only those where `characteristic.can_read()` is true. 6. Finally, `peripheral.read(service_uuid, characteristic_uuid)` reads the selected characteristic five times, once per second. This scan -> connect -> discover -> read pattern is one of the most common SimpleBLE workflows, and it is a good foundation for building more advanced applications. Build and run your application [#build-and-run-your-application] With both files in place, go to your project directory and run: ```bash cmake -S . -B build cmake --build build ./build/simpleble_quickstart ``` When the program starts, it will guide you through the full interaction: 1. Scan for nearby peripherals 2. Ask you which device to connect to 3. Show the readable characteristics it discovered 4. Read the selected characteristic five times Where to next? [#where-to-next] * [Advertise, Serve, Publish](../peripheral/advertise-serve-publish) if this host should advertise and serve GATT instead * [Recipes](../../recipes/scan) for connect, write, notify, and reconnect snippets across bindings * [API reference](../api) for the complete C++ surface area * [Examples](../examples) for write, notify, and connect flows * [FAQ](../faq) for common questions and troubleshooting tips