Merge pull request #1119 from OffbandMesh/feat/1054-factory-reset

feat(#1057): factory reset the connected radio
fix/1122-notify-logging
Benjamin Wiechel 5 days ago committed by GitHub
commit 6b8356fc87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -56,7 +56,9 @@ import '../storage/client_id_store.dart';
import '../storage/contact_discovery_store.dart';
import '../storage/contact_settings_store.dart';
import '../storage/contact_store.dart';
import '../storage/factory_reset_store.dart';
import '../storage/message_store.dart';
import '../storage/radio_data_wipe.dart';
import '../storage/unread_store.dart';
import '../utils/app_logger.dart';
import '../utils/battery_utils.dart';
@ -297,6 +299,28 @@ class ContactSyncShortfall {
/// What the radio said when asked for one contact by key. (#762)
enum ContactKeyCheckResult { onRadio, notFound, unresolved }
/// Why a factory reset was not sent. (#1054)
enum FactoryResetRefusal { notConnected }
/// Where a factory reset stands, for the app-wide banner. (#1054)
enum FactoryResetStatus {
/// Sent. Over BLE the radio's pairing is gone too, so the OS one must go.
sentBle,
sent,
/// The radio came back with a new identity and its app data was deleted.
complete,
/// New identity; the user chose to keep the app data.
completeKept,
/// Same identity on reconnect: the radio was not wiped. Nothing deleted.
notReset,
/// New identity, but deleting the old app data failed. Retried next connect.
deleteFailed,
}
class _PendingKeyCheck {
final String keyHex;
final DateTime startedAt;
@ -6856,6 +6880,127 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildRebootFrame());
}
final FactoryResetStore _factoryResetStore = FactoryResetStore();
FactoryResetStatus? _factoryResetStatus;
FactoryResetStatus? get factoryResetStatus => _factoryResetStatus;
void dismissFactoryResetStatus() {
_factoryResetStatus = null;
notifyListeners();
}
@visibleForTesting
String? transportIdentityForTest;
/// Which physical device the link goes to, independent of its identity,
/// which a factory reset replaces.
String? get _transportIdentity =>
transportIdentityForTest ??
switch (_activeTransport) {
MeshCoreTransportType.bluetooth => _deviceId,
MeshCoreTransportType.usb => activeUsbPort,
MeshCoreTransportType.tcp => activeTcpEndpoint,
};
String? get _radioPrefix {
final hex = selfPublicKeyHex;
return hex.length >= 10 ? hex.substring(0, 10) : null;
}
/// Wipes the connected radio (#1054). Null when sent, else why not.
///
/// The app data is NOT deleted here. The radio drops the link before it
/// formats, so nothing proves the wipe until it reconnects with a new
/// identity; see [_resolvePendingFactoryReset]. The session is ended on
/// purpose so it does not auto-reconnect into a wiped radio.
Future<FactoryResetRefusal?> factoryResetDevice({
required bool deleteAppData,
}) async {
final deviceId = _transportIdentity;
final prefix = _radioPrefix;
if (!isConnected || deviceId == null || prefix == null) {
return FactoryResetRefusal.notConnected;
}
await _factoryResetStore.save(
PendingFactoryReset(
deviceId: deviceId,
radioPrefix: prefix,
deleteAppData: deleteAppData,
),
);
final overBle = _activeTransport == MeshCoreTransportType.bluetooth;
_appDebugLogService?.info(
'Factory reset sent device=$deviceId radio=$prefix '
'deleteAppData=$deleteAppData',
tag: 'Connection',
);
await sendFrame(buildFactoryResetFrame());
_factoryResetStatus = overBle
? FactoryResetStatus.sentBle
: FactoryResetStatus.sent;
notifyListeners();
// Give the frame time to leave before the transport closes.
await Future<void>.delayed(const Duration(milliseconds: 500));
await disconnect();
return null;
}
/// Settles a pending factory reset on the first SELF_INFO of a connect.
///
/// Only a connect to the same device decides anything. Same identity means
/// the wipe did not happen and every byte of app data stays. A failed delete
/// keeps the record, so the next connect tries again.
Future<void> _resolvePendingFactoryReset() async {
final pending = _factoryResetStore.load();
final deviceId = _transportIdentity;
final prefix = _radioPrefix;
if (pending == null || deviceId == null || prefix == null) return;
final outcome = resolveFactoryReset(
pending,
deviceId: deviceId,
radioPrefix: prefix,
);
if (outcome == null) return;
if (outcome == FactoryResetOutcome.wipeAppData) {
try {
final removed = await wipeRadioData(pending.radioPrefix);
await _appSettingsService?.forgetRadio(pending.radioPrefix);
_appDebugLogService?.info(
'Factory reset confirmed, removed $removed stored items for '
'${pending.radioPrefix}',
tag: 'Connection',
);
} catch (e) {
_appDebugLogService?.error(
'Factory reset confirmed but deleting ${pending.radioPrefix} '
'failed, will retry next connect: $e',
tag: 'Connection',
);
_factoryResetStatus = FactoryResetStatus.deleteFailed;
notifyListeners();
return;
}
}
await _factoryResetStore.clear();
_factoryResetStatus = switch (outcome) {
FactoryResetOutcome.wipeAppData => FactoryResetStatus.complete,
FactoryResetOutcome.keepAppData => FactoryResetStatus.completeKept,
FactoryResetOutcome.notReset => FactoryResetStatus.notReset,
};
_appDebugLogService?.info(
'Factory reset resolved: $outcome',
tag: 'Connection',
);
notifyListeners();
}
@visibleForTesting
Future<void> resolvePendingFactoryResetForTest() =>
_resolvePendingFactoryReset();
Future<void> setPrivacyMode(bool enabled) async {
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
}
@ -8904,6 +9049,10 @@ class MeshCoreConnector extends ChangeNotifier {
// another radio's data.
storageService?.setPublicKeyHex = selfPublicKeyHex;
// A factory reset is proven or disproven here, by the identity this
// device now reports (#1054).
unawaited(_resolvePendingFactoryReset());
// Now that we have self info, we can load all the persisted data for this
// node. Each step is timed: this sequence has stalled the UI isolate for
// ~40s on a large store, and the timings say which step is responsible.

@ -239,6 +239,7 @@ const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdFactoryReset = 51;
const int cmdGetStats = 56;
const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58;
@ -1494,6 +1495,13 @@ Uint8List buildRebootFrame() {
return Uint8List.fromList([cmdReboot, ...utf8.encode('reboot')]);
}
// Build CMD_FACTORY_RESET frame (stock since firmware ver code 7)
// Format: [cmd]["reset"]. The radio disables its link before formatting, so
// success is seen as a drop, never an OK.
Uint8List buildFactoryResetFrame() {
return Uint8List.fromList([cmdFactoryReset, ...utf8.encode('reset')]);
}
// Build CMD_SYNC_NEXT_MESSAGE frame
Uint8List buildSyncNextMessageFrame() {
return Uint8List.fromList([cmdSyncNextMessage]);

@ -433,6 +433,21 @@
"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_factoryReset":"Factory Reset",
"settings_factoryResetSubtitle": "Erase the radio and give it a new identity",
"settings_factoryResetConfirm": "This erases everything on the radio: its identity, contacts, channels and settings. It can't be undone.",
"settings_factoryResetDeleteAppData": "Also delete this radio's data stored in the app",
"settings_factoryResetNotConnected": "Factory reset not sent: no radio is connected.",
"factoryResetSentTitle": "Factory reset sent",
"factoryResetSentBody": "The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.",
"factoryResetSentBleBody": "The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer's Bluetooth settings, then reconnect to finish.",
"factoryResetCompleteTitle": "Factory reset complete",
"factoryResetCompleteBody": "The radio has a new identity. Its old data was deleted from the app.",
"factoryResetCompleteKeptBody": "The radio has a new identity. Its old data was kept in the app.",
"factoryResetNotResetTitle": "Factory reset did not happen",
"factoryResetNotResetBody": "The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.",
"factoryResetDeleteFailedTitle": "Factory reset done, app cleanup failed",
"factoryResetDeleteFailedBody": "The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.",
"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.",

@ -1486,6 +1486,96 @@ 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_factoryReset.
///
/// In en, this message translates to:
/// **'Factory Reset'**
String get settings_factoryReset;
/// No description provided for @settings_factoryResetSubtitle.
///
/// In en, this message translates to:
/// **'Erase the radio and give it a new identity'**
String get settings_factoryResetSubtitle;
/// No description provided for @settings_factoryResetConfirm.
///
/// In en, this message translates to:
/// **'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.'**
String get settings_factoryResetConfirm;
/// No description provided for @settings_factoryResetDeleteAppData.
///
/// In en, this message translates to:
/// **'Also delete this radio\'s data stored in the app'**
String get settings_factoryResetDeleteAppData;
/// No description provided for @settings_factoryResetNotConnected.
///
/// In en, this message translates to:
/// **'Factory reset not sent: no radio is connected.'**
String get settings_factoryResetNotConnected;
/// No description provided for @factoryResetSentTitle.
///
/// In en, this message translates to:
/// **'Factory reset sent'**
String get factoryResetSentTitle;
/// No description provided for @factoryResetSentBody.
///
/// In en, this message translates to:
/// **'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.'**
String get factoryResetSentBody;
/// No description provided for @factoryResetSentBleBody.
///
/// In en, this message translates to:
/// **'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.'**
String get factoryResetSentBleBody;
/// No description provided for @factoryResetCompleteTitle.
///
/// In en, this message translates to:
/// **'Factory reset complete'**
String get factoryResetCompleteTitle;
/// No description provided for @factoryResetCompleteBody.
///
/// In en, this message translates to:
/// **'The radio has a new identity. Its old data was deleted from the app.'**
String get factoryResetCompleteBody;
/// No description provided for @factoryResetCompleteKeptBody.
///
/// In en, this message translates to:
/// **'The radio has a new identity. Its old data was kept in the app.'**
String get factoryResetCompleteKeptBody;
/// No description provided for @factoryResetNotResetTitle.
///
/// In en, this message translates to:
/// **'Factory reset did not happen'**
String get factoryResetNotResetTitle;
/// No description provided for @factoryResetNotResetBody.
///
/// In en, this message translates to:
/// **'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.'**
String get factoryResetNotResetBody;
/// No description provided for @factoryResetDeleteFailedTitle.
///
/// In en, this message translates to:
/// **'Factory reset done, app cleanup failed'**
String get factoryResetDeleteFailedTitle;
/// No description provided for @factoryResetDeleteFailedBody.
///
/// In en, this message translates to:
/// **'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.'**
String get factoryResetDeleteFailedBody;
/// No description provided for @settings_blePin.
///
/// In en, this message translates to:

@ -809,6 +809,62 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Сигурни ли сте, че искате да рестартирате устройството? Ще бъдете откъснати.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -806,6 +806,62 @@ 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_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -797,6 +797,62 @@ 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_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -807,6 +807,62 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'¿Está seguro de que desea reiniciar el dispositivo? Se desconectará.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -811,6 +811,62 @@ 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_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -809,6 +809,62 @@ 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_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -809,6 +809,62 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Sei sicuro di voler riavviare il dispositivo? Sarai disconnesso.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -783,6 +783,62 @@ class AppLocalizationsJa extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'本当にデバイスを再起動したいですか? その場合、接続が切断されます。';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -783,6 +783,62 @@ class AppLocalizationsKo extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'정말로 장치를 재부팅하시겠습니까? 이 경우 연결이 끊어집니다.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -803,6 +803,62 @@ 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_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -811,6 +811,62 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Czy na pewno chcesz zrestartować urządzenie? Będziesz odłączony.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -809,6 +809,62 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Tem certeza de que deseja reiniciar o dispositivo? Você será desconectado.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -808,6 +808,62 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Вы уверены, что хотите перезагрузить устройство? Вы будете отключены.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -803,6 +803,62 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Ste si istý, že chcete zariadenie reštartovať? Budete odpojení.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -801,6 +801,62 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -798,6 +798,62 @@ 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_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -805,6 +805,62 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_rebootDeviceConfirm =>
'Ви впевнені, що хочете перезавантажити пристрій? Вас буде відключено.';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -776,6 +776,62 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_rebootDeviceConfirm => '确定要重启设备吗?这将断开与设备的连接。';
@override
String get settings_factoryReset => 'Factory Reset';
@override
String get settings_factoryResetSubtitle =>
'Erase the radio and give it a new identity';
@override
String get settings_factoryResetConfirm =>
'This erases everything on the radio: its identity, contacts, channels and settings. It can\'t be undone.';
@override
String get settings_factoryResetDeleteAppData =>
'Also delete this radio\'s data stored in the app';
@override
String get settings_factoryResetNotConnected =>
'Factory reset not sent: no radio is connected.';
@override
String get factoryResetSentTitle => 'Factory reset sent';
@override
String get factoryResetSentBody =>
'The radio is erasing itself and will restart with a new identity. Reconnect to it to finish.';
@override
String get factoryResetSentBleBody =>
'The radio is erasing itself and will restart with a new identity. Its Bluetooth pairing is erased too: remove the old pairing for this radio in your phone or computer\'s Bluetooth settings, then reconnect to finish.';
@override
String get factoryResetCompleteTitle => 'Factory reset complete';
@override
String get factoryResetCompleteBody =>
'The radio has a new identity. Its old data was deleted from the app.';
@override
String get factoryResetCompleteKeptBody =>
'The radio has a new identity. Its old data was kept in the app.';
@override
String get factoryResetNotResetTitle => 'Factory reset did not happen';
@override
String get factoryResetNotResetBody =>
'The radio reconnected with the same identity, so it was not erased. Nothing was deleted from the app.';
@override
String get factoryResetDeleteFailedTitle =>
'Factory reset done, app cleanup failed';
@override
String get factoryResetDeleteFailedBody =>
'The radio was erased, but deleting its old data from the app failed. The app will try again the next time this radio connects.';
@override
String get settings_blePin => 'BLE pairing PIN';

@ -37,6 +37,7 @@ import 'services/storage_health_service.dart';
import 'services/radio_preset_service.dart';
import 'storage/drift/blob_store.dart';
import 'widgets/contact_sync_shortfall_banner.dart';
import 'widgets/factory_reset_banner.dart';
import 'widgets/storage_unavailable_banner.dart';
import 'storage/prefs_manager.dart';
import 'utils/app_logger.dart';
@ -369,7 +370,11 @@ class MeshCoreApp extends StatelessWidget {
onDismiss: connector.dismissContactSyncShortfall,
onKeep: connector.resolveContactSyncKeepLocal,
onUseRadio: connector.resolveContactSyncUseRadio,
child: child ?? const SizedBox.shrink(),
child: FactoryResetBanner(
status: connector.factoryResetStatus,
onDismiss: connector.dismissFactoryResetStatus,
child: child ?? const SizedBox.shrink(),
),
),
),
),

@ -875,6 +875,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
subtitle: Text(l10n.settings_rebootDeviceSubtitle),
onTap: () => _confirmReboot(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.delete_forever, color: Colors.red),
title: Text(l10n.settings_factoryReset),
subtitle: Text(l10n.settings_factoryResetSubtitle),
onTap: () => _confirmFactoryReset(context, connector),
),
],
),
);
@ -1287,6 +1294,63 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
void _confirmFactoryReset(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
var deleteAppData = true;
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setState) => AlertDialog(
title: Text(l10n.settings_factoryReset),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.settings_factoryResetConfirm),
const SizedBox(height: 8),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
value: deleteAppData,
onChanged: (value) =>
setState(() => deleteAppData = value ?? deleteAppData),
title: Text(l10n.settings_factoryResetDeleteAppData),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(dialogContext);
final refusal = await connector.factoryResetDevice(
deleteAppData: deleteAppData,
);
if (refusal == null || !context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(switch (refusal) {
FactoryResetRefusal.notConnected =>
l10n.settings_factoryResetNotConnected,
}),
backgroundColor: Colors.red,
persist: true,
);
},
child: Text(
l10n.settings_factoryReset,
style: const TextStyle(color: Colors.red),
),
),
],
),
),
);
}
Future<void> _gpxExport(
GpxExport exporter,
String name,

@ -297,6 +297,18 @@ class AppSettingsService extends ChangeNotifier {
);
}
/// Drops [devicePrefix]'s notify modes, the per-radio part of this global
/// blob, after that radio was factory reset (#1054).
Future<void> forgetRadio(String devicePrefix) async {
final prefix = _scopePrefix(devicePrefix);
if (prefix.isEmpty) return;
final byRadio = _copyByRadio();
if (byRadio.remove(prefix) == null) return;
await updateSettings(
_settings.copyWith(channelNotifyModesByRadio: byRadio),
);
}
/// One-time migration of the pre-#839 global state into the FIRST radio seen
/// after upgrade. Called by the connector once that radio's channels are
/// known. Later radios start clean, so the ambiguous legacy state never

