feat(#1100): set and surface the radio's BLE pairing PIN (cmd 37)

Stock-parity gap: the companion radio persists a pin set over
CMD_SET_DEVICE_PIN (37) and reports the configured value in device
info bytes 4..7, but the client exposed neither.

- cmdSetDevicePin + buildSetDevicePinFrame ([cmd][pin x4 LE]).
- parseDevicePin reads the device-info field; deviceBlePin getter
  (0 = firmware build default, null until reported), nulled on each
  new connection handshake; setDevicePin validates with firmware's
  own rule (0 or 100000-999999) and sends like the other config
  writes.
- Settings tile in the device Actions card, pin shown in PLAIN text
  (owner ruling 2026-11-06: no masking, parity with other mesh
  clients). Edit dialog with inline validation and Reset to default;
  persistent re-pair notice on success, persistent error when the
  send fails. English strings only; untranslated.json regenerated.

Gemini gate on the diff: fixed the unsaveable prefill of CLI-set
out-of-range pins, the failed-send success lie, and a controller
leak; double-tap idempotence and the short-frame fallback justified
in the PR. Negative evidence: mutating the encoding, the parse
offset, or the validation fails 4 of the 7 new tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/1104/head
diffwireauto 1 week ago committed by Strycher
parent ad319721ca
commit 655e396183

@ -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<void> 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<void> 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];

@ -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() {

@ -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",

@ -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:

@ -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 => 'Отстрани';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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';

@ -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 => 'デバッグ';

@ -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 => '디버깅';

@ -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';

@ -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';

@ -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';

@ -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 => 'Отладка';

@ -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';

@ -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';

@ -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';

@ -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 => 'Налагодження';

@ -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 => '调试';

@ -847,6 +847,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
},
),
_buildBlePinTile(context),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.restart_alt, color: Colors.orange),
@ -859,6 +860,134 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
/// #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<MeshCoreConnector, int?>(
(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<MeshCoreConnector>()),
),
],
);
}
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<void> _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

@ -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<Uint8List> 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);
});
});
}

@ -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",

Loading…
Cancel
Save

Powered by TurnKey Linux.