Unify UUID formatting (#55)

* Unify UUID formatting

* Update Readme

* Update Readme

* Apply suggestions from code review

Co-authored-by: Foti Dim <foti@navideck.com>

* 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 <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2024-07-05 21:23:55 +05:30
committed by GitHub
parent 9cd2faee66
commit 9bb209f8b5
12 changed files with 178 additions and 49 deletions
+28
View File
@@ -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<BleDevice> 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
+13
View File
@@ -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.
+12 -4
View File
@@ -1,13 +1,21 @@
import 'package:universal_ble/universal_ble.dart';
class BleService {
String uuid;
late String uuid;
List<BleCharacteristic> characteristics;
BleService(this.uuid, this.characteristics);
BleService(String uuid, this.characteristics) {
this.uuid = BleUuid.parse(uuid);
}
}
class BleCharacteristic {
String uuid;
late String uuid;
List<CharacteristicProperty> properties;
BleCharacteristic(this.uuid, this.properties);
BleCharacteristic(String uuid, this.properties) {
this.uuid = BleUuid.parse(uuid);
}
}
enum CharacteristicProperty {
+34
View File
@@ -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<String> {
List<String> toValidUUIDList() => map(BleUuid.parse).toList();
}
+1 -1
View File
@@ -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';
-34
View File
@@ -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<String> {
List<String> toValidUUIDList() => map((e) => UUID(e).value).toList();
}
+9 -5
View File
@@ -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,
),
@@ -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),
@@ -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() ??
[],
);
@@ -50,6 +50,7 @@ abstract class UniversalBlePlatform {
List<String>? 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;
@@ -21,7 +21,6 @@ class UniversalBleWeb extends UniversalBlePlatform {
@override
Future<BleConnectionState> 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
+70
View File
@@ -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,
);
});
});
}