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>
- 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>
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>
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>
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>
Implements a complete data migration system allowing users to export all app
data from the old app (com.lxmf.messenger) and import it into the new app
(tech.torlando.columba) when changing applicationId for F-Droid publishing.
## Feature Overview
Users can export their data to a `.columba` file (ZIP archive) containing:
- manifest.json: Serialized MigrationBundle with all data
- attachments/: Directory with message attachments
The import process restores all data and restarts the Reticulum service to
apply the imported identities and peer information.
## Data Migrated
| Data Type | Description |
|-----------|-------------|
| Identities | Private keys, display names, destination hashes |
| Conversations | Peer info, last message, unread counts |
| Messages | Full message history with status and timestamps |
| Contacts | Custom nicknames, notes, tags, pinned status |
| Announces | Known peers with public keys for network recognition |
| Settings | Notifications, auto-announce, theme preferences |
| Attachments | Message attachments (images, files, etc.) |
## New Files
- `migration/MigrationData.kt` - Data classes for serialization
- `migration/MigrationExporter.kt` - Export logic with progress callbacks
- `migration/MigrationImporter.kt` - Import logic with validation
- `viewmodel/MigrationViewModel.kt` - UI state management
- `ui/screens/MigrationScreen.kt` - Export/import UI
- `ui/screens/settings/cards/DataMigrationCard.kt` - Settings integration
## Key Implementation Details
### Export Flow
1. Collect all data from Room database
2. Base64-encode binary fields (keys, attachments)
3. Serialize to JSON manifest
4. Create ZIP archive with manifest + attachments
5. Share via FileProvider
### Import Flow
1. Parse ZIP and validate manifest version
2. Import identities (with Reticulum key recovery)
3. Bulk insert conversations, messages, contacts
4. Import announces to database
5. Extract attachments to app storage
6. Apply settings and mark onboarding complete
7. Restart service to apply changes
### Announce Restoration
InterfaceConfigManager Step 9b restores all announce peer identities to
Python Reticulum's known_destinations cache on service restart, enabling
immediate peer recognition without re-announcing.
## DAO Additions
- `AnnounceDao`: getAllAnnouncesSync(), insertAnnounces(), getAnnounceCount()
- `ContactDao`: getAllContactsSync(), insertContacts()
- `ConversationDao`: getAllConversationsList(), insertConversations()
- `MessageDao`: getAllMessagesForIdentity(), insertMessages()
## Dependencies
Added kotlinx-serialization for JSON serialization of migration bundles.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Add optional network_name and passphrase fields to TCP interface
configuration, enabling Reticulum's Interface Access Code (IFAC)
cryptographic authentication.
Changes:
- Add networkName and passphrase fields to TCPClient data class
- Update InterfaceRepository JSON serialization/deserialization
- Add UI fields in Advanced Options with info text explaining IFAC
- Passphrase field hidden by default with visibility toggle
- Pass IFAC params from Kotlin to Python via JSON bridge
- Generate network_name and passphrase in config file
IFAC allows interfaces with matching network_name and passphrase to
communicate securely by signing and verifying packets using keys
derived from these shared credentials.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>