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
@@ -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';