Fix timing bug where BLE scanner/advertiser/GATT connections persist after
service restart because System.exit(0) kills the process before the async
Python shutdown chain can complete. Add synchronous stopImmediate() methods
that run directly on Main thread before process exit.
Add user-configurable BLE power settings (Performance/Balanced/Battery Saver/
Custom presets) with per-interface controls for scan interval, scan duration,
and advertising refresh interval. Settings flow through the full stack:
UI → Room DB → Python config → Kotlin BLE bridge → Scanner/Advertiser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When Columba starts without WiFi (or with only cellular), AutoInterface
scans for network interfaces once during __init__ and never again. If
WiFi connects later, the new wlan0 interface is invisible — no multicast
discovery sockets are created, no peers are found.
Two bugs fixed:
1. NetworkChangeManager only fired on network *switches* (lastNetworkId
!= null), not on first connection. Changed condition to also fire
when lastNetworkId is null, with the existing binder.isInitialized()
guard preventing premature invocation during startup.
2. No mechanism existed to tell AutoInterface about new interfaces.
Added auto_interface_manager.py which surgically hot-adds only NEW
interfaces to the existing AutoInterface — creating multicast/unicast
discovery sockets, joining the multicast group, starting discovery
threads, and creating UDP data servers. This avoids tearing down
existing peer connections on already-adopted interfaces.
The hot-add runs before the LXMF announce on network change, so the
announce goes out on the newly added interface.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
COLUMBA-16: Wrap probeLinkSpeed() IPC in withContext(Dispatchers.IO)
to prevent ANR when called from Main thread via MessagingScreen.
COLUMBA-4Q: Convert restoreAnnounceIdentities() to suspend fun and
wrap IPC call in withContext(Dispatchers.IO) to prevent ANR during
interface restart identity restoration.
COLUMBA-4R: Prevent OOM from 222MB+ JSON parse of large attachments.
Python now writes attachments >2MB to staging files instead of
hex-encoding inline. Kotlin adds 10MB size guard before JSONObject
parse and resolves staging files via _binary_ref for direct binary
loading without hex encode/decode overhead.
COLUMBA-3F: Add SELECT DISTINCT to getEnrichedContacts query to
prevent duplicate rows from multi-announce LEFT JOINs, which caused
"Key was already used" crashes in LazyColumn. Defense-in-depth
distinctBy added in ContactsViewModel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 'Myself' as selectable group host and implement host self-telemetry storage so a hosting device includes its own coordinates in group telemetry behavior.
Unify local/remote collector send flow via TelemetryCollectorManager, store local telemetry through storeOwnTelemetry when collector=self, and re-sync Python host mode before local store to prevent intermittent 'Host mode not enabled'.
Also update UI/Settings behavior so host selection and host mode activation are explicit separate controls, and add/adjust tests across Kotlin and Python paths.
LXMF fires spurious failure/sent callbacks after a message is already
confirmed delivered. This triggered propagation retries that overwrote
'delivered' (double checkmark) with 'propagated' (single checkmark).
Python: add _successfully_delivered tracking set (mirrors existing
_successfully_propagated pattern) to guard _on_message_delivered,
_on_message_failed, and _on_message_sent from regressing state.
Kotlin: make 'delivered' truly terminal — block any status update that
would change it to a non-delivered state (defense-in-depth).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The announce_table in RNS is only populated when transport mode is enabled.
On non-transport nodes, interface extraction always returned None, causing
announces to show "Unknown" interface.
Extract interface lookup into python/interface_lookup.py module that checks
announce_table first, then falls back to path_table (which is always
populated regardless of transport mode).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a large file attachment exceeds the recipient's size limit and
falls back to propagation, notify the recipient immediately rather
than waiting for propagation to succeed. The previous deferred
approach had three bugs that prevented the notification from ever
appearing:
1. Tracking entry was placed after the immediate-success return,
so synchronous propagation success skipped notification entirely
2. If propagation failed (max_relay_retries_exceeded), the
notification was never sent since it waited for success
3. Field 16 (APP_EXTENSIONS_FIELD) was not in the meaningful_fields
set, so the recipient's _on_lxmf_delivery filtered the
notification as an "empty probe message"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update 4 Python docstrings and 1 log message that still referenced
the old 1s heartbeat interval after it was changed to 5s idle.
Remove dead WifiLock entries from detekt NoRelaxedMocksRule allowlist.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove WiFi lock (WIFI_MODE_FULL_HIGH_PERF) — the battery whitelist already
ensures network access during Doze, and 802.11 power-save mode only adds
100-300ms latency without dropping the TCP connection. Briar operates
successfully without a WiFi lock. This alone reduces WiFi radio power by ~10x
during idle periods.
Increase health check interval from 5s to 30s (stale threshold 10s→60s),
lock refresh interval from 5min to 2h, Python maintenance loop from 1s to
30s, and Python heartbeat idle interval from 1s to 5s. These were all
over-provisioned relative to their actual detection/timeout requirements.
Net effect: timer-driven wake-ups drop from ~7,932/hour to ~960/hour.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When both deployment methods fail (filesystem copy and pkgutil),
raise FileNotFoundError so the caller's try/except and has_socks
check can properly fail-fast.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
If any enabled interface uses SOCKS proxy and deployment fails, return
an error instead of letting RNS hit a confusing module-not-found error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use shutil.copy2 (sibling file via __file__) as primary deployment
method for TorClientInterface.py, with pkgutil.get_data as fallback
- Remove premature self.online = True before timeout configuration
- Prevent disabling SOCKS proxy toggle when .onion address is entered
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous socket.create_connection monkey-patch wasn't actually
intercepting Reticulum's TCP connections (RNS uses socket.connect()
directly, never create_connection). Replace with a proper RNS external
interface module that subclasses TCPClientInterface.
- Add TorClientInterface.py: overrides connect() with SOCKS5 handshake,
uses Username/Password auth for Tor stream isolation (different
circuits per interface), Tor-appropriate keepalive timeouts
- Remove ~175 lines of monkey-patch code from reticulum_wrapper.py
- Deploy interface to {configdir}/interfaces/ via pkgutil (RNS loads
external interfaces from this directory)
- Config generation emits type=TorClientInterface with proxy_host/port
when socks_proxy_enabled is set
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace bare sock.recv() calls with _recv_exact() helper that loops
until all expected bytes arrive, preventing misparse when TCP delivers
data in smaller chunks
- Parse SOCKS5 connect response in two stages: 4-byte header first,
then variable-length bind address based on ATYP
- Add @Suppress("TooManyFunctions") to TcpClientWizardViewModel (17
functions, threshold 15) since each function maps to a UI control
https://claude.ai/code/session_01DxBMKpvbEp6ckVEnLDb6Cv
Enable TCP client interfaces to route connections through a SOCKS5
proxy (e.g., Orbot) for Tor .onion address support. When a user enters
a .onion hostname, the SOCKS proxy toggle auto-enables.
Changes across all layers of the stack:
- Data model: add socksProxyEnabled/Host/Port fields to TCPClient
- Serialization: JSON round-trip through DB and Python config builder
- Python wrapper: minimal SOCKS5 handshake implementation that
monkey-patches socket.create_connection for proxied targets
- UI: "Connect via Tor (Orbot)" toggle with proxy host/port fields
in the TCP client wizard review step
- Community servers: add interloper node .onion address
https://claude.ai/code/session_01DxBMKpvbEp6ckVEnLDb6Cv
- Update LXST-kt: defer playback stream start until prebuffer fills,
set MODE_IN_COMMUNICATION for Oboe voice calls
- Reduce announce handler logging from INFO+separators to single DEBUG
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The LXMF icon appearance field (0x04) format used by Sideband and
MeshChat is [icon_name, fg_bytes, bg_bytes], but Columba had the
colors reversed as [icon_name, bg_bytes, fg_bytes]. This caused icons
to render with swapped colors in other LXMF clients while appearing
correct within Columba (which consistently used the wrong order for
both packing and unpacking).
Fixed all packing locations (send_lxmf_message, send_location_telemetry,
send_lxmf_message_with_method, appearance_from_marker_symbol) and all
unpacking locations (_on_lxmf_delivery, poll_received_messages,
unpack_telemetry_stream) to use the correct [name, fg, bg] order.
https://claude.ai/code/session_01XYEj7YKEJhABdHSUaXpiHy
LXMF error states are PR_NO_PATH=0xf0, PR_LINK_FAILED=0xf1,
PR_TRANSFER_FAILED=0xf2 — not sequential from 8. Use >= 0xf0 to match
the actual constant definitions from LXMRouter.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On fast transports with cached paths/links, LXMF can complete sync within
milliseconds. The heartbeat loop was in 1-second idle mode and missed the
COMPLETE state entirely, leaving the UI stuck in "syncing" state.
Three coordinated fixes:
- Use -1 sentinel instead of None when resetting propagation state so the
heartbeat immediately switches to 100ms fast-polling
- Add brief post-request polling loop (up to 2s) to catch fast completions
before the heartbeat picks up
- Treat IDLE-during-active-sync as sync completion in PropagationNodeManager
(handles case where COMPLETE was missed but LXMF returned to IDLE)
- Increase SharedFlow extraBufferCapacity from 1 to 5 to prevent dropping
rapid state transitions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change unpack_telemetry_stream appearance keys from name/fg/bg to
icon_name/foreground_color/background_color to match parseAppearanceJson
- Remove # prefix from hex color strings (Kotlin expects raw RRGGBB)
- Cap _pending_location_events buffer at 100 to prevent unbounded growth
- Remove accidental spec docs from PR
- Update all test assertions to match new key names and format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add identityRepository to TelemetryCollectorManager so collector-bound
telemetry includes FIELD_ICON_APPEARANCE (matching LocationSharingManager)
- Fix misleading log/comments: empty allowed_requesters set blocks all
requests (not allows all) — code behavior was already correct
- Update TelemetryCollectorManagerTest with new constructor parameter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of dropping location events when kotlin_location_received_callback
isn't registered yet (startup race), buffer them and drain on registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Align all pack/unpack sites with Sideband wire format [name, bg, fg].
Add @nullable to AIDL icon params since they're optional.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Location detection was gated on kotlin_location_received_callback being
set. Messages arriving before LocationSharingManager registered the
callback (startup race) had is_location_only=False, causing them to
appear as empty chat bubbles.
Now: field detection (FIELD_TELEMETRY, FIELD_COLUMBA_META) runs
unconditionally, only the callback invocation checks registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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
- 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>
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>
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>
- 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 "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>
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>