mirror of
https://github.com/torlando-tech/columba
synced 2026-08-12 18:07:15 -04:00
BLE Connection Cleanup: - Add pending identity timeout (30s) to disconnect non-Reticulum devices (e.g., AirTags, BLE scanners) that connect but never complete handshake - Fix cancelConnection not triggering cleanup callback - manually clean up state and fire onCentralDisconnected after cancel - Notify Python immediately for all peripheral connections (with or without identity) so it can track pending connections UI Improvements: - Add live-updating connection duration timer in BLE connections screen (updates every second using LaunchedEffect) Build Changes: - Point to ble-reticulum fix branch with identity timeout changes - Load BLEInterface from local ble_modules for testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
778 lines
35 KiB
Python
778 lines
35 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
AndroidBLEDriver - Platform-specific BLE driver for Android
|
|
|
|
Implements the BLEDriverInterface from ble-reticulum for Android devices.
|
|
Bridges Python to KotlinBLEBridge via Chaquopy for native Android BLE access.
|
|
|
|
This enables Reticulum BLE networking on Android by providing the same driver
|
|
interface as the Linux implementation, but using Android's native BLE stack.
|
|
|
|
Author: Columba Project
|
|
License: MIT
|
|
"""
|
|
|
|
import RNS
|
|
import sys
|
|
import os
|
|
import time
|
|
import threading
|
|
from typing import List, Optional, Callable
|
|
|
|
# Add parent interfaces directory to path for imports
|
|
# When deployed, bluetooth_driver.py is in the parent interfaces/ directory
|
|
_interfaces_dir = os.path.dirname(os.path.dirname(__file__))
|
|
if _interfaces_dir not in sys.path:
|
|
sys.path.insert(0, _interfaces_dir)
|
|
|
|
# Import driver interface from bluetooth_driver (deployed in interfaces/)
|
|
from bluetooth_driver import BLEDriverInterface, BLEDevice, DriverState
|
|
|
|
|
|
class AndroidBLEDriver(BLEDriverInterface):
|
|
"""
|
|
Android BLE driver implementing BLEDriverInterface from ble-reticulum.
|
|
|
|
This driver bridges Python to the KotlinBLEBridge via Chaquopy, providing
|
|
native Android BLE access while implementing the standard driver interface.
|
|
|
|
The KotlinBLEBridge handles:
|
|
- BLE scanning and advertising
|
|
- GATT client/server operations
|
|
- Dual-mode operation (central + peripheral)
|
|
- Protocol v2.2 identity tracking
|
|
- MTU negotiation
|
|
- Connection management and error recovery
|
|
|
|
Note: Fragmentation/reassembly handled by BLEInterface (parent class)
|
|
|
|
This driver simply translates between the BLEDriverInterface and the
|
|
KotlinBLEBridge, handling Chaquopy interop and callback routing.
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
"""
|
|
Initialize the Android BLE driver.
|
|
|
|
Args:
|
|
**kwargs: Configuration parameters (accepted for compatibility with
|
|
BLEInterface but not used - Android driver gets config via Kotlin)
|
|
"""
|
|
super().__init__()
|
|
|
|
self._state = DriverState.IDLE
|
|
self.kotlin_bridge = None
|
|
self._transport_identity = None
|
|
self._service_uuid = None
|
|
self._rx_char_uuid = None
|
|
self._tx_char_uuid = None
|
|
self._identity_char_uuid = None
|
|
|
|
# Track connected peers
|
|
self._connected_peers = []
|
|
self._peer_roles = {} # address -> "central" or "peripheral"
|
|
self._peer_mtus = {} # address -> mtu (int)
|
|
|
|
# Identity tracking for duplicate detection (MAC rotation handling)
|
|
self._address_to_identity = {} # address -> identity_hash (32-char hex)
|
|
self._identity_to_address = {} # identity_hash -> address
|
|
|
|
# Thread safety for identity handling (prevents race conditions)
|
|
self._identity_lock = threading.Lock()
|
|
self._pending_identities = {} # address -> identity bytes (cached before connection)
|
|
|
|
# Configuration (from kwargs or defaults)
|
|
self._service_discovery_delay = kwargs.get('service_discovery_delay', 0.5)
|
|
self._power_mode = "balanced"
|
|
|
|
RNS.log("AndroidBLEDriver: Initialized", RNS.LOG_DEBUG)
|
|
|
|
# --- Lifecycle & Configuration ---
|
|
|
|
def start(self, service_uuid: str, rx_char_uuid: str, tx_char_uuid: str, identity_char_uuid: str):
|
|
"""Initialize the driver and Android BLE stack."""
|
|
try:
|
|
if self._state != DriverState.IDLE:
|
|
RNS.log(f"AndroidBLEDriver: Cannot start - current state: {self._state}", RNS.LOG_WARNING)
|
|
return
|
|
|
|
RNS.log("AndroidBLEDriver: Starting...", RNS.LOG_INFO)
|
|
|
|
# Store UUIDs
|
|
self._service_uuid = service_uuid
|
|
self._rx_char_uuid = rx_char_uuid
|
|
self._tx_char_uuid = tx_char_uuid
|
|
self._identity_char_uuid = identity_char_uuid
|
|
|
|
# Clear pending identities from previous session
|
|
with self._identity_lock:
|
|
self._pending_identities.clear()
|
|
|
|
# Get Kotlin bridge
|
|
if self.kotlin_bridge is None:
|
|
self.kotlin_bridge = self._get_kotlin_bridge()
|
|
if self.kotlin_bridge is None:
|
|
raise Exception("Failed to get KotlinBLEBridge")
|
|
|
|
# Setup callbacks
|
|
self._setup_kotlin_callbacks()
|
|
|
|
# Initialize Kotlin BLE stack
|
|
self.kotlin_bridge.startAsync(
|
|
service_uuid, rx_char_uuid, tx_char_uuid, identity_char_uuid
|
|
)
|
|
|
|
# Set identity if we have one
|
|
if self._transport_identity:
|
|
self.kotlin_bridge.setIdentity(self._transport_identity)
|
|
|
|
self._state = DriverState.IDLE
|
|
RNS.log("AndroidBLEDriver: Started successfully", RNS.LOG_INFO)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Failed to start: {e}", RNS.LOG_ERROR)
|
|
if self.on_error:
|
|
self.on_error("critical", f"Failed to start driver: {e}", e)
|
|
raise
|
|
|
|
def stop(self):
|
|
"""Stop all BLE activity and release resources."""
|
|
try:
|
|
if self._state == DriverState.IDLE:
|
|
return
|
|
|
|
RNS.log("AndroidBLEDriver: Stopping...", RNS.LOG_INFO)
|
|
|
|
if self.kotlin_bridge:
|
|
self.kotlin_bridge.stopAsync()
|
|
|
|
self._connected_peers.clear()
|
|
self._peer_roles.clear()
|
|
self._state = DriverState.IDLE
|
|
|
|
RNS.log("AndroidBLEDriver: Stopped", RNS.LOG_INFO)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error stopping: {e}", RNS.LOG_ERROR)
|
|
if self.on_error:
|
|
self.on_error("error", f"Error stopping driver: {e}", e)
|
|
|
|
def set_identity(self, identity_bytes: bytes):
|
|
"""Set the local transport identity (16 bytes)."""
|
|
if len(identity_bytes) != 16:
|
|
raise ValueError("Identity must be 16 bytes")
|
|
|
|
self._transport_identity = identity_bytes
|
|
|
|
if self.kotlin_bridge:
|
|
self.kotlin_bridge.setIdentity(identity_bytes)
|
|
|
|
hex_identity = identity_bytes.hex()
|
|
RNS.log(f"AndroidBLEDriver: Identity set: {hex_identity}", RNS.LOG_DEBUG)
|
|
|
|
# --- State & Properties ---
|
|
|
|
@property
|
|
def state(self) -> DriverState:
|
|
"""Return current driver state."""
|
|
return self._state
|
|
|
|
@property
|
|
def connected_peers(self) -> List[str]:
|
|
"""Return list of connected peer addresses."""
|
|
return self._connected_peers.copy()
|
|
|
|
# --- Core Actions ---
|
|
|
|
def start_scanning(self):
|
|
"""Start BLE scanning for nearby Reticulum nodes."""
|
|
try:
|
|
if not self.kotlin_bridge:
|
|
raise Exception("Driver not started")
|
|
|
|
self.kotlin_bridge.startScanningAsync()
|
|
|
|
self._state = DriverState.SCANNING
|
|
RNS.log("AndroidBLEDriver: Scanning started", RNS.LOG_DEBUG)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Failed to start scanning: {e}", RNS.LOG_ERROR)
|
|
if self.on_error:
|
|
self.on_error("error", f"Failed to start scanning: {e}", e)
|
|
|
|
def stop_scanning(self):
|
|
"""Stop BLE scanning."""
|
|
try:
|
|
if self.kotlin_bridge:
|
|
self.kotlin_bridge.stopScanningAsync()
|
|
|
|
if self._state == DriverState.SCANNING:
|
|
self._state = DriverState.IDLE
|
|
|
|
RNS.log("AndroidBLEDriver: Scanning stopped", RNS.LOG_DEBUG)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error stopping scan: {e}", RNS.LOG_ERROR)
|
|
|
|
def start_advertising(self, device_name: str, identity: bytes):
|
|
"""Start BLE advertising."""
|
|
try:
|
|
if not self.kotlin_bridge:
|
|
raise Exception("Driver not started")
|
|
|
|
self.kotlin_bridge.startAdvertisingAsync(device_name)
|
|
|
|
self._state = DriverState.ADVERTISING
|
|
RNS.log(f"AndroidBLEDriver: Advertising started as '{device_name}'", RNS.LOG_DEBUG)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Failed to start advertising: {e}", RNS.LOG_ERROR)
|
|
if self.on_error:
|
|
self.on_error("error", f"Failed to start advertising: {e}", e)
|
|
|
|
def stop_advertising(self):
|
|
"""Stop BLE advertising."""
|
|
try:
|
|
if self.kotlin_bridge:
|
|
self.kotlin_bridge.stopAdvertisingAsync()
|
|
|
|
if self._state == DriverState.ADVERTISING:
|
|
self._state = DriverState.IDLE
|
|
|
|
RNS.log("AndroidBLEDriver: Advertising stopped", RNS.LOG_DEBUG)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error stopping advertising: {e}", RNS.LOG_ERROR)
|
|
|
|
def should_connect(self, peer_address: str) -> bool:
|
|
"""Check if we should initiate connection based on MAC address sorting.
|
|
|
|
This implements the MAC sorting algorithm from the BLE protocol:
|
|
- Lower MAC address initiates connection (acts as central)
|
|
- Higher MAC address waits (acts as peripheral only)
|
|
|
|
This prevents dual connections by ensuring both devices independently
|
|
agree on connection direction.
|
|
|
|
NOTE: This method is exposed for BLEInterface to call BEFORE connect().
|
|
The MAC sorting decision has been moved to Python layer per RFC architecture.
|
|
|
|
Args:
|
|
peer_address: BLE MAC address of peer to potentially connect to
|
|
|
|
Returns:
|
|
True if we should connect (our MAC < peer MAC), False otherwise
|
|
"""
|
|
try:
|
|
if not self.kotlin_bridge:
|
|
RNS.log("AndroidBLEDriver: Cannot check shouldConnect - no bridge", RNS.LOG_WARNING)
|
|
return False
|
|
|
|
result = self.kotlin_bridge.shouldConnect(peer_address)
|
|
RNS.log(f"AndroidBLEDriver: shouldConnect({peer_address}) = {result}", RNS.LOG_DEBUG)
|
|
return bool(result)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error in shouldConnect: {e}", RNS.LOG_ERROR)
|
|
return False
|
|
|
|
def connect(self, address: str):
|
|
"""Connect to a peer device (central role).
|
|
|
|
NOTE: MAC sorting is no longer enforced by Kotlin. BLEInterface should
|
|
call should_connect() first if it wants to follow the MAC sorting protocol.
|
|
This method now connects unconditionally when called.
|
|
"""
|
|
try:
|
|
if not self.kotlin_bridge:
|
|
raise Exception("Driver not started")
|
|
|
|
self.kotlin_bridge.connectAsync(address)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Failed to connect to {address}: {e}", RNS.LOG_ERROR)
|
|
if self.on_error:
|
|
self.on_error("error", f"Failed to connect to {address}: {e}", e)
|
|
|
|
def disconnect(self, address: str):
|
|
"""Disconnect from a peer device (both central and peripheral connections)."""
|
|
try:
|
|
if self.kotlin_bridge:
|
|
# Kotlin bridge launches coroutines internally
|
|
self.kotlin_bridge.disconnectAsync(address)
|
|
|
|
RNS.log(f"AndroidBLEDriver: Disconnecting from {address}", RNS.LOG_DEBUG)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error disconnecting from {address}: {e}", RNS.LOG_ERROR)
|
|
|
|
def disconnect_central(self, address: str):
|
|
"""Disconnect our central connection TO a peer device.
|
|
|
|
Used by BLEInterface for deduplication when we want to keep the
|
|
peripheral connection (them connected to us) but close our
|
|
central connection (us connected to them).
|
|
"""
|
|
try:
|
|
if self.kotlin_bridge:
|
|
self.kotlin_bridge.disconnectCentralAsync(address)
|
|
RNS.log(f"AndroidBLEDriver: Disconnecting central connection to {address}", RNS.LOG_DEBUG)
|
|
else:
|
|
RNS.log("AndroidBLEDriver: Cannot disconnect central - no bridge", RNS.LOG_WARNING)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error disconnecting central from {address}: {e}", RNS.LOG_ERROR)
|
|
|
|
def disconnect_peripheral(self, address: str):
|
|
"""Disconnect a peripheral connection FROM a peer device.
|
|
|
|
Used by BLEInterface for deduplication when we want to keep our
|
|
central connection (us connected to them) but close the
|
|
peripheral connection (them connected to us).
|
|
"""
|
|
try:
|
|
if self.kotlin_bridge:
|
|
self.kotlin_bridge.disconnectPeripheralAsync(address)
|
|
RNS.log(f"AndroidBLEDriver: Disconnecting peripheral connection from {address}", RNS.LOG_DEBUG)
|
|
else:
|
|
RNS.log("AndroidBLEDriver: Cannot disconnect peripheral - no bridge", RNS.LOG_WARNING)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error disconnecting peripheral from {address}: {e}", RNS.LOG_ERROR)
|
|
|
|
def send(self, address: str, data: bytes):
|
|
"""Send data to a connected peer (data already fragmented by BLEInterface)."""
|
|
try:
|
|
if not self.kotlin_bridge:
|
|
raise Exception("Driver not started")
|
|
|
|
self.kotlin_bridge.sendAsync(address, data)
|
|
|
|
RNS.log(f"AndroidBLEDriver: Sent {len(data)} bytes to {address}", RNS.LOG_EXTREME)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Failed to send to {address}: {e}", RNS.LOG_ERROR)
|
|
if self.on_error:
|
|
self.on_error("error", f"Failed to send to {address}: {e}", e)
|
|
|
|
# --- GATT Characteristic Operations ---
|
|
|
|
def read_characteristic(self, address: str, char_uuid: str) -> bytes:
|
|
"""Read a GATT characteristic (not directly supported - identity via callback)."""
|
|
# This method is here for interface compatibility. In this driver,
|
|
# characteristic reads are handled by the Kotlin bridge, and the results
|
|
# (like the peer identity) are passed up via callbacks.
|
|
RNS.log(f"AndroidBLEDriver: read_characteristic is not implemented for direct use. Use callbacks instead.", RNS.LOG_WARNING)
|
|
return b""
|
|
|
|
def write_characteristic(self, address: str, char_uuid: str, data: bytes):
|
|
"""Write a GATT characteristic (use send() instead)."""
|
|
# This method is here for interface compatibility. In this driver,
|
|
# all data transmission is handled by the send() method, which abstracts
|
|
# away the specific characteristic writes.
|
|
RNS.log(f"AndroidBLEDriver: write_characteristic is not implemented for direct use. Use send() instead.", RNS.LOG_WARNING)
|
|
|
|
def start_notify(self, address: str, char_uuid: str, callback: Callable[[bytes], None]):
|
|
"""Subscribe to notifications (handled automatically by Kotlin bridge)."""
|
|
# Note: Notifications are automatically handled by KotlinBLEBridge
|
|
# Data comes through on_data_received callback
|
|
RNS.log(f"AndroidBLEDriver: start_notify not needed (automatic in bridge)", RNS.LOG_DEBUG)
|
|
|
|
# --- Configuration & Queries ---
|
|
|
|
def get_local_address(self) -> str:
|
|
"""Get the local Bluetooth adapter address."""
|
|
# WARNING: Android privacy features prevent apps from accessing the real
|
|
# Bluetooth MAC address. Returning a placeholder. This could have
|
|
# unforeseen consequences in the ble-reticulum logic, which might rely
|
|
# on a real address for tie-breaking or other functions.
|
|
return "00:00:00:00:00:00" # Placeholder
|
|
|
|
def get_peer_role(self, address: str) -> Optional[str]:
|
|
"""Get the connection role for a peer ('central' or 'peripheral')."""
|
|
return self._peer_roles.get(address)
|
|
|
|
def set_service_discovery_delay(self, seconds: float):
|
|
"""Set delay between connection and service discovery (not needed on Android)."""
|
|
self._service_discovery_delay = seconds
|
|
RNS.log(f"AndroidBLEDriver: Service discovery delay set to {seconds}s (ignored on Android)", RNS.LOG_DEBUG)
|
|
|
|
def set_power_mode(self, mode: str):
|
|
"""Set power mode for scanning ('aggressive', 'balanced', 'saver')."""
|
|
if mode not in ["aggressive", "balanced", "saver"]:
|
|
raise ValueError(f"Invalid power mode: {mode}")
|
|
|
|
self._power_mode = mode
|
|
RNS.log(f"AndroidBLEDriver: Power mode set to {mode}", RNS.LOG_DEBUG)
|
|
# Note: Could propagate to KotlinBLEBridge if needed
|
|
|
|
def get_peer_mtu(self, address: str) -> Optional[int]:
|
|
"""Get the negotiated MTU for a peer."""
|
|
return self._peer_mtus.get(address)
|
|
|
|
def ensure_advertising(self) -> bool:
|
|
"""Ensure advertising is active, restarting if silently stopped.
|
|
|
|
Android may silently stop BLE advertising when:
|
|
- App goes to background
|
|
- Screen turns off
|
|
- Device enters Doze mode
|
|
|
|
This method checks if advertising is active and restarts it if needed.
|
|
|
|
Returns:
|
|
True if advertising was already active, False if restart was triggered
|
|
"""
|
|
try:
|
|
if self.kotlin_bridge is None:
|
|
RNS.log("AndroidBLEDriver: Cannot ensure advertising - no bridge", RNS.LOG_WARNING)
|
|
return False
|
|
|
|
result = self.kotlin_bridge.ensureAdvertising()
|
|
if not result:
|
|
RNS.log("AndroidBLEDriver: Advertising was stopped, restarting...", RNS.LOG_INFO)
|
|
return bool(result)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error ensuring advertising: {e}", RNS.LOG_ERROR)
|
|
return False
|
|
|
|
def request_identity_resync(self, address: str) -> bool:
|
|
"""Request identity resync from Kotlin for a peer.
|
|
|
|
Called by BLEInterface when it receives data from a peer but has no
|
|
identity mapping. This can happen if Python's disconnect callback
|
|
fired but Kotlin maintained the GATT connection.
|
|
|
|
Args:
|
|
address: BLE MAC address of the peer
|
|
|
|
Returns:
|
|
True if identity was found and callback will be fired, False otherwise
|
|
"""
|
|
try:
|
|
if self.kotlin_bridge is None:
|
|
RNS.log("AndroidBLEDriver: Cannot resync identity - no bridge", RNS.LOG_WARNING)
|
|
return False
|
|
|
|
result = self.kotlin_bridge.requestIdentityResync(address)
|
|
RNS.log(f"AndroidBLEDriver: Identity resync for {address}: {'found' if result else 'not found'}", RNS.LOG_DEBUG)
|
|
return bool(result)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error requesting identity resync for {address}: {e}", RNS.LOG_ERROR)
|
|
return False
|
|
|
|
# --- Internal Methods ---
|
|
|
|
def _get_kotlin_bridge(self):
|
|
"""Get KotlinBLEBridge instance from global wrapper."""
|
|
try:
|
|
# Import the global wrapper instance
|
|
import reticulum_wrapper
|
|
wrapper = reticulum_wrapper._global_wrapper_instance
|
|
|
|
if wrapper is None:
|
|
RNS.log("AndroidBLEDriver: No global wrapper instance found", RNS.LOG_ERROR)
|
|
return None
|
|
|
|
if wrapper.kotlin_ble_bridge is None:
|
|
RNS.log("AndroidBLEDriver: No BLE bridge set in wrapper. Call set_ble_bridge() from Kotlin first.", RNS.LOG_ERROR)
|
|
return None
|
|
|
|
RNS.log("AndroidBLEDriver: Kotlin bridge acquired from wrapper", RNS.LOG_DEBUG)
|
|
return wrapper.kotlin_ble_bridge
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Failed to get Kotlin bridge: {e}", RNS.LOG_ERROR)
|
|
import traceback
|
|
RNS.log(traceback.format_exc(), RNS.LOG_ERROR)
|
|
return None
|
|
|
|
def _setup_kotlin_callbacks(self):
|
|
"""Setup callbacks from Kotlin to Python."""
|
|
if not self.kotlin_bridge:
|
|
return
|
|
|
|
self.kotlin_bridge.setOnDeviceDiscovered(lambda address, name, rssi, service_uuids: self._handle_device_discovered(address, name, rssi, service_uuids))
|
|
# Accept 4 params: address, mtu, role, identity_hash (identity_hash may be None for Protocol v1 or peripheral)
|
|
self.kotlin_bridge.setOnConnected(lambda address, mtu, role, identity_hash=None: self._handle_connected(address, mtu, role, identity_hash))
|
|
self.kotlin_bridge.setOnDisconnected(lambda address: self._handle_disconnected(address))
|
|
self.kotlin_bridge.setOnDataReceived(lambda address, data: self._handle_data_received(address, data))
|
|
self.kotlin_bridge.setOnIdentityReceived(lambda address, identity_hash: self._handle_identity_received(address, identity_hash))
|
|
self.kotlin_bridge.setOnMtuNegotiated(lambda address, mtu: self._handle_mtu_negotiated(address, mtu))
|
|
self.kotlin_bridge.setOnAddressChanged(lambda old_addr, new_addr, identity_hash: self._handle_address_changed(old_addr, new_addr, identity_hash))
|
|
|
|
RNS.log("AndroidBLEDriver: Kotlin callbacks configured", RNS.LOG_DEBUG)
|
|
|
|
def _handle_device_discovered(self, address: str, name: Optional[str], rssi: int, service_uuids: Optional[List[str]]):
|
|
"""Handle device discovered event from Kotlin."""
|
|
try:
|
|
# service_uuids arrives as proper Python list (converted from Kotlin Array)
|
|
device = BLEDevice(
|
|
address=address,
|
|
name=name or "Unknown",
|
|
rssi=rssi,
|
|
service_uuids=service_uuids or []
|
|
)
|
|
|
|
if self.on_device_discovered:
|
|
self.on_device_discovered(device)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error handling device discovered: {e}", RNS.LOG_ERROR)
|
|
|
|
def _handle_connected(self, address: str, mtu: int, role: str = "central", identity_hash: Optional[str] = None):
|
|
"""Handle peer connected event from Kotlin.
|
|
|
|
Args:
|
|
address: Peer MAC address
|
|
mtu: Negotiated MTU
|
|
role: Connection role ("central" or "peripheral")
|
|
identity_hash: 32-char hex identity string (Protocol v2.2), or None for Protocol v1/peripheral
|
|
"""
|
|
try:
|
|
# DEBUG: Log callback entry for tracking callback chain
|
|
RNS.log(f"AndroidBLEDriver: [CALLBACK] _handle_connected ENTRY: address={address}, mtu={mtu}, role={role}, identity_hash={identity_hash[:16] if identity_hash else 'None'}...", RNS.LOG_WARNING)
|
|
|
|
if address not in self._connected_peers:
|
|
self._connected_peers.append(address)
|
|
|
|
# Set role from parameter (passed from Kotlin)
|
|
self._peer_roles[address] = role
|
|
|
|
RNS.log(f"AndroidBLEDriver: Connected to {address} (MTU={mtu}, role={role}, identity={'present' if identity_hash else 'pending'})", RNS.LOG_INFO)
|
|
|
|
# Use identity from callback (passed directly to avoid race condition with onIdentityReceived)
|
|
# Fall back to _pending_identities for backwards compatibility
|
|
identity = None
|
|
if identity_hash:
|
|
try:
|
|
identity = bytes.fromhex(identity_hash)
|
|
RNS.log(f"AndroidBLEDriver: Using identity from onConnected callback: {identity_hash[:16]}...", RNS.LOG_DEBUG)
|
|
except ValueError as e:
|
|
RNS.log(f"AndroidBLEDriver: Invalid identity_hash format: {e}", RNS.LOG_WARNING)
|
|
|
|
if identity is None:
|
|
# Fall back to pending identities (from earlier onIdentityReceived if it arrived first)
|
|
# Use lock to prevent race condition with _handle_identity_received
|
|
with self._identity_lock:
|
|
identity = self._pending_identities.pop(address, None)
|
|
if identity:
|
|
RNS.log(f"AndroidBLEDriver: Using identity from pending cache", RNS.LOG_DEBUG)
|
|
|
|
# Track identity-to-address mapping for debugging/logging purposes only
|
|
# Deduplication is handled by BLEInterface, not the driver
|
|
# In dual-mode BLE, the same identity may legitimately have two addresses
|
|
# (one for central connection, one for peripheral connection)
|
|
if identity:
|
|
identity_hex = identity.hex()
|
|
with self._identity_lock:
|
|
existing_address = self._identity_to_address.get(identity_hex)
|
|
if existing_address and existing_address != address:
|
|
# Same identity at different address - log but don't disconnect
|
|
# This is normal in dual-mode BLE or during MAC rotation
|
|
RNS.log(
|
|
f"AndroidBLEDriver: Identity {identity_hex[:16]}... also at {address} (was at {existing_address})",
|
|
RNS.LOG_DEBUG
|
|
)
|
|
# Update mappings - we track the most recent address per identity
|
|
self._identity_to_address[identity_hex] = address
|
|
self._address_to_identity[address] = identity_hex
|
|
|
|
# Notify BLEInterface of connection (regardless of role)
|
|
# Dual-connection prevention is handled by KotlinBLEBridge (lines 954-959)
|
|
# If this callback fires, there's only ONE connection type (central XOR peripheral)
|
|
if self.on_device_connected:
|
|
# Call on_device_connected with identity if available (Protocol v2.2)
|
|
# or None if no identity (Protocol v1 device, or peripheral pending handshake)
|
|
self.on_device_connected(address, identity)
|
|
if identity:
|
|
RNS.log(f"AndroidBLEDriver: Notified {role} connection with identity for {address}", RNS.LOG_DEBUG)
|
|
else:
|
|
RNS.log(f"AndroidBLEDriver: Notified {role} connection without identity for {address} (will receive via handshake)", RNS.LOG_DEBUG)
|
|
|
|
# Report MTU negotiation
|
|
if self.on_mtu_negotiated:
|
|
self.on_mtu_negotiated(address, mtu)
|
|
|
|
# DEBUG: Log callback completion
|
|
RNS.log(f"AndroidBLEDriver: [CALLBACK] _handle_connected EXIT: address={address}, called on_device_connected={self.on_device_connected is not None}, called on_mtu_negotiated={self.on_mtu_negotiated is not None}", RNS.LOG_WARNING)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error handling connected: {e}", RNS.LOG_ERROR)
|
|
|
|
def _handle_disconnected(self, address: str):
|
|
"""Handle peer disconnected event from Kotlin."""
|
|
try:
|
|
if address in self._connected_peers:
|
|
self._connected_peers.remove(address)
|
|
if address in self._peer_roles:
|
|
del self._peer_roles[address]
|
|
if address in self._peer_mtus:
|
|
del self._peer_mtus[address]
|
|
|
|
# Clean up identity mappings for this address
|
|
with self._identity_lock:
|
|
identity_hex = self._address_to_identity.pop(address, None)
|
|
if identity_hex:
|
|
# Only remove identity->address mapping if it still points to this address
|
|
# (might have been updated to a new address during MAC rotation)
|
|
if self._identity_to_address.get(identity_hex) == address:
|
|
del self._identity_to_address[identity_hex]
|
|
RNS.log(f"AndroidBLEDriver: Cleared identity mapping for {address} ({identity_hex[:16]}...)", RNS.LOG_DEBUG)
|
|
|
|
RNS.log(f"AndroidBLEDriver: Disconnected from {address}", RNS.LOG_INFO)
|
|
|
|
if self.on_device_disconnected:
|
|
self.on_device_disconnected(address)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error handling disconnected: {e}", RNS.LOG_ERROR)
|
|
|
|
def _handle_address_changed(self, old_address: str, new_address: str, identity_hash: str):
|
|
"""Handle address change event from Kotlin during dual connection deduplication.
|
|
|
|
When Kotlin deduplicates a dual connection (same identity connected as both
|
|
central and peripheral), it closes one direction and notifies Python via
|
|
this callback so Python can update its 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
|
|
"""
|
|
try:
|
|
RNS.log(
|
|
f"AndroidBLEDriver: Address changed for {identity_hash[:8]}: {old_address} -> {new_address}",
|
|
RNS.LOG_INFO
|
|
)
|
|
|
|
if self.on_address_changed:
|
|
self.on_address_changed(old_address, new_address, identity_hash)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error handling address changed: {e}", RNS.LOG_ERROR)
|
|
|
|
def _handle_data_received(self, address: str, data: bytes):
|
|
"""Handle data received event from Kotlin (already defragmented).
|
|
|
|
Note: Due to Android MAC randomization, data may arrive from an address
|
|
that has a pending identity but never received onConnected. This happens
|
|
when the same peer identity is seen from different MACs. In this case,
|
|
we finalize the connection using the pending identity before passing data.
|
|
"""
|
|
try:
|
|
RNS.log(f"AndroidBLEDriver: Received {len(data)} bytes from {address}", RNS.LOG_EXTREME)
|
|
|
|
# Check if this address has a pending identity but never got onConnected
|
|
# This handles the case where Android MAC randomization causes identity
|
|
# to arrive from one MAC but onConnected for a different MAC with same identity
|
|
if address not in self._connected_peers:
|
|
with self._identity_lock:
|
|
pending_identity = self._pending_identities.get(address)
|
|
if pending_identity:
|
|
# Finalize connection with pending identity
|
|
RNS.log(f"AndroidBLEDriver: Finalizing connection for {address} from pending identity (data arrived first)", RNS.LOG_DEBUG)
|
|
self._connected_peers.append(address)
|
|
self._peer_roles[address] = "peripheral" # Data arriving = peripheral role
|
|
# Remove from pending before callback to prevent double-use
|
|
del self._pending_identities[address]
|
|
|
|
# Call on_device_connected to create identity mappings
|
|
if self.on_device_connected:
|
|
self.on_device_connected(address, pending_identity)
|
|
|
|
# IMPORTANT: Also call on_mtu_negotiated to create reassembler
|
|
# Without this, BLEInterface has identity but no reassembler
|
|
mtu = self._peer_mtus.get(address, 23) # Default to BLE 4.0 minimum
|
|
if self.on_mtu_negotiated:
|
|
self.on_mtu_negotiated(address, mtu)
|
|
|
|
if self.on_data_received:
|
|
self.on_data_received(address, data)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error handling data received: {e}", RNS.LOG_ERROR)
|
|
|
|
def _handle_identity_received(self, address: str, identity_hash: str):
|
|
"""Handle identity received event from Kotlin (Protocol v2.2).
|
|
|
|
This is called when the central's identity is received via the GATT identity
|
|
characteristic (for peripheral mode). For central mode, identity is passed
|
|
directly in the onConnected callback, so this should not be called.
|
|
|
|
IMPORTANT: For peripheral connections, this callback may arrive AFTER
|
|
onConnected has already fired. In that case, we must notify BLEInterface
|
|
of the identity so it can spawn the peer interface.
|
|
|
|
Thread safety: Uses _identity_lock to prevent race conditions with
|
|
_handle_connected when identity and connection callbacks interleave.
|
|
"""
|
|
try:
|
|
# DEBUG: Log callback entry
|
|
RNS.log(f"AndroidBLEDriver: [CALLBACK] _handle_identity_received ENTRY: address={address}, identity={identity_hash[:16]}..., already_connected={address in self._connected_peers}", RNS.LOG_WARNING)
|
|
|
|
RNS.log(f"AndroidBLEDriver: Identity received from {address}: {identity_hash[:16]}...",
|
|
RNS.LOG_INFO)
|
|
|
|
# Convert hex string to bytes (16 bytes = 32 hex chars)
|
|
identity_bytes = bytes.fromhex(identity_hash)
|
|
|
|
# Use lock to prevent race condition with _handle_connected
|
|
with self._identity_lock:
|
|
# Track identity-to-address mapping for debugging/logging purposes only
|
|
# Deduplication is handled by BLEInterface, not the driver
|
|
# In dual-mode BLE, the same identity may legitimately have two addresses
|
|
# (one for central connection, one for peripheral connection)
|
|
existing_address = self._identity_to_address.get(identity_hash)
|
|
if existing_address and existing_address != address:
|
|
# Same identity at different address - log but don't disconnect
|
|
# This is normal in dual-mode BLE or during MAC rotation
|
|
RNS.log(
|
|
f"AndroidBLEDriver: Identity {identity_hash[:16]}... also at {address} (was at {existing_address})",
|
|
RNS.LOG_DEBUG
|
|
)
|
|
|
|
# Update identity mappings
|
|
self._identity_to_address[identity_hash] = address
|
|
self._address_to_identity[address] = identity_hash
|
|
|
|
# Check if peer is already connected (common case for peripheral mode)
|
|
# where onConnected fires before identity is received
|
|
if address in self._connected_peers:
|
|
# Peer is already connected - notify BLEInterface immediately
|
|
# This allows BLEInterface to spawn the peer interface with identity
|
|
RNS.log(f"AndroidBLEDriver: Late identity for connected peer {address}, notifying BLEInterface", RNS.LOG_DEBUG)
|
|
if self.on_device_connected:
|
|
self.on_device_connected(address, identity_bytes)
|
|
RNS.log(f"AndroidBLEDriver: [CALLBACK] _handle_identity_received: Called on_device_connected for late identity at {address}", RNS.LOG_WARNING)
|
|
else:
|
|
# Peer not yet connected - cache identity for when onConnected fires
|
|
# This handles the race where identity arrives before connection
|
|
self._pending_identities[address] = identity_bytes
|
|
RNS.log(f"AndroidBLEDriver: Cached identity for {address}, waiting for connection complete", RNS.LOG_DEBUG)
|
|
RNS.log(f"AndroidBLEDriver: [CALLBACK] _handle_identity_received: Cached identity (peer not connected yet) for {address}", RNS.LOG_WARNING)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error handling identity received: {e}", RNS.LOG_ERROR)
|
|
|
|
def _handle_mtu_negotiated(self, address: str, mtu: int):
|
|
"""Handle MTU negotiation completion from Kotlin."""
|
|
try:
|
|
# DEBUG: Log callback entry
|
|
RNS.log(f"AndroidBLEDriver: [CALLBACK] _handle_mtu_negotiated ENTRY: address={address}, mtu={mtu}", RNS.LOG_WARNING)
|
|
|
|
RNS.log(f"AndroidBLEDriver: MTU negotiated with {address}: {mtu}", RNS.LOG_INFO)
|
|
|
|
# Store MTU for this peer
|
|
self._peer_mtus[address] = mtu
|
|
|
|
if self.on_mtu_negotiated:
|
|
self.on_mtu_negotiated(address, mtu)
|
|
RNS.log(f"AndroidBLEDriver: [CALLBACK] _handle_mtu_negotiated: Called on_mtu_negotiated for {address}", RNS.LOG_WARNING)
|
|
|
|
except Exception as e:
|
|
RNS.log(f"AndroidBLEDriver: Error handling MTU negotiated: {e}", RNS.LOG_ERROR)
|