Files
universal_ble/lib/src/universal_ble_web/universal_ble_web.dart
T
Rohit Sangwan 0d6b612497 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>
2024-07-19 10:39:49 +02:00

453 lines
14 KiB
Dart

import 'dart:async';
import 'dart:collection';
import 'dart:typed_data';
import 'package:flutter_web_bluetooth/flutter_web_bluetooth.dart';
import 'package:universal_ble/src/models/model_exports.dart';
import 'package:universal_ble/src/universal_ble_platform_interface.dart';
class UniversalBleWeb extends UniversalBlePlatform {
static UniversalBleWeb? _instance;
static UniversalBleWeb get instance => _instance ??= UniversalBleWeb._();
UniversalBleWeb._() {
_setupListeners();
}
final Map<String, BluetoothDevice> _bluetoothDeviceList = {};
final Map<String, StreamSubscription> _deviceAdvertisementStreamList = {};
final Map<String, StreamSubscription> _connectedDeviceStreamList = {};
final Map<String, StreamSubscription> _characteristicStreamList = {};
@override
Future<BleConnectionState> getConnectionState(String deviceId) async {
BluetoothDevice? device = _getDeviceById(deviceId);
bool connected = await device?.connected.first ?? false;
return connected
? BleConnectionState.connected
: BleConnectionState.disconnected;
}
@override
Future<void> connect(
String deviceId, {
Duration? connectionTimeout = const Duration(seconds: 10),
}) async {
var device = _getDeviceById(deviceId);
if (device == null) throw "$deviceId Not Found";
await device.connect(timeout: connectionTimeout);
// Subscribe to Connection Stream
if (_connectedDeviceStreamList[deviceId] != null) {
_connectedDeviceStreamList[deviceId]?.cancel();
}
_connectedDeviceStreamList[deviceId] = device.connected.listen((event) {
if (!event) _cleanConnection(deviceId);
onConnectionChange?.call(deviceId, event);
});
}
@override
Future<void> disconnect(String deviceId) async {
_cleanConnection(deviceId);
onConnectionChange?.call(deviceId, false);
_getDeviceById(deviceId)?.disconnect();
}
@override
Future<List<BleService>> discoverServices(String deviceId) async {
final device = _getDeviceById(deviceId);
final services = await device?.discoverServices();
if (services == null) return [];
var discoveredServices = <BleService>[];
for (var service in services) {
final characteristics = await service.getCharacteristics();
List<BleCharacteristic> bleCharacteristics = [];
for (BluetoothCharacteristic characteristic in characteristics) {
List<CharacteristicProperty> properties = [];
if (characteristic.properties.broadcast) {
properties.add(CharacteristicProperty.broadcast);
}
if (characteristic.properties.read) {
properties.add(CharacteristicProperty.read);
}
if (characteristic.properties.writeWithoutResponse) {
properties.add(CharacteristicProperty.writeWithoutResponse);
}
if (characteristic.properties.write) {
properties.add(CharacteristicProperty.write);
}
if (characteristic.properties.notify) {
properties.add(CharacteristicProperty.notify);
}
if (characteristic.properties.indicate) {
properties.add(CharacteristicProperty.indicate);
}
if (characteristic.properties.authenticatedSignedWrites) {
properties.add(CharacteristicProperty.authenticatedSignedWrites);
}
bleCharacteristics.add(
BleCharacteristic(characteristic.uuid.toString(), properties),
);
}
discoveredServices.add(
BleService(service.uuid.toString(), bleCharacteristics),
);
}
return discoveredServices;
}
@override
Future<AvailabilityState> getBluetoothAvailabilityState() async {
bool isSupported = FlutterWebBluetooth.instance.isBluetoothApiSupported;
if (!isSupported) {
return AvailabilityState.unsupported;
}
bool isAvailable = await FlutterWebBluetooth.instance.isAvailable.first;
if (isSupported && !isAvailable) {
return AvailabilityState.poweredOff;
} else if (isAvailable) {
return AvailabilityState.poweredOn;
}
return AvailabilityState.unknown;
}
@override
Future<void> startScan({
ScanFilter? scanFilter,
}) async {
if (scanFilter == null || scanFilter.withServices.isEmpty) {
UniversalBlePlatform.logInfo(
"Service list empty: on web you have to specify services in the ScanFilter in order to be able to access those after connecting",
isError: true,
);
}
BluetoothDevice device = await FlutterWebBluetooth.instance.requestDevice(
scanFilter?.toRequestOptionsBuilder() ??
RequestOptionsBuilder.acceptAllDevices(),
);
// Update local device list
_bluetoothDeviceList[device.id] = device;
// Update Scan Result
updateScanResult(device.toBleScanResult());
_watchDeviceAdvertisements(device);
}
@override
bool receivesAdvertisements(String deviceId) =>
_getDeviceById(deviceId)?.hasWatchAdvertisements() ?? false;
/// 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 {
_disposeAdvertisementWatcher();
}
@override
Future<void> setNotifiable(
String deviceId,
String service,
String characteristic,
BleInputProperty bleInputProperty,
) async {
final bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
characteristicId: characteristic,
);
if (bleCharacteristic == null) {
throw Exception(
'Characteristic $characteristic for service $service not found',
);
}
String characteristicKey = "${deviceId}_${service}_$characteristic";
if (bleInputProperty == BleInputProperty.notification ||
bleInputProperty == BleInputProperty.indication) {
if (bleCharacteristic.isNotifying) {
throw Exception("Already listening to this characteristic");
}
if (_characteristicStreamList[characteristicKey] != null) {
_characteristicStreamList[characteristicKey]?.cancel();
}
await bleCharacteristic.startNotifications();
_characteristicStreamList[characteristicKey] = bleCharacteristic.value
.map((event) => event.buffer.asUint8List())
.listen((event) {
updateCharacteristicValue(deviceId, characteristic, event);
});
}
// Cancel Notification
else if (bleInputProperty == BleInputProperty.disabled) {
await bleCharacteristic.stopNotifications();
_characteristicStreamList.removeWhere((key, value) {
if (key == characteristicKey) value.cancel();
return key == characteristicKey;
});
}
}
@override
Future<void> writeValue(
String deviceId,
String service,
String characteristic,
Uint8List value,
BleOutputProperty bleOutputProperty,
) async {
final bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
characteristicId: characteristic,
);
if (bleCharacteristic == null) {
throw Exception(
'Characteristic $characteristic for service $service not found',
);
}
if (bleOutputProperty == BleOutputProperty.withResponse) {
await bleCharacteristic.writeValueWithResponse(Uint8List.fromList(value));
} else {
await bleCharacteristic
.writeValueWithoutResponse(Uint8List.fromList(value));
}
}
@override
Future<Uint8List> readValue(
String deviceId,
String service,
String characteristic,
) async {
var bleCharacteristic = await _getBleCharacteristic(
deviceId: deviceId,
serviceId: service,
characteristicId: characteristic,
);
if (bleCharacteristic == null) {
throw Exception(
'Characteristic $characteristic for service $service not found');
}
var data = await bleCharacteristic.readValue();
return data.buffer.asUint8List();
}
/// `Unimplemented`
@override
Future<int> requestMtu(String deviceId, int expectedMtu) {
throw UnimplementedError();
}
@override
Future<bool> isPaired(String deviceId) {
throw UnimplementedError();
}
@override
Future<void> pair(String deviceId) {
throw UnimplementedError();
}
@override
Future<void> unPair(String deviceId) {
throw UnimplementedError();
}
@override
Future<List<BleDevice>> getSystemDevices(
List<String>? withServices,
) {
throw UnimplementedError();
}
/// Helpers
void _setupListeners() {
FlutterWebBluetooth.instance.isAvailable.listen(
(bool isAvailable) {
AvailabilityState newState = AvailabilityState.unknown;
if (!FlutterWebBluetooth.instance.isBluetoothApiSupported) {
newState = AvailabilityState.unsupported;
} else if (FlutterWebBluetooth.instance.isBluetoothApiSupported &&
!isAvailable) {
newState = AvailabilityState.poweredOff;
} else if (isAvailable) {
newState = AvailabilityState.poweredOn;
}
onAvailabilityChange?.call(newState);
},
);
}
void _cleanConnection(String deviceId) {
_connectedDeviceStreamList.removeWhere((key, value) {
if (key == deviceId) value.cancel();
return key == deviceId;
});
_characteristicStreamList.removeWhere((key, value) {
if (key.contains(deviceId)) value.cancel();
return key.contains(deviceId);
});
_disposeAdvertisementWatcher(deviceId);
// _bluetoothDeviceList.removeWhere((element) => element.id == deviceId);
}
Future<BluetoothCharacteristic?> _getBleCharacteristic({
required String deviceId,
required String serviceId,
required String characteristicId,
}) async {
final device = _getDeviceById(deviceId);
List<BluetoothService> services = await device?.discoverServices() ?? [];
BluetoothService? service;
for (BluetoothService bleService in services) {
if (bleService.uuid.toString() == serviceId.toString()) {
service = bleService;
break;
}
}
return await service?.getCharacteristic(characteristicId.toString());
}
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();
}
}
extension _BluetoothDeviceExtension on BluetoothDevice {
BleDevice toBleScanResult({
int? rssi,
UnmodifiableMapView<int, ByteData>? manufacturerDataMap,
List<String> services = const [],
}) {
return BleDevice(
name: name,
deviceId: id,
manufacturerData: manufacturerDataMap?.toUint8List(),
manufacturerDataHead: manufacturerDataMap?.toUint8List(),
rssi: rssi,
services: services,
);
}
}
extension _UnmodifiableMapViewExtension on UnmodifiableMapView<int, ByteData> {
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(
RequestFilterBuilder(
services: [service],
),
);
}
// Add manufacturer data filter
for (var manufacturerData in withManufacturerData) {
filters.add(
RequestFilterBuilder(
manufacturerData: [
ManufacturerDataFilterBuilder(
companyIdentifier: manufacturerData.companyIdentifier,
dataPrefix: manufacturerData.data,
mask: manufacturerData.mask,
),
],
),
);
int? companyId = manufacturerData.companyIdentifier;
if (companyId != null) {
manufacturerCompanyIdentifiers.add(companyId);
}
}
// Add name filter
for (var name in withNamePrefix) {
filters.add(
RequestFilterBuilder(namePrefix: name),
);
}
if (filters.isEmpty) {
return RequestOptionsBuilder.acceptAllDevices();
}
return RequestOptionsBuilder(
filters,
optionalServices: withServices.toValidUUIDList(),
optionalManufacturerData: manufacturerCompanyIdentifiers,
);
}
}