feat(#978): split Button and buzzer into Node Settings and Message Settings

Button actions stay under Node Settings, shown only on caps byte 2 0x02. Buzzer and tones move to Message Settings as their own card under the phone-notifications card, shown only on 0x01. DeviceUiView now renders one part, requests only that part's state, and shows a refusal only on the screen it belongs to. Boards with a buzzer and no button exist (m5stack_unit_c6l, thinknode_m9, wio-tracker-l1-eink), which is why the homes are separate.

Placement and entry gates only: the screen already showed each half from its own bit, and every wire write is unchanged. The Buzzer and tones subtitle says 'When this radio beeps' rather than the mockup's 'and which tone', because no tone picker exists until #971 and #970.

Tests: all four button/buzzer combinations, and that each entry opens and requests only its own half. Full suite 1506 pass.

Agent: MistyLynx (session 6c0d5797)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pull/994/head
diffwireauto 3 weeks ago committed by Strycher
parent 4209b75284
commit bf47a26155

@ -324,6 +324,10 @@
"settings_gpsSleepFieldLabel": "Seconds between attempts",
"settings_gpsSleepFieldHint": "0 for the firmware default. The radio raises anything under 300 to 300.",
"settings_gpsSleepInvalid": "Enter a whole number of seconds, 0 or more.",
"settings_buttonActions": "Button actions",
"settings_buttonActionsSubtitle": "Assign what the button does",
"settings_buzzerTones": "Buzzer and tones",
"settings_buzzerTonesSubtitle": "When this radio beeps",
"settings_contactSettings": "Contact Settings",
"settings_contactSettingsSubtitle": "Settings for how contacts are added.",
"settings_privacyMode": "Privacy Mode",

@ -1162,6 +1162,30 @@ abstract class AppLocalizations {
/// **'Enter a whole number of seconds, 0 or more.'**
String get settings_gpsSleepInvalid;
/// No description provided for @settings_buttonActions.
///
/// In en, this message translates to:
/// **'Button actions'**
String get settings_buttonActions;
/// No description provided for @settings_buttonActionsSubtitle.
///
/// In en, this message translates to:
/// **'Assign what the button does'**
String get settings_buttonActionsSubtitle;
/// No description provided for @settings_buzzerTones.
///
/// In en, this message translates to:
/// **'Buzzer and tones'**
String get settings_buzzerTones;
/// No description provided for @settings_buzzerTonesSubtitle.
///
/// In en, this message translates to:
/// **'When this radio beeps'**
String get settings_buzzerTonesSubtitle;
/// No description provided for @settings_contactSettings.
///
/// In en, this message translates to:

@ -618,6 +618,18 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Настройки за контакти';

@ -620,6 +620,18 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Kontakteinstellungen';

@ -614,6 +614,18 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Contact Settings';

@ -618,6 +618,18 @@ class AppLocalizationsEs extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Configuración de contacto';

@ -621,6 +621,18 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Paramètres de contact';

@ -620,6 +620,18 @@ class AppLocalizationsHu extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Kapcsolat beállítások';

@ -620,6 +620,18 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Impostazioni di contatto';

@ -603,6 +603,18 @@ class AppLocalizationsJa extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => '連絡設定';

@ -603,6 +603,18 @@ class AppLocalizationsKo extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => '연락처 설정';

@ -617,6 +617,18 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Contactinstellingen';

@ -623,6 +623,18 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Ustawienia kontaktów';

@ -620,6 +620,18 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Configurações de Contato';

@ -619,6 +619,18 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Настройки контактов';

@ -618,6 +618,18 @@ class AppLocalizationsSk extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Nastavenia kontaktov';

@ -616,6 +616,18 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Nastavitve stika';

@ -615,6 +615,18 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Kontaktinställningar';

@ -617,6 +617,18 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => 'Налаштування контактів';

@ -599,6 +599,18 @@ class AppLocalizationsZh extends AppLocalizations {
String get settings_gpsSleepInvalid =>
'Enter a whole number of seconds, 0 or more.';
@override
String get settings_buttonActions => 'Button actions';
@override
String get settings_buttonActionsSubtitle => 'Assign what the button does';
@override
String get settings_buzzerTones => 'Buzzer and tones';
@override
String get settings_buzzerTonesSubtitle => 'When this radio beeps';
@override
String get settings_contactSettings => '联系人设置';

@ -13,13 +13,22 @@ import '../../connector/offband_device_ui.dart';
///
/// English-only for now, matching the serial-capture pane (#430); localization
/// is a follow-up rather than a blocker on shipping the capability.
///
/// Shows one [part] at a time (#978): button actions open from Node Settings,
/// the buzzer from Message Settings. They are separate features with separate
/// bits, and boards with a buzzer and no button exist.
class DeviceUiView extends StatefulWidget {
const DeviceUiView({super.key});
const DeviceUiView({super.key, required this.part});
final DeviceUiPart part;
@override
State<DeviceUiView> createState() => _DeviceUiViewState();
}
/// Which half of the device UI a [DeviceUiView] shows.
enum DeviceUiPart { buttons, buzzer }
class _DeviceUiViewState extends State<DeviceUiView> {
@override
void initState() {
@ -29,17 +38,34 @@ class _DeviceUiViewState extends State<DeviceUiView> {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final c = context.read<MeshCoreConnector>();
c.requestButtonMatrix();
c.requestNotifyScope();
switch (widget.part) {
case DeviceUiPart.buttons:
c.requestButtonMatrix();
case DeviceUiPart.buzzer:
c.requestNotifyScope();
}
});
}
/// An error is shown on the screen it belongs to. One that could not be
/// attributed to a write is shown on both halves rather than hidden.
bool _ownsError(DeviceUiSurface? surface) => switch (surface) {
null => true,
DeviceUiSurface.buttonMatrix => widget.part == DeviceUiPart.buttons,
DeviceUiSurface.notifyScope ||
DeviceUiSurface.channelTune => widget.part == DeviceUiPart.buzzer,
DeviceUiSurface.gpsMode => false,
};
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
final showButtons = connector.supportsButtonMatrix;
final showScope = connector.supportsNotifyScope;
final showButtons =
widget.part == DeviceUiPart.buttons &&
connector.supportsButtonMatrix;
final showScope =
widget.part == DeviceUiPart.buzzer && connector.supportsNotifyScope;
// A radio advertising neither bit has no button and no buzzer to
// configure, so it gets nothing. The tile that leads here is gated the
// same way, so this is a belt-and-braces guard rather than a path a
@ -82,7 +108,8 @@ class _DeviceUiViewState extends State<DeviceUiView> {
}
return ListView(
children: [
if (connector.deviceUiError != null)
if (connector.deviceUiError != null &&
_ownsError(connector.deviceUiErrorSurface))
_ErrorBanner(
message: connector.deviceUiError!,
onDismiss: connector.clearDeviceUiError,
@ -98,7 +125,6 @@ class _DeviceUiViewState extends State<DeviceUiView> {
),
),
..._scopeTiles(connector),
const Divider(height: 24),
],
if (showButtons) ...[
const _SectionHeader('Button actions'),

@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../connector/meshcore_connector.dart';
import '../../l10n/l10n.dart';
import '../../services/app_settings_service.dart';
import '../../services/notification_service.dart';
import '../../helpers/settings_persist.dart';
import '../../helpers/snack_bar_builder.dart';
import 'device_ui_view.dart';
/// Embeddable view (no Scaffold) for the Message Settings shell pane.
///
/// Renders notification toggles and message-handling settings. Both cards read
/// and write only [AppSettingsService].
/// Renders notification toggles and message-handling settings, which read and
/// write only [AppSettingsService]. Between them, on a radio with a buzzer,
/// sits the entry to the radio's own buzzer settings (#978).
class MessageSettingsView extends StatelessWidget {
const MessageSettingsView({super.key});
@ -23,6 +26,7 @@ class MessageSettingsView extends StatelessWidget {
children: [
_buildNotificationsCard(context, settingsService),
const SizedBox(height: 16),
const _RadioBuzzerEntry(),
_buildMessagingCard(context, settingsService),
],
);
@ -404,3 +408,40 @@ class MessageSettingsView extends StatelessWidget {
);
}
}
/// Entry to the radio's buzzer settings (#978), shown only where the radio
/// advertises a buzzer (caps byte 2 `0x01`). Separate from the card above,
/// which governs whether this PHONE notifies; this governs the radio.
class _RadioBuzzerEntry extends StatelessWidget {
const _RadioBuzzerEntry();
@override
Widget build(BuildContext context) {
final connector = context.watch<MeshCoreConnector>();
if (!connector.supportsNotifyScope) return const SizedBox.shrink();
final l10n = context.l10n;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Card(
child: ListTile(
leading: const Icon(Icons.volume_up_outlined),
title: Text(l10n.settings_buzzerTones),
subtitle: Text(l10n.settings_buzzerTonesSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: Text(l10n.settings_buzzerTones),
centerTitle: true,
),
body: const DeviceUiView(part: DeviceUiPart.buzzer),
),
),
),
),
),
);
}
}

