Universal BLE

repository·main·Indexed 41 days ago

https://github.com/navideck/universal_ble

A cross-platform Flutter plugin providing Bluetooth Low Energy (BLE) capabilities for Android, iOS, macOS, Windows, Linux, and Web. It supports both Client Mode (via UniversalBle) for scanning and interacting with devices, and Peripheral Mode (via UniversalBlePeripheral) for advertising services and responding to connections.

Tokens
23K
Snippets
60
Records
77
Agent score
50%

What's inside Universal BLE

  1. Overview of Universal BLE

    main
    Universal BLE is a cross-platform Bluetooth Low Energy (BLE) plugin for Flutter. It supports a wide range of platforms including Android, iOS, macOS, Windows, Linux, and Web. The plugin is designed to handle both Client Mode (acting as a central device) and Peripheral Mode (acting as a peripheral device).
  2. Compare Client Mode and Peripheral Mode support

    main

    Universal BLE provides two distinct API surfaces depending on your use case:

    Client Mode (UniversalBle)

    Used when your app needs to scan for, connect to, and interact with external BLE devices.

    • Core capabilities: Scanning, connecting, discovering services, reading/writing data, and subscriptions.
    • Platform availability: Most features are widely supported across all platforms, though some specific features like autoConnect or enable/disable Bluetooth may be restricted on certain platforms (e.g., iOS/macOS/Web).

    Peripheral Mode (UniversalBlePeripheral)

    Used when your app needs to act as a BLE peripheral that advertises services and responds to connections from other devices.

    • Core capabilities: Advertising, managing services/characteristics, and updating characteristic values.
    • Platform availability: Highly supported on Android, iOS, macOS, and Windows. Linux support is currently in progress (indicated by 🚧). Web is not supported for Peripheral Mode.
  3. Configure and manage Command Queues

    main

    To prevent overlapping operations that might crash the BLE stack (especially on Android), UniversalBle uses a command queue system.

    Queue Types

    • QueueType.auto (Default): Android uses per-device queues; other platforms run commands in parallel.
    • QueueType.global: All commands across all devices run in a single serial queue.
    • QueueType.perDevice: Each device has its own serial queue.
    • QueueType.none: Disables queuing entirely (not recommended for Android).

    Custom Queues

    You can group specific commands into a custom queue by providing a queueId string. Commands with the same queueId will be serialized relative to each other, even if the global queueType is set to parallel.

    Queue Management

    • Monitor Progress: Set UniversalBle.onQueueUpdate to receive updates on remaining items in a queue.
    • Clear Queues: Use UniversalBle.clearQueue(id) where id can be a deviceId, a custom queueId, or BleCommand.globalQueueId. Calling clearQueue() with no arguments clears all queues.
    // Set global strategy
    UniversalBle.queueType = QueueType.perDevice;
    
    // Use a custom queue for specific operations
    UniversalBle.write(deviceId, service, char, value, queueId: 'sync_group');
    
    // Monitor queue progress
    UniversalBle.onQueueUpdate = (String id, int remainingItems) {
      print("Queue $id has $remainingItems items left");
    };
    
    // Clear a specific queue
    UniversalBle.clearQueue('sync_group');
  4. Check Bluetooth availability

    main

    Ensure Bluetooth is powered on before initiating a scan. You can check the current state with getBluetoothAvailabilityState() or listen to changes via availabilityStream or the onAvailabilityChange handler.

    AvailabilityState state = await UniversalBle.getBluetoothAvailabilityState();
    // Start scan only if Bluetooth is powered on
    if (state == AvailabilityState.poweredOn) {
      UniversalBle.startScan();
    }
    
    // Listen to bluetooth availability changes using stream
    UniversalBle.availabilityStream.listen((state) {
      if (state == AvailabilityState.poweredOn) {
        UniversalBle.startScan();
      }
    });
    
    // Or set a handler
    UniversalBle.onAvailabilityChange = (state) {};
  5. Run HIL tests on Web (Chrome)

    main

    The HIL suite includes an interactive Web Bluetooth runner located in lib/main.dart.

    Requirements:

    • A secure context (HTTPS or localhost).
    • A real user gesture.
    • Browser device permission.

    Steps:

    1. Run the application: flutter run -d chrome.
    2. In the browser, click Select device and run.
    3. Select UniversalBLE-HIL from the Chrome device chooser.
    flutter run -d chrome
  6. Run HIL baseline and fault injection tests

    main

    Once the environment is set up and the firmware is advertising, you can run the Windows-based test suites.

    Running Baseline Tests

    To run the standard BLE behavior tests:

    flutter test integration_test/baseline_hil_test.dart -d windows

    Running Fault Injection Tests

    To run tests that simulate hostile peripheral behavior (timing races, disconnects, etc.):

    flutter test integration_test/fault_injection_hil_test.dart -d windows

    Advanced Execution Options

    • Skip Scanning: If you know the Windows device ID, use --dart-define=HIL_DEVICE_ID=... to skip the initial scan.
    • Extended Reconnects: For longer reconnect testing, use --dart-define=HIL_RECONNECT_CYCLES=N.
    • Rerun Single Case: Use --plain-name to rerun a specific failing case individually.
    # Run baseline tests
    flutter test integration_test/baseline_hil_test.dart -d windows
    
    # Run fault injection tests
    flutter test integration_test/fault_injection_hil_test.dart -d windows
    
    # Run with a specific device ID
    flutter test integration_test/baseline_hil_test.dart -d windows --dart-define=HIL_DEVICE_ID=AA:BB:CC:DD:EE:FF
    
    # Run with extended reconnect cycles
    flutter test integration_test/baseline_hil_test.dart -d windows --dart-define=HIL_RECONNECT_CYCLES=100
  7. Pair BLE devices

    main

    Pairing allows access to encrypted characteristics. Behavior varies by platform:

    Android, Windows, and Linux

    Trigger pairing directly using bleDevice.pair().

    Apple and Web

    Pairing is typically triggered automatically by the OS when attempting to read/write an encrypted characteristic. To manually trigger it, bleDevice.pair() only works if the device has an encrypted read characteristic.

    If you need to target a specific encrypted characteristic, provide a pairingCommand:

    await bleDevice.pair(pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC"));

    Managing Pairing State

    • Check Status: Use bleDevice.isPaired(). On Apple/Web, you must pass the same pairingCommand used to trigger pairing to get a non-null result.
    • Monitor Changes: Listen to bleDevice.pairingStateStream for real-time updates.
    • Unpair: Use bleDevice.unpair() to remove the pairing.

    Tip: To find which characteristic is encrypted, attempt to read/write all discovered characteristics one by one while unpaired; the one that triggers the OS pairing prompt is your target.

    // Trigger pairing (Android/Win/Linux)
    await bleDevice.pair();
    
    // Trigger pairing with specific command (Apple/Web)
    await bleDevice.pair(pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC"));
    
    // Check status
    bool? isPaired = bleDevice.isPaired();
    
    // Listen for state changes
    bleDevice.pairingStateStream.listen((bool paired) {
      // Handle change
    });
  8. Configure Android Manifest Permissions

    main

    To use Universal BLE on Android, you must declare the necessary permissions in your AndroidManifest.xml.

    Standard BLE Permissions: Use these for general Bluetooth connectivity.

    Location-based Scanning: If your app uses iBeacons or requires BLUETOOTH_SCAN to determine location, you must use the more permissive location declarations instead of the neverForLocation flag.

    Peripheral Mode: If your app acts as a BLE peripheral (advertising), you must also include the BLUETOOTH_ADVERTISE permission.

    <!-- Standard BLE Permissions -->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
    
    <!-- If using iBeacons or location-based scanning -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    
    <!-- For Peripheral Mode (Advertising) -->
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
  9. Pair a BLE device

    main

    Pairing behavior varies by platform:

    Android, Windows, and Linux

    Trigger pairing directly using: await UniversalBle.pair(deviceId);

    Apple and Web

    Pairing is typically triggered automatically by the OS when attempting to access an encrypted characteristic.

    To manually trigger pairing on these platforms, you must provide a pairingCommand targeting an encrypted characteristic: UniversalBle.pair(deviceId, pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC"));

    Unpairing

    To remove pairing information, use: UniversalBle.unpair(deviceId);

    // Android, Windows, Linux
    await UniversalBle.pair(deviceId);
    
    // Apple and Web (requires specific encrypted characteristic)
    UniversalBle.pair(deviceId, pairingCommand: BleCommand(service: "SERVICE", characteristic: "ENCRYPTED_CHARACTERISTIC"));
    
    // Unpair
    UniversalBle.unpair(deviceId);
  10. Set up Peripheral Mode with UniversalBlePeripheral

    main

    To use your app as a BLE peripheral (server), use the UniversalBlePeripheral class. Before starting, you must check if the platform supports peripheral mode and if the peripheral state is ready.

    import 'package:universal_ble/universal_ble.dart';
    
    final caps = await UniversalBlePeripheral.getCapabilities();
    if (!caps.supportsPeripheralMode) return;
    
    final readiness = await UniversalBlePeripheral.getAvailabilityState();
    if (readiness != PeripheralReadinessState.ready) return;
  11. Subscribe to BLE characteristic notifications and indications

    main

    To receive real-time updates from a BLE characteristic, you must first obtain the BleCharacteristic instance via bleDevice.getCharacteristic.

    Listening to Values

    Use the onValueReceived stream to listen for incoming Uint8List data whenever the characteristic's value changes.

    Enabling Subscriptions

    • Notifications: Use characteristic.notifications.subscribe(). This throws an exception if the characteristic does not support notifications.
    • Indications: Use characteristic.indications.subscribe(). This throws an exception if the characteristic does not support indications.
    • Unsubscribe: Use characteristic.unsubscribe() to stop receiving updates.

    Checking Subscription Status

    You can check if a characteristic is currently subscribed using several methods:

    • On the characteristic: characteristic.isSubscribed or characteristic.notifications.isSubscribed.
    • Globally: UniversalBle.isSubscribed(deviceId, characteristicId).
    • Per device: UniversalBle.getSubscribedCharacteristics(deviceId) returns a list of all subscribed UUIDs.
    // Listen to value changes
    characteristic.onValueReceived.listen((value) {
      debugPrint('Received value: ${value.toString()}');
    });
    
    // Subscribe to notifications
    await characteristic.notifications.subscribe();
    
    // Subscribe to indications
    await characteristic.indications.subscribe();
    
    // Unsubscribe
    await characteristic.unsubscribe();
    
    // Check status
    bool isSubscribed = characteristic.isSubscribed;
  12. Start and Stop Advertising in Peripheral Mode

    main

    Advertising allows peripherals to be discovered. Behavior varies by platform:

    • Android: Passing localName may temporarily change the system Bluetooth device name. The plugin restores the original name automatically.
    • Windows: Use services: [] and manufacturerData for connectionless advertising. Custom localName is not supported. Payload is limited to 27 bytes.
    • iOS/macOS: Supports localName and service UUIDs, but does not support manufacturer data.

    Use advertisingStateStream to monitor for asynchronous failures.

    import 'dart:typed_data';
    import 'package:flutter/foundation.dart';
    import 'package:universal_ble/universal_ble.dart';
    
    final isWindows = !kIsWeb && defaultTargetPlatform == TargetPlatform.windows;
    final caps = await UniversalBlePeripheral.getCapabilities();
    
    await UniversalBlePeripheral.startAdvertising(
      services: [batteryService],
      localName: isWindows ? null : 'UniversalBlePeripheral',
      manufacturerData: isWindows || !caps.supportsManufacturerDataInAdvertisement
          ? null
          : ManufacturerData(
              0x012d,
              Uint8List.fromList([0x03, 0x00, 0x64, 0x00]),
            ),
      platformConfig: PeripheralPlatformConfig(
        android: PeripheralAndroidOptions(
          addManufacturerDataInScanResponse: false,
        ),
      ),
    );
    
    // To stop:
    await UniversalBlePeripheral.stopAdvertising();