Add 27 tests covering:
- get_discovered_interfaces(): TCP interfaces, radio interfaces with LoRa
params, interfaces with location data, multiple interfaces sorted by
status and stamp value, bytes/string transport_id handling, None value
filtering, exception handling
- _get_discovery_status_name(): status code to name conversion
- _create_config_file() discovery options: discover_interfaces,
autoconnect_discovered_interfaces, interface_discovery_sources,
required_discovery_value, bootstrap_only for TCP clients
This improves patch coverage from ~56% to target higher coverage.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add unit tests for the RNS 1.1.x discovery functionality:
Python tests:
- test_autoconnected_endpoints.py: 14 tests for get_autoconnected_interface_endpoints()
- test_discovery_enabled.py: 12 tests for is_discovery_enabled()
Kotlin tests:
- DiscoveredInterfacesViewModelTest.kt: 22 tests covering autoconnected endpoints
state, isAutoconnected() helper, and discovery settings loading
- DiscoveredInterfacesScreenTest.kt: 29 tests for UI helpers including
isYggdrasilAddress(), formatInterfaceType(), and icon/badge visibility
Also fixes:
- Fix detekt ComplexCondition issues in RNodeWizardScreen, MapScreen, MainActivity
- Fix detekt LongMethod in ColumbaApplication by extracting restorePeerIdentities()
- Fix detekt SwallowedException in InterfaceRepository with verbose logging
- Fix detekt MatchingDeclarationName by moving FocusInterfaceDetails to own file
- Fix detekt ReturnCount in isYggdrasilAddress and ViewModel helper functions
- Update StartupConfigLoaderTest for new discovery settings
- Update TcpClientWizardViewModelTest for allInterfaceEntities changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add "Connected" badge showing which discovered interfaces are currently
auto-connected by RNS discovery
- Add getAutoconnectedEndpoints() API to fetch active auto-connected interfaces
- Use type-specific icons: globe for TCP, antenna for radio, incognito for I2P,
tree-pine for Yggdrasil (detected via 0200::/7 IPv6 range)
- Show I2P b32 addresses with .b32.i2p suffix
- Display human-readable interface type labels (e.g., "Backbone (TCP)")
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add toggle in Discovered Interfaces screen to enable/disable discovery
- Service auto-restarts when toggle is changed (no app restart needed)
- Show bootstrap-enabled interface names in settings card
- Add discovery settings to SettingsRepository and StartupConfigLoader
- Include discover_interfaces and autoconnect_discovered_interfaces in config
- Include bootstrap_only for TCPClient interfaces in buildConfigJson
- Fix Python wrapper bug: discovered_interfaces() returns list, not dict
- Add bottom padding to list to avoid nav bar overlap
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add unit tests for ReceivingInterfaceInfo:
- 25 Kotlin tests covering TCP, Auto, BLE, RNode, Serial, and unknown interfaces
- Tests for friendly name extraction, interface type parsing, and edge cases
Refactor ReceivingInterfaceInfo to reduce cyclomatic complexity:
- Extract InterfaceCategory enum for display properties
- Extract categorizeInterface() and looksLikeAddress() helper functions
- Main function now delegates to smaller, focused functions
Add 2 additional Python tests for interface name formatting:
- Test interface.name same as class name returns class only
- Test interface.name being None returns class only
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update the "Received Via" card in Node Details to show the user-configured
interface name (e.g., "Sideband Server") instead of generic type names
(e.g., "TCP/IP").
Python changes:
- Build formatted interface string "ClassName[UserConfiguredName]" from
the interface object's type and .name attribute
- This allows Kotlin to extract both the friendly name and interface type
Kotlin changes:
- Add extractFriendlyName() to parse user-configured name from brackets
- Add extractInterfaceType() to parse class name before brackets
- Display friendly name as main text, interface type as subtitle
The card now shows:
- Title: "Received Via"
- Content: "Sideband Server" (user-configured name)
- Subtitle: "TCPClientInterface" (interface type)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive discovery data to DiscoveredInterface including:
- Transport/network IDs and status info (statusCode, lastHeard, hops)
- TCP-specific fields (reachableOn, port)
- Radio-specific fields (frequency, bandwidth, SF, CR, modulation)
- Location fields (latitude, longitude, height)
- Helper properties for interface type detection
Update Python bridge to return full discovery data from RNS 1.1.x API.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for RNS 1.1.0+ interface discovery and bootstrap features,
enabling new users to connect without relying on deprecated public
infrastructure.
Changes:
- Add discovery settings to ReticulumConfig (discoverInterfaces,
autoconnectDiscoveredInterfaces, interfaceDiscoverySources,
requiredDiscoveryValue)
- Add bootstrapOnly option to TCPClient interface config
- Update Python config generation for discovery and bootstrap options
- Add get_discovered_interfaces() and is_discovery_enabled() API methods
- Extend AIDL interface with discovery methods
- Mark 3 reliable community servers as bootstrap candidates (Beleth,
Quad4, FireZen)
- Add bootstrap toggle to TCP Client wizard UI
Note: The RNS fork needs to be rebased on 1.1.2 to enable the actual
discovery features. The code is ready but awaiting the dependency update.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Harden 4 Python tests that were flaky due to mock object identity
comparisons by using explicit sentinel values for link status constants
- Add failure annotations to CI workflow so root cause job is clearly
marked when fail-fast cancels sibling jobs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cover all branches: success with items, empty list, lowercase
normalization, empty string filtering, set storage, deduplication,
and list replacement.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add requester to allowed list in tests that expect requests to succeed
- Add test_blocks_request_from_non_allowed_requester to verify blocking
- Add test_blocks_request_when_allowed_list_empty to verify empty = block all
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Empty telemetry stream responses (0 entries from collector) were
appearing as blank messages in chat because is_location_only was
only set when the stream had entries. Now telemetry stream messages
are always marked as location-only regardless of whether they
contain data.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements the ability to control which contacts can request telemetry
when acting as a collector (host mode). Users can select specific
contacts who are allowed to request their group's location data.
Requests from non-allowed contacts are silently blocked.
Changes:
- Add telemetry_allowed_requesters state and filtering in Python layer
- Add AIDL interface and bridge for setTelemetryAllowedRequesters
- Add SettingsRepository storage with DataStore persistence
- Add TelemetryCollectorManager state flow and Python sync
- Add SettingsViewModel state management with ContactRepository
- Add UI components: AllowedRequestersSection and AllowedRequestersDialog
- Fix ArrayList to Python list conversion in ReticulumServiceBinder
- Fix state preservation in SettingsViewModel.loadSettings()
- Add CLAUDE.md with Chaquopy ArrayList pitfall documentation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Expand Python test coverage to include:
TestSendTelemetryRequestSuccess (6 tests):
- Successful send with immediate identity recall
- Successful send with cached identity
- Error when local_lxmf_destination is missing
- Error when identity cannot be resolved after path request
- jarray conversion handling
- Correct FIELD_COMMANDS structure building
TestOnLxmfDeliveryFieldCommands (4 tests):
- FIELD_COMMANDS handling when collector enabled
- Skipping FIELD_COMMANDS when collector disabled
- Identity retry mechanism in delivery handler
- Ignoring non-collector requests
Total Python tests: 45 (up from 35)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add 14 new tests covering:
- _send_telemetry_stream_response: filtering by timebase, LXMF message
creation, handling empty telemetry, cleanup of expired entries
- send_telemetry_request: error handling for uninitialized states
- FIELD_COMMANDS constants: verify all protocol constants exist
- Timebase filtering: verify received_at vs timestamp filtering
These tests improve coverage for the Python telemetry collector
implementation, specifically the FIELD_TELEMETRY_STREAM response
generation and FIELD_COMMANDS request handling paths.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 4 tests to SettingsRepositoryTest for host mode persistence
- Add 7 tests to TelemetryCollectorManagerTest for host mode management
- Add 6 tests to SettingsViewModelTest for host mode UI state
- Create test_telemetry_host_mode.py with 21 Python tests for:
- pack_telemetry_stream function
- set_telemetry_collector_enabled method
- _store_telemetry_for_collector method
- _cleanup_expired_telemetry method
- Integration tests for full workflow
- Fix detekt issues with appropriate suppressions
- Fix SwallowedException by including exception in log
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enable Columba to act as a telemetry collector compatible with Sideband's
protocol. When Host Group mode is enabled, Columba will:
- Store incoming FIELD_TELEMETRY (0x02) location data from peers
- Handle FIELD_COMMANDS (0x09) telemetry requests
- Respond with FIELD_TELEMETRY_STREAM (0x03) containing all stored entries
Implementation details:
- In-memory storage with 24-hour TTL, keeping latest per source
- Uses received_at timestamp for timebase filtering (handles clock skew)
- Follows Sideband's identity recall + path request pattern
- UI toggle in Location Sharing settings card
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enable requesting location data from group host (telemetry collector):
- Add send_telemetry_request() to Python wrapper using FIELD_COMMANDS
- Add path request retry logic when identity not immediately known
- Add request toggle and interval settings to UI
- TelemetryCollectorManager handles periodic location requests
- New AIDL/protocol methods for sendTelemetryRequest
UI renamed from "Telemetry Collector" to "Group Tracker" with
user-friendly labels (Group Host, Share with group, etc.)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously Columba was using FIELD_IMAGE (0x06) for telemetry
commands, causing Sideband to interpret requests as image
attachments. Add the correct FIELD_COMMANDS constant (0x09)
per the LXMF specification.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Prevents race condition where two threads compete for USB reads
if the previous thread doesn't terminate within timeout.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Set readTimeout=100ms to reduce CPU usage from continuous polling
- Escalate thread leak warning to ERROR level for visibility
- Capture deviceId early in detach handler for stability across Android versions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add hex character validation for collector address (prevents crash)
- Fix periodic send race condition by awaiting in-progress sends
- Fix auto-confirm timing using LaunchedEffect for state sync
- Optimize SQL queries from O(n²) to O(n) using JOIN
- Add future timestamp rejection (>1hr) to prevent UI issues
- Add icon name validation (alphanumeric/underscore, max 50 chars)
- Add comprehensive tests for Python validation logic
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use location.time instead of System.currentTimeMillis() for accurate
location capture timestamp in TelemetryCollectorManager
- Add timestamp validation in Python to prevent issues with None/0 values
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements support for sending location telemetry to a collector and
receiving bulk telemetry streams from multiple sources.
Features:
- TelemetryCollectorManager for scheduled/manual telemetry sends
- TelemetryCollectorCard UI for configuration in Settings
- FIELD_TELEMETRY_STREAM (0x03) handling in Python layer
- Database migration for appearanceJson field on received locations
- SettingsRepository preferences for collector address, intervals, etc.
Testing:
- TelemetryCollectorManagerTest with mockk and Turbine
- TelemetryCollectorCardTest with Compose testing
- SettingsViewModelTest telemetry method tests
- SettingsRepositoryTest telemetry preference tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extracted business logic from ViewModels and UI components into testable utility classes (InterfaceFormattingUtils, DeviceNameValidator, RssiThrottler, DeviceClassifier) to enable comprehensive unit testing without platform dependencies. Added 116 new tests covering formatting, validation, throttling, and device classification logic, increasing patch coverage from 23.5% to approximately 35-40%.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When pairing a USB-connected RNode via Bluetooth, the wizard now:
- Adds "Pair via USB" option to the Bluetooth tab
- Receives PIN from RNode over USB connection
- Scans for unbonded RNodes and shows device selection list
- Auto-selects if only one unbonded RNode is found
- Uses BroadcastReceiver to properly detect bond state changes
This solves the issue where multiple RNodes in range would cause
pairing failures because the PIN belongs to the USB-connected device,
not necessarily the first discovered one.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix SwallowedException in KotlinUSBBridge by adding verbose logging
- Fix ImplicitDefaultLocale by using Locale.US for PIN formatting
- Add @Suppress for NestedBlockDepth in KISS protocol parser
- Remove unused generateDefaultInterfaceName and waitForBondState functions
- Add @Suppress for loadExistingConfig complexity (inherent to config mapping)
- Update DebugViewModelEventDrivenTest with interfaceRepository parameter
- Update ReviewConfigStepTest with isUsbMode and getConnectionTypeString mocks
- Fix test_usb_bridge.py mock to use Chaquopy getter methods
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix CompanionDeviceManager association path (Android 12+) not updating
interface name when a device is selected
- Extract identifier from Bluetooth device name (e.g., "RNode 958F" -> "958F")
- Add BLE/BT suffix based on device type (e.g., "RNode 958F BLE")
- Remove unused device hash query code from KotlinUSBBridge, usb_bridge.py,
and reticulum_wrapper.py
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove device hash query complexity for USB RNodes since CMD_DEV_HASH
returns a different hash than what's used for Bluetooth naming. USB
interfaces now simply use "RNode USB" as the default name.
The device hash query code is retained in KotlinUSBBridge for potential
future use (debugging, device identification).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When switching between multiple USB RNodes, the read loop thread was not
being stopped on disconnect. This caused multiple threads to compete for
data from the shared USB buffer, leading to:
- KISS frames being split across threads
- Detection responses consumed by wrong/stale threads
- Detection timeout despite successful response
The fix:
- Clear _running flag in disconnect callback to stop read loop
- Add safety check in _start_usb() to stop any stale read thread
- Reset detection state on reconnection for clean configuration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use SQLite json_extract() for reliable VID/PID queries instead of
fragile LIKE pattern matching in InterfaceDao
- Fix Chaquopy interop: use .size() for Java ArrayList instead of len()
- Fix Kotlin data class property access: use getter methods (getDeviceId())
instead of direct property access
- Add debug logging for USB device lookup flow
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Interface Stats Screen:
- Add "CONNECTING" status badge with spinner when interface is connecting
- Show spinner for up to 15 seconds on screen load if interface is offline
- Use tertiary color for connecting state to distinguish from online/offline
- Reset connecting state when interface goes offline (for replug detection)
Service Binding Fix:
- Wait up to 10 seconds for service to bind before attempting RNode reconnect
- Previously, reconnectRNodeInterface() silently did nothing if service wasn't
bound yet, causing cold start reconnects to fail
- Add proper logging for service binding wait and timeout cases
Also adds connecting field to Python interface stats for future use.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
USB connections are reliable enough to skip the stabilization delay
that was previously added for safety. This reduces reconnect time
by 0.5 seconds.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When USB RNode is replugged:
- BroadcastReceiver catches USB attach event early
- Checks if USB permission is already granted
- If no permission: skips reconnect, allows retry via Activity intent
- If permission granted: reconnects immediately
Also:
- Clear stale USB connection in Python before reconnecting
- Track if reconnect was attempted to allow permission-based retry
- Listen for USB detach events to reset debounce state
This fixes the race condition where BroadcastReceiver fired before
Android granted USB permission, causing reconnect to fail.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a USB device is attached:
- Check if it's already configured as an RNode interface
- If configured: navigate directly to the new Interface Stats screen
- If not configured: navigate to RNode wizard with USB pre-selected
New Interface Stats screen displays:
- Online/offline status with enable/disable toggle
- Connection type and details (TCP/USB/BLE/Bluetooth)
- RNode radio settings (frequency, bandwidth, SF, TX power)
- Live traffic statistics (RX/TX bytes, RSSI, SNR)
- Edit button to modify interface configuration
Also:
- Make interface cards clickable in management screen
- Add singleTask launchMode to prevent duplicate Activity instances
- Add get_interface_stats() to Python wrapper for live stats
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The USB RNode interface was saved to database correctly but failed to
start because:
1. The usb_device_id was missing from _pending_rnode_config in
reticulum_wrapper.py
2. The KotlinUSBBridge was never passed to the Python side (unlike
KotlinRNodeBridge for Bluetooth)
3. The connect() call didn't pass baud rate (Chaquopy doesn't support
Kotlin default parameters)
Changes:
- Add usb_device_id to _pending_rnode_config dictionary
- Add set_usb_bridge() method to reticulum_wrapper.py
- Set USB bridge in ReticulumServiceBinder before Python init
- Pass baud rate (115200) explicitly to usb_bridge.connect()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive tests for the get_rssi() method in AndroidBLEInterface
and signal metrics capture in reticulum_wrapper.
New test files:
- test_android_ble_interface.py: 9 tests for AndroidBLEInterface.get_rssi()
- test_ble_modules_android_ble_interface.py: 7 tests for ble_modules version
Updated test_wrapper_messaging.py:
- Add TestOnLxmfDeliverySignalMetrics class with 4 tests verifying
RSSI and SNR capture during LXMF message delivery
Co-Authored-By: Claude Opus 4.5 <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 signal quality metrics (RSSI and SNR) to received messages, displayed
on the message detail screen. RNode-delivered messages show both metrics;
BLE shows RSSI only (Android limitation).
Changes:
- Python layer: New signal_quality.py module for extracting metrics from
Reticulum interfaces at message delivery time
- Database: Add receivedRssi (INTEGER) and receivedSnr (REAL) columns with
migration 30->31
- Protocol: Parse rssi/snr from JSON in ServiceReticulumProtocol and
EventHandler
- UI: New SignalQualityInfo utility with color-coded signal strength/quality
display in MessageDetailScreen
Signal thresholds:
- RSSI: Excellent (>-50), Good (-50 to -70), Fair (-70 to -85), Weak (-85 to -100), Very Weak (<-100) dBm
- SNR: Excellent (>10), Good (5-10), Fair (0-5), Poor (-5-0), Very Poor (<-5) dB
Closes#209
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive unit tests for the USB bridge and RNode interface:
Python tests (test_usb_bridge.py - 43 tests):
- USB bridge setup and initialization
- Device enumeration
- Permission handling
- Connection/disconnection
- Read/write operations
- Callback setters
Python tests (test_rnode_interface.py - added USB tests):
- USB mode constants (MODE_USB, CMD_BT_PIN, BT_CTRL_PAIRING_MODE)
- USB mode interface configuration
- Bluetooth pairing mode over USB
- KISS Bluetooth command framing
- Fixed existing tests to work with new connection_mode field
Kotlin tests (KotlinUSBBridgeTest.kt - 21 tests):
- Bridge initialization
- Connection listener registration/removal
- Duplicate listener prevention
- Listener exception isolation
- Bluetooth PIN callback handling
- Write/read operations when not connected
- Permission checking
- Thread safety for concurrent listener registration
- Coroutine scope shutdown
- Disconnect state clearing
- UsbDeviceInfo data class
- Supported VID verification
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add USB serial support for RNode devices with two modes:
1. USB Serial Mode: Operate RNode entirely over USB cable
2. Bluetooth Pairing Mode: Pair RNode via USB (for T114 and devices without user button)
Key changes:
- Add KotlinUSBBridge for USB serial communication using usb-serial-for-android library
- Support FTDI, CP210x, CH340, PL2303, and CDC-ACM USB serial drivers
- Create Python usb_bridge.py module for Python-side USB access
- Extend rnode_interface.py with MODE_USB and Bluetooth pairing commands
- Add USB tab to RNode wizard UI with device enumeration and permission handling
- Implement Bluetooth pairing mode flow with PIN display
- Add DiscoveredUsbDevice data class for USB device information
- Update InterfaceConfig.RNode with usbDeviceId field
- Add USB host feature declaration to AndroidManifest
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>