From efaa72acbd84afd80463b1c6ae16e86b9405b2fb Mon Sep 17 00:00:00 2001 From: Foti Dim Date: Mon, 15 Jul 2024 05:59:30 +0200 Subject: [PATCH] Improve BleUuid API and tests (#62) * Improve tests * Fix tests * More improvements and ToDos * Final API changes Add tests * Update readme --------- Co-authored-by: Rohit Sangwan --- .github/workflows/test_ble_uuid_parser.yml | 36 +++ CHANGELOG.md | 2 +- README.md | 32 +-- example/lib/home/home.dart | 1 - .../lib/home/widgets/scan_filter_widget.dart | 2 +- lib/src/models/ble_service.dart | 4 +- .../{ble_uuid.dart => ble_uuid_parser.dart} | 37 ++- lib/src/models/model_exports.dart | 2 +- lib/src/universal_ble.dart | 12 +- .../universal_ble_pigeon_channel.dart | 2 +- lib/src/universal_ble_platform_interface.dart | 3 +- test/ble_uuid_parser_test.dart | 226 ++++++++++++++++++ test/universal_ble_test.dart | 74 ------ 13 files changed, 318 insertions(+), 115 deletions(-) create mode 100644 .github/workflows/test_ble_uuid_parser.yml rename lib/src/models/{ble_uuid.dart => ble_uuid_parser.dart} (54%) create mode 100644 test/ble_uuid_parser_test.dart delete mode 100644 test/universal_ble_test.dart diff --git a/.github/workflows/test_ble_uuid_parser.yml b/.github/workflows/test_ble_uuid_parser.yml new file mode 100644 index 0000000..5acc44c --- /dev/null +++ b/.github/workflows/test_ble_uuid_parser.yml @@ -0,0 +1,36 @@ +name: BleUuidParser Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Install dependencies + run: flutter pub get + + - name: Analyze project source + run: flutter analyze + + - name: Run Flutter tests + run: flutter test + + - name: Install Chrome + uses: browser-actions/setup-chrome@latest + + - name: Run Flutter tests on Chrome + uses: coactions/setup-xvfb@v1 + with: + run: flutter test --platform chrome \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1695bfb..0491197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## 0.11.0 * Unify UUID format across all platforms, 128-bit lowercase -* Add BleUUID utility methods for UUID parsing +* Add BleUuidParser utility methods for UUID parsing * Improve Android error handling * Fix Android disconnection events sometimes missed * Improve cleanup after disconnection on Apple and Android diff --git a/README.md b/README.md index 8bd952d..d1701f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Universal BLE +# UniversalBLE [![universal_ble version](https://img.shields.io/pub/v/universal_ble?label=universal_ble)](https://pub.dev/packages/universal_ble) @@ -16,6 +16,9 @@ A cross-platform (Android/iOS/macOS/Windows/Linux/Web) Bluetooth Low Energy (BLE - [Bluetooth Availability](#bluetooth-availability) - [Command Queue](#command-queue) - [Timeout](#timeout) +- [UUID Format Agnostic](#uuid-format-agnostic) + +## Usage ### API Support Matrix @@ -260,36 +263,37 @@ UniversalBle.timeout = const Duration(seconds: 10); UniversalBle.timeout = null; ``` -## UUID format +## UUID Format Agnostic -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` +UniversalBLE is agnostic to the UUID format of services and characteristics regardless of the platform the app runs on. When passing a UUID, you can pass it in any format (long/short) or character case (upper/lower case) you want. UniversalBLE will take care of necessary conversions, across all platforms, so that you don't need to worry about underlying platform differences. -When passing a UUID you can pass it in any character case or format (long/short) you want. The plugin will take care of conversions. +For consistency, all characteristic and service UUIDs will be returned in **lowercase 128-bit format**, across all platforms, e.g. `0000180a-0000-1000-8000-00805f9b34fb`. -### Utility methods +### Utility Methods -`BleUuid.parse()` converts a string to 128 bit UUID format: +If you need to convert any UUIDs in your app you can use the following methods. + +- `BleUuidParser.string()` converts a string to a 128-bit UUID formatted string: ```dart -BleUuid.parse("180A"); // "0000180a-0000-1000-8000-00805f9b34fb" +BleUuidParser.string("180A"); // "0000180a-0000-1000-8000-00805f9b34fb" -BleUuid.parse("0000180A-0000-1000-8000-00805F9B34FB"); // "0000180a-0000-1000-8000-00805f9b34fb" +BleUuidParser.string("0000180A-0000-1000-8000-00805F9B34FB"); // "0000180a-0000-1000-8000-00805f9b34fb" ``` -`BleUuid.extend()` creates a 128 bit Bluetooth UUID from short (16 or 32 bit) format: +- `BleUuidParser.number()` converts a number to a 128-bit UUID formatted string: ```dart -BleUuid.extend(0x180A); // "0000180a-0000-1000-8000-00805f9b34fb" +BleUuidParser.number(0x180A); // "0000180a-0000-1000-8000-00805f9b34fb" ``` -`BleUuid.equals()` compares two UUIDs: +- `BleUuidParser.compare()` compares two differently formatted UUIDs: ```dart -BleUuid.equals("180a","0000180A-0000-1000-8000-00805F9B34FB"); // true +BleUuidParser.compare("180a","0000180A-0000-1000-8000-00805F9B34FB"); // true ``` -## Platform-Specific Setup +## Platform-specific Setup ### Android diff --git a/example/lib/home/home.dart b/example/lib/home/home.dart index 24d6330..f6f52d1 100644 --- a/example/lib/home/home.dart +++ b/example/lib/home/home.dart @@ -1,6 +1,5 @@ // ignore_for_file: use_build_context_synchronously -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:universal_ble/universal_ble.dart'; import 'package:universal_ble_example/data/capabilities.dart'; diff --git a/example/lib/home/widgets/scan_filter_widget.dart b/example/lib/home/widgets/scan_filter_widget.dart index 898c5af..5d1a41d 100644 --- a/example/lib/home/widgets/scan_filter_widget.dart +++ b/example/lib/home/widgets/scan_filter_widget.dart @@ -37,7 +37,7 @@ class _ScanFilterWidgetState extends State { List services = widget.servicesFilterController.text.split(','); for (String service in services) { try { - serviceUUids.add(BleUuid.parse(service.trim())); + serviceUUids.add(BleUuidParser.string(service.trim())); } on FormatException catch (_) { throw Exception("Invalid Service UUID $service"); } diff --git a/lib/src/models/ble_service.dart b/lib/src/models/ble_service.dart index 9d903a2..304f32e 100644 --- a/lib/src/models/ble_service.dart +++ b/lib/src/models/ble_service.dart @@ -5,7 +5,7 @@ class BleService { List characteristics; BleService(String uuid, this.characteristics) { - this.uuid = BleUuid.parse(uuid); + this.uuid = BleUuidParser.string(uuid); } } @@ -14,7 +14,7 @@ class BleCharacteristic { List properties; BleCharacteristic(String uuid, this.properties) { - this.uuid = BleUuid.parse(uuid); + this.uuid = BleUuidParser.string(uuid); } } diff --git a/lib/src/models/ble_uuid.dart b/lib/src/models/ble_uuid_parser.dart similarity index 54% rename from lib/src/models/ble_uuid.dart rename to lib/src/models/ble_uuid_parser.dart index 95a7ba0..663f065 100644 --- a/lib/src/models/ble_uuid.dart +++ b/lib/src/models/ble_uuid_parser.dart @@ -1,11 +1,17 @@ -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) { +class BleUuidParser { + BleUuidParser._(); + + /// Parse a string to a valid 128-bit UUID. + /// Throws `FormatException` if the string does not hold a valid UUID format. + static String string(String uuid) { if (uuid.length < 4) { throw const FormatException('Invalid UUID'); } + if (uuid.startsWith('0x')) { + uuid = uuid.substring(2); + } + if (uuid.length <= 8) { uuid = "${uuid.padLeft(8, '0')}-0000-1000-8000-00805f9b34fb"; } @@ -41,17 +47,22 @@ class BleUuid { return uuid.toLowerCase(); } - /// Parse 16/32 bit uuid like `0x1800` to 128 bit uuid like `00001800-0000-1000-8000-00805f9b34fb` - static String extend(int short) => - parse(short.toRadixString(16).padLeft(4, '0')); + /// Parse an int number into a 128-bit UUID string. + /// e.g. `0x1800` to `00001800-0000-1000-8000-00805f9b34fb`. + static String number(int short) { + if (short <= 0xFF || short > 0xFFFF) { + throw const FormatException('Invalid UUID'); + } + return string(short.toRadixString(16).padLeft(4, '0')); + } - /// 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) => - parse(uuid1) == parse(uuid2); + /// Compare two UUIDs regardless of their format. + /// Throws `FormatException` if the UUID is invalid. + static bool compareStrings(String uuid1, String uuid2) => + string(uuid1) == string(uuid2); } -/// Parse a list of strings to a list of UUIDs +/// Parse a list of strings to a list of UUIDs. extension StringListToUUID on List { - List toValidUUIDList() => map(BleUuid.parse).toList(); + List toValidUUIDList() => map(BleUuidParser.string).toList(); } diff --git a/lib/src/models/model_exports.dart b/lib/src/models/model_exports.dart index 7104da4..5767122 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/ble_uuid.dart'; +export 'package:universal_ble/src/models/ble_uuid_parser.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/universal_ble.dart b/lib/src/universal_ble.dart index 3260efe..bcb1bc1 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, - BleUuid.parse(service), - BleUuid.parse(characteristic), + BleUuidParser.string(service), + BleUuidParser.string(characteristic), bleInputProperty, ), deviceId: deviceId, @@ -129,8 +129,8 @@ class UniversalBle { return await _bleCommandQueue.queueCommand( () => _platform.readValue( deviceId, - BleUuid.parse(service), - BleUuid.parse(characteristic), + BleUuidParser.string(service), + BleUuidParser.string(characteristic), ), deviceId: deviceId, ); @@ -148,8 +148,8 @@ class UniversalBle { await _bleCommandQueue.queueCommand( () => _platform.writeValue( deviceId, - BleUuid.parse(service), - BleUuid.parse(characteristic), + BleUuidParser.string(service), + BleUuidParser.string(characteristic), value, bleOutputProperty, ), 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 ee718f6..75b63bb 100644 --- a/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart +++ b/lib/src/universal_ble_pigeon/universal_ble_pigeon_channel.dart @@ -209,7 +209,7 @@ extension _UniversalBleScanResultExtension on UniversalBleScanResult { isSystemDevice: isSystemDevice, services: services ?.where((e) => e != null) - .map((e) => BleUuid.parse(e!)) + .map((e) => BleUuidParser.string(e!)) .toList() ?? [], ); diff --git a/lib/src/universal_ble_platform_interface.dart b/lib/src/universal_ble_platform_interface.dart index 0ad5931..b2b392b 100644 --- a/lib/src/universal_ble_platform_interface.dart +++ b/lib/src/universal_ble_platform_interface.dart @@ -65,7 +65,8 @@ abstract class UniversalBlePlatform { /// `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); + onValueChange?.call( + deviceId, BleUuidParser.string(characteristicId), value); } OnAvailabilityChange? onAvailabilityChange; diff --git a/test/ble_uuid_parser_test.dart b/test/ble_uuid_parser_test.dart new file mode 100644 index 0000000..50d8612 --- /dev/null +++ b/test/ble_uuid_parser_test.dart @@ -0,0 +1,226 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:universal_ble/universal_ble.dart'; + +void main() { + group('Parsing string', () { + group('Succeeds', () { + test('128-bit UUID in lowercase', () { + expect( + BleUuidParser.string('0000180a-0000-1000-8000-00805f9b34fb'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('128-bit UUID in uppercase', () { + expect( + BleUuidParser.string('0000180A-0000-1000-8000-00805F9B34FB'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('16-bit UUID in lowercase', () { + expect( + BleUuidParser.string('180a'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('16-bit UUID in uppercase', () { + expect( + BleUuidParser.string('180A'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('32-bit UUID', () { + expect( + BleUuidParser.string('0000180a'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('UUID without dashes', () { + expect( + BleUuidParser.string('0000180a00001000800000805f9b34fb'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('Lowercase 16-bit string to 128-bit starting with 0x', () { + expect( + BleUuidParser.string('0x0180a'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('Uppercase 16-bit string to 128-bit starting with 0x', () { + expect( + BleUuidParser.string('0x0180A'), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + }); + group('Fails', () { + test('Invalid UUID length is less than 4', () { + expect( + () => BleUuidParser.string('123'), + throwsFormatException, + ); + }); + + test('Invalid UUID without dashes and short length', () { + expect( + () => BleUuidParser.string('0000180a00001000800000805f9b34'), + throwsFormatException, + ); + }); + + test('Invalid UUID without dashes and long length', () { + expect( + () => BleUuidParser.string('0000180a00001000800000805f9b34fb34'), + throwsFormatException, + ); + }); + + test('Invalid UUID is missing a dash', () { + expect( + () => BleUuidParser.string('0000180a-0000-1000-800000805f9b34fb'), + throwsFormatException, + ); + }); + + test('Invalid UUID has too many dashes', () { + expect( + () => BleUuidParser.string('0000-180a-0000-1000-8000-0080-5f9b-34fb'), + throwsFormatException, + ); + }); + + test('Invalid UUID is too short for 128 bits', () { + expect( + () => BleUuidParser.string('01'), + throwsFormatException, + ); + + expect( + () => BleUuidParser.string('0000180a-0000-1000-8000-00805f9b34f'), + throwsFormatException, + ); + }); + + test('Invalid UUID is too long for 128 bits', () { + expect( + () => BleUuidParser.string('0000180a-0000-1000-8000-00805f9b34fba'), + throwsFormatException, + ); + }); + + test('Invalid UUID contains non-hex characters', () { + expect( + () => BleUuidParser.string('0000180g-0000-1000-8000-00805f9b34fba'), + throwsFormatException, + ); + }); + }); + }); + + group('Parsing number', () { + group('Succeeds', () { + test('16-bit UUID in lowercase', () { + expect( + BleUuidParser.number(0x180a), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('16-bit UUID in uppercase', () { + expect( + BleUuidParser.number(0x180A), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + + test('32-bit UUID', () { + expect( + BleUuidParser.number(0x0000180A), + equals('0000180a-0000-1000-8000-00805f9b34fb'), + ); + }); + }); + group('Fails', () { + test('Invalid UUID is 8 bits', () { + expect( + () => BleUuidParser.number(0x18), + throwsFormatException, + ); + }); + + test('Invalid UUID is more than 32 bits', () { + expect( + () => BleUuidParser.number(0x180A01), + throwsFormatException, + ); + }); + }); + }); + + group('CompareStrings', () { + group('Succeeds', () { + test('Identical UUIDs', () { + expect( + BleUuidParser.compareStrings('0000180a-0000-1000-8000-00805f9b34fb', + '0000180a-0000-1000-8000-00805f9b34fb'), + isTrue, + ); + }); + + test('UUIDs with different case', () { + expect( + BleUuidParser.compareStrings('180A', '180a'), + isTrue, + ); + }); + + test('16-bit and 128-bit UUIDs', () { + expect( + BleUuidParser.compareStrings( + '0000180a-0000-1000-8000-00805f9b34fb', '180a'), + isTrue, + ); + }); + + test('16-bit and 128-bit UUIDs without dashes', () { + expect( + BleUuidParser.compareStrings( + '0000180A00001000800000805F9B34FB', '180a'), + isTrue, + ); + }); + + test('32-bit and 128-bit UUIDs', () { + expect( + BleUuidParser.compareStrings( + '0000180A', '0000180a-0000-1000-8000-00805f9b34fb'), + isTrue, + ); + }); + }); + + group('Fails with', () { + test('Different UUIDs', () { + expect( + BleUuidParser.compareStrings('180A', '180B'), + isFalse, + ); + }); + + test('Different UUIDs with different formats', () { + expect( + BleUuidParser.compareStrings( + '0000180A', '0000180b-0000-1000-8000-00805f9b34fb'), + isFalse, + ); + }); + }); + }); +} diff --git a/test/universal_ble_test.dart b/test/universal_ble_test.dart deleted file mode 100644 index 57ace3f..0000000 --- a/test/universal_ble_test.dart +++ /dev/null @@ -1,74 +0,0 @@ -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"), - ); - expect( - BleUuid.parse('8000dd00-dd00-ffff-ffff-ffffffffffff'), - equals("8000dd00-dd00-ffff-ffff-ffffffffffff"), - ); - // 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, - ); - }); - }); -}