Commit graph

43 commits

Author SHA1 Message Date
torlando-tech
04d7acf622 feat: add reply-to-message feature (WIP)
- Add LXMF Field 16 support for extensible app extensions
- Add replyToMessageId column to MessageEntity with migration 23→24
- Add swipe-to-reply gesture component (toward center)
- Add ReplyPreviewBubble and ReplyInputBar UI components
- Add async reply preview loading in MessagingViewModel
- Update MessagingScreen with reply UI integration
- Add 17 tests for reply parsing in MessageMapper
- Add 11 tests for reply UI components

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 14:17:06 -05:00
torlando-tech
7f0e517ec0 feat: implement location sharing with cease message support
Add bidirectional location sharing between contacts:
- LocationSharingManager handles outgoing sharing sessions
- LocationTelemetry model with cease flag for stop notifications
- ReceivedLocationDao/Entity for storing incoming locations
- ContactLocationBottomSheet for viewing contact details on map
- MapViewModel combines contacts + announces for display names
- Python wrapper filters location-only messages from chat view

When a user stops sharing, a cease message is sent to recipients
causing immediate marker removal instead of waiting for expiry.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 19:02:52 -05:00
torlando-tech
39487bf64c feat: add file transfer support via LXMF Field 5
Implement sending and receiving file attachments of any type using
LXMF Field 5 (FILE_ATTACHMENTS). Key features:

- Send any file type via file picker (multiple files supported)
- 512KB combined size limit (same as images)
- File display card with type icon, filename, and size
- Size indicator with visual feedback when approaching limits
- Tap received files to save them

Technical changes:
- Add FileAttachment data class and FileUtils utilities
- Add FileAttachmentCard and FileAttachmentPreviewRow UI components
- Extend MessagingViewModel with file attachment state management
- Update Python wrapper to handle Field 5 send/receive
- Add Field 5 parsing to MessageMapper
- Update protocol layer to pass file attachments through
- Handle Java ArrayList to Python list conversion in wrapper

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 17:29:22 -05:00
torlando-tech
ee6fabc58b refactor: remove message polling entirely, use startup drain
Replace continuous 2-30s fallback polling with a one-time startup drain.
The pending_inbound queue already handles messages that arrive before
callback registration, so we only need to drain it once at startup.

Changes:
- Remove startMessagesPolling() loop entirely from PollingManager
- Remove messagesPoller SmartPoller (no longer needed)
- Add drainPendingMessages() for one-time startup drain
- Update ReticulumServiceBinder to call drain instead of startPolling
- Remove messagePollingJob from ServiceState
- Update Python docstrings to reflect new architecture

Architecture is now:
- Startup: drain any queued messages
- Runtime: 100% event-driven via callbacks
- No continuous polling for messages

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 21:55:03 -05:00
torlando-tech
9e119fc890 refactor: remove 1s message polling, use event-driven callbacks
Message delivery is now primarily event-driven via Python callbacks.
The aggressive 1s conversationPoller is removed since the event-driven
callback infrastructure is already in place and working:

- ReticulumServiceBinder registers kotlin_message_received_callback
- Python _on_lxmf_delivery() invokes callback for immediate delivery
- PollingManager.handleMessageReceivedEvent() processes the events

Changes:
- Remove conversationPoller (1s fixed interval) from PollingManager
- Keep messagesPoller (2-30s adaptive) as fallback safety net
- Simplify setConversationActive() to just track state
- Reduce verbose debug logging in poll_received_messages()
- Remove PATH TABLE DIAGNOSTIC spam from Python polling

This significantly reduces battery drain when conversations are active
by eliminating the Python/Kotlin boundary crossing every second.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 21:40:43 -05:00
torlando-tech
5a2356bbfc feat: Add native Kotlin stamp generator for Android
Replace Python multiprocessing-based stamp generation with native Kotlin
implementation. Python multiprocessing fails on Android due to lack of
sem_open support and aggressive process killing.

