Commit graph

65 commits

Author SHA1 Message Date
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
2a1de3311b test: add Python tests for stamp generator callback
Added 5 Python tests for set_stamp_generator_callback:
- Callback storage in instance variable
- Registration with LXMF LXStamper
- Graceful handling of import errors
- Graceful handling of registration errors
- Setting callback to None (clearing)

Also fixed detekt EqualsNullCall violation in StampGeneratorTest.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 18:52:21 -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
6506042600 test: Improve Python test coverage for LXStamper patch and SENT state check
Added comprehensive integration tests that actually execute the production code:

LXStamper Threading Patch Tests:
- test_initialize_patches_lxstamper_job_android: Calls initialize() and then
  exercises the patched job_android function with low difficulty
- test_initialize_lxstamper_cancellation_path: Tests cancellation via active_jobs
- Fixed module mocking to properly set mock_lxmf.LXStamper attribute

SENT State Check Tests:
- test_send_does_not_call_on_message_sent_for_outbound_state
- test_send_handles_missing_state_attribute_gracefully
- test_send_handles_state_check_exception_gracefully
- test_send_with_propagated_state_does_not_trigger_sent_callback

Coverage increased from 81% to 83%.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:17:23 -05:00
torlando-tech
d9c1a194a0 test: Fix test_send_with_immediate_sent_state_check mock setup 2025-12-16 14:17:12 -05:00
torlando-tech
a89cb1eaee test: Add unit tests for LXStamper threading patch and state check
Add test coverage for the threading-based stamp generation and
immediate SENT state check to improve patch coverage.

Tests added:
- test_lxstamper_patched_to_use_threading: Verify LXStamper.job_android is patched
- test_lxstamper_patch_graceful_failure: Verify graceful failure handling
- TestLXStamperThreading: Test threading-based stamp generation behavior
- test_send_with_immediate_sent_state_check: Test SENT state detection

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 14:17:06 -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
c3eb6aac00 test: Add unit tests to improve patch coverage
- Add ContactToggleResult emission tests to MessagingViewModelTest
- Add public key extraction tests to test_wrapper_messaging.py
- Create MessageCollectorTest.kt for public key handling

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 18:53:42 -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
7091fbe67f test: Add 30 tests to improve TCP RNode patch coverage to 80%+
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>
2025-12-14 01:58:19 -05:00
torlando-tech
907c28bb27 test: Improve patch coverage from 20.94% to 80%+ with 61 new tests
Add comprehensive test coverage for TCP RNode functionality, focusing on uncovered branches and edge cases:

- RNodeWizardViewModel (13 tests): TCP validation, RSSI polling, pairing retry, CDM association, manual device name validation
- DeviceDiscoveryStep (18 tests): TCP mode UI, manual entry forms, device card interactions, edit mode
- ReviewConfigStep (8 tests): New test file for config review UI, region cards, duty cycle warnings, advanced settings
- reticulum_wrapper.py (12 tests): Error handling, callback registration, BLE path cleanup, state transitions
- Stub modules (5 tests): New test file for usb4a, jnius, usbserial4a import validation
- InterfaceRepository (5 tests): UDP/AutoInterface/AndroidBLE validation edge cases

All 61 tests pass successfully. Coverage improvements target TCP validation error paths, UI state management, Python error handling, and configuration validation.

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

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2025-12-14 01:30:09 -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
0f67b57202 test: Improve transport node toggle code coverage
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>
2025-12-13 21:48:55 -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
73d35b0754 test: Add path request retry logic tests
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>
2025-12-12 22:53:10 -05:00
torlando-tech
c61d58d31a test: Add Phase 2 tests for reticulum_wrapper.py coverage
Add 84 new tests to improve coverage of remaining uncovered code paths:

New files:
- test_wrapper_config.py: Tests for _remove_autointerface_from_config(), _setup_interface()
- test_wrapper_announce_polling.py: Tests for poll_received_announces()

Updated files:
- test_announce_handler.py: Add _announce_handler() internal logic tests
  (table extraction, LXMF parsing, stamp costs, Kotlin bridge notification)
