From c8dea671eb4cbc847dd3ac4bde2a327e2457feb1 Mon Sep 17 00:00:00 2001 From: Strycher Date: Tue, 14 Jul 2026 13:07:37 -0400 Subject: [PATCH] feat(#180): offload error visibility + capability signal for UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connector parses ADD/REMOVE/CLEAR replies; ADD ok=0 (node store full at 32) sets blockOffloadStoreFull (local stays authoritative), cleared on reconcile. BlockedView shows a firmware-offload status row when supportsOffbandBlock — 'active', or a 'radio block list full' warning. Adds block_offload* l10n. Epic B #166 / B5 #180. Co-Authored-By: Claude Opus 4.8 --- lib/connector/meshcore_connector.dart | 33 ++++++++++++- lib/l10n/app_en.arb | 4 +- lib/l10n/app_localizations.dart | 12 +++++ lib/l10n/app_localizations_bg.dart | 8 +++ lib/l10n/app_localizations_de.dart | 8 +++ lib/l10n/app_localizations_en.dart | 8 +++ lib/l10n/app_localizations_es.dart | 8 +++ lib/l10n/app_localizations_fr.dart | 8 +++ lib/l10n/app_localizations_hu.dart | 8 +++ lib/l10n/app_localizations_it.dart | 8 +++ lib/l10n/app_localizations_ja.dart | 8 +++ lib/l10n/app_localizations_ko.dart | 8 +++ lib/l10n/app_localizations_nl.dart | 8 +++ lib/l10n/app_localizations_pl.dart | 8 +++ lib/l10n/app_localizations_pt.dart | 8 +++ lib/l10n/app_localizations_ru.dart | 8 +++ lib/l10n/app_localizations_sk.dart | 8 +++ lib/l10n/app_localizations_sl.dart | 8 +++ lib/l10n/app_localizations_sv.dart | 8 +++ lib/l10n/app_localizations_uk.dart | 8 +++ lib/l10n/app_localizations_zh.dart | 8 +++ lib/screens/settings/blocked_view.dart | 34 ++++++++++++- untranslated.json | 68 +++++++++++++++++++------- 23 files changed, 275 insertions(+), 20 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 0f8d55f..ceb4b04 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -273,6 +273,7 @@ class MeshCoreConnector extends ChangeNotifier { List? _blockDumpKeys; int? _blockDumpExpected; Timer? _blockListRetryTimer; + bool _blockOffloadStoreFull = false; String? _firmwareVersion; String? _deviceModel; int? _offbandCaps; @@ -549,6 +550,10 @@ class MeshCoreConnector extends ChangeNotifier { /// (Offband firmware, capability-gated). Absent → app-local block only. bool get supportsOffbandBlock => firmwareSupportsOffbandBlock(_offbandCaps, _firmwareVerCode); + + /// True when the radio's block store hit `MAX_BLOCKED_KEYS` (32) and rejected + /// an ADD (ok=0). Local block still applies; those keys just aren't portable. + bool get blockOffloadStoreFull => _blockOffloadStoreFull; Map? get currentCustomVars => _currentCustomVars; int? get batteryMillivolts => _batteryMillivolts; int? get storageUsedKb => _storageUsedKb; @@ -4085,7 +4090,14 @@ class MeshCoreConnector extends ChangeNotifier { /// → END `[0xC2 0x03 0xFE]`. Normal END and `APP_START` early-END share `0xFE`; /// truncation is detected by `count` vs key-frames received. void _handleOffbandBlockFrame(Uint8List frame) { - if (frame.length < 3 || frame[1] != offbandBlockList) return; + if (frame.length < 2) return; + if (frame[1] != offbandBlockList) { + // ADD/REMOVE/CLEAR reply: [0xC2][sub][ok]. + final reply = parseOffbandBlockReply(frame); + if (reply != null) _handleOffbandBlockReply(reply); + return; + } + if (frame.length < 3) return; final marker = frame[2]; if (marker == 0xFF) { _blockDumpKeys = []; @@ -4140,9 +4152,28 @@ class MeshCoreConnector extends ChangeNotifier { /// reconcile runs when the dump completes. void _reconcileFirmwareBlock() { if (!supportsOffbandBlock) return; + if (_blockOffloadStoreFull) { + _blockOffloadStoreFull = false; + notifyListeners(); + } unawaited(sendFrame(buildOffbandBlockListFrame())); } + /// Handle an ADD/REMOVE/CLEAR reply. ADD ok=0 = node store full + /// (`MAX_BLOCKED_KEYS`); local stays authoritative, surface it for the UI. + void _handleOffbandBlockReply(OffbandBlockReply reply) { + _appDebugLogService?.info( + 'Offband block reply: sub=${reply.sub} ok=${reply.ok}', + tag: 'Block', + ); + if (reply.sub == offbandBlockAdd && + reply.ok == 0 && + !_blockOffloadStoreFull) { + _blockOffloadStoreFull = true; + notifyListeners(); + } + } + /// Push one local block change to the firmware store (Offband, capability /// gated). Wired as `BlockService.firmwareSync`. void _pushBlockChange(String keyHex, bool blocked) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 544042a..f6e4575 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -2599,5 +2599,7 @@ "block_none": "No blocked users", "block_keysSection": "Blocked contacts", "block_namesSection": "Blocked names", - "block_namesHint": "Name-only blocks (channel senders) — each upgrades to a full block once we learn their key." + "block_namesHint": "Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.", + "block_offloadActive": "Firmware offload active — blocks sync to this radio", + "block_offloadStoreFull": "Radio block list full (32) — extra blocks stay app-only" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b93dce7..0b4982e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -7869,6 +7869,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'** String get block_namesHint; + + /// No description provided for @block_offloadActive. + /// + /// In en, this message translates to: + /// **'Firmware offload active — blocks sync to this radio'** + String get block_offloadActive; + + /// No description provided for @block_offloadStoreFull. + /// + /// In en, this message translates to: + /// **'Radio block list full (32) — extra blocks stay app-only'** + String get block_offloadStoreFull; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 72c112a..425fb8f 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -4607,4 +4607,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ba6254b..5f700a9 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -4625,4 +4625,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 91542b9..764304b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -4529,4 +4529,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 44b1947..a9fb07f 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -4612,4 +4612,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index a6ff879..525014c 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -4641,4 +4641,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 9b9df38..e980304 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -4628,4 +4628,12 @@ class AppLocalizationsHu extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index dbfa675..7be06e7 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -4617,4 +4617,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 49331ac..7e38e10 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -4382,4 +4382,12 @@ class AppLocalizationsJa extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index a1715b1..6d4e381 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -4383,4 +4383,12 @@ class AppLocalizationsKo extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 1ca42ba..558412b 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -4592,4 +4592,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 76be17e..2c0ed90 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -4629,4 +4629,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 20f77ac..730bd81 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -4605,4 +4605,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 9f54ec1..8e4b6af 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -4623,4 +4623,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 244f862..53b8341 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -4588,4 +4588,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 3b95d56..5940652 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -4586,4 +4586,12 @@ class AppLocalizationsSl extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 3087ff1..d0cdf67 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -4561,4 +4561,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 7fdeb17..178cc09 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -4624,4 +4624,12 @@ class AppLocalizationsUk extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 048af15..8ebdcd3 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -4256,4 +4256,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get block_namesHint => 'Name-only blocks (channel senders) — each upgrades to a full block once we learn their key.'; + + @override + String get block_offloadActive => + 'Firmware offload active — blocks sync to this radio'; + + @override + String get block_offloadStoreFull => + 'Radio block list full (32) — extra blocks stay app-only'; } diff --git a/lib/screens/settings/blocked_view.dart b/lib/screens/settings/blocked_view.dart index 1990857..438e212 100644 --- a/lib/screens/settings/blocked_view.dart +++ b/lib/screens/settings/blocked_view.dart @@ -19,12 +19,23 @@ class BlockedView extends StatelessWidget { builder: (context, block, connector, child) { final keys = block.blockedKeys.toList()..sort(); final names = block.blockedNames.keys.toList()..sort(); + final indicator = _offloadIndicator(context, connector); if (keys.isEmpty && names.isEmpty) { - return Center(child: Text(context.l10n.block_none)); + return ListView( + padding: const EdgeInsets.all(16), + children: [ + ?indicator, + Padding( + padding: const EdgeInsets.symmetric(vertical: 48), + child: Center(child: Text(context.l10n.block_none)), + ), + ], + ); } return ListView( padding: const EdgeInsets.all(16), children: [ + ?indicator, if (keys.isNotEmpty) ...[ _sectionHeader(context.l10n.block_keysSection), ...keys.map((k) => _keyTile(context, block, connector, k)), @@ -47,6 +58,27 @@ class BlockedView extends StatelessWidget { ); } + /// Firmware-offload status row (only when the radio supports block offload): + /// "active" normally, or a "store full" warning when the node hit its cap. + Widget? _offloadIndicator(BuildContext context, MeshCoreConnector connector) { + if (!connector.supportsOffbandBlock) return null; + final full = connector.blockOffloadStoreFull; + return Card( + child: ListTile( + dense: true, + leading: Icon( + full ? Icons.warning_amber : Icons.sync, + color: full ? Colors.amber.shade800 : Colors.green, + ), + title: Text( + full + ? context.l10n.block_offloadStoreFull + : context.l10n.block_offloadActive, + ), + ), + ); + } + Widget _sectionHeader(String text) => Padding( padding: const EdgeInsets.fromLTRB(8, 8, 8, 8), child: Text( diff --git a/untranslated.json b/untranslated.json index f69e92a..daf67ec 100644 --- a/untranslated.json +++ b/untranslated.json @@ -46,7 +46,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "de": [ @@ -96,7 +98,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "es": [ @@ -146,7 +150,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "fr": [ @@ -196,7 +202,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "hu": [ @@ -246,7 +254,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "it": [ @@ -296,7 +306,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "ja": [ @@ -346,7 +358,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "ko": [ @@ -396,7 +410,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "nl": [ @@ -446,7 +462,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "pl": [ @@ -496,7 +514,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "pt": [ @@ -546,7 +566,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "ru": [ @@ -596,7 +618,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "sk": [ @@ -646,7 +670,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "sl": [ @@ -696,7 +722,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "sv": [ @@ -746,7 +774,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "uk": [ @@ -796,7 +826,9 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ], "zh": [ @@ -846,6 +878,8 @@ "block_none", "block_keysSection", "block_namesSection", - "block_namesHint" + "block_namesHint", + "block_offloadActive", + "block_offloadStoreFull" ] }