feat(#180): offload error visibility + capability signal for UI

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 <noreply@anthropic.com>
pull/237/head
Strycher 1 week ago
parent ac64a7bdd1
commit c8dea671eb

@ -273,6 +273,7 @@ class MeshCoreConnector extends ChangeNotifier {
List<String>? _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<String, String>? 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 = <String>[];
@ -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) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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"
]
}

Loading…
Cancel
Save

Powered by TurnKey Linux.