@ -0,0 +1,100 @@
import 'dart:convert';
import 'prefs_manager.dart';
/// A factory reset that was sent and not yet proven (#1054).
///
/// The firmware disables the link before it formats, so the client never
/// hears whether the wipe worked. The proof is the next connect to the same
/// device: a new identity means it worked. Until then this record keeps the
/// old radio's data alive, so a reset that failed cannot cost the user their
/// history.
class PendingFactoryReset {
const PendingFactoryReset({
required this.deviceId,
required this.radioPrefix,
required this.deleteAppData,
});
/// The transport identity the reset went to: BLE remote id, USB port key or
/// TCP endpoint. A connect to anything else says nothing about this reset.
final String deviceId;
/// First 10 hex of the radio's key before the reset.
final String radioPrefix;
/// The user's choice in the confirm dialog.
final bool deleteAppData;
Map<String, dynamic> toJson() => {
'device_id': deviceId,
'radio_prefix': radioPrefix,
'delete_app_data': deleteAppData,
};
static PendingFactoryReset? fromJson(Map<String, dynamic> json) {
final deviceId = json['device_id'];
final radioPrefix = json['radio_prefix'];
final deleteAppData = json['delete_app_data'];
if (deviceId is! String ||
radioPrefix is! String ||
deleteAppData is! bool) {
return null;
}
return PendingFactoryReset(
deviceId: deviceId,
radioPrefix: radioPrefix,
deleteAppData: deleteAppData,
);
}
}
enum FactoryResetOutcome {
/// New identity, and the user asked for the app data to go.
wipeAppData,
/// New identity, and the user asked to keep the app data.
keepAppData,
/// Same identity: the radio was not wiped. Nothing is deleted.
notReset,
}
/// What connecting to [deviceId] as [radioPrefix] says about [pending].
///
/// Null when it is a different device, so the record stays and nothing is
/// decided on the strength of some other radio.
FactoryResetOutcome? resolveFactoryReset(
PendingFactoryReset pending, {
required String deviceId,
required String radioPrefix,
}) {
if (deviceId != pending.deviceId) return null;
if (radioPrefix == pending.radioPrefix) return FactoryResetOutcome.notReset;
return pending.deleteAppData
? FactoryResetOutcome.wipeAppData
: FactoryResetOutcome.keepAppData;
}
/// Survives an app restart between the reset and the reconnect, so closing
/// the app in between does not quietly skip the delete the user asked for.
class FactoryResetStore {
static const String _key = 'factory_reset_pending';
PendingFactoryReset? load() {
final raw = PrefsManager.instance.getString(_key);
if (raw == null) return null;
try {
return PendingFactoryReset.fromJson(
jsonDecode(raw) as Map<String, dynamic>,
);
} catch (_) {
return null;
}
}
Future<void> save(PendingFactoryReset pending) =>
PrefsManager.instance.setString(_key, jsonEncode(pending.toJson()));
Future<void> clear() => PrefsManager.instance.remove(_key);
}

