From 6bb8b85a9bb09133ddc059c99963c7bfb74194d7 Mon Sep 17 00:00:00 2001 From: Strycher Date: Tue, 14 Jul 2026 12:59:52 -0400 Subject: [PATCH] feat(#178): BLOCK_LIST streamed-dump parser + truncation detection Route 0xC2 frames in _handleFrame; accumulate the LIST dump (START 0xFF count -> KEY index+key:32 -> END 0xFE). Detect truncation (count vs received; APP_START early-END uses the same 0xFE) and re-request on a 2s settle. Never derive removals from a partial pull. Union reconcile wired in B4. Epic B #166 / B3 #178. Co-Authored-By: Claude Opus 4.8 --- lib/connector/meshcore_connector.dart | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index dd808a5..4670a60 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -269,6 +269,10 @@ class MeshCoreConnector extends ChangeNotifier { bool? _clientRepeat; MeshCoreRadioStateSnapshot? _rememberedNonRepeatRadioState; int? _firmwareVerCode; + // BLOCK_LIST (0xC2/0x03) streamed-dump accumulation (B3). + List? _blockDumpKeys; + int? _blockDumpExpected; + Timer? _blockListRetryTimer; String? _firmwareVersion; String? _deviceModel; int? _offbandCaps; @@ -4074,6 +4078,51 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } + /// Route an incoming `0xC2` frame. Only the BLOCK_LIST streamed dump is parsed + /// here (B3); ADD/REMOVE/CLEAR replies are handled by the sync layer (B4). + /// LIST dump: START `[0xC2 0x03 0xFF count]` → KEY `[0xC2 0x03 index key:32]` + /// → 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; + final marker = frame[2]; + if (marker == 0xFF) { + _blockDumpKeys = []; + _blockDumpExpected = frame.length >= 4 ? frame[3] : 0; + } else if (marker == 0xFE) { + final keys = _blockDumpKeys ?? []; + final expected = _blockDumpExpected; + final truncated = expected != null && keys.length < expected; + _blockDumpKeys = null; + _blockDumpExpected = null; + _onFirmwareBlockListReceived(keys, truncated: truncated); + } else if (frame.length >= 35 && _blockDumpKeys != null) { + // KEY frame: marker is the slot index (< MAX_BLOCKED_KEYS, never 0xFE/0xFF). + _blockDumpKeys!.add(pubKeyToHex(frame.sublist(3, 35))); + } + } + + /// Called when a BLOCK_LIST dump ends. A truncated dump (early-END) is + /// re-requested once the link settles; never derive removals from a partial + /// pull. The union reconcile against the local list is wired in B4. + void _onFirmwareBlockListReceived( + List nodeKeys, { + required bool truncated, + }) { + _appDebugLogService?.info( + 'Firmware block list: ${nodeKeys.length} keys (truncated=$truncated)', + tag: 'Block', + ); + if (truncated) { + _blockListRetryTimer?.cancel(); + _blockListRetryTimer = Timer(const Duration(seconds: 2), () { + if (isConnected && supportsOffbandBlock) { + unawaited(sendFrame(buildOffbandBlockListFrame())); + } + }); + } + } + void _handleFrame(List data) { if (data.isEmpty) return; _lastRxTime = DateTime.now(); @@ -4095,6 +4144,9 @@ class MeshCoreConnector extends ChangeNotifier { case respCodeOffbandGps: _handleOffbandGps(frame); break; + case cmdOffbandBlock: + _handleOffbandBlockFrame(frame); + break; case respCodeSelfInfo: debugPrint('Got SELF_INFO'); _handleSelfInfo(frame);