Remove conditional that only checked recipient_dest.hash when hashes
didn't match - links are stored under recipient_dest.hash so this check
must always run. Also clean up stale links from both direct_links and
backchannel_links when found.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Clean up stale/closed links when discovered during existing link check
- Use identity comparison in finally block to only remove our own link,
preventing concurrent calls from incorrectly removing each other's links
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Clean up stored link and call teardown() when an exception occurs,
matching the cleanup done on timeout. Safely handles cases where
recipient_dest or link may not be defined yet.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, request_path() was called unconditionally even when a path
already existed. This generated unnecessary network broadcasts. Now we
only request a path if we don't have one. If the existing path is stale,
link establishment will fail and can be retried.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When link establishment times out, the link object was being removed
from direct_links but teardown() wasn't called. This left the underlying
RNS link in limbo, potentially consuming resources. Now properly closes
the link before removing from the dictionary.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add 35 new tests for link management in reticulum_wrapper.py:
- TestEstablishLink: 12 tests for establish_link()
- TestCloseLink: 7 tests for close_link()
- TestGetLinkStatus: 6 tests for get_link_status()
- TestProbeLinkSpeed: 8 tests for probe_link_speed()
- TestNextHopBitrate: 2 tests for next_hop_bitrate functionality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add LXMF mock in setUp/tearDown for TestOnLXMFDelivery tests
(LXMF is None in test environment, causing AttributeError)
- Update test_get_hop_count_returns_none to properly mock RNS.Transport
(function now implements hop count, test needed updating)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add missing delivery_method field to error returns in send_lxmf_message_with_method
- Use try/finally for temp image file cleanup to prevent leaks
- Add race condition guard in LinkSpeedProbe.probe() to skip if already probing
- Populate next_hop_bitrate_bps in probe_link_speed results from RNS interface
- Add try/finally bitmap recycling in ImageUtils to prevent memory leaks
- Fix None→0 conversion in RoutingManager helpers to omit field instead
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of sending empty LXMF messages to probe link speed, now uses
the reliable establish_link() function. Also checks backchannel_links
for existing incoming links from peer.
This removes ~80 lines of complex probe message handling code.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Always request fresh path even when one exists (avoids stale routes)
- Check backchannel_links in addition to direct_links for existing links
- Links are bidirectional, so incoming links from peer count as "active"
This fixes the issue where Device A shows "Online" but Device B shows
"last seen X minutes ago" even though they have an active link between them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixed 3 bugs in establish_link() that prevented manual link establishment
from working while LXMF DIRECT messages succeeded:
1. Missing identity recall fallbacks - now tries destination hash,
identity hash, and local cache (matching send_lxmf_message pattern)
2. Reversed path request logic - now requests path when one doesn't
exist instead of when one already exists
3. Wrong key for storing links - now uses recipient_dest.hash
(created destination hash) instead of input hash
Also updated close_link() and get_link_status() with same hash
handling logic for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ConversationLinkManager to track active links per conversation
- Open link when entering conversation, close after 5min inactivity
- Update online indicator to show "Online" when link is active
- Add establish_link(), close_link(), get_link_status() to Python wrapper
- Add AIDL methods and protocol support for link management
- LinkSpeedProbe now uses existing links from ConversationLinkManager
Note: Manual link establishment times out (investigating), but detecting
existing links from LXMF DIRECT message exchange works correctly.
🐾 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
Large images (>500KB) exceed Android Binder IPC transaction limits (~1MB).
Now large images are written to temp files and the path is passed through
AIDL instead of the raw bytes.
Changes:
- Add imageDataPath parameter to AIDL sendLxmfMessageWithMethod
- ServiceReticulumProtocol writes large images to temp files
- Python reads from file path and deletes temp after reading
- Cap loadBitmap to MAX_PREVIEW_DIMENSION regardless of preset
(prevents Canvas crash for ORIGINAL preset with huge images)
🐛 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
- Fix direct_links key lookup (LXMF uses hex strings, not bytes)
- Add find_direct_link() helper to check both key formats
- Fix float-to-long conversion for link rates from Python
- Reset probe state before starting new probe
- Check for late link establishment after timeout
🐛 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
- Send empty LXMF message to establish link instead of raw Link
- Add delivery method parameter to probe (direct vs propagated)
- Filter empty probe messages on receive side to prevent rendering
- Fix UnboundLocalError for has_text_content in message filtering
- Add next_hop_bitrate_bps field for heuristic-based recommendations
- Monitor message state for SENT/DELIVERED detection
🐛 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
Add ability to probe actual link speed to a destination by establishing
a Link and measuring the establishment rate. This provides end-to-end
path speed measurement that accounts for all intermediate hops.
Python layer:
- probe_link_speed() establishes link, measures rate, tears down
- Reuses existing active links if available
- Returns establishment_rate_bps, expected_rate_bps, rtt_seconds, hops
Kotlin layer:
- LinkSpeedProbeResult data class with helper methods
- Full AIDL/Service/Protocol stack integration
- Mock implementation for testing
Next: UI integration for quality selection dialog
🤖 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
1. PropagationTestBase: Use setUp/tearDown to properly scope the LXMF
mock instead of setting it at module level (which leaked between tests)
2. test_propagated_falls_back_to_direct_without_propagation_node: Updated
test to match new behavior - PROPAGATED now falls back to DIRECT when
no propagation node is set (instead of failing)
3. test_on_message_failed_retries_via_propagation: Set mock_message.fields
to empty dict so "5 in fields" check works correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The get_propagation_state() method uses LXMF.LXMRouter.PR_* constants to
map state integers to human-readable names. When LXMF was mocked as a
plain MagicMock(), these constants became MagicMock objects instead of
integers, causing the state lookup to fail.
Fix: Define all PR_* constants in the mock setup and set
reticulum_wrapper.LXMF to the mock (since the module-level variable
is only populated during initialize() which tests skip).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move native stamp generator registration to pre-init callback
(prevents Python's multiprocessing.Manager() from hanging on Android)
- Start PropagationNodeManager early after service binding
- Add retry logic for relay sync when Python not yet initialized
- Fall back to DIRECT when PROPAGATED requested but no relay set
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Run ktlintFormat to fix formatting issues
- Refactor supersedePendingFileNotifications to use functional approach
instead of loop with multiple continues
- Extract tryParseNotificationMatch helper to reduce return statements
- Use PropagationState constants from ReticulumProtocol instead of
duplicating them in ServiceNotificationManager
- Simplify complex condition in MessageMapper with helper variables
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Defer pending file notification until propagation is confirmed on relay
- Track hasFileAttachments before extraction to fix supersede matching
- Add service-side supersede logic for pending file notifications
- Only show sync complete toast for manual syncs, not automatic/periodic
- Add _isManualSync flag to PropagationNodeManager for toast control
- Add real-time progress tracking with 1% threshold for callbacks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, manual sync would immediately show "Sync complete" even when
large files were still downloading in the background. This fix implements
event-driven sync completion that waits for LXMF's PR_COMPLETE state.
Changes:
- Python: Add propagation state callback with 100ms polling during sync
- AIDL: Add onPropagationStateChanged callback
- ServiceReticulumProtocol: Add propagationStateFlow for state observation
- PropagationNodeManager: Wait for PR_COMPLETE before emitting success
- UI: Sync button shows spinner during sync, tappable for status details
- New SyncStatusBottomSheet component shows real-time sync progress
- Tests: Update PropagationNodeManagerTest for new async completion flow
The sync now properly tracks LXMF propagation states:
- PR_PATH_REQUESTED → PR_LINK_ESTABLISHING → PR_LINK_ESTABLISHED
- → PR_REQUEST_SENT → PR_RECEIVING → PR_COMPLETE
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add test coverage for:
- SettingsRepository: incoming message size limit save/load with clamping
- SettingsViewModel: runtime limit updates and protocol integration
- FileUtils: temp attachment write, cleanup, and size limit functions
- Python reticulum_wrapper: initialize with limit and runtime adjustment
This improves code coverage for the incoming message size limit feature
which allows users to configure the maximum size of incoming messages.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test_send_with_both_image_and_file_attachments test was failing
because the mocked LXMF module returned a MagicMock for FIELD_IMAGE
instead of the expected integer 6. Set the constant explicitly on the
mock to ensure the fields dict uses the correct key.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add user-configurable setting to control maximum incoming LXMF message size:
- Default 1MB (1024KB), range 512KB to 128MB
- UI with preset chips (1MB, 5MB, 10MB, 25MB, Unlimited) plus custom dialog
- Runtime update via LXMRouter.delivery_per_transfer_limit
- Persisted in DataStore preferences
Implementation:
- SettingsRepository: new preference with flow and save method
- AIDL: setIncomingMessageSizeLimit(int limitKb) method
- Python: set_incoming_message_size_limit() and delivery_limit in init
- SettingsViewModel: state field and update method
- MessageDeliveryRetrievalCard: size limit UI section
Also fixes test issues:
- MessagingViewModelTest: update mock parameter count (10 params)
- ConversationRepositoryTest: add missing attachmentStorage mock
- MessageDeliveryRetrievalCardTest: handle duplicate "Custom" chips
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove 512KB file size limit and implement file-based transfer for large
attachments to bypass Android Binder IPC (~1MB) and SQLite CursorWindow
(~2MB) limits.
Changes:
- Add temp file transfer for attachments >500KB via AIDL paths
- Extract large attachments to disk before database storage
- Move hex encoding to background thread with efficient lookup table
- Add compression warning dialog with hint to use file attachments
- Python reads large files from disk paths and cleans up temp files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Send icon appearance when sending messages to enable Sideband/MeshChat
users to see Columba profile icons (was not being passed before)
- Add profile icon fields to ContactMarker for map rendering
- Create MarkerBitmapFactory.createProfileIconMarker() for MDI icon markers
- Use ProfileIcon in ContactLocationBottomSheet instead of Identicon
- Update MapScreen to show profile icons on map markers when available
- Add comprehensive unit tests for new functionality
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BLEInterface.py is provided by ble-reticulum pip dependency.
No need to maintain a duplicate local copy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The fix/ble-peer-interface-cleanup branch has been merged to main.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ktlint_standard_function-naming = disabled for Compose functions
- Move inline comments to separate lines per ktlint rules
- Fix import ordering and multiline expression formatting
- Fix line length exceeding 160 characters
- Auto-format trailing commas and expression body placement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rewrite _validate_spawned_interfaces() with 3-pass approach:
- Pass 1: Collect orphaned addresses
- Pass 2: Clean up address mappings, track interfaces to detach
- Pass 3: Only detach interfaces with zero connected addresses
- Fragmenters only cleaned up when interface fully detached
- Enhance _spawn_peer_interface() reuse logic:
- Update address_to_identity and identity_to_address when reusing
- Cancel pending detach for the identity
- Mark interface as online
- Fix disconnect callbacks to preserve fragmenters:
- _device_disconnected_callback: defer fragmenter cleanup to grace period
- handle_central_disconnected: same fragmenter preservation
- _process_pending_detaches: clean up fragmenters on actual detach
- Rename _cleanup_stale_interface() to _cleanup_stale_address():
- No longer detaches interface during MAC rotation
- Only cleans up stale address-specific mappings
- Interface preserved for reuse with new address
Fixes orphaned peer interfaces and "No fragmenter for peer" warnings
during BLE MAC address rotation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When an address disconnects, check if other addresses still have the
same identity. Only detach the peer interface when ALL addresses for
that identity are disconnected.
Additionally, use a 2-second grace period before detaching to allow
new connections to establish during MAC rotation. If a new address
with the same identity connects within the grace period, cancel the
pending detachment.
This fixes the issue where peer interfaces were being rapidly created
and destroyed during MAC rotation, causing connection instability.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
BLE Connection Cleanup:
- Add pending identity timeout (30s) to disconnect non-Reticulum devices
(e.g., AirTags, BLE scanners) that connect but never complete handshake
- Fix cancelConnection not triggering cleanup callback - manually clean up
state and fire onCentralDisconnected after cancel
- Notify Python immediately for all peripheral connections (with or without
identity) so it can track pending connections
UI Improvements:
- Add live-updating connection duration timer in BLE connections screen
(updates every second using LaunchedEffect)
Build Changes:
- Point to ble-reticulum fix branch with identity timeout changes
- Load BLEInterface from local ble_modules for testing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement FIELD_ICON_APPEARANCE (0x04) for Sideband/MeshChat interop:
- Parse icon appearance from incoming LXMF messages
- Include icon in outgoing messages when identity has icon set
- Format: [icon_name, foreground_rgb, background_rgb]
- Convert RGB bytes to/from hex strings for Kotlin bridge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add tests for the new request_identity_resync() method that covers all
code paths in android_ble_driver.py:
- No bridge returns False with warning log
- Identity found returns True
- Identity not found returns False
- Exception caught and logged, returns False
Tests import and exercise the real AndroidBLEDriver class to provide
actual codecov coverage on the source file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When Python's disconnect callback fires but the driver layer maintains
the GATT connection, data was being dropped because address_to_identity
was cleared.
Changes:
- Point ble-reticulum dependency to fix/identity-cache-on-disconnect branch
- Add requestIdentityResync() to KotlinBLEBridge for identity recovery
- Add request_identity_resync() to android_ble_driver.py as bridge method
The ble-reticulum fix adds identity caching (60s TTL) and cache lookup
in _handle_ble_data. When cache is also empty, it calls the driver's
request_identity_resync() method to recover identity from Kotlin.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The previous approach relied on mock_rns global which wasn't properly
connected to the reimported module. Now we:
1. Store reference to the actual module (abd_module)
2. Patch RNS.log in the module's namespace in setUp
3. Restore it in tearDown
4. Capture log calls directly instead of using mock assertions
This ensures the log capture works regardless of import order or
coverage instrumentation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add tests for the new ensure_advertising() method that covers all
code paths in android_ble_driver.py:
- No bridge returns False with warning log
- Bridge returns True when advertising active
- Bridge returns False triggers restart log
- Exception caught and logged, returns False
Tests import and exercise the real AndroidBLEDriver class to provide
actual codecov coverage on the source file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Android silently stops BLE advertising when the app goes to background,
screen turns off, or device enters Doze mode - without calling any
callback. This leaves the app thinking it's advertising when it's not.
Add a proactive refresh mechanism that restarts advertising every 60
seconds to ensure the device remains discoverable. Also add bridge
methods to allow Python to check/ensure advertising is active.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace submodule with pip install from GitHub. This simplifies
dependency management and avoids namespace collision with RNS.Interfaces.
Changes:
- Remove external/ble-reticulum submodule
- Update build.gradle.kts to install from GitHub branch
- Update reticulum_wrapper.py to use ble_reticulum package
- Remove duplicate BLE files from python/ble_modules/
- Fix KotlinBLEBridge address callback deduplication
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The `import json` statements inside _on_lxmf_delivery() caused Python
to treat `json` as a local variable throughout the function scope.
This resulted in "cannot access local variable 'json'" errors when
json.dumps() was called before those imports ran.
Since json and time are already imported at module level, the local
imports were unnecessary and harmful.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes#155 - Position sharing was being interpreted as "audio messages"
by Sideband because Columba was using the wrong LXMF field and encoding.
Changes:
- Switch from FIELD_AUDIO (0x07) to FIELD_TELEMETRY (0x02)
- Use msgpack-packed Telemeter binary format instead of JSON
- Add FIELD_COLUMBA_META (0x70) for Columba-specific signals (cease)
- Add backwards compatibility for receiving legacy field 7 messages
- Add u-msgpack-python dependency for binary serialization
- Add 36 unit tests for pack/unpack round-trip verification
The telemetry format now matches Sideband's sense.py Location sensor:
- Latitude/longitude as signed int microdegrees
- Altitude/speed/bearing/accuracy in centimeter units
- Unix timestamp in seconds
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add test_wrapper_heartbeat.py with 19 tests for Python heartbeat thread
and global exception handler
- Add DebugViewModelPersistenceTest.kt with tests for new persistence
debug info fields (heartbeatAgeSeconds, healthCheckRunning, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes#40
The shutdown() method was trying to clear self.announce_app_data,
but this attribute was never initialized in __init__. This caused
an AttributeError during shutdown, preventing clean Python wrapper
cleanup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The reaction detection code checks lxmf_message.fields, so tests
need to mock this attribute to avoid TypeError.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive test suite for ReticulumWrapper reaction methods:
- TestSendReaction: Tests send_reaction method including success case,
not initialized, no router, various emojis, identity not found,
cached identity fallback, Reticulum unavailable, and callback registration
- TestReactionReceiveCallback: Tests _on_lxmf_delivery reaction detection,
skipping regular message processing, missing callback handling,
regular messages not treated as reactions, callback error handling,
and various emoji types including ZWJ sequences
- TestReactionReceivePolling: Tests poll_received_messages marking
reaction messages with is_reaction flag and reaction fields
- TestSetReactionReceivedCallback: Tests callback registration
- TestReactionField16Structure: Verifies Field 16 contains required
keys (reaction_to, emoji, sender) with correct values
- TestReactionEdgeCases: Tests jarray conversion, empty emoji,
missing sender, partial Field 16, and exception handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>