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:
Vendored
+30
-1
@@ -19,6 +19,35 @@
|
||||
"--dart-define",
|
||||
"MOCK=true",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "example_web",
|
||||
"cwd": "example",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": [
|
||||
"-d",
|
||||
"web-server",
|
||||
"--web-hostname=127.0.0.1",
|
||||
"--web-port=8080"
|
||||
],
|
||||
"preLaunchTask": "open_chrome"
|
||||
},
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"label": "open_chrome",
|
||||
"type": "shell",
|
||||
"command": "open",
|
||||
"args": [
|
||||
"-na",
|
||||
"Google Chrome",
|
||||
"--args",
|
||||
"--user-data-dir=/tmp/temporary-chrome-profile-dir",
|
||||
"--disable-web-security",
|
||||
"--disable-site-isolation-trials"
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
## 0.11.1
|
||||
* Trim spaces in UUIDs
|
||||
* Receive advertisement events on web
|
||||
* Improve cleanup after disconnection on web
|
||||
|
||||
## 0.11.0
|
||||
* Unify UUID format across all platforms, 128-bit lowercase
|
||||
|
||||
@@ -115,7 +115,7 @@ You can optionally set filters when scanning.
|
||||
|
||||
##### With Services
|
||||
|
||||
When setting this parameter, the scan results will only include devices that advertize any of the specified services. This is the primary filter. All devices are first filtered by services, then further filtered by other criteria. This parameter is mandatory on [web](#web) if you want to access those services.
|
||||
When setting this parameter, the scan results will only include devices that advertise any of the specified services. This is the primary filter. All devices are first filtered by services, then further filtered by other criteria. This parameter is mandatory on [web](#web) if you want to access those services.
|
||||
|
||||
```dart
|
||||
List<String> withServices;
|
||||
@@ -328,7 +328,7 @@ When publishing on Windows you need to declare the following [capabilities](http
|
||||
|
||||
### Web
|
||||
|
||||
On web, the `withServices` parameter in the ScanFilter is used as [optional_services](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#optionalservices) as well as a services filter. On web you have to set this parameter to ensure that you can access the specified services after connecting to the device. You can leave it empty for the rest of the platforms if your device does not advertize services.
|
||||
On web, the `withServices` parameter in the ScanFilter is used as [optional_services](https://developer.mozilla.org/en-US/docs/Web/API/Bluetooth/requestDevice#optionalservices) as well as a services filter. On web you have to set this parameter to ensure that you can access the specified services after connecting to the device. You can leave it empty for the rest of the platforms if your device does not advertise services.
|
||||
|
||||
```dart
|
||||
ScanFilter(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
import 'package:universal_ble_example/data/capabilities.dart';
|
||||
@@ -50,8 +52,7 @@ class _MyAppState extends State<MyApp> {
|
||||
};
|
||||
|
||||
UniversalBle.onScanResult = (result) {
|
||||
// debugPrint("BleDevice: ${result.name} ${result.services}");
|
||||
// debugPrint("${result.name} ${result.manufacturerData}");
|
||||
log(result.toString());
|
||||
int index = _bleDevices.indexWhere((e) => e.deviceId == result.deviceId);
|
||||
if (index == -1) {
|
||||
_bleDevices.add(result);
|
||||
|
||||
@@ -74,6 +74,7 @@ class _ScanFilterWidgetState extends State<ScanFilterWidget> {
|
||||
ScanFilter(
|
||||
withServices: serviceUUids,
|
||||
withNamePrefix: namePrefixes,
|
||||
withManufacturerData: manufacturerDataFilters,
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -93,16 +93,20 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
|
||||
}
|
||||
|
||||
Future<void> _discoverServices() async {
|
||||
var services = await UniversalBle.discoverServices(widget.deviceId);
|
||||
print('${services.length} services discovered');
|
||||
discoveredServices.clear();
|
||||
setState(() {
|
||||
discoveredServices = services;
|
||||
});
|
||||
try {
|
||||
var services = await UniversalBle.discoverServices(widget.deviceId);
|
||||
print('${services.length} services discovered');
|
||||
discoveredServices.clear();
|
||||
setState(() {
|
||||
discoveredServices = services;
|
||||
});
|
||||
|
||||
if (kIsWeb) {
|
||||
_addLog("DiscoverServices",
|
||||
'${services.length} services discovered,\nNote: Only services added in ScanFilter will be discovered');
|
||||
if (kIsWeb) {
|
||||
_addLog("DiscoverServices",
|
||||
'${services.length} services discovered,\nNote: Only services added in ScanFilter will be discovered');
|
||||
}
|
||||
} catch (e) {
|
||||
_addLog("DiscoverServicesError", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ packages:
|
||||
path: ".."
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.11.0"
|
||||
version: "0.11.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user