feat: Linux BLE pairing support via bluetoothctl

Add Linux BLE pairing helper that drives bluetoothctl for pair/trust/PIN
entry, with Completer-based flow control, explicit retry loop, and named
timeout constants.

- LinuxBlePairingService: pair-and-trust with up to 2 retries
- LinuxBleErrorClassifier: map bluetoothctl stderr to user-facing errors
- Conditional import stub for web builds (dart.library.io gate)
- Scanner screen: PIN dialog integration for Linux pairing flow
- MeshCoreConnector: Linux pairing/recovery/reconnect wiring
- l10n: 4 new pairing keys across all 14 locales
- 12 unit tests (pairing service + error classifier)
This commit is contained in:
just-stuff-tm
2026-03-15 16:28:57 -04:00
parent cb63b48b78
commit 29660d520e
39 changed files with 2031 additions and 40 deletions
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.",
"appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
"appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinTitle": "PIN за свързване чрез Bluetooth",
"scanner_linuxPairingShowPin": "Покажи PIN",
"scanner_linuxPairingHidePin": "Скриване на PIN кода",
"scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв)."
}
+12 -1
View File
@@ -1938,5 +1938,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde",
"appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen",
"appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingHidePin": "PIN verbergen",
"scanner_linuxPairingPinPrompt": "Geben Sie den PIN-Code für {deviceName} ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).",
"scanner_linuxPairingShowPin": "PIN anzeigen",
"scanner_linuxPairingPinTitle": "PIN für die Bluetooth-Verbindung"
}
+11
View File
@@ -127,6 +127,17 @@
}
}
},
"scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN",
"scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Show PIN",
"scanner_linuxPairingHidePin": "Hide PIN",
"scanner_stop": "Stop",
"scanner_scan": "Scan",
"scanner_bluetoothOff": "Bluetooth is off",
+12 -1
View File
@@ -1938,5 +1938,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.",
"appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes",
"appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).",
"scanner_linuxPairingHidePin": "Ocultar PIN",
"scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth",
"scanner_linuxPairingShowPin": "Mostrar código PIN"
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.",
"appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
"appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Afficher le code PIN",
"scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth",
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
"scanner_linuxPairingHidePin": "Masquer le code PIN"
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.",
"appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio",
"appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Mostra PIN",
"scanner_linuxPairingHidePin": "Nascondi il PIN",
"scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).",
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth"
}
+24
View File
@@ -592,6 +592,30 @@ abstract class AppLocalizations {
/// **'Connection failed: {error}'**
String scanner_connectionFailed(String error);
/// No description provided for @scanner_linuxPairingPinTitle.
///
/// In en, this message translates to:
/// **'Bluetooth Pairing PIN'**
String get scanner_linuxPairingPinTitle;
/// No description provided for @scanner_linuxPairingPinPrompt.
///
/// In en, this message translates to:
/// **'Enter PIN for {deviceName} (leave blank if none).'**
String scanner_linuxPairingPinPrompt(String deviceName);
/// No description provided for @scanner_linuxPairingShowPin.
///
/// In en, this message translates to:
/// **'Show PIN'**
String get scanner_linuxPairingShowPin;
/// No description provided for @scanner_linuxPairingHidePin.
///
/// In en, this message translates to:
/// **'Hide PIN'**
String get scanner_linuxPairingHidePin;
/// No description provided for @scanner_stop.
///
/// In en, this message translates to:
+14
View File
@@ -268,6 +268,20 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Връзката не успя: $error';
}
@override
String get scanner_linuxPairingPinTitle => 'PIN за свързване чрез Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).';
}
@override
String get scanner_linuxPairingShowPin => 'Покажи PIN';
@override
String get scanner_linuxPairingHidePin => 'Скриване на PIN кода';
@override
String get scanner_stop => 'Спрете';
+14
View File
@@ -271,6 +271,20 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Verbindungsfehler: $error';
}
@override
String get scanner_linuxPairingPinTitle => 'PIN für die Bluetooth-Verbindung';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Geben Sie den PIN-Code für $deviceName ein (lassen Sie das Feld leer, falls kein PIN-Code vorhanden ist).';
}
@override
String get scanner_linuxPairingShowPin => 'PIN anzeigen';
@override
String get scanner_linuxPairingHidePin => 'PIN verbergen';
@override
String get scanner_stop => 'Stopp';
+14
View File
@@ -265,6 +265,20 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Connection failed: $error';
}
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth Pairing PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Enter PIN for $deviceName (leave blank if none).';
}
@override
String get scanner_linuxPairingShowPin => 'Show PIN';
@override
String get scanner_linuxPairingHidePin => 'Hide PIN';
@override
String get scanner_stop => 'Stop';
+15
View File
@@ -269,6 +269,21 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Error de conexión: $error';
}
@override
String get scanner_linuxPairingPinTitle =>
'PIN para emparejar dispositivos Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Introduzca el código PIN para $deviceName (deje en blanco si no hay ninguno).';
}
@override
String get scanner_linuxPairingShowPin => 'Mostrar código PIN';
@override
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
@override
String get scanner_stop => 'Detener';
+15
View File
@@ -271,6 +271,21 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Échec de la connexion : $error';
}
@override
String get scanner_linuxPairingPinTitle =>
'Code PIN pour la connexion Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Entrez le code PIN pour $deviceName (laissez vide si nécessaire).';
}
@override
String get scanner_linuxPairingShowPin => 'Afficher le code PIN';
@override
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
@override
String get scanner_stop => 'Arrêter';
+15
View File
@@ -271,6 +271,21 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Connessione fallita: $error';
}
@override
String get scanner_linuxPairingPinTitle =>
'PIN per l\'accoppiamento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).';
}
@override
String get scanner_linuxPairingShowPin => 'Mostra PIN';
@override
String get scanner_linuxPairingHidePin => 'Nascondi il PIN';
@override
String get scanner_stop => 'Interrompere';
+14
View File
@@ -267,6 +267,20 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Verbinding mislukt: $error';
}
@override
String get scanner_linuxPairingPinTitle => 'PIN voor Bluetooth-koppeling';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Voer het pincode-in voor $deviceName in (laat dit leeg als er geen is).';
}
@override
String get scanner_linuxPairingShowPin => 'Toon PIN';
@override
String get scanner_linuxPairingHidePin => 'Verberg PIN';
@override
String get scanner_stop => 'Stoppen';
+15
View File
@@ -272,6 +272,21 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Połączenie nieudane: $error';
}
@override
String get scanner_linuxPairingPinTitle =>
'PIN do sparowania przez Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Wprowadź kod PIN dla $deviceName (pust, jeśli nie jest wymagany).';
}
@override
String get scanner_linuxPairingShowPin => 'Wyświetl kod PIN';
@override
String get scanner_linuxPairingHidePin => 'Ukryj kod PIN';
@override
String get scanner_stop => 'Zatrzymaj';
+14
View File
@@ -270,6 +270,20 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Falha na conexão: $error';
}
@override
String get scanner_linuxPairingPinTitle => 'PIN de pareamento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Insira o código PIN para $deviceName (deixe em branco se não houver).';
}
@override
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
@override
String get scanner_linuxPairingHidePin => 'Esconder o PIN';
@override
String get scanner_stop => 'Pare';
+15
View File
@@ -270,6 +270,21 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Подключение не удалось: $error';
}
@override
String get scanner_linuxPairingPinTitle =>
'PIN для сопряжения устройств по Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Введите PIN-код для $deviceName (оставьте поле пустым, если PIN-код отсутствует).';
}
@override
String get scanner_linuxPairingShowPin => 'Показать PIN-код';
@override
String get scanner_linuxPairingHidePin => 'Скрыть PIN-код';
@override
String get scanner_stop => 'Стоп';
+14
View File
@@ -269,6 +269,20 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Pripojenie zlyhalo: $error';
}
@override
String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Zadajte PIN pre $deviceName (nechajte prázdne, ak neexistuje).';
}
@override
String get scanner_linuxPairingShowPin => 'Zobraziť PIN';
@override
String get scanner_linuxPairingHidePin => 'Skryť PIN';
@override
String get scanner_stop => 'Zastavte';
+15
View File
@@ -267,6 +267,21 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Pošlo je z povezavo: $error';
}
@override
String get scanner_linuxPairingPinTitle =>
'PIN za združevanje preko Bluetootha';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Vnesite PIN kodo za $deviceName (ostavite prazno, če nimate kode).';
}
@override
String get scanner_linuxPairingShowPin => 'Prikaži PIN';
@override
String get scanner_linuxPairingHidePin => 'Skrijte PIN';
@override
String get scanner_stop => 'Prekliči';
+14
View File
@@ -266,6 +266,20 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Anslutning misslyckades: $error';
}
@override
String get scanner_linuxPairingPinTitle => 'PIN för Bluetooth-parning';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Ange PIN-kod för $deviceName (lämna tomt om ingen finns).';
}
@override
String get scanner_linuxPairingShowPin => 'Visa PIN-kod';
@override
String get scanner_linuxPairingHidePin => 'Dölj PIN-kod';
@override
String get scanner_stop => 'Stoppa';
+15
View File
@@ -269,6 +269,21 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Помилка підключення: $error';
}
@override
String get scanner_linuxPairingPinTitle =>
'PIN для з\'єднання через Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Введіть PIN-код для $deviceName (залиште поле порожнім, якщо немає).';
}
@override
String get scanner_linuxPairingShowPin => 'Показати PIN-код';
@override
String get scanner_linuxPairingHidePin => 'Приховати PIN-код';
@override
String get scanner_stop => 'Стоп';
+14
View File
@@ -256,6 +256,20 @@ class AppLocalizationsZh extends AppLocalizations {
return '连接失败:$error';
}
@override
String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return '输入 $deviceName 的 PIN 码(如果为空,则留空)。';
}
@override
String get scanner_linuxPairingShowPin => '显示PIN码';
@override
String get scanner_linuxPairingHidePin => '隐藏PIN码';
@override
String get scanner_stop => '停止';
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering",
"appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen",
"appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinTitle": "PIN voor Bluetooth-koppeling",
"scanner_linuxPairingShowPin": "Toon PIN",
"scanner_linuxPairingHidePin": "Verberg PIN",
"scanner_linuxPairingPinPrompt": "Voer het pincode-in voor {deviceName} in (laat dit leeg als er geen is)."
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Waga usunięta z trasy po nieudanej dostawie",
"appSettings_maxMessageRetries": "Maksymalna liczba prób wysłania wiadomości",
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingHidePin": "Ukryj kod PIN",
"scanner_linuxPairingPinTitle": "PIN do sparowania przez Bluetooth",
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pust, jeśli nie jest wymagany).",
"scanner_linuxPairingShowPin": "Wyświetl kod PIN"
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.",
"appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens",
"appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinTitle": "PIN de pareamento Bluetooth",
"scanner_linuxPairingPinPrompt": "Insira o código PIN para {deviceName} (deixe em branco se não houver).",
"scanner_linuxPairingShowPin": "Mostrar PIN",
"scanner_linuxPairingHidePin": "Esconder o PIN"
}
+12 -1
View File
@@ -1150,5 +1150,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.",
"appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения",
"appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Показать PIN-код",
"scanner_linuxPairingPinPrompt": "Введите PIN-код для {deviceName} (оставьте поле пустым, если PIN-код отсутствует).",
"scanner_linuxPairingHidePin": "Скрыть PIN-код",
"scanner_linuxPairingPinTitle": "PIN для сопряжения устройств по Bluetooth"
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie",
"appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ",
"appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (nechajte prázdne, ak neexistuje).",
"scanner_linuxPairingShowPin": "Zobraziť PIN",
"scanner_linuxPairingHidePin": "Skryť PIN",
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth"
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.",
"appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
"appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingShowPin": "Prikaži PIN",
"scanner_linuxPairingPinPrompt": "Vnesite PIN kodo za {deviceName} (ostavite prazno, če nimate kode).",
"scanner_linuxPairingHidePin": "Skrijte PIN",
"scanner_linuxPairingPinTitle": "PIN za združevanje preko Bluetootha"
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök",
"appSettings_maxMessageRetries": "Maximalt antal försök",
"appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinPrompt": "Ange PIN-kod för {deviceName} (lämna tomt om ingen finns).",
"scanner_linuxPairingHidePin": "Dölj PIN-kod",
"scanner_linuxPairingShowPin": "Visa PIN-kod",
"scanner_linuxPairingPinTitle": "PIN för Bluetooth-parning"
}
+12 -1
View File
@@ -1910,5 +1910,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки",
"appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення",
"appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingHidePin": "Приховати PIN-код",
"scanner_linuxPairingPinTitle": "PIN для з'єднання через Bluetooth",
"scanner_linuxPairingPinPrompt": "Введіть PIN-код для {deviceName} (залиште поле порожнім, якщо немає).",
"scanner_linuxPairingShowPin": "Показати PIN-код"
}
+12 -1
View File
@@ -1915,5 +1915,16 @@
"appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。",
"appSettings_maxMessageRetries": "最大消息重试次数",
"appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
"path_routeWeight": "{weight}/{max}"
"path_routeWeight": "{weight}/{max}",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
"scanner_linuxPairingHidePin": "隐藏PIN码",
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。",
"scanner_linuxPairingShowPin": "显示PIN码"
}