feat(#179): union sync on connect + push block/unblock to firmware

BlockService gains a firmwareSync sink (fired on real block/unblock/promote) and
importKeys (pull side, no echo). Connector sets the sink in initialize, pushes
ADD/REMOVE via 0xC2 when supportsOffbandBlock, and on device-info (caps landed)
pulls BLOCK_LIST. On dump completion: UNION — import node keys missing locally
(no echo), push local keys the node lacks; never remove (only explicit unblock
sends REMOVE). Non-Offband radios skip all of it (app-local only).

Epic B #166 / B4 #179.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/237/head
Strycher 1 week ago
parent 6bb8b85a9b
commit ac64a7bdd1

@ -1049,6 +1049,7 @@ class MeshCoreConnector extends ChangeNotifier {
_backgroundService = backgroundService;
_timeoutPredictionService = timeoutPredictionService;
_blockService = blockService;
blockService?.firmwareSync = _pushBlockChange;
_usbManager.setDebugLogService(_appDebugLogService);
_tcpConnector.setDebugLogService(_appDebugLogService);
@ -4120,7 +4121,52 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(sendFrame(buildOffbandBlockListFrame()));
}
});
return;
}
// Complete dump UNION. Import node keys missing locally (no echo back),
// then push local keys the node is missing. Never remove (union): only an
// explicit user unblock issues a REMOVE.
final blockService = _blockService;
if (blockService == null) return;
final nodeSet = nodeKeys.map((k) => k.toLowerCase()).toSet();
final local = blockService.blockedKeys;
unawaited(blockService.importKeys(nodeSet.difference(local)));
for (final key in local.difference(nodeSet)) {
_pushBlockChange(key, true);
}
}
/// On (re)connect to a block-capable radio, pull its block list; the union
/// reconcile runs when the dump completes.
void _reconcileFirmwareBlock() {
if (!supportsOffbandBlock) return;
unawaited(sendFrame(buildOffbandBlockListFrame()));
}
/// Push one local block change to the firmware store (Offband, capability
/// gated). Wired as `BlockService.firmwareSync`.
void _pushBlockChange(String keyHex, bool blocked) {
if (!supportsOffbandBlock) return;
final key = _pubKeyHexToBytes(keyHex);
if (key.length != pubKeySize) return;
unawaited(
sendFrame(
blocked
? buildOffbandBlockAddFrame(key)
: buildOffbandBlockRemoveFrame(key),
),
);
}
Uint8List _pubKeyHexToBytes(String hex) {
final clean = hex.length >= pubKeySize * 2
? hex.substring(0, pubKeySize * 2)
: hex;
final out = Uint8List(clean.length ~/ 2);
for (var i = 0; i < out.length; i++) {
out[i] = int.parse(clean.substring(i * 2, i * 2 + 2), radix: 16);
}
return out;
}
void _handleFrame(List<int> data) {
@ -4485,6 +4531,8 @@ class MeshCoreConnector extends ChangeNotifier {
// Caps just landed; (re)evaluate GPS polling in case a `gps=1` custom-var
// frame arrived before this device-info reply set support. (#144)
_reconcileGpsPolling();
// Caps landed if the radio speaks block, pull its list and union-reconcile.
_reconcileFirmwareBlock();
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
final reportedContacts = frame[2];

@ -16,6 +16,12 @@ class BlockService extends ChangeNotifier {
final Set<String> _blockedKeys = {};
final Map<String, int> _blockedNames = {};
/// Set by the connector: pushes a single key change to the firmware block
/// store when the connected radio supports it (Epic B). `blocked` = add vs
/// remove. Fired only on an actual local change; NOT fired by [importKeys]
/// (the pull side), so a synced key never echoes back to the radio.
void Function(String keyHex, bool blocked)? firmwareSync;
/// Load persisted state. Call once during app startup.
Future<void> load() async {
_blockedKeys
@ -38,13 +44,30 @@ class BlockService extends ChangeNotifier {
_blockedNames.containsKey(name.trim().toLowerCase());
Future<void> block(String publicKeyHex) async {
if (!_blockedKeys.add(publicKeyHex.toLowerCase())) return;
final key = publicKeyHex.toLowerCase();
if (!_blockedKeys.add(key)) return;
await _store.saveKeys(_blockedKeys);
firmwareSync?.call(key, true);
notifyListeners();
}
Future<void> unblock(String publicKeyHex) async {
if (!_blockedKeys.remove(publicKeyHex.toLowerCase())) return;
final key = publicKeyHex.toLowerCase();
if (!_blockedKeys.remove(key)) return;
await _store.saveKeys(_blockedKeys);
firmwareSync?.call(key, false);
notifyListeners();
}
/// Merge keys learned from the firmware block list into the local set WITHOUT
/// echoing them back to the radio (used by the connect-time union pull).
/// Never removes the union only adds.
Future<void> importKeys(Iterable<String> keysHex) async {
var changed = false;
for (final k in keysHex) {
if (_blockedKeys.add(k.toLowerCase())) changed = true;
}
if (!changed) return;
await _store.saveKeys(_blockedKeys);
notifyListeners();
}
@ -72,10 +95,12 @@ class BlockService extends ChangeNotifier {
Future<void> maybePromote(String name, String publicKeyHex) async {
final n = name.trim().toLowerCase();
if (n.isEmpty || !_blockedNames.containsKey(n)) return;
final key = publicKeyHex.toLowerCase();
_blockedNames.remove(n);
_blockedKeys.add(publicKeyHex.toLowerCase());
final added = _blockedKeys.add(key);
await _store.saveNames(_blockedNames);
await _store.saveKeys(_blockedKeys);
if (added) firmwareSync?.call(key, true);
notifyListeners();
}

Loading…
Cancel
Save

Powered by TurnKey Linux.