btleplug

repository·master·Indexed 22 days ago

https://github.com/deviceplug/btleplug

An asynchronous Rust library for Bluetooth Low Energy (BLE) communication, designed for host/central mode. It supports Windows 10, macOS (>= 10.15), Linux, iOS, and Android. The library does not support Bluetooth 2/Classic or peripheral mode.

Tokens
13.1K
Snippets
38
Records
54
Agent score
75%

What's inside btleplug

  1. Overview of btleplug

    master

    btleplug is an asynchronous Rust library for Bluetooth Low Energy (BLE) communication. It is designed for host/central mode only (connecting to devices), not for peripheral mode (acting as a BLE device).

    Key constraints:

    • It does not support Bluetooth 2/Classic.
    • For peripheral mode, use alternatives like bluster or ble-peripheral-rust.

    Supported Platforms:

    • Windows 10
    • macOS (>= 10.15)
    • Linux
    • iOS
    • Android (including Flutter support)
  2. Overview of the BLE Integration Test System

    master

    The BLE Integration Test System is an end-to-end testing framework designed to validate btleplug API methods using real or emulated BLE peripherals. It uses a custom GATT profile to exercise the full range of btleplug capabilities, including reads, writes (with/without response), notifications, indications, advertisements, MTU exchange, connection parameters, and RSSI monitoring.

    There are two primary ways to run these tests:

    1. Hardware-based: Using an nRF52840 DK running Zephyr firmware as the test peripheral.
    2. Virtual/Emulated: Using a Python Bumble-based virtual peripheral for quick developer feedback without requiring physical hardware.
  3. Avoid CoreBluetooth caching issues in Rust tests

    master

    When running multiple tests in the same process, creating a new Manager (and thus a new CBCentralManager) can cause CoreBluetooth to stop reporting peripherals discovered by previous instances.

    To mitigate this, share a single Adapter across all tests using a tokio::sync::OnceCell and leak the Manager so the underlying CBCentralManager remains alive for the process lifetime.

    static ADAPTER: OnceCell<Adapter> = OnceCell::const_new();
  4. Prevent Rust panics from crashing Android instrumentation

    master

    On Android, all 28 integration tests share a single process. A Rust panic! calls abort(), which kills the entire process and stops all subsequent tests.

    To prevent this, wrap JNI test functions in std::panic::catch_unwind() within tests/android/rust/src/lib.rs. This allows panics to be caught and converted into JNI java/lang/AssertionError exceptions, which the Android test runner treats as individual JUnit failures rather than process-ending crashes.

    Implementation pattern:

    fn run_test(env: &JNIEnv, f: impl std::future::Future<Output = ()>) {
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            runtime().block_on(f);
        }));
        if let Err(panic) = result {
            let msg = if let Some(s) = panic.downcast_ref::<&str>() {
                s.to_string()
            } else if let Some(s) = panic.downcast_ref::<String>() {
                s.clone()
            } else {
                "test panicked".to_string()
            };
            env.throw_new("java/lang/AssertionError", &msg).ok();
        }
    }
    fn run_test(env: &JNIEnv, f: impl std::future::Future<Output = ()>) {
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            runtime().block_on(f);
        }));
        if let Err(panic) = result {
            let msg = if let Some(s) = panic.downcast_ref::<&str>() {
                s.to_string()
            } else if let Some(s) = panic.downcast_ref::<String>() {
                s.clone()
            } else {
                "test panicked".to_string()
            };
            env.throw_new("java/lang/AssertionError", &msg).ok();
        }
    }
  5. Understand the BLE Integration Test System Architecture

    master

    The btleplug integration test system validates the library's API by interacting with a real or simulated BLE peripheral. The system consists of three components:

    1. Rust Integration Tests: Located in the tests/ directory, these use the btleplug API to perform scanning, connecting, and GATT operations.
    2. Test Peripheral: A GATT server that implements a specific test profile. This can be:
      • Hardware: An nRF52840 DK running Zephyr firmware.
      • Software: A Python Bumble implementation running on the host OS.
    3. BLE Radio: The communication medium (either physical hardware or the host's BLE stack).

    The Rust tests are identical regardless of whether you use hardware or Bumble. The tests discover the peripheral by looking for the advertised device name "btleplug-test".

    ┌─────────────────────────┐     BLE Radio     ┌─────────────────────────┐
    │  Rust Integration Tests │ ◄──────────────► │  nRF52840 DK (Zephyr)   │
    │  (tests/*.rs)           │                   │  GATT Test Server       │
    │                         │                   │  (test-peripheral/      │
    │                         │                   │   zephyr/)              │
    └─────────────────────────┘                   └─────────────────────────┘
    
             OR (no hardware)
    
    ┌─────────────────────────┐   Host BLE Stack  ┌─────────────────────────┐
    │  Rust Integration Tests │ ◄──────────────► │  Python Bumble           │
    │  (same tests)           │                   │  GATT Test Server       │
    │                         │                   │  (test-peripheral/      │
    │                         │                   │   bumble/)              │
    └─────────────────────────┘                   └─────────────────────────┘
  6. BLE Integration Test System Glossary

    master

    Key terms used within the integration test system:

    • GATT: Generic Attribute Profile; the protocol used to expose structured data via services and characteristics.
    • Central: The BLE client role (played by btleplug). It scans, connects, and communicates.
    • Peripheral: The BLE server role (the test device). It advertises and hosts GATT services.
    • Characteristic: A GATT data endpoint with a UUID, properties (read/write/notify/indicate), and a value.
    • Descriptor: Metadata attached to a characteristic (e.g., CCCD for enabling notifications).
    • Notification/Indication: Server-initiated updates. Notifications are unacknowledged; indications require acknowledgement.
    • MTU: Maximum Transmission Unit; the negotiated maximum payload size for an ATT packet.
    • RSSI: Received Signal Strength Indicator; signal power in dBm.
    • Zephyr: The RTOS used for the nRF52840 DK firmware.
    • Bumble: A pure-Python BLE stack used for the virtual peripheral.
  7. Configure Proguard/R8 for Android

    master

    Because btleplug's Java classes are accessed exclusively via JNI, Android's R8/Proguard might strip them as dead code if minifyEnabled is true. You must add keep rules to your proguard-rules.pro file to prevent this.

    # In build.gradle
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    # In proguard-rules.pro
    # btleplug Java classes (accessed only via JNI)
    -keep class com.nonpolynomial.** { *; }
    -keep class io.github.gedgygedgy.** { *; }
  8. Run JNI utility tests on host machines (macOS/Linux)

    master

    You can run the jni_utils test suite on non-Android host machines (macOS, Linux, Windows) without requiring an Android SDK. This is achieved using the jni-host-tests cargo feature, which enables a minimal droidplug shim that only includes the jni_utils module and its pure Java dependencies (gedgygedgy).

    To run these tests, use the provided host test script which handles JDK detection, compiles the necessary Java sources with javac, packages them into a JAR, and executes the tests via cargo test.

    ./scripts/run-jni-tests.sh
  9. Set up the Python Bumble Software Peripheral

    master

    If you do not have dedicated hardware, you can run a simulated peripheral using Python Bumble. This runs on the host OS's BLE stack.

    Requirements:

    • Python installed.
    • A BLE adapter on the host machine.

    Commands:

    cd test-peripheral/bumble
    pip install -r requirements.txt
    python test_peripheral.py

    Note: Bumble runs through the host OS BLE stack. It may not perfectly replicate hardware-specific timing, MTU negotiation behavior, or RSSI values. Tests sensitive to these parameters should be run with actual hardware.

  10. Hardware Requirements for BLE Integration Testing

    master

    To perform full-scale integration testing with physical hardware, the following components are required:

    • nRF52840 DK: The primary hardware peripheral used for testing.
    • USB cable: For connecting the DK to the host.
    • Host BLE adapter: Built-in laptop Bluetooth or a USB dongle for desktops.
    • Software Toolchain:
      • Zephyr SDK: For building the test peripheral firmware.
      • nRF Command Line Tools: For flashing the firmware to the DK.
  11. Run BLE Integration Tests

    master

    Integration tests in btleplug are categorized by their hardware requirements. You can run them using standard Cargo commands:

    • Run all integration tests: Use cargo test (requires a peripheral like an nRF52 DK or a Bumble virtual peripheral).
    • Run unit tests only: Skip the integration tests to avoid hardware dependencies.

    To ensure test isolation and prevent state leakage between runs, the system is designed to reset the peripheral state via a Control Point 0x05 command at the start of each test.

    # Example: Running tests with hardware enabled
    BTLEPLUG_TEST_ENABLED=true cargo test
  12. Run the Rust Integration Test Suite

    master

    Integration tests are located in the tests/ directory and are gated with #[ignore] by default because they require an external peripheral.

    To run tests, you must have either the Zephyr firmware running on an nRF52 DK or the Python Bumble server running.

    Run all integration tests:

    # Note: must include --ignored to run the gated tests
    cargo test --test '*' -- --ignored

    Run a specific test file:

    cargo test --test test_read_write

    Run only unit tests (skipping integration tests):

    cargo test --lib

    Customizing Peripheral Discovery: You can override the default device name ("btleplug-test") by setting the BTLEPLUG_TEST_PERIPHERAL environment variable. This is useful if you have multiple test boards.

    # With hardware peripheral (nRF52 DK running, or Bumble started):
    cargo test --test test_read_write
    
    # All integration tests:
    cargo test --test '*'
    
    # Skip integration tests (unit tests only):
    cargo test --lib
    
    # To actually run the ignored integration tests:
    cargo test --test '*' -- --ignored