@ -0,0 +1,49 @@
import 'drift/blob_store.dart';
import 'scoping_inventory.dart';
/// Deletes everything this app stores for one radio, after that radio was
/// factory reset (#1054).
///
/// Driven by [storageSurfaces] rather than a list of stores, so a per-radio
/// store added later is wiped without anyone remembering to add it here: the
/// inventory test already refuses an undeclared key. Every per-radio key in
/// this repo is the surface key followed by the 10-hex radio prefix, with or
/// without a suffix, so a prefix match per surface finds all of them in both
/// backends and in the message rows.
///
/// [radioPrefix] must be exactly 10 lowercase hex. Anything else throws before
/// a single delete: an empty prefix would match every radio's data.
///
/// Returns how many keys and row groups were removed.
Future<int> wipeRadioData(String radioPrefix) async {
if (!RegExp(r'^[0-9a-f]{10}$').hasMatch(radioPrefix)) {
throw ArgumentError.value(
radioPrefix,
'radioPrefix',
'must be the 10-hex radio key prefix',
);
}
final blobs = BlobStore.instance;
final prefixes = [
for (final s in storageSurfaces)
if (s.verdict == ScopingVerdict.perRadio) '${s.key}$radioPrefix',
];
var removed = 0;
for (final prefix in prefixes) {
for (final key in await blobs.keysWithPrefix(prefix)) {
await blobs.deleteEverywhere(key);
removed++;
}
}
for (final storeKey in await blobs.messageStoreKeys()) {
if (prefixes.any(storeKey.startsWith)) {
await blobs.deleteMessageRows(storeKey);
removed++;
}
}
return removed;
}

