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>
198 lines
6.4 KiB
Python
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
|