@ -430,23 +430,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () => _editNodeName(context, connector),
),
// Location and the GPS toggle moved to their own category (#977).
// Button and buzzer: device UI config (#474/#475). Owner-placed
// under Node Settings, directly above the public key.
// Button actions (#474), directly above the public key. The
// buzzer moved to Message Settings (#978): boards with a buzzer
// and no button exist, so the two are separate entries on
// separate bits.
//
// Gated STRICTLY on the capability bits. A radio without the
// hardware has no button to configure, so it gets no tile, no
// screen and no command. This is the negative test in #474:
// "A device that does not advertise the bit shows no screen and
// the client emits no command."
if (connector.supportsButtonMatrix ||
connector.supportsNotifyScope) ...[
// Gated STRICTLY on the capability bit. A radio without a button
// gets no tile, no screen and no command. This is the negative
// test in #474: "A device that does not advertise the bit shows
// no screen and the client emits no command."
if (connector.supportsButtonMatrix) ...[
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.radio_button_checked_outlined),
title: const Text('Button and buzzer'),
subtitle: const Text(
'Assign button actions and set when this radio beeps',
),
title: Text(l10n.settings_buttonActions),
subtitle: Text(l10n.settings_buttonActionsSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
@ -454,10 +452,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: const Text('Button and buzzer'),
title: Text(l10n.settings_buttonActions),
centerTitle: true,
),
body: const DeviceUiView(),
body: const DeviceUiView(part: DeviceUiPart.buttons),
),
),
);

