ble-reticulum/bluetooth_driver.py
torlando-tech c04dbca7cd Refactor BLEInterface to driver-based architecture
Major architectural refactoring to separate high-level Reticulum protocol
logic from platform-specific Bluetooth operations. This enables code sharing
between pure Python and Android (Columba) implementations, improves
testability, and creates a clean boundary for future platform support.

ARCHITECTURE CHANGES:

1. **Driver Abstraction Layer**
   - Created BLEDriverInterface (bluetooth_driver.py) defining the contract
     for all platform-specific BLE drivers
   - Abstraction includes 18 methods + 6 callbacks for complete BLE lifecycle
   - Enhanced BLEDevice dataclass with service_uuids and manufacturer_data
   - Added on_mtu_negotiated callback for delayed MTU reporting
   - Added on_error callback for consistent platform error reporting

2. **Linux Driver Implementation**
   - Created LinuxBluetoothDriver (linux_bluetooth_driver.py, 1534 lines)
   - Moved ALL bleak/bluezero/D-Bus code from BLEInterface
   - Preserves 5 critical platform workarounds:
     * BlueZ ServicesResolved race condition patch
     * D-Bus LE-only connection (ConnectDevice)
     * BLE Agent registration for Just Works pairing
     * MTU negotiation with 3-method fallback
     * Service discovery delay for bluezero timing
   - Role-aware send() automatically chooses GATT write vs notification
   - Dedicated asyncio event loop management in separate thread
   - Configuration via constructor (no Reticulum dependencies)

3. **Refactored BLEInterface**
   - Removed 801 lines (32.3% reduction: 2479 → 1678 lines)
   - Removed all platform-specific imports (bleak, bluezero, dbus_fast)
   - Removed 9 async methods (moved to driver)
   - Driver dependency injection via constructor
   - Implemented 6 driver callbacks for event handling
   - PRESERVED high-level logic:
     * Peer scoring algorithm (RSSI + history + recency)
     * Connection blacklist with exponential backoff
     * MAC-based connection direction (prevents dual connections)
     * Fragmentation/reassembly orchestration (identity-based keying)
     * Interface spawning per peer

4. **Simplified BLEPeerInterface**
   - Removed connection_type, client, mtu parameters
   - Deleted _send_via_central() and _send_via_peripheral() methods
   - Single send path via driver.send() (driver handles role routing)
   - 77 lines removed from peer interface class

5. **Mock Driver for Testing**
   - Created MockBLEDriver (tests/mock_ble_driver.py)
   - Complete BLEDriverInterface implementation without hardware
   - Bidirectional communication via link_drivers()
   - Enables unit testing of BLEInterface logic (fragmentation, reassembly,
     peer lifecycle, blacklist management)

CRITICAL FIXES:

1. **Restored Periodic Cleanup Task** (CRITICAL: prevents memory leaks)
   - Converted from async (driver-owned loop) to threading.Timer
   - Runs every 30 seconds to clean stale reassembly buffers
   - Essential for long-running instances (Pi Zero with 512MB RAM)
   - Properly cancelled in detach() for clean shutdown

2. **Fixed Naming Consistency**
   - Renamed processOutgoing → process_outgoing (snake_case)

FILES MODIFIED:
- src/RNS/Interfaces/BLEInterface.py (refactored, -801 lines)

FILES ADDED:
- bluetooth_driver.py (driver abstraction interface)
- linux_bluetooth_driver.py (Linux/BlueZ implementation, 1534 lines)
- tests/mock_ble_driver.py (mock driver for unit tests)
- REFACTORING_GUIDE.md (comprehensive refactoring documentation)
- BLE_PROTOCOL_v2.2.md (protocol specification)
- tests/test_refactor_suite.py (initial test suite)

BENEFITS:

1. **Testability** - Mock driver enables hardware-free unit testing
2. **Portability** - Easy to create Android/Windows/macOS drivers
3. **Maintainability** - Platform quirks isolated in single driver file
4. **Code Sharing** - High-level logic shared across all platforms
5. **Clean Architecture** - Clear separation of concerns

TESTING REQUIRED:

- Tier 1 (Unit): Test with MockBLEDriver (fragmentation, reassembly, lifecycle)
- Tier 2 (Integration): Test on Raspberry Pi hardware (scanning, connecting,
  dual mode, MTU negotiation, identity exchange)
- Tier 3 (Regression): Full Reticulum stack (announces, LXMF, multi-hop)
- Tier 4 (Edge Cases): MAC rotation, identity handshake, reconnection,
  reassembly timeout, discovery cache pruning

BACKWARD COMPATIBILITY:

- Configuration: Fully backward compatible (same config parameters)
- Protocol: No changes to BLE wire protocol (v2.2)
- Interface API: Unchanged for Reticulum Transport integration

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 23:03:54 -05:00

198 lines
6.4 KiB
Python

