Skip to content

Conversation

@torlando-tech
Copy link
Owner

@torlando-tech torlando-tech commented Dec 6, 2025

Summary

This PR adds comprehensive RNode LoRa radio support to Columba, enabling long-range mesh communication via RNode hardware devices. The implementation includes a full configuration wizard, regional frequency presets, real-time spectrum visualization, and BLE connectivity improvements.

Features

RNode Configuration Wizard

A step-by-step wizard guides users through RNode setup:

  1. Device Discovery - Scan and connect to RNode devices via Bluetooth
  2. Region Selection - Choose regulatory region with automatic frequency/power limits
  3. Frequency Slot Selection - Meshtastic-compatible slot picker with spectrum visualizer
  4. Modem Preset - Select from predefined modem configurations (Long Range, Medium, Fast, etc.)
  5. Review & Apply - Summary of all settings before applying to device

Regional Frequency Presets

Comprehensive regulatory compliance for 20+ regions:

  • Americas: US 915, Brazil 902, Argentina 915
  • Europe: EU 868 (split into 4 ERC 70-03 sub-bands), Russia 868, Ukraine 868
  • Asia-Pacific: AU 915, NZ 865, Japan 920, Korea 920, AS923 variants
  • Other: India 865, China 470, South Africa 868

Each region includes:

  • Frequency range limits
  • Maximum TX power limits
  • Duty cycle restrictions (auto-applied airtime limits)
  • Community preset slot alignments

Spectrum Visualization

  • Real-time frequency spectrum display
  • Shows selected frequency position relative to band limits
  • Displays community preset frequencies for easy alignment
  • Visual bandwidth indicator

RNode OLED Display

  • Display Columba logo on RNode's external framebuffer
  • Configurable via UI toggle in settings
  • Custom framebuffer protocol implementation

BLE Connectivity Improvements

  • Keepalive auto-disconnect: Dead connections cleaned up after 3 consecutive failures
  • Race condition fix: Proper handling of pending central connections during deduplication
  • Reconnection cooldown: Prevents scanner loops after connection deduplication
  • Write synchronization: Prevents dropped BLE commands

Files Changed

New Files

