From d1d94e5252a496772b2c6ffea70196a6cd763183 Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Tue, 4 Nov 2025 00:50:42 -0500 Subject: [PATCH] fix(ble): Pass peer identity via callback to eliminate redundant read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- src/RNS/Interfaces/BLEInterface.py | 56 ++++++++------------ src/RNS/Interfaces/bluetooth_driver.py | 2 +- src/RNS/Interfaces/linux_bluetooth_driver.py | 12 +++-- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/src/RNS/Interfaces/BLEInterface.py b/src/RNS/Interfaces/BLEInterface.py index e618f56..b073ecf 100644 --- a/src/RNS/Interfaces/BLEInterface.py +++ b/src/RNS/Interfaces/BLEInterface.py @@ -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): diff --git a/src/RNS/Interfaces/bluetooth_driver.py b/src/RNS/Interfaces/bluetooth_driver.py index b39a8ba..2274025 100644 --- a/src/RNS/Interfaces/bluetooth_driver.py +++ b/src/RNS/Interfaces/bluetooth_driver.py @@ -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 diff --git a/src/RNS/Interfaces/linux_bluetooth_driver.py b/src/RNS/Interfaces/linux_bluetooth_driver.py index 6029d27..c9275b3 100644 --- a/src/RNS/Interfaces/linux_bluetooth_driver.py +++ b/src/RNS/Interfaces/linux_bluetooth_driver.py @@ -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")