fix(ble): Pass peer identity via callback to eliminate redundant read

## Problem

The central device was timing out when trying to read the identity
characteristic from peripheral devices, causing connection failures:

```
ERROR: Error reading characteristic ...28e6 from B8:27:EB:10:28:CD: TimeoutError
```

Root cause: The driver already reads the identity during connection setup
(line 806 in _connect_to_peer), but then BLEInterface tried to read it
AGAIN in _device_connected_callback. The second read consistently timed
out, likely due to BlueZ/D-Bus caching issues or characteristic state.

## Solution

Changed the `on_device_connected` callback signature to pass the peer
identity directly, following the established pattern of other callbacks
like `on_data_received(address, data)` and `on_mtu_negotiated(address, mtu)`.

### Changes

1. **Driver Interface** (bluetooth_driver.py)
   - Updated callback: `on_device_connected(str, Optional[bytes])`
   - Identity is None for peripheral connections (arrives via handshake)

2. **PeerConnection** (linux_bluetooth_driver.py)
   - Added `peer_identity: Optional[bytes]` field
   - Store identity read during connection setup

3. **Connection Flow** (linux_bluetooth_driver.py)
   - Central: Pass identity to callback after reading it
   - Peripheral: Pass None (identity comes later via handshake)

4. **BLEInterface** (BLEInterface.py)
   - Updated callback signature to accept peer_identity parameter
   - Removed buggy `read_characteristic()` call
   - Use passed identity directly for central connections
   - Added typing.Optional import

## Benefits

-  Eliminates redundant GATT read operation
-  Fixes timeout bug for central connections
-  More efficient: reuses identity already read by driver
-  Cleaner architecture: follows callback pattern consistency
-  Explicit about identity availability by connection role

## Testing

Tested on Raspberry Pi Zero W devices with BlueZ 5.82:
- Central connections now receive identity immediately
- Peripheral connections correctly wait for handshake
- No more timeout errors

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
torlando-tech 2025-11-04 00:50:42 -05:00
commit d1d94e5252
3 changed files with 31 additions and 39 deletions

View file

@ -41,6 +41,7 @@ import threading
import time
import asyncio
from collections import deque
from typing import Optional
# Add interface directory to path for importing other BLE modules
# This is needed when loaded as external interface
@ -647,54 +648,43 @@ class BLEInterface(Interface):
except Exception as e:
RNS.log(f"{self} failed to initiate connection to {device.name}: {e}", RNS.LOG_ERROR)
def _device_connected_callback(self, address: str):
def _device_connected_callback(self, address: str, peer_identity: Optional[bytes]):
"""
Driver callback: Handle successful device connection.
Called when driver has established a connection. We read the identity
characteristic and prepare to receive data.
Called when driver has established a connection. For central connections,
the peer_identity is provided. For peripheral connections, identity will
arrive later via handshake.
Args:
address: MAC address of connected peer
peer_identity: 16-byte identity hash (None for peripheral connections)
"""
# Check connection role to determine identity exchange method
role = self.driver.get_peer_role(address)
if role == "central":
# We are the central, we must read the peer's identity
RNS.log(f"{self} connected to {address} as CENTRAL, reading identity...", RNS.LOG_INFO)
try:
identity_bytes = self.driver.read_characteristic(
address,
BLEInterface.CHARACTERISTIC_IDENTITY_UUID
)
if peer_identity is not None:
# Central mode: identity provided by driver
if len(peer_identity) == 16:
identity_hash = self._compute_identity_hash(peer_identity)
if identity_bytes and len(identity_bytes) == 16:
peer_identity = bytes(identity_bytes)
identity_hash = self._compute_identity_hash(peer_identity)
# Store identity mappings
self.address_to_identity[address] = peer_identity
self.identity_to_address[identity_hash] = address
# Store identity mappings
self.address_to_identity[address] = peer_identity
self.identity_to_address[identity_hash] = address
RNS.log(f"{self} received peer identity from {address}: {identity_hash}", RNS.LOG_INFO)
self._record_connection_success(address)
else:
RNS.log(f"{self} invalid identity from {address}, disconnecting", RNS.LOG_WARNING)
self.driver.disconnect(address)
self._record_connection_failure(address)
except Exception as e:
RNS.log(f"{self} failed to read identity from {address}: {e}", RNS.LOG_ERROR)
RNS.log(f"{self} connected to {address} as CENTRAL, received identity: {identity_hash}", RNS.LOG_INFO)
self._record_connection_success(address)
else:
RNS.log(f"{self} invalid identity from {address} (wrong length), disconnecting", RNS.LOG_WARNING)
self.driver.disconnect(address)
self._record_connection_failure(address)
elif role == "peripheral":
# We are the peripheral, we must wait for the central to send its identity
# Peripheral mode: identity will arrive via handshake
RNS.log(f"{self} connected to {address} as PERIPHERAL, waiting for identity handshake...", RNS.LOG_INFO)
# The identity will be received in `handle_peripheral_data` or `_data_received_callback`
# No action is needed here.
pass
# The identity will be received in `_data_received_callback`
else:
RNS.log(f"{self} connected to {address}, but role is unknown. Disconnecting.", RNS.LOG_WARNING)
RNS.log(f"{self} connected to {address}, but identity not provided and role is {role}. Disconnecting.", RNS.LOG_WARNING)
self.driver.disconnect(address)
def _mtu_negotiated_callback(self, address: str, mtu: int):

View file

@ -44,7 +44,7 @@ class BLEDriverInterface(ABC):
# implement and assign these callbacks to receive events from the driver.
on_device_discovered: Optional[Callable[[BLEDevice], None]] = None
on_device_connected: Optional[Callable[[str], None]] = None # address (MTU reported separately)
on_device_connected: Optional[Callable[[str, Optional[bytes]], None]] = None # address, peer_identity (None for peripheral role)
on_device_disconnected: Optional[Callable[[str], None]] = None # address
on_data_received: Optional[Callable[[str, bytes], None]] = None # address, data
on_mtu_negotiated: Optional[Callable[[str, int], None]] = None # address, mtu

View file

@ -254,6 +254,7 @@ class PeerConnection:
mtu: int = 23 # Negotiated MTU
connection_type: str = "unknown" # "central" or "peripheral"
connected_at: float = 0.0
peer_identity: Optional[bytes] = None # 16-byte identity hash
class LinuxBluetoothDriver(BLEDriverInterface):
@ -822,7 +823,8 @@ class LinuxBluetoothDriver(BLEDriverInterface):
client=client,
mtu=mtu,
connection_type="central",
connected_at=time.time()
connected_at=time.time(),
peer_identity=peer_identity
)
with self._peers_lock:
@ -846,10 +848,10 @@ class LinuxBluetoothDriver(BLEDriverInterface):
except Exception as e:
self._log(f"Failed to send identity handshake: {e}", "WARNING")
# Notify callback
# Notify callback with peer identity
if self.on_device_connected:
try:
self.on_device_connected(address)
self.on_device_connected(address, peer_identity)
except Exception as e:
self._log(f"Error in device connected callback: {e}", "ERROR")
@ -1511,10 +1513,10 @@ class BluezeroGATTServer:
self._log(f"Central connected: {central_address} (MTU: {effective_mtu})")
# Notify callback
# Notify callback (identity not available yet for peripheral connections)
if self.driver.on_device_connected:
try:
self.driver.on_device_connected(central_address)
self.driver.on_device_connected(central_address, None)
except Exception as e:
self._log(f"Error in device connected callback: {e}", "ERROR")