ble filtering

This commit is contained in:
Ben Allfree
2026-02-24 22:41:03 -08:00
parent e3148dd449
commit d88786bb0f
+67 -153
View File
@@ -30,7 +30,6 @@ import '../storage/message_store.dart';
import '../storage/unread_store.dart'; import '../storage/unread_store.dart';
import '../utils/app_logger.dart'; import '../utils/app_logger.dart';
import '../utils/battery_utils.dart'; import '../utils/battery_utils.dart';
import '../utils/platform_info.dart';
import 'meshcore_protocol.dart'; import 'meshcore_protocol.dart';
class MeshCoreUuids { class MeshCoreUuids {
@@ -686,111 +685,59 @@ class MeshCoreConnector extends ChangeNotifier {
}) async { }) async {
if (_state == MeshCoreConnectionState.scanning) return; if (_state == MeshCoreConnectionState.scanning) return;
try { _scanResults.clear();
_scanResults.clear(); _setState(MeshCoreConnectionState.scanning);
_setState(MeshCoreConnectionState.scanning);
// Ensure any previous scan is fully stopped // Ensure any previous scan is fully stopped
try { await FlutterBluePlus.stopScan();
await FlutterBluePlus.stopScan(); await _scanSubscription?.cancel();
} catch (_) {}
try { // On iOS/macOS, wait for Bluetooth to be powered on before scanning
await _scanSubscription?.cancel(); if (defaultTargetPlatform == TargetPlatform.iOS ||
} catch (_) {} defaultTargetPlatform == TargetPlatform.macOS) {
_scanSubscription = null; // Wait for adapter state to be powered on
final adapterState = await FlutterBluePlus.adapterState.first;
// On iOS/macOS, wait for Bluetooth to be powered on before scanning if (adapterState != BluetoothAdapterState.on) {
if (PlatformInfo.isIOS || PlatformInfo.isMacOS) { // Wait for the adapter to turn on, with timeout
// Wait for adapter state to be powered on await FlutterBluePlus.adapterState
final adapterState = await FlutterBluePlus.adapterState.first; .firstWhere((state) => state == BluetoothAdapterState.on)
if (adapterState != BluetoothAdapterState.on) { .timeout(
// Wait for the adapter to turn on, with timeout const Duration(seconds: 5),
await FlutterBluePlus.adapterState onTimeout: () {
.firstWhere((state) => state == BluetoothAdapterState.on) _setState(MeshCoreConnectionState.disconnected);
.timeout( throw Exception('Bluetooth adapter not available');
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));
} }
_scanSubscription = FlutterBluePlus.scanResults.listen( // Add a small delay to allow BLE stack to fully initialize
(results) { await Future.delayed(const Duration(milliseconds: 300));
_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();
} }
_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<void> stopScan() async { Future<void> stopScan() async {
if (_state == MeshCoreConnectionState.scanning) { await FlutterBluePlus.stopScan();
_setState(MeshCoreConnectionState.disconnected); await _scanSubscription?.cancel();
}
try {
await FlutterBluePlus.stopScan();
} catch (e) {
debugPrint("stopScan error: $e");
}
try {
if (_scanSubscription != null) {
await _scanSubscription!.cancel();
}
} catch (_) {}
_scanSubscription = null; _scanSubscription = null;
// On web, don't clear results immediately so the picked device remains visible if (_state == MeshCoreConnectionState.scanning) {
if (!PlatformInfo.isWeb) { _setState(MeshCoreConnectionState.disconnected);
_scanResults.clear();
notifyListeners();
} }
} }
@@ -818,17 +765,11 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners(); notifyListeners();
try { try {
_connectionSubscription = device.connectionState.listen( _connectionSubscription = device.connectionState.listen((state) {
(state) { if (state == BluetoothConnectionState.disconnected && isConnected) {
if (state == BluetoothConnectionState.disconnected && isConnected) { _handleDisconnection();
_handleDisconnection(); }
} });
},
onError: (Object e) {
debugPrint("connectionState stream error: $e");
if (isConnected) _handleDisconnection();
},
);
await device.connect( await device.connect(
timeout: const Duration(seconds: 15), timeout: const Duration(seconds: 15),
@@ -837,17 +778,11 @@ class MeshCoreConnector extends ChangeNotifier {
); );
// Request larger MTU for sending larger frames // Request larger MTU for sending larger frames
if (!PlatformInfo.isWeb) { try {
try { final mtu = await device.requestMtu(185);
final mtu = await device.requestMtu(185); debugPrint('MTU set to: $mtu');
debugPrint('MTU set to: $mtu'); } catch (e) {
} catch (e) { debugPrint('MTU request failed: $e, using default');
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));
} }
List<BluetoothService> services = await device.discoverServices(); List<BluetoothService> services = await device.discoverServices();
@@ -877,44 +812,23 @@ class MeshCoreConnector extends ChangeNotifier {
throw Exception("MeshCore characteristics not found"); throw Exception("MeshCore characteristics not found");
} }
// Setup listener BEFORE enabling notifications so we don't miss anything // Retry setNotifyValue with increasing delays
_notifySubscription = _txCharacteristic!.onValueReceived.listen( bool notifySet = false;
_handleFrame, for (int attempt = 0; attempt < 3 && !notifySet; attempt++) {
onError: (Object e) { try {
debugPrint("onValueReceived stream error: $e"); if (attempt > 0) {
}, await Future.delayed(Duration(milliseconds: 500 * attempt));
);
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<bool>
});
// 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;
} }
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); _setState(MeshCoreConnectionState.connected);