Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 60 additions & 30 deletions packages/flutter_reactive_ble/lib/src/device_connector.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:flutter_reactive_ble/src/device_scanner.dart';
import 'package:flutter_reactive_ble/src/rx_ext/repeater.dart';
Expand Down Expand Up @@ -110,49 +112,77 @@ class DeviceConnectorImpl implements DeviceConnector {
Duration? connectionTimeout,
List<Uuid> withServices,
Duration prescanDuration,
) {
) async* {
if (_deviceIsDiscoveredRecently(
deviceId: id, cacheValidity: _scanRegistryCacheValidityPeriod)) {
return connect(
yield* connect(
id: id,
servicesWithCharacteristicsToDiscover:
servicesWithCharacteristicsToDiscover,
connectionTimeout: connectionTimeout,
);
} else {
final scanSubscription = _deviceScanner
.scanForDevices(
withServices: withServices, scanMode: ScanMode.lowLatency)
.listen((DiscoveredDevice scanData) {}, onError: (Object _) {});
return;
}

// get a sync function context so we can cancel the stream on a simple
// timer
Stream<DiscoveredDevice> scanWithTimeout() {
final controller = StreamController<DiscoveredDevice>();

final stream = _deviceScanner.scanForDevices(
withServices: withServices, scanMode: ScanMode.lowLatency);

final scanSubscription =
stream.listen(controller.add, onError: controller.addError);
Future<void>.delayed(prescanDuration).then<void>((_) {
scanSubscription.cancel();
controller.close();
});

return _deviceScanner.currentScan!.future
.then((_) => true)
.catchError((Object _) => false)
.asStream()
.asyncExpand(
(succeeded) {
if (succeeded) {
return _connectIfRecentlyDiscovered(
id, servicesWithCharacteristicsToDiscover, connectionTimeout);
} else {
// When the scan fails 99% of the times it is due to violation of the scan threshold:
// https://blog.classycode.com/undocumented-android-7-ble-behavior-changes-d1a9bd87d983
//
// Previously we used "autoconnect" but that gives slow connection times (up to 2 min) on a lot of devices.
return Future<void>.delayed(_delayAfterScanFailure)
.asStream()
.asyncExpand((_) => _connectIfRecentlyDiscovered(
id,
servicesWithCharacteristicsToDiscover,
connectionTimeout,
));
}
},
return controller.stream;
}

var didScanDevice = false;
try {
await for (final device in scanWithTimeout()) {
if (device.id == id) {
didScanDevice = true;
break;
}
}
// ignore: avoid_catches_without_on_clauses
} catch (_) {
// When the scan fails 99% of the times it is due to violation of the scan threshold:
// https://blog.classycode.com/undocumented-android-7-ble-behavior-changes-d1a9bd87d983
//
// Previously we used "autoconnect" but that gives slow connection times (up to 2 min) on a lot of devices.
await Future<void>.delayed(_delayAfterScanFailure);

yield* _connectIfRecentlyDiscovered(
id,
servicesWithCharacteristicsToDiscover,
connectionTimeout,
);
return;
}

if (didScanDevice) {
yield* connect(
id: id,
servicesWithCharacteristicsToDiscover:
servicesWithCharacteristicsToDiscover,
connectionTimeout: connectionTimeout,
);
return;
}

yield ConnectionStateUpdate(
deviceId: id,
connectionState: DeviceConnectionState.disconnected,
failure: const GenericFailure(
code: ConnectionError.failedToConnect,
message: "Device is not advertising"),
);
}

Stream<ConnectionStateUpdate> _awaitCurrentScanAndConnect(
Expand Down
46 changes: 30 additions & 16 deletions packages/flutter_reactive_ble/test/device_connector_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,6 @@ void main() {
const deviceId = '123';
final uuidDeviceToScan = Uuid.parse('FEFF');
final uuidCurrentScan = Uuid.parse('FEFE');
final discoveredDevice = DiscoveredDevice(
id: 'deviceId',
manufacturerData: Uint8List.fromList([0]),
serviceUuids: const [],
name: 'test',
rssi: -39,
connectable: Connectable.unknown,
serviceData: const {},
);

setUp(() {
_connectionStateUpdateStream = Stream.fromIterable([
Expand Down Expand Up @@ -235,15 +226,23 @@ void main() {
});

group('And device is not discovered in a previous scan', () {
setUp(() {
when(_scanner.scanForDevices(
withServices: anyNamed('withServices'),
scanMode: anyNamed('scanMode'),
)).thenAnswer((_) => Stream.fromIterable([discoveredDevice]));
});

group('And device is not found after scanning', () {
setUp(() {
when(_scanner.scanForDevices(
withServices: anyNamed('withServices'),
scanMode: anyNamed('scanMode'),
)).thenAnswer((_) => Stream.fromIterable([
DiscoveredDevice(
id: 'deviceId',
manufacturerData: Uint8List.fromList([0]),
serviceUuids: const [],
name: 'test',
rssi: -39,
connectable: Connectable.unknown,
serviceData: const {},
)
]));

when(_registry.deviceIsDiscoveredRecently(
deviceId: deviceId,
cacheValidity: anyNamed('cacheValidity')))
Expand Down Expand Up @@ -272,6 +271,21 @@ void main() {
});
group('And device found after scanning', () {
setUp(() {
when(_scanner.scanForDevices(
withServices: anyNamed('withServices'),
scanMode: anyNamed('scanMode'),
)).thenAnswer((_) => Stream.fromIterable([
DiscoveredDevice(
id: deviceId,
manufacturerData: Uint8List.fromList([0]),
serviceUuids: const [],
name: 'test',
rssi: -39,
connectable: Connectable.unknown,
serviceData: const {},
)
]));

final responses = [false, true, true, true];
when(_registry.deviceIsDiscoveredRecently(
deviceId: deviceId,
Expand Down