Receive advertisement events on web (#63)

* Fix web manufacturer discovery

* Minor Fixes

* Cleanups

* Add canWatchAdvertisements api

* Improve Cleanups

* Improve code level documentation

* Improve documentation

---------

Co-authored-by: Foti Dim <fdimanidis@gmail.com>
This commit is contained in:
Rohit Sangwan
2024-07-19 14:09:49 +05:30
committed by GitHub
parent c0c678c735
commit 0d6b612497
11 changed files with 136 additions and 67 deletions
+8 -3
View File
@@ -12,12 +12,17 @@ class BleDevice {
Uint8List? manufacturerDataHead;
Uint8List? manufacturerData;
/// Returns connection state of device,
/// All platforms will return `Connected/Disconnected` states
/// `Android` and `Apple` can also return `Connecting/Disconnecting` states
/// Returns connection state of the device.
/// All platforms will return `Connected/Disconnected` states.
/// `Android` and `Apple` can also return `Connecting/Disconnecting` states.
Future<BleConnectionState> get connectionState =>
UniversalBle.getConnectionState(deviceId);
/// On web, it returns true if the web browser supports receiving advertisements from this device.
/// The rest of the platforms will always return true.
bool get receivesAdvertisements =>
UniversalBle.receivesAdvertisements(deviceId);
BleDevice({
required this.deviceId,
required this.name,
+13 -3
View File
@@ -207,9 +207,9 @@ class UniversalBle {
);
}
/// Returns connection state of device,
/// All platforms will return `Connected/Disconnected` states
/// `Android` and `Apple` can also return `Connecting/Disconnecting` states
/// Returns connection state of the device.
/// All platforms will return `Connected/Disconnected` states.
/// `Android` and `Apple` can also return `Connecting/Disconnecting` states.
static Future<BleConnectionState> getConnectionState(String deviceId) async {
return await _bleCommandQueue.queueCommand(
() => _platform.getConnectionState(deviceId),
@@ -225,6 +225,16 @@ class UniversalBle {
);
}
/// [receivesAdvertisements] returns true on web if the browser supports receiving advertisements from a certain `deviceId`.
/// The rest of the platforms will always return true.
/// If true, then you will be getting scanResult updates for this device.
///
/// For this feature to work, you need to enable the `chrome://flags/#enable-experimental-web-platform-features` flag.
/// Not every browser supports this API yet.
/// Even if the browser supports it, sometimes it won't fire any advertisement events even though the device may be sending them.
static bool receivesAdvertisements(String deviceId) =>
_platform.receivesAdvertisements(deviceId);
/// Get Bluetooth state availability
static set onAvailabilityChange(OnAvailabilityChange? onAvailabilityChange) {
_platform.onAvailabilityChange = onAvailabilityChange;
@@ -50,7 +50,6 @@ abstract class UniversalBlePlatform {
List<String>? withServices,
);
/// `onScanResult` interceptor to filter by name
void updateScanResult(BleDevice bleDevice) {
// Filter by name
ScanFilter? scanFilter = _scanFilter;
@@ -62,7 +61,8 @@ abstract class UniversalBlePlatform {
onScanResult?.call(bleDevice);
}
/// `onValueChange` interceptor to parse the native uuids to 128 bit uuid, to keep consistency
bool receivesAdvertisements(String deviceId) => true;
void updateCharacteristicValue(
String deviceId, String characteristicId, Uint8List value) {
onValueChange?.call(
@@ -135,46 +135,46 @@ class UniversalBleWeb extends UniversalBlePlatform {
// Update Scan Result
updateScanResult(device.toBleScanResult());
/// This will work only if `chrome://flags/#enable-experimental-web-platform-features` is enabled
if (FlutterWebBluetooth.instance.hasRequestLEScan) {
// Check if platform can watch advertisements
if (device.hasWatchAdvertisements()) {
if (_deviceAdvertisementStreamList[device.id] == null) {
_deviceAdvertisementStreamList[device.id]?.cancel();
await device.unwatchAdvertisements();
}
_watchDeviceAdvertisements(device);
}
_deviceAdvertisementStreamList[device.id] =
device.advertisements.listen((event) {
updateScanResult(
device.toBleScanResult(
rssi: event.rssi,
manufacturerDataMap: event.manufacturerData,
services: event.uuids,
),
);
});
@override
bool receivesAdvertisements(String deviceId) =>
_getDeviceById(deviceId)?.hasWatchAdvertisements() ?? false;
await device.watchAdvertisements();
/// This will work only if `chrome://flags/#enable-experimental-web-platform-features` is enabled
Future<void> _watchDeviceAdvertisements(BluetoothDevice device) async {
try {
if (!device.hasWatchAdvertisements()) return;
if (_deviceAdvertisementStreamList[device.id] != null) {
_deviceAdvertisementStreamList[device.id]?.cancel();
await device.unwatchAdvertisements();
}
_deviceAdvertisementStreamList[device.id] =
device.advertisements.listen((event) {
updateScanResult(
device.toBleScanResult(
rssi: event.rssi,
manufacturerDataMap: event.manufacturerData,
services: event.uuids,
),
);
});
device.advertisementsUseMemory = true;
await device.watchAdvertisements();
} catch (e) {
UniversalBlePlatform.logInfo(
"WebWatchAdvertisementError: $e",
isError: true,
);
}
}
@override
Future<void> stopScan() async {
// Cancel advertisement streams
if (FlutterWebBluetooth.instance.hasRequestLEScan) {
_deviceAdvertisementStreamList.removeWhere((key, value) {
value.cancel();
return true;
});
for (var element in _bluetoothDeviceList.entries) {
if (element.value.hasWatchAdvertisements()) {
element.value.unwatchAdvertisements();
}
}
}
_disposeAdvertisementWatcher();
}
@override
@@ -328,6 +328,7 @@ class UniversalBleWeb extends UniversalBlePlatform {
if (key.contains(deviceId)) value.cancel();
return key.contains(deviceId);
});
_disposeAdvertisementWatcher(deviceId);
// _bluetoothDeviceList.removeWhere((element) => element.id == deviceId);
}
@@ -350,6 +351,17 @@ class UniversalBleWeb extends UniversalBlePlatform {
BluetoothDevice? _getDeviceById(String id) => _bluetoothDeviceList[id];
void _disposeAdvertisementWatcher([String? deviceId]) {
_deviceAdvertisementStreamList.removeWhere((key, value) {
if (deviceId != null && key != deviceId) return false;
value.cancel();
_getDeviceById(deviceId ?? key)
?.unwatchAdvertisements()
.onError((_, __) {});
return true;
});
}
@override
Future<bool> enableBluetooth() {
throw UnimplementedError();
@@ -374,24 +386,24 @@ extension _BluetoothDeviceExtension on BluetoothDevice {
}
extension _UnmodifiableMapViewExtension on UnmodifiableMapView<int, ByteData> {
Uint8List toUint8List() {
int totalLength =
values.fold<int>(0, (prev, element) => prev + element.lengthInBytes);
Uint8List result = Uint8List(totalLength);
int offset = 0;
for (var entry in entries) {
var byteData = entry.value;
var sublist = byteData.buffer.asUint8List();
result.setRange(offset, offset + sublist.length, sublist);
offset += sublist.length;
}
return result;
Uint8List? toUint8List() {
List<MapEntry<int, ByteData>> sorted = entries.toList()
..sort((a, b) => a.key - b.key);
if (sorted.isEmpty) return null;
int companyId = sorted.first.key;
List<int> manufacturerDataValue = sorted.first.value.buffer.asUint8List();
final byteData = ByteData(2);
byteData.setInt16(0, companyId, Endian.host);
List<int> bytes = byteData.buffer.asUint8List();
return Uint8List.fromList(bytes + manufacturerDataValue);
}
}
extension ScanFilterExtension on ScanFilter {
RequestOptionsBuilder toRequestOptionsBuilder() {
List<RequestFilterBuilder> filters = [];
List<int> manufacturerCompanyIdentifiers = [];
// Add services filter
for (var service in withServices.toValidUUIDList()) {
filters.add(
@@ -414,6 +426,10 @@ extension ScanFilterExtension on ScanFilter {
],
),
);
int? companyId = manufacturerData.companyIdentifier;
if (companyId != null) {
manufacturerCompanyIdentifiers.add(companyId);
}
}
// Add name filter
@@ -430,6 +446,7 @@ extension ScanFilterExtension on ScanFilter {
return RequestOptionsBuilder(
filters,
optionalServices: withServices.toValidUUIDList(),
optionalManufacturerData: manufacturerCompanyIdentifiers,
);
}
}