Add initial contacts sync handling for web Bluetooth transport

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