@ -248,6 +248,15 @@ const List<StorageSurface> storageSurfaces = [
reason: 'keyed by repeater node, same reasoning as repeater_passwords',
owner: 'lib/services/storage_service.dart',
),
StorageSurface(
'factory_reset_pending',
verdict: ScopingVerdict.globalOnPurpose,
reason:
'one factory reset awaiting proof (#1054). It spans two identities of '
'the same device, the key before the wipe and the key after, so it '
'cannot live under either. It names the old radio inside the record',
owner: 'lib/storage/factory_reset_store.dart',
),
StorageSurface(
'map_removed_marker_ids',
verdict: ScopingVerdict.globalOnPurpose,

@ -0,0 +1,118 @@
import 'package:flutter/material.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
/// Shown above the whole app while a factory reset is outstanding and once it
/// settles (#1054). The settings screen is gone by then, because the reset
/// ends the session, so the outcome is reported here and stays until
/// dismissed. Sits in `MaterialApp.builder`, like [ContactSyncShortfallBanner].
class FactoryResetBanner extends StatelessWidget {
final FactoryResetStatus? status;
final VoidCallback onDismiss;
final Widget child;
const FactoryResetBanner({
super.key,
required this.status,
required this.onDismiss,
required this.child,
});
@override
Widget build(BuildContext context) {
final current = status;
if (current == null) return child;
return Column(
children: [
_banner(context, current),
Expanded(child: child),
],
);
}
Widget _banner(BuildContext context, FactoryResetStatus current) {
final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme;
final failed =
current == FactoryResetStatus.notReset ||
current == FactoryResetStatus.deleteFailed;
final background = failed
? scheme.errorContainer
: scheme.secondaryContainer;
final foreground = failed
? scheme.onErrorContainer
: scheme.onSecondaryContainer;
final (title, body) = switch (current) {
FactoryResetStatus.sentBle => (
l10n.factoryResetSentTitle,
l10n.factoryResetSentBleBody,
),
FactoryResetStatus.sent => (
l10n.factoryResetSentTitle,
l10n.factoryResetSentBody,
),
FactoryResetStatus.complete => (
l10n.factoryResetCompleteTitle,
l10n.factoryResetCompleteBody,
),
FactoryResetStatus.completeKept => (
l10n.factoryResetCompleteTitle,
l10n.factoryResetCompleteKeptBody,
),
FactoryResetStatus.notReset => (
l10n.factoryResetNotResetTitle,
l10n.factoryResetNotResetBody,
),
FactoryResetStatus.deleteFailed => (
l10n.factoryResetDeleteFailedTitle,
l10n.factoryResetDeleteFailedBody,
),
};
return Material(
color: background,
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 10, 4, 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
failed ? Icons.error_outline : Icons.restart_alt,
color: foreground,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontWeight: FontWeight.bold,
color: foreground,
),
),
const SizedBox(height: 2),
Text(body, style: TextStyle(color: foreground)),
],
),
),
// No tooltip: this sits above the Navigator, where there is no
// Overlay, and a Tooltip without one blanks the window (#713).
IconButton(
icon: Icon(
Icons.close,
color: foreground,
semanticLabel: l10n.common_close,
),
onPressed: onDismiss,
),
],
),
),
),
);
}
}