Changes:
- Add StampGenerator.kt with HKDF, SHA256, and parallel stamp search
- Add StampGeneratorTest.kt with Python-generated test vectors
- Add callback in PythonWrapperManager to bridge Python to Kotlin
- Register stamp generator in ReticulumServiceBinder
- Update requirements.txt to use LXMF fork with external generator support
- Add msgpack-core dependency for MessagePack encoding

Performance: ~9300 rounds/sec (vs ~1400 with broken Python multiprocessing)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 18:29:59 -05:00
torlando-tech
7db2aa714b fix: Update message status immediately for propagated messages
When sending messages via propagation (directly to relay), the message
status was stuck at "pending" because send_lxmf_message_with_method()
didn't check for immediate SENT state transition like the older
send_lxmf_message() method does.

For PROPAGATED messages, the relay accepts the message immediately and
LXMF transitions to SENT state. However, the end recipient never sends
a delivery confirmation (they pull from the relay), so we must check
the state immediately after handle_outbound() and invoke the status
callback.

This fix adds the same state check that exists in send_lxmf_message()
to send_lxmf_message_with_method(), ensuring propagated messages show
"sent" status (single checkmark) instead of staying "pending".

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:11:23 -05:00
torlando-tech
a83ac4f0e0 fix: Show accurate toast feedback when saving contacts
Previously, MessagingScreen showed "Saved to Contacts" immediately
without waiting for the async operation result. If the peer's public
key wasn't available (e.g., they never announced), the save would
silently fail despite showing success.

Changes:
- Add ContactToggleResult sealed class to emit success/error from toggleContact()
- Update MessagingScreen to collect results and show accurate toasts
- Extract sender's public key from RNS identity cache in poll_received_messages()
- Add publicKey field to ReceivedMessage and pass through the pipeline
- Store public key from messages in peer_identities table

Now users see "Identity not available - peer hasn't announced" when
save fails, and contacts can be saved from message senders even if
they never announced.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 18:01:50 -05:00
Torlando
47b1955d74
Merge pull request #95 from torlando-tech/feature/bulk-peer-restore
perf: Optimize peer identity restoration from minutes to milliseconds
2025-12-15 10:40:03 -05:00
torlando-tech
aa0f757953 fix: Enable async TCP startup to prevent initialization timeout
Configure TCPClientInterface.SYNCHRONOUS_START = False during RNS import
to run TCP connections in background threads instead of blocking.

Problem: Reticulum initialization was taking 15.7s, exceeding the 15s
ANR timeout. The main cause was synchronous TCP connection attempts to
unreachable hosts, each blocking for up to 5 seconds.

Solution: Set SYNCHRONOUS_START = False after importing RNS but before
calling RNS.Reticulum(). This makes TCP connections non-blocking while
preserving all reconnection logic (initial_connect() already handles
success/failure paths and spawns reconnect threads as needed).

Result: Initialization time reduced from 15.7s to 11.9s, well under
the 15s timeout threshold.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 10:08:37 -05:00
torlando-tech
d71de9f86f perf: Optimize peer identity restoration from minutes to milliseconds
"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>
2025-12-14 22:13:35 -05:00
torlando-tech
271a9e3afb fix: Correct TCP RNode config format and add stub modules
## 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>
2025-12-14 00:10:49 -05:00
torlando-tech
599e0753ea feat: Add TCP/WiFi connectivity for RNode devices
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>
2025-12-13 22:57:59 -05:00
torlando-tech
e6b4d670e3 feat: Add transport node toggle in settings
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>
2025-12-13 21:48:55 -05:00
torlando-tech
82a395f563 fix: Distinguish propagated vs delivered status for relay messages
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>
2025-12-13 00:50:04 -05:00
torlando-tech
2c56e98ba5 feat: Add automatic relay fallback and manual retry for failed messages
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>
2025-12-13 00:01:13 -05:00
torlando-tech
fa40217e60 feat: Auto-request path for unknown destinations
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>
2025-12-12 19:18:48 -05:00
torlando-tech
c6d543cecb fix: Add propagation stamp generation for retry fallback
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>
2025-12-12 18:36:36 -05:00
torlando-tech
4225f9e3e3 feat: Add timeout fallback for opportunistic messages to propagation
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>
2025-12-12 18:16:46 -05:00
torlando-tech
afc27d6cdb refactor: Fix detekt/ktlint issues and remove runBlocking antipattern
- 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>
2025-12-12 16:58:06 -05:00
torlando-tech
a11ee755f2 feat(relay): Add propagation node relay management with auto-selection
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>
2025-12-12 01:01:05 -05:00
torlando-tech
504051af96 fix: make AutoInterface port conflicts non-fatal
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>
2025-12-10 22:59:08 -05:00
torlando-tech
bd6d72c150 fix: clean up AutoInterface patches and restore config-based logging
- 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>
2025-12-10 16:13:31 -05:00
torlando-tech
e9842fa2c0 feat: auto-restart with own instance when shared instance goes offline
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>
2025-12-09 23:44:16 -05:00
torlando-tech
166bb16300 fix: address 8 critical issues from PR review with TDD
Critical fixes:
- Fix memory leak: RSSI polling now stopped in onCleared()
- Fix silent BLE scan errors: user-friendly error messages added
- Fix double-check locking bug in Python RNode initialization
- Fix interface registration order: start() before Transport register
- Fix race condition: use threading.Event for read loop control
- Fix write retry: implement exponential backoff (0.3s, 1s, 3s)
- Fix BLE write latch race: null check prevents stale callbacks
- Fix MTU request hang: 2-second timeout falls back to discoverServices

