@ -165,6 +165,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 ;
@ -797,6 +798,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 ( ) ;
@ -820,15 +824,37 @@ class MeshCoreConnector extends ChangeNotifier {
rethrow ;
rethrow ;
}
}
/ / 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 ( ) ;
late final List < BluetoothService > services ;
try {
services = await device . discoverServices ( ) ;
} catch ( error ) {
debugPrint ( ' [BLE Connect] service discovery failure: $ error ' ) ;
if ( PlatformInfo . isWeb & &
error . toString ( ) . contains ( ' GATT Server is disconnected ' ) ) {
debugPrint (
' [BLE Connect] retrying service discovery after transient web disconnect ' ,
) ;
await Future < void > . delayed ( const Duration ( milliseconds: 300 ) ) ;
await device . connect (
timeout: const Duration ( seconds: 15 ) ,
mtu: null ,
license: License . free ,
) ;
services = await device . discoverServices ( ) ;
} else {
rethrow ;
}
}
BluetoothService ? uartService ;
BluetoothService ? uartService ;
for ( var service in services ) {
for ( var service in services ) {
@ -855,18 +881,32 @@ class MeshCoreConnector extends ChangeNotifier {
throw Exception ( " MeshCore characteristics not found " ) ;
throw Exception ( " MeshCore characteristics not found " ) ;
}
}
/ / Retry setNotifyValue with increasing delays
if ( PlatformInfo . isWeb ) {
bool notifySet = false ;
debugPrint ( ' Starting setNotifyValue(true) ' ) ;
for ( int attempt = 0 ; attempt < 3 & & ! notifySet ; attempt + + ) {
debugPrint ( ' Web: Calling setNotifyValue(true) without awaiting ' ) ;
try {
unawaited ( ( ) async {
if ( attempt > 0 ) {
try {
await Future . delayed ( Duration ( milliseconds: 500 * attempt ) ) ;
await _txCharacteristic ! . setNotifyValue ( true ) ;
} catch ( error ) {
debugPrint ( ' [BLE Connect] notify failure (web, ignored): $ 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 ( ' [BLE Connect] notify failure: $ 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 (
@ -881,19 +921,27 @@ class MeshCoreConnector extends ChangeNotifier {
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 ) ) ;
}
/ / Keep device clock aligned on every connection .
unawaited ( syncTime ( ) ) ;
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 ) ;
@ -1012,6 +1060,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 = >
@ -1020,7 +1069,9 @@ class MeshCoreConnector extends ChangeNotifier {
_activeTransport = = MeshCoreTransportType . bluetooth ;
_activeTransport = = MeshCoreTransportType . bluetooth ;
bool get _shouldGateInitialChannelSync = >
bool get _shouldGateInitialChannelSync = >
_activeTransport = = MeshCoreTransportType . usb ;
_activeTransport = = MeshCoreTransportType . usb | |
( _activeTransport = = MeshCoreTransportType . bluetooth & &
PlatformInfo . isWeb ) ;
void _cancelReconnectTimer ( ) {
void _cancelReconnectTimer ( ) {
_reconnectTimer ? . cancel ( ) ;
_reconnectTimer ? . cancel ( ) ;
@ -1121,6 +1172,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 ;
@ -1226,6 +1278,28 @@ class MeshCoreConnector extends ChangeNotifier {
void _scheduleSelfInfoRetry ( ) {
void _scheduleSelfInfoRetry ( ) {
_selfInfoRetryTimer ? . cancel ( ) ;
_selfInfoRetryTimer ? . cancel ( ) ;
if ( PlatformInfo . isWeb & &
_activeTransport = = MeshCoreTransportType . bluetooth ) {
var attempts = 0 ;
const maxAttempts = 3 ;
_selfInfoRetryTimer = Timer . periodic ( const Duration ( seconds: 10 ) , (
timer ,
) {
if ( ! isConnected | | ! _awaitingSelfInfo ) {
timer . cancel ( ) ;
return ;
}
if ( _isLoadingContacts | | _isSyncingChannels | | _channelSyncInFlight ) {
return ;
}
attempts + = 1 ;
unawaited ( sendFrame ( buildAppStartFrame ( ) ) ) ;
if ( attempts > = maxAttempts ) {
timer . cancel ( ) ;
}
} ) ;
return ;
}
_selfInfoRetryTimer = Timer . periodic ( const Duration ( milliseconds: 3500 ) , (
_selfInfoRetryTimer = Timer . periodic ( const Duration ( milliseconds: 3500 ) , (
timer ,
timer ,
) {
) {
@ -2009,6 +2083,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 ;
@ -2156,7 +2236,14 @@ class MeshCoreConnector extends ChangeNotifier {
_selfInfoRetryTimer = null ;
_selfInfoRetryTimer = null ;
notifyListeners ( ) ;
notifyListeners ( ) ;
getContacts ( ) ;
/ / 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 ) {
if ( _shouldGateInitialChannelSync ) {
_maybeStartInitialChannelSync ( ) ;
_maybeStartInitialChannelSync ( ) ;
}
}
@ -3109,6 +3196,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 {
@ -3772,6 +3867,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 ;