diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 0ce68841..bfdf3d51 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -490,6 +490,13 @@ class MeshCoreConnector extends ChangeNotifier { static const int _defaultMaxChannels = 40; int _maxContacts = _defaultMaxContacts; int _maxChannels = _defaultMaxChannels; + + /// The radio's configured BLE pairing pin from device info (#1100). + /// 0 = the firmware build default (on display builds that means a random + /// per-session pin shown on the device). Null until a device-info frame + /// long enough to carry it arrives. + int? _deviceBlePin; + int? get deviceBlePin => _deviceBlePin; int? _contactSyncTotal; int _contactSyncReceived = 0; bool _contactSyncUsesSinceFilter = false; @@ -4044,6 +4051,8 @@ class MeshCoreConnector extends ChangeNotifier { _unreadStore.setPublicKeyHex = ''; _discoveryContactStore.setPublicKeyHex = ''; storageService?.setPublicKeyHex = ''; + // Device-scoped: the next radio's device info supplies its own pin (#1100). + _deviceBlePin = null; _applySelfKeyToBlockService(); } @@ -6309,6 +6318,21 @@ class MeshCoreConnector extends ChangeNotifier { await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}'); } + /// Sets the radio's BLE pairing pin over CMD_SET_DEVICE_PIN (#1100). + /// [pin] is 0 (return to the firmware build default) or a 6-digit value; + /// the range mirrors firmware's own validation, which answers ERR outside + /// it. The OS bond becomes stale after a change: the UI tells the user a + /// forget/re-pair may be needed. + Future setDevicePin(int pin) async { + if (!isConnected) return; + if (pin != 0 && (pin < 100000 || pin > 999999)) { + throw ArgumentError.value(pin, 'pin', 'must be 0 or a 6-digit pin'); + } + await sendFrame(buildSetDevicePinFrame(pin)); + _deviceBlePin = pin; + notifyListeners(); + } + Future setTelemetryModeBase( int base, int location, @@ -8113,6 +8137,14 @@ class MeshCoreConnector extends ChangeNotifier { static int? parseOffbandCaps2(Uint8List frame) => frame.length >= 85 ? frame[84] : null; + /// The configured BLE pairing pin, bytes 4..7 LE of device info's 8-byte + /// header, written unconditionally by the companion firmware right after + /// max contacts/channels (MyMesh.cpp, CMD_DEVICE_QUERY). Null only on a + /// frame too short to carry it. (#1100) + static int? parseDevicePin(Uint8List frame) => frame.length >= 8 + ? frame[4] | (frame[5] << 8) | (frame[6] << 16) | (frame[7] << 24) + : null; + void _handleDeviceInfo(Uint8List frame) { if (frame.length < 4) return; if (_shouldGateInitialChannelSync) { @@ -8210,6 +8242,9 @@ class MeshCoreConnector extends ChangeNotifier { // Caps landed, if the radio speaks block, pull its list and union-reconcile. _reconcileFirmwareBlock(); + // BLE pairing pin, bytes 4..7 of the header (#1100). + _deviceBlePin = parseDevicePin(frame) ?? _deviceBlePin; + // Firmware reports MAX_CONTACTS / 2 for v3+ device info. final reportedContacts = frame[2]; final reportedChannels = frame[3]; diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index ceedb35c..4554367f 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -233,6 +233,7 @@ const int cmdGetContactByKey = 30; const int cmdGetChannel = 31; const int cmdSetChannel = 32; const int cmdSendTracePath = 36; +const int cmdSetDevicePin = 37; const int cmdSetOtherParams = 38; const int cmdSendTelemetryReq = 39; const int cmdGetCustomVar = 40; @@ -1443,6 +1444,18 @@ Uint8List buildSetCustomVarFrame(String value) { return writer.toBytes(); } +// Build CMD_SET_DEVICE_PIN frame (#1100) +// Format: [cmd][pin x4 LE]. Firmware accepts 0 (return to the build default, +// which on display builds means a random per-session pin) or a 6-digit pin +// 100000-999999, persists it and answers OK; anything else answers ERR +// (companion_radio MyMesh.cpp, CMD_SET_DEVICE_PIN). +Uint8List buildSetDevicePinFrame(int pin) { + final writer = BufferWriter(); + writer.writeByte(cmdSetDevicePin); + writer.writeInt32LE(pin); + return writer.toBytes(); +} + // Build CMD_REBOOT frame // Format: [cmd]["reboot"] Uint8List buildRebootFrame() { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fc5a4677..621a29d2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -431,6 +431,28 @@ "settings_rebootDevice": "Reboot Device", "settings_rebootDeviceSubtitle": "Restart the MeshCore device", "settings_rebootDeviceConfirm": "Are you sure you want to reboot the device? You will be disconnected.", + "settings_blePin": "BLE pairing PIN", + "settings_blePinDefault": "Firmware default", + "settings_blePinHelp": "6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware's built-in behavior.", + "settings_blePinInvalid": "Enter a 6-digit PIN between 100000 and 999999", + "settings_blePinSaved": "PIN set to {pin}. Already-paired devices may need to forget the radio and pair again.", + "@settings_blePinSaved": { + "placeholders": { + "pin": { + "type": "String" + } + } + }, + "settings_blePinResetDone": "PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.", + "settings_blePinSendFailed": "The PIN was not changed: sending to the radio failed ({error})", + "@settings_blePinSendFailed": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "settings_blePinResetAction": "Reset to default", "settings_debug": "Debug", "settings_experimental": "Experimental", "settings_experimentalSubtitle": "Advanced and in-development features", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c64ec3c1..dbecf4a6 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1474,6 +1474,54 @@ abstract class AppLocalizations { /// **'Are you sure you want to reboot the device? You will be disconnected.'** String get settings_rebootDeviceConfirm; + /// No description provided for @settings_blePin. + /// + /// In en, this message translates to: + /// **'BLE pairing PIN'** + String get settings_blePin; + + /// No description provided for @settings_blePinDefault. + /// + /// In en, this message translates to: + /// **'Firmware default'** + String get settings_blePinDefault; + + /// No description provided for @settings_blePinHelp. + /// + /// In en, this message translates to: + /// **'6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'** + String get settings_blePinHelp; + + /// No description provided for @settings_blePinInvalid. + /// + /// In en, this message translates to: + /// **'Enter a 6-digit PIN between 100000 and 999999'** + String get settings_blePinInvalid; + + /// No description provided for @settings_blePinSaved. + /// + /// In en, this message translates to: + /// **'PIN set to {pin}. Already-paired devices may need to forget the radio and pair again.'** + String settings_blePinSaved(String pin); + + /// No description provided for @settings_blePinResetDone. + /// + /// In en, this message translates to: + /// **'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'** + String get settings_blePinResetDone; + + /// No description provided for @settings_blePinSendFailed. + /// + /// In en, this message translates to: + /// **'The PIN was not changed: sending to the radio failed ({error})'** + String settings_blePinSendFailed(String error); + + /// No description provided for @settings_blePinResetAction. + /// + /// In en, this message translates to: + /// **'Reset to default'** + String get settings_blePinResetAction; + /// No description provided for @settings_debug. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 1de8aee6..ab5b6670 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -802,6 +802,37 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Отстрани'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2ff95514..e9c17717 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -799,6 +799,37 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Fehlerbehebung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a54bed81..063a76b8 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -790,6 +790,37 @@ class AppLocalizationsEn extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Are you sure you want to reboot the device? You will be disconnected.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Debug'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 9d2ecb51..6dce8dad 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -800,6 +800,37 @@ class AppLocalizationsEs extends AppLocalizations { String get settings_rebootDeviceConfirm => '¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Depurar'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index e5765685..e0ba7965 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -804,6 +804,37 @@ class AppLocalizationsFr extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Êtes-vous sûr de vouloir redémarrer l\'appareil ? Vous serez déconnecté.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Déboguer'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 68aa77eb..9b37efb5 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -802,6 +802,37 @@ class AppLocalizationsHu extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Biztosan szeretné újraindítani a készüléket? Ebben az esetben a kapcsolat megszűnik.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Hibakeresés'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index f11a8a5c..fe1b3af3 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -802,6 +802,37 @@ class AppLocalizationsIt extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Sei sicuro di voler riavviare il dispositivo? Sarai disconnesso.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Risoluzione dei problemi'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 1fb331a8..c444c550 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -776,6 +776,37 @@ class AppLocalizationsJa extends AppLocalizations { String get settings_rebootDeviceConfirm => '本当にデバイスを再起動したいですか? その場合、接続が切断されます。'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'デバッグ'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 9a7c320c..b113c164 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -776,6 +776,37 @@ class AppLocalizationsKo extends AppLocalizations { String get settings_rebootDeviceConfirm => '정말로 장치를 재부팅하시겠습니까? 이 경우 연결이 끊어집니다.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => '디버깅'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 2962b084..0db0a53c 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -796,6 +796,37 @@ class AppLocalizationsNl extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Ben je er zeker van dat je het apparaat opnieuw wilt opstarten? Je wordt losgekoppeld.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Foutopsporing'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 9f412ea4..24a05cdc 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -804,6 +804,37 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Czy na pewno chcesz zrestartować urządzenie? Będziesz odłączony.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Naprawianie błędów'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index b65a487e..9d24878e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -802,6 +802,37 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Depurar'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index fc66e2db..9c4f376c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -801,6 +801,37 @@ class AppLocalizationsRu extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Отладка'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 324dbb65..31d1e52a 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -796,6 +796,37 @@ class AppLocalizationsSk extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Ste si istý, že chcete zariadenie reštartovať? Budete odpojení.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Ladenie'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 7d2d1f8c..75bcc351 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -794,6 +794,37 @@ class AppLocalizationsSl extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Odpravljanje napak'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 5d93902f..cdd05b50 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -791,6 +791,37 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Är du säker på att du vill starta om enheten? Du kommer att bli avkopplad.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Felsök'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 38f71fa0..5cb26871 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -798,6 +798,37 @@ class AppLocalizationsUk extends AppLocalizations { String get settings_rebootDeviceConfirm => 'Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => 'Налагодження'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9620e163..d89901aa 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -769,6 +769,37 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_rebootDeviceConfirm => '确定要重启设备吗?这将断开与设备的连接。'; + @override + String get settings_blePin => 'BLE pairing PIN'; + + @override + String get settings_blePinDefault => 'Firmware default'; + + @override + String get settings_blePinHelp => + '6 digits (100000-999999). New phones and PCs must enter it when pairing. Reset to default to return to the firmware\'s built-in behavior.'; + + @override + String get settings_blePinInvalid => + 'Enter a 6-digit PIN between 100000 and 999999'; + + @override + String settings_blePinSaved(String pin) { + return 'PIN set to $pin. Already-paired devices may need to forget the radio and pair again.'; + } + + @override + String get settings_blePinResetDone => + 'PIN reset to the firmware default. Already-paired devices may need to forget the radio and pair again.'; + + @override + String settings_blePinSendFailed(String error) { + return 'The PIN was not changed: sending to the radio failed ($error)'; + } + + @override + String get settings_blePinResetAction => 'Reset to default'; + @override String get settings_debug => '调试'; diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index b516b0e1..1f009509 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -847,6 +847,7 @@ class _SettingsScreenState extends State { ); }, ), + _buildBlePinTile(context), const Divider(height: 1), ListTile( leading: const Icon(Icons.restart_alt, color: Colors.orange), @@ -859,6 +860,134 @@ class _SettingsScreenState extends State { ); } + /// #1100: the radio's BLE pairing pin, shown in plain text (owner decision + /// 2026-11-06: no masking, matching other mesh clients; any connected + /// client can read it from device info anyway). Hidden until a device-info + /// frame carries the field. select() so only this tile rebuilds when the + /// pin changes (the #81 lesson). + Widget _buildBlePinTile(BuildContext context) { + final l10n = context.l10n; + final blePin = context.select( + (c) => c.deviceBlePin, + ); + if (blePin == null) return const SizedBox.shrink(); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider(height: 1), + ListTile( + leading: const Icon(Icons.password), + title: Text(l10n.settings_blePin), + subtitle: Text( + // Firmware prints the pin as %06d; mirror it, and CLI-set values + // outside the 6-digit range still display as the radio shows them. + blePin == 0 + ? l10n.settings_blePinDefault + : blePin.toString().padLeft(6, '0'), + ), + onTap: () => _editBlePin(context, context.read()), + ), + ], + ); + } + + void _editBlePin(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + final current = connector.deviceBlePin ?? 0; + // Prefill only a value cmd 37 would accept back. The firmware CLI can + // store out-of-range pins (unvalidated atoi); offering one for re-save + // would only invite the validation refusal (#1100 review). + final controller = TextEditingController( + text: current >= 100000 && current <= 999999 ? current.toString() : '', + ); + String? errorText; + showDialog( + context: context, + builder: (dialogContext) => StatefulBuilder( + builder: (dialogContext, setDialogState) => AlertDialog( + title: Text(l10n.settings_blePin), + content: TextField( + controller: controller, + autofocus: true, + keyboardType: TextInputType.number, + maxLength: 6, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: InputDecoration( + helperText: l10n.settings_blePinHelp, + helperMaxLines: 4, + errorText: errorText, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + _sendBlePin( + context, + connector, + 0, + l10n.settings_blePinResetDone, + ); + }, + child: Text(l10n.settings_blePinResetAction), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(l10n.common_cancel), + ), + TextButton( + onPressed: () { + final pin = int.tryParse(controller.text); + if (pin == null || pin < 100000 || pin > 999999) { + // Refuse the write at the attempt and say why, inline: + // the range is firmware's own validation (#1100). + setDialogState(() { + errorText = l10n.settings_blePinInvalid; + }); + return; + } + Navigator.pop(dialogContext); + _sendBlePin( + context, + connector, + pin, + l10n.settings_blePinSaved(controller.text), + ); + }, + child: Text(l10n.common_save), + ), + ], + ), + ), + ).whenComplete(controller.dispose); + } + + /// The success notice shows only once the frame was actually written; a + /// transport failure surfaces as a persistent error instead of silently + /// showing a pin the radio never received (#1100 review). + Future _sendBlePin( + BuildContext context, + MeshCoreConnector connector, + int pin, + String successText, + ) async { + final l10n = context.l10n; + try { + await connector.setDevicePin(pin); + } catch (e) { + if (!context.mounted) return; + showDismissibleSnackBar( + context, + content: Text(l10n.settings_blePinSendFailed(e.toString())), + backgroundColor: Theme.of(context).colorScheme.error, + persist: true, + ); + return; + } + if (!context.mounted) return; + showDismissibleSnackBar(context, content: Text(successText), persist: true); + } + Widget _buildDebugCard(BuildContext context) { final l10n = context.l10n; // #884: the Device CLI console is offered only when the connected radio diff --git a/test/connector/device_pin_test.dart b/test/connector/device_pin_test.dart new file mode 100644 index 00000000..90755e5d --- /dev/null +++ b/test/connector/device_pin_test.dart @@ -0,0 +1,93 @@ +// #1100: BLE pairing pin. The companion radio persists a pin set over +// CMD_SET_DEVICE_PIN (37) and reports the configured value in device info +// bytes 4..7 LE (companion_radio MyMesh.cpp). These tests pin the frame +// format, the parse, and the connector's validation. + +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/connector/meshcore_protocol.dart'; + +void main() { + group('buildSetDevicePinFrame', () { + test('encodes [cmd 37][pin x4 LE]', () { + expect( + buildSetDevicePinFrame(123456), + Uint8List.fromList([37, 0x40, 0xE2, 0x01, 0x00]), + ); + }); + + test('0 clears to the firmware default', () { + expect(buildSetDevicePinFrame(0), Uint8List.fromList([37, 0, 0, 0, 0])); + }); + }); + + group('parseDevicePin', () { + Uint8List deviceInfo(int pin, {int length = 8}) { + final frame = Uint8List(length); + frame[0] = respCodeDeviceInfo; + frame[1] = 14; // ver code + frame[2] = 100; // MAX_CONTACTS / 2 + frame[3] = 40; // MAX_GROUP_CHANNELS + if (length >= 8) { + frame[4] = pin & 0xFF; + frame[5] = (pin >> 8) & 0xFF; + frame[6] = (pin >> 16) & 0xFF; + frame[7] = (pin >> 24) & 0xFF; + } + return frame; + } + + test('reads bytes 4..7 LE', () { + expect(MeshCoreConnector.parseDevicePin(deviceInfo(654321)), 654321); + expect(MeshCoreConnector.parseDevicePin(deviceInfo(0)), 0); + }); + + test('null on a frame too short to carry the field', () { + expect( + MeshCoreConnector.parseDevicePin(deviceInfo(0, length: 4)), + isNull, + ); + }); + }); + + group('setDevicePin', () { + late MeshCoreConnector c; + late List sent; + + setUp(() { + c = MeshCoreConnector(); + sent = []; + c.sendFrameOverrideForTest = sent.add; + c.setConnectedForTest(); + }); + + test('sends cmd 37 and updates the cached pin', () async { + await c.setDevicePin(234567); + expect(sent.single, buildSetDevicePinFrame(234567)); + expect(c.deviceBlePin, 234567); + }); + + test( + 'refuses an out-of-range pin without sending (firmware range)', + () async { + for (final bad in [1, 99999, 1000000, -1]) { + await expectLater( + () => c.setDevicePin(bad), + throwsArgumentError, + reason: '$bad is outside 0 / 100000-999999', + ); + } + expect(sent, isEmpty); + expect(c.deviceBlePin, isNull); + }, + ); + + test('0 is a valid reset to the build default', () async { + await c.setDevicePin(0); + expect(sent.single, buildSetDevicePinFrame(0)); + expect(c.deviceBlePin, 0); + }); + }); +} diff --git a/untranslated.json b/untranslated.json index 3af74f33..010ef01f 100644 --- a/untranslated.json +++ b/untranslated.json @@ -77,6 +77,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -421,6 +429,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -765,6 +781,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -1109,6 +1133,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -1453,6 +1485,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -1797,6 +1837,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -2141,6 +2189,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -2485,6 +2541,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -2829,6 +2893,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -3173,6 +3245,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -3517,6 +3597,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -3861,6 +3949,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -4205,6 +4301,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -4549,6 +4653,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -4893,6 +5005,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -5237,6 +5357,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription", @@ -5581,6 +5709,14 @@ "settings_badgeBatteryReading", "settings_buzzerTones", "settings_buzzerTonesSubtitle", + "settings_blePin", + "settings_blePinDefault", + "settings_blePinHelp", + "settings_blePinInvalid", + "settings_blePinSaved", + "settings_blePinResetDone", + "settings_blePinSendFailed", + "settings_blePinResetAction", "settings_experimental", "settings_experimentalSubtitle", "settings_experimentalDescription",