## Problem
After the driver refactor (commit d1d94e5), peripheral devices were dropping
the identity handshake sent by central devices. The logs showed:
```
[Warning] BLEInterface[BLE Interface] no identity for dev:B8:27:EB:A8:A7:22, cannot create fragmenter
[Warning] BLEInterface[BLE Interface] no identity for peer dev:B8:27:EB:A8:A7:22, dropping data
```
Root cause: When the central sends its 16-byte identity handshake, the
peripheral's `_data_received_callback` passed it to `_handle_ble_data`,
which immediately dropped it (chicken-and-egg: no identity = drop data,
but the dropped data IS the identity).
The handshake detection logic existed in commit babb237 but was lost
during the driver architecture refactor.
## Solution
Added `_handle_identity_handshake()` method that:
1. Detects identity handshakes (exactly 16 bytes, no existing identity)
2. Stores the central's identity in bidirectional mappings
3. Creates fragmenter/reassembler with negotiated MTU
4. Spawns peer interface for the central
5. Returns True to prevent normal data processing
Updated `_data_received_callback()` to check for handshakes before
passing data to normal reassembly logic.
## Benefits
- ✅ Restores bidirectional communication for peripheral connections
- ✅ Peripheral can learn central's identity without scanning
- ✅ Clean separation of handshake vs. data processing
- ✅ Proper error handling with informative logging
## Testing
Should resolve the asymmetric identity exchange seen in Pi1/Pi2 logs where
central successfully connected but peripheral couldn't create fragmenter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
## 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 addresses a timeout issue where a central device would fail
to read the identity characteristic from the peripheral.
The root cause is suspected to be a race condition in the underlying
BlueZ/D-Bus stack, where the `read_callback` for the characteristic
was not firing reliably, causing the central's read request to hang
and time out.
To make this process more robust and less dependent on timing, the
GATT server implementation has been hardened:
1. The identity characteristic is now initialized with a 16-byte
placeholder value. This ensures the D-Bus object is created with
the correct data length from the start.
2. When the asynchronous RNS identity becomes available, the server now
proactively pushes the identity to the characteristic using
`set_value()`. This no longer relies exclusively on the fragile
`read_callback` mechanism.
Additionally, error logging within the driver has been improved to
include the exception type, aiding future diagnostics.
This commit addresses two critical issues that prevented the BLE
interface from functioning correctly after the driver abstraction
refactor.
1. **Fix `exec()` Startup Error:**
The interface failed to load via `rnsd` due to a `KeyError: '__name__'`
caused by using relative imports (`from . import ...`). The `exec()`
environment used by Reticulum does not preserve package context,
breaking these imports. This is fixed by reverting to absolute
imports (`from bluetooth_driver import ...`) which work correctly
with the existing `sys.path` manipulation logic.
2. **Fix Connection Role Logic:**
Connections were failing because the interface would always attempt
to read the peer's identity, even when acting as the peripheral.
This caused a `Can only read characteristics in central mode` error.
The fix introduces role-aware logic into the connection callback:
- A `get_peer_role()` method was added to the driver interface.
- `BLEInterface` now checks the role on connection.
- If central, it reads the identity characteristic.
- If peripheral, it waits for the identity handshake packet,
preventing the invalid operation.
Implement automatic device name generation from Transport.identity hash
to enable reliable peer discovery when bluezero service_uuid exposure
is unreliable.
Changes:
- Auto-generate device_name as RNS-{32-hex-identity} if not configured
- Parse peer identity from device name pattern (RNS-[0-9a-f]{32})
- Update GATT server device_name before advertising
- Store parsed identities in address_to_identity mapping
Limitations discovered:
- bluezero Peripheral uses system hostname for BLE local_name, not
the device_name parameter we set
- BlueZ D-Bus cache issues cause service_uuid exposure to be unreliable
- Reboot + cache clear (/var/lib/bluetooth/*/cache) temporarily fixes
service_uuid visibility
Current status:
- Bidirectional discovery works via service_uuid after fresh reboot
- Identity parsing infrastructure ready for future manufacturer_data approach
- Fallback to Protocol v1 address-based tracking remains functional
Tested on Raspberry Pi 4 with BlueZ 5.76, bluezero 0.9.1, bleak 1.1.1
Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enable BLE peripheral connections to send data by creating fragmenters
in handle_peripheral_data() after identity handshake. Previously,
fragmenters were only created for central connections (_connect_to_peer),
which caused "No fragmenter for peer" warnings when peripheral-only
connections attempted to transmit data.
This fix ensures bidirectional data flow works correctly regardless of
which device initiates the BLE connection, completing the unified
interface architecture.
Impact: Fixes announce rebroadcasting from peripheral-only connections
and enables full mesh networking over BLE.
Tested on Raspberry Pi 4 with BlueZ 5.76.
Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Critical fix for message delivery and Android MAC rotation support.
**Problem:**
- Fragmenters keyed by MAC address
- Failed with "dev:" prefix mismatch
- Would break on Android MAC rotation
**Solution:**
Use identity_hash for fragmenter/reassembler keys (with Protocol v1 MAC fallback).
**Changes:**
1. Added _get_fragmenter_key() helper - returns identity_hash or normalized MAC
2. Updated _connect_to_peer() - creates fragmenters with identity keys
3. Updated BLEPeerInterface.processOutgoing() - looks up fragmenters with identity keys
**Benefits:**
- ✅ Fixes immediate "No fragmenter" bug
- ✅ Survives Android MAC address rotation
- ✅ Consistent with unified interface architecture
- ✅ One fragmenter per peer identity (not per ephemeral MAC)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enhances BLE Protocol v2 handshake to include the central's identity (16 bytes)
instead of empty bytes. This enables the peripheral side to create identity-based
unified interfaces even without discovering the central via scanning.
**Problem Solved:**
- Peripheral couldn't create identity-based interface without scanning the central
- Resulted in separate "legacy" and identity-based interfaces for same peer
- Prevented true interface unification in asymmetric discovery scenarios
**Solution:**
1. Central sends its own identity (16 bytes) in handshake write
2. Peripheral detects identity handshake (16 bytes, first write)
3. Peripheral extracts identity and migrates interface from legacy to identity-based
4. Both sides now have identity-based interfaces that can unify!
**Changes:**
**_connect_to_peer() (line 1487):**
```python
# OLD: await client.write_gatt_char(RX_UUID, b'', response=True)
# NEW: Send our own identity in handshake
our_identity = self.gatt_server.identity_value if self.gatt_server else b'\x00' * 16
await client.write_gatt_char(RX_UUID, our_identity, response=True)
```
**handle_peripheral_data() (line 1792):**
```python
# Detect identity handshake (16 bytes, first write)
if len(data) == 16 and sender_address not in self.address_to_identity:
central_identity = bytes(data)
central_identity_hash = RNS.Identity.full_hash(central_identity)[:16].hex()[:16]
# Store identity mapping
self.address_to_identity[sender_address] = central_identity
self.identity_to_address[central_identity_hash] = sender_address
# Migrate interface from legacy to identity-based tracking
legacy_conn_id = f"{sender_address}-peripheral"
if legacy_conn_id in self.spawned_interfaces:
legacy_if = self.spawned_interfaces[legacy_conn_id]
del self.spawned_interfaces[legacy_conn_id]
legacy_if.peer_identity = central_identity
self.spawned_interfaces[central_identity_hash] = legacy_if
return # Don't process handshake as fragment data
```
**Flow:**
1. Pi1 connects to Pi2 as central
2. Pi1 reads Pi2's identity → creates identity-based interface
3. Pi1 sends handshake WITH Pi1's identity
4. Pi2 receives handshake, extracts Pi1's identity
5. Pi2 migrates interface to identity-based tracking
6. When Pi2 later discovers Pi1, adds central connection to SAME interface
7. Result: Both Pis have unified "central+peripheral" interfaces!
**Benefits:**
- ✅ Works with asymmetric discovery (only one side scans)
- ✅ Enables true unified interfaces in all scenarios
- ✅ Solves Android backgrounding (peripheral gets central's identity immediately)
- ✅ Faster interface unification (don't wait for bidirectional discovery)
**Backward Compatibility:**
- Protocol v1 devices send/receive empty handshake, work as before
- Handshake size detection (0 vs 16 bytes) determines protocol version
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Major architectural improvement enabling one BLEPeerInterface to handle BOTH
central and peripheral connections for a given peer identity, eliminating
duplicate interfaces and fixing ACK routing issues.
**Key Changes:**
1. **BLEPeerInterface Dual-Connection Support:**
- Added has_central_connection/has_peripheral_connection flags
- Added add_central_connection() and add_peripheral_connection() methods
- Intelligent routing in processOutgoing() - prefers central, falls back to peripheral
- Graceful degradation when only one connection type exists
2. **Identity-Based Interface Tracking:**
- Changed spawned_interfaces key from address-based to identity_hash
- Added address_to_identity and identity_to_address mapping dicts
- Enables stable peer tracking despite MAC address rotation
3. **Unified Spawning Method:**
- Created _spawn_or_update_peer_interface() to replace old _spawn_peer_interface()
- Checks if interface exists, adds new connection type if so
- Creates new interface with first connection type otherwise
4. **Updated Connection Handlers:**
- handle_central_connected(): Uses unified interface spawning for peripheral connections
- handle_central_disconnected(): Removes peripheral connection, only detaches if no connections remain
- Disconnect callback in _connect_to_peer(): Removes central connection with graceful cleanup
5. **Updated Data Routing:**
- _handle_ble_data(): Routes by identity_hash instead of address-based conn_id
- handle_peripheral_data(): Routes by identity_hash with Protocol v1 fallback
**Benefits:**
- ✅ Fixes ACK routing issue (only 1 interface per peer instead of 2-4)
- ✅ Identity-based tracking immune to MAC rotation
- ✅ Path redundancy - can use both connections if available
- ✅ Android backgrounding ready - peripheral path survives when app can't scan
- ✅ Backward compatible with Protocol v1 devices
**Testing:**
- Pi-to-Pi bidirectional discovery
- Round-trip LXMF messaging with ACK verification
- Connection dynamics (loss/recovery)
Fixes ACK routing issue discovered in testing session 2025-10-31.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Sends empty WRITE to RX characteristic immediately after connection
to guarantee remote side's on_central_connected callback fires.
Problem: Peripheral callback triggered by WRITE events, not connections.
When central connects and only READs (Identity characteristic), the
peripheral's on_central_connected never fires, preventing peer interface
spawning on the peripheral side.
Solution: After reading Identity, write empty bytes to RX characteristic.
This triggers the WRITE callback which calls _handle_central_connected(),
ensuring bidirectional peer interface spawning.
Benefits:
- Works for Pi-to-Pi (ensures both sides spawn interfaces)
- Works for Android-to-Pi (Pi spawns interface when Android connects)
- Minimal overhead (single empty GATT write)
- Backwards compatible (empty write is harmless)
Implementation:
- Added after Identity read in _connect_to_peer()
- Uses write_gatt_char() with response=True for reliability
- Non-critical failure (logged as warning, doesn't block connection)
- TODO comment for future handshake protocol enhancements
This solves the asymmetric peer spawning issue seen in testing where
only the central side had a peer interface.
Tested: Enables bidirectional data flow for single-direction discoveries.
Removed calls to self.owner.tunnel(peer_if) which caused AttributeError.
Root cause: Transport class doesn't have a tunnel() method. The tunnel()
method was incorrectly assumed based on other interface patterns, but
direct peer interfaces (like I2PInterface) only use:
RNS.Transport.interfaces.append(peer_if)
No tunnel registration is needed for direct peer connections.
Changes:
- Removed tunnel() call from central connection spawn (~line 1607)
- Removed tunnel() call from peripheral connection spawn (~line 1778)
- Added explanatory comment about I2PInterface pattern
This fixes the AttributeError seen in Pi logs:
"failed to connect: AttributeError: type object 'Transport'
has no attribute 'tunnel'"
Peer interfaces still register correctly via RNS.Transport.interfaces[].
Tested: Interface spawning works, AttributeError eliminated.
Fixes critical discovery issues caused by BlueZ/Bleak limitations.
Root cause analysis (via nRF Connect + debug logging):
1. Bleak doesn't parse service UUIDs from advertisement data (service_uuids=[])
despite UUIDs being present (verified with nRF Connect showing correct UUID)
2. Name-based fallback works but RSSI -127 caused rejection
3. BlueZ hides connected/known devices from scan results
Changes:
- Added debug logging to detection_callback to diagnose Bleak data parsing
- Accept RSSI -127 as valid (BlueZ sentinel for "RSSI unknown")
- Confirmed name fallback pattern (RNS-*) works when service UUID fails
Test results:
- nRF Connect confirms correct UUID in advertisement: 37145b00-442d-4a94-917f-8f42c5da28e3
- Bleak sees device name "RNS-Pi1" but service_uuids=[]
- After bluetoothctl remove + RSSI fix: discovered via name pattern
- Asymmetric success: Pi 1→Pi 2 peer interface spawned, 72 bytes transmitted
Known issues:
- Bleak/BlueZ doesn't populate service_uuids from advertisement (Linux limitation)
- BlueZ auto-reconnects and hides devices from scans (requires bluetoothctl remove)
- Asymmetric discovery due to scan-hiding issue
Related: BLE_TEST_RESULTS_2025_10_31.md, BLE_DISCOVERY_TROUBLESHOOTING.md
Fixed discovery failure caused by GATT server advertising wrong service UUIDs.
Root cause: BLEGATTServer and BLEInterface were using placeholder/test UUIDs
(00000001-5824-4f48-9e1a-3b3e8f0c1234 etc.) instead of the Reticulum standard
UUID namespace (37145b00-442d-4a94-917f-8f42c5da28e*).
This caused Pis to advertise services that scanners couldn't recognize,
blocking all BLE discovery and connection attempts.
Changes:
- BLEGATTServer.py: Updated all 4 service/characteristic UUIDs
- BLEInterface.py: Updated all 4 service/characteristic UUIDs
Diagnosed using nRF Connect mobile app which showed wrong UUIDs being advertised.
Related: BLE_TEST_RESULTS_2025_10_31.md
Problem:
- BLEPeerInterface was spawning but never calling owner.tunnel(self)
- Result: 0 tunnel table entries, no data transmission
- Peer interfaces showed as "reachable" but Transport couldn't route through them
Solution:
- Added owner.tunnel(peer_if) call after interface creation
- Applied to both spawn locations (central and peripheral connections)
- Pattern matches Android implementation in android_ble_interface.py
Changes:
- Line 1599-1601: Added tunnel registration for central connections
- Line 1770-1772: Added tunnel registration for peripheral connections
Testing:
- Peer interfaces now appear in rnstatus output
- BLEPeerInterface[RNS-Pi2/central] visible and marked as "reachable"
- AttributeError logged during tunnel() but interface still spawns
- Further investigation needed for data transmission
References:
- BLE_DATA_ROUTING_ISSUE.md - Root cause analysis
- BLE_SESSION_2025_10_31_PROGRESS.md - Detailed session notes
- android_ble_interface.py:403 - Reference implementation
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace timeout-based polling with cleaner event-driven approach using Interface.final_init() lifecycle hook. Launches background thread that waits indefinitely for Transport.identity (which is guaranteed to load), then starts GATT server with valid 16-byte identity value.
Benefits:
- No arbitrary timeout (Transport.identity WILL load, just timing varies)
- Uses proper Interface lifecycle hook (final_init)
- Non-blocking background thread
- GATT server guaranteed to have valid identity when it starts
- Cleaner separation of concerns
Same polling mechanism as I2PInterface, but better integrated with Interface lifecycle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Move self.online = True BEFORE waiting for Transport.identity to break circular dependency. Reticulum loads Transport.identity only after interfaces are online, so blocking before self.online = True creates infinite wait.
New sequence:
1. Set self.online = True (unblocks Reticulum startup)
2. Reticulum loads Transport.identity from storage
3. Wait completes successfully
4. Identity set on GATT server
5. GATT server starts with valid 16-byte identity
Reduced timeout from 30s to 10s since identity should load within 1s once interface is online.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Change from async deferred loading to synchronous wait before GATT server startup. This ensures the Identity characteristic is created with a valid 16-byte value instead of empty [], preventing BlueZ from rejecting or corrupting the advertisement which caused "0 matching service UUID" discovery failures.
The bug: Identity characteristic was being created with value=[] because the GATT server thread started before Transport.identity was loaded from storage (~1s timing window). BlueZ may silently reject advertisements when validating GATT databases with empty READ characteristics.
The fix: Block interface startup for up to 30s waiting for Transport.identity (typically available within 0.5-1s), then set it on GATT server BEFORE starting the server thread. Identity characteristic now always has valid 16-byte value when registered with BlueZ.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Move Transport.identity extraction from synchronous startup to async background task. The identity is loaded from storage AFTER interface initialization, causing "Transport.identity not available yet" warning. Now polls for identity every 1s for up to 30s and sets it when available.
Fixes Protocol v2 identity characteristic serving on GATT server.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements BLE Protocol v2 with Transport identity GATT characteristic to solve Android MAC address rotation issues. Adds IDENTITY_CHAR_UUID (00000004-...) that serves the 16-byte RNS.Transport.identity.hash, enabling reliable bidirectional mesh connectivity with Android devices whose BLE MAC addresses rotate every ~15 minutes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Addresses the common "Not Powered" error where Bluetooth adapters are
powered off, preventing BLE operations. This issue is particularly common
on Raspberry Pi after boot or system updates.
Changes:
1. README.md - Added comprehensive troubleshooting section
- New section: "Bluetooth adapter not powered"
- Documented symptoms, causes, and 4 solution methods
- Instructions for automatic power-on at boot
- Cross-referenced from other sections
2. BLEInterface.py - Enhanced error handling in _discover_peers()
- Detect "Not Powered" errors specifically
- Show clear, actionable error messages instead of stack traces
- Provide direct solution commands
- Link to troubleshooting documentation
- Gracefully handle error without crashing
3. install.sh - Automatic power state checking
- New "Step 5B: Bluetooth Adapter Power State" section
- Check if adapter is powered using `bluetoothctl show`
- Automatically power on adapter if needed
- Verify operation succeeded
- Provide troubleshooting steps if power-on fails
- Check for rfkill blocks
4. examples/bluetooth-power-on.service - New systemd service
- Ensures Bluetooth is powered on at boot
- Optional but recommended for production
- Includes installation instructions in README
5. examples/config_example.toml - Added troubleshooting entry #7
- Documents power state issue in config comments
- Cross-references systemd service example
- Notes that installer (v1.x+) handles this automatically
Impact:
- Users get clear guidance instead of cryptic stack traces
- Installer automatically fixes the issue during setup
- Reduces support burden for common power state errors
- Enables automatic recovery at boot via systemd service
Fixes: "Not Powered" / "No powered Bluetooth adapters found" errors
Tested on: Raspberry Pi Zero 2 W with Raspberry Pi OS Lite 64-bit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The BLE interface now dynamically resolves the interface directory
by checking RNS.Reticulum.configdir when loaded via exec() by Reticulum.
This allows users to specify custom config directories using the
--config flag without encountering import errors.
Changes:
- Update BLEInterface.py to use RNS.Reticulum.configdir when available
- Add fallback to default ~/.reticulum/interfaces for backward compatibility
- Add comprehensive test coverage for config directory resolution
- Update documentation to mention custom config directory support
Fixes#2🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>