"Apply Changes" was taking 4.5+ minutes with ~4000 peer identities because
the previous implementation created full RNS objects for each peer:
- Created RNS.Identity from public key
- Created RNS.Destination for LXMF delivery
- Registered with RNS.Transport
- Called RNS.Identity.recall() to verify
But RNS's Identity.known_destinations is just a dict that remember() populates
directly. This change bypasses the expensive object creation and directly
populates the dict.
## Changes
### Python (`reticulum_wrapper.py`)
- Add `bulk_restore_announce_identities()`: Direct dict population for announces
where destination_hash is already available (no computation needed)
- Add `bulk_restore_peer_identities()`: Lightweight hash computation for peer
identities - computes LXMF delivery destination hash from public key without
creating full RNS objects
### Kotlin
- Add `restoreAnnounceIdentities()` to AIDL interface, binder, and protocol
- Update `MessagingManager` to call new bulk Python methods
- Update `InterfaceConfigManager` to use appropriate bulk restore for each type
### Tests
- Add 25 new Python tests for bulk restore functions covering:
- Success cases with valid data
- Invalid inputs (missing fields, invalid hex, invalid base64)
- Empty lists
- Large batch performance
- Equivalence with Identity.remember() dict format
## Performance Results (tested with ~4000 identities)
| Operation | Before | After |
|-----------|--------|-------|
| 1832 peer identities | ~4.5 min | 88ms |
| 2080 announce identities | (included above) | 142ms |
| **Total** | ~4.5 min | **~230ms** |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive test coverage for TCP RNode functionality:
RNodeWizardViewModelTest.kt (+16 tests):
- getPopularPresetsForRegion tests for all region types (US, AU, EU sub-bands)
- Tests for regions with no presets (Brazil, Russia, Japan)
- Asia-Pacific region filtering tests
- Frequency band exclusion tests
ServiceReticulumProtocolTest.kt (+2 tests):
- buildConfigJson includes TCP host/port for RNode
- buildConfigJson omits tcp_host when null (Bluetooth mode)
InterfaceRepositoryTest.kt (+1 test):
- Validates TCP RNode with invalid hostname format is skipped
test_wrapper_config.py (+6 tests):
- TCP RNode config generation with all LoRa parameters
- Airtime limits (st_alock, lt_alock) handling
- Interface mode handling (gateway, boundary, full)
test_wrapper_ble.py (+5 tests):
- get_paired_rnodes success/error/exception handling
- Empty device list and null response handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
## Config Format Fix
The Android RNodeInterface expects `tcp_host = hostname` as a config
field, not `port = tcp://hostname`. This is because the Android
interface's __init__() reads `tcp_host` to set `use_tcp = True`,
which then bypasses USB/Bluetooth code paths entirely.
Changed reticulum_wrapper.py to output:
```
tcp_host = 10.0.0.9
```
Instead of:
```
port = tcp://10.0.0.9
```
## Stub Modules for Android RNodeInterface
Added minimal stub modules required for Android RNodeInterface
initialization on Chaquopy:
- python/usbserial4a/__init__.py - Stub serial4a class
- python/jnius/__init__.py - Stub autoclass() function
- python/usb4a/__init__.py - Stub usb class with get_usb_device()
These stubs satisfy unconditional import checks in Android
RNodeInterface.__init__() that happen BEFORE TCP mode is evaluated.
When use_tcp=True, the actual USB/Bluetooth code paths are never
executed, so the stubs don't need full implementations.
## Additional Fixes
- ServiceReticulumProtocol.kt: Add missing tcp_host/tcp_port serialization
- ReviewConfigStep.kt: Show WiFi icon and "WiFi / TCP" for TCP mode
- RNodeWizardViewModel.kt: Add helper methods for TCP mode detection
- DeviceDiscoveryStep.kt: Remove port field (hardcoded in RNS)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for connecting to WiFi-enabled RNode devices via TCP as an
alternative to Bluetooth. This enables RNodes with WiFi chips to be
accessed over the network using the standard Reticulum RNodeInterface.
## Data Model Changes
- Add `tcpHost` and `tcpPort` fields to `InterfaceConfig.RNode`
- Update `connectionMode` to support "tcp" in addition to "classic"/"ble"
- Extend serialization/deserialization in InterfaceRepository with validation
## UI Changes (DeviceDiscoveryStep)
- Add connection type selector with "Bluetooth" and "WiFi / TCP" chips
- Add TcpConnectionForm composable with:
- IP address or hostname input field
- Port input field (default: 7633)
- "Test Connection" button with validation feedback
- Extract BluetoothDeviceDiscovery as separate composable
- Only start Bluetooth scanning when in Bluetooth mode
## ViewModel Changes (RNodeWizardViewModel)
- Add RNodeConnectionType enum (BLUETOOTH, TCP_WIFI)
- Add TCP state fields: tcpHost, tcpPort, validation state
- Add methods: setConnectionType(), updateTcpHost(), updateTcpPort(),
validateTcpConnection()
- Update canProceed() to handle TCP mode
- Update saveConfiguration() to create TCP config
- Update loadExistingConfig() to restore TCP settings when editing
## Python Config Generation (reticulum_wrapper.py)
- For TCP mode: write to config file using standard RNodeInterface
format with `port = tcp://host:port`
- For Bluetooth mode: continue using ColumbaRNodeInterface (unchanged)
## Technical Notes
TCP connections can use Reticulum's standard RNodeInterface since TCP
sockets are standard Python (no jnius/Android-specific code needed).
The custom ColumbaRNodeInterface remains necessary only for Bluetooth
connections due to Android Bluetooth API requirements.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add additional unit tests for the transport node feature:
InterfaceConfigManager (3 tests):
- Verify getTransportNodeEnabled() is called during config apply
- Verify enableTransport=true is passed to config when enabled
- Verify enableTransport=false is passed to config when disabled
ServiceReticulumProtocol (3 tests):
- Verify enableTransport defaults to true in ReticulumConfig
- Verify enableTransport can be set to false
- Verify enableTransport can be set to true explicitly
Python get_debug_info (3 tests):
- Verify transport_enabled returns true from RNS when enabled
- Verify transport_enabled returns false from RNS when disabled
- Verify transport_enabled returns false when not initialized
Related to #49🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add a settings toggle to enable/disable Reticulum transport node mode.
When enabled (default), the device forwards traffic for the mesh network.
When disabled, only handles its own traffic without relaying for peers.
Changes:
- Add transportNodeEnabled preference in SettingsRepository
- Add enableTransport field to ReticulumConfig model
- Add state and toggle method in SettingsViewModel
- Pass setting through InterfaceConfigManager and ColumbaApplication
- Update Python wrapper to use config value instead of hardcoded yes
- Fix get_debug_info() to read actual RNS transport status
- Add toggle UI in NetworkCard with Hub icon and description
- Wire up toggle in SettingsScreen with service restart on change
Tests:
- Add 6 SettingsViewModel tests for transport node toggle
- Add 11 Python tests for transport node config generation
- Add 5 ReticulumConfig model tests for enableTransport field
Closes#49🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When messages are sent via propagation (relay), LXMF sets state=SENT
when the relay accepts the message, not state=DELIVERED. Previously,
our callback ignored this distinction and always reported "delivered",
misleading users into thinking the recipient had received the message.
Changes:
- Check lxmf_message.state in _on_message_delivered() callback
- Report "propagated" status when state=SENT (relay accepted)
- Report "delivered" status only when state=DELIVERED (recipient confirmed)
- UI shows single checkmark for propagated (same as sent)
- Added unit tests for both direct and propagated delivery scenarios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a selected relay is offline and message propagation fails, the system
now automatically tries alternative relays before marking the message as
permanently failed. Also adds a manual "Retry" option in the message context
menu for failed messages.
Key changes:
- Python: Modified _on_message_failed() to request alternative relays from
Kotlin when propagation fails, with tracking to prevent infinite loops
- Kotlin: Added getAlternativeRelay() to PropagationNodeManager to find the
nearest available relay excluding previously tried ones
- IPC: Added onAlternativeRelayRequested callback and provideAlternativeRelay
method for Python-Kotlin communication
- UI: Added "Retry" menu item for failed messages and retryFailedMessage()
in MessagingViewModel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add 3 tests for the identity lookup retry mechanism in send_lxmf_message_with_method():
- test_requests_path_when_identity_not_found
- test_path_request_timeout_returns_error
- test_path_request_exception_handled
Uses time.sleep mocking to avoid actual 5-second waits.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When sending to a destination whose identity isn't cached (e.g., peer hasn't
announced since app start), automatically call RNS.Transport.request_path()
and wait up to 5 seconds for the network to respond with the path.
This enables sending messages to known peers without waiting for their
periodic announces, improving UX for users resuming conversations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When retrying an opportunistic message via propagation, the message was
missing the required propagation stamp (proof-of-work), causing the
propagation node to reject it.
Added:
- Clear propagation_packed and propagation_stamp before retry
- Set defer_propagation_stamp=True to trigger stamp generation
- Message now goes through pending_deferred_stamps → stamp generation →
pending_outbound → delivery
Also added unit tests for:
- Stamp generation flag configuration on retry
- Fresh messages without prior propagation data
- No retry when propagation node not configured
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests were expecting `_announce_handler_ref` (single handler) but
implementation uses `_announce_handlers` dict with aspect-specific
handlers. Tests were never run in CI until recent coverage changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When sending opportunistic messages to offline recipients, the message
would get stuck in "Sent" state forever waiting for a delivery receipt
that never arrives. This adds a 30-second timeout mechanism that
triggers propagation fallback for undelivered opportunistic messages.
Changes:
- Add tracking dict for opportunistic messages with timestamps
- Add background timer thread checking every 10s for timeouts
- After 30s without delivery, trigger _on_message_failed() to retry
via propagation node (using existing fallback logic)
- Update deliveryMethod to "propagated" when retrying via propagation
so UI correctly shows "Delivered to propagation network"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix TooManyFunctions in PropagationNodeManager by removing restartPeriodicSync
- Fix TooManyFunctions in MessagingViewModel by extracting helpers to top-level
- Fix CyclomaticComplexity in sendMessage by refactoring into smaller methods
- Fix DestructuringDeclarationWithTooManyEntries in MessageDetailScreen
- Remove runBlocking antipattern from PropagationNodeManager StateFlow init
- Apply ktlint formatting fixes across codebase
- Update tests for refactored code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement comprehensive relay (propagation node) support:
- Add PropagationNodeManager for auto-selection of nearest relay by hop count
- Add "Set as My Relay" button on node details for propagation nodes
- Add "MY RELAY" section in contacts screen (separate from pinned)
- Show "(auto)" badge when relay is auto-selected vs manually chosen
- Add "Unset as Your Relay?" confirmation dialog with auto-selection explanation
- Add relay management methods to ContactDao (setAsMyRelay, clearMyRelay, getMyRelay)
- Add isMyRelay field to ContactEntity and EnrichedContact
- Add message delivery settings (default method, retry via relay on fail)
- Add Python layer support for propagation node configuration
- Add instrumented tests for relay DAO operations
The relay system follows Sideband's algorithm: auto-select nearest node,
only switch if new node has fewer or equal hops. Users can manually select
a relay which disables auto-selection until re-enabled.
🤖 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>
When another Reticulum app (e.g., Sideband) is already using AutoInterface
ports (29716/42671), RNS would call sys.exit(255) and crash the service.
This fix:
- Pre-checks IPv6 UDP port availability before RNS initialization
- Detects conflicts and removes AutoInterface from config proactively
- Allows the rest of RNS (including RNode) to initialize normally
- Adds getFailedInterfaces() AIDL method for future UI warnings
The port check uses IPv6 UDP sockets without SO_REUSEADDR to match how
AutoInterface actually binds (IPv6 multicast for discovery, IPv6 link-local
for data transfer).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Revert LOG_EXTREME hardcoding to use config-based log level
- Remove all diagnostic patches (TX/RX logging, Transport patches, etc.)
- Fix socket.if_nametoindex patch to use netinfo fallback on Windows
and Android/Chaquopy where the socket function is unreliable
- Remove old disabled/commented patch code
The v1.0.4 RNS release has correct AutoInterfacePeer.should_ingress_limit()
behavior (returns False), so runtime patches for ingress limiting and
forward-to-peers are no longer needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When Columba detects that the shared Reticulum instance (e.g., Sideband)
goes offline while using it, Columba now automatically:
1. Sets preferOwnInstance = true (so toggle shows correct state)
2. Restarts the service with Columba's own network interfaces
3. Shows a "Restarting Service" modal during the restart
4. Displays an informational banner explaining what happened
5. Re-enables Manage Interfaces, BLE Connections, and Service Control
Key changes:
- Add isSharedInstanceAvailable() to AIDL and service protocol
- Add availability monitor that detects shared instance going offline
- Save preferOwnInstance=true on auto-restart (fixes toggle state bug)
- Pass sharedInstanceOnline to NetworkCard, BleConnectionsCard, ServiceControlCard
- Update informational state condition in SharedInstanceBannerCard
- Add comprehensive unit tests for transition flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
- 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>
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>
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>
- 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>
Call ensureIdentityFileExists() to recover identity file from database
keyData if missing. Remove silent Python fallback to prevent identity
mismatches that cause message delivery to fail in shared instance mode.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use forked RNS from torlando-tech/Reticulum@fix-phy-stats-rpc with patch
to catch exceptions in __update_phy_stats(). This prevents crashes when
connecting to another app's shared instance (e.g., Sideband) where RPC
authentication fails due to different identity keys.
Without this patch, RPC AuthenticationError would propagate up and crash
the entire packet processing chain, breaking message delivery.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Android's app sandboxing prevents Unix domain sockets from working
between different apps. Added `shared_instance_type = tcp` to the
RNS config so Columba can properly connect to Sideband's shared
instance via TCP on port 37428.
Additional changes:
- Replace one-way button with bidirectional toggle for instance mode
- Show banner when using own instance (so user can toggle back)
- Add restart dialog when switching instance modes
- Disable Service Control card when using shared instance
- Disable BLE Connections card when using shared instance
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract and display stamp cost information from LXMF announces:
- Propagation nodes: show stamp cost with flexibility range and peering cost
- Regular peers: show stamp cost when available
Changes:
- Python: extract stamp costs using LXMF canonical functions
- Add stampCost, stampCostFlexibility, peeringCost to AnnounceEvent model
- Pass stamp costs through PollingManager and ServiceReticulumProtocol
- Add database migration 20->21 for new columns
- Display stamp cost info cards in AnnounceDetailScreen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Offload announce name parsing to Python's canonical LXMF functions:
- LXMF.display_name_from_app_data() for lxmf.delivery and nomadnetwork.node
- LXMF.pn_name_from_app_data() for lxmf.propagation
Pass pre-parsed displayName from Python to Kotlin via AnnounceEvent.
Simplify AppDataParser to just use displayName or generate fallback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add backend infrastructure to detect and connect to shared Reticulum
instances (e.g., from Sideband running on the same device).
Changes:
- ReticulumConfig: Add preferOwnInstance field
- reticulum_wrapper.py: Add _check_shared_instance_available() to detect
TCP shared instances on port 37428, update _create_config_file() to
support shared instance mode, return is_shared_instance in initialize()
- ServiceReticulumProtocol: Parse is_shared_instance from result and
save to SettingsRepository
- PythonWrapperManager: Parse is_shared_instance and pass to callback
- ReticulumServiceBinder: Include is_shared_instance in callback JSON
- InterfaceConfigManager: Pass preferOwnInstance to ReticulumConfig
- ColumbaApplication: Load and pass preferOwnInstance preference
Flow:
1. User preference preferOwnInstance loaded from SettingsRepository
2. If false, Python checks for TCP connection to 127.0.0.1:37428
3. If shared instance found, config uses share_instance=yes mode
4. is_shared_instance result saved to SettingsRepository
5. UI reacts via SettingsViewModel reading from repository
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
AutoInterface was using incorrect default ports (48555/49555) that
didn't match RNS defaults (29716/42671), preventing peer discovery
on local WiFi/Ethernet.
Made discovery_port and data_port nullable/optional. When omitted,
RNS automatically uses its defaults. This is more future-proof than
hardcoding values.
- Changed port types from Int to Int? with null default
- Updated serialization to only write ports when explicitly set
- Updated UI placeholders to show RNS defaults (29716/42671)
- Added detekt suppressions for validation function complexity
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Enable importing contacts using only a 32-character destination hash
from Sideband's "Copy Address" feature, in addition to full lxma:// URLs.
Key changes:
- Add ContactStatus enum (ACTIVE, PENDING_IDENTITY, UNRESOLVED)
- Make publicKey nullable in ContactEntity for pending contacts
- Add parseIdentityInput() to InputValidator for flexible input parsing
- Add IdentityResolutionManager for background identity resolution
- Check existing announces when adding hash-only contacts
- Update UI with pending/unresolved status indicators
- Add PendingContactBottomSheet for managing unresolved contacts
- Hook announce callbacks for instant resolution when peer announces
- Add recallIdentity() AIDL method to check Reticulum's identity cache
- Database migration 17->18 for new schema
The flow:
1. User pastes 32-char hash -> check announces table for existing identity
2. If found -> add as ACTIVE contact immediately
3. If not found -> add as PENDING_IDENTITY, background resolution kicks in
4. When announce received -> instantly resolve pending contact
5. After 48h timeout -> mark as UNRESOLVED with retry option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add ensureIdentityFileExists() to IdentityRepository to verify/recover
identity files from keyData backup before service restart
- Update InterfaceConfigManager to use canonical identity_<hash> paths
instead of fragile default_identity file
- Add Python safety check to refuse creating new identity when specific
path was requested but file is missing
- Remove copy-to-default_identity logic from IdentityManagerViewModel
- Add unit tests for identity file recovery scenarios
This fixes the bug where the Python service would silently create a new
identity when the default_identity file was deleted during service restart,
causing the UI to show different identities on different screens.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>