Add high level API (#160)

* Add high-level api extensions for BleDevice and Characteristic

* Add doc comments and move few files to utils

* Add advance documentation

* Make the high level APIs the new default in the readme

* More readme cleanups

* Add pair api, improve notifications apis and deprecate writeValue

* Use highLevel apis in Example

* Rename HighLevel isPaired api to hasPairing api

* Update Readme

* Rename low level readValue api to read and update Readme

* Update UniversalBle to use new apis instead of deprecated internally

* Split queueCommand and queueCommandWithoutTimeout

* Split setNotifiable into subscribeNotifications, subscribeIndications and unsubscribe

* Fix web imports

* Update changelog

* Switch back to isPaired

Co-authored-by: Rohit Sangwan <rohitsangwan647@gmail.com>

* Fix BleDevice toString method

* Switch back to isPaired

* Change peripheral to device

* Rename cached to preferCached

* Remove preferCached from discoverServices()

* Update changelog

* Use cached services

* Add ///

* Reverse withoutResponse to withResponse to avoid negation

* Link to low level API

* Move connectionState to public readme

* Rename disableSubscriptions to unsubscribe

* Improve documentation

* Fix getService cache

* Add NotFoundException

---------

Co-authored-by: Foti Dim <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2025-06-19 16:32:14 +05:30
committed by GitHub
parent 72090571a3
commit 0758f5e91d
32 changed files with 940 additions and 278 deletions
+9 -3
View File
@@ -1,6 +1,12 @@
## 0.20
* Fix `getBluetoothAvailabilityState` reporting wrong Bluetooth status on browsers where Web Bluetooth can be globally disabled
* `startScan` API on Web platforms now throws a `WebBluetoothGloballyDisabled` exception if scanning cannot proceed due to Web Bluetooth being globally disabled
## 0.20.0
* Add new high level API. Services are auto-discovered. the BleDevice class offers convenient member methods and properties.
* BREAKING CHANGE: `bleDevice.isPaired` is now `bleDevice.paired`
* Deprecate `readValue` in favor of `read`
* Deprecate `writeValue` in favor of `write`
* Deprecate `setNotifiable` in favor of `subscribeNotifications`, `subscribeIndications`, `unsubscribe`
* Fix `getBluetoothAvailabilityState` reporting wrong Bluetooth status on browsers where Web Bluetooth can globally be disabled
* `startScan` on web now throws a `WebBluetoothGloballyDisabled` exception if scanning cannot proceed due to Web Bluetooth being globally disabled
* EXAMPLE APP: Migrate to the new high level API
## 0.19.0
* Get and prefer advertised name of scanned devices on Apple
+133
View File
@@ -0,0 +1,133 @@
# Low Level API
### Connecting
```dart
// Connect to a device using the `deviceId` of the BleDevice received from `UniversalBle.onScanResult`
String deviceId = bleDevice.deviceId;
UniversalBle.connect(deviceId);
// Disconnect from a device
UniversalBle.disconnect(deviceId);
// Get connection/disconnection updates using stream
UniversalBle.connectionStream(deviceId).listen((bool isConnected) {
debugPrint('Is device $deviceId connected?: $isConnected');
});
// Or set a handler to get updates of all devices
UniversalBle.onConnectionChange = (String deviceId, bool isConnected, String? error) {
debugPrint('Is device $deviceId connected?: $isConnected. Error: $error');
}
```
### Discovering Services
After establishing a connection, you need to discover services. This method will discover all services and their characteristics.
```dart
// Discover services of a specific device
UniversalBle.discoverServices(deviceId);
```
### Reading & Writing data
You need to first [discover services](#discovering-services) before you are able to read and write to characteristics.
```dart
// Read data from a characteristic
UniversalBle.read(deviceId, serviceId, characteristicId);
// Write data to a characteristic
UniversalBle.write(deviceId, serviceId, characteristicId, value);
// Subscribe to a characteristic notifications
UniversalBle.subscribeNotifications(deviceId, serviceId, characteristicId);
// Subscribe to a characteristic indications
UniversalBle.subscribeIndications(deviceId, serviceId, characteristicId);
// Get characteristic notifications/indications updates using stream
UniversalBle.characteristicValueStream(deviceId, characteristicId).listen((Uint8List value) {
debugPrint('OnValueChange $deviceId, $characteristicId, ${hex.encode(value)}');
});
// Or set a handler to get updates of all characteristics
UniversalBle.onValueChange = (String deviceId, String characteristicId, Uint8List value) {
debugPrint('onValueChange $deviceId, $characteristicId, ${hex.encode(value)}');
}
// Unsubscribe from notifications/indications
UniversalBle.unsubscribe(deviceId, serviceId, characteristicId);
```
### Pairing
#### Trigger pairing
##### Pair on Android, Windows, Linux
```dart
await UniversalBle.pair(deviceId);
```
##### Pair on Apple and web
For Apple and Web, pairing support depends on the device. Pairing is triggered automatically by the OS when you try to read/write from/to an encrypted characteristic.
Calling `UniversalBle.pair(deviceId)` will only trigger pairing if the device has an _encrypted read characteristic_.
If your device only has encrypted write characteristics or you happen to know which encrypted read characteristic you want to use, you can pass it with a `pairingCommand`.
```dart
UniversalBle.pair(deviceId, pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));
```
After pairing you can check the pairing status.
#### Pairing status
##### Pair on Android, Windows, Linux
```dart
// Check current pairing state
bool? isPaired = UniversalBle.isPaired(deviceId);
```
##### Pair on Apple and web
For `Apple` and `Web`, you have to pass a "pairingCommand" with an encrypted read or write characteristic. If you don't pass it then it will return `null`.
```dart
bool? isPaired = await UniversalBle.isPaired(deviceId, pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));
```
##### Discovering encrypted characteristic
To discover encrypted characteristics, make sure your device is not paired and use the example app to read/write to all discovered characteristics one by one. If one of them triggers pairing, that means it is encrypted and you can use it to construct `BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC")`.
#### Pairing state changes
```dart
// Get pairing state updates using stream
UniversalBle.pairingStateStream(deviceId).listen((bool paired) {
// Handle pairing state change
});
// Or set a handler to get pairing state updates of all devices
UniversalBle.onPairingStateChange = (String deviceId, bool paired) {}
```
#### Unpair
```dart
UniversalBle.unpair(deviceId);
```
### Request MTU
This method will **attempt** to set the MTU (Maximum Transmission Unit) but it is not guaranteed to succeed due to platform limitations. It will always return the current MTU.
```dart
int mtu = await UniversalBle.requestMtu(widget.deviceId, 247);
```
+125 -51
View File
@@ -14,6 +14,7 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
- [Reading & Writing data](#reading--writing-data)
- [Pairing](#pairing)
- [Bluetooth Availability](#bluetooth-availability)
- [Requesting MTU](#requesting-mtu)
- [Command Queue](#command-queue)
- [Timeout](#timeout)
- [UUID Format Agnostic](#uuid-format-agnostic)
@@ -26,12 +27,12 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE
| connect/disconnect | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| getSystemDevices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| discoverServices | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| readValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| writeValue | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| setNotifiable | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| read | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| write | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| subscriptions | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| pair | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ⏺ |
| unpair | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| isPaired | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| onPairingStateChange | ✔️ | ⏺ | ⏺ | ✔️ | ✔️ | ⏺ |
| getBluetoothAvailabilityState | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
| enable/disable Bluetooth | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ |
@@ -57,7 +58,7 @@ import 'package:universal_ble/universal_ble.dart';
```dart
// Get scan updates from stream
UniversalBle.scanStream.listen((bleDevice) {
UniversalBle.scanStream.listen((BleDevice bleDevice) {
// e.g. Use BleDevice ID to connect
});
@@ -155,64 +156,139 @@ List<String> withNamePrefix;
### Connecting
#### Connect
Connects to the BLE device. This method initiates a connection to the Bluetooth device.
```dart
// Connect to a device using the `deviceId` of the BleDevice received from `UniversalBle.onScanResult`
String deviceId = bleDevice.deviceId;
UniversalBle.connect(deviceId);
await bleDevice.connect();
```
// Disconnect from a device
UniversalBle.disconnect(deviceId);
#### Disconnect
// Get connection/disconnection updates using stream
UniversalBle.connectionStream(deviceId).listen((bool isConnected) {
debugPrint('OnConnectionChange $deviceId, $isConnected');
Disconnects from the BLE device. This method terminates the connection to the Bluetooth device.
```dart
await bleDevice.disconnect();
```
#### Connection Stream
```dart
bleDevice.connectionStream.listen((isConnected) {
debugPrint('Is device connected?: $isConnected');
});
```
// Or set a handler to get updates of all devices
UniversalBle.onConnectionChange = (String deviceId, bool isConnected, String? error) {
debugPrint('OnConnectionChange $deviceId, $isConnected Error: $error');
}
#### IsConnected
// Get current connection state
```dart
bool isConnected = await bleDevice.isConnected;
```
#### Connection state
```dart
// Can be connected, disconnected, connecting or disconnecting
BleConnectionState connectionState = await bleDevice.connectionState;
```
### Discovering Services
After establishing a connection, you need to discover services. This method will discover all services and their characteristics.
After establishing a connection, services need to be discovered. This method will discover all services and their characteristics.
If you don't call this method then it will be automatically called when you try to get any service or characteristic.
#### DiscoverServices
Discovers the services offered by the device. Returns a `Future<List<BleService>>`. After discovery services are cached and each call of this method updates the cache.
```dart
// Discover services of a specific device
UniversalBle.discoverServices(deviceId);
List<BleService> services = await bleDevice.discoverServices();
for (var service in services) {
debugPrint('Service UUID: ${service.uuid}');
}
```
### Reading & Writing data
#### GetService
Retrieves a specific service. Returns a `Future<BleService>`.
- `service`: The UUID of the service.
- `preferCached`: If `true` (default), cached services are used. If cache is empty, `discoverServices()` will be called.
```dart
BleService service = await bleDevice.getService('180a');
```
#### GetCharacteristic
Retrieves a specific characteristic from a service. Returns a `Future<BleCharacteristic>`.
- `service`: The UUID of the service.
- `characteristic`: The UUID of the characteristic.
- `preferCached`: If `true` (default), cached services are used. If cache is empty, `discoverServices()` will be called
```dart
BleCharacteristic characteristic = await bleDevice.getCharacteristic('180a','2a56');
```
Or retrieve from `BleService`
```dart
BleCharacteristic characteristic = await service.getCharacteristic('2a56');
```
## Reading & Writing data
You need to first [discover services](#discovering-services) before you are able to read and write to characteristics.
```dart
// Read data from a characteristic
UniversalBle.readValue(deviceId, serviceId, characteristicId);
Uint8List value = await characteristic.read();
```
// Write data to a characteristic
UniversalBle.writeValue(deviceId, serviceId, characteristicId, value);
```dart
await characteristic.write([0x01, 0x02, 0x03]);
// Subscribe to a characteristic
UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProperty.notification);
await characteristic.write([0x01, 0x02, 0x03], withResponse: false);
```
// Get characteristic updates using stream
UniversalBle.characteristicValueStream(deviceId, characteristicId).listen((Uint8List value) {
debugPrint('OnValueChange $deviceId, $characteristicId, ${hex.encode(value)}');
## Subscriptions
Get `BleCharacteristic` using `bleDevice.getCharacteristic`
### OnValueReceived
A stream of `Uint8List` that emits values received from the characteristic. Listen to this stream to receive updates whenever the characteristic's value changes.
```dart
characteristic.onValueReceived.listen((value) {
debugPrint('Received value: ${value.toString()}');
});
```
// Or set a handler to get updates of all characteristics
UniversalBle.onValueChange = (String deviceId, String characteristicId, Uint8List value) {
debugPrint('onValueChange $deviceId, $characteristicId, ${hex.encode(value)}');
}
### Notifications
// Unsubscribe from a characteristic
UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProperty.disabled);
Subscribe to notifications for this characteristic. Throws an exception if the characteristic does not support notifications.
```dart
await characteristic.notifications.subscribe();
```
### Indications
Subscribe to indications for this characteristic. Throws an exception if the characteristic does not support indications.
```dart
await characteristic.indications.subscribe();
```
### Unsubscribe
Unsubscribe from notifications and indications of this characteristic.
```dart
await characteristic.unsubscribe();
```
### Pairing
@@ -222,18 +298,18 @@ UniversalBle.setNotifiable(deviceId, serviceId, characteristicId, BleInputProper
##### Pair on Android, Windows, Linux
```dart
await UniversalBle.pair(deviceId);
await bleDevice.pair();
```
##### Pair on Apple and web
For Apple and Web, pairing support depends on the device. Pairing is triggered automatically by the OS when you try to read/write from/to an encrypted characteristic.
Calling `UniversalBle.pair(deviceId)` will only trigger pairing if the device has an *encrypted read characteristic*.
Calling `bleDevice.pair()` will only trigger pairing if the device has an *encrypted read characteristic*.
If your device only has encrypted write characteristics or you happen to know which encrypted read characteristic you want to use, you can pass it with a `pairingCommand`.
```dart
UniversalBle.pair(deviceId, pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));
await bleDevice.pair(pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));
```
After pairing you can check the pairing status.
@@ -243,7 +319,7 @@ After pairing you can check the pairing status.
```dart
// Check current pairing state
bool? isPaired = UniversalBle.isPaired(deviceId);
bool? isPaired = bleDevice.isPaired();
```
##### Pair on Apple and web
@@ -251,7 +327,7 @@ bool? isPaired = UniversalBle.isPaired(deviceId);
For `Apple` and `Web`, you have to pass a "pairingCommand" with an encrypted read or write characteristic. If you don't pass it then it will return `null`.
```dart
bool? isPaired = await UniversalBle.isPaired(deviceId, pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));
bool? isPaired = await bleDevice.isPaired(pairingCommand: BleCommand(service:"SERVICE", characteristic:"ENCRYPTED_CHARACTERISTIC"));
```
##### Discovering encrypted characteristic
@@ -261,17 +337,14 @@ To discover encrypted characteristics, make sure your device is not paired and u
```dart
// Get pairing state updates using stream
UniversalBle.pairingStateStream(deviceId).listen((bool isPaired) {
bleDevice.pairingStateStream.listen((bool paired) {
// Handle pairing state change
});
// Or set a handler to get pairing state updates of all devices
UniversalBle.onPairingStateChange = (String deviceId, bool isPaired) {}
```
#### Unpair
```dart
UniversalBle.unpair(deviceId);
bleDevice.unpair();
```
### Bluetooth Availability
@@ -292,12 +365,10 @@ UniversalBle.enableBluetooth();
UniversalBle.disableBluetooth();
```
### Request MTU
This method will **attempt** to set the MTU (Maximum Transmission Unit) but it is not guaranteed to succeed due to platform limitations. It will always return the current MTU.
### Requesting MTU
```dart
int mtu = await UniversalBle.requestMtu(widget.deviceId, 247);
int mtu = await bleDevice.requestMtu(256);
```
#### Platform Limitations
@@ -469,6 +540,9 @@ class UniversalBleMock extends UniversalBlePlatform {
UniversalBle.setInstance(UniversalBleMock());
```
## Low level API
For more granular control, you can use the [Low-Level API](README.low_level.md). This API is "Device ID"-based, offering greater flexibility by enabling direct calls without the need for object instances.
## 🧩 Apps using Universal BLE
+9 -11
View File
@@ -41,13 +41,13 @@ class _MyAppState extends State<MyApp> {
UniversalBle.queueType = _queueType;
UniversalBle.timeout = const Duration(seconds: 10);
UniversalBle.onAvailabilityChange = (state) {
UniversalBle.availabilityStream.listen((state) {
setState(() {
bleAvailabilityState = state;
});
};
});
UniversalBle.onScanResult = (result) {
UniversalBle.scanStream.listen((result) {
// log(result.toString());
int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId);
if (index == -1) {
@@ -59,7 +59,7 @@ class _MyAppState extends State<MyApp> {
_bleDevices[index] = result;
}
setState(() {});
};
});
// UniversalBle.onQueueUpdate = (String id, int remainingItems) {
// debugPrint("Queue: $id RemainingItems: $remainingItems");
@@ -259,13 +259,11 @@ class _MyAppState extends State<MyApp> {
bleDevice: device,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PeripheralDetailPage(
device.deviceId,
device.name ?? "Unknown Peripheral",
),
));
context,
MaterialPageRoute(
builder: (_) => PeripheralDetailPage(device),
),
);
UniversalBle.stopScan();
setState(() {
_isScanning = false;
@@ -30,7 +30,7 @@ class ScannedItemWidget extends StatelessWidget {
visible: manufacturerData != null,
child: Text(manufacturerData.toString()),
),
bleDevice.isPaired == true
bleDevice.paired == true
? const Text(
"Paired",
style: TextStyle(color: Colors.green),
@@ -1,5 +1,3 @@
// ignore_for_file: avoid_print, depend_on_referenced_packages
import 'dart:async';
import 'package:convert/convert.dart';
@@ -13,9 +11,8 @@ import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart';
import 'package:universal_ble_example/widgets/responsive_view.dart';
class PeripheralDetailPage extends StatefulWidget {
final String deviceId;
final String deviceName;
const PeripheralDetailPage(this.deviceId, this.deviceName, {super.key});
final BleDevice bleDevice;
const PeripheralDetailPage(this.bleDevice, {super.key});
@override
State<StatefulWidget> createState() {
@@ -24,30 +21,32 @@ class PeripheralDetailPage extends StatefulWidget {
}
class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
late final bleDevice = widget.bleDevice;
bool isConnected = false;
GlobalKey<FormState> valueFormKey = GlobalKey<FormState>();
List<BleService> discoveredServices = [];
final List<String> _logs = [];
final binaryCode = TextEditingController();
({
BleService service,
BleCharacteristic characteristic
})? selectedCharacteristic;
StreamSubscription? connectionStreamSubscription;
StreamSubscription? pairingStateSubscription;
BleService? selectedService;
BleCharacteristic? selectedCharacteristic;
@override
void initState() {
super.initState();
UniversalBle.onConnectionChange = _handleConnectionChange;
connectionStreamSubscription =
bleDevice.connectionStream.listen(_handleConnectionChange);
pairingStateSubscription =
bleDevice.pairingStateStream.listen(_handlePairingStateChange);
UniversalBle.onValueChange = _handleValueChange;
UniversalBle.onPairingStateChange = _handlePairingStateChange;
_asyncInits();
}
void _asyncInits() {
UniversalBle.getConnectionState(
widget.deviceId,
).then((state) {
bleDevice.connectionState.then((state) {
if (state == BleConnectionState.connected) {
setState(() {
isConnected = true;
@@ -59,7 +58,8 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
@override
void dispose() {
super.dispose();
UniversalBle.onConnectionChange = null;
connectionStreamSubscription?.cancel();
pairingStateSubscription?.cancel();
UniversalBle.onValueChange = null;
}
@@ -69,18 +69,10 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
});
}
void _handleConnectionChange(
String deviceId,
bool isConnected,
String? error,
) {
print(
'_handleConnectionChange $deviceId, $isConnected ${error != null ? 'Error: $error' : ''}',
);
void _handleConnectionChange(bool isConnected) {
debugPrint('_handleConnectionChange $isConnected');
setState(() {
if (deviceId == widget.deviceId) {
this.isConnected = isConnected;
}
this.isConnected = isConnected;
});
_addLog('Connection', isConnected ? "Connected" : "Disconnected");
// Auto Discover Services
@@ -93,12 +85,12 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
String deviceId, String characteristicId, Uint8List value) {
String s = String.fromCharCodes(value);
String data = '$s\nraw : ${value.toString()}';
print('_handleValueChange $deviceId, $characteristicId, $s');
debugPrint('_handleValueChange $characteristicId, $s');
_addLog("Value", data);
}
void _handlePairingStateChange(String deviceId, bool isPaired) {
print('isPaired $deviceId, $isPaired');
void _handlePairingStateChange(bool isPaired) {
debugPrint('isPaired $isPaired');
_addLog("PairingStateChange - isPaired", isPaired);
}
@@ -106,10 +98,9 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
const webWarning =
"Note: Only services added in ScanFilter or WebOptions will be discovered";
try {
var services = await UniversalBle.discoverServices(widget.deviceId);
print('${services.length} services discovered');
print(services);
discoveredServices.clear();
var services = await bleDevice.discoverServices();
debugPrint('${services.length} services discovered');
debugPrint(services.toString());
setState(() {
discoveredServices = services;
});
@@ -126,13 +117,10 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
}
Future<void> _readValue() async {
BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic;
if (selectedCharacteristic == null) return;
try {
Uint8List value = await UniversalBle.readValue(
widget.deviceId,
selectedCharacteristic!.service.uuid,
selectedCharacteristic!.characteristic.uuid,
);
Uint8List value = await selectedCharacteristic.read();
String s = String.fromCharCodes(value);
String data = '$s\nraw : ${value.toString()}';
_addLog('Read', data);
@@ -142,6 +130,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
}
Future<void> _writeValue() async {
BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic;
if (selectedCharacteristic == null ||
!valueFormKey.currentState!.validate() ||
binaryCode.text.isEmpty) {
@@ -157,54 +146,59 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
}
try {
await UniversalBle.writeValue(
widget.deviceId,
selectedCharacteristic!.service.uuid,
selectedCharacteristic!.characteristic.uuid,
await selectedCharacteristic.write(
value,
_hasSelectedCharacteristicProperty(
[CharacteristicProperty.writeWithoutResponse])
? BleOutputProperty.withoutResponse
: BleOutputProperty.withResponse,
withResponse: _hasSelectedCharacteristicProperty(
[CharacteristicProperty.writeWithoutResponse],
),
);
_addLog('Write', value);
} catch (e) {
print(e);
debugPrint(e.toString());
_addLog('WriteError', e);
}
}
Future<void> _setBleInputProperty(BleInputProperty inputProperty) async {
Future<void> _subscribeChar() async {
BleCharacteristic? selectedCharacteristic = this.selectedCharacteristic;
if (selectedCharacteristic == null) return;
try {
if (inputProperty != BleInputProperty.disabled) {
List<CharacteristicProperty> properties =
selectedCharacteristic!.characteristic.properties;
if (properties.contains(CharacteristicProperty.notify)) {
inputProperty = BleInputProperty.notification;
} else if (properties.contains(CharacteristicProperty.indicate)) {
inputProperty = BleInputProperty.indication;
} else {
throw 'No notify or indicate property';
}
}
await UniversalBle.setNotifiable(
widget.deviceId,
selectedCharacteristic!.service.uuid,
selectedCharacteristic!.characteristic.uuid,
inputProperty,
);
_addLog('BleInputProperty', inputProperty);
var subscription = _getCharacteristicSubscription(selectedCharacteristic);
if (subscription == null) throw 'No notify or indicate property';
await subscription.subscribe();
_addLog('BleCharSubscription', 'Subscribed');
// Updates can also be handled by
// subscription.listen((data) {});
} catch (e) {
_addLog('NotifyError', e);
}
}
Future<void> _unsubscribeChar() async {
try {
await selectedCharacteristic?.unsubscribe();
_addLog('BleCharSubscription', 'UnSubscribed');
} catch (e) {
_addLog('NotifyError', e);
}
}
CharacteristicSubscription? _getCharacteristicSubscription(
BleCharacteristic characteristic) {
var properties = characteristic.properties;
if (properties.contains(CharacteristicProperty.notify)) {
return characteristic.notifications;
} else if (properties.contains(CharacteristicProperty.indicate)) {
return characteristic.indications;
}
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("${widget.deviceName} - ${widget.deviceId}"),
title: Text("${bleDevice.name ?? "Unknown"} - ${bleDevice.deviceId}"),
elevation: 4,
actions: [
Padding(
@@ -234,13 +228,10 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
: ServicesListWidget(
discoveredServices: discoveredServices,
scrollable: true,
onTap: (BleService service,
BleCharacteristic characteristic) {
onTap: (service, characteristic) {
setState(() {
selectedCharacteristic = (
service: service,
characteristic: characteristic
);
selectedService = service;
selectedCharacteristic = characteristic;
});
},
),
@@ -264,9 +255,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
enabled: !isConnected,
onPressed: () async {
try {
await UniversalBle.connect(
widget.deviceId,
);
await bleDevice.connect();
_addLog("ConnectionResult", true);
} catch (e) {
_addLog('ConnectError (${e.runtimeType})', e);
@@ -277,7 +266,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
text: 'Disconnect',
enabled: isConnected,
onPressed: () {
UniversalBle.disconnect(widget.deviceId);
bleDevice.disconnect();
},
),
],
@@ -294,17 +283,17 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
child: Card(
child: ListTile(
title: SelectableText(
"Characteristic: ${selectedCharacteristic!.characteristic.uuid}",
"Characteristic: ${selectedCharacteristic?.uuid}",
),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SelectableText(
"Service: ${selectedCharacteristic!.service.uuid}",
"Service: ${selectedService?.uuid}",
),
Text(
"Properties: ${selectedCharacteristic!.characteristic.properties.map((e) => e.name)}",
"Properties: ${selectedCharacteristic?.properties.map((e) => e.name)}",
),
],
),
@@ -360,9 +349,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
onPressed: () async {
_addLog(
'ConnectionState',
await UniversalBle.getConnectionState(
widget.deviceId,
),
await bleDevice.connectionState,
);
},
text: 'Connection State',
@@ -371,8 +358,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
PlatformButton(
enabled: isConnected,
onPressed: () async {
int mtu = await UniversalBle.requestMtu(
widget.deviceId, 247);
int mtu = await bleDevice.requestMtu(247);
_addLog('MTU', mtu);
},
text: 'Request Mtu',
@@ -403,8 +389,7 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
CharacteristicProperty.notify,
CharacteristicProperty.indicate
]),
onPressed: () => _setBleInputProperty(
BleInputProperty.notification),
onPressed: _subscribeChar,
text: 'Subscribe',
),
PlatformButton(
@@ -414,21 +399,19 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
CharacteristicProperty.notify,
CharacteristicProperty.indicate
]),
onPressed: () => _setBleInputProperty(
BleInputProperty.disabled),
onPressed: _unsubscribeChar,
text: 'Unsubscribe',
),
PlatformButton(
enabled: BleCapabilities.supportsAllPairingKinds,
onPressed: () async {
try {
await UniversalBle.pair(
widget.deviceId,
// pairingCommand: BleCommand(
// service: "",
// characteristic: "",
// ),
);
await bleDevice.pair(
// pairingCommand: BleCommand(
// service: "",
// characteristic: "",
// ),
);
_addLog("Pairing Result", true);
} catch (e) {
_addLog('PairError (${e.runtimeType})', e);
@@ -438,20 +421,19 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
),
PlatformButton(
onPressed: () async {
bool? isPaired = await UniversalBle.isPaired(
widget.deviceId,
// pairingCommand: BleCommand(
// service: "",
// characteristic: "",
// ),
);
_addLog('IsPaired', isPaired);
bool? isPaired = await bleDevice.isPaired(
// pairingCommand: BleCommand(
// service: "",
// characteristic: "",
// ),
);
_addLog('isPaired', isPaired);
},
text: 'IsPaired',
text: 'isPaired',
),
PlatformButton(
onPressed: () async {
await UniversalBle.unpair(widget.deviceId);
await bleDevice.unpair();
},
text: 'Unpair',
),
@@ -462,13 +444,10 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
if (deviceType != DeviceType.desktop)
ServicesListWidget(
discoveredServices: discoveredServices,
onTap: (BleService service,
BleCharacteristic characteristic) {
onTap: (service, characteristic) {
setState(() {
selectedCharacteristic = (
service: service,
characteristic: characteristic
);
selectedService = service;
selectedCharacteristic = characteristic;
});
},
),
@@ -497,9 +476,8 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
}
bool _hasSelectedCharacteristicProperty(
List<CharacteristicProperty> properties) =>
properties.any((property) =>
selectedCharacteristic?.characteristic.properties
.contains(property) ??
false);
List<CharacteristicProperty> properties) {
return properties.any((property) =>
selectedCharacteristic?.properties.contains(property) ?? false);
}
}
+2 -2
View File
@@ -20,9 +20,9 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/universal_ble/darwin
SPEC CHECKSUMS:
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6
universal_ble: ff19787898040d721109c6324472e5dd4bc86adc
PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
+1 -1
View File
@@ -402,7 +402,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.19.1"
version: "0.20.0"
vector_math:
dependency: transitive
description:
@@ -0,0 +1,137 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
/// Extension methods for [BleCharacteristic] to simplify common operations.
extension BleCharacteristicExtension on BleCharacteristic {
/// A stream of [Uint8List] that emits values received from the characteristic.
Stream<Uint8List> get onValueReceived =>
UniversalBle.characteristicValueStream(_deviceId, uuid);
/// Subscribes to notifications for this characteristic.
///
/// Throws an exception if the characteristic does not support notifications.
CharacteristicSubscription get notifications =>
CharacteristicSubscription(this, CharacteristicProperty.notify);
/// Subscribes to indications for this characteristic.
///
/// Throws an exception if the characteristic does not support indications.
CharacteristicSubscription get indications =>
CharacteristicSubscription(this, CharacteristicProperty.indicate);
/// Unsubscribes notifications/indications from this characteristic.
Future<void> unsubscribe() =>
UniversalBle.unsubscribe(_deviceId, _serviceId, uuid);
/// Reads the current value of the characteristic.
Future<Uint8List> read() => UniversalBle.read(
_deviceId,
_serviceId,
uuid,
);
/// Writes a value to the characteristic.
///
/// [value] is the list of bytes to write.
/// [withResponse] indicates whether the write should be performed with a response from the device.
/// Default is true, meaning the device will acknowledge the write operation.
/// If set to false, the write operation will be performed without waiting for a response.
Future<void> write(List<int> value, {bool withResponse = true}) async {
await UniversalBle.write(
_deviceId,
_serviceId,
uuid,
Uint8List.fromList(value),
withoutResponse: !withResponse,
);
}
String get _deviceId {
String? deviceId = metaData?.deviceId;
if (deviceId == null) {
throw "DeviceId is not preset in characteristic metaData";
}
return deviceId;
}
String get _serviceId {
String? serviceId = metaData?.serviceId;
if (serviceId == null) {
throw "ServiceId is not preset in characteristic metaData";
}
return serviceId;
}
}
/// Manages subscription to a characteristic's notifications or indications.
///
/// Instances are typically obtained via the `notifications` or `indications`
/// getters on `BleCharacteristic`.
///
/// call [subscribe] to instruct the device to start sending data.
/// call [unsubscribe] To stop receiving data and instruct the device to cease sending,
/// call [listen] to register a callback to receive this data..
/// use [isSupported] to check if this operation is supported by the characteristic
///
class CharacteristicSubscription {
final BleCharacteristic _characteristic;
final CharacteristicProperty _property;
/// Indicates whether the characteristic supports the requested subscription type
/// (notifications or indications).
final bool isSupported;
CharacteristicSubscription(
this._characteristic,
this._property,
) : isSupported = _characteristic.properties.contains(_property);
/// Registers a listener for incoming data from the characteristic.
StreamSubscription listen(
void Function(Uint8List event) onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return _characteristic.onValueReceived.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
/// Subscribes to this characteristic.
Future<void> subscribe() {
if (!isSupported) throw Exception('Operation not supported');
if (_property == CharacteristicProperty.indicate) {
return UniversalBle.subscribeIndications(
_characteristic._deviceId,
_characteristic._serviceId,
_characteristic.uuid,
);
}
return UniversalBle.subscribeNotifications(
_characteristic._deviceId,
_characteristic._serviceId,
_characteristic.uuid,
);
}
/// Unsubscribes from this characteristic.
Future<void> unsubscribe() {
if (!isSupported) throw Exception('Operation not supported');
return UniversalBle.unsubscribe(
_characteristic._deviceId,
_characteristic._serviceId,
_characteristic.uuid,
);
}
@override
String toString() =>
"CharacteristicSubscription(property: ${_property.name}, isSupported: $isSupported, characteristic: ${_characteristic.uuid})";
}
@@ -0,0 +1,126 @@
import 'package:universal_ble/src/utils/cache_handler.dart';
import 'package:universal_ble/universal_ble.dart';
/// Extension methods for [BleDevice] to simplify common operations.
extension BleDeviceExtension on BleDevice {
/// A stream of [bool] that emits connection status changes for the device.
Stream<bool> get connectionStream => UniversalBle.connectionStream(deviceId);
/// A stream of [bool] that emits pairing status changes for the device.
Stream<bool> get pairingStateStream =>
UniversalBle.pairingStateStream(deviceId);
/// Checks if the device is currently connected.
Future<bool> get isConnected async =>
await UniversalBle.getConnectionState(deviceId) ==
BleConnectionState.connected;
/// Connects to the device.
Future<void> connect() => UniversalBle.connect(deviceId);
/// Disconnects from the device.
Future<void> disconnect() => UniversalBle.disconnect(deviceId);
/// Requests a specific MTU (Maximum Transmission Unit) size for the connection.
Future<int> requestMtu(int expectedMtu) =>
UniversalBle.requestMtu(deviceId, expectedMtu);
/// Check if a device is paired.
///
/// For `Apple` and `Web`, you have to pass a "pairingCommand" with an encrypted read or write characteristic.
/// Returns true/false if it manages to execute the command.
/// Returns null when no `pairingCommand` is passed.
/// Note that it will trigger pairing if the device is not already paired.
Future<bool?> isPaired({
BleCommand? pairingCommand,
Duration? connectionTimeout,
}) {
return UniversalBle.isPaired(
deviceId,
pairingCommand: pairingCommand,
connectionTimeout: connectionTimeout,
);
}
/// Pair a device.
///
/// It throws error if pairing fails.
///
/// On `Apple` and `Web`, it only works on devices with encrypted characteristics.
/// It is advised to pass a pairingCommand with an encrypted read or write characteristic.
/// When not passing a pairingCommand, you should afterwards use [isPaired] with a pairingCommand
/// to verify the pairing state.
///
/// On `Web/Windows` and `Web/Linux`, it does not work for devices that use `ConfirmOnly` pairing.
/// Can throw `PairingException`, `ConnectionException` or `PlatformException`.
Future<void> pair({
BleCommand? pairingCommand,
Duration? connectionTimeout,
}) {
return UniversalBle.pair(
deviceId,
pairingCommand: pairingCommand,
connectionTimeout: connectionTimeout,
);
}
/// Unpair a device.
///
/// It might throw an error if device is not paired.
Future<void> unpair() => UniversalBle.unpair(deviceId);
/// Discovers the services offered by the device.
///
/// Returns cached services if already discovered after connection.
Future<List<BleService>> discoverServices() async {
List<BleService> servicesCache =
await UniversalBle.discoverServices(deviceId);
CacheHandler.instance.saveServices(deviceId, servicesCache);
return servicesCache;
}
/// Retrieves a specific service.
///
/// [service] is the UUID of the service.
/// [preferCached] indicates whether to use cached services. If cache is empty, discoverServices() will be called.
/// might throw [NotFoundException]
Future<BleService> getService(
String service, {
bool preferCached = true,
}) async {
List<BleService> discoveredServices = [];
if (preferCached) {
discoveredServices = CacheHandler.instance.getServices(deviceId) ?? [];
}
if (discoveredServices.isEmpty) {
discoveredServices = await discoverServices();
}
if (discoveredServices.isEmpty) {
throw ServiceNotFoundException('No services found');
}
return discoveredServices.firstWhere(
(s) => BleUuidParser.compareStrings(s.uuid, service),
orElse: () => throw ServiceNotFoundException(
'Service "$service" not available',
),
);
}
/// Retrieves a specific characteristic from a service.
///
/// [service] is the UUID of the service.
/// [characteristic] is the UUID of the characteristic.
/// [preferCached] indicates whether to use cached services. If cache is empty, discoverServices() will be called.
/// might throw [NotFoundException]
Future<BleCharacteristic> getCharacteristic(
String characteristic, {
required String service,
bool preferCached = true,
}) async {
BleService bluetoothService =
await getService(service, preferCached: preferCached);
return bluetoothService.getCharacteristic(characteristic);
}
}
@@ -0,0 +1,20 @@
import 'package:universal_ble/universal_ble.dart';
/// Extension methods for [BleService] objects.
extension BleServiceExtension on BleService {
/// Retrieves a [BleCharacteristic] from the service by its UUID.
///
/// Throws an error if no characteristics are found or if the characteristic
/// with the given UUID is not available.
BleCharacteristic getCharacteristic(String characteristicId) {
if (characteristics.isEmpty) {
throw CharacteristicNotFoundException('No characteristics found');
}
return characteristics.firstWhere(
(c) => BleUuidParser.compareStrings(c.uuid, characteristicId),
orElse: () => throw CharacteristicNotFoundException(
'Characteristic "$characteristicId" not available',
),
);
}
}
+3
View File
@@ -0,0 +1,3 @@
export 'package:universal_ble/src/extensions/ble_characteristic_extension.dart';
export 'package:universal_ble/src/extensions/ble_service_extension.dart';
export 'package:universal_ble/src/extensions/ble_device_extension.dart';
+2 -2
View File
@@ -8,8 +8,8 @@ class BleCapabilities {
/// because they only support "Numeric Comparison" and "Passkey Entry".
///
/// For more fine-grained control it is recommended to use `triggersConfirmOnlyPairing`
/// in conjunction with the pairing method of your peripheral,
/// e.g. if (!BleCapabilities.triggersConfirmOnlyPairing && peripheralUsesConfirmOnlyPairing) throw "In-app pairing not supported";
/// in conjunction with the pairing method of your device,
/// e.g. if (!BleCapabilities.triggersConfirmOnlyPairing && deviceUsesConfirmOnlyPairing) throw "In-app pairing not supported";
static final bool supportsAllPairingKinds =
triggersConfirmOnlyPairing || hasSystemPairingApi;
+5 -3
View File
@@ -7,7 +7,9 @@ class BleDevice {
String? name;
String? rawName;
int? rssi;
bool? isPaired;
bool? paired;
/// List of services advertised by the device.
List<String> services;
bool? isSystemDevice;
List<ManufacturerData> manufacturerDataList;
@@ -32,7 +34,7 @@ class BleDevice {
required this.deviceId,
required String? name,
this.rssi,
this.isPaired,
this.paired,
this.services = const [],
this.isSystemDevice,
this.manufacturerDataList = const [],
@@ -47,7 +49,7 @@ class BleDevice {
'deviceId: $deviceId, '
'name: $name, '
'rssi: $rssi, '
'isPaired: $isPaired, '
'paired: $paired, '
'services: $services, '
'isSystemDevice: $isSystemDevice, '
'manufacturerDataList: $manufacturerDataList';
-8
View File
@@ -2,17 +2,9 @@ enum BleInputProperty {
disabled,
notification,
indication;
const BleInputProperty();
factory BleInputProperty.parse(int index) => BleInputProperty.values[index];
}
enum BleOutputProperty {
withResponse,
withoutResponse;
const BleOutputProperty();
factory BleOutputProperty.parse(int index) => BleOutputProperty.values[index];
}
+35 -8
View File
@@ -1,12 +1,13 @@
import 'package:universal_ble/universal_ble.dart';
class BleService {
late String uuid;
String uuid;
List<BleCharacteristic> characteristics;
BleService(String uuid, this.characteristics) {
this.uuid = BleUuidParser.string(uuid);
}
BleService(
String uuid,
this.characteristics,
) : uuid = BleUuidParser.string(uuid);
@override
String toString() {
@@ -15,17 +16,43 @@ class BleService {
}
class BleCharacteristic {
late String uuid;
String uuid;
List<CharacteristicProperty> properties;
({String deviceId, String serviceId})? metaData;
BleCharacteristic(String uuid, this.properties) {
this.uuid = BleUuidParser.string(uuid);
}
BleCharacteristic(
String uuid,
this.properties,
) : uuid = BleUuidParser.string(uuid);
BleCharacteristic.withMetaData({
required String deviceId,
required String serviceId,
required String uuid,
required this.properties,
}) : uuid = BleUuidParser.string(uuid),
metaData = (
deviceId: deviceId,
serviceId: BleUuidParser.string(serviceId),
);
@override
String toString() {
return 'BleCharacteristic{uuid: $uuid, properties: $properties}';
}
@override
bool operator ==(Object other) {
if (other is! BleCharacteristic) return false;
if (other.uuid != uuid) return false;
if (other.properties != properties) return false;
if (other.metaData?.deviceId != metaData?.deviceId) return false;
if (other.metaData?.serviceId != metaData?.serviceId) return false;
return true;
}
@override
int get hashCode => uuid.hashCode ^ properties.hashCode ^ metaData.hashCode;
}
enum CharacteristicProperty {
+1 -1
View File
@@ -4,7 +4,7 @@ class ManufacturerDataFilter {
/// Must be of integer type, in hex or decimal form (e.g. 0x004c or 76).
int companyIdentifier;
/// Matches as prefix the peripheral's advertised data.
/// Matches as prefix the device's advertised data.
Uint8List? payloadPrefix;
/// For each bit in the mask, set it to 1 if it needs to match
+120 -35
View File
@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:universal_ble/src/ble_command_queue.dart';
import 'package:universal_ble/src/utils/ble_command_queue.dart';
import 'package:universal_ble/src/universal_ble_linux/universal_ble_linux.dart';
import 'package:universal_ble/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart';
import 'package:universal_ble/src/universal_ble_web/universal_ble_web.dart';
import 'package:universal_ble/src/universal_logger.dart';
import 'package:universal_ble/src/utils/universal_logger.dart';
import 'package:universal_ble/universal_ble.dart';
class UniversalBle {
@@ -71,12 +71,11 @@ class UniversalBle {
ScanFilter? scanFilter,
PlatformConfig? platformConfig,
}) async {
return await _bleCommandQueue.queueCommand(
return await _bleCommandQueue.queueCommandWithoutTimeout(
() => _platform.startScan(
scanFilter: scanFilter,
platformConfig: platformConfig,
),
withTimeout: false,
);
}
@@ -84,9 +83,8 @@ class UniversalBle {
/// Set [onScanResult] listener to `null` if you don't need it anymore.
/// It might throw errors if Bluetooth is not available.
static Future<void> stopScan() async {
return await _bleCommandQueue.queueCommand(
return await _bleCommandQueue.queueCommandWithoutTimeout(
() => _platform.stopScan(),
withTimeout: false,
);
}
@@ -157,29 +155,54 @@ class UniversalBle {
}
/// Set a characteristic notifiable.
/// Set `bleInputProperty` to [BleInputProperty.notification] or [BleInputProperty.indication].
/// Updates will arrive in [onValueChange] listener.
/// To stop listening to a characteristic, set `bleInputProperty` to [BleInputProperty.disabled].
static Future<void> setNotifiable(
/// Updates will arrive in [onValueChange] listener and [characteristicValueStream]
/// call [unsubscribe] to stop updates
static Future<void> subscribeNotifications(
String deviceId,
String service,
String characteristic,
BleInputProperty bleInputProperty,
) async {
return await _bleCommandQueue.queueCommand(
() => _platform.setNotifiable(
deviceId,
BleUuidParser.string(service),
BleUuidParser.string(characteristic),
bleInputProperty,
),
deviceId: deviceId,
return _sendBleInputPropertyCommand(
deviceId,
service,
characteristic,
BleInputProperty.notification,
);
}
/// Set a characteristic notifiable.
/// Updates will arrive in [onValueChange] listener and [characteristicValueStream]
/// call [unsubscribe] to stop updates
static Future<void> subscribeIndications(
String deviceId,
String service,
String characteristic,
) async {
return _sendBleInputPropertyCommand(
deviceId,
service,
characteristic,
BleInputProperty.indication,
);
}
/// Stop characteristic notifications/indication updates
static Future<void> unsubscribe(
String deviceId,
String service,
String characteristic,
) async {
return _sendBleInputPropertyCommand(
deviceId,
service,
characteristic,
BleInputProperty.disabled,
);
}
/// Read a characteristic value.
/// On iOS and MacOS this command will also trigger [onValueChange] listener.
static Future<Uint8List> readValue(
static Future<Uint8List> read(
String deviceId,
String service,
String characteristic, {
@@ -198,21 +221,23 @@ class UniversalBle {
}
/// Write a characteristic value.
/// To write a characteristic value with response, set `bleOutputProperty` to [BleOutputProperty.withResponse].
static Future<void> writeValue(
/// To write a characteristic value without response, set [withoutResponse] to [true].
static Future<void> write(
String deviceId,
String service,
String characteristic,
Uint8List value,
BleOutputProperty bleOutputProperty,
) async {
Uint8List value, {
bool withoutResponse = false,
}) async {
await _bleCommandQueue.queueCommand(
() => _platform.writeValue(
deviceId,
BleUuidParser.string(service),
BleUuidParser.string(characteristic),
value,
bleOutputProperty,
withoutResponse
? BleOutputProperty.withoutResponse
: BleOutputProperty.withResponse,
),
deviceId: deviceId,
);
@@ -367,6 +392,66 @@ class UniversalBle {
}
}
@Deprecated(
"Use [subscribeNotifications] or [subscribeIndications] or [unsubscribe] instead")
static Future<void> setNotifiable(
String deviceId,
String service,
String characteristic,
BleInputProperty bleInputProperty,
) async {
return _sendBleInputPropertyCommand(
deviceId,
service,
characteristic,
bleInputProperty,
);
}
@Deprecated("Use [write] instead")
static Future<void> writeValue(
String deviceId,
String service,
String characteristic,
Uint8List value,
BleOutputProperty bleOutputProperty,
) async {
await write(
deviceId,
service,
characteristic,
value,
withoutResponse: bleOutputProperty == BleOutputProperty.withoutResponse,
);
}
@Deprecated("Use [read] instead")
static Future<Uint8List> readValue(
String deviceId,
String service,
String characteristic, {
final Duration? timeout,
}) {
return read(deviceId, service, characteristic, timeout: timeout);
}
static Future<void> _sendBleInputPropertyCommand(
String deviceId,
String service,
String characteristic,
BleInputProperty bleInputProperty,
) async {
return await _bleCommandQueue.queueCommand(
() => _platform.setNotifiable(
deviceId,
BleUuidParser.string(service),
BleUuidParser.string(characteristic),
bleInputProperty,
),
deviceId: deviceId,
);
}
static Future<void> _connectAndExecuteBleCommand(
String deviceId,
BleCommand? bleCommand, {
@@ -407,7 +492,7 @@ class UniversalBle {
for (BleCharacteristic characteristic in service.characteristics) {
if (characteristic.properties.contains(CharacteristicProperty.read)) {
containsReadCharacteristics = true;
await readValue(
await read(
deviceId,
service.uuid,
characteristic.uuid,
@@ -447,12 +532,12 @@ class UniversalBle {
}
// Check if BleCommand Supports Read or Write
BleOutputProperty? bleOutputProperty;
bool? withoutResponse;
if (characteristic.properties.contains(CharacteristicProperty.write)) {
bleOutputProperty = BleOutputProperty.withResponse;
withoutResponse = false;
} else if (characteristic.properties
.contains(CharacteristicProperty.writeWithoutResponse)) {
bleOutputProperty = BleOutputProperty.withoutResponse;
withoutResponse = true;
} else if (!characteristic.properties
.contains(CharacteristicProperty.read)) {
throw PairingException(
@@ -463,17 +548,17 @@ class UniversalBle {
Uint8List? value = bleCommand.writeValue;
try {
if (value != null && bleOutputProperty != null) {
await writeValue(
if (value != null && withoutResponse != null) {
await write(
deviceId,
bleCommand.service,
bleCommand.characteristic,
value,
bleOutputProperty,
withoutResponse: withoutResponse,
);
} else {
// Fallback to read if supported
await readValue(
await read(
deviceId,
bleCommand.service,
bleCommand.characteristic,
@@ -497,7 +582,7 @@ class UniversalBle {
static set onConnectionChange(OnConnectionChange? onConnectionChange) =>
_platform.onConnectionChange = onConnectionChange;
/// Get characteristic value updates, set `bleInputProperty` in [setNotifiable] to [BleInputProperty.notification] or [BleInputProperty.indication].
/// Get characteristic value updates, after calling [subscribeNotifications] or [subscribeIndications]
static set onValueChange(OnValueChange? onValueChange) =>
_platform.onValueChange = onValueChange;
+18
View File
@@ -30,6 +30,24 @@ class WebBluetoothGloballyDisabled implements Exception {
String toString() => message;
}
class NotFoundException implements Exception {}
class ServiceNotFoundException implements NotFoundException {
String message;
ServiceNotFoundException(this.message);
@override
String toString() => message;
}
class CharacteristicNotFoundException implements NotFoundException {
String message;
CharacteristicNotFoundException(this.message);
@override
String toString() => message;
}
String _errorParser(dynamic error) {
if (error == null) {
return "Failed";
@@ -3,9 +3,9 @@ import 'dart:async';
import 'package:bluez/bluez.dart';
import 'package:flutter/services.dart';
import 'package:universal_ble/src/models/model_exports.dart';
import 'package:universal_ble/src/universal_ble_filter_util.dart';
import 'package:universal_ble/src/utils/universal_ble_filter_util.dart';
import 'package:universal_ble/src/universal_ble_platform_interface.dart';
import 'package:universal_ble/src/universal_logger.dart';
import 'package:universal_ble/src/utils/universal_logger.dart';
class UniversalBleLinux extends UniversalBlePlatform {
UniversalBleLinux._();
@@ -185,14 +185,23 @@ class UniversalBleLinux extends UniversalBlePlatform {
List<BleService> services = [];
for (final service in device.gattServices) {
String serviceId = service.uuid.toString();
final characteristics = service.characteristics.map((e) {
final properties = List<CharacteristicProperty>.from(e.flags
.map((e) => e.toCharacteristicProperty())
.where((element) => element != null)
.toList());
return BleCharacteristic(e.uuid.toString(), properties);
return BleCharacteristic.withMetaData(
deviceId: deviceId,
serviceId: serviceId,
uuid: e.uuid.toString(),
properties: properties,
);
}).toList();
services.add(BleService(service.uuid.toString(), characteristics));
services.add(
BleService(serviceId, characteristics),
);
}
return services;
}
@@ -604,7 +613,7 @@ extension BlueZDeviceExtension on BlueZDevice {
return BleDevice(
name: name,
deviceId: address,
isPaired: paired,
paired: paired,
rssi: rssi,
isSystemDevice: isSystemDevice,
services: uuids.map((e) => e.toString()).toList(),
@@ -69,7 +69,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
await _channel.discoverServices(deviceId);
return List<BleService>.from(universalBleServices
.where((e) => e != null)
.map((e) => e!.toBleService())
.map((e) => e!.toBleService(deviceId))
.toList());
}
@@ -169,14 +169,16 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform {
}
extension _BleServiceExtension on UniversalBleService {
BleService toBleService() {
BleService toBleService(String deviceId) {
List<BleCharacteristic> bleCharacteristics = [];
for (UniversalBleCharacteristic? characteristic in characteristics ?? []) {
if (characteristic == null) continue;
List<int?>? properties = List<int?>.from(characteristic.properties);
bleCharacteristics.add(BleCharacteristic(
characteristic.uuid,
List<CharacteristicProperty>.from(
bleCharacteristics.add(BleCharacteristic.withMetaData(
deviceId: deviceId,
serviceId: uuid,
uuid: characteristic.uuid,
properties: List<CharacteristicProperty>.from(
properties.map((e) => CharacteristicProperty.parse(e ?? 1)),
),
));
@@ -228,7 +230,7 @@ extension _UniversalBleScanResultExtension on UniversalBleScanResult {
name: name,
deviceId: deviceId,
rssi: rssi,
isPaired: isPaired,
paired: isPaired,
isSystemDevice: isSystemDevice,
services: services?.map(BleUuidParser.string).toList() ?? [],
manufacturerDataList: manufacturerDataList
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:universal_ble/src/universal_ble_stream_controller.dart';
import 'package:universal_ble/src/utils/cache_handler.dart';
import 'package:universal_ble/src/utils/universal_ble_stream_controller.dart';
import 'package:universal_ble/universal_ble.dart';
abstract class UniversalBlePlatform {
@@ -122,6 +123,10 @@ abstract class UniversalBlePlatform {
try {
onConnectionChange?.call(deviceId, isConnected, error);
} catch (_) {}
if (!isConnected) {
CacheHandler.instance.resetDeviceCache(deviceId);
}
}
void updateCharacteristicValue(
@@ -3,7 +3,7 @@ import 'dart:collection';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart';
import 'package:universal_ble/src/universal_logger.dart';
import 'package:universal_ble/src/utils/universal_logger.dart';
import 'package:universal_ble/universal_ble.dart';
class UniversalBleWeb extends UniversalBlePlatform {
@@ -58,7 +58,9 @@ class UniversalBleWeb extends UniversalBlePlatform {
@override
Future<List<BleService>> discoverServices(String deviceId) async =>
(await _getServices(deviceId)).map((e) => e._bleService).toList();
(await _getServices(deviceId))
.map((e) => e._bleService(deviceId))
.toList();
@override
Future<AvailabilityState> getBluetoothAvailabilityState() async {
@@ -463,20 +465,24 @@ class _UniversalWebBluetoothService {
return null;
}
BleService get _bleService => BleService(
BleService _bleService(String deviceId) => BleService(
service.uuid,
characteristics.map((e) {
return BleCharacteristic(e.uuid, [
if (e.properties.broadcast) CharacteristicProperty.broadcast,
if (e.properties.read) CharacteristicProperty.read,
if (e.properties.write) CharacteristicProperty.write,
if (e.properties.writeWithoutResponse)
CharacteristicProperty.writeWithoutResponse,
if (e.properties.notify) CharacteristicProperty.notify,
if (e.properties.indicate) CharacteristicProperty.indicate,
if (e.properties.authenticatedSignedWrites)
CharacteristicProperty.authenticatedSignedWrites,
]);
return BleCharacteristic.withMetaData(
deviceId: deviceId,
serviceId: service.uuid,
uuid: e.uuid,
properties: [
if (e.properties.broadcast) CharacteristicProperty.broadcast,
if (e.properties.read) CharacteristicProperty.read,
if (e.properties.write) CharacteristicProperty.write,
if (e.properties.writeWithoutResponse)
CharacteristicProperty.writeWithoutResponse,
if (e.properties.notify) CharacteristicProperty.notify,
if (e.properties.indicate) CharacteristicProperty.indicate,
if (e.properties.authenticatedSignedWrites)
CharacteristicProperty.authenticatedSignedWrites,
]);
}).toList(),
);
}
@@ -13,16 +13,28 @@ class BleCommandQueue {
Future<T> queueCommand<T>(
Future<T> Function() command, {
bool withTimeout = true,
String? deviceId,
Duration? timeout,
}) {
Duration? duration = timeout ?? (withTimeout ? this.timeout : null);
Duration? timeoutDuration = timeout ?? this.timeout;
if (timeoutDuration == null) {
return queueCommandWithoutTimeout(command, deviceId: deviceId);
}
return switch (queueType) {
QueueType.global => _queue().add(command, duration),
QueueType.perDevice => _queue(deviceId).add(command, duration),
QueueType.none =>
duration != null ? command().timeout(duration) : command(),
QueueType.global => _queue().add(command, timeoutDuration),
QueueType.perDevice => _queue(deviceId).add(command, timeoutDuration),
QueueType.none => command().timeout(timeoutDuration),
};
}
Future<T> queueCommandWithoutTimeout<T>(
Future<T> Function() command, {
String? deviceId,
}) {
return switch (queueType) {
QueueType.global => _queue().add(command),
QueueType.perDevice => _queue(deviceId).add(command),
QueueType.none => command(),
};
}
+28
View File
@@ -0,0 +1,28 @@
import 'package:universal_ble/src/models/model_exports.dart';
/// Manages an in-memory cache for Bluetooth devices
class CacheHandler {
static CacheHandler? _instance;
static CacheHandler get instance => _instance ??= CacheHandler._();
CacheHandler._();
/// Internal cache to store discovered services for each device.
final Map<String, List<BleService>> _servicesCache = {};
/// Saves the discovered Bluetooth services for a specific device in the cache.
void saveServices(String deviceId, List<BleService>? services) {
if (services == null) {
_servicesCache.remove(deviceId);
} else {
_servicesCache[deviceId] = services;
}
}
/// Retrieves the cached Bluetooth services for a specific device.
List<BleService>? getServices(String deviceId) => _servicesCache[deviceId];
/// Resets the cache for a specific device, removing all stored services.
void resetDeviceCache(String deviceId) {
_servicesCache.remove(deviceId);
}
}
+1
View File
@@ -4,3 +4,4 @@ export 'package:universal_ble/src/universal_ble_exceptions.dart';
export 'package:universal_ble/src/universal_ble_platform_interface.dart';
export 'package:universal_ble/src/universal_ble.dart';
export 'package:universal_ble/src/models/model_exports.dart';
export 'package:universal_ble/src/extensions/exports.dart';
+1 -1
View File
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_ble/src/universal_ble_filter_util.dart';
import 'package:universal_ble/src/utils/universal_ble_filter_util.dart';
import 'dart:typed_data';
import 'package:universal_ble/universal_ble.dart';
+1 -1
View File
@@ -1,7 +1,7 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_ble/src/universal_ble_filter_util.dart';
import 'package:universal_ble/src/utils/universal_ble_filter_util.dart';
import 'package:universal_ble/universal_ble.dart';
void main() {
@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_ble/src/universal_ble_stream_controller.dart';
import 'package:universal_ble/src/utils/universal_ble_stream_controller.dart';
void main() {
group("Test UniversalBleStreamController", () {