- Add @Volatile to cross-thread power settings fields (KotlinBLEBridge,
BleScanner, BleAdvertiser) for JMM visibility between Python/Kotlin threads
- Remove dead **power_kwargs from AndroidBLEDriver.start()
- Add ordering-safety comment in AndroidBLEInterface.__init__()
- Upgrade closeImmediate() off-thread log from Log.w to Log.e
- Deduplicate preset values in InterfaceConfigDialog using BlePowerPreset.getSettings()
- Add scan overlap warning when scan duration >= active scan interval
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix timing bug where BLE scanner/advertiser/GATT connections persist after
service restart because System.exit(0) kills the process before the async
Python shutdown chain can complete. Add synchronous stopImmediate() methods
that run directly on Main thread before process exit.
Add user-configurable BLE power settings (Performance/Balanced/Battery Saver/
Custom presets) with per-interface controls for scan interval, scan duration,
and advertising refresh interval. Settings flow through the full stack:
UI → Room DB → Python config → Kotlin BLE bridge → Scanner/Advertiser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extends signal metrics support to BLE interfaces by implementing RSSI
tracking for BLE peer connections. Uses existing KotlinBLEBridge to
query the last RSSI value from connected peers via the Android GATT
connection.
Changes:
- Add getPeerRssi() method to KotlinBLEBridge for querying BLE RSSI
- Add get_peer_rssi() to AndroidBLEDriver with last-receive tracking
- Add get_rssi() to AndroidBLEInterface wrapper classes
- Handle BLEPeerInterface in signal_quality.py for proper RSSI extraction
- Add 16 comprehensive unit tests for new functionality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add consistent Columba:BLE:<Layer>:<Component> log tag pattern across
all BLE components for improved debugging and log filtering.
Kotlin components now use abbreviated tags (K:Bridge, K:Scan, K:Client,
K:Adv, K:Server, K:Queue, K:ConnMgr, K:Pair) and Python driver uses
Py:Driver. Also fixes callback trace logging from WARNING to DEBUG level.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When Kotlin passes ByteArray to Python via Chaquopy, it arrives as a
jarray('B') (Java array), not Python bytes. Java arrays don't have
Python methods like .hex(), so calling .hex() on identity bytes
caused "jarray('B')' object has no attribute 'hex'" errors.
Add ensure_bytes() helper function and apply it to:
- _handle_data_received: data parameter
- _handle_duplicate_identity_detected: identity_bytes parameter
This fixes the duplicate identity detection callback which was failing
silently and allowing connections that should have been rejected.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
On Linux, BLEInterface sets driver.on_duplicate_identity_detected to
check if an identity is already connected at a different MAC address.
On Android, this callback was never wired up, allowing duplicate
connections during MAC rotation.
Changes:
- KotlinBLEBridge: Add onDuplicateIdentityDetected callback field
- KotlinBLEBridge: Add setOnDuplicateIdentityDetected setter method
- KotlinBLEBridge: Check for duplicates in handleIdentityReceived()
before accepting connection, using safe message format
- AndroidBLEDriver: Wire up callback in _setup_kotlin_callbacks()
- AndroidBLEDriver: Add _handle_duplicate_identity_detected() method
- Update tests to verify callback is properly wired
- Update BLE architecture docs with callback flow
When duplicate detected, connection is rejected with safe log message
"Duplicate identity rejected for {address}" which doesn't trigger the
blacklist mechanism.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BLEInterface.py is provided by ble-reticulum pip dependency.
No need to maintain a duplicate local copy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The fix/ble-peer-interface-cleanup branch has been merged to main.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ktlint_standard_function-naming = disabled for Compose functions
- Move inline comments to separate lines per ktlint rules
- Fix import ordering and multiline expression formatting
- Fix line length exceeding 160 characters
- Auto-format trailing commas and expression body placement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rewrite _validate_spawned_interfaces() with 3-pass approach:
- Pass 1: Collect orphaned addresses
- Pass 2: Clean up address mappings, track interfaces to detach
- Pass 3: Only detach interfaces with zero connected addresses
- Fragmenters only cleaned up when interface fully detached
- Enhance _spawn_peer_interface() reuse logic:
- Update address_to_identity and identity_to_address when reusing
- Cancel pending detach for the identity
- Mark interface as online
- Fix disconnect callbacks to preserve fragmenters:
- _device_disconnected_callback: defer fragmenter cleanup to grace period
- handle_central_disconnected: same fragmenter preservation
- _process_pending_detaches: clean up fragmenters on actual detach
- Rename _cleanup_stale_interface() to _cleanup_stale_address():
- No longer detaches interface during MAC rotation
- Only cleans up stale address-specific mappings
- Interface preserved for reuse with new address
Fixes orphaned peer interfaces and "No fragmenter for peer" warnings
during BLE MAC address rotation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When an address disconnects, check if other addresses still have the
same identity. Only detach the peer interface when ALL addresses for
that identity are disconnected.
Additionally, use a 2-second grace period before detaching to allow
new connections to establish during MAC rotation. If a new address
with the same identity connects within the grace period, cancel the
pending detachment.
This fixes the issue where peer interfaces were being rapidly created
and destroyed during MAC rotation, causing connection instability.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
When Python's disconnect callback fires but the driver layer maintains
the GATT connection, data was being dropped because address_to_identity
was cleared.
Changes:
- Point ble-reticulum dependency to fix/identity-cache-on-disconnect branch
- Add requestIdentityResync() to KotlinBLEBridge for identity recovery
- Add request_identity_resync() to android_ble_driver.py as bridge method
The ble-reticulum fix adds identity caching (60s TTL) and cache lookup
in _handle_ble_data. When cache is also empty, it calls the driver's
request_identity_resync() method to recover identity from Kotlin.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Android silently stops BLE advertising when the app goes to background,
screen turns off, or device enters Doze mode - without calling any
callback. This leaves the app thinking it's advertising when it's not.
Add a proactive refresh mechanism that restarts advertising every 60
seconds to ensure the device remains discoverable. Also add bridge
methods to allow Python to check/ensure advertising is active.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace submodule with pip install from GitHub. This simplifies
dependency management and avoids namespace collision with RNS.Interfaces.
Changes:
- Remove external/ble-reticulum submodule
- Update build.gradle.kts to install from GitHub branch
- Update reticulum_wrapper.py to use ble_reticulum package
- Remove duplicate BLE files from python/ble_modules/
- Fix KotlinBLEBridge address callback deduplication
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bug 1 - MAC Rotation:
- Bypass MAC sorting after MAC rotation cleanup to ensure reconnection
- When a peer's MAC address rotates, we must reconnect regardless of
which device has the higher MAC address
- Updated ble-reticulum submodule with fix and tests
Bug 2 - Dual Connection Deduplication:
- Add onAddressChanged callback from Kotlin to Python
- When Kotlin deduplicates a dual connection (central + peripheral),
Python now updates its address mappings so messages continue to flow
- Updates identity_to_address, address_to_identity, and fragmenter keys
Files changed:
- KotlinBLEBridge.kt: Add setOnAddressChanged callback
- bluetooth_driver.py: Add on_address_changed to abstract interface
- android_ble_driver.py: Register and handle address change events
- BLEInterface.py: Add _address_changed_callback to update mappings
- Added unit tests for both bugs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>