@ -0,0 +1,180 @@
// #1057 (epic #1054): factory reset of the connected radio.
//
// The radio drops the link before it formats, so success is only ever seen as
// the same device coming back with a new identity. These pin that the app data
// waits for that proof, and that every branch where it cannot tell keeps it.
import 'dart:typed_data';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/storage/drift/blob_store.dart';
import 'package:meshcore_open/storage/drift/offband_database.dart';
import 'package:meshcore_open/storage/factory_reset_store.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late OffbandDatabase db;
late MeshCoreConnector connector;
late List<Uint8List> sent;
const oldPrefix = 'aaaaaaaaaa';
const newPrefix = 'bbbbbbbbbb';
Uint8List keyOf(int fill) => Uint8List.fromList(List.filled(32, fill));
setUp(() async {
SharedPreferences.setMockInitialValues({});
PrefsManager.reset();
await PrefsManager.initialize();
db = OffbandDatabase(NativeDatabase.memory());
BlobStore.overrideForTest(BlobStore(db));
sent = [];
connector = MeshCoreConnector()
..sendFrameOverrideForTest = sent.add
..transportIdentityForTest = 'dev-1'
..setSelfPublicKeyForTest(keyOf(0xaa))
..setFirmwareVerCodeForTest(14);
});
tearDown(() async {
await db.close();
BlobStore.clearTestOverride();
});
Future<void> seedOldRadio() async {
await BlobStore.instance.write('contacts$oldPrefix', '[]');
await PrefsManager.instance.setString('channels$oldPrefix', '[]');
}
Future<bool> oldRadioDataExists() async =>
(await BlobStore.instance.keysWithPrefix(
'contacts$oldPrefix',
)).isNotEmpty ||
PrefsManager.instance.getString('channels$oldPrefix') != null;
test('the frame is the command then "reset"', () {
expect(buildFactoryResetFrame(), [cmdFactoryReset, ...'reset'.codeUnits]);
expect(cmdFactoryReset, 51);
});
group('refusals send nothing', () {
test('not connected', () async {
final refusal = await connector.factoryResetDevice(deleteAppData: true);
expect(refusal, FactoryResetRefusal.notConnected);
expect(sent, isEmpty);
expect(FactoryResetStore().load(), isNull);
});
});
// Owner ruling: no FIRMWARE_VER_CODE gating. A radio that cannot reset keeps
// its identity, so the next connect resolves the record as "not reset".
for (final verCode in [null, 0, 6]) {
test('ver code $verCode is never consulted: the reset is sent', () async {
await seedOldRadio();
connector
..setConnectedForTest()
..setFirmwareVerCodeForTest(verCode);
final refusal = await connector.factoryResetDevice(deleteAppData: true);
expect(refusal, isNull);
expect(sent, [buildFactoryResetFrame()]);
});
}
test('a sent reset records the pending wipe, keeps the data, and ends the '
'session', () async {
await seedOldRadio();
connector.setConnectedForTest();
final refusal = await connector.factoryResetDevice(deleteAppData: true);
expect(refusal, isNull);
expect(sent.single, buildFactoryResetFrame());
final pending = FactoryResetStore().load();
expect(pending?.deviceId, 'dev-1');
expect(pending?.radioPrefix, oldPrefix);
expect(pending?.deleteAppData, isTrue);
expect(await oldRadioDataExists(), isTrue);
expect(connector.state, MeshCoreConnectionState.disconnected);
expect(connector.factoryResetStatus, isNotNull);
});
group('the next connect settles it', () {
Future<void> pend({bool deleteAppData = true}) => FactoryResetStore().save(
PendingFactoryReset(
deviceId: 'dev-1',
radioPrefix: oldPrefix,
deleteAppData: deleteAppData,
),
);
test('new identity, box ticked: old data deleted', () async {
await seedOldRadio();
await pend();
connector.setSelfPublicKeyForTest(keyOf(0xbb));
await connector.resolvePendingFactoryResetForTest();
expect(await oldRadioDataExists(), isFalse);
expect(FactoryResetStore().load(), isNull);
expect(connector.factoryResetStatus, FactoryResetStatus.complete);
});
test('new identity, box unticked: old data kept', () async {
await seedOldRadio();
await pend(deleteAppData: false);
connector.setSelfPublicKeyForTest(keyOf(0xbb));
await connector.resolvePendingFactoryResetForTest();
expect(await oldRadioDataExists(), isTrue);
expect(FactoryResetStore().load(), isNull);
expect(connector.factoryResetStatus, FactoryResetStatus.completeKept);
});
test('same identity: not reset, data kept', () async {
await seedOldRadio();
await pend();
await connector.resolvePendingFactoryResetForTest();
expect(await oldRadioDataExists(), isTrue);
expect(FactoryResetStore().load(), isNull);
expect(connector.factoryResetStatus, FactoryResetStatus.notReset);
});
test('a different device: nothing decided, data and record kept', () async {
await seedOldRadio();
await pend();
connector
..transportIdentityForTest = 'dev-2'
..setSelfPublicKeyForTest(keyOf(0xbb));
await connector.resolvePendingFactoryResetForTest();
expect(await oldRadioDataExists(), isTrue);
expect(FactoryResetStore().load(), isNotNull);
expect(connector.factoryResetStatus, isNull);
});
test('the new radio\'s own data is never touched', () async {
await pend();
await BlobStore.instance.write('contacts$newPrefix', '[]');
connector.setSelfPublicKeyForTest(keyOf(0xbb));
await connector.resolvePendingFactoryResetForTest();
expect(
await BlobStore.instance.keysWithPrefix('contacts$newPrefix'),
isNotEmpty,
);
});
});
}

