From d88786bb0f6a2be72cfc1bd74b2bf5548baeebeb Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Tue, 24 Feb 2026 22:41:03 -0800 Subject: [PATCH] ble filtering --- lib/connector/meshcore_connector.dart | 220 ++++++++------------------ 1 file changed, 67 insertions(+), 153 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index e4eaaf6..ba62232 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -30,7 +30,6 @@ import '../storage/message_store.dart'; import '../storage/unread_store.dart'; import '../utils/app_logger.dart'; import '../utils/battery_utils.dart'; -import '../utils/platform_info.dart'; import 'meshcore_protocol.dart'; class MeshCoreUuids { @@ -686,111 +685,59 @@ class MeshCoreConnector extends ChangeNotifier { }) async { if (_state == MeshCoreConnectionState.scanning) return; - try { - _scanResults.clear(); - _setState(MeshCoreConnectionState.scanning); + _scanResults.clear(); + _setState(MeshCoreConnectionState.scanning); - // Ensure any previous scan is fully stopped - try { - await FlutterBluePlus.stopScan(); - } catch (_) {} + // Ensure any previous scan is fully stopped + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); - try { - await _scanSubscription?.cancel(); - } catch (_) {} - _scanSubscription = null; - - // On iOS/macOS, wait for Bluetooth to be powered on before scanning - if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { - // Wait for adapter state to be powered on - final adapterState = await FlutterBluePlus.adapterState.first; - if (adapterState != BluetoothAdapterState.on) { - // Wait for the adapter to turn on, with timeout - await FlutterBluePlus.adapterState - .firstWhere((state) => state == BluetoothAdapterState.on) - .timeout( - const Duration(seconds: 5), - onTimeout: () { - _setState(MeshCoreConnectionState.disconnected); - throw Exception('Bluetooth adapter not available'); - }, - ); - } - - // Add a small delay to allow BLE stack to fully initialize - await Future.delayed(const Duration(milliseconds: 300)); + // On iOS/macOS, wait for Bluetooth to be powered on before scanning + if (defaultTargetPlatform == TargetPlatform.iOS || + defaultTargetPlatform == TargetPlatform.macOS) { + // Wait for adapter state to be powered on + final adapterState = await FlutterBluePlus.adapterState.first; + if (adapterState != BluetoothAdapterState.on) { + // Wait for the adapter to turn on, with timeout + await FlutterBluePlus.adapterState + .firstWhere((state) => state == BluetoothAdapterState.on) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + _setState(MeshCoreConnectionState.disconnected); + throw Exception('Bluetooth adapter not available'); + }, + ); } - _scanSubscription = FlutterBluePlus.scanResults.listen( - (results) { - _scanResults.clear(); - for (var result in results) { - if (result.device.platformName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("MeshCore-") || - result.advertisementData.advName.startsWith("Whisper-")) { - _scanResults.add(result); - } - } - notifyListeners(); - }, - onError: (Object e) { - debugPrint("scanResults stream error: $e"); - stopScan(); - }, - ); - - if (PlatformInfo.isWeb) { - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - ); - // On web, the chooser returns once a device is picked, but the scanResults - // stream might take a moment to emit the last result. Wait briefly so the - // device appears in the UI before stopScan() clears the list. - await Future.delayed(const Duration(milliseconds: 500)); - } else { - await FlutterBluePlus.startScan( - withServices: [Guid(MeshCoreUuids.service)], - timeout: timeout, - androidScanMode: AndroidScanMode.lowLatency, - ); - - await Future.delayed(timeout); - } - } catch (e) { - debugPrint("Scan error: $e"); - // On web, suppress common cancellation and chooser errors - if (kIsWeb) return; - - if (!PlatformInfo.isWeb) { - rethrow; - } - } finally { - await stopScan(); + // Add a small delay to allow BLE stack to fully initialize + await Future.delayed(const Duration(milliseconds: 300)); } + + _scanSubscription = FlutterBluePlus.scanResults.listen((results) { + _scanResults.clear(); + _scanResults.addAll(results); + notifyListeners(); + }); + + await FlutterBluePlus.startScan( + withKeywords: ["MeshCore-", "Whisper-"], + webOptionalServices: [Guid(MeshCoreUuids.service)], + timeout: timeout, + androidScanMode: AndroidScanMode.lowLatency, + ); + + await Future.delayed(timeout); + await stopScan(); } Future stopScan() async { - if (_state == MeshCoreConnectionState.scanning) { - _setState(MeshCoreConnectionState.disconnected); - } - - try { - await FlutterBluePlus.stopScan(); - } catch (e) { - debugPrint("stopScan error: $e"); - } - - try { - if (_scanSubscription != null) { - await _scanSubscription!.cancel(); - } - } catch (_) {} + await FlutterBluePlus.stopScan(); + await _scanSubscription?.cancel(); _scanSubscription = null; - // On web, don't clear results immediately so the picked device remains visible - if (!PlatformInfo.isWeb) { - _scanResults.clear(); - notifyListeners(); + if (_state == MeshCoreConnectionState.scanning) { + _setState(MeshCoreConnectionState.disconnected); } } @@ -818,17 +765,11 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); try { - _connectionSubscription = device.connectionState.listen( - (state) { - if (state == BluetoothConnectionState.disconnected && isConnected) { - _handleDisconnection(); - } - }, - onError: (Object e) { - debugPrint("connectionState stream error: $e"); - if (isConnected) _handleDisconnection(); - }, - ); + _connectionSubscription = device.connectionState.listen((state) { + if (state == BluetoothConnectionState.disconnected && isConnected) { + _handleDisconnection(); + } + }); await device.connect( timeout: const Duration(seconds: 15), @@ -837,17 +778,11 @@ class MeshCoreConnector extends ChangeNotifier { ); // Request larger MTU for sending larger frames - if (!PlatformInfo.isWeb) { - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); - } - } else { - // On Chrome Web Bluetooth, give the GATT connection a moment to settle - // before discovering services, which is a common quirk to avoid timeouts. - await Future.delayed(const Duration(milliseconds: 500)); + try { + final mtu = await device.requestMtu(185); + debugPrint('MTU set to: $mtu'); + } catch (e) { + debugPrint('MTU request failed: $e, using default'); } List services = await device.discoverServices(); @@ -877,44 +812,23 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - // Setup listener BEFORE enabling notifications so we don't miss anything - _notifySubscription = _txCharacteristic!.onValueReceived.listen( - _handleFrame, - onError: (Object e) { - debugPrint("onValueReceived stream error: $e"); - }, - ); - - debugPrint('Starting setNotifyValue(true)'); - if (PlatformInfo.isWeb) { - // On Web, setNotifyValue often hangs indefinitely on the Promise resolution. - // We trigger it but don't await its completion to avoid blocking the connection flow. - debugPrint('Web: Calling setNotifyValue(true) without awaiting'); - // ignore: unawaited_futures - _txCharacteristic!.setNotifyValue(true, timeout: 2).catchError((e) { - debugPrint('Web setNotifyValue error (ignoring): $e'); - return false; // catchError must return a bool to match Future - }); - // Give the browser a moment to process the underlying startNotifications call - await Future.delayed(const Duration(milliseconds: 500)); - } else { - // Native platforms handle setNotifyValue blockingly with CCCD descriptors - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); - } - debugPrint('Calling setNotifyValue(true), attempt ${attempt + 1}'); - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; + // Retry setNotifyValue with increasing delays + bool notifySet = false; + for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { + try { + if (attempt > 0) { + await Future.delayed(Duration(milliseconds: 500 * attempt)); } + await _txCharacteristic!.setNotifyValue(true); + notifySet = true; + } catch (e) { + debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); + if (attempt == 2) rethrow; } } - debugPrint('setNotifyValue(true) configuration completed'); + _notifySubscription = _txCharacteristic!.onValueReceived.listen( + _handleFrame, + ); _setState(MeshCoreConnectionState.connected);