628 lines
22 KiB
Dart
628 lines
22 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:anyhow/anyhow.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:rxdart/rxdart.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
part 'bluetooth.g.dart';
|
|
|
|
final log = Logger('BluetoothController');
|
|
|
|
@Riverpod(keepAlive: true)
|
|
Future<BluetoothController> bluetooth(Ref ref) async {
|
|
ref.keepAlive();
|
|
final controller = BluetoothController();
|
|
log.info(await controller.init());
|
|
return controller;
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
Stream<(ConnectionStatus, BluetoothDevice?)> connectionStatus(Ref ref) {
|
|
// Get the (potentially still loading) BluetoothController
|
|
final asyncController = ref.watch(bluetoothProvider);
|
|
|
|
// If the controller is ready, return its stream. Otherwise, return an empty stream.
|
|
// The provider will automatically update when the controller becomes ready.
|
|
return asyncController.when(
|
|
data: (controller) => controller.connectionStateStream,
|
|
loading: () => Stream.value((ConnectionStatus.disconnected, null)),
|
|
error: (_, __) => Stream.value((ConnectionStatus.disconnected, null)),
|
|
);
|
|
}
|
|
|
|
/// Represents the connection status of the Bluetooth device.
|
|
enum ConnectionStatus { disconnected, connecting, connected, disconnecting }
|
|
|
|
class BluetoothController {
|
|
StreamSubscription<BluetoothAdapterState>? _btStateSubscription;
|
|
StreamSubscription<List<ScanResult>>? _scanResultsSubscription;
|
|
List<ScanResult> _latestScanResults = [];
|
|
StreamSubscription<void>? _servicesResetSubscription;
|
|
final Map<String, Map<Guid, BluetoothService>> _servicesByDevice = {};
|
|
final Map<String, Map<String, BluetoothCharacteristic>>
|
|
_characteristicsByDevice = {};
|
|
// Connection State
|
|
BluetoothDevice? _connectedDevice;
|
|
StreamSubscription<BluetoothConnectionState>? _connectionStateSubscription;
|
|
final _connectionStateSubject =
|
|
BehaviorSubject<(ConnectionStatus, BluetoothDevice?)>.seeded(
|
|
(ConnectionStatus.disconnected, null));
|
|
|
|
/// Stream providing the current connection status and the connected device (if any).
|
|
Stream<(ConnectionStatus, BluetoothDevice?)> get connectionStateStream =>
|
|
_connectionStateSubject.stream;
|
|
|
|
/// Gets the latest connection status and device.
|
|
(ConnectionStatus, BluetoothDevice?) get currentConnectionState =>
|
|
_connectionStateSubject.value;
|
|
|
|
Future<Result<void>> init() async {
|
|
log.severe("CALLED FBPON!");
|
|
if (await FlutterBluePlus.isSupported == false) {
|
|
log.severe("Bluetooth is not supported on this device!");
|
|
return bail("Bluetooth is not supported on this device!");
|
|
}
|
|
|
|
_btStateSubscription =
|
|
FlutterBluePlus.adapterState.listen((BluetoothAdapterState state) {
|
|
if (state == BluetoothAdapterState.on) {
|
|
log.info("Bluetooth is on!");
|
|
// usually start scanning, connecting, etc
|
|
} else {
|
|
log.info("Bluetooth is off!");
|
|
// show an error to the user, etc
|
|
}
|
|
});
|
|
|
|
if (!kIsWeb && Platform.isAndroid) {
|
|
await FlutterBluePlus.turnOn();
|
|
}
|
|
|
|
connectionStateStream.listen((state) {
|
|
log.info('Connection state changed: $state');
|
|
});
|
|
|
|
return Ok(null);
|
|
}
|
|
|
|
/// Start scanning for Bluetooth devices
|
|
///
|
|
/// [withServices] - Optional list of service UUIDs to filter devices by
|
|
/// [withNames] - Optional list of device names to filter by
|
|
/// [timeout] - Optional duration after which scanning will automatically stop
|
|
Future<Result<void>> startScan({
|
|
List<Guid>? withServices,
|
|
List<String>? withNames,
|
|
Duration? timeout,
|
|
}) async {
|
|
try {
|
|
// Wait for Bluetooth to be enabled
|
|
await FlutterBluePlus.adapterState
|
|
.where((val) => val == BluetoothAdapterState.on)
|
|
.first;
|
|
|
|
// Set up scan results listener
|
|
_scanResultsSubscription = FlutterBluePlus.onScanResults.listen(
|
|
(results) {
|
|
if (results.isNotEmpty) {
|
|
_latestScanResults = results;
|
|
ScanResult latestResult = results.last;
|
|
log.info(
|
|
'${latestResult.device.remoteId}: "${latestResult.advertisementData.advName}" found!');
|
|
}
|
|
},
|
|
onError: (e) {
|
|
log.severe('Scan error: $e');
|
|
},
|
|
);
|
|
|
|
// Clean up subscription when scanning completes
|
|
FlutterBluePlus.cancelWhenScanComplete(_scanResultsSubscription!);
|
|
|
|
// Start scanning with optional parameters
|
|
await FlutterBluePlus.startScan(
|
|
withServices: withServices ?? [],
|
|
withNames: withNames ?? [],
|
|
timeout: timeout,
|
|
);
|
|
|
|
return Ok(null);
|
|
} catch (e) {
|
|
return bail('Failed to start Bluetooth scan: $e');
|
|
}
|
|
}
|
|
|
|
/// Stop an ongoing Bluetooth scan
|
|
Future<Result<void>> stopScan() async {
|
|
try {
|
|
await FlutterBluePlus.stopScan();
|
|
return Ok(null);
|
|
} catch (e) {
|
|
return bail('Failed to stop Bluetooth scan: $e');
|
|
}
|
|
}
|
|
|
|
/// Get the latest scan results
|
|
List<ScanResult> get scanResults => _latestScanResults;
|
|
|
|
/// Wait for the current scan to complete
|
|
Future<Result<void>> waitForScanToComplete() async {
|
|
try {
|
|
await FlutterBluePlus.isScanning.where((val) => val == false).first;
|
|
return Ok(null);
|
|
} catch (e) {
|
|
return bail('Error waiting for scan to complete: $e');
|
|
}
|
|
}
|
|
|
|
/// Check if currently scanning
|
|
Future<bool> get isScanning async {
|
|
return await FlutterBluePlus.isScanning.first;
|
|
}
|
|
|
|
/// Connects to a specific Bluetooth device.
|
|
///
|
|
/// Ensures that only one device is connected at a time. If another device
|
|
/// is already connected or connecting, it will be disconnected first.
|
|
Future<Result<void>> connect(BluetoothDevice device,
|
|
{Duration? timeout}) async {
|
|
final currentState = currentConnectionState;
|
|
final currentDevice = currentState.$2;
|
|
|
|
// Prevent connecting if already connected/connecting to the *same* device
|
|
if (device.remoteId == currentDevice?.remoteId &&
|
|
(currentState.$1 == ConnectionStatus.connected ||
|
|
currentState.$1 == ConnectionStatus.connecting)) {
|
|
log.info('Currently connected device: ${currentState.$2}');
|
|
log.info('Already connected or connecting to ${device.remoteId}.');
|
|
return Ok(null); // Or potentially an error/different status?
|
|
}
|
|
|
|
log.info('Attempting to connect to ${device.remoteId}...');
|
|
|
|
// If connecting or connected to a *different* device, disconnect it first.
|
|
if (currentDevice != null && device.remoteId != currentDevice.remoteId) {
|
|
log.info(
|
|
'Disconnecting from previous device ${currentDevice.remoteId} first.');
|
|
final disconnectResult = await disconnect();
|
|
if (disconnectResult.isErr()) {
|
|
return disconnectResult
|
|
.context('Failed to disconnect from previous device');
|
|
}
|
|
// Wait a moment for the disconnection to fully process
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
}
|
|
|
|
try {
|
|
// Cancel any previous connection state listener before starting a new one
|
|
await _connectionStateSubscription?.cancel();
|
|
_connectionStateSubscription =
|
|
device.connectionState.listen((BluetoothConnectionState state) async {
|
|
log.info('[${device.remoteId}] Connection state changed: $state');
|
|
switch (state) {
|
|
case BluetoothConnectionState.connected:
|
|
_connectedDevice = device;
|
|
_updateConnectionState(ConnectionStatus.connected, device);
|
|
// IMPORTANT: Discover services after connecting
|
|
try {
|
|
_attachServicesResetListener(device);
|
|
final servicesResult =
|
|
await _discoverAndCacheServices(device, force: true);
|
|
if (servicesResult.isErr()) {
|
|
throw servicesResult.unwrapErr();
|
|
}
|
|
log.info(
|
|
'[${device.remoteId}] Services discovered: \n${servicesResult.unwrap().map((e) => e.uuid.toString()).join('\n')}');
|
|
} catch (e) {
|
|
log.severe(
|
|
'[${device.remoteId}] Error discovering services: $e. Disconnecting.');
|
|
// Disconnect if service discovery fails, as the connection might be unusable
|
|
await disconnect();
|
|
}
|
|
break;
|
|
case BluetoothConnectionState.disconnected:
|
|
if (_connectionStateSubject.value.$1 !=
|
|
ConnectionStatus.connected) {
|
|
log.warning(
|
|
'[${device.remoteId}] Disconnected WITHOUT being connected! Reason: ${device.disconnectReason?.code} ${device.disconnectReason?.description}\nDoing nothing');
|
|
break;
|
|
} else {
|
|
log.warning(
|
|
'[${device.remoteId}] Disconnected. Reason: ${device.disconnectReason?.code} ${device.disconnectReason?.description}');
|
|
// Only clean up if this is the device we were connected/connecting to
|
|
if (_connectionStateSubject.value.$2?.remoteId ==
|
|
device.remoteId) {
|
|
// Clean up connection state, handling disconnection.
|
|
// In general, reconnection is better, but this is how it's handled here.
|
|
// App behavior would be to go back to the homepage on disconnection
|
|
_cleanUpConnection();
|
|
} else {
|
|
log.info(
|
|
'[${device.remoteId}] Received disconnect for a device we were not tracking.');
|
|
}
|
|
break;
|
|
}
|
|
case BluetoothConnectionState.connecting:
|
|
case BluetoothConnectionState.disconnecting:
|
|
// deprecated states
|
|
log.warning(
|
|
'Received unexpected connection state: ${device.connectionState}. This should not happen.');
|
|
break;
|
|
}
|
|
});
|
|
|
|
await device.connect(
|
|
license: License.free,
|
|
timeout: timeout ?? const Duration(seconds: 15),
|
|
mtu: 512,
|
|
);
|
|
|
|
// Note: Success is primarily handled by the connectionState listener
|
|
log.info(
|
|
'Connection initiated for ${device.remoteId}. Waiting for state change.');
|
|
_connectionStateSubject.add((ConnectionStatus.connected, device));
|
|
return Ok(null);
|
|
} catch (e) {
|
|
log.severe('Failed to connect to ${device.remoteId}: $e');
|
|
_cleanUpConnection(); // Clean up state on connection failure
|
|
return bail('Failed to connect to ${device.remoteId}: $e');
|
|
}
|
|
}
|
|
|
|
/// Connects to a device using its remote ID string with a specific timeout.
|
|
Future<Result<void>> connectById(String remoteId,
|
|
{Duration timeout = const Duration(seconds: 10)}) async {
|
|
log.info('Attempting to connect by ID: $remoteId with timeout: $timeout');
|
|
try {
|
|
// Get the BluetoothDevice object from the ID
|
|
final device = BluetoothDevice.fromId(remoteId);
|
|
|
|
// Call the existing connect method, passing the device and timeout
|
|
// Assumes the 'connect' method below is modified to accept the timeout.
|
|
return await connect(device, timeout: timeout); // Pass timeout here
|
|
} catch (e, st) {
|
|
// Catch potential errors from fromId or during connection setup before connect() is called
|
|
log.severe('Error connecting by ID $remoteId: $e');
|
|
_cleanUpConnection(); // Ensure state is cleaned up
|
|
return bail('Failed to initiate connection for ID $remoteId: $e', st);
|
|
}
|
|
}
|
|
|
|
/// Disconnects from the currently connected device.
|
|
Future<Result<void>> disconnect() async {
|
|
final deviceToDisconnect =
|
|
_connectedDevice ?? _connectionStateSubject.value.$2;
|
|
if (deviceToDisconnect == null) {
|
|
log.info('No device is currently connected or connecting.');
|
|
// Ensure state is definitely disconnected if called unnecessarily
|
|
_cleanUpConnection();
|
|
return Ok(null);
|
|
}
|
|
|
|
log.info('Disconnecting from ${deviceToDisconnect.remoteId}...');
|
|
_updateConnectionState(ConnectionStatus.disconnecting, deviceToDisconnect);
|
|
|
|
try {
|
|
await deviceToDisconnect.disconnect();
|
|
log.info('Disconnect command sent to ${deviceToDisconnect.remoteId}.');
|
|
// State update to disconnected is handled by the connectionState listener
|
|
// but we call cleanup here as a safety measure in case the listener fails
|
|
_cleanUpConnection();
|
|
return Ok(null);
|
|
} catch (e) {
|
|
log.severe(
|
|
'Failed to disconnect from ${deviceToDisconnect.remoteId}: $e');
|
|
// Even on error, try to clean up the state
|
|
_cleanUpConnection();
|
|
return bail(
|
|
'Failed to disconnect from ${deviceToDisconnect.remoteId}: $e');
|
|
}
|
|
}
|
|
|
|
void _updateConnectionState(
|
|
ConnectionStatus status, BluetoothDevice? device) {
|
|
// Avoid emitting redundant states
|
|
if (_connectionStateSubject.value.$1 == status &&
|
|
_connectionStateSubject.value.$2?.remoteId == device?.remoteId) {
|
|
return;
|
|
}
|
|
_connectionStateSubject.add((status, device));
|
|
log.fine(
|
|
'Connection state updated: $status, Device: ${device?.remoteId ?? 'none'}');
|
|
}
|
|
|
|
Future<Result<List<BluetoothService>>> discoverServices(
|
|
BluetoothDevice device, {
|
|
bool force = false,
|
|
}) async {
|
|
return _discoverAndCacheServices(device, force: force);
|
|
}
|
|
|
|
Future<Result<void>> writeCharacteristic(
|
|
BluetoothDevice device,
|
|
String serviceUuid,
|
|
String characteristicUuid,
|
|
List<int> value, {
|
|
bool withoutResponse = false,
|
|
bool allowLongWrite = false,
|
|
int timeout = 15,
|
|
}) async {
|
|
final serviceGuid = Guid(serviceUuid);
|
|
final characteristicGuid = Guid(characteristicUuid);
|
|
final chrResult =
|
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
|
if (chrResult.isErr()) {
|
|
return chrResult.context('Failed to resolve characteristic for write');
|
|
}
|
|
|
|
try {
|
|
await chrResult.unwrap().write(
|
|
value,
|
|
withoutResponse: withoutResponse,
|
|
allowLongWrite: allowLongWrite,
|
|
timeout: timeout,
|
|
);
|
|
return Ok(null);
|
|
} catch (e) {
|
|
return bail('Error writing characteristic $characteristicUuid: $e');
|
|
}
|
|
}
|
|
|
|
Future<Result<StreamSubscription<List<int>>>> subscribeToNotifications(
|
|
BluetoothDevice device,
|
|
String serviceUuid,
|
|
String characteristicUuid, {
|
|
void Function(List<int>)? onValue,
|
|
bool useLastValueStream = false,
|
|
int timeout = 15,
|
|
}) async {
|
|
return _subscribeToCharacteristic(
|
|
device,
|
|
serviceUuid,
|
|
characteristicUuid,
|
|
useLastValueStream: useLastValueStream,
|
|
timeout: timeout,
|
|
forceIndications: false,
|
|
onValue: onValue,
|
|
);
|
|
}
|
|
|
|
Future<Result<StreamSubscription<List<int>>>> subscribeToIndications(
|
|
BluetoothDevice device,
|
|
String serviceUuid,
|
|
String characteristicUuid, {
|
|
void Function(List<int>)? onValue,
|
|
bool useLastValueStream = false,
|
|
int timeout = 15,
|
|
}) async {
|
|
return _subscribeToCharacteristic(
|
|
device,
|
|
serviceUuid,
|
|
characteristicUuid,
|
|
useLastValueStream: useLastValueStream,
|
|
timeout: timeout,
|
|
forceIndications: true,
|
|
onValue: onValue,
|
|
);
|
|
}
|
|
|
|
Future<Result<void>> unsubscribeFromCharacteristic(
|
|
BluetoothDevice device,
|
|
String serviceUuid,
|
|
String characteristicUuid, {
|
|
int timeout = 15,
|
|
}) async {
|
|
final serviceGuid = Guid(serviceUuid);
|
|
final characteristicGuid = Guid(characteristicUuid);
|
|
final chrResult =
|
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
|
if (chrResult.isErr()) {
|
|
return chrResult
|
|
.context('Failed to resolve characteristic to unsubscribe');
|
|
}
|
|
|
|
try {
|
|
await chrResult.unwrap().setNotifyValue(false, timeout: timeout);
|
|
return Ok(null);
|
|
} catch (e) {
|
|
return bail('Error disabling notifications for $characteristicUuid: $e');
|
|
}
|
|
}
|
|
|
|
/// Helper function to clean up connection resources and state.
|
|
Future<void> _cleanUpConnection() async {
|
|
log.fine('Cleaning up connection state and subscriptions.');
|
|
_connectedDevice = null;
|
|
await _servicesResetSubscription?.cancel();
|
|
_servicesResetSubscription = null;
|
|
_servicesByDevice.clear();
|
|
_characteristicsByDevice.clear();
|
|
await _connectionStateSubscription?.cancel();
|
|
_connectionStateSubscription = null;
|
|
_updateConnectionState(ConnectionStatus.disconnected, null);
|
|
}
|
|
|
|
Future<Result<void>> dispose() async {
|
|
await _scanResultsSubscription?.cancel();
|
|
await _btStateSubscription?.cancel();
|
|
await disconnect(); // Ensure disconnection on dispose
|
|
await _connectionStateSubject.close();
|
|
return Ok(null);
|
|
}
|
|
|
|
Future<Result<List<int>>> readCharacteristic(
|
|
BluetoothDevice device, String svcUuid, String characteristic) async {
|
|
// Implement reading characteristic logic here
|
|
// This is a placeholder implementation
|
|
log.info(
|
|
'Reading characteristic from device: $device, characteristic: $characteristic');
|
|
final serviceUUID = Guid(svcUuid);
|
|
final characteristicUUID = Guid(characteristic);
|
|
|
|
if (!device.servicesList.map((e) => e.uuid).contains(serviceUUID)) {
|
|
return bail('Service $svcUuid not found on device ${device.remoteId}');
|
|
}
|
|
|
|
final BluetoothService service =
|
|
(device.servicesList).firstWhere((s) => s.uuid == serviceUUID);
|
|
|
|
if (service.characteristics.isEmpty ||
|
|
!service.characteristics
|
|
.map((c) => c.uuid)
|
|
.contains(characteristicUUID)) {
|
|
return bail(
|
|
'Characteristic $characteristic not found on device ${device.remoteId}');
|
|
}
|
|
|
|
try {
|
|
final val = await service.characteristics
|
|
.firstWhere((c) => c.uuid == characteristicUUID)
|
|
.read();
|
|
return Ok(val);
|
|
} catch (e) {
|
|
return bail('Error reading characteristic: $e');
|
|
}
|
|
}
|
|
|
|
String _deviceKey(BluetoothDevice device) => device.remoteId.str;
|
|
|
|
String _characteristicKey(Guid serviceUuid, Guid characteristicUuid) =>
|
|
'${serviceUuid.toString()}|${characteristicUuid.toString()}';
|
|
|
|
void _cacheServices(BluetoothDevice device, List<BluetoothService> services) {
|
|
final serviceMap = <Guid, BluetoothService>{};
|
|
final characteristicMap = <String, BluetoothCharacteristic>{};
|
|
|
|
for (final service in services) {
|
|
serviceMap[service.uuid] = service;
|
|
for (final chr in service.characteristics) {
|
|
characteristicMap[_characteristicKey(service.uuid, chr.uuid)] = chr;
|
|
}
|
|
}
|
|
|
|
_servicesByDevice[_deviceKey(device)] = serviceMap;
|
|
_characteristicsByDevice[_deviceKey(device)] = characteristicMap;
|
|
}
|
|
|
|
void _attachServicesResetListener(BluetoothDevice device) {
|
|
_servicesResetSubscription?.cancel();
|
|
_servicesResetSubscription = device.onServicesReset.listen((_) async {
|
|
log.info('[${device.remoteId}] Services reset. Re-discovering.');
|
|
final res = await _discoverAndCacheServices(device, force: true);
|
|
if (res.isErr()) {
|
|
log.severe(
|
|
'[${device.remoteId}] Failed to re-discover services: ${res.unwrapErr()}');
|
|
}
|
|
});
|
|
device.cancelWhenDisconnected(_servicesResetSubscription!);
|
|
}
|
|
|
|
Future<Result<List<BluetoothService>>> _discoverAndCacheServices(
|
|
BluetoothDevice device, {
|
|
bool force = false,
|
|
}) async {
|
|
try {
|
|
if (!force) {
|
|
final cached = _servicesByDevice[_deviceKey(device)];
|
|
if (cached != null && cached.isNotEmpty) {
|
|
return Ok(cached.values.toList());
|
|
}
|
|
}
|
|
if (!force && device.servicesList.isNotEmpty) {
|
|
_cacheServices(device, device.servicesList);
|
|
return Ok(device.servicesList);
|
|
}
|
|
|
|
final services = await device.discoverServices();
|
|
_cacheServices(device, services);
|
|
return Ok(services);
|
|
} catch (e) {
|
|
return bail('Failed to discover services for ${device.remoteId}: $e');
|
|
}
|
|
}
|
|
|
|
Future<Result<BluetoothCharacteristic>> _getCharacteristic(
|
|
BluetoothDevice device,
|
|
Guid serviceUuid,
|
|
Guid characteristicUuid,
|
|
) async {
|
|
final deviceKey = _deviceKey(device);
|
|
final cached = _characteristicsByDevice[deviceKey]
|
|
?[_characteristicKey(serviceUuid, characteristicUuid)];
|
|
if (cached != null) {
|
|
return Ok(cached);
|
|
}
|
|
|
|
final discoverResult = await _discoverAndCacheServices(device);
|
|
if (discoverResult.isErr()) {
|
|
return bail(discoverResult.unwrapErr().toString());
|
|
}
|
|
|
|
final refreshed = _characteristicsByDevice[deviceKey]
|
|
?[_characteristicKey(serviceUuid, characteristicUuid)];
|
|
if (refreshed == null) {
|
|
return bail(
|
|
'Characteristic $characteristicUuid not found on service $serviceUuid for device ${device.remoteId}');
|
|
}
|
|
|
|
return Ok(refreshed);
|
|
}
|
|
|
|
Future<Result<StreamSubscription<List<int>>>> _subscribeToCharacteristic(
|
|
BluetoothDevice device,
|
|
String serviceUuid,
|
|
String characteristicUuid, {
|
|
required bool forceIndications,
|
|
required bool useLastValueStream,
|
|
required int timeout,
|
|
void Function(List<int>)? onValue,
|
|
}) async {
|
|
final serviceGuid = Guid(serviceUuid);
|
|
final characteristicGuid = Guid(characteristicUuid);
|
|
final chrResult =
|
|
await _getCharacteristic(device, serviceGuid, characteristicGuid);
|
|
if (chrResult.isErr()) {
|
|
return bail('Failed to resolve characteristic subscription: '
|
|
'${chrResult.unwrapErr()}');
|
|
}
|
|
|
|
final characteristic = chrResult.unwrap();
|
|
final properties = characteristic.properties;
|
|
if (forceIndications && !properties.indicate) {
|
|
return bail(
|
|
'Characteristic $characteristicUuid does not support indications');
|
|
}
|
|
if (!forceIndications && !properties.notify && !properties.indicate) {
|
|
return bail(
|
|
'Characteristic $characteristicUuid does not support notifications');
|
|
}
|
|
if (forceIndications && !kIsWeb && !Platform.isAndroid) {
|
|
return bail('Indications can only be forced on Android.');
|
|
}
|
|
|
|
try {
|
|
final stream = useLastValueStream
|
|
? characteristic.lastValueStream
|
|
: characteristic.onValueReceived;
|
|
final subscription = stream.listen(onValue ?? (_) {});
|
|
device.cancelWhenDisconnected(subscription);
|
|
|
|
await characteristic.setNotifyValue(
|
|
true,
|
|
timeout: timeout,
|
|
forceIndications: forceIndications,
|
|
);
|
|
|
|
return Ok(subscription);
|
|
} catch (e) {
|
|
return bail(
|
|
'Error subscribing to characteristic $characteristicUuid: $e');
|
|
}
|
|
}
|
|
}
|