@ -0,0 +1,174 @@
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/storage/drift/blob_store.dart';
import 'package:meshcore_open/storage/drift/offband_database.dart';
import 'package:meshcore_open/storage/factory_reset_store.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
import 'package:meshcore_open/storage/radio_data_wipe.dart';
import 'package:meshcore_open/storage/scoping_inventory.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// #1057: the factory-reset app-data wipe and the reconnect decision.
void main() {
late OffbandDatabase db;
const wiped = 'aaaaaaaaaa';
const other = 'bbbbbbbbbb';
final perRadio = [
for (final s in storageSurfaces)
if (s.verdict == ScopingVerdict.perRadio) s.key,
];
final global = [
for (final s in storageSurfaces)
if (s.verdict == ScopingVerdict.globalOnPurpose) s.key,
];
setUp(() async {
SharedPreferences.setMockInitialValues({});
PrefsManager.reset();
await PrefsManager.initialize();
db = OffbandDatabase(NativeDatabase.memory());
BlobStore.overrideForTest(BlobStore(db));
});
tearDown(() async {
await db.close();
BlobStore.clearTestOverride();
});
/// Every perRadio surface gets a drift key, a prefs key and message rows for
/// both radios, so the wipe has to tell them apart in every backend.
Future<void> seed() async {
final blobs = BlobStore.instance;
final prefs = PrefsManager.instance;
for (final radio in [wiped, other]) {
for (final surface in perRadio) {
await blobs.write('$surface${radio}_drift', 'v');
await prefs.setString('$surface${radio}_prefs', 'v');
await blobs.upsertMessageRows('$surface${radio}_rows', const [
MessageRowInput(mergeKey: 'm', timestampMs: 1, payload: '{}'),
]);
}
}
for (final surface in global) {
await prefs.setString(surface, 'v');
}
}
Future<Set<String>> allKeys() async => {
...await BlobStore.instance.keysWithPrefix(''),
...await BlobStore.instance.messageStoreKeys(),
};
test('removes every perRadio surface of the reset radio, in every '
'backend', () async {
await seed();
await wipeRadioData(wiped);
final left = await allKeys();
for (final surface in perRadio) {
expect(
left.where((k) => k.startsWith('$surface$wiped')),
isEmpty,
reason: '$surface survived the wipe',
);
}
});
test('leaves another radio and every global surface alone', () async {
await seed();
await wipeRadioData(wiped);
final left = await allKeys();
for (final surface in perRadio) {
for (final suffix in ['_drift', '_prefs', '_rows']) {
expect(left, contains('$surface$other$suffix'));
}
}
for (final surface in global) {
expect(PrefsManager.instance.getString(surface), 'v');
}
});
test('refuses anything but a 10-hex prefix, before deleting', () async {
await seed();
final before = await allKeys();
for (final bad in [
'',
'aaaaa',
'AAAAAAAAAA',
'aaaaaaaaaaa',
'zzzzzzzzzz',
]) {
await expectLater(wipeRadioData(bad), throwsArgumentError);
}
expect(await allKeys(), before);
});
group('resolveFactoryReset', () {
const pending = PendingFactoryReset(
deviceId: 'dev-1',
radioPrefix: wiped,
deleteAppData: true,
);
test('new key on the same device with the box ticked wipes', () {
expect(
resolveFactoryReset(pending, deviceId: 'dev-1', radioPrefix: other),
FactoryResetOutcome.wipeAppData,
);
});
test('new key with the box unticked keeps', () {
const keep = PendingFactoryReset(
deviceId: 'dev-1',
radioPrefix: wiped,
deleteAppData: false,
);
expect(
resolveFactoryReset(keep, deviceId: 'dev-1', radioPrefix: other),
FactoryResetOutcome.keepAppData,
);
});
test('same key means the radio was not reset', () {
expect(
resolveFactoryReset(pending, deviceId: 'dev-1', radioPrefix: wiped),
FactoryResetOutcome.notReset,
);
});
test('a different device decides nothing', () {
expect(
resolveFactoryReset(pending, deviceId: 'dev-2', radioPrefix: other),
isNull,
);
});
});
test('the pending record survives a restart and clears', () async {
final store = FactoryResetStore();
await store.save(
const PendingFactoryReset(
deviceId: 'COM7',
radioPrefix: wiped,
deleteAppData: true,
),
);
final reloaded = FactoryResetStore().load();
expect(reloaded?.deviceId, 'COM7');
expect(reloaded?.radioPrefix, wiped);
expect(reloaded?.deleteAppData, isTrue);
await store.clear();
expect(FactoryResetStore().load(), isNull);
});
test('an unreadable pending record loads as none', () async {
await PrefsManager.instance.setString('factory_reset_pending', '{oops');
expect(FactoryResetStore().load(), isNull);
});
}

@ -79,6 +79,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -458,6 +473,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -837,6 +867,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -1216,6 +1261,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -1595,6 +1655,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -1974,6 +2049,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -2353,6 +2443,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -2732,6 +2837,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -3111,6 +3231,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -3490,6 +3625,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -3869,6 +4019,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -4248,6 +4413,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -4627,6 +4807,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -5006,6 +5201,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -5385,6 +5595,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -5764,6 +5989,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",
@ -6143,6 +6383,21 @@
"settings_badgeBatteryReading",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_factoryReset",
"settings_factoryResetSubtitle",
"settings_factoryResetConfirm",
"settings_factoryResetDeleteAppData",
"settings_factoryResetNotConnected",
"factoryResetSentTitle",
"factoryResetSentBody",
"factoryResetSentBleBody",
"factoryResetCompleteTitle",
"factoryResetCompleteBody",
"factoryResetCompleteKeptBody",
"factoryResetNotResetTitle",
"factoryResetNotResetBody",
"factoryResetDeleteFailedTitle",
"factoryResetDeleteFailedBody",
"settings_blePin",
"settings_blePinDefault",
"settings_blePinHelp",

Loading…
Cancel
Save

Powered by TurnKey Linux.