From ee3af52c0fd99d5444553d2e846671c0cc83eeee Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 01:50:38 -0500 Subject: [PATCH] Add initial contacts sync handling for web Bluetooth transport --- lib/connector/meshcore_connector.dart | 128 +++++++++++++++++++------- 1 file changed, 95 insertions(+), 33 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 392be9a..4b39068 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -164,6 +164,7 @@ class MeshCoreConnector extends ChangeNotifier { bool _awaitingSelfInfo = false; bool _hasReceivedDeviceInfo = false; bool _pendingInitialChannelSync = false; + bool _pendingInitialContactsSync = false; bool _preserveContactsOnRefresh = false; static const int _defaultMaxContacts = 32; static const int _defaultMaxChannels = 8; @@ -783,6 +784,9 @@ class MeshCoreConnector extends ChangeNotifier { _lastDeviceDisplayName = _deviceDisplayName; _manualDisconnect = false; _cancelReconnectTimer(); + if (PlatformInfo.isWeb) { + _resetConnectionHandshakeState(); + } unawaited(_backgroundService?.start()); notifyListeners(); @@ -799,12 +803,14 @@ class MeshCoreConnector extends ChangeNotifier { license: License.free, ); - // Request larger MTU for sending larger frames - try { - final mtu = await device.requestMtu(185); - debugPrint('MTU set to: $mtu'); - } catch (e) { - debugPrint('MTU request failed: $e, using default'); + // Request larger MTU only on native platforms; web does not support it. + if (!PlatformInfo.isWeb) { + 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(); @@ -834,17 +840,30 @@ class MeshCoreConnector extends ChangeNotifier { throw Exception("MeshCore characteristics not found"); } - bool notifySet = false; - for (int attempt = 0; attempt < 3 && !notifySet; attempt++) { - try { - if (attempt > 0) { - await Future.delayed(Duration(milliseconds: 500 * attempt)); + if (PlatformInfo.isWeb) { + debugPrint('Starting setNotifyValue(true)'); + debugPrint('Web: Calling setNotifyValue(true) without awaiting'); + unawaited(() async { + try { + await _txCharacteristic!.setNotifyValue(true); + } catch (error) { + debugPrint('Web setNotifyValue error (ignoring): $error'); + } + }()); + debugPrint('setNotifyValue(true) configuration completed'); + } else { + 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; } - await _txCharacteristic!.setNotifyValue(true); - notifySet = true; - } catch (e) { - debugPrint('setNotifyValue attempt ${attempt + 1}/3 failed: $e'); - if (attempt == 2) rethrow; } } _notifySubscription = _txCharacteristic!.onValueReceived.listen( @@ -852,22 +871,34 @@ class MeshCoreConnector extends ChangeNotifier { ); _setState(MeshCoreConnectionState.connected); + if (_shouldGateInitialChannelSync) { + _hasReceivedDeviceInfo = false; + _pendingInitialChannelSync = true; + } await _requestDeviceInfo(); _startBatteryPolling(); - final gotSelfInfo = await _waitForSelfInfo( - timeout: const Duration(seconds: 3), - ); - if (!gotSelfInfo) { - await refreshDeviceInfo(); - await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + // Chrome's Web Bluetooth stack commonly delays incoming notifications + // until the non-blocking notify setup settles. Avoid stacking extra + // startup writes while that is happening. + } else { + final gotSelfInfo = await _waitForSelfInfo( + timeout: const Duration(seconds: 3), + ); + if (!gotSelfInfo) { + await refreshDeviceInfo(); + await _waitForSelfInfo(timeout: const Duration(seconds: 3)); + } + + unawaited(syncTime()); } - // Keep device clock aligned on every connection. - await syncTime(); - // Fetch channels so we can track unread counts for incoming messages - unawaited(getChannels()); + if (!_shouldGateInitialChannelSync) { + unawaited(getChannels()); + } } catch (e) { debugPrint("Connection error: $e"); await disconnect(manual: false); @@ -981,6 +1012,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; } bool get _shouldAutoReconnect => @@ -988,6 +1020,11 @@ class MeshCoreConnector extends ChangeNotifier { _lastDeviceId != null && _activeTransport == MeshCoreTransportType.bluetooth; + bool get _shouldGateInitialChannelSync => + _activeTransport == MeshCoreTransportType.usb || + (_activeTransport == MeshCoreTransportType.bluetooth && + PlatformInfo.isWeb); + void _cancelReconnectTimer() { _reconnectTimer?.cancel(); _reconnectTimer = null; @@ -1087,6 +1124,7 @@ class MeshCoreConnector extends ChangeNotifier { _awaitingSelfInfo = false; _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false; @@ -1191,6 +1229,10 @@ class MeshCoreConnector extends ChangeNotifier { void _scheduleSelfInfoRetry() { _selfInfoRetryTimer?.cancel(); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + return; + } _selfInfoRetryTimer = Timer.periodic(const Duration(milliseconds: 3500), ( timer, ) { @@ -1974,6 +2016,12 @@ class MeshCoreConnector extends ChangeNotifier { _preserveContactsOnRefresh = false; notifyListeners(); unawaited(_persistContacts()); + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + _isSyncingChannels && + !_channelSyncInFlight) { + unawaited(_requestNextChannel()); + } if (!_didInitialQueueSync || _pendingQueueSync) { _didInitialQueueSync = true; _pendingQueueSync = false; @@ -2120,16 +2168,22 @@ class MeshCoreConnector extends ChangeNotifier { _selfInfoRetryTimer = null; notifyListeners(); - // Auto-fetch contacts after getting self info - getContacts(); - if (_activeTransport == MeshCoreTransportType.usb) { + // Auto-fetch contacts after getting self info. On web BLE, defer this + // until after channel 0 so startup writes stay serialized. + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth) { + _pendingInitialContactsSync = true; + } else { + getContacts(); + } + if (_shouldGateInitialChannelSync) { _maybeStartInitialChannelSync(); } } void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; - if (_activeTransport == MeshCoreTransportType.usb) { + if (_shouldGateInitialChannelSync) { _hasReceivedDeviceInfo = true; } _firmwareVerCode = frame[1]; @@ -2156,14 +2210,13 @@ class MeshCoreConnector extends ChangeNotifier { unawaited(loadChannelSettings(maxChannels: nextMaxChannels)); unawaited(loadAllChannelMessages(maxChannels: nextMaxChannels)); if (isConnected && - (_activeTransport != MeshCoreTransportType.usb || - !_pendingInitialChannelSync)) { + (!_shouldGateInitialChannelSync || !_pendingInitialChannelSync)) { unawaited(getChannels(maxChannels: nextMaxChannels)); } } } notifyListeners(); - if (_activeTransport == MeshCoreTransportType.usb) { + if (_shouldGateInitialChannelSync) { _maybeStartInitialChannelSync(); } } @@ -3075,6 +3128,14 @@ class MeshCoreConnector extends ChangeNotifier { // Move to next channel _nextChannelIndexToRequest++; + if (PlatformInfo.isWeb && + _activeTransport == MeshCoreTransportType.bluetooth && + channel.index == 0 && + _pendingInitialContactsSync) { + _pendingInitialContactsSync = false; + unawaited(getContacts()); + return; + } unawaited(_requestNextChannel()); return; } else { @@ -3738,6 +3799,7 @@ class MeshCoreConnector extends ChangeNotifier { // They're only cleared on manual disconnect via disconnect() method _hasReceivedDeviceInfo = false; _pendingInitialChannelSync = false; + _pendingInitialContactsSync = false; _maxContacts = _defaultMaxContacts; _maxChannels = _defaultMaxChannels; _isSyncingQueuedMessages = false;