From 9bb209f8b59829fc52a434feaf309b7949b66a9b Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 5 Jul 2024 21:23:55 +0530 Subject: [PATCH] Unify UUID formatting (#55) * Unify UUID formatting * Update Readme * Update Readme * Apply suggestions from code review Co-authored-by: Foti Dim * Resolve Comments * Add test and support 32bit uuid parsing * Add a toString() method to the BleDevice class for easier debugging and logging * Rename Uuid to BleUuid * Rename uuid file to ble_uuid * Update Readme --------- Co-authored-by: Foti Dim --- README.md | 28 ++++++++ lib/src/models/ble_device.dart | 13 ++++ lib/src/models/ble_service.dart | 16 +++-- lib/src/models/ble_uuid.dart | 34 +++++++++ lib/src/models/model_exports.dart | 2 +- lib/src/models/uuid.dart | 34 --------- lib/src/universal_ble.dart | 14 ++-- .../universal_ble_linux.dart | 2 +- .../universal_ble_pigeon_channel.dart | 4 +- lib/src/universal_ble_platform_interface.dart | 7 ++ .../universal_ble_web/universal_ble_web.dart | 3 +- test/universal_ble_test.dart | 70 +++++++++++++++++++ 12 files changed, 178 insertions(+), 49 deletions(-) create mode 100644 lib/src/models/ble_uuid.dart delete mode 100644 lib/src/models/uuid.dart create mode 100644 test/universal_ble_test.dart diff --git a/README.md b/README.md index 2b131d8..12a511a 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Already connected devices, connected either through previous sessions, other app // On `Apple`, `withServices` is required to get connected devices, else [1800] service will be used as default filter. List devices = await UniversalBle.getSystemDevices(withServices: []); ``` + For each such device the `isSystemDevice` property will be `true`. You still need to explicitly [connect](#connecting) to them before being able to use them. @@ -259,6 +260,33 @@ UniversalBle.timeout = const Duration(seconds: 10); UniversalBle.timeout = null; ``` +## UUID format + +All characteristic and service UUIDs will be returned in lowercase and in 128 bit format, across all platforms. +e.g. `0000180a-0000-1000-8000-00805f9b34fb` + +When passing a UUID you can pass it in any character case or format (long/short) you want. + +You can use `BleUuid.parse()` to convert a string to 128 bit UUID format. For example: + +```dart +BleUuid.parse("180A"); // "0000180a-0000-1000-8000-00805f9b34fb" + +BleUuid.parse("0000180A-0000-1000-8000-00805F9B34FB"); // "0000180a-0000-1000-8000-00805f9b34fb" +``` + +or `BleUuid.extend()` to create a valid 128 bit Bluetooth UUID from short (16 or 32 bit) encoding. For example: + +```dart +BleUuid.extend(0x180A); // "0000180a-0000-1000-8000-00805f9b34fb" +``` + +or `BleUuid.equals()` to compare two UUIDs. For example: + +```dart +BleUuid.equals("180a","0000180A-0000-1000-8000-00805F9B34FB"); // true +``` + ## Platform-Specific Setup ### Android diff --git a/lib/src/models/ble_device.dart b/lib/src/models/ble_device.dart index ef83b48..ac1441e 100644 --- a/lib/src/models/ble_device.dart +++ b/lib/src/models/ble_device.dart @@ -31,6 +31,19 @@ class BleDevice { this.manufacturerDataHead = manufacturerDataHead ?? Uint8List.fromList([]); this.manufacturerData = manufacturerData ?? manufacturerDataHead; } + + @override + String toString() { + return 'BleDevice: ' + 'deviceId: $deviceId, ' + 'name: $name, ' + 'rssi: $rssi, ' + 'isPaired: $isPaired, ' + 'services: $services, ' + 'isSystemDevice: $isSystemDevice, ' + 'manufacturerDataHead: $manufacturerDataHead, ' + 'manufacturerData: $manufacturerData'; + } } /// Represents the manufacturer data of a BLE device. diff --git a/lib/src/models/ble_service.dart b/lib/src/models/ble_service.dart index 33f1139..9d903a2 100644 --- a/lib/src/models/ble_service.dart +++ b/lib/src/models/ble_service.dart @@ -1,13 +1,21 @@ +import 'package:universal_ble/universal_ble.dart'; + class BleService { - String uuid; + late String uuid; List characteristics; - BleService(this.uuid, this.characteristics); + + BleService(String uuid, this.characteristics) { + this.uuid = BleUuid.parse(uuid); + } } class BleCharacteristic { - String uuid; + late String uuid; List properties; - BleCharacteristic(this.uuid, this.properties); + + BleCharacteristic(String uuid, this.properties) { + this.uuid = BleUuid.parse(uuid); + } } enum CharacteristicProperty { diff --git a/lib/src/models/ble_uuid.dart b/lib/src/models/ble_uuid.dart new file mode 100644 index 0000000..034f238 --- /dev/null +++ b/lib/src/models/ble_uuid.dart @@ -0,0 +1,34 @@ +import 'package:bluez/bluez.dart'; + +class BleUuid { + /// Parse a String to valid UUID and convert a 16 bit UUID to 128 bit UUID + /// Throws `FormatException` if the UUID is invalid + static String parse(String uuid) { + if (uuid.length < 4) throw const FormatException("Invalid UUID"); + if (uuid.length <= 8) { + uuid = "${uuid.padLeft(8, '0')}-0000-1000-8000-00805f9b34fb"; + } else if (!uuid.contains("-")) { + if (uuid.length != 32) throw const FormatException("Invalid UUID"); + uuid = "${uuid.substring(0, 8)}-${uuid.substring(8, 12)}" + "-${uuid.substring(12, 16)}-${uuid.substring(16, 20)}-${uuid.substring(20, 32)}"; + } + return BlueZUUID.fromString(uuid).toString(); + } + + /// Parse 16/32 bit uuid like `0x1800` to 128 bit uuid like `00001800-0000-1000-8000-00805f9b34fb` + static String extend(int short) { + BlueZUUID blueZUUID = BlueZUUID.short(short); + return blueZUUID.toString(); + } + + /// Compare two UUIDs to automatically convert both to 128 bit UUIDs + /// Throws `FormatException` if the UUID is invalid + static bool equals(String uuid1, String uuid2) { + return parse(uuid1) == parse(uuid2); + } +} + +/// Parse a list of strings to a list of UUIDs +extension StringListToUUID on List { + List toValidUUIDList() => map(BleUuid.parse).toList(); +} diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index 580d486..7104da4 100644 --- a/lib/src/models/model_exports.dart +++ b/lib/src/models/model_exports.dart @@ -1,5 +1,5 @@ export 'package:universal_ble/src/models/queue_type.dart'; -export 'package:universal_ble/src/models/uuid.dart'; +export 'package:universal_ble/src/models/ble_uuid.dart'; export 'package:universal_ble/src/models/scan_filter.dart'; export 'package:universal_ble/src/models/ble_property.dart'; export 'package:universal_ble/src/models/ble_service.dart'; diff --git a/lib/src/models/uuid.dart b/lib/src/models/uuid.dart deleted file mode 100644 index 2ca8dd8..0000000 --- a/lib/src/models/uuid.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:bluez/bluez.dart'; - -class UUID { - late String value; - - UUID(String uuid) { - // To validate the short UUID - if (uuid.length <= 4) { - try { - int shortValue = int.parse(uuid, radix: 16); - value = BlueZUUID.short(shortValue).toString(); - return; - } catch (_) {} - } - // To validate the UUID - BlueZUUID.fromString(uuid); - value = uuid; - } - - factory UUID.fromShort(int short) { - BlueZUUID blueZUUID = BlueZUUID.short(short); - return UUID(blueZUUID.toString()); - } - - @override - String toString() { - return value; - } -} - -/// Parse a list of strings to a list of UUIDs -extension StringListToUUID on List { - List toValidUUIDList() => map((e) => UUID(e).value).toList(); -} diff --git a/lib/src/universal_ble.dart b/lib/src/universal_ble.dart index b74c313..3260efe 100644 --- a/lib/src/universal_ble.dart +++ b/lib/src/universal_ble.dart @@ -111,8 +111,8 @@ class UniversalBle { return await _bleCommandQueue.queueCommand( () => _platform.setNotifiable( deviceId, - service, - characteristic, + BleUuid.parse(service), + BleUuid.parse(characteristic), bleInputProperty, ), deviceId: deviceId, @@ -127,7 +127,11 @@ class UniversalBle { String characteristic, ) async { return await _bleCommandQueue.queueCommand( - () => _platform.readValue(deviceId, service, characteristic), + () => _platform.readValue( + deviceId, + BleUuid.parse(service), + BleUuid.parse(characteristic), + ), deviceId: deviceId, ); } @@ -144,8 +148,8 @@ class UniversalBle { await _bleCommandQueue.queueCommand( () => _platform.writeValue( deviceId, - service, - characteristic, + BleUuid.parse(service), + BleUuid.parse(characteristic), value, bleOutputProperty, ), diff --git a/lib/src/universal_ble_linux/universal_ble_linux.dart b/lib/src/universal_ble_linux/universal_ble_linux.dart index 97bc4d3..84891c2 100644 --- a/lib/src/universal_ble_linux/universal_ble_linux.dart +++ b/lib/src/universal_ble_linux/universal_ble_linux.dart @@ -192,7 +192,7 @@ class UniversalBleLinux extends UniversalBlePlatform { for (String property in properties) { switch (property) { case BluezProperty.value: - onValueChange?.call( + updateCharacteristicValue( deviceId, characteristic, Uint8List.fromList(char.value), diff --git a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart index a9eab8c..ee718f6 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -132,7 +132,7 @@ class UniversalBlePigeonChannel extends UniversalBlePlatform { onConnectionChange?.call(deviceId, connected), valueChanged: (String deviceId, String characteristicId, Uint8List value) => - onValueChange?.call(deviceId, characteristicId, value), + updateCharacteristicValue(deviceId, characteristicId, value), pairStateChange: (String deviceId, bool isPaired, String? error) => onPairingStateChange?.call(deviceId, isPaired, error), )); @@ -209,7 +209,7 @@ extension _UniversalBleScanResultExtension on UniversalBleScanResult { isSystemDevice: isSystemDevice, services: services ?.where((e) => e != null) - .map((e) => UUID(e!).toString()) + .map((e) => BleUuid.parse(e!)) .toList() ?? [], ); diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index dbffb68..0ad5931 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -50,6 +50,7 @@ abstract class UniversalBlePlatform { List? withServices, ); + /// `onScanResult` interceptor to filter by name void updateScanResult(BleDevice bleDevice) { // Filter by name ScanFilter? scanFilter = _scanFilter; @@ -61,6 +62,12 @@ abstract class UniversalBlePlatform { onScanResult?.call(bleDevice); } + /// `onValueChange` interceptor to parse the native uuids to 128 bit uuid, to keep consistency + void updateCharacteristicValue( + String deviceId, String characteristicId, Uint8List value) { + onValueChange?.call(deviceId, BleUuid.parse(characteristicId), value); + } + OnAvailabilityChange? onAvailabilityChange; OnScanResult? onScanResult; OnConnectionChange? onConnectionChange; diff --git a/lib/src/universal_ble_web/universal_ble_web.dart b/lib/src/universal_ble_web/universal_ble_web.dart index e22c0b3..7cd0792 100644 --- a/lib/src/universal_ble_web/universal_ble_web.dart +++ b/lib/src/universal_ble_web/universal_ble_web.dart @@ -21,7 +21,6 @@ class UniversalBleWeb extends UniversalBlePlatform { @override Future getConnectionState(String deviceId) async { - // TODO: Test this on Web (All platforms) BluetoothDevice? device = _getDeviceById(deviceId); bool connected = await device?.connected.first ?? false; return connected @@ -214,7 +213,7 @@ class UniversalBleWeb extends UniversalBlePlatform { _characteristicStreamList[characteristicKey] = bleCharacteristic.value .map((event) => event.buffer.asUint8List()) .listen((event) { - onValueChange?.call(deviceId, characteristic, event); + updateCharacteristicValue(deviceId, characteristic, event); }); } // Cancel Notification diff --git a/test/universal_ble_test.dart b/test/universal_ble_test.dart new file mode 100644 index 0000000..1c8b89e --- /dev/null +++ b/test/universal_ble_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:universal_ble/universal_ble.dart'; + +void main() { + group("UUID", () { + test("Invalid UUID", () { + expect( + () => BleUuid.parse('0x0180a'), + throwsFormatException, + ); + expect( + () => BleUuid.parse('0000-180a-0000-1000-8000-0080-5f9b-34fb'), + throwsFormatException, + ); + }); + + test("Valid UUID", () { + // Parse 128-bit lowercase uuid + expect( + BleUuid.parse('0000180a-0000-1000-8000-00805f9b34fb'), + equals("0000180a-0000-1000-8000-00805f9b34fb"), + ); + // Parse 128-bit uppercase uuid + expect( + BleUuid.parse('0000180A-0000-1000-8000-00805F9B34FB'), + equals("0000180a-0000-1000-8000-00805f9b34fb"), + ); + // Parse 16-bit uuid string to 128-bit uuid string + expect( + BleUuid.parse("180a"), + equals("0000180a-0000-1000-8000-00805f9b34fb"), + ); + // Parse 16-bit uuid to 128-bit + expect( + BleUuid.extend(0x180A), + equals("0000180a-0000-1000-8000-00805f9b34fb"), + ); + // 32-bit UUID + expect( + BleUuid.parse("0000180a"), + equals("0000180a-0000-1000-8000-00805f9b34fb"), + ); + // UUID without dashes + expect( + BleUuid.parse('0000180a00001000800000805f9b34fb'), + equals("0000180a-0000-1000-8000-00805f9b34fb"), + ); + }); + + test("Compare UUID", () { + // Compare UUID strings, case and format insensitive + expect( + BleUuid.equals('0000180a-0000-1000-8000-00805f9b34fb', '180a'), + isTrue, + ); + expect( + BleUuid.equals('180A', '180a'), + isTrue, + ); + expect( + BleUuid.equals('0000180A00001000800000805F9B34FB', '180a'), + isTrue, + ); + expect( + BleUuid.equals('0000180A', '0000180a-0000-1000-8000-00805f9b34fb'), + isTrue, + ); + }); + }); +}