feat: working connection, conn setting, and gear ratio setting for universal shifters
This commit is contained in:
627
lib/controller/bluetooth.old.dart
Normal file
627
lib/controller/bluetooth.old.dart
Normal file
@ -0,0 +1,627 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user