from abc import ABC, abstractmethod
from typing import List, Optional, Callable, Dict
from enum import Enum, auto
from dataclasses import dataclass, field
# --- Data Structures ---
@dataclass
class BLEDevice:
"""Represents a discovered BLE device."""
address: str
name: str
rssi: int
service_uuids: List[str] = field(default_factory=list)
manufacturer_data: Dict[int, bytes] = field(default_factory=dict)
class DriverState(Enum):
"""Represents the state of the BLE driver."""
IDLE = auto()
SCANNING = auto()
ADVERTISING = auto()
# Note: More granular states like CONNECTING could be added if the
# high-level logic requires them, but the list of connected peers
# might be sufficient for most use cases.
# --- Driver Interface ---
class BLEDriverInterface(ABC):
"""
Abstract interface for a platform-specific BLE driver.
This contract separates the high-level Reticulum BLE interface logic
from the low-level, platform-specific Bluetooth operations. It is designed
to be implemented by different backend libraries (e.g., bleak/bluezero on Linux,
or a Chaquopy-bridged Kotlin implementation on Android).
The driver is responsible for managing the actual BLE connections, but it
reports events asynchronously via the provided callbacks.
"""
# --- Callbacks ---
# The consumer of this driver (e.g., a high-level BLEInterface) must
# 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_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
on_error: Optional[Callable[[str, str, Optional[Exception]], None]] = None # severity, message, exception
# --- Lifecycle & Configuration ---
@abstractmethod
def start(self, service_uuid: str, rx_char_uuid: str, tx_char_uuid: str, identity_char_uuid: str):
"""
Initializes the driver and its underlying BLE stack. This includes
setting up the GATT server characteristics required for the peripheral role.
This method should be called before any other operations.
"""
pass
@abstractmethod
def stop(self):
"""
Stops all BLE activity (scanning, advertising, connections) and releases all
underlying system resources.
"""
pass
@abstractmethod
def set_identity(self, identity_bytes: bytes):
"""
Sets the value of the read-only Identity characteristic for the local GATT server.
This must be called before starting advertising.
"""
pass
# --- State & Properties ---
@property
@abstractmethod
def state(self) -> DriverState:
"""Returns the current operational state of the driver."""
pass
@property
@abstractmethod
def connected_peers(self) -> List[str]:
"""Returns a list of MAC addresses for all currently connected peers."""
pass
# --- Core Actions ---
@abstractmethod
def start_scanning(self):
"""
Starts scanning for devices advertising the configured service UUID.
Discovered devices will be reported via the on_device_discovered callback.
"""
pass
@abstractmethod
def stop_scanning(self):
"""Stops scanning for devices."""
pass
@abstractmethod
def start_advertising(self, device_name: str, identity: bytes):
"""
Starts advertising the configured service UUID and the given device name.
The identity parameter is used to populate the Identity characteristic.
"""
pass
@abstractmethod
def stop_advertising(self):
"""Stops advertising."""
pass
@abstractmethod
def connect(self, address: str):
"""
Initiates a connection to a peer device (central role).
Connection status is reported via on_device_connected/on_device_disconnected.
"""
pass
@abstractmethod
def disconnect(self, address: str):
"""Disconnects from a peer device."""
pass
@abstractmethod
def send(self, address: str, data: bytes):
"""
Sends data to a connected peer.
The driver implementation is responsible for choosing the correct underlying BLE
operation (GATT Write for central role, or Notification for peripheral role)
based on the current connection type for the given address. This method
should ideally block or be awaitable until the send operation is confirmed
by the BLE stack to ensure sequential transmission.
"""
pass
# --- GATT Characteristic Operations ---
@abstractmethod
def read_characteristic(self, address: str, char_uuid: str) -> bytes:
"""
Reads a GATT characteristic value from a connected peer.
Raises an exception if the operation fails.
"""
pass
@abstractmethod
def write_characteristic(self, address: str, char_uuid: str, data: bytes):
"""
Writes a value to a GATT characteristic on a connected peer.
Raises an exception if the operation fails.
"""
pass
@abstractmethod
def start_notify(self, address: str, char_uuid: str, callback: Callable[[bytes], None]):
"""
Subscribes to notifications from a GATT characteristic on a connected peer.
The callback will be invoked whenever a notification is received.
"""
pass
# --- Configuration & Queries ---
@abstractmethod
def get_local_address(self) -> str:
"""
Returns the MAC address of the local Bluetooth adapter.
Used for connection direction determination (MAC sorting).
"""
pass
@abstractmethod
def set_service_discovery_delay(self, seconds: float):
"""
Sets the delay between connection establishment and service discovery.
This is a workaround for bluezero D-Bus registration timing issues.
"""
pass
@abstractmethod
def set_power_mode(self, mode: str):
"""
Sets the power mode for scanning operations.
Valid modes: "aggressive", "balanced", "saver"
"""
pass