- test_opportunistic_timeout.py: Add threading tests for timer lifecycle
- test_wrapper_peer_identity.py: Add error handling path tests

Total tests: 431 → 515

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 22:39:49 -05:00
torlando-tech
28cf53db53 test: Add comprehensive unit tests for reticulum_wrapper.py
Add 299 new tests across 10 test files to improve Python code coverage:

- conftest.py: Shared pytest fixtures for RNS/LXMF mocking
- test_wrapper_initialization.py: Constructor, bridges, config, init/shutdown
- test_wrapper_messaging.py: Message send, delivery callbacks, polling
- test_wrapper_identity.py: Identity CRUD, import/export, recovery
- test_wrapper_destination.py: Destination creation, announces
- test_wrapper_peer_identity.py: Peer identity recall/store/restore
- test_wrapper_propagation.py: Propagation node management
- test_wrapper_ble.py: BLE and RNode interface initialization
- test_wrapper_path.py: Path table, has_path, request_path
- test_wrapper_utilities.py: Debug info, echo, utility methods

All tests use pre-import mocking pattern to avoid RNS/LXMF dependencies.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 22:22:10 -05:00
Torlando
c84d0e9f2b
Merge pull request #79 from torlando-tech/feature/opportunistic-timeout
Feature/opportunistic timeout
2025-12-12 19:36:03 -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
1629c34107 test: Add coverage tests for sync and delivery callbacks
Add 14 new tests to improve code coverage:

PropagationNodeManager (5 tests):
- syncWithPropagationNode skips when no relay configured
- syncWithPropagationNode skips when already syncing
- syncWithPropagationNode updates lastSyncTimestamp on success
- syncWithPropagationNode handles protocol failure gracefully
- triggerSync emits NoRelay when no relay configured

Python delivery callbacks (6 tests):
- _on_message_delivered calls Kotlin callback with correct JSON
- _on_message_delivered handles missing callback gracefully
- _on_message_failed calls Kotlin callback with failed status
- _on_message_failed handles missing callback gracefully
- _on_message_sent calls Kotlin callback with sent status
- _on_message_sent handles missing callback gracefully

Python propagation node setting (3 tests):
- set_outbound_propagation_node stores node hash
- set_outbound_propagation_node clears with None
- get_outbound_propagation_node returns hex string

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-12 19:18:10 -05:00
Torlando
21ec3284fb
Merge pull request #78 from torlando-tech/feature/opportunistic-timeout
Feature/opportunistic timeout
2025-12-12 18:41:00 -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
44f49c58a5 fix: Update announce handler tests to match implementation
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>
2025-12-12 18:25:13 -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
0f29de2054 fix(ble): Fix MAC rotation reconnection and dual connection deduplication
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>
2025-12-11 13:02:35 -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
5a9fd950de fix: TDD fixes for BLE deduplication cooldown asymmetry and test stability
- 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>
2025-12-09 18:56:16 -05:00
torlando-tech
4a70196e31 fix: TDD fixes for PR #36 and custom mode UX improvements
- Fix Python RSSI/SNR race condition with proper lock acquisition
- Add manual device name validation (32 char limit, RNode name check)
- Add regulatory warnings for custom mode without region selection
- Preserve user-modified field errors during wizard navigation
- Improve custom mode UX: highlight selection, skip modem/slot steps
- Auto-expand advanced settings in custom mode review
- Hide region/modem/slot summary cards in custom mode
- Split large test file to pass detekt LargeClass rule

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 16:00:12 -05:00
torlando-tech
6fc4b519a6 fix: increase TX power validation limit to support regional maximums
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>
2025-12-09 14:14:45 -05:00
torlando-tech
cf49225939 fix: Python RNode interface thread safety and CI improvements
- 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>
2025-12-09 13:58:30 -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
b6b34603ec fix: add BLE write synchronization to prevent dropped commands
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>
2025-12-08 21:58:48 -05:00
torlando-tech
5d1cb96e30 refactor: extract RNodeConfigValidator and add comprehensive unit tests
- 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>
2025-12-08 21:58:48 -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
b72d0227e8 fix: disable external framebuffer when logo setting is turned off
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>
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