From 9ef1c51d64c30a6e1a2ab30e315f885eb2b15e91 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 7 Feb 2026 03:36:23 -0700 Subject: [PATCH 1/6] Add configurable battery chemistry for repeaters - Add per-repeater battery chemistry setting (NMC, LiFePO4, LiPo) - Create shared battery_utils.dart for voltage-to-percentage calculation - Support optional firmware-reported chemistry (protocol extension at byte 60) - Show lock icon when chemistry is firmware-reported, dropdown when user-configurable - Add l10n string for "Battery Type" label Addresses #116 --- lib/l10n/app_en.arb | 1 + lib/l10n/app_localizations.dart | 6 ++ lib/l10n/app_localizations_bg.dart | 3 + lib/l10n/app_localizations_de.dart | 3 + lib/l10n/app_localizations_en.dart | 3 + lib/l10n/app_localizations_es.dart | 3 + lib/l10n/app_localizations_fr.dart | 3 + lib/l10n/app_localizations_it.dart | 3 + lib/l10n/app_localizations_nl.dart | 3 + lib/l10n/app_localizations_pl.dart | 3 + lib/l10n/app_localizations_pt.dart | 3 + lib/l10n/app_localizations_ru.dart | 3 + lib/l10n/app_localizations_sk.dart | 3 + lib/l10n/app_localizations_sl.dart | 3 + lib/l10n/app_localizations_sv.dart | 3 + lib/l10n/app_localizations_uk.dart | 3 + lib/l10n/app_localizations_zh.dart | 3 + lib/models/app_settings.dart | 17 +++- lib/screens/repeater_status_screen.dart | 123 +++++++++++++++++++++--- lib/services/app_settings_service.dart | 17 ++++ lib/utils/battery_utils.dart | 22 +++++ untranslated.json | 58 ++++++++++- 22 files changed, 276 insertions(+), 13 deletions(-) create mode 100644 lib/utils/battery_utils.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ee5cf7d6..899a3963 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -806,6 +806,7 @@ }, "repeater_systemInformation": "System Information", "repeater_battery": "Battery", + "repeater_batteryChemistry": "Battery Type", "repeater_clockAtLogin": "Clock (at login)", "repeater_uptime": "Uptime", "repeater_queueLength": "Queue Length", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 055667fa..39f218b5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3081,6 +3081,12 @@ abstract class AppLocalizations { /// **'Battery'** String get repeater_battery; + /// No description provided for @repeater_batteryChemistry. + /// + /// In en, this message translates to: + /// **'Battery Type'** + String get repeater_batteryChemistry; + /// No description provided for @repeater_clockAtLogin. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 701429e6..e9ac99f1 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1705,6 +1705,9 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_battery => 'Батерия'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Часовник (при влизане)'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 514a7a19..84852c7c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1702,6 +1702,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_battery => 'Akku'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Uhr (bei Anmeldung)'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 040d8090..ff102ff4 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1675,6 +1675,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_battery => 'Battery'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Clock (at login)'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index e65cbcd3..4a55f4bd 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1699,6 +1699,9 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_battery => 'Batería'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Reloj (al inicio de sesión)'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4496fc84..4bf92558 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1710,6 +1710,9 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_battery => 'Batterie'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Horloge (au démarrage)'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 02345c4e..0a0f3058 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1699,6 +1699,9 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_battery => 'Batteria'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Orologio (all\'accesso)'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 292181fc..3e741935 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1693,6 +1693,9 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_battery => 'Batterij'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Tijd (bij aanmelden)'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 08323295..2f0a39fd 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1703,6 +1703,9 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_battery => 'Bateria'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Godzina (przy logowaniu)'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index eadea3bc..74816b03 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1700,6 +1700,9 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_battery => 'Bateria'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Relógio (no login)'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index ec0f1ba3..11f2fdb3 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1702,6 +1702,9 @@ class AppLocalizationsRu extends AppLocalizations { @override String get repeater_battery => 'Батарея'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Время (при входе)'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 346047b0..0b9fecd7 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1695,6 +1695,9 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_battery => 'Batéria'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Čas (při přihlášení)'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index ed711224..9bcd74e8 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1694,6 +1694,9 @@ class AppLocalizationsSl extends AppLocalizations { @override String get repeater_battery => 'Baterija'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Ure (pri prijavi)'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 97b849fa..894e8ee3 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1684,6 +1684,9 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_battery => 'Batteri'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Klocka (vid inloggning)'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 899d540d..78ad27f3 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1703,6 +1703,9 @@ class AppLocalizationsUk extends AppLocalizations { @override String get repeater_battery => 'Батарея'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => 'Годинник (при вході)'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 77467924..32f91e46 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1623,6 +1623,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_battery => '电池'; + @override + String get repeater_batteryChemistry => 'Battery Type'; + @override String get repeater_clockAtLogin => '登录时的时间'; diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index 3edb68fa..bd674b4d 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -21,6 +21,7 @@ class AppSettings { final String? languageOverride; // null = system default final bool appDebugLogEnabled; final Map batteryChemistryByDeviceId; + final Map repeaterBatteryChemistryByPublicKey; AppSettings({ this.clearPathOnMaxRetry = false, @@ -43,7 +44,10 @@ class AppSettings { this.languageOverride, this.appDebugLogEnabled = false, Map? batteryChemistryByDeviceId, - }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}; + Map? repeaterBatteryChemistryByPublicKey, + }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, + repeaterBatteryChemistryByPublicKey = + repeaterBatteryChemistryByPublicKey ?? {}; Map toJson() { return { @@ -67,6 +71,8 @@ class AppSettings { 'language_override': languageOverride, 'app_debug_log_enabled': appDebugLogEnabled, 'battery_chemistry_by_device_id': batteryChemistryByDeviceId, + 'repeater_battery_chemistry_by_public_key': + repeaterBatteryChemistryByPublicKey, }; } @@ -101,6 +107,11 @@ class AppSettings { (key, value) => MapEntry(key.toString(), value.toString()), ) ?? {}, + repeaterBatteryChemistryByPublicKey: + (json['repeater_battery_chemistry_by_public_key'] as Map?)?.map( + (key, value) => MapEntry(key.toString(), value.toString()), + ) ?? + {}, ); } @@ -125,6 +136,7 @@ class AppSettings { Object? languageOverride = _unset, bool? appDebugLogEnabled, Map? batteryChemistryByDeviceId, + Map? repeaterBatteryChemistryByPublicKey, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -154,6 +166,9 @@ class AppSettings { appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled, batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId, + repeaterBatteryChemistryByPublicKey: + repeaterBatteryChemistryByPublicKey ?? + this.repeaterBatteryChemistryByPublicKey, ); } } diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 472b0137..bc4269a4 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -9,6 +9,8 @@ import '../models/path_selection.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; import '../services/repeater_command_service.dart'; +import '../services/app_settings_service.dart'; +import '../utils/battery_utils.dart'; import '../widgets/path_management_dialog.dart'; class RepeaterStatusScreen extends StatefulWidget { @@ -54,6 +56,7 @@ class _RepeaterStatusScreenState extends State { int? _dupFlood; int? _dupDirect; PathSelection? _pendingStatusSelection; + String? _reportedChemistry; // From firmware, if available @override void initState() { @@ -157,6 +160,17 @@ class _RepeaterStatusScreenState extends State { offset += 2; final rxAirSecs = data.getUint32(offset, Endian.little); + // Check for optional chemistry byte at offset 60 (protocol extension) + String? reportedChem; + if (frame.length > _statusResponseBytes) { + final chemByte = frame[_statusResponseBytes]; + reportedChem = switch (chemByte) { + 0x01 => 'lifepo4', + 0x02 => 'lipo', + _ => 'nmc', + }; + } + _statusTimeout?.cancel(); if (!mounted) return; setState(() { @@ -178,6 +192,7 @@ class _RepeaterStatusScreenState extends State { _lastSnr = lastSnrRaw / 4.0; _dupDirect = directDups; _dupFlood = floodDups; + _reportedChemistry = reportedChem; }); _recordStatusResult(true); } @@ -251,6 +266,7 @@ class _RepeaterStatusScreenState extends State { _directRx = null; _dupFlood = null; _dupDirect = null; + _reportedChemistry = null; }); try { @@ -438,6 +454,12 @@ class _RepeaterStatusScreenState extends State { Widget _buildSystemInfoCard() { final l10n = context.l10n; + final settingsService = context.watch(); + final userChemistry = + settingsService.batteryChemistryForRepeater(widget.repeater.publicKeyHex); + // Prefer firmware-reported chemistry, fall back to user setting + final chemistry = _reportedChemistry ?? userChemistry; + return Card( child: Padding( padding: const EdgeInsets.all(16), @@ -461,7 +483,12 @@ class _RepeaterStatusScreenState extends State { ], ), const Divider(), - _buildInfoRow(l10n.repeater_battery, _batteryText()), + _buildInfoRow(l10n.repeater_battery, _batteryText(chemistry)), + _buildBatteryChemistryRow( + settingsService, + chemistry, + isFromFirmware: _reportedChemistry != null, + ), _buildInfoRow(l10n.repeater_clockAtLogin, _clockText()), _buildInfoRow(l10n.repeater_uptime, _formatDuration(_uptimeSecs)), _buildInfoRow(l10n.repeater_queueLength, _formatValue(_queueLen)), @@ -472,6 +499,88 @@ class _RepeaterStatusScreenState extends State { ); } + Widget _buildBatteryChemistryRow( + AppSettingsService settingsService, + String currentChemistry, { + required bool isFromFirmware, + }) { + final l10n = context.l10n; + + // Map chemistry code to display name + String chemistryDisplayName(String chem) { + return switch (chem) { + 'lifepo4' => l10n.appSettings_batteryLifepo4, + 'lipo' => l10n.appSettings_batteryLipo, + _ => l10n.appSettings_batteryNmc, + }; + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 130, + child: Text( + l10n.repeater_batteryChemistry, + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded( + child: isFromFirmware + // Firmware-reported: show as read-only text with indicator + ? Row( + children: [ + Text( + chemistryDisplayName(currentChemistry), + style: const TextStyle(fontWeight: FontWeight.w400), + ), + const SizedBox(width: 8), + Icon( + Icons.lock_outline, + size: 16, + color: Colors.grey[500], + ), + ], + ) + // User-configurable: show dropdown + : DropdownButton( + value: currentChemistry, + isDense: true, + underline: const SizedBox.shrink(), + onChanged: (value) { + if (value != null) { + settingsService.setBatteryChemistryForRepeater( + widget.repeater.publicKeyHex, + value, + ); + } + }, + items: [ + DropdownMenuItem( + value: 'nmc', + child: Text(l10n.appSettings_batteryNmc), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), + ), + ], + ), + ), + ], + ), + ); + } + Widget _buildRadioStatsCard() { final l10n = context.l10n; return Card( @@ -589,21 +698,13 @@ class _RepeaterStatusScreenState extends State { return double.tryParse(value.toString()); } - String _batteryText() { + String _batteryText(String chemistry) { if (_batteryMv == null) return '—'; - final percent = _batteryPercentFromMv(_batteryMv!); + final percent = estimateBatteryPercent(_batteryMv!, chemistry); final volts = (_batteryMv! / 1000.0).toStringAsFixed(2); return '$percent% / ${volts}V'; } - int _batteryPercentFromMv(int millivolts) { - const minMv = 3000; - const maxMv = 4200; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); - } - String _clockText() { if (_statusRequestedAt == null) return '—'; final dt = _statusRequestedAt!; diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index c1e8fc62..5e637d99 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -132,4 +132,21 @@ class AppSettingsService extends ChangeNotifier { _settings.copyWith(batteryChemistryByDeviceId: updated), ); } + + String batteryChemistryForRepeater(String publicKeyHex) { + return _settings.repeaterBatteryChemistryByPublicKey[publicKeyHex] ?? 'nmc'; + } + + Future setBatteryChemistryForRepeater( + String publicKeyHex, + String chemistry, + ) async { + final updated = Map.from( + _settings.repeaterBatteryChemistryByPublicKey, + ); + updated[publicKeyHex] = chemistry; + await updateSettings( + _settings.copyWith(repeaterBatteryChemistryByPublicKey: updated), + ); + } } diff --git a/lib/utils/battery_utils.dart b/lib/utils/battery_utils.dart new file mode 100644 index 00000000..05bdb0a1 --- /dev/null +++ b/lib/utils/battery_utils.dart @@ -0,0 +1,22 @@ +/// Returns the (minMv, maxMv) voltage range for the given battery chemistry. +(int, int) batteryVoltageRange(String chemistry) { + switch (chemistry) { + case 'lifepo4': + return (2600, 3650); + case 'lipo': + return (3000, 4200); + case 'nmc': + default: + return (3000, 4200); + } +} + +/// Estimates battery percentage from millivolts based on chemistry type. +int estimateBatteryPercent(int millivolts, String chemistry) { + final range = batteryVoltageRange(chemistry); + final minMv = range.$1; + final maxMv = range.$2; + if (millivolts <= minMv) return 0; + if (millivolts >= maxMv) return 100; + return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); +} diff --git a/untranslated.json b/untranslated.json index 9e26dfee..3d59b411 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1 +1,57 @@ -{} \ No newline at end of file +{ + "bg": [ + "repeater_batteryChemistry" + ], + + "de": [ + "repeater_batteryChemistry" + ], + + "es": [ + "repeater_batteryChemistry" + ], + + "fr": [ + "repeater_batteryChemistry" + ], + + "it": [ + "repeater_batteryChemistry" + ], + + "nl": [ + "repeater_batteryChemistry" + ], + + "pl": [ + "repeater_batteryChemistry" + ], + + "pt": [ + "repeater_batteryChemistry" + ], + + "ru": [ + "repeater_batteryChemistry" + ], + + "sk": [ + "repeater_batteryChemistry" + ], + + "sl": [ + "repeater_batteryChemistry" + ], + + "sv": [ + "repeater_batteryChemistry" + ], + + "uk": [ + "repeater_batteryChemistry" + ], + + "zh": [ + "repeater_batteryChemistry" + ] +} From 3713228ceded2394073c20c497e6e4e7ec148a86 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 7 Feb 2026 04:26:44 -0700 Subject: [PATCH 2/6] Add companion radio chemistry detection - Detect optional chemistry byte at offset 11 in BATT_AND_STORAGE response - Prefer firmware-reported chemistry over user setting - Use shared battery_utils.dart for percentage calculation - Log chemistry type when detected Extends battery chemistry feature to companion radio in addition to repeaters. --- lib/connector/meshcore_connector.dart | 43 +++++++++++++++------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 2c56c373..38514adc 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -15,6 +15,7 @@ import '../models/path_selection.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/smaz.dart'; import '../services/app_debug_log_service.dart'; +import '../utils/battery_utils.dart'; import '../services/ble_debug_log_service.dart'; import '../services/message_retry_service.dart'; import '../services/path_history_service.dart'; @@ -92,6 +93,7 @@ class MeshCoreConnector extends ChangeNotifier { int? _currentSf; int? _currentCr; int? _batteryMillivolts; + String? _reportedBatteryChemistry; // From firmware, if available double? _selfLatitude; double? _selfLongitude; bool _isLoadingContacts = false; @@ -219,30 +221,17 @@ class MeshCoreConnector extends ChangeNotifier { ); String _batteryChemistryForDevice() { + // Prefer firmware-reported chemistry if available + if (_reportedBatteryChemistry != null) return _reportedBatteryChemistry!; + // Fall back to user setting final deviceId = _device?.remoteId.toString(); if (deviceId == null || _appSettingsService == null) return 'nmc'; return _appSettingsService!.batteryChemistryForDevice(deviceId); } + // Uses shared utility from battery_utils.dart int _estimateBatteryPercent(int millivolts, String chemistry) { - final range = _batteryVoltageRange(chemistry); - final minMv = range.$1; - final maxMv = range.$2; - if (millivolts <= minMv) return 0; - if (millivolts >= maxMv) return 100; - return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); - } - - (int, int) _batteryVoltageRange(String chemistry) { - switch (chemistry) { - case 'lifepo4': - return (2600, 3650); - case 'lipo': - return (3000, 4200); - case 'nmc': - default: - return (3000, 4200); - } + return estimateBatteryPercent(millivolts, chemistry); } List getMessages(Contact contact) { @@ -923,6 +912,7 @@ class MeshCoreConnector extends ChangeNotifier { _selfLatitude = null; _selfLongitude = null; _batteryMillivolts = null; + _reportedBatteryChemistry = null; _batteryRequested = false; _awaitingSelfInfo = false; _maxContacts = _defaultMaxContacts; @@ -1892,11 +1882,26 @@ class MeshCoreConnector extends ChangeNotifier { // [1-2] = battery_mv (uint16 LE) // [3-6] = storage_used_kb (uint32 LE) // [7-10] = storage_total_kb (uint32 LE) + // [11] = battery_chemistry (optional, 0=NMC, 1=LiFePO4, 2=LiPo) if (frame.length >= 3) { _batteryMillivolts = readUint16LE(frame, 1); + + // Check for optional chemistry byte at offset 11 (protocol extension) + if (frame.length > 11) { + final chemByte = frame[11]; + _reportedBatteryChemistry = switch (chemByte) { + 0x01 => 'lifepo4', + 0x02 => 'lipo', + _ => 'nmc', + }; + } + final volts = (_batteryMillivolts! / 1000.0).toStringAsFixed(2); + final chemInfo = _reportedBatteryChemistry != null + ? ' [$_reportedBatteryChemistry]' + : ''; _appDebugLogService?.info( - 'Pulled battery: $volts V ($_batteryMillivolts mV)', + 'Pulled battery: $volts V ($_batteryMillivolts mV)$chemInfo', tag: 'Battery', ); notifyListeners(); From da778b95bb0bdd73997823e8a33ff3af59e8a43a Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 7 Feb 2026 04:34:48 -0700 Subject: [PATCH 3/6] Update battery chemistry protocol with new types - Rename NMC to LiPo as standard for dev boards - Add 'none' option for external power (wall-powered devices) - Add lead acid support (12V solar/off-grid setups) - Protocol: 0x00=none, 0x01=lipo, 0x02=lifepo4, 0x03=leadacid - Add migration for old stored values (nmc/liion -> lipo) - Make return type nullable for 'none' chemistry --- lib/connector/meshcore_connector.dart | 13 +++------ lib/l10n/app_en.arb | 7 ++--- lib/l10n/app_localizations.dart | 20 +++++++++----- lib/l10n/app_localizations_bg.dart | 7 +++-- lib/l10n/app_localizations_de.dart | 7 +++-- lib/l10n/app_localizations_en.dart | 9 ++++--- lib/l10n/app_localizations_es.dart | 7 +++-- lib/l10n/app_localizations_fr.dart | 7 +++-- lib/l10n/app_localizations_it.dart | 7 +++-- lib/l10n/app_localizations_nl.dart | 7 +++-- lib/l10n/app_localizations_pl.dart | 7 +++-- lib/l10n/app_localizations_pt.dart | 7 +++-- lib/l10n/app_localizations_ru.dart | 7 +++-- lib/l10n/app_localizations_sk.dart | 7 +++-- lib/l10n/app_localizations_sl.dart | 7 +++-- lib/l10n/app_localizations_sv.dart | 7 +++-- lib/l10n/app_localizations_uk.dart | 7 +++-- lib/l10n/app_localizations_zh.dart | 7 +++-- lib/screens/app_settings_screen.dart | 14 ++++++---- lib/screens/repeater_status_screen.dart | 28 ++++++++++--------- lib/services/app_settings_service.dart | 10 ++++--- lib/utils/battery_utils.dart | 36 ++++++++++++++++++++----- untranslated.json | 28 +++++++++++++++++++ 23 files changed, 187 insertions(+), 76 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 38514adc..7982170b 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -225,12 +225,12 @@ class MeshCoreConnector extends ChangeNotifier { if (_reportedBatteryChemistry != null) return _reportedBatteryChemistry!; // Fall back to user setting final deviceId = _device?.remoteId.toString(); - if (deviceId == null || _appSettingsService == null) return 'nmc'; + if (deviceId == null || _appSettingsService == null) return 'lipo'; return _appSettingsService!.batteryChemistryForDevice(deviceId); } // Uses shared utility from battery_utils.dart - int _estimateBatteryPercent(int millivolts, String chemistry) { + int? _estimateBatteryPercent(int millivolts, String chemistry) { return estimateBatteryPercent(millivolts, chemistry); } @@ -1882,18 +1882,13 @@ class MeshCoreConnector extends ChangeNotifier { // [1-2] = battery_mv (uint16 LE) // [3-6] = storage_used_kb (uint32 LE) // [7-10] = storage_total_kb (uint32 LE) - // [11] = battery_chemistry (optional, 0=NMC, 1=LiFePO4, 2=LiPo) + // [11] = battery_chemistry (optional: 0=none, 1=lipo, 2=lifepo4, 3=leadacid) if (frame.length >= 3) { _batteryMillivolts = readUint16LE(frame, 1); // Check for optional chemistry byte at offset 11 (protocol extension) if (frame.length > 11) { - final chemByte = frame[11]; - _reportedBatteryChemistry = switch (chemByte) { - 0x01 => 'lifepo4', - 0x02 => 'lipo', - _ => 'nmc', - }; + _reportedBatteryChemistry = chemistryFromByte(frame[11]); } final volts = (_batteryMillivolts! / 1000.0).toStringAsFixed(2); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 899a3963..e1c8c0c2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -206,9 +206,10 @@ } }, "appSettings_batteryChemistryConnectFirst": "Connect to a device to choose", - "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", - "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)", - "appSettings_batteryLipo": "LiPo (3.0-4.2V)", + "appSettings_batteryNone": "No Battery (External Power)", + "appSettings_batteryLipo": "LiPo (3.7V)", + "appSettings_batteryLifepo4": "LiFePO4 (3.2V)", + "appSettings_batteryLeadAcid": "Lead Acid (12V)", "appSettings_mapDisplay": "Map Display", "appSettings_showRepeaters": "Show Repeaters", "appSettings_showRepeatersSubtitle": "Display repeater nodes on the map", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 39f218b5..391869fa 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1108,23 +1108,29 @@ abstract class AppLocalizations { /// **'Connect to a device to choose'** String get appSettings_batteryChemistryConnectFirst; - /// No description provided for @appSettings_batteryNmc. + /// No description provided for @appSettings_batteryNone. /// /// In en, this message translates to: - /// **'18650 NMC (3.0-4.2V)'** - String get appSettings_batteryNmc; + /// **'No Battery (External Power)'** + String get appSettings_batteryNone; + + /// No description provided for @appSettings_batteryLipo. + /// + /// In en, this message translates to: + /// **'LiPo (3.7V)'** + String get appSettings_batteryLipo; /// No description provided for @appSettings_batteryLifepo4. /// /// In en, this message translates to: - /// **'LiFePO4 (2.6-3.65V)'** + /// **'LiFePO4 (3.2V)'** String get appSettings_batteryLifepo4; - /// No description provided for @appSettings_batteryLipo. + /// No description provided for @appSettings_batteryLeadAcid. /// /// In en, this message translates to: - /// **'LiPo (3.0-4.2V)'** - String get appSettings_batteryLipo; + /// **'Lead Acid (12V)'** + String get appSettings_batteryLeadAcid; /// No description provided for @appSettings_mapDisplay. /// diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index e9ac99f1..c421bffa 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -548,13 +548,16 @@ class AppLocalizationsBg extends AppLocalizations { 'Свържете се с устройство, за да изберете.'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)'; @override String get appSettings_batteryLifepo4 => 'Литиево желязо фосфат (2.6-3.65V)'; @override - String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Карта за показване'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 84852c7c..2cfa1be0 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -545,13 +545,16 @@ class AppLocalizationsDe extends AppLocalizations { 'Verbinde ein Gerät, um zu wählen'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0–4,2 V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Kartendarstellung'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ff102ff4..a32cd448 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -540,13 +540,16 @@ class AppLocalizationsEn extends AppLocalizations { 'Connect to a device to choose'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; @override - String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65V)'; + String get appSettings_batteryLipo => 'LiPo (3.7V)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; + String get appSettings_batteryLifepo4 => 'LiFePO4 (3.2V)'; + + @override + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Map Display'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 4a55f4bd..ae08e195 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -546,13 +546,16 @@ class AppLocalizationsEs extends AppLocalizations { 'Conéctate a un dispositivo para elegir'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Visualización del Mapa'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 4bf92558..dbbd52dc 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -548,13 +548,16 @@ class AppLocalizationsFr extends AppLocalizations { 'Connectez un appareil pour choisir'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6-3,65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Affichage de la carte'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 0a0f3058..3cab469f 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -545,13 +545,16 @@ class AppLocalizationsIt extends AppLocalizations { 'Connetti a un dispositivo per scegliere'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6-3,65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Visualizzazione Mappa'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3e741935..71a0398c 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -543,13 +543,16 @@ class AppLocalizationsNl extends AppLocalizations { 'Verbind met een apparaat om te selecteren'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6-3,65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Kaartweergave'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2f0a39fd..2eaeac13 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -548,13 +548,16 @@ class AppLocalizationsPl extends AppLocalizations { 'Połącz się z urządzeniem, aby wybrać'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6-3,65 V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Wyświetlanie mapy'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 74816b03..34982b07 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -547,13 +547,16 @@ class AppLocalizationsPt extends AppLocalizations { 'Conecte-se a um dispositivo para escolher'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6-3,65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Exibição do Mapa'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 11f2fdb3..13cce28d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -546,13 +546,16 @@ class AppLocalizationsRu extends AppLocalizations { 'Подключитесь к устройству, чтобы выбрать'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0–4.2 В)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6–3.65 В)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Отображение карты'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 0b9fecd7..62eac575 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -540,13 +540,16 @@ class AppLocalizationsSk extends AppLocalizations { 'Pripojte sa k zariadeniu na výber'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Zobrazenie mapy'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 9bcd74e8..06d4b436 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -542,13 +542,16 @@ class AppLocalizationsSl extends AppLocalizations { 'Za izbiro se poveži z napravo'; @override - String get appSettings_batteryNmc => '18650 NMC (3,0-4,2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)'; @override - String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Prikaz zemljevida'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 894e8ee3..9d1bcd45 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -537,13 +537,16 @@ class AppLocalizationsSv extends AppLocalizations { 'Anslut till en enhet för att välja'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Kartvisning'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 78ad27f3..2a544a63 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -544,13 +544,16 @@ class AppLocalizationsUk extends AppLocalizations { 'Підключіть пристрій, щоб вибрати'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2В)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)'; @override String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65В)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => 'Відображення карти'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 32f91e46..d23a539b 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -517,13 +517,16 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_batteryChemistryConnectFirst => '连接到设备以进行选择'; @override - String get appSettings_batteryNmc => '18650 型号,NMC 电池(3.0-4.2V)'; + String get appSettings_batteryNone => 'No Battery (External Power)'; + + @override + String get appSettings_batteryLipo => '锂离子电池 (3.0-4.2V)'; @override String get appSettings_batteryLifepo4 => '磷酸铁锂 (2.6-3.65V)'; @override - String get appSettings_batteryLipo => '锂离子电池 (3.0-4.2V)'; + String get appSettings_batteryLeadAcid => 'Lead Acid (12V)'; @override String get appSettings_mapDisplay => '地图展示'; diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 135babd4..160a0364 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -393,7 +393,7 @@ class AppSettingsScreen extends StatelessWidget { final isConnected = connector.isConnected && deviceId != null; final selection = isConnected ? settingsService.batteryChemistryForDevice(deviceId) - : 'nmc'; + : 'lipo'; return Card( child: Column( @@ -430,16 +430,20 @@ class AppSettingsScreen extends StatelessWidget { : null, items: [ DropdownMenuItem( - value: 'nmc', - child: Text(context.l10n.appSettings_batteryNmc), + value: 'none', + child: Text(context.l10n.appSettings_batteryNone), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(context.l10n.appSettings_batteryLipo), ), DropdownMenuItem( value: 'lifepo4', child: Text(context.l10n.appSettings_batteryLifepo4), ), DropdownMenuItem( - value: 'lipo', - child: Text(context.l10n.appSettings_batteryLipo), + value: 'leadacid', + child: Text(context.l10n.appSettings_batteryLeadAcid), ), ], ), diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index bc4269a4..902ce853 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -161,14 +161,10 @@ class _RepeaterStatusScreenState extends State { final rxAirSecs = data.getUint32(offset, Endian.little); // Check for optional chemistry byte at offset 60 (protocol extension) + // 0=none, 1=lipo, 2=lifepo4, 3=leadacid String? reportedChem; if (frame.length > _statusResponseBytes) { - final chemByte = frame[_statusResponseBytes]; - reportedChem = switch (chemByte) { - 0x01 => 'lifepo4', - 0x02 => 'lipo', - _ => 'nmc', - }; + reportedChem = chemistryFromByte(frame[_statusResponseBytes]); } _statusTimeout?.cancel(); @@ -509,9 +505,11 @@ class _RepeaterStatusScreenState extends State { // Map chemistry code to display name String chemistryDisplayName(String chem) { return switch (chem) { - 'lifepo4' => l10n.appSettings_batteryLifepo4, + 'none' => l10n.appSettings_batteryNone, 'lipo' => l10n.appSettings_batteryLipo, - _ => l10n.appSettings_batteryNmc, + 'lifepo4' => l10n.appSettings_batteryLifepo4, + 'leadacid' => l10n.appSettings_batteryLeadAcid, + _ => l10n.appSettings_batteryLipo, }; } @@ -562,16 +560,20 @@ class _RepeaterStatusScreenState extends State { }, items: [ DropdownMenuItem( - value: 'nmc', - child: Text(l10n.appSettings_batteryNmc), + value: 'none', + child: Text(l10n.appSettings_batteryNone), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), ), DropdownMenuItem( value: 'lifepo4', child: Text(l10n.appSettings_batteryLifepo4), ), DropdownMenuItem( - value: 'lipo', - child: Text(l10n.appSettings_batteryLipo), + value: 'leadacid', + child: Text(l10n.appSettings_batteryLeadAcid), ), ], ), @@ -700,7 +702,9 @@ class _RepeaterStatusScreenState extends State { String _batteryText(String chemistry) { if (_batteryMv == null) return '—'; + if (chemistry == 'none') return 'N/A (external power)'; final percent = estimateBatteryPercent(_batteryMv!, chemistry); + if (percent == null) return '—'; final volts = (_batteryMv! / 1000.0).toStringAsFixed(2); return '$percent% / ${volts}V'; } diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 5e637d99..ca408c3f 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -13,8 +13,9 @@ class AppSettingsService extends ChangeNotifier { String batteryChemistryForDevice(String deviceId) { final stored = _settings.batteryChemistryByDeviceId[deviceId]; - if (stored == 'liion') return 'nmc'; - return stored ?? 'nmc'; + // Migrate old values to new protocol + if (stored == 'liion' || stored == 'nmc') return 'lipo'; + return stored ?? 'lipo'; } Future loadSettings() async { @@ -134,7 +135,10 @@ class AppSettingsService extends ChangeNotifier { } String batteryChemistryForRepeater(String publicKeyHex) { - return _settings.repeaterBatteryChemistryByPublicKey[publicKeyHex] ?? 'nmc'; + final stored = _settings.repeaterBatteryChemistryByPublicKey[publicKeyHex]; + // Migrate old values to new protocol + if (stored == 'nmc') return 'lipo'; + return stored ?? 'lipo'; } Future setBatteryChemistryForRepeater( diff --git a/lib/utils/battery_utils.dart b/lib/utils/battery_utils.dart index 05bdb0a1..6f69beed 100644 --- a/lib/utils/battery_utils.dart +++ b/lib/utils/battery_utils.dart @@ -1,22 +1,46 @@ +// Battery chemistry protocol values: +// 0x00 = none (no battery / external power) +// 0x01 = lipo (3.7V lithium polymer) +// 0x02 = lifepo4 (3.2V lithium iron phosphate) +// 0x03 = leadacid (12V lead acid) + /// Returns the (minMv, maxMv) voltage range for the given battery chemistry. -(int, int) batteryVoltageRange(String chemistry) { +/// Returns null for 'none' (no battery). +(int, int)? batteryVoltageRange(String chemistry) { switch (chemistry) { - case 'lifepo4': - return (2600, 3650); + case 'none': + return null; // No battery case 'lipo': return (3000, 4200); - case 'nmc': + case 'lifepo4': + return (2600, 3650); + case 'leadacid': + return (10500, 12700); // 12V lead acid default: - return (3000, 4200); + return (3000, 4200); // Default to lipo curve } } /// Estimates battery percentage from millivolts based on chemistry type. -int estimateBatteryPercent(int millivolts, String chemistry) { +/// Returns null for 'none' (no battery). +int? estimateBatteryPercent(int millivolts, String chemistry) { + if (chemistry == 'none') return null; final range = batteryVoltageRange(chemistry); + if (range == null) return null; final minMv = range.$1; final maxMv = range.$2; if (millivolts <= minMv) return 0; if (millivolts >= maxMv) return 100; return (((millivolts - minMv) * 100) / (maxMv - minMv)).round(); } + +/// Converts chemistry byte from protocol to string identifier. +String chemistryFromByte(int byte) { + return switch (byte) { + 0x00 => 'none', + 0x01 => 'lipo', + 0x02 => 'lifepo4', + 0x03 => 'leadacid', + _ => 'lipo', // Default fallback + }; +} diff --git a/untranslated.json b/untranslated.json index 3d59b411..448ba8a9 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,57 +1,85 @@ { "bg": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "de": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "es": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "fr": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "it": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "nl": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "pl": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "pt": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "ru": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "sk": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "sl": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "sv": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "uk": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "zh": [ + "appSettings_batteryNone", + "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ] } From 6e3ccc100d648cc844e364b10c328004fcd0eaa2 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 7 Feb 2026 05:14:05 -0700 Subject: [PATCH 4/6] Fix battery type UI for 'none' chemistry - Hide battery type row when chemistry is 'none' (redundant with N/A) - Fix text overflow with Flexible + ellipsis - Shorten label to "No Battery" for cleaner display --- lib/l10n/app_en.arb | 2 +- lib/l10n/app_localizations.dart | 2 +- lib/l10n/app_localizations_bg.dart | 2 +- lib/l10n/app_localizations_de.dart | 2 +- lib/l10n/app_localizations_en.dart | 2 +- lib/l10n/app_localizations_es.dart | 2 +- lib/l10n/app_localizations_fr.dart | 2 +- lib/l10n/app_localizations_it.dart | 2 +- lib/l10n/app_localizations_nl.dart | 2 +- lib/l10n/app_localizations_pl.dart | 2 +- lib/l10n/app_localizations_pt.dart | 2 +- lib/l10n/app_localizations_ru.dart | 2 +- lib/l10n/app_localizations_sk.dart | 2 +- lib/l10n/app_localizations_sl.dart | 2 +- lib/l10n/app_localizations_sv.dart | 2 +- lib/l10n/app_localizations_uk.dart | 2 +- lib/l10n/app_localizations_zh.dart | 2 +- lib/screens/repeater_status_screen.dart | 21 +++++++++++++-------- 18 files changed, 30 insertions(+), 25 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e1c8c0c2..0e767ad7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -206,7 +206,7 @@ } }, "appSettings_batteryChemistryConnectFirst": "Connect to a device to choose", - "appSettings_batteryNone": "No Battery (External Power)", + "appSettings_batteryNone": "No Battery", "appSettings_batteryLipo": "LiPo (3.7V)", "appSettings_batteryLifepo4": "LiFePO4 (3.2V)", "appSettings_batteryLeadAcid": "Lead Acid (12V)", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 391869fa..cb1df2fb 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1111,7 +1111,7 @@ abstract class AppLocalizations { /// No description provided for @appSettings_batteryNone. /// /// In en, this message translates to: - /// **'No Battery (External Power)'** + /// **'No Battery'** String get appSettings_batteryNone; /// No description provided for @appSettings_batteryLipo. diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index c421bffa..585fb94d 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -548,7 +548,7 @@ class AppLocalizationsBg extends AppLocalizations { 'Свържете се с устройство, за да изберете.'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2cfa1be0..cf14cec1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -545,7 +545,7 @@ class AppLocalizationsDe extends AppLocalizations { 'Verbinde ein Gerät, um zu wählen'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a32cd448..8beffea5 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -540,7 +540,7 @@ class AppLocalizationsEn extends AppLocalizations { 'Connect to a device to choose'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3.7V)'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index ae08e195..b03ee95e 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -546,7 +546,7 @@ class AppLocalizationsEs extends AppLocalizations { 'Conéctate a un dispositivo para elegir'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index dbbd52dc..73c82a68 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -548,7 +548,7 @@ class AppLocalizationsFr extends AppLocalizations { 'Connectez un appareil pour choisir'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 3cab469f..75c9821d 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -545,7 +545,7 @@ class AppLocalizationsIt extends AppLocalizations { 'Connetti a un dispositivo per scegliere'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 71a0398c..04b4cf21 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -543,7 +543,7 @@ class AppLocalizationsNl extends AppLocalizations { 'Verbind met een apparaat om te selecteren'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 2eaeac13..69850b02 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -548,7 +548,7 @@ class AppLocalizationsPl extends AppLocalizations { 'Połącz się z urządzeniem, aby wybrać'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 34982b07..7ee5121d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -547,7 +547,7 @@ class AppLocalizationsPt extends AppLocalizations { 'Conecte-se a um dispositivo para escolher'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 13cce28d..9b974c9d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -546,7 +546,7 @@ class AppLocalizationsRu extends AppLocalizations { 'Подключитесь к устройству, чтобы выбрать'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 62eac575..bfeb7d53 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -540,7 +540,7 @@ class AppLocalizationsSk extends AppLocalizations { 'Pripojte sa k zariadeniu na výber'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 06d4b436..b2ca4f1d 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -542,7 +542,7 @@ class AppLocalizationsSl extends AppLocalizations { 'Za izbiro se poveži z napravo'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 9d1bcd45..b0472692 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -537,7 +537,7 @@ class AppLocalizationsSv extends AppLocalizations { 'Anslut till en enhet för att välja'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 2a544a63..6346d7bc 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -544,7 +544,7 @@ class AppLocalizationsUk extends AppLocalizations { 'Підключіть пристрій, щоб вибрати'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index d23a539b..7f51ec0c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -517,7 +517,7 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_batteryChemistryConnectFirst => '连接到设备以进行选择'; @override - String get appSettings_batteryNone => 'No Battery (External Power)'; + String get appSettings_batteryNone => 'No Battery'; @override String get appSettings_batteryLipo => '锂离子电池 (3.0-4.2V)'; diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 902ce853..0d12fd5e 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -480,11 +480,13 @@ class _RepeaterStatusScreenState extends State { ), const Divider(), _buildInfoRow(l10n.repeater_battery, _batteryText(chemistry)), - _buildBatteryChemistryRow( - settingsService, - chemistry, - isFromFirmware: _reportedChemistry != null, - ), + // Only show chemistry row if there's a battery to configure + if (chemistry != 'none') + _buildBatteryChemistryRow( + settingsService, + chemistry, + isFromFirmware: _reportedChemistry != null, + ), _buildInfoRow(l10n.repeater_clockAtLogin, _clockText()), _buildInfoRow(l10n.repeater_uptime, _formatDuration(_uptimeSecs)), _buildInfoRow(l10n.repeater_queueLength, _formatValue(_queueLen)), @@ -533,9 +535,12 @@ class _RepeaterStatusScreenState extends State { // Firmware-reported: show as read-only text with indicator ? Row( children: [ - Text( - chemistryDisplayName(currentChemistry), - style: const TextStyle(fontWeight: FontWeight.w400), + Flexible( + child: Text( + chemistryDisplayName(currentChemistry), + style: const TextStyle(fontWeight: FontWeight.w400), + overflow: TextOverflow.ellipsis, + ), ), const SizedBox(width: 8), Icon( From bb9b3c308959c9bb23bad1c8994cf996c770fc20 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 7 Feb 2026 06:12:21 -0700 Subject: [PATCH 5/6] Add Battery Settings to Node Settings section - Add Battery Settings dialog showing current voltage/percentage - User chemistry selection now overrides firmware default - Show "Device default: X" hint only for meaningful firmware values - Fix battery percent calculation to use user setting as primary - Shorten label from "Battery Chemistry" to "Type" --- lib/connector/meshcore_connector.dart | 15 ++-- lib/l10n/app_en.arb | 9 ++ lib/l10n/app_localizations.dart | 24 ++++++ lib/l10n/app_localizations_bg.dart | 14 +++ lib/l10n/app_localizations_de.dart | 14 +++ lib/l10n/app_localizations_en.dart | 14 +++ lib/l10n/app_localizations_es.dart | 14 +++ lib/l10n/app_localizations_fr.dart | 14 +++ lib/l10n/app_localizations_it.dart | 14 +++ lib/l10n/app_localizations_nl.dart | 14 +++ lib/l10n/app_localizations_pl.dart | 14 +++ lib/l10n/app_localizations_pt.dart | 14 +++ lib/l10n/app_localizations_ru.dart | 14 +++ lib/l10n/app_localizations_sk.dart | 14 +++ lib/l10n/app_localizations_sl.dart | 14 +++ lib/l10n/app_localizations_sv.dart | 14 +++ lib/l10n/app_localizations_uk.dart | 14 +++ lib/l10n/app_localizations_zh.dart | 14 +++ lib/screens/app_settings_screen.dart | 99 ++++++++++++++-------- lib/screens/settings_screen.dart | 117 ++++++++++++++++++++++++++ untranslated.json | 56 ++++++++++++ 21 files changed, 492 insertions(+), 38 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 7982170b..9fa0032f 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -205,6 +205,7 @@ class MeshCoreConnector extends ChangeNotifier { int? get currentCr => _currentCr; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; + String? get reportedBatteryChemistry => _reportedBatteryChemistry; int get maxContacts => _maxContacts; int get maxChannels => _maxChannels; bool get isSyncingQueuedMessages => _isSyncingQueuedMessages; @@ -221,12 +222,16 @@ class MeshCoreConnector extends ChangeNotifier { ); String _batteryChemistryForDevice() { - // Prefer firmware-reported chemistry if available - if (_reportedBatteryChemistry != null) return _reportedBatteryChemistry!; - // Fall back to user setting + // User setting is the source of truth (they can override firmware) final deviceId = _device?.remoteId.toString(); - if (deviceId == null || _appSettingsService == null) return 'lipo'; - return _appSettingsService!.batteryChemistryForDevice(deviceId); + if (deviceId != null && _appSettingsService != null) { + return _appSettingsService!.batteryChemistryForDevice(deviceId); + } + // Fall back to firmware if available and meaningful (not 'none') + if (_reportedBatteryChemistry != null && _reportedBatteryChemistry != 'none') { + return _reportedBatteryChemistry!; + } + return 'lipo'; } // Uses shared utility from battery_utils.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0e767ad7..235c4960 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -82,6 +82,15 @@ "settings_radioSettings": "Radio Settings", "settings_radioSettingsSubtitle": "Frequency, power, spreading factor", "settings_radioSettingsUpdated": "Radio settings updated", + "settings_batterySettings": "Battery Settings", + "settings_batterySettingsSubtitle": "Battery chemistry type", + "settings_batteryType": "Type", + "settings_batteryFirmwareDefault": "Device default: {chemistry}", + "@settings_batteryFirmwareDefault": { + "placeholders": { + "chemistry": {"type": "String"} + } + }, "settings_location": "Location", "settings_locationSubtitle": "GPS coordinates", "settings_locationUpdated": "Location and GPS settings updated", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cb1df2fb..74530d35 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -460,6 +460,30 @@ abstract class AppLocalizations { /// **'Radio settings updated'** String get settings_radioSettingsUpdated; + /// No description provided for @settings_batterySettings. + /// + /// In en, this message translates to: + /// **'Battery Settings'** + String get settings_batterySettings; + + /// No description provided for @settings_batterySettingsSubtitle. + /// + /// In en, this message translates to: + /// **'Battery chemistry type'** + String get settings_batterySettingsSubtitle; + + /// No description provided for @settings_batteryType. + /// + /// In en, this message translates to: + /// **'Type'** + String get settings_batteryType; + + /// No description provided for @settings_batteryFirmwareDefault. + /// + /// In en, this message translates to: + /// **'Device default: {chemistry}'** + String settings_batteryFirmwareDefault(String chemistry); + /// No description provided for @settings_location. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 585fb94d..b167f816 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -188,6 +188,20 @@ class AppLocalizationsBg extends AppLocalizations { String get settings_radioSettingsUpdated => 'Радио настройките са актуализирани'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Местоположение'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index cf14cec1..70ad67d9 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -187,6 +187,20 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Ort'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 8beffea5..0c1aa189 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -186,6 +186,20 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Radio settings updated'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Location'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index b03ee95e..bb6fb26b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -187,6 +187,20 @@ class AppLocalizationsEs extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Ubicación'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 73c82a68..582629d5 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -187,6 +187,20 @@ class AppLocalizationsFr extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Emplacement'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 75c9821d..f044a5d9 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -187,6 +187,20 @@ class AppLocalizationsIt extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Posizione'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 04b4cf21..165dc41c 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -186,6 +186,20 @@ class AppLocalizationsNl extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Radio instellingen bijgewerkt'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Locatie'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 69850b02..c5a18a23 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -188,6 +188,20 @@ class AppLocalizationsPl extends AppLocalizations { String get settings_radioSettingsUpdated => 'Ustawienia radia zostały zaktualizowane'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Lokalizacja'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 7ee5121d..5692803a 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -188,6 +188,20 @@ class AppLocalizationsPt extends AppLocalizations { String get settings_radioSettingsUpdated => 'Configurações de rádio atualizadas'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Localização'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9b974c9d..cfbd681c 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -186,6 +186,20 @@ class AppLocalizationsRu extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Позиция'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index bfeb7d53..2fa91e66 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -187,6 +187,20 @@ class AppLocalizationsSk extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Lokalita'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index b2ca4f1d..8bb10473 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -187,6 +187,20 @@ class AppLocalizationsSl extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Lokacija'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index b0472692..f181a1ef 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -187,6 +187,20 @@ class AppLocalizationsSv extends AppLocalizations { String get settings_radioSettingsUpdated => 'Radioinställningarna har uppdaterats'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Plats'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 6346d7bc..34774611 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -187,6 +187,20 @@ class AppLocalizationsUk extends AppLocalizations { @override String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => 'Розташування'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 7f51ec0c..93f928b1 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -184,6 +184,20 @@ class AppLocalizationsZh extends AppLocalizations { @override String get settings_radioSettingsUpdated => '收音机设置已更新'; + @override + String get settings_batterySettings => 'Battery Settings'; + + @override + String get settings_batterySettingsSubtitle => 'Battery chemistry type'; + + @override + String get settings_batteryType => 'Type'; + + @override + String settings_batteryFirmwareDefault(String chemistry) { + return 'Device default: $chemistry'; + } + @override String get settings_location => '地点'; diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 160a0364..ddddad66 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -391,10 +391,28 @@ class AppSettingsScreen extends StatelessWidget { ) { final deviceId = connector.deviceId; final isConnected = connector.isConnected && deviceId != null; - final selection = isConnected - ? settingsService.batteryChemistryForDevice(deviceId) + final isFromFirmware = connector.reportedBatteryChemistry != null; + final chemistry = isConnected + ? (isFromFirmware + ? connector.reportedBatteryChemistry! + : settingsService.batteryChemistryForDevice(deviceId)) : 'lipo'; + // Don't show battery card if firmware reports no battery + if (isFromFirmware && chemistry == 'none') { + return const SizedBox.shrink(); + } + + String chemistryDisplayName(String chem) { + return switch (chem) { + 'none' => context.l10n.appSettings_batteryNone, + 'lipo' => context.l10n.appSettings_batteryLipo, + 'lifepo4' => context.l10n.appSettings_batteryLifepo4, + 'leadacid' => context.l10n.appSettings_batteryLeadAcid, + _ => context.l10n.appSettings_batteryLipo, + }; + } + return Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -416,37 +434,52 @@ class AppSettingsScreen extends StatelessWidget { ) : context.l10n.appSettings_batteryChemistryConnectFirst, ), - trailing: DropdownButton( - value: selection, - onChanged: isConnected - ? (value) { - if (value != null) { - settingsService.setBatteryChemistryForDevice( - deviceId, - value, - ); - } - } - : null, - items: [ - DropdownMenuItem( - value: 'none', - child: Text(context.l10n.appSettings_batteryNone), - ), - DropdownMenuItem( - value: 'lipo', - child: Text(context.l10n.appSettings_batteryLipo), - ), - DropdownMenuItem( - value: 'lifepo4', - child: Text(context.l10n.appSettings_batteryLifepo4), - ), - DropdownMenuItem( - value: 'leadacid', - child: Text(context.l10n.appSettings_batteryLeadAcid), - ), - ], - ), + trailing: isFromFirmware + // Firmware-reported: show as read-only text with lock icon + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(chemistryDisplayName(chemistry)), + const SizedBox(width: 8), + Icon( + Icons.lock_outline, + size: 16, + color: Colors.grey[500], + ), + ], + ) + // User-configurable: show dropdown + : DropdownButton( + value: chemistry, + onChanged: isConnected + ? (value) { + if (value != null) { + settingsService.setBatteryChemistryForDevice( + deviceId, + value, + ); + } + } + : null, + items: [ + DropdownMenuItem( + value: 'none', + child: Text(context.l10n.appSettings_batteryNone), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(context.l10n.appSettings_batteryLipo), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(context.l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'leadacid', + child: Text(context.l10n.appSettings_batteryLeadAcid), + ), + ], + ), ), ], ), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 415d5081..bc83741f 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -5,8 +5,10 @@ import 'package:package_info_plus/package_info_plus.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../l10n/app_localizations.dart'; import '../l10n/l10n.dart'; import '../models/radio_settings.dart'; +import '../services/app_settings_service.dart'; import 'app_settings_screen.dart'; import 'app_debug_log_screen.dart'; import 'ble_debug_log_screen.dart'; @@ -225,6 +227,14 @@ class _SettingsScreenState extends State { onTap: () => _editLocation(context, connector), ), const Divider(height: 1), + ListTile( + leading: const Icon(Icons.battery_full), + title: Text(l10n.settings_batterySettings), + subtitle: Text(l10n.settings_batterySettingsSubtitle), + trailing: const Icon(Icons.chevron_right), + onTap: () => _showBatterySettings(context, connector), + ), + const Divider(height: 1), ListTile( leading: const Icon(Icons.visibility_off_outlined), title: Text(l10n.settings_privacyMode), @@ -428,6 +438,113 @@ class _SettingsScreenState extends State { ); } + void _showBatterySettings(BuildContext context, MeshCoreConnector connector) { + final l10n = context.l10n; + final settingsService = Provider.of(context, listen: false); + final deviceId = connector.deviceId; + final firmwareChemistry = connector.reportedBatteryChemistry; + // User setting takes precedence, fall back to firmware, then default + final chemistry = deviceId != null + ? settingsService.batteryChemistryForDevice(deviceId) + : (firmwareChemistry ?? 'lipo'); + + final millivolts = connector.batteryMillivolts; + final percent = connector.batteryPercent; + + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.settings_batterySettings), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (millivolts != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '${(millivolts / 1000.0).toStringAsFixed(2)}V', + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + if (percent != null) ...[ + const SizedBox(width: 12), + Text( + '($percent%)', + style: TextStyle(fontSize: 18, color: Colors.grey[400]), + ), + ], + ], + ), + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: Text(l10n.settings_batteryType), + trailing: DropdownButton( + value: chemistry, + onChanged: deviceId != null + ? (value) { + if (value != null) { + settingsService.setBatteryChemistryForDevice(deviceId, value); + Navigator.pop(dialogContext); + // Reopen to show updated value + _showBatterySettings(context, connector); + } + } + : null, + items: [ + DropdownMenuItem( + value: 'none', + child: Text(l10n.appSettings_batteryNone), + ), + DropdownMenuItem( + value: 'lipo', + child: Text(l10n.appSettings_batteryLipo), + ), + DropdownMenuItem( + value: 'lifepo4', + child: Text(l10n.appSettings_batteryLifepo4), + ), + DropdownMenuItem( + value: 'leadacid', + child: Text(l10n.appSettings_batteryLeadAcid), + ), + ], + ), + ), + if (firmwareChemistry != null && firmwareChemistry != 'none') + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + l10n.settings_batteryFirmwareDefault( + _chemistryDisplayName(l10n, firmwareChemistry), + ), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(l10n.common_close), + ), + ], + ), + ); + } + + String _chemistryDisplayName(AppLocalizations l10n, String chem) { + return switch (chem) { + 'none' => l10n.appSettings_batteryNone, + 'lipo' => l10n.appSettings_batteryLipo, + 'lifepo4' => l10n.appSettings_batteryLifepo4, + 'leadacid' => l10n.appSettings_batteryLeadAcid, + _ => l10n.appSettings_batteryLipo, + }; + } + void _editLocation(BuildContext context, MeshCoreConnector connector) { final l10n = context.l10n; final latController = TextEditingController(); diff --git a/untranslated.json b/untranslated.json index 448ba8a9..92070d89 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,83 +1,139 @@ { "bg": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "de": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "es": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "fr": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "it": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "nl": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "pl": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "pt": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "ru": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "sk": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "sl": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "sv": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "uk": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" ], "zh": [ + "settings_batterySettings", + "settings_batterySettingsSubtitle", + "settings_batteryType", + "settings_batteryFirmwareDefault", "appSettings_batteryNone", "appSettings_batteryLeadAcid", "repeater_batteryChemistry" From d663d39725ebbff1e7b731f02461aec4797ceed1 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 7 Feb 2026 15:40:36 -0700 Subject: [PATCH 6/6] Add power state monitoring to repeater status screen Parse and display power source information from protocol extension bytes: - USB connection status (blue icon) - Solar connection status (orange icon) - Charging indicator (green icon) - Input voltage display Protocol bytes: frame[61]=power_state flags, frame[62-63]=input_mv (LE uint16) Backward compatible: row hidden when firmware doesn't send extension bytes. --- lib/l10n/app_bg.arb | 7 ++ lib/l10n/app_de.arb | 7 ++ lib/l10n/app_en.arb | 6 ++ lib/l10n/app_es.arb | 7 ++ lib/l10n/app_fr.arb | 7 ++ lib/l10n/app_it.arb | 7 ++ lib/l10n/app_localizations.dart | 36 ++++++++++ lib/l10n/app_localizations_bg.dart | 20 +++++- lib/l10n/app_localizations_de.dart | 20 +++++- lib/l10n/app_localizations_en.dart | 18 +++++ lib/l10n/app_localizations_es.dart | 20 +++++- lib/l10n/app_localizations_fr.dart | 20 +++++- lib/l10n/app_localizations_it.dart | 20 +++++- lib/l10n/app_localizations_nl.dart | 20 +++++- lib/l10n/app_localizations_pl.dart | 20 +++++- lib/l10n/app_localizations_pt.dart | 20 +++++- lib/l10n/app_localizations_ru.dart | 20 +++++- lib/l10n/app_localizations_sk.dart | 20 +++++- lib/l10n/app_localizations_sl.dart | 20 +++++- lib/l10n/app_localizations_sv.dart | 20 +++++- lib/l10n/app_localizations_uk.dart | 20 +++++- lib/l10n/app_localizations_zh.dart | 20 +++++- lib/l10n/app_nl.arb | 7 ++ lib/l10n/app_pl.arb | 7 ++ lib/l10n/app_pt.arb | 7 ++ lib/l10n/app_ru.arb | 7 ++ lib/l10n/app_sk.arb | 7 ++ lib/l10n/app_sl.arb | 7 ++ lib/l10n/app_sv.arb | 7 ++ lib/l10n/app_uk.arb | 7 ++ lib/l10n/app_zh.arb | 7 ++ lib/screens/repeater_status_screen.dart | 92 +++++++++++++++++++++++++ lib/utils/battery_utils.dart | 26 +++++++ 33 files changed, 542 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 460bc9d1..b707a438 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Информация за системата", "repeater_battery": "Батерия", + "repeater_batteryChemistry": "Тип батерия", + "repeater_powerSource": "Източник на захранване", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Слънчева", + "repeater_powerCharging": "Зареждане", + "repeater_powerBatteryOnly": "Само батерия", + "repeater_powerNone": "Няма захранване", "repeater_clockAtLogin": "Часовник (при влизане)", "repeater_uptime": "Наличност", "repeater_queueLength": "Дължина на опашката", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 0a72559c..4ff35a9e 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Systeminformation", "repeater_battery": "Akku", + "repeater_batteryChemistry": "Akkutyp", + "repeater_powerSource": "Stromquelle", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solar", + "repeater_powerCharging": "Lädt", + "repeater_powerBatteryOnly": "Nur Akku", + "repeater_powerNone": "Keine Stromquelle", "repeater_clockAtLogin": "Uhr (bei Anmeldung)", "repeater_uptime": "Verfügbarkeit", "repeater_queueLength": "Warteschlangenlänge", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 235c4960..e36d239b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -817,6 +817,12 @@ "repeater_systemInformation": "System Information", "repeater_battery": "Battery", "repeater_batteryChemistry": "Battery Type", + "repeater_powerSource": "Power Source", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solar", + "repeater_powerCharging": "Charging", + "repeater_powerBatteryOnly": "Battery only", + "repeater_powerNone": "No power source", "repeater_clockAtLogin": "Clock (at login)", "repeater_uptime": "Uptime", "repeater_queueLength": "Queue Length", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index c6dad1ff..5e44d2bf 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Información del sistema", "repeater_battery": "Batería", + "repeater_batteryChemistry": "Tipo de batería", + "repeater_powerSource": "Fuente de alimentación", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solar", + "repeater_powerCharging": "Cargando", + "repeater_powerBatteryOnly": "Solo batería", + "repeater_powerNone": "Sin fuente de alimentación", "repeater_clockAtLogin": "Reloj (al inicio de sesión)", "repeater_uptime": "Tiempo de actividad", "repeater_queueLength": "Longitud de la cola", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index c1157ed3..f42db7d4 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Informations Système", "repeater_battery": "Batterie", + "repeater_batteryChemistry": "Type de batterie", + "repeater_powerSource": "Source d'alimentation", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solaire", + "repeater_powerCharging": "En charge", + "repeater_powerBatteryOnly": "Batterie seule", + "repeater_powerNone": "Aucune source", "repeater_clockAtLogin": "Horloge (au démarrage)", "repeater_uptime": "Disponibilité", "repeater_queueLength": "Longueur de la file d'attente", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index c32e8638..23ac734a 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Informazioni di sistema", "repeater_battery": "Batteria", + "repeater_batteryChemistry": "Tipo di batteria", + "repeater_powerSource": "Fonte di alimentazione", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solare", + "repeater_powerCharging": "In carica", + "repeater_powerBatteryOnly": "Solo batteria", + "repeater_powerNone": "Nessuna alimentazione", "repeater_clockAtLogin": "Orologio (all'accesso)", "repeater_uptime": "Disponibilità", "repeater_queueLength": "Lunghezza della coda", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 74530d35..da98dc18 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -3117,6 +3117,42 @@ abstract class AppLocalizations { /// **'Battery Type'** String get repeater_batteryChemistry; + /// No description provided for @repeater_powerSource. + /// + /// In en, this message translates to: + /// **'Power Source'** + String get repeater_powerSource; + + /// No description provided for @repeater_powerUsb. + /// + /// In en, this message translates to: + /// **'USB'** + String get repeater_powerUsb; + + /// No description provided for @repeater_powerSolar. + /// + /// In en, this message translates to: + /// **'Solar'** + String get repeater_powerSolar; + + /// No description provided for @repeater_powerCharging. + /// + /// In en, this message translates to: + /// **'Charging'** + String get repeater_powerCharging; + + /// No description provided for @repeater_powerBatteryOnly. + /// + /// In en, this message translates to: + /// **'Battery only'** + String get repeater_powerBatteryOnly; + + /// No description provided for @repeater_powerNone. + /// + /// In en, this message translates to: + /// **'No power source'** + String get repeater_powerNone; + /// No description provided for @repeater_clockAtLogin. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index b167f816..744ce952 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1723,7 +1723,25 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_battery => 'Батерия'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Тип батерия'; + + @override + String get repeater_powerSource => 'Източник на захранване'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Слънчева'; + + @override + String get repeater_powerCharging => 'Зареждане'; + + @override + String get repeater_powerBatteryOnly => 'Само батерия'; + + @override + String get repeater_powerNone => 'Няма захранване'; @override String get repeater_clockAtLogin => 'Часовник (при влизане)'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 70ad67d9..724f3fa1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1720,7 +1720,25 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_battery => 'Akku'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Akkutyp'; + + @override + String get repeater_powerSource => 'Stromquelle'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solar'; + + @override + String get repeater_powerCharging => 'Lädt'; + + @override + String get repeater_powerBatteryOnly => 'Nur Akku'; + + @override + String get repeater_powerNone => 'Keine Stromquelle'; @override String get repeater_clockAtLogin => 'Uhr (bei Anmeldung)'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0c1aa189..86399d03 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1695,6 +1695,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_batteryChemistry => 'Battery Type'; + @override + String get repeater_powerSource => 'Power Source'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solar'; + + @override + String get repeater_powerCharging => 'Charging'; + + @override + String get repeater_powerBatteryOnly => 'Battery only'; + + @override + String get repeater_powerNone => 'No power source'; + @override String get repeater_clockAtLogin => 'Clock (at login)'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index bb6fb26b..c4e083e9 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1717,7 +1717,25 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_battery => 'Batería'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Tipo de batería'; + + @override + String get repeater_powerSource => 'Fuente de alimentación'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solar'; + + @override + String get repeater_powerCharging => 'Cargando'; + + @override + String get repeater_powerBatteryOnly => 'Solo batería'; + + @override + String get repeater_powerNone => 'Sin fuente de alimentación'; @override String get repeater_clockAtLogin => 'Reloj (al inicio de sesión)'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 582629d5..6d7f7434 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1728,7 +1728,25 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_battery => 'Batterie'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Type de batterie'; + + @override + String get repeater_powerSource => 'Source d\'alimentation'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solaire'; + + @override + String get repeater_powerCharging => 'En charge'; + + @override + String get repeater_powerBatteryOnly => 'Batterie seule'; + + @override + String get repeater_powerNone => 'Aucune source'; @override String get repeater_clockAtLogin => 'Horloge (au démarrage)'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index f044a5d9..9d7fa8aa 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1717,7 +1717,25 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_battery => 'Batteria'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Tipo di batteria'; + + @override + String get repeater_powerSource => 'Fonte di alimentazione'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solare'; + + @override + String get repeater_powerCharging => 'In carica'; + + @override + String get repeater_powerBatteryOnly => 'Solo batteria'; + + @override + String get repeater_powerNone => 'Nessuna alimentazione'; @override String get repeater_clockAtLogin => 'Orologio (all\'accesso)'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 165dc41c..1b593e96 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1711,7 +1711,25 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_battery => 'Batterij'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Batterijtype'; + + @override + String get repeater_powerSource => 'Stroombron'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Zonne-energie'; + + @override + String get repeater_powerCharging => 'Opladen'; + + @override + String get repeater_powerBatteryOnly => 'Alleen batterij'; + + @override + String get repeater_powerNone => 'Geen stroombron'; @override String get repeater_clockAtLogin => 'Tijd (bij aanmelden)'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index c5a18a23..45bd49e0 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1721,7 +1721,25 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_battery => 'Bateria'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Typ baterii'; + + @override + String get repeater_powerSource => 'Źródło zasilania'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solarny'; + + @override + String get repeater_powerCharging => 'Ładowanie'; + + @override + String get repeater_powerBatteryOnly => 'Tylko bateria'; + + @override + String get repeater_powerNone => 'Brak źródła zasilania'; @override String get repeater_clockAtLogin => 'Godzina (przy logowaniu)'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5692803a..4515a7ec 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1718,7 +1718,25 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_battery => 'Bateria'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Tipo de bateria'; + + @override + String get repeater_powerSource => 'Fonte de energia'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solar'; + + @override + String get repeater_powerCharging => 'Carregando'; + + @override + String get repeater_powerBatteryOnly => 'Apenas bateria'; + + @override + String get repeater_powerNone => 'Sem fonte de energia'; @override String get repeater_clockAtLogin => 'Relógio (no login)'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index cfbd681c..e29b2366 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1720,7 +1720,25 @@ class AppLocalizationsRu extends AppLocalizations { String get repeater_battery => 'Батарея'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Тип батареи'; + + @override + String get repeater_powerSource => 'Источник питания'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Солнечная'; + + @override + String get repeater_powerCharging => 'Зарядка'; + + @override + String get repeater_powerBatteryOnly => 'Только батарея'; + + @override + String get repeater_powerNone => 'Нет питания'; @override String get repeater_clockAtLogin => 'Время (при входе)'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 2fa91e66..0e3dea6f 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1713,7 +1713,25 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_battery => 'Batéria'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Typ batérie'; + + @override + String get repeater_powerSource => 'Zdroj napájania'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Solárna'; + + @override + String get repeater_powerCharging => 'Nabíjanie'; + + @override + String get repeater_powerBatteryOnly => 'Len batéria'; + + @override + String get repeater_powerNone => 'Žiadny zdroj napájania'; @override String get repeater_clockAtLogin => 'Čas (při přihlášení)'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 8bb10473..fc6756b0 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1712,7 +1712,25 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_battery => 'Baterija'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Tip baterije'; + + @override + String get repeater_powerSource => 'Vir napajanja'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Sončna'; + + @override + String get repeater_powerCharging => 'Polnjenje'; + + @override + String get repeater_powerBatteryOnly => 'Samo baterija'; + + @override + String get repeater_powerNone => 'Ni vira napajanja'; @override String get repeater_clockAtLogin => 'Ure (pri prijavi)'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index f181a1ef..bc7bde5b 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1702,7 +1702,25 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_battery => 'Batteri'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Batterityp'; + + @override + String get repeater_powerSource => 'Strömkälla'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Sol'; + + @override + String get repeater_powerCharging => 'Laddar'; + + @override + String get repeater_powerBatteryOnly => 'Endast batteri'; + + @override + String get repeater_powerNone => 'Ingen strömkälla'; @override String get repeater_clockAtLogin => 'Klocka (vid inloggning)'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 34774611..6fb64253 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1721,7 +1721,25 @@ class AppLocalizationsUk extends AppLocalizations { String get repeater_battery => 'Батарея'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => 'Тип батареї'; + + @override + String get repeater_powerSource => 'Джерело живлення'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => 'Сонячна'; + + @override + String get repeater_powerCharging => 'Заряджання'; + + @override + String get repeater_powerBatteryOnly => 'Тільки батарея'; + + @override + String get repeater_powerNone => 'Немає джерела живлення'; @override String get repeater_clockAtLogin => 'Годинник (при вході)'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 93f928b1..6e901a36 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1641,7 +1641,25 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_battery => '电池'; @override - String get repeater_batteryChemistry => 'Battery Type'; + String get repeater_batteryChemistry => '电池类型'; + + @override + String get repeater_powerSource => '电源'; + + @override + String get repeater_powerUsb => 'USB'; + + @override + String get repeater_powerSolar => '太阳能'; + + @override + String get repeater_powerCharging => '充电中'; + + @override + String get repeater_powerBatteryOnly => '仅电池'; + + @override + String get repeater_powerNone => '无电源'; @override String get repeater_clockAtLogin => '登录时的时间'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index e94deb37..2de54e4f 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Systeeminformatie", "repeater_battery": "Batterij", + "repeater_batteryChemistry": "Batterijtype", + "repeater_powerSource": "Stroombron", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Zonne-energie", + "repeater_powerCharging": "Opladen", + "repeater_powerBatteryOnly": "Alleen batterij", + "repeater_powerNone": "Geen stroombron", "repeater_clockAtLogin": "Tijd (bij aanmelden)", "repeater_uptime": "Beschikbaarheid", "repeater_queueLength": "Wachttijd", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 44552c3c..eede34bb 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Informacje o systemie", "repeater_battery": "Bateria", + "repeater_batteryChemistry": "Typ baterii", + "repeater_powerSource": "Źródło zasilania", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solarny", + "repeater_powerCharging": "Ładowanie", + "repeater_powerBatteryOnly": "Tylko bateria", + "repeater_powerNone": "Brak źródła zasilania", "repeater_clockAtLogin": "Godzina (przy logowaniu)", "repeater_uptime": "Dostępność", "repeater_queueLength": "Długość kolejki", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 56a7f2b2..1a527bd2 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Informações do Sistema", "repeater_battery": "Bateria", + "repeater_batteryChemistry": "Tipo de bateria", + "repeater_powerSource": "Fonte de energia", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solar", + "repeater_powerCharging": "Carregando", + "repeater_powerBatteryOnly": "Apenas bateria", + "repeater_powerNone": "Sem fonte de energia", "repeater_clockAtLogin": "Relógio (no login)", "repeater_uptime": "Disponibilidade", "repeater_queueLength": "Comprimento da Fila", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0bca5ef0..5e608a9b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -486,6 +486,13 @@ "repeater_errorLoadingStatus": "Ошибка загрузки статуса: {error}", "repeater_systemInformation": "Системная информация", "repeater_battery": "Батарея", + "repeater_batteryChemistry": "Тип батареи", + "repeater_powerSource": "Источник питания", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Солнечная", + "repeater_powerCharging": "Зарядка", + "repeater_powerBatteryOnly": "Только батарея", + "repeater_powerNone": "Нет питания", "repeater_clockAtLogin": "Время (при входе)", "repeater_uptime": "Время работы", "repeater_queueLength": "Длина очереди", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index d61cca61..955d088e 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Informácie o systéme", "repeater_battery": "Batéria", + "repeater_batteryChemistry": "Typ batérie", + "repeater_powerSource": "Zdroj napájania", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Solárna", + "repeater_powerCharging": "Nabíjanie", + "repeater_powerBatteryOnly": "Len batéria", + "repeater_powerNone": "Žiadny zdroj napájania", "repeater_clockAtLogin": "Čas (při přihlášení)", "repeater_uptime": "Dostupnosť", "repeater_queueLength": "Dĺžka fronty", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index cbc4e3f6..2d9239c9 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Informacije o sistemu", "repeater_battery": "Baterija", + "repeater_batteryChemistry": "Tip baterije", + "repeater_powerSource": "Vir napajanja", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Sončna", + "repeater_powerCharging": "Polnjenje", + "repeater_powerBatteryOnly": "Samo baterija", + "repeater_powerNone": "Ni vira napajanja", "repeater_clockAtLogin": "Ure (pri prijavi)", "repeater_uptime": "Čas delovanja", "repeater_queueLength": "Dolžina čakalne vrste", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 05f77cb8..f939abb2 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -898,6 +898,13 @@ }, "repeater_systemInformation": "Systeminformation", "repeater_battery": "Batteri", + "repeater_batteryChemistry": "Batterityp", + "repeater_powerSource": "Strömkälla", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Sol", + "repeater_powerCharging": "Laddar", + "repeater_powerBatteryOnly": "Endast batteri", + "repeater_powerNone": "Ingen strömkälla", "repeater_clockAtLogin": "Klocka (vid inloggning)", "repeater_uptime": "Tillgänglighet", "repeater_queueLength": "Köans längd", diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 3362d409..6fcca663 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -899,6 +899,13 @@ }, "repeater_systemInformation": "Системна інформація", "repeater_battery": "Батарея", + "repeater_batteryChemistry": "Тип батареї", + "repeater_powerSource": "Джерело живлення", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "Сонячна", + "repeater_powerCharging": "Заряджання", + "repeater_powerBatteryOnly": "Тільки батарея", + "repeater_powerNone": "Немає джерела живлення", "repeater_clockAtLogin": "Годинник (при вході)", "repeater_uptime": "Час роботи", "repeater_queueLength": "Довжина черги", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c941461a..cda23ba7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -921,6 +921,13 @@ }, "repeater_systemInformation": "系统信息", "repeater_battery": "电池", + "repeater_batteryChemistry": "电池类型", + "repeater_powerSource": "电源", + "repeater_powerUsb": "USB", + "repeater_powerSolar": "太阳能", + "repeater_powerCharging": "充电中", + "repeater_powerBatteryOnly": "仅电池", + "repeater_powerNone": "无电源", "repeater_clockAtLogin": "登录时的时间", "repeater_uptime": "正常运行时间", "repeater_queueLength": "排队长度", diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart index 0d12fd5e..1c5e9f65 100644 --- a/lib/screens/repeater_status_screen.dart +++ b/lib/screens/repeater_status_screen.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../l10n/l10n.dart'; +import '../l10n/app_localizations.dart'; import '../models/contact.dart'; import '../models/path_selection.dart'; import '../connector/meshcore_connector.dart'; @@ -57,6 +58,7 @@ class _RepeaterStatusScreenState extends State { int? _dupDirect; PathSelection? _pendingStatusSelection; String? _reportedChemistry; // From firmware, if available + PowerState? _powerState; // Power source info from firmware @override void initState() { @@ -163,8 +165,20 @@ class _RepeaterStatusScreenState extends State { // Check for optional chemistry byte at offset 60 (protocol extension) // 0=none, 1=lipo, 2=lifepo4, 3=leadacid String? reportedChem; + PowerState? powerState; if (frame.length > _statusResponseBytes) { reportedChem = chemistryFromByte(frame[_statusResponseBytes]); + + // Parse power state byte (offset 61) and input voltage (bytes 62-63) + if (frame.length > _statusResponseBytes + 1) { + final powerFlags = frame[_statusResponseBytes + 1]; + int? inputMv; + if (frame.length > _statusResponseBytes + 3) { + inputMv = frame[_statusResponseBytes + 2] | + (frame[_statusResponseBytes + 3] << 8); + } + powerState = PowerState(powerFlags, inputMv); + } } _statusTimeout?.cancel(); @@ -189,6 +203,7 @@ class _RepeaterStatusScreenState extends State { _dupDirect = directDups; _dupFlood = floodDups; _reportedChemistry = reportedChem; + _powerState = powerState; }); _recordStatusResult(true); } @@ -263,6 +278,7 @@ class _RepeaterStatusScreenState extends State { _dupFlood = null; _dupDirect = null; _reportedChemistry = null; + _powerState = null; }); try { @@ -487,6 +503,8 @@ class _RepeaterStatusScreenState extends State { chemistry, isFromFirmware: _reportedChemistry != null, ), + // Show power source row if firmware reports power state + if (_powerState != null) _buildPowerSourceRow(l10n), _buildInfoRow(l10n.repeater_clockAtLogin, _clockText()), _buildInfoRow(l10n.repeater_uptime, _formatDuration(_uptimeSecs)), _buildInfoRow(l10n.repeater_queueLength, _formatValue(_queueLen)), @@ -588,6 +606,80 @@ class _RepeaterStatusScreenState extends State { ); } + Widget _buildPowerSourceRow(AppLocalizations l10n) { + final power = _powerState; + if (power == null) return const SizedBox.shrink(); + + // Build list of status parts + final parts = []; + if (power.isUsbConnected) parts.add(l10n.repeater_powerUsb); + if (power.isSolarConnected) parts.add(l10n.repeater_powerSolar); + if (power.isCharging) parts.add(l10n.repeater_powerCharging); + + // Add input voltage if available + final voltageStr = power.inputVoltageString; + if (voltageStr != null) parts.add('${voltageStr}V'); + + // Fallback text if no external power + final statusText = parts.isEmpty + ? (power.isBatteryPresent + ? l10n.repeater_powerBatteryOnly + : l10n.repeater_powerNone) + : parts.join(' / '); + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 130, + child: Text( + l10n.repeater_powerSource, + style: TextStyle( + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + ), + Expanded( + child: Row( + children: [ + // USB icon + if (power.isUsbConnected) + Padding( + padding: const EdgeInsets.only(right: 6), + child: Icon(Icons.usb, size: 18, color: Colors.blue[600]), + ), + // Solar icon + if (power.isSolarConnected) + Padding( + padding: const EdgeInsets.only(right: 6), + child: + Icon(Icons.wb_sunny, size: 18, color: Colors.orange[600]), + ), + // Charging icon + if (power.isCharging) + Padding( + padding: const EdgeInsets.only(right: 6), + child: Icon(Icons.battery_charging_full, + size: 18, color: Colors.green[600]), + ), + Flexible( + child: Text( + statusText, + style: const TextStyle(fontWeight: FontWeight.w400), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + ], + ), + ); + } + Widget _buildRadioStatsCard() { final l10n = context.l10n; return Card( diff --git a/lib/utils/battery_utils.dart b/lib/utils/battery_utils.dart index 6f69beed..77866493 100644 --- a/lib/utils/battery_utils.dart +++ b/lib/utils/battery_utils.dart @@ -44,3 +44,29 @@ String chemistryFromByte(int byte) { _ => 'lipo', // Default fallback }; } + +/// Power state flags parsed from protocol byte 61. +/// Bitmask: USB=0x01, Solar=0x02, Charging=0x04, Battery=0x08 +class PowerState { + static const int usbConnected = 0x01; + static const int solarConnected = 0x02; + static const int charging = 0x04; + static const int batteryPresent = 0x08; + + final int flags; + final int? inputMv; + + const PowerState(this.flags, [this.inputMv]); + + bool get isUsbConnected => (flags & usbConnected) != 0; + bool get isSolarConnected => (flags & solarConnected) != 0; + bool get isCharging => (flags & charging) != 0; + bool get isBatteryPresent => (flags & batteryPresent) != 0; + bool get hasExternalPower => isUsbConnected || isSolarConnected; + + /// Returns input voltage as a formatted string (e.g., "5.03"), or null if unavailable. + String? get inputVoltageString { + if (inputMv == null || inputMv == 0) return null; + return (inputMv! / 1000.0).toStringAsFixed(2); + } +}