ble_plus 1.0.1
ble_plus: ^1.0.1 copied to clipboard
A Flutter plugin for Bluetooth Low Energy.
ble_plus #
A production-ready Flutter BLE plugin with Central and Peripheral roles across 6 platforms.
Features #
- Central Mode: Scan, connect, discover services, read/write/notify characteristics
- Peripheral Mode: Advertise, GATT server, handle read/write requests, send notifications
- L2CAP Channels: High-throughput data streaming (iOS 11+, Android 10+, macOS 10.14+)
- Background Support: iOS state restoration, Android foreground service
- Connection Parameters: Request/inspect connection intervals
- Descriptor R/W: Read and write characteristic descriptors
- Unified Error Handling: Sealed
BleErrorhierarchy with platform error codes - Configurable Logging: Debug, info, warning, error levels with custom callbacks
- 6 Platforms: Android, iOS, macOS, Web, Linux, Windows
Platform Support Matrix #
| Feature | Android | iOS | macOS | Web | Linux | Windows |
|---|---|---|---|---|---|---|
| Central: Scan | ✅ | ✅ | ✅ | ✅¹ | ✅ | ✅ |
| Central: Connect | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Central: GATT R/W/Notify | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Central: Descriptors | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Central: MTU Request | ✅ | Auto | Auto | Auto | ❌ | Auto |
| Central: Read RSSI | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Central: Connection Params | ✅ | Read-only | Read-only | ❌ | ❌ | ❌ |
| Peripheral: Advertise | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Peripheral: GATT Server | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Peripheral: Notifications | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| L2CAP Channels | ✅² | ✅³ | ✅⁴ | ❌ | ❌ | ❌ |
| Background Mode | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Bond/Pair Management | ✅ | Auto | Auto | ❌ | ✅ | ✅⁵ |
¹ Web scanning uses a device picker dialog (requires user gesture + HTTPS). Chrome/Edge only.
² Android L2CAP requires API 29+ (Android 10).
³ iOS L2CAP requires iOS 11+.
⁴ macOS L2CAP requires macOS 10.14+.
⁵ Windows:getBondState()supported;removeBond()not available.
Platform-Specific Limitations #
Web #
- Central role only — no peripheral/advertising support (Web Bluetooth API limitation)
- No L2CAP, no background mode, no descriptor R/W, no RSSI
- Scanning shows a device picker dialog (not continuous scan) — requires user gesture
- Requires HTTPS (or localhost for development)
- Chrome/Edge only — not supported in Firefox or Safari
- Must specify service UUID filters when scanning (no wildcard scan)
Linux #
- Central role only — no peripheral or L2CAP support
- Uses BlueZ D-Bus API — requires
bluezpackage installed - No MTU request, RSSI read, descriptor R/W, or background support
- Scanning requires appropriate D-Bus permissions
Windows #
- Central role only — no peripheral or L2CAP support
- Uses WinRT BLE APIs — requires Windows 10 or later
- No RSSI read, descriptor R/W, or background support
- MTU is auto-negotiated by the OS
iOS / macOS #
- MTU is auto-negotiated —
requestMtu()returns current value but cannot set it - Connection parameters are read-only — iOS/macOS manages intervals automatically
- L2CAP requires iOS 11+ / macOS 10.14+
- Background mode (iOS only) requires
UIBackgroundModesin Info.plist
Android #
- L2CAP requires API 29+ (Android 10)
- Peripheral role requires
BLUETOOTH_ADVERTISEpermission (Android 12+) BLUETOOTH_SCAN+BLUETOOTH_CONNECT+ACCESS_FINE_LOCATIONrequired for scanning- Background mode uses foreground service architecture
Quick Start #
import 'package:ble_plus/ble_plus.dart';
// ── Central: Scan and Connect ──────────────────────────────
final central = BleCentral();
// Scan for devices
central.startScan(withServices: [Guid('180D')]).listen((result) {
print('Found: ${result.device.displayName} RSSI: ${result.rssi}');
});
// Connect
final connection = await central.connect(device);
final services = await connection.discoverServices();
// Read a characteristic
final value = await connection.readCharacteristic(char);
// Subscribe to notifications
connection.subscribeToCharacteristic(char).listen((data) {
print('Notification: $data');
});
// Read/write descriptors
final descValue = await connection.readDescriptor(descriptor);
await connection.writeDescriptor(descriptor, [0x01, 0x00]);
// Read RSSI
final rssi = await connection.readRssi();
// Request connection parameters (Android)
await connection.requestConnectionParameters(ConnectionPriority.high);
// ── Bond / Pair ────────────────────────────────────────────
// Check bond state
final bondState = await connection.bondState; // none, bonding, bonded
// Initiate pairing
await connection.createBond();
// Listen for bond state changes
connection.bondStateStream.listen((state) {
print('Bond state: $state'); // none → bonding → bonded
});
// Remove bond (Android/Linux)
await connection.removeBond();
// ── Connection State ───────────────────────────────────────
// Check current state
print('Connected: ${connection.isConnected}');
print('State: ${connection.connectionState}');
// Listen for connection state changes (real-time)
connection.stateStream.listen((state) {
if (state == BleConnectionState.disconnected) {
print('Device disconnected!');
}
});
// Disconnect
await connection.disconnect();
// ── Peripheral: Advertise ──────────────────────────────────
final peripheral = BlePeripheral();
await peripheral.addService(GattServiceDefinition(
uuid: Guid('180D'),
characteristics: [
GattCharacteristicDefinition(
uuid: Guid('2A37'),
properties: CharacteristicProperties(notify: true, read: true),
permissions: CharacteristicPermissions(read: true),
),
],
));
await peripheral.startAdvertising(AdvertiseSettings(
localName: 'MyDevice',
serviceUuids: [Guid('180D')],
));
// Listen for connected centrals
peripheral.onCentralConnected.listen((device) {
print('Central connected: ${device.id}');
});
// Handle read requests
peripheral.onReadRequest.listen((request) {
request.respond([0x00, 72]); // Send heart rate data
});
// Send notifications
await peripheral.notifyCharacteristic(
Guid('180D'), Guid('2A37'), [0x00, 75],
);
// ── L2CAP Channels ─────────────────────────────────────────
// Central side
final channel = await connection.openL2CapChannel(psm, secure: true);
channel.inputStream.listen((data) => print('Received: $data'));
await channel.write([0x01, 0x02, 0x03]);
await channel.close();
// Peripheral side
final psm = await peripheral.publishL2CapChannel(secure: true);
peripheral.onL2CapChannelOpened.listen((channel) {
channel.inputStream.listen((data) => print('Data: $data'));
});
// ── Background Mode ────────────────────────────────────────
// iOS state restoration
await central.enableBackground(
iosRestorationIdentifier: 'my_app_central',
);
// Listen for restored devices after app relaunch
central.restoredDevices.listen((devices) {
print('Restored ${devices.length} devices');
});
// ── Platform Capabilities ──────────────────────────────────
// Check what the current platform supports before using features
final caps = central.capabilities;
if (caps.peripheralRole) {
// Safe to use peripheral features
}
if (caps.l2cap) {
// Safe to open L2CAP channels
}
BLE Connection Flow #
┌──────────────┐ scan ┌──────────────┐
│ BleCentral │──────────────▶│ BleScanResult│
│ │ │ .device │
│ startScan() │ │ .rssi │
│ stopScan() │ │ .advData │
└──────┬───────┘ └──────────────┘
│ connect(device)
▼
┌──────────────┐ discoverServices() ┌──────────────┐
│BleConnection │─────────────────────▶│ BleService │
│ │ │ .uuid │
│ readChar() │ │ .chars[] │
│ writeChar() │ └──────────────┘
│ subscribe() │
│ readDesc() │
│ writeDesc() │
│ requestMtu() │
│ readRssi() │
│ openL2Cap() │
│ disconnect() │
└──────────────┘
Setup #
Android #
Add permissions to AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
iOS #
Add to Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to communicate with BLE devices.</string>
For background BLE:
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
macOS #
Add to macos/Runner/DebugProfile.entitlements and Release.entitlements:
<key>com.apple.security.device.bluetooth</key>
<true/>
Add to macos/Runner/Info.plist:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to communicate with BLE devices.</string>
Web #
Ensure your app is served over HTTPS (or localhost). Add service UUIDs to scan filters:
central.startScan(withServices: [Guid('180D')]); // Required on Web
Linux #
Ensure bluez is installed:
sudo apt install bluez
Windows #
No additional setup required. Windows 10 or later needed.
Error Handling #
try {
await connection.readCharacteristic(char);
} on BleTimeoutError catch (e) {
// Connection or operation timed out
} on BleGattError catch (e) {
// GATT operation failed (e.code gives specific reason)
} on BleConnectionError catch (e) {
// Device disconnected or connection lost
} on BleUnsupportedError catch (e) {
// Feature not available on this platform
} on BlePermissionError catch (e) {
// Missing required permissions (e.missingPermissions)
} on BleScanError catch (e) {
// Scan failed (adapter off, etc.)
}
Configurable Logging #
final central = BleCentral(
logger: BleLogger(
level: BleLogLevel.debug,
onLog: (level, tag, message) {
// Integrate with your logging framework
print('[$level][$tag] $message');
},
),
);
Architecture #
Single-package Flutter plugin with native platform channels:
lib/
├── ble_plus.dart # Public API exports
└── src/
├── ble_central.dart # Central role (scan, connect)
├── ble_connection.dart # Connected device operations
├── ble_peripheral.dart # Peripheral role (advertise, GATT server)
├── ble_l2cap_channel.dart # L2CAP streaming
├── ble_logger.dart # Configurable logging
├── errors/ # Sealed error types
├── models/ # BleDevice, BleService, etc.
└── platform/ # Platform interface + implementations
├── ble_plus_platform.dart # Abstract platform interface
├── method_channel_ble_plus.dart # Android/iOS/macOS
├── ble_plus_web.dart # Web Bluetooth API
├── ble_plus_linux.dart # BlueZ D-Bus
└── ble_plus_windows.dart # WinRT BLE
android/ # Kotlin (BluetoothGatt, GATT Server, PeripheralManager)
ios/ # Swift (CoreBluetooth Central + Peripheral)
macos/ # Swift (CoreBluetooth Central + Peripheral)
linux/ # C++ (BlueZ GDBus)
windows/ # C++ (WinRT BLE)
Limitations Fixes #
ble_plus addresses all known limitations :
| Limitation | Fixed in ble_plus |
|---|---|
| No peripheral/server role | ✅ Full BlePeripheral API |
| No L2CAP channels | ✅ BleL2CapChannel |
| Global singleton architecture | ✅ Instance-based BleCentral/BlePeripheral |
Generic PlatformException errors |
✅ Sealed BleError hierarchy |
| No configurable logging | ✅ BleLogger with levels + callbacks |
| No connection parameters | ✅ requestConnectionParameters() |
| Stream handling issues | ✅ Properly closed streams, reliable onDone |
| No descriptor R/W | ✅ readDescriptor() / writeDescriptor() |
| No MTU change stream | ✅ connection.mtuStream |
| No bond/pair management | ✅ createBond(), removeBond(), bondState, bondStateStream |
| No real-time connection state | ✅ isConnected, connectionState, stateStream |
| No Web/Linux/Windows | ✅ All 6 platforms supported |
| No macOS (community fork) | ✅ Native CoreBluetooth implementation |
| No background mode architecture | ✅ iOS restoration + Android FGS |
| Hard to test/mock | ✅ Platform interface pattern, DI via logger |
License #
BSD-3-Clause