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>
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>
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>
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>
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 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>
- Fix BLE cooldown asymmetry: set deduplication cooldown BEFORE deciding
which connection to keep, ensuring cooldown is applied regardless of
whether central or peripheral connection is retained
- Fix InterfaceManagementViewModel test OOM: make STATUS_POLL_INTERVAL_MS
testable (const→var) and add conditional return when interval <= 0
- Fix KISS.unescape() to properly skip invalid escape sequences (0xDB
followed by invalid byte) and trailing FESC bytes instead of corrupting
the data buffer
- Add comprehensive unit tests for all fixes following strict TDD approach
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Python RNode interface hardcoded a 22 dBm maximum TX power, which
rejected valid regional settings (US allows 30 dBm, NZ allows 36 dBm).
Changes:
- Update Python validation to accept TX power up to 36 dBm (NZ max)
- Add Python tests for regional TX power limits (US 30, EU 27, NZ 36)
- Add Kotlin tests for NZ region validation (36 dBm max)
The Kotlin validator already correctly uses regional limits. The RNode
firmware provides final hardware validation and returns error 0x40 if
TX power exceeds device capability.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add lock around self.online status in _set_online() and process_outgoing()
to prevent frame loss during online/offline transitions
- Add lock around RSSI/SNR updates for consistency with other radio state
- Log warning for malformed KISS escape sequences instead of silent ignore
- Add Python unit tests for thread safety fixes (6 new tests, 68 total)
- Add Python pytest step to CI workflow before Gradle tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Register interface with RNS.Transport before start() to fix race
condition where auto-reconnect succeeds but interface wasn't tracked
- Add online status callback chain: Python → KotlinRNodeBridge →
ReticulumServiceBinder → ServiceReticulumProtocol → ViewModel
- ViewModel now observes interfaceStatusChanged flow for immediate
refresh when RNode connects/disconnects
- Change diagnostic logs from INFO to DEBUG level for production
- Add unit tests for RNodeOnlineStatusListener functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Android BLE is asynchronous - writeCharacteristic() only queues the write.
Without waiting for onCharacteristicWrite() callback, subsequent writes
are silently dropped by the BLE stack.
Changes:
- Add CountDownLatch-based synchronization in KotlinRNodeBridge
- Each BLE write now waits for callback confirmation before proceeding
- Add small delays in Python for framebuffer writes (defensive)
This fixes reliability issues with RNode framebuffer commands on BLE.
Note: T114 still won't show framebuffer due to upstream firmware bug
(bt_ssp_pin set at startup, never cleared).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When user disables the logo display setting and applies changes,
explicitly call disable_external_framebuffer() to restore normal
RNode UI without requiring a device restart.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add support for displaying the Columba constellation logo on RNode's
OLED display when connected. The logo is sent via KISS protocol using
the external framebuffer commands (CMD_FB_EXT, CMD_FB_WRITE).
Changes:
- Add conversion script to render icon to 64x64 monochrome bitmap
- Add columba_logo.py with 512-byte framebuffer data
- Add framebuffer methods to ColumbaRNodeInterface
- Auto-display logo after successful RNode connection
- Enable by default via enable_framebuffer config option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
When the RNode disconnects (power cycle, out of range, etc.), the
interface now automatically attempts to reconnect:
- Starts a background reconnection loop on disconnect detection
- Tries to reconnect every 10 seconds, up to 30 attempts (~5 minutes)
- Logs progress: "Reconnection attempt X/30 for RNode..."
- Stops reconnection loop when connection succeeds or interface is stopped
Also fixes CompanionDeviceManager-triggered reconnection:
- initialize_rnode_interface() now checks for existing offline interface
- Calls start() to reconnect instead of failing due to missing config
- Handles case where interface already exists but config was cleared
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Set HW_MTU to 500 to prevent RNS from truncating packet data before
link_id computation, which was causing link establishment failures
- Increase BLE stabilization delay from 0.5s to 1.5s to allow connection
to fully establish before configuration
- Add retry logic to writes (3 attempts with 0.3s delays) to handle
transient BLE connection issues
- Add diagnostic logging to writeSync() for easier debugging
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements RNode interface support for LoRa communication via paired
Bluetooth RNode devices. Uses a Kotlin Bridge architecture where Kotlin
handles Bluetooth I/O and Python handles the KISS protocol.
- **KotlinRNodeBridge**: Handles Bluetooth Classic (SPP/RFCOMM) and BLE
(Nordic UART Service) connections to RNode hardware. Manages connection
lifecycle, data buffering, and provides read/write APIs to Python.
- **ColumbaRNodeInterface**: Python interface implementing KISS protocol
for RNode communication. Handles frame escaping, command parsing, radio
configuration, and integrates with RNS Transport layer.
- **UI Components**: Added RNode configuration fields to InterfaceConfigDialog
including device name selector, connection mode (Classic/BLE), frequency,
bandwidth, spreading factor, coding rate, and TX power settings.
- Supports both Bluetooth Classic (UUID: 00001101-0000-1000-8000-00805F9B34FB)
and BLE via Nordic UART Service (UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e)
- Thread-safe circular buffer for BLE packet reassembly
- Automatic device discovery from paired devices list
- Connection state management with callbacks
- Full KISS protocol implementation (FEND/FESC escape sequences)
- RNode detection and firmware version validation
- Radio parameter configuration (frequency, bandwidth, SF, CR, TX power)
- Airtime limiting support (short-term and long-term)
- Required RNS Transport interface attributes for compatibility
- set_rnode_bridge() to receive Kotlin bridge reference
- initialize_rnode_interface() called during bridge setup
- RNode interface registered with RNS.Transport.interfaces
1. **Chaquopy ByteArray conversion**: Raw bytes from Kotlin needed explicit
`bytes()` conversion in Python due to Chaquopy's jarray handling.
2. **KISS frame format**: Initial detection commands were missing FEND
delimiters, causing RNode to not respond to detection requests.
3. **RNS Transport compatibility**: Required iteratively adding interface
attributes (bitrate, rxb, txb, mode, mtu, HW_MTU, FIXED_MTU,
AUTOCONFIGURE_MTU, announce_rate_target, ifac_size, etc.) and methods
(sent_announce(), received_announce(), process_held_announces(),
should_ingress_limit()) to satisfy RNS Transport requirements.
4. **Owner inbound routing**: Changed from owner.inbound() to direct
RNS.Transport.inbound() calls since owner was ReticulumWrapper, not
Transport.
Successfully tested bidirectional communication:
- Announces sent and received between Columba and Sideband via LoRa
- Links established with ~1.8s RTT over LoRa
- Messages delivered from Columba to Sideband
- Messages received from Sideband (routing to correct identity required)
- python/rnode_interface.py (NEW): KISS protocol and RNode interface
- reticulum/rnode/KotlinRNodeBridge.kt (NEW): Bluetooth bridge
- python/reticulum_wrapper.py: RNode bridge integration
- ReticulumServiceBinder.kt: Bridge initialization in setupBridges()
- InterfaceConfigDialog.kt: RNode UI configuration fields
- InterfaceManagementViewModel.kt: RNode state management
- ReticulumConfig.kt: RNode data model with targetDeviceName, connectionMode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>