File Description
python/rnode_interface.py Python RNode interface implementation
python/test_rnode_interface.py Unit tests for RNode interface
reticulum/rnode/KotlinRNodeBridge.kt Kotlin-Python bridge for RNode
app/ui/screens/rnode/*.kt Wizard UI screens (5 steps)
app/data/model/RNodeRegionalPreset.kt Regional frequency/power presets
app/viewmodel/RNodeWizardViewModel.kt Wizard state management
app/viewmodel/RNodeConfigValidator.kt Configuration validation logic
app/service/RNodeCompanionService.kt Background RNode service
scripts/convert_icon_to_framebuffer.py Logo conversion utility

Modified Files

File Changes
KotlinBLEBridge.kt Deduplication fixes, keepalive improvements
BleGattClient.kt Keepalive failure tracking
BlePairingHandler.kt PIN pairing support for RNode
InterfaceManagementScreen.kt RNode wizard integration
MigrationImporter.kt RNode config import support
AnnounceEntity.kt Simplified equals() method

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    RNode Wizard UI                          │
│  (DeviceDiscovery → Region → Frequency → Modem → Review)    │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│              RNodeWizardViewModel                           │
│  - State management, validation, preset calculations        │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│              KotlinRNodeBridge                              │
│  - BLE data routing, command serialization                  │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│              rnode_interface.py                             │
│  - KISS framing, RNode commands, state machine              │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│              KotlinBLEBridge                                │
│  - BLE GATT client/server, connection management            │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
                        [RNode Hardware]

Test Coverage

  • RNodeRegionalPresetsTest.kt - Regional preset validation
  • FrequencySlotCalculatorTest.kt - Slot calculation logic
  • ModemPresetTest.kt - Modem configuration tests
  • RNodeConfigValidatorTest.kt - Validation edge cases
  • RNodeWizardViewModelTest.kt - ViewModel state tests
  • test_rnode_interface.py - Python interface tests

Test Plan

  • Connect to RNode via Bluetooth
  • Complete wizard for each major region (US, EU, AU, AS923)
  • Verify frequency limits are enforced
  • Verify TX power limits are enforced
  • Verify duty cycle restrictions apply airtime limits
  • Test spectrum visualizer shows correct frequency position
  • Test modem presets configure correct parameters
  • Verify RNode receives and applies configuration
  • Test logo display toggle
  • Verify BLE connection stability over extended periods

Breaking Changes

None - this is additive functionality.

Related Issues

Implements RNode LoRa interface support for Columba.

🤖 Generated with Claude Code

torlando-tech and others added 29 commits December 8, 2025 21:58
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>
Add a guided 3-step wizard for configuring RNode LoRa interfaces that makes
setup accessible to non-technical users.

## New Features

### Step 1: Device Discovery
- Auto-scan for RNode devices via Bluetooth Classic (bonded) and BLE (NUS UUID)
- Display discovered devices with type badge (Classic/BLE), signal strength, paired status
- In-app Bluetooth pairing support with system pairing dialog
- Manual device entry fallback with Bluetooth type selection
- Edit mode shows current device with option to change

### Step 2: Region Selection
- Searchable country list with 40+ regional presets from Reticulum wiki
- Presets for US, EU (Germany, Belgium, Netherlands, etc.), UK, Australia, Asia
- City-specific presets where regulations differ (e.g., Sydney vs Melbourne)
- Each preset shows frequency, bandwidth, spreading factor, TX power
- Custom mode for advanced users who want manual configuration

### Step 3: Review & Configure
- Device summary with Bluetooth type
- Editable interface name
- Region summary (if preset selected)
- Radio settings always visible: frequency, bandwidth, SF, CR, TX power
- Expandable advanced settings: airtime limits, interface mode
- Full validation with error messages

## Technical Changes

### New Files
- `RNodeRegionalPreset.kt`: Data models for presets, BluetoothType enum, DiscoveredRNode
- `RNodeWizardViewModel.kt`: State management, BLE scanning, pairing, validation
- `RNodeWizardScreen.kt`: Main wizard container with step navigation
- `DeviceDiscoveryStep.kt`: Bluetooth device scanning and selection UI
- `RegionSelectionStep.kt`: Country/preset selection UI
- `ReviewConfigStep.kt`: Configuration review and editing UI

### Modified Files
- `MainActivity.kt`: Add wizard route, hide main nav bar during wizard
- `InterfaceManagementScreen.kt`: Route RNode interfaces to wizard, add type selector

## UX Improvements
- Wizard has its own bottom bar with step indicators and Next/Save button
- Main app navigation bar hidden during wizard for full-screen experience
- 100dp bottom spacer pattern for proper scrolling past navigation elements
- Supports both adding new and editing existing RNode interfaces

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When applying interface configuration changes, a flag is set to prevent
auto-initialization during the restart. If the process crashes before
clearing this flag, subsequent app starts would skip initialization
indefinitely, leaving the service non-functional.

Now checks the service status when the flag is set. If the service is
SHUTDOWN/ERROR, the flag is considered stale and cleared, allowing
normal initialization to proceed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The search text field wasn't filtering the country list because
getFilteredCountries() was called inside LazyColumn items block
where Compose wasn't properly tracking the searchQuery dependency.

Moved filtered countries computation to top level with remember()
keyed on searchQuery to ensure recomposition when user types.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The regional preset cards in the Choose Region step were showing
frequency, bandwidth, SF, and TX power but missing the coding rate.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
RNode interfaces are now configured through the dedicated wizard flow
(RNodeWizardScreen), making the RNode-related code in InterfaceConfigDialog
unreachable. Removed:
- RNodeFields composable (169 lines)
- RNodeConnectionModeSelector composable (53 lines)
- RNode entry from interface type selector

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Previously, all bonded devices were marked as Classic, causing BLE
devices that were already paired to be incorrectly classified.

Changes:
- BLE scan runs first to definitively identify BLE devices
- Device types are cached in SharedPreferences for offline devices
- Bonded devices not found in BLE scan use cached type or UNKNOWN
- UNKNOWN devices show warning and allow manual type selection
- Edit mode now always scans to detect correct device type
- Selected device type updates when scan finds correct type

This ensures paired BLE devices are correctly identified even when
they were bonded before the app detected their type.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When selecting an interface type from the picker, showAddDialog() was
called before updateConfigState(), causing the type to be reset to
defaults. Swapped the order so the selected type is preserved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add CDM permissions to manifest for Android 12+ device association
- Implement device association flow in RNodeWizardViewModel with BLE/Classic filters
- Show native Android device picker when selecting RNode in wizard
- Add pending changes flag mechanism so "Apply Changes" button appears after
  RNode wizard saves an interface (fixes button not showing issue)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add RNodeCompanionService that Android binds when associated RNode devices
  appear/connect, enabling background operation
- Register existing companion device associations on app startup
- Call startObservingDevicePresence() after successful CDM association
- Add REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE permission
- Fix RNode LoRa button color in interface type selector (remove highlight)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Shorten log messages in ColumbaApplication.kt
- Break long strings in RNodeWizardViewModel.kt
- Regenerate detekt baseline for new RNode wizard code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add live RSSI updates during BLE scanning (every 3 seconds)
- Read RSSI from active RNode BLE connection via readRemoteRssi()
- Store actual BluetoothDevice from scan for proper BLE bonding
- Fix BlePairingHandler to only auto-confirm "Just Works" pairing
- Add RSSI polling in edit mode for connected RNode devices
- Improve pairing UX with two-phase timeout (5s start, 60s PIN entry)

🤖 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>
RNode Auto-Reconnection:
- RNodeCompanionService now triggers reconnection when CompanionDeviceManager
  detects the RNode has reappeared after going out of BLE range
- Add reconnectRNodeInterface() to AIDL interface and ReticulumServiceBinder
- Add thread-safe initialization lock in reticulum_wrapper.py to prevent
  concurrent RNode initialization race conditions
- Use 2-second debounce delay before reconnecting to ensure device stability

Interface Status UI Improvements:
- InterfaceManagementViewModel now polls Reticulum every 3 seconds for
  interface online/offline status
- Update isBleInterface() to include RNode type for proper BLE handling
- Add "Interface Offline" error state to getErrorMessage() for enabled
  interfaces that aren't passing traffic
- Make error badges clickable to show detailed error dialog
- Add InterfaceErrorDialog component for detailed interface issue info
- IdentityScreen: make offline interface rows clickable for troubleshooting

Build & Deploy:
- deploy.sh now supports multiple connected devices, deploying to all of
  them in sequence instead of requiring a single device

🤖 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>
Add a Reconnect button to the Interface Management screen that appears
when an RNode interface is enabled but offline. This provides a manual
fallback for users when automatic reconnection attempts are exhausted
or CompanionDeviceManager doesn't detect the device.

🤖 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>
Add a Switch toggle in the RNode wizard's Advanced Settings to let
users enable/disable displaying the Columba logo on RNode's OLED.

Changes:
- Add enableFramebuffer field to InterfaceConfig.RNode data class
- Add state and update method to RNodeWizardViewModel
- Add Switch toggle in ReviewConfigStep Advanced Settings
- Add JSON serialization in InterfaceRepository

The setting defaults to enabled (true) and is persisted to the database.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <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 a new frequency slot selection step to the RNode configuration wizard,
matching Meshtastic's channel/slot system for easier coordination between
mesh networks.

Key changes:
- Add FrequencySlotStep with visual spectrum bar and slot picker
- Add ModemPresetStep for selecting LoRa modulation parameters
- Expand FrequencyRegion to include 18 regional bands with regulatory limits:
  - maxTxPower: Maximum allowed TX power (enforced in validation)
  - dutyCycle: Duty cycle percentage (1%, 10%, or 100%)
  - Regions: US, Brazil, EU 868/433, Russia, Ukraine, Australia/NZ,
    Japan, Korea, Taiwan, China, India, Thailand, Singapore,
    Malaysia, Philippines, and 2.4 GHz worldwide
- Add FrequencySlotCalculator for Meshtastic-compatible slot math
- Show Meshtastic interference warnings for slots 9 and 20 (US)
- Default to slot 50 (US/AU) to avoid Meshtastic interference
- Enforce regional TX power limits in validation with clear error messages
- Display duty cycle restrictions in region selection UI

Reference: https://meshtastic.org/docs/configuration/radio/lora/#region

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Surface RNode hardware errors to users with helpful messages:
- 0x01: Radio initialization failed
- 0x02: Transmission failed
- 0x04: Data queue overflowed
- 0x40: Invalid configuration (suggests reducing TX power)

Add error callback mechanism to propagate errors from Python interface
through Kotlin bridge to the UI layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Display regional max TX power (e.g., "Max: 12 dBm") under TX power field
- Show frequency range hint (e.g., "869.4-869.7 MHz") under frequency field
- Add duty cycle warning card for restricted regions (EU 868/433, Ukraine)
- Add IME padding to Review step so keyboard doesn't cover input fields
- Real-time validation: fields turn red immediately when value exceeds limit
- Validation blocks saving if TX power or frequency exceeds regional limits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Disable the Next button on the region selection step until
a frequency region is explicitly selected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The hardware back button now goes to the previous wizard step
instead of exiting the wizard entirely. Only exits on the first step.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Handle edge case where narrow frequency bands (like EU 868 with LongFast)
result in only one available slot. The Slider component requires steps >= 0,
but maxSlot - 1 became -1 causing a crash.

- Hide slider and show informational message when only one slot available
- Add guard in spectrum bar to prevent divide-by-zero with single slot

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the single eu_868 region with 4 separate sub-bands per ERC 70-03:

- eu_868_l: 865-868 MHz (1% duty cycle, 14 dBm)
  For UK, Italy Brescia/Treviso, Netherlands Rotterdam presets
- eu_868_m: 868-868.6 MHz (1% duty cycle, 14 dBm)
  For Spain Madrid, Switzerland Bern, LoRaWAN default channels
- eu_868_p: 869.4-869.65 MHz (10% duty cycle, 27 dBm)
  For Germany Darmstadt/Wiesbaden, Italy Salerno (Meshtastic default)
- eu_868_q: 869.7-870 MHz (1% duty cycle, 14 dBm)
  Additional channel space

This allows users to legally use frequencies that Meshtastic chose not to
support, such as the popular 867.2 MHz and 868.1 MHz community presets.

References:
- TTN EU868: https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/eu868/
- ERC 70-03: https://www.disk91.com/2017/technology/sigfox/all-what-you-need-to-know-about-regulation-on-rf-868mhz-for-lpwan/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ndaries

- Add customFrequency state to track when a preset is selected directly
- Add selectPresetFrequency() to bypass slot calculation for non-aligned presets
- Make PopularPresetCard always clickable regardless of slot alignment
- Update CurrentSlotCard to show preset name when custom frequency selected
- Fix Brugge preset to be under Belgium instead of Netherlands
- Update Swedish 433 MHz preset to include Gothenburg, Borås, and Älvsered

This fixes the issue where EU 868.P presets like Germany Darmstadt (869.4 MHz)
couldn't be selected because they don't align with the 869.525 MHz slot.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When a region with duty cycle restrictions is selected, automatically
set the stAlock and ltAlock (short-term and long-term airtime limits)
to match the regional duty cycle percentage.

For example, EU 868 sub-band M (1% duty cycle) will now auto-fill
both airtime limit fields with "1.0".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add stAlockError and ltAlockError fields to track validation state
- Validate airtime limits cannot exceed regional duty cycle percentage
- Show error state on input fields when limits are exceeded
- Show regional max in placeholder text (e.g., "Max: 1%")
- Update helper text to explain regional duty cycle restrictions
- Block saving when airtime limits exceed regional maximum

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
torlando-tech and others added 14 commits December 8, 2025 21:58
Change message from "Configure airtime limits..." to indicate that
airtime limits are applied automatically, showing the duty cycle
percentage that has been set.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
NZ 865 MHz (864-868 MHz) was incorrectly showing AU presets which
operate at 925 MHz. Since no NZ-specific presets are defined for
the 865 MHz band, return an empty list instead.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Philippines (915-918 MHz) was incorrectly showing AS923 presets at
920.5 MHz which are outside its valid frequency range.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Brazil (902-907.5 MHz) was incorrectly showing US presets at 914.875 MHz
which are outside its valid frequency range.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Russia (868.7-869.2 MHz) and Ukraine (868-868.6 MHz) were incorrectly
showing EU presets which use different frequency ranges.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Japan (920.8-927.8 MHz) was incorrectly showing AS923 presets at
920.5 MHz which are below its valid frequency range.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add real-time validation in updateSpreadingFactor, updateCodingRate,
and updateBandwidth functions to show errors as the user types:
- Spreading Factor: 7-12
- Coding Rate: 5-8
- Bandwidth: 7.8 kHz - 1625 kHz

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Move Frequency, Bandwidth, SF, CR, and TX Power fields into the
collapsible Advanced Settings section on the Review Settings page
to simplify the default view.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When selecting a popular preset that doesn't align with slot boundaries,
the frequency spectrum bar now calculates and displays the exact position
of the preset frequency. Uses tertiary color to differentiate from
slot-based selections.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Extract validation logic from RNodeWizardViewModel into RNodeConfigValidator
- Add unit tests for FrequencySlotCalculator, ModemPreset, FrequencyRegion
- Add unit tests for RNodeRegionalPresets, CommunitySlots
- Add unit tests for RNodeConfigValidator (45 tests)
- Add Python tests for KISS protocol and config validation (52 tests)
- Refactor startDeviceScan() into smaller helper methods
- Update detekt baseline for new code structure

🤖 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>
- Add RNodeConfigInput data class to reduce parameter count
- Add @Suppress annotations for LongParameterList, ReturnCount, CyclomaticComplexMethod
- Format code to pass ktlint checks
- Add convenience overloads for backward compatibility with tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add @Suppress annotations for LargeClass, ReturnCount, CyclomaticComplexMethod, LongMethod
- Extract complex condition to variable in InterfaceManagementScreen
- Suppress UnusedParameter for FrequencySpectrumBar bandwidth parameter
- Format code to pass ktlint checks

Files fixed:
- KotlinRNodeBridge.kt
- RNodeWizardViewModel.kt
- RNodeRegionalPreset.kt
- InterfaceManagementScreen.kt
- InterfaceManagementUtils.kt
- FrequencySlotStep.kt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update InterfaceConfigExt.kt to use targetDeviceName, connectionMode,
  stAlock, ltAlock, and enableFramebuffer instead of old port field
- Update InterfaceConfigExtTest.kt with new RNode field assertions
- Update InterfaceRepositoryTest.kt with new RNode JSON format
- Add LongMethod suppression to buildConfigJson

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
torlando-tech and others added 3 commits December 8, 2025 22:54
- Add keepalive failure auto-disconnect after 3 consecutive failures
  (BleGattClient.kt) - fixes stale connections not being cleaned up

- Fix race condition in dual connection deduplication where central
  identity was incorrectly treated as "stale" because handlePeerConnected
  hadn't run yet (KotlinBLEBridge.kt)

- Add reconnection cooldown after deduplication to prevent scanner from
  reconnecting as central to a peer we're already connected to as
  peripheral (KotlinBLEBridge.kt)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename DEDUPLICATION_COOLDOWN_MS to deduplicationCooldownMs (VariableNaming)
- Simplify complex condition in handleIdentityReceived (ComplexCondition)
- Add @Suppress for necessary multiple returns in shouldSkipDiscoveredDevice

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use single return with chained conditions instead of multiple early
returns to reduce cyclomatic complexity. Extract nullable ByteArray
comparison to helper function.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@torlando-tech torlando-tech changed the title Feature/rnode feat: RNode LoRa interface support with configuration wizard Dec 9, 2025
@torlando-tech torlando-tech linked an issue Dec 9, 2025 that may be closed by this pull request
torlando-tech and others added 7 commits December 8, 2025 23:33
…ig values

When navigating between regions or selecting presets, validation errors
from previous selections were persisting even though new valid values
were being set. Now clear frequencyError, txPowerError, stAlockError,
ltAlockError, bandwidthError, spreadingFactorError, and codingRateError
when their respective values are programmatically updated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…zard

When selecting a different frequency region, the region's default settings
(frequency, tx power, airtime limits) are now applied immediately. This
fixes a bug where validation errors from a previous region would persist
even after changing to a new region with valid defaults.

Added unit test to verify validation errors are cleared when changing regions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…or RNode

- Add retry logic (up to 3 attempts) for BLE GATT connections to handle
  transient failures like GATT error 133
- Log GATT disconnect status code with human-readable names for easier debugging
- Extract single connection attempt into helper function for cleaner retry loop

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive unit tests for PR 59 features:

KotlinBLEBridgeDeduplicationTest.kt (15 tests):
- shouldSkipDiscoveredDevice() method for deduplication cooldown
- pendingCentralConnections tracking for race condition fix
- recentlyDeduplicatedIdentities cooldown management

BleGattClientKeepaliveTest.kt (12 tests):
- consecutiveKeepaliveFailures counter logic
- MAX_CONNECTION_FAILURES threshold behavior
- Keepalive timing constants verification

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fix detekt UnusedPrivateProperty warning.

🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add LoRa RNode support

2 participants