struct.pack("!H") requires 0-65535 but coarsened locations with
approxRadius > 655m produce values exceeding this, crashing the send.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two bugs prevented icon rendering on Columba-to-Columba location sharing:
1. Python: location_event dict passed to kotlin_location_received_callback
never included FIELD_ICON_APPEARANCE data — it was only parsed for the
message callback path, not the location callback path.
2. Kotlin MapViewModel: marker icons were read exclusively from the RNS
announce cache (announce?.iconName), ignoring the appearanceJson stored
in ReceivedLocationEntity. Announce cache only updates on chat messages,
so icon changes didn't propagate until a regular message was sent.
Fix: parse FIELD_ICON_APPEARANCE into location_event['appearance'] on
receive, and prefer telemetry appearance over announce data in MapViewModel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Columba was sending FIELD_TELEMETRY (0x02) without FIELD_ICON_APPEARANCE
(0x04) when sharing location. Sideband includes both, so Sideband icons
rendered correctly but Columba-to-Columba icons were lost.
Threads icon data through the full call chain:
- LocationSharingManager reads active identity's icon from IdentityRepository
- ReticulumProtocol.sendLocationTelemetry accepts IconAppearance param
- ServiceReticulumProtocol/Binder pass icon_name/fg/bg through AIDL
- Python send_location_telemetry attaches FIELD_ICON_APPEARANCE to LXMF fields
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves Greptile review comment — the parameter was accepted and stored
but never passed by any caller, creating dead infrastructure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Sideband FIELD_TELEMETRY_STREAM appearance format is
[icon_name, bg_rgb_bytes, fg_rgb_bytes] but the unpacker was treating
positions 1 and 2 as fg and bg respectively, swapping the colours.
- Fix appearance byte order in unpack_telemetry_stream to match Sideband
format [icon_name, background, foreground]
- Fix FIELD_ICON_APPEARANCE constant comment to document correct order
- Allow hyphens in icon name validation for MDI names (e.g. "sail-boat")
- Add human-readable sensor logging in unpack_location_telemetry
- Add MARKER_SYMBOL_REGISTRY mapping 45 symbol keys to MDI icon names
- Add _color_from_symbol_key() for deterministic background colour
derivation from marker symbol keys
- Add appearance_from_marker_symbol() helper for building Sideband-
compatible appearance tuples from marker metadata
- Update _send_telemetry_stream_response to derive appearance from
marker symbol when no explicit appearance is stored
- Update _store_telemetry_for_collector to accept marker_symbol param
- Persist appearance JSON in LocationSharingManager.handleReceivedLocation
- Update tests for corrected byte order and add marker symbol tests
Closes#405https://claude.ai/code/session_01Y5NnRbAHQAVjfR5RezQyvm
- Add Mutex to serialize mute IPC calls in CallViewModel, preventing
race between PTT button release (setCallMuted(true)) and PTT mode
toggle (setCallMuted(false)) on Dispatchers.IO thread pool
- Move link.teardown() outside _call_handler_lock in Python
call_manager.hangup() to prevent deadlock: teardown() can trigger
__link_closed() synchronously on the same thread, which tries to
re-acquire the non-reentrant threading.Lock, blocking all subsequent
call() invocations forever
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two root causes of intermittent failure:
- mock_time.time.side_effect list exhaustion raised StopIteration
(str(StopIteration()) == '', causing AssertionError: '' != 'Timeout')
- RNS.Transport.active_links was an unmocked Mock object, raising
TypeError when production code iterated it
Replace fixed side_effect list with counter function and set
active_links = [] to prevent both failure modes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a threading.Event that hangup() sets and the identity/path
discovery loops check, so a cancelled outgoing call exits promptly
instead of blocking the thread for up to 75 seconds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs identity recall, destination hash, and has_path status so
call setup issues are visible without code changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Announce lxst.telephony destination alongside LXMF announces so
remote peers discover both messaging and call paths together
- Add periodic re-announcing in CallManager (3-hour interval,
matching reference LXST Telephony implementation)
- Auto-request identity path from network when unknown, with 5s
retry loop (mirrors existing LXMF pattern in reticulum_wrapper)
- Move setupLxstCallManager() to first position in setupBridges()
so Telephone is available immediately after Python init (was last,
causing up to ~47s delay before calls could be placed)
- Remove stale "call.audio" announce handler that could never match
the actual "lxst.telephony" aspect
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The call() method checked and set self.active_call without holding
_call_handler_lock. During the up-to-70s path discovery window, an
incoming call could set active_call via __caller_identified, which
call() would then overwrite — orphaning the incoming link.
Fix: set _busy=True under lock before path discovery so incoming
calls see the reservation and get rejected. Clear _busy in finally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents orphaned Reticulum links when call() is invoked while an
existing call is active or connecting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use local reference capture instead of locks for receive_audio_packet()
and receive_signal() to prevent race with hangup()/__link_closed()
nulling self.active_call. Lock-free approach avoids audio jitter from
contention on the 25-50Hz packet path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add _call_handler_lock to answer(), hangup(), and __link_closed()
to prevent race conditions on self.active_call (Sentry HIGH)
- Remove invalid `=*` gitignore pattern (Greptile)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 40 new tests across 12 test classes covering previously untested
code paths: incoming/outgoing link handling, packet reception, signal
routing, audio forwarding, and Kotlin callback notification. Add
pragma: no cover to RNS fallback stub (dead code in test context).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
call_manager.py was rewritten during LXST-kt extraction as a thin
Reticulum transport layer — profile constants, telephone attribute,
audio bridge, and callback handlers all moved to Kotlin. Rewrite all
50 tests to match the new API surface: active_call (RNS.Link),
packet forwarding, signal routing, and bridge wiring.
Key fix: use RNS.Link.ACTIVE symbolically (not hardcoded int) since
conftest.py globally replaces sys.modules['RNS'] with MagicMock.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove vestigial Python LXST artifacts that are no longer needed
now that audio, codecs, and filters are handled by Kotlin LXST-kt:
- Remove numpy pip dependency (was for Python audio float32 conversion)
- Remove pycodec2 pip install and pre-built wheels (Kotlin JNI handles codecs)
- Delete chaquopy_audio_backend.py (617 lines, replaced by AudioDevice.kt)
- Delete lxst and lxst-filterlib wheels (never installed)
- Remove dead set_audio_bridge() plumbing from wrapper and manager
- Update call_manager.py to use renamed onInboundPacket/onInboundSignal
- Update LXST-kt submodule with transport-agnostic core/ renames
Remaining Python: call_manager.py (Reticulum bridge), rns, lxmf, u-msgpack.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Python's _notify_kotlin callback (via Chaquopy function reference) silently
fails to reach Telephone.onIncomingCall(), leaving isIncomingCall=false so
answer() always rejected. Fix with three changes:
- Add Telephone.prepareForAnswer() for lightweight JIT state setup
- Binder answerCall() falls back to CallBridge identity when answer() fails
- VoiceCallScreen auto-answer no longer gates on callState, preventing
notification answer button from starting a new outgoing call
Also: answer() now returns Boolean, onIncomingCall() no longer sends
duplicate STATUS_RINGING (Python already sent it to remote).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add set_kotlin_audio_active() method to CallManager
- Delegates to chaquopy_audio_backend.set_kotlin_audio_active()
- Provides Kotlin-accessible method to control Python audio disable
- Logs state change for debugging
- Add global _kotlin_audio_active flag (default False)
- Add set_kotlin_audio_active() and is_kotlin_audio_active() functions
- ChaquopyRecorder._record_chunk() returns silence when flag True
- ChaquopyPlayer.play() drops frames when flag True
- Prevents dual audio pipeline conflict during Kotlin calls
- Add STATUS_* signalling constants (0x00-0x06) to CallManager
- Send INTEGER signals via send_signal() in all LXST callbacks:
- _handle_ringing() -> STATUS_RINGING (0x04)
- _handle_established() -> STATUS_CONNECTING (0x05) + STATUS_ESTABLISHED (0x06)
- _handle_ended() -> STATUS_AVAILABLE (0x03)
- _handle_busy() -> STATUS_BUSY (0x00)
- _handle_rejected() -> STATUS_REJECTED (0x01)
- Preserve existing _notify_kotlin() string events for UI
This fixes the signal type mismatch (Gap 1 from 11-VERIFICATION.md)
where Kotlin Telephone expected INTEGER signals but Python only sent
STRING events. Kotlin Telephone can now transition to ESTABLISHED state
and open audio pipelines.
- Add on_state_changed() for Kotlin state notifications
- Add on_profile_changed() for profile sync
- Add set_kotlin_telephone_callback() for callback registration
- Add _notify_kotlin() for Python->Kotlin event notifications
- Update receive_audio_packet() to forward to LXST Packetizer
- Update receive_signal() to forward to LXST signalling
- Wire existing handlers to notify Kotlin Telephone
The second create_identity() method at line 7376 shadows the first one at
line 2338. Making display_name optional allows callers that don't pass a
name to work correctly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update Reticulum fork from rebase-1.1.3 to fix/socket-leak-1.1.3 branch.
The fix closes sockets on connection failure in TCPInterface.connect() and
BackboneInterface.connect() to prevent resource leak during reconnection
attempts (~780 leaked sockets/hour, ~4.3 MB/hour native memory growth).
Addresses memory growth identified during Phase 9 profiling investigation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cover start/stop, snapshots, periodic scheduling, and thread safety.
13 test cases for ~90% coverage of memory_profiler.py.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add threading.Lock to synchronize access to _profiling_active and
_snapshot_timer between stop_profiling() and timer callback.
Prevents orphaned timer continuing after stop_profiling() returns.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create memory_profiler.py with tracemalloc snapshot comparison
- Add enable_memory_profiling() method to ReticulumWrapper
- Schedule periodic snapshots using threading.Timer (Chaquopy-compatible)
- Filter frozen importlib and unknown traces to reduce noise
- Log top 10 growing allocations to Android logcat via logging_utils
- Provide get_memory_profile() for current stats query
- Zero overhead when disabled (lazy import pattern)
Add 27 tests covering:
- get_discovered_interfaces(): TCP interfaces, radio interfaces with LoRa
params, interfaces with location data, multiple interfaces sorted by
status and stamp value, bytes/string transport_id handling, None value
filtering, exception handling
- _get_discovery_status_name(): status code to name conversion
- _create_config_file() discovery options: discover_interfaces,
autoconnect_discovered_interfaces, interface_discovery_sources,
required_discovery_value, bootstrap_only for TCP clients
This improves patch coverage from ~56% to target higher coverage.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add unit tests for the RNS 1.1.x discovery functionality:
Python tests:
- test_autoconnected_endpoints.py: 14 tests for get_autoconnected_interface_endpoints()
- test_discovery_enabled.py: 12 tests for is_discovery_enabled()
Kotlin tests:
- DiscoveredInterfacesViewModelTest.kt: 22 tests covering autoconnected endpoints
state, isAutoconnected() helper, and discovery settings loading
- DiscoveredInterfacesScreenTest.kt: 29 tests for UI helpers including
isYggdrasilAddress(), formatInterfaceType(), and icon/badge visibility
Also fixes:
- Fix detekt ComplexCondition issues in RNodeWizardScreen, MapScreen, MainActivity
- Fix detekt LongMethod in ColumbaApplication by extracting restorePeerIdentities()
- Fix detekt SwallowedException in InterfaceRepository with verbose logging
- Fix detekt MatchingDeclarationName by moving FocusInterfaceDetails to own file
- Fix detekt ReturnCount in isYggdrasilAddress and ViewModel helper functions
- Update StartupConfigLoaderTest for new discovery settings
- Update TcpClientWizardViewModelTest for allInterfaceEntities changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add "Connected" badge showing which discovered interfaces are currently
auto-connected by RNS discovery
- Add getAutoconnectedEndpoints() API to fetch active auto-connected interfaces
- Use type-specific icons: globe for TCP, antenna for radio, incognito for I2P,
tree-pine for Yggdrasil (detected via 0200::/7 IPv6 range)
- Show I2P b32 addresses with .b32.i2p suffix
- Display human-readable interface type labels (e.g., "Backbone (TCP)")
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add toggle in Discovered Interfaces screen to enable/disable discovery
- Service auto-restarts when toggle is changed (no app restart needed)
- Show bootstrap-enabled interface names in settings card
- Add discovery settings to SettingsRepository and StartupConfigLoader
- Include discover_interfaces and autoconnect_discovered_interfaces in config
- Include bootstrap_only for TCPClient interfaces in buildConfigJson
- Fix Python wrapper bug: discovered_interfaces() returns list, not dict
- Add bottom padding to list to avoid nav bar overlap
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add unit tests for ReceivingInterfaceInfo:
- 25 Kotlin tests covering TCP, Auto, BLE, RNode, Serial, and unknown interfaces
- Tests for friendly name extraction, interface type parsing, and edge cases
Refactor ReceivingInterfaceInfo to reduce cyclomatic complexity:
- Extract InterfaceCategory enum for display properties
- Extract categorizeInterface() and looksLikeAddress() helper functions
- Main function now delegates to smaller, focused functions
Add 2 additional Python tests for interface name formatting:
- Test interface.name same as class name returns class only
- Test interface.name being None returns class only
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update the "Received Via" card in Node Details to show the user-configured
interface name (e.g., "Sideband Server") instead of generic type names
(e.g., "TCP/IP").
Python changes:
- Build formatted interface string "ClassName[UserConfiguredName]" from
the interface object's type and .name attribute
- This allows Kotlin to extract both the friendly name and interface type
Kotlin changes:
- Add extractFriendlyName() to parse user-configured name from brackets
- Add extractInterfaceType() to parse class name before brackets
- Display friendly name as main text, interface type as subtitle
The card now shows:
- Title: "Received Via"
- Content: "Sideband Server" (user-configured name)
- Subtitle: "TCPClientInterface" (interface type)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive discovery data to DiscoveredInterface including:
- Transport/network IDs and status info (statusCode, lastHeard, hops)
- TCP-specific fields (reachableOn, port)
- Radio-specific fields (frequency, bandwidth, SF, CR, modulation)
- Location fields (latitude, longitude, height)
- Helper properties for interface type detection
Update Python bridge to return full discovery data from RNS 1.1.x API.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add support for RNS 1.1.0+ interface discovery and bootstrap features,
enabling new users to connect without relying on deprecated public
infrastructure.
Changes:
- Add discovery settings to ReticulumConfig (discoverInterfaces,
autoconnectDiscoveredInterfaces, interfaceDiscoverySources,
requiredDiscoveryValue)
- Add bootstrapOnly option to TCPClient interface config
- Update Python config generation for discovery and bootstrap options
- Add get_discovered_interfaces() and is_discovery_enabled() API methods
- Extend AIDL interface with discovery methods
- Mark 3 reliable community servers as bootstrap candidates (Beleth,
Quad4, FireZen)
- Add bootstrap toggle to TCP Client wizard UI
Note: The RNS fork needs to be rebased on 1.1.2 to enable the actual
discovery features. The code is ready but awaiting the dependency update.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Harden 4 Python tests that were flaky due to mock object identity
comparisons by using explicit sentinel values for link status constants
- Add failure annotations to CI workflow so root cause job is clearly
marked when fail-fast cancels sibling jobs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Cover all branches: success with items, empty list, lowercase
normalization, empty string filtering, set storage, deduplication,
and list replacement.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add requester to allowed list in tests that expect requests to succeed
- Add test_blocks_request_from_non_allowed_requester to verify blocking
- Add test_blocks_request_when_allowed_list_empty to verify empty = block all
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Empty telemetry stream responses (0 entries from collector) were
appearing as blank messages in chat because is_location_only was
only set when the stream had entries. Now telemetry stream messages
are always marked as location-only regardless of whether they
contain data.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>