- Add @Volatile to cross-thread power settings fields (KotlinBLEBridge,
BleScanner, BleAdvertiser) for JMM visibility between Python/Kotlin threads
- Remove dead **power_kwargs from AndroidBLEDriver.start()
- Add ordering-safety comment in AndroidBLEInterface.__init__()
- Upgrade closeImmediate() off-thread log from Log.w to Log.e
- Deduplicate preset values in InterfaceConfigDialog using BlePowerPreset.getSettings()
- Add scan overlap warning when scan duration >= active scan interval
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Addresses two concurrency issues flagged in PR review:
1. Add _hot_add_lock to prevent concurrent hot_add_interfaces() calls from
racing to adopt the same interface — the GIL releases during socket
syscalls so two coroutines could both bind the same ports.
2. Reorder _add_interface so the UDP data server is created BEFORE any
discovery threads start. Previously, if _IPv6UDPServer() raised after
threads were already blocking on recvfrom(), socket cleanup would cause
OSError in those threads.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap socket/thread/server setup in try/except so that already-created
sockets are closed if a later step fails, preventing EADDRINUSE on retry.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address Greptile review comments on PR #587:
- Replace global socketserver.UDPServer.address_family mutation with
_IPv6UDPServer subclass to avoid affecting other UDPServer instances
- Defer adopted_interfaces/link_local_addresses/multicast_echoes
registration until after socket and thread setup succeeds, preventing
permanently stale state on partial failure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests _write_attachment_staging() directly and exercises field 5/6/7
large-vs-small branching through _on_lxmf_delivery, covering all
19 new executable lines in reticulum_wrapper.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 23 Python tests for auto_interface_manager.py covering interface
scanning, filtering (ignored/allowed/adopted), link-local detection,
hot-add orchestration, and socket/thread setup
- Update 4 NetworkChangeManagerTest tests to match new behavior where
first network connection triggers callback (needed for hot-add)
- Add 2 ReticulumServiceBinderTest tests for restartAutoInterface()
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>
The idle heartbeat interval was changed from 1s to 5s, but the test's
join(timeout=3) was not updated. The thread may be sleeping when
initialized is set to False and needs up to 5s to wake and check the
loop condition.
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>
Wire up the existing TorClientInterface backend to the UI by adding
SOCKS5 proxy toggle, host, and port fields under TCP Client advanced
options. Also fix socket resource leak during reconnect cycles.
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>
- Add else clause for unsupported SOCKS5 ATYP values to prevent socket
desynchronization from unconsumed bind address bytes
- Move hostname length validation into _socks5_handshake() where
self.target_ip is used, rather than checking config value pre-init
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
Voice calls degraded progressively from 60ms to 130ms per-frame arrival
rate over 20 seconds, even on fast 1-hop local WiFi links. Root cause:
each RNS.Packet.send() holds the Python GIL for encryption (AES-256-CBC
+ HMAC-SHA256) and transport dispatch, creating a feedback loop where
both devices' TX/RX paths compete for GIL time.
Batch 3 audio frames per RNS.Packet.send() call, reducing crypto
overhead from ~16.7 calls/sec to ~5.6 calls/sec (67% reduction).
The LXST wire format already supports frame lists ({0x01: [f1, f2, f3]})
and the receiver already handles both single frames and lists.
Results: packet arrival rate stable at ~60ms/frame for 40+ seconds,
zero silence callbacks, zero PLC, buffer steady at 6-9 frames.
Also updates LXST-kt submodule with adaptive playout drain for ring
buffer latency bounding during packet bursts and speaker toggles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
__link_closed() was calling Kotlin callbacks (signal + onCallEnded)
while holding _call_handler_lock. The signal triggered Kotlin hangup()
synchronously, running NativePlaybackEngine.destroy() which blocks on
Oboe stream close — keeping the Python lock held for the entire
duration and preventing subsequent call() from acquiring it.
Fix: move Kotlin callbacks outside the lock (same pattern as hangup())
and dispatch hangup() async in Kotlin's STATUS_AVAILABLE handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use concrete string sentinels for Link.ACTIVE/CLOSED instead of
auto-created MagicMock attributes (Mock == Mock comparison is
nondeterministic across Python versions). Also provide extra
time.time() side_effect values and set Transport.active_links = []
to prevent StopIteration on unexpected calls.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
1. test_telemetry_host_mode.py: TestSendTelemetryRequestSuccess.setUp()
was setting reticulum_wrapper.RETICULUM_AVAILABLE = True without
restoring it in tearDown(), leaking into other test files on the
same xdist worker. Now saves/restores all three globals.
2. test_wrapper_path.py: Added defensive @patch to mock-mode test so
it's immune to global leaks from other test files.
3. ci.yml: Removed gh-run-cancel fail-fast from all jobs. The cancel
API cancels the entire run including the calling job, making every
job show "cancelled" with no way to identify which actually failed.
The ci-passed gate job already handles overall status reporting.
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>
- Replace runtime pip install with pytest.skip in test_pr422 (CI-safe)
- Use valid hex colors in MapViewModel appearance test
- Fix appearanceJson docstring to match actual stored schema
- Remove stray artifacts: =0.21.0, AUDIO_DEBUG_ANALYSIS.md,
dispatcher-audit-report.txt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers accuracy clamping, FIELD_ICON_APPEARANCE byte order
(bg/fg) across all pack/unpack sites, icon name hyphen validation,
startup race buffering, and end-to-end send/receive consistency.
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>