@ -0,0 +1,161 @@
// Pins #978: button actions live under Node Settings on caps byte 2 0x02, the
// buzzer under Message Settings on 0x01, and each opens only its own half.
// All four combinations are covered, because boards with a buzzer and no
// button exist (m5stack_unit_c6l, thinknode_m9, wio-tracker-l1-eink).
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/offband_device_ui.dart';
import 'package:meshcore_open/l10n/app_localizations.dart';
import 'package:meshcore_open/screens/settings_screen.dart';
import 'package:meshcore_open/services/app_settings_service.dart';
import 'package:meshcore_open/storage/drift/blob_store.dart';
import 'package:meshcore_open/storage/drift/offband_database.dart';
import 'package:meshcore_open/storage/prefs_manager.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _Radio extends MeshCoreConnector {
_Radio({required this.button, required this.buzzer});
final bool button;
final bool buzzer;
final List<String> requests = [];
@override
bool get supportsButtonMatrix => button;
@override
bool get supportsNotifyScope => buzzer;
// Values already read, so no screen sits on an animating spinner.
@override
ButtonMatrix? get buttonMatrix =>
const ButtonMatrix(assignments: {}, supportedActions: 0);
@override
DeviceNotifyScope? get deviceNotifyScope => DeviceNotifyScope.all;
@override
Future<void> requestButtonMatrix() async => requests.add('matrix');
@override
Future<void> requestNotifyScope() async => requests.add('scope');
}
void main() {
late OffbandDatabase db;
setUp(() async {
SharedPreferences.setMockInitialValues({});
PrefsManager.reset();
await PrefsManager.initialize();
db = OffbandDatabase(NativeDatabase.memory());
BlobStore.overrideForTest(BlobStore(db));
});
tearDown(() async {
BlobStore.clearTestOverride();
await db.close();
});
Future<void> pumpSettings(WidgetTester tester, _Radio radio) async {
radio.sendFrameOverrideForTest = (_) {};
tester.view.physicalSize = const Size(400, 1000);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await tester.pumpWidget(
MultiProvider(
providers: [
ChangeNotifierProvider<MeshCoreConnector>.value(value: radio),
ChangeNotifierProvider<AppSettingsService>(
create: (_) => AppSettingsService(),
),
],
child: const MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: SettingsScreen(),
),
),
);
await tester.pumpAndSettle();
}
Future<void> tapText(WidgetTester tester, String text) async {
await tester.scrollUntilVisible(find.text(text), 200);
await tester.tap(find.text(text));
await tester.pumpAndSettle();
}
Future<void> back(WidgetTester tester) async {
await tester.pageBack();
await tester.pumpAndSettle();
}
/// What Node Settings and Message Settings each offer for this radio.
Future<(bool, bool)> entries(WidgetTester tester, _Radio radio) async {
await pumpSettings(tester, radio);
await tapText(tester, 'Node Settings');
final hasButton = find.text('Button actions').evaluate().isNotEmpty;
await back(tester);
await tapText(tester, 'Message Settings');
final hasBuzzer = find.text('Buzzer and tones').evaluate().isNotEmpty;
return (hasButton, hasBuzzer);
}
testWidgets('button only: an entry in Node Settings, none for a buzzer', (
tester,
) async {
final r = await entries(tester, _Radio(button: true, buzzer: false));
expect(r, equals((true, false)));
});
testWidgets('buzzer only: an entry in Message Settings, none for a button', (
tester,
) async {
final r = await entries(tester, _Radio(button: false, buzzer: true));
expect(r, equals((false, true)));
});
testWidgets('both: each in its own place', (tester) async {
final r = await entries(tester, _Radio(button: true, buzzer: true));
expect(r, equals((true, true)));
});
testWidgets('neither: no entry anywhere', (tester) async {
final r = await entries(tester, _Radio(button: false, buzzer: false));
expect(r, equals((false, false)));
});
testWidgets('the combined Button and buzzer entry is gone', (tester) async {
await pumpSettings(tester, _Radio(button: true, buzzer: true));
await tapText(tester, 'Node Settings');
expect(find.text('Button and buzzer'), findsNothing);
});
testWidgets('Buzzer and tones opens only the buzzer half', (tester) async {
final radio = _Radio(button: true, buzzer: true);
await pumpSettings(tester, radio);
await tapText(tester, 'Message Settings');
await tapText(tester, 'Buzzer and tones');
expect(find.text('Device notification scope'), findsOneWidget);
// A button row: present whenever the button half renders.
expect(find.text('Single press'), findsNothing);
expect(radio.requests, equals(['scope']));
});
testWidgets('Button actions opens only the button half', (tester) async {
final radio = _Radio(button: true, buzzer: true);
await pumpSettings(tester, radio);
await tapText(tester, 'Node Settings');
await tapText(tester, 'Button actions');
expect(find.text('Single press'), findsOneWidget);
expect(find.text('Device notification scope'), findsNothing);
expect(radio.requests, equals(['matrix']));
});
}

@ -59,6 +59,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -369,6 +373,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -679,6 +687,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -989,6 +1001,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -1299,6 +1315,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -1609,6 +1629,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -1919,6 +1943,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -2229,6 +2257,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -2539,6 +2571,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -2849,6 +2885,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -3159,6 +3199,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -3469,6 +3513,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -3779,6 +3827,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -4089,6 +4141,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -4399,6 +4455,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -4709,6 +4769,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",
@ -5019,6 +5083,10 @@
"settings_gpsSleepFieldLabel",
"settings_gpsSleepFieldHint",
"settings_gpsSleepInvalid",
"settings_buttonActions",
"settings_buttonActionsSubtitle",
"settings_buzzerTones",
"settings_buzzerTonesSubtitle",
"settings_experimental",
"settings_experimentalSubtitle",
"settings_experimentalDescription",

Loading…
Cancel
Save

Powered by TurnKey Linux.