Tests added:
- RSSI polling cancellation test
- BLE scan error handling test
- Thread safety tests for read loop
- Write retry exponential backoff tests

Also includes ktlint format auto-fixes.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 13:07:10 -05:00
torlando-tech
d8300627a9 feat: add event-driven UI refresh for RNode connection status
- 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>
2025-12-09 11:40:28 -05:00
torlando-tech
af930401d6 feat: add human-readable RNode error messages with UI callback
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>
2025-12-08 21:58:48 -05:00
torlando-tech
d75bd2d77f feat: display Columba logo on RNode OLED via external framebuffer
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>
2025-12-08 21:58:48 -05:00
torlando-tech
45f4d02482 feat: add automatic RNode reconnection after disconnect
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>
2025-12-08 21:58:48 -05:00
torlando-tech
d91287ef66 feat: add automatic RNode reconnection and interface status UI
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>
2025-12-08 21:58:48 -05:00
torlando-tech
2b46964125 feat: add RNode LoRa interface support via Bluetooth
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>
2025-12-08 21:58:47 -05:00
Torlando
485a95e5cc
Merge pull request #58 from torlando-tech/bugfix/42-shared-instance
Bugfix/42 shared instance
2025-12-08 21:51:59 -05:00
torlando-tech
d0a84eb399 fix: ensure identity file exists before Python initialization (#42)
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>
2025-12-08 12:11:18 -05:00
torlando-tech
20d1bbbfe1 fix: use TCP for shared instance on Android (#42)
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>
2025-12-07 23:44:09 -05:00
torlando-tech
9868ec5463 feat: display stamp costs in node details screen
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>
2025-12-07 19:23:42 -05:00
torlando-tech
b037553397 fix: use Python LXMF functions for app_data name parsing (#41)
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>
2025-12-07 19:23:42 -05:00
torlando-tech
6cecf39303 feat: implement backend shared instance detection (#42)
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>
2025-12-07 15:57:09 -05:00
torlando-tech
16a2594256 fix: use RNS default ports for AutoInterface #38
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>
2025-12-07 01:05:58 -05:00
torlando-tech
c36afe7c36 feat: add Sideband contact import support
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>
2025-12-04 10:20:51 -05:00
torlando-tech
6e24f6677a fix: prevent identity mismatch by using canonical file paths
- 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>
2025-12-03 22:12:16 -05:00
torlando-tech
d0050e2ce5 feat: add data migration export/import feature
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>
2025-12-02 09:25:03 -05:00
torlando-tech
808b3f3aac fix: add IFAC network_name and passphrase support for TCP interfaces
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>
2025-12-01 19:28:21 -05:00
torlando-tech
0ce4fba7a3 initial commit 2025-11-30 19:59:14 -05:00