fix: add identity cache to prevent data loss on reconnection
When Python's disconnect callback fires but the driver layer (Android/Kotlin) maintains or quickly re-establishes the GATT connection, data was being dropped because address_to_identity was cleared. Changes: - Add _identity_cache with 60-second TTL to preserve identities after disconnect - Cache identity in _device_disconnected_callback before cleanup - Check cache in _handle_ble_data and restore identity if found - Add on_address_changed callback for dual connection deduplication - Add _address_changed_callback to migrate identity mappings - Support driver.request_identity_resync() for fallback recovery This fixes the "no identity for peer X, dropping data" warning that occurred when the Python layer lost track of a peer that was still connected at the driver level. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3b3450ab78
commit
46299f3147
2 changed files with 106 additions and 4 deletions
|
|
@ -376,6 +376,10 @@ class BLEInterface(Interface):
|
|||
self.spawned_interfaces = {} # identity_hash (16 hex chars) -> BLEPeerInterface
|
||||
self.address_to_identity = {} # address -> peer_identity (16-byte identity)
|
||||
self.identity_to_address = {} # identity_hash -> address (for reverse lookup)
|
||||
# Cache for recently disconnected identities (address -> (identity, timestamp))
|
||||
# Used to restore identity when peer reconnects before cache expiry (60s)
|
||||
self._identity_cache = {}
|
||||
self._identity_cache_ttl = 60 # seconds
|
||||
|
||||
# Fragmentation
|
||||
self.fragmenters = {} # address -> BLEFragmenter (per MTU)
|
||||
|
|
@ -410,6 +414,7 @@ class BLEInterface(Interface):
|
|||
self.driver.on_device_disconnected = self._device_disconnected_callback
|
||||
self.driver.on_error = self._error_callback
|
||||
self.driver.on_duplicate_identity_detected = self._check_duplicate_identity
|
||||
self.driver.on_address_changed = self._address_changed_callback
|
||||
|
||||
# Redirect Python logging to RNS logging for proper formatting
|
||||
self._setup_logging_redirect()
|
||||
|
|
@ -985,6 +990,12 @@ class BLEInterface(Interface):
|
|||
peer_identity = self.address_to_identity.get(address)
|
||||
if peer_identity:
|
||||
identity_hash = self._compute_identity_hash(peer_identity)
|
||||
|
||||
# Cache identity before cleanup - allows restoration if peer reconnects
|
||||
# without a full identity handshake (e.g., Android maintains GATT connection)
|
||||
self._identity_cache[address] = (peer_identity, time.time())
|
||||
RNS.log(f"{self} cached identity for {address} (TTL {self._identity_cache_ttl}s)", RNS.LOG_DEBUG)
|
||||
|
||||
if identity_hash in self.spawned_interfaces:
|
||||
peer_if = self.spawned_interfaces[identity_hash]
|
||||
peer_if.detach()
|
||||
|
|
@ -1054,6 +1065,66 @@ class BLEInterface(Interface):
|
|||
|
||||
RNS.log(f"{self} cleaned up stale state for {old_address}", RNS.LOG_DEBUG)
|
||||
|
||||
def _address_changed_callback(self, old_address: str, new_address: str, identity_hash: str):
|
||||
"""
|
||||
Driver callback: Handle address change during dual connection deduplication.
|
||||
|
||||
When the driver deduplicates a dual connection (same identity connected as both
|
||||
central and peripheral), it closes one direction and notifies us to update
|
||||
our address mappings.
|
||||
|
||||
Args:
|
||||
old_address: The address that was closed/removed
|
||||
new_address: The address that remains active
|
||||
identity_hash: The 32-char hex identity hash for this peer
|
||||
"""
|
||||
RNS.log(
|
||||
f"{self} address changed for {identity_hash[:8]}: {old_address} -> {new_address}",
|
||||
RNS.LOG_INFO
|
||||
)
|
||||
|
||||
# Get peer identity from old address before cleanup
|
||||
peer_identity = self.address_to_identity.get(old_address)
|
||||
if not peer_identity:
|
||||
# Try cache if not in active mapping
|
||||
cached = self._identity_cache.get(old_address)
|
||||
if cached:
|
||||
peer_identity = cached[0]
|
||||
del self._identity_cache[old_address]
|
||||
|
||||
if peer_identity:
|
||||
# Migrate address_to_identity mapping
|
||||
if old_address in self.address_to_identity:
|
||||
del self.address_to_identity[old_address]
|
||||
self.address_to_identity[new_address] = peer_identity
|
||||
|
||||
# Update identity_to_address to point to new address
|
||||
computed_hash = self._compute_identity_hash(peer_identity)
|
||||
self.identity_to_address[computed_hash] = new_address
|
||||
|
||||
# Migrate fragmenter/reassembler from old to new key
|
||||
old_frag_key = self._get_fragmenter_key(peer_identity, old_address)
|
||||
new_frag_key = self._get_fragmenter_key(peer_identity, new_address)
|
||||
with self.frag_lock:
|
||||
if old_frag_key in self.fragmenters:
|
||||
self.fragmenters[new_frag_key] = self.fragmenters.pop(old_frag_key)
|
||||
if old_frag_key in self.reassemblers:
|
||||
self.reassemblers[new_frag_key] = self.reassemblers.pop(old_frag_key)
|
||||
|
||||
RNS.log(f"{self} migrated identity mappings from {old_address} to {new_address}", RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log(f"{self} no identity found for {old_address} during address change", RNS.LOG_WARNING)
|
||||
|
||||
# Clean up old address from other state
|
||||
if old_address in self.pending_mtu:
|
||||
mtu = self.pending_mtu.pop(old_address)
|
||||
self.pending_mtu[new_address] = mtu
|
||||
|
||||
with self.peer_lock:
|
||||
if old_address in self.peers:
|
||||
peer_data = self.peers.pop(old_address)
|
||||
self.peers[new_address] = peer_data
|
||||
|
||||
def _error_callback(self, severity: str, message: str, exc: Exception = None):
|
||||
"""
|
||||
Driver callback: Handle driver errors.
|
||||
|
|
@ -1520,8 +1591,28 @@ class BLEInterface(Interface):
|
|||
# Look up peer identity to compute fragmenter key
|
||||
peer_identity = self.address_to_identity.get(peer_address)
|
||||
if not peer_identity:
|
||||
RNS.log(f"{self} no identity for peer {peer_address}, dropping data", RNS.LOG_WARNING)
|
||||
return
|
||||
# Try identity cache - peer may have "disconnected" from Python's view
|
||||
# but Android/driver layer maintains the GATT connection
|
||||
cached = self._identity_cache.get(peer_address)
|
||||
if cached and (time.time() - cached[1]) < self._identity_cache_ttl:
|
||||
peer_identity = cached[0]
|
||||
# Restore identity mapping - peer is still active
|
||||
self.address_to_identity[peer_address] = peer_identity
|
||||
identity_hash = self._compute_identity_hash(peer_identity)
|
||||
self.identity_to_address[identity_hash] = peer_address
|
||||
RNS.log(f"{self} restored identity from cache for {peer_address}", RNS.LOG_DEBUG)
|
||||
# Remove from cache since it's now active again
|
||||
del self._identity_cache[peer_address]
|
||||
else:
|
||||
# Neither active mapping nor cache has identity - request resync from driver
|
||||
# This handles the case where Android maintained connection but Python lost state
|
||||
if hasattr(self.driver, 'request_identity_resync'):
|
||||
RNS.log(f"{self} requesting identity resync for {peer_address}", RNS.LOG_DEBUG)
|
||||
self.driver.request_identity_resync(peer_address)
|
||||
# Drop this packet - next one should work after resync completes
|
||||
else:
|
||||
RNS.log(f"{self} no identity for peer {peer_address}, dropping data", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
# Compute identity-based fragmenter key (matches peripheral data handler)
|
||||
frag_key = self._get_fragmenter_key(peer_identity, peer_address)
|
||||
|
|
@ -1577,8 +1668,18 @@ class BLEInterface(Interface):
|
|||
peer_identity = self.address_to_identity.get(peer_address, None)
|
||||
|
||||
if not peer_identity:
|
||||
RNS.log(f"{self} no identity for peer {peer_address}, packet dropped", RNS.LOG_WARNING)
|
||||
return
|
||||
# Fallback to cache (should already be restored above, but defensive)
|
||||
cached = self._identity_cache.get(peer_address)
|
||||
if cached and (time.time() - cached[1]) < self._identity_cache_ttl:
|
||||
peer_identity = cached[0]
|
||||
self.address_to_identity[peer_address] = peer_identity
|
||||
identity_hash = self._compute_identity_hash(peer_identity)
|
||||
self.identity_to_address[identity_hash] = peer_address
|
||||
del self._identity_cache[peer_address]
|
||||
RNS.log(f"{self} restored identity from cache for {peer_address} (reassembly)", RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log(f"{self} no identity for peer {peer_address}, packet dropped", RNS.LOG_WARNING)
|
||||
return
|
||||
|
||||
identity_hash = self._compute_identity_hash(peer_identity)
|
||||
peer_if = self.spawned_interfaces.get(identity_hash, None)
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class BLEDriverInterface(ABC):
|
|||
on_data_received: Optional[Callable[[str, bytes], None]] = None # address, data
|
||||
on_mtu_negotiated: Optional[Callable[[str, int], None]] = None # address, mtu
|
||||
on_error: Optional[Callable[[str, str, Optional[Exception]], None]] = None # severity, message, exception
|
||||
on_address_changed: Optional[Callable[[str, str, str], None]] = None # old_address, new_address, identity_hash
|
||||
|
||||
# --- Lifecycle & Configuration ---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue