fix(#698): bounded GATT-133 connect retry + human-readable connect errors

connect() on non-Linux platforms now treats Android GATT status 133
(ANDROID_SPECIFIC_ERROR) as the transient failure it usually is: clean
close, 800 ms delay, one bounded retry, then surface. The scanner's
connect-failure snackbar no longer shows raw exception text: it maps
133, timeouts and everything else to localized plain-language messages,
persists until dismissed, and sends the raw detail to the app log
(#522 presentation acceptance).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dev
Strycher 5 days ago committed by Benjamin Wiechel
parent 783144cb6a
commit 3f05e67773

@ -2520,18 +2520,42 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
} else {
try {
await device.connect(
timeout: connectTimeout,
mtu: null,
license: License.free,
);
} catch (error) {
_appDebugLogService?.error(
'device.connect() failure: $error',
tag: 'BLE Connect',
);
rethrow;
var attempt = 0;
while (true) {
attempt++;
try {
await device.connect(
timeout: connectTimeout,
mtu: null,
license: License.free,
);
break;
} catch (error) {
// Android GATT status 133 (ANDROID_SPECIFIC_ERROR) is the
// notorious, frequently transient connect failure. Standard
// mitigation (#522/#698): clean close, short delay, one bounded
// retry before surfacing anything to the user.
final isTransient133 =
error is FlutterBluePlusException && error.code == 133;
_appDebugLogService?.error(
'device.connect() failure (attempt $attempt): $error',
tag: 'BLE Connect',
);
if (!isTransient133 || attempt >= 2) rethrow;
try {
await device.disconnect(queue: false, timeout: 5);
} catch (cleanupError) {
_appDebugLogService?.warn(
'cleanup disconnect before 133 retry failed: $cleanupError',
tag: 'BLE Connect',
);
}
await Future<void>.delayed(const Duration(milliseconds: 800));
_appDebugLogService?.info(
'retrying connect after GATT 133',
tag: 'BLE Connect',
);
}
}
}

@ -134,6 +134,18 @@
"scanner_scan": "Scan",
"scanner_bluetoothOff": "Bluetooth is off",
"scanner_bluetoothOffMessage": "Please turn on Bluetooth to scan for devices",
"scanner_connectFailedTransient": "The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.",
"@scanner_connectFailedTransient": {
"description": "Human-readable text for an Android GATT 133 connect failure after the bounded retry also failed (#698)."
},
"scanner_connectFailedTimeout": "The connection timed out. Check that the radio is powered, in range, and not already connected to another device.",
"@scanner_connectFailedTimeout": {
"description": "Human-readable text for a connect timeout (#698)."
},
"scanner_connectFailedGeneric": "Could not connect to the radio. Details are in the App Debug Log.",
"@scanner_connectFailedGeneric": {
"description": "Human-readable fallback for any other connect failure; the raw exception goes to the log only (#522/#698)."
},
"scanner_bleReleaseWarning": "Radio may still be held by this device",
"@scanner_bleReleaseWarning": {
"description": "Persistent banner title when a BLE disconnect was never confirmed by the OS, so the radio may stay latched and stop advertising (#697)."

@ -640,6 +640,24 @@ abstract class AppLocalizations {
/// **'Please turn on Bluetooth to scan for devices'**
String get scanner_bluetoothOffMessage;
/// Human-readable text for an Android GATT 133 connect failure after the bounded retry also failed (#698).
///
/// In en, this message translates to:
/// **'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.'**
String get scanner_connectFailedTransient;
/// Human-readable text for a connect timeout (#698).
///
/// In en, this message translates to:
/// **'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.'**
String get scanner_connectFailedTimeout;
/// Human-readable fallback for any other connect failure; the raw exception goes to the log only (#522/#698).
///
/// In en, this message translates to:
/// **'Could not connect to the radio. Details are in the App Debug Log.'**
String get scanner_connectFailedGeneric;
/// Persistent banner title when a BLE disconnect was never confirmed by the OS, so the radio may stay latched and stop advertising (#697).
///
/// In en, this message translates to:

@ -290,6 +290,18 @@ class AppLocalizationsBg extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -293,6 +293,18 @@ class AppLocalizationsDe extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Bitte aktivieren Sie Bluetooth, um nach Geräten zu suchen.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -287,6 +287,18 @@ class AppLocalizationsEn extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Please turn on Bluetooth to scan for devices';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -291,6 +291,18 @@ class AppLocalizationsEs extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, active el Bluetooth para escanear dispositivos.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -293,6 +293,18 @@ class AppLocalizationsFr extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Veuillez activer le Bluetooth pour rechercher des appareils.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -290,6 +290,18 @@ class AppLocalizationsHu extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Kérjük, kapcsolja be a Bluetooth-ot, hogy eszközök keresése lehessen.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -293,6 +293,18 @@ class AppLocalizationsIt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Si prega di attivare il Bluetooth per effettuare la scansione dei dispositivi.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -280,6 +280,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get scanner_bluetoothOffMessage => 'Bluetoothを有効にして、デバイスを検索してください。';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -279,6 +279,18 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get scanner_bluetoothOffMessage => '블루투스를 켜서 장치를 검색해주세요.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -289,6 +289,18 @@ class AppLocalizationsNl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -294,6 +294,18 @@ class AppLocalizationsPl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -292,6 +292,18 @@ class AppLocalizationsPt extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Por favor, ative o Bluetooth para escanear por dispositivos.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -292,6 +292,18 @@ class AppLocalizationsRu extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Пожалуйста, включите Bluetooth, чтобы найти устройства.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -291,6 +291,18 @@ class AppLocalizationsSk extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosím, zapnite Bluetooth, aby ste mohli skenovať pre zariadenia.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -289,6 +289,18 @@ class AppLocalizationsSl extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Prosimo, vklopite Bluetooth, da lahko poiščete naprave.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -288,6 +288,18 @@ class AppLocalizationsSv extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Vänligen aktivera Bluetooth för att söka efter enheter.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -291,6 +291,18 @@ class AppLocalizationsUk extends AppLocalizations {
String get scanner_bluetoothOffMessage =>
'Будь ласка, увімкніть Bluetooth, щоб сканувати пристрої.';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -277,6 +277,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get scanner_bluetoothOffMessage => '请开启蓝牙以搜索设备';
@override
String get scanner_connectFailedTransient =>
'The radio refused the connection (a common transient Bluetooth error). It was already retried; wait a few seconds and try again.';
@override
String get scanner_connectFailedTimeout =>
'The connection timed out. Check that the radio is powered, in range, and not already connected to another device.';
@override
String get scanner_connectFailedGeneric =>
'Could not connect to the radio. Details are in the App Debug Log.';
@override
String get scanner_bleReleaseWarning =>
'Radio may still be held by this device';

@ -346,16 +346,35 @@ class _ScannerScreenState extends State<ScannerScreen> {
);
return;
}
// Never show a raw exception to the user (#522/#698): the full detail
// goes to the log, the snackbar gets a plain-language message and
// persists until dismissed.
appLogger.error('Connect failed: $e', tag: 'ScannerScreen');
if (context.mounted) {
showDismissibleSnackBar(
context,
content: Text(context.l10n.scanner_connectionFailed(e.toString())),
content: Text(
context.l10n.scanner_connectionFailed(_describeConnectError(e)),
),
backgroundColor: Colors.red,
persist: true,
);
}
}
}
String _describeConnectError(Object e) {
final l10n = context.l10n;
if (e is FlutterBluePlusException && e.code == 133) {
return l10n.scanner_connectFailedTransient;
}
if (e is TimeoutException ||
(e is FlutterBluePlusException && e.function == 'connect')) {
return l10n.scanner_connectFailedTimeout;
}
return l10n.scanner_connectFailedGeneric;
}
Future<String?> _promptLinuxPairingPin(
BuildContext context,
String deviceName,

@ -1,5 +1,8 @@
{
"bg": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -203,6 +206,9 @@
],
"de": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -406,6 +412,9 @@
],
"es": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -609,6 +618,9 @@
],
"fr": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -812,6 +824,9 @@
],
"hu": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -1015,6 +1030,9 @@
],
"it": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -1218,6 +1236,9 @@
],
"ja": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -1421,6 +1442,9 @@
],
"ko": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -1624,6 +1648,9 @@
],
"nl": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -1827,6 +1854,9 @@
],
"pl": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -2030,6 +2060,9 @@
],
"pt": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -2233,6 +2266,9 @@
],
"ru": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -2436,6 +2472,9 @@
],
"sk": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -2639,6 +2678,9 @@
],
"sl": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -2842,6 +2884,9 @@
],
"sv": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -3045,6 +3090,9 @@
],
"uk": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",
@ -3248,6 +3296,9 @@
],
"zh": [
"scanner_connectFailedTransient",
"scanner_connectFailedTimeout",
"scanner_connectFailedGeneric",
"scanner_bleReleaseWarning",
"scanner_bleReleaseWarningMessage",
"settings_messageSettings",

Loading…
Cancel
Save

Powered by TurnKey Linux.