- 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>