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 <rohitsangwan647@gmail.com>
This commit is contained in:
Foti Dim
2024-07-15 05:59:30 +02:00
committed by GitHub
parent 44412508e7
commit efaa72acbd
13 changed files with 318 additions and 115 deletions
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
## 0.11.0 ## 0.11.0
* Unify UUID format across all platforms, 128-bit lowercase * 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 * Improve Android error handling
* Fix Android disconnection events sometimes missed * Fix Android disconnection events sometimes missed
* Improve cleanup after disconnection on Apple and Android * Improve cleanup after disconnection on Apple and Android
+18 -14
View File
@@ -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) [![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) - [Bluetooth Availability](#bluetooth-availability)
- [Command Queue](#command-queue) - [Command Queue](#command-queue)
- [Timeout](#timeout) - [Timeout](#timeout)
- [UUID Format Agnostic](#uuid-format-agnostic)
## Usage
### API Support Matrix ### API Support Matrix
@@ -260,36 +263,37 @@ UniversalBle.timeout = const Duration(seconds: 10);
UniversalBle.timeout = null; 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. 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.
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. 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 ```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 ```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 ```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 ### Android
-1
View File
@@ -1,6 +1,5 @@
// ignore_for_file: use_build_context_synchronously // ignore_for_file: use_build_context_synchronously
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:universal_ble/universal_ble.dart'; import 'package:universal_ble/universal_ble.dart';
import 'package:universal_ble_example/data/capabilities.dart'; import 'package:universal_ble_example/data/capabilities.dart';
@@ -37,7 +37,7 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
List<String> services = widget.servicesFilterController.text.split(','); List<String> services = widget.servicesFilterController.text.split(',');
for (String service in services) { for (String service in services) {
try { try {
serviceUUids.add(BleUuid.parse(service.trim())); serviceUUids.add(BleUuidParser.string(service.trim()));
} on FormatException catch (_) { } on FormatException catch (_) {
throw Exception("Invalid Service UUID $service"); throw Exception("Invalid Service UUID $service");
} }
+2 -2
View File
@@ -5,7 +5,7 @@ class BleService {
List<BleCharacteristic> characteristics; List<BleCharacteristic> characteristics;
BleService(String uuid, this.characteristics) { BleService(String uuid, this.characteristics) {
this.uuid = BleUuid.parse(uuid); this.uuid = BleUuidParser.string(uuid);
} }
} }
@@ -14,7 +14,7 @@ class BleCharacteristic {
List<CharacteristicProperty> properties; List<CharacteristicProperty> properties;
BleCharacteristic(String uuid, this.properties) { BleCharacteristic(String uuid, this.properties) {
this.uuid = BleUuid.parse(uuid); this.uuid = BleUuidParser.string(uuid);
} }
} }
@@ -1,11 +1,17 @@
class BleUuid { class BleUuidParser {
/// Parse a String to valid UUID and convert a 16 bit UUID to 128 bit UUID BleUuidParser._();
/// Throws `FormatException` if the UUID is invalid
static String parse(String uuid) { /// 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) { if (uuid.length < 4) {
throw const FormatException('Invalid UUID'); throw const FormatException('Invalid UUID');
} }
if (uuid.startsWith('0x')) {
uuid = uuid.substring(2);
}
if (uuid.length <= 8) { if (uuid.length <= 8) {
uuid = "${uuid.padLeft(8, '0')}-0000-1000-8000-00805f9b34fb"; uuid = "${uuid.padLeft(8, '0')}-0000-1000-8000-00805f9b34fb";
} }
@@ -41,17 +47,22 @@ class BleUuid {
return uuid.toLowerCase(); return uuid.toLowerCase();
} }
/// Parse 16/32 bit uuid like `0x1800` to 128 bit uuid like `00001800-0000-1000-8000-00805f9b34fb` /// Parse an int number into a 128-bit UUID string.
static String extend(int short) => /// e.g. `0x1800` to `00001800-0000-1000-8000-00805f9b34fb`.
parse(short.toRadixString(16).padLeft(4, '0')); 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 /// Compare two UUIDs regardless of their format.
/// Throws `FormatException` if the UUID is invalid /// Throws `FormatException` if the UUID is invalid.
static bool equals(String uuid1, String uuid2) => static bool compareStrings(String uuid1, String uuid2) =>
parse(uuid1) == parse(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<String> { extension StringListToUUID on List<String> {
List<String> toValidUUIDList() => map(BleUuid.parse).toList(); List<String> toValidUUIDList() => map(BleUuidParser.string).toList();
} }
+1 -1
View File
@@ -1,5 +1,5 @@
export 'package:universal_ble/src/models/queue_type.dart'; 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/scan_filter.dart';
export 'package:universal_ble/src/models/ble_property.dart'; export 'package:universal_ble/src/models/ble_property.dart';
export 'package:universal_ble/src/models/ble_service.dart'; export 'package:universal_ble/src/models/ble_service.dart';
+6 -6
View File
@@ -111,8 +111,8 @@ class UniversalBle {
return await _bleCommandQueue.queueCommand( return await _bleCommandQueue.queueCommand(
() => _platform.setNotifiable( () => _platform.setNotifiable(
deviceId, deviceId,
BleUuid.parse(service), BleUuidParser.string(service),
BleUuid.parse(characteristic), BleUuidParser.string(characteristic),
bleInputProperty, bleInputProperty,
), ),
deviceId: deviceId, deviceId: deviceId,
@@ -129,8 +129,8 @@ class UniversalBle {
return await _bleCommandQueue.queueCommand( return await _bleCommandQueue.queueCommand(
() => _platform.readValue( () => _platform.readValue(
deviceId, deviceId,
BleUuid.parse(service), BleUuidParser.string(service),
BleUuid.parse(characteristic), BleUuidParser.string(characteristic),
), ),
deviceId: deviceId, deviceId: deviceId,
); );
@@ -148,8 +148,8 @@ class UniversalBle {
await _bleCommandQueue.queueCommand( await _bleCommandQueue.queueCommand(
() => _platform.writeValue( () => _platform.writeValue(
deviceId, deviceId,
BleUuid.parse(service), BleUuidParser.string(service),
BleUuid.parse(characteristic), BleUuidParser.string(characteristic),
value, value,
bleOutputProperty, bleOutputProperty,
), ),
@@ -209,7 +209,7 @@ extension _UniversalBleScanResultExtension on UniversalBleScanResult {
isSystemDevice: isSystemDevice, isSystemDevice: isSystemDevice,
services: services services: services
?.where((e) => e != null) ?.where((e) => e != null)
.map((e) => BleUuid.parse(e!)) .map((e) => BleUuidParser.string(e!))
.toList() ?? .toList() ??
[], [],
); );
@@ -65,7 +65,8 @@ abstract class UniversalBlePlatform {
/// `onValueChange` interceptor to parse the native uuids to 128 bit uuid, to keep consistency /// `onValueChange` interceptor to parse the native uuids to 128 bit uuid, to keep consistency
void updateCharacteristicValue( void updateCharacteristicValue(
String deviceId, String characteristicId, Uint8List value) { String deviceId, String characteristicId, Uint8List value) {
onValueChange?.call(deviceId, BleUuid.parse(characteristicId), value); onValueChange?.call(
deviceId, BleUuidParser.string(characteristicId), value);
} }
OnAvailabilityChange? onAvailabilityChange; OnAvailabilityChange? onAvailabilityChange;
+226
View File
@@ -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,
);
});
});
});
}
-74
View File
@@ -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,
);
});
});
}