Commit graph

25 commits

Author SHA1 Message Date
torlando-tech
b292532d20 fix: batch TX audio frames to eliminate progressive call degradation
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>
2026-02-16 14:45:06 -05:00
torlando-tech
ed635ac6d8 fix: prevent _call_handler_lock deadlock on call reconnect
__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>
2026-02-16 00:22:17 -05:00
torlando-tech
5efcf3f7eb fix: resolve PTT mute race condition and hangup deadlock
- 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>
2026-02-08 19:54:18 -05:00
torlando-tech
8d277aacad fix(python): cancel path discovery loops on hangup
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>
2026-02-08 14:02:54 -05:00
torlando-tech
83f500a462 fix(python): add debug logging to call_manager.call() flow
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>
2026-02-08 13:59:16 -05:00
torlando-tech
51a9c0db50 feat(telephony): improve call setup reliability and init speed
- 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>
2026-02-08 12:43:23 -05:00
torlando-tech
13bedfe429 fix(python): guard call() with _busy flag to prevent race with incoming calls
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>
2026-02-07 22:44:21 -05:00
torlando-tech
9e51d70652 fix(python): reject outgoing call if active_call already set
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>
2026-02-07 21:10:47 -05:00
torlando-tech
d139938531 fix(python): eliminate TOCTOU race in audio/signal hot path
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>
2026-02-07 19:52:48 -05:00
torlando-tech
c9054ece03 fix: address Sentry/Greptile review comments on PR #416
- 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>
2026-02-07 19:18:25 -05:00
torlando-tech
990cddde1d test(python): improve call_manager.py coverage from 48% to 94%
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>
2026-02-07 19:05:02 -05:00
torlando-tech
1c497946b3 chore: remove dead Python LXST dependencies and audio backend
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>
2026-02-07 16:02:34 -05:00
torlando-tech
96a93038e2 fix(lxst): fix incoming call answer button not picking up calls
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>
2026-02-07 16:02:33 -05:00
torlando-tech
1d0cf79f0a feat(11.6): enable bidirectional voice calls with crash fixes
Wire Kotlin→Python audio transmit path by registering call_manager as
NetworkPacketBridge's pythonNetworkHandler. Fix wuqi-opus SIGSEGV by
capping decode sample rate to fit 1024-sample buffer. Fix LineSink
sample rate mismatch and dual-sink race condition. Re-enable full
transmit pipeline (Packetizer, Mixer, LineSource). Add jarray→bytes
conversion for Chaquopy type compatibility.

Key changes:
- PythonWrapperManager: register setPythonNetworkHandler after CallManager init
- Opus: compute safe decode sample rate for wuqi-opus buffer limit
- LineSink: add releasedFlag to prevent stale frame auto-restart
- Telephone: re-enable transmit pipeline, fix pipeline lifecycle
- NetworkPacketBridge: add diagnostic logging for consumer coroutine
- call_manager.py: add jarray→bytes conversion, diagnostic logging
- LinkSource: add packet decoding with codec header parsing
- Remove obsolete lxst_debug_instrumentation.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 16:02:33 -05:00
torlando-tech
44077010d4 feat(11.6-01): add Kotlin audio active check to LineSource instrumentation
- Check is_kotlin_audio_active() in instrumented_ingest_job()
- Skip frame processing when Kotlin audio active
- Log skip every 100 frames for verification
- Maintain timing with sleep(frame_time)
- Provides additional safety layer beyond recorder silence
2026-02-07 16:02:33 -05:00
torlando-tech
b405dfa462 feat(11.6-01): add set_kotlin_audio_active() to CallManager
- 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
2026-02-07 16:02:33 -05:00
torlando-tech
a28e7db15f feat(11.6-01): add _kotlin_audio_active flag to disable Python LXST audio
- 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
2026-02-07 16:02:33 -05:00
torlando-tech
f2fb803e5b feat(11.5-01): wire INTEGER signals from Python to Kotlin
- 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.
2026-02-07 16:02:33 -05:00
torlando-tech
d5615bae86 feat(11-04): add Kotlin Telephone integration to call_manager.py
- 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
2026-02-07 16:02:33 -05:00
torlando-tech
cae71db622 feat(10-05): add network bridge methods to call_manager.py
- Add _kotlin_network_bridge instance variable
- Update initialize() to accept kotlin_network_bridge parameter
- Update initialize_call_manager() to pass network bridge
- Add send_audio_packet() for Python->Kotlin packets
- Add send_signal() for Python->Kotlin signalling
- Add receive_audio_packet() stub for Kotlin->Python (Phase 11)
- Add receive_signal() stub for Kotlin->Python (Phase 11)
2026-02-07 16:02:33 -05:00
torlando-tech
a745afd43a chore(07-02): add opus-jni dependency to build.gradle.kts
- Added cn.entertech.android:wuqi-opus:1.0.3 from Maven Central
- Library wraps theeasiestway/android-opus-codec (libopus 1.3.1)
- Provides OpusEncoder/OpusDecoder with JNI bindings
- Native .so libraries for arm64-v8a and x86_64 ABIs
2026-02-07 16:02:33 -05:00
torlando-tech
0ba904d28d fix: resolve call startup issues with retry and async IO
- Remove redundant path request code from call_manager.py - LXST's
  Telephony.call() already handles path discovery correctly
- Move AIDL call operations to IO dispatcher to prevent ANR when
  LXST blocks for path discovery (up to 70s)
- Add retry mechanism in CallViewModel for CallManager initialization
  (retries up to 10x with 1s delay after fresh install)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 22:06:23 -05:00
torlando-tech
830853c829 fix: use mute_transmit() instead of unmute_transmit() for LXST calls
LXST's Mixer.unmute() has a bug where unmute(True) sets self.muted=True
instead of self.muted=False (inverted logic). This caused the unmute
button to not work - calling unmute_transmit() actually kept audio muted.

Changes:
- call_manager.py: Use telephone.mute_transmit(muted) with explicit
  True/False instead of separate mute_transmit()/unmute_transmit() calls
- KotlinAudioBridge: Remove software mute (LXST handles at codec level)
- ReticulumServiceBinder: Improve logging for mute state changes

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 22:06:23 -05:00
torlando-tech
0c91aeb442 feat: implement LXST voice calls with multi-process IPC
Complete voice call implementation for Android with cross-process support:

- Add call AIDL interface (IReticulumService/IReticulumServiceCallback)
- Implement ServiceReticulumProtocol for call IPC to :reticulum process
- Add ReticulumServiceBinder call methods bridging to Python CallManager
- Wire KotlinAudioBridge to Chaquopy audio backend for audio streaming
- Add incoming call notifications with IPC broadcast to UI process
- Fix microphone recording by adding FOREGROUND_SERVICE_MICROPHONE
- Support mute/unmute via software mute in audio bridge
- Add libopus native dependency for Opus codec support

Audio routing now works correctly between devices with earpiece default.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 22:06:23 -05:00
torlando-tech
fa59db20b8 feat: add LXST voice call support
Implement real-time voice calling over Reticulum using LXST Telephony:

- KotlinAudioBridge: Android AudioTrack/AudioRecord wrapper for Python
- chaquopy_audio_backend.py: Chaquopy-compatible replacement for Pyjnius soundcard
- CallBridge: Bidirectional state management between Python LXST and Kotlin UI
- call_manager.py: LXST Telephone wrapper with Kotlin callbacks

- VoiceCallScreen: Active call UI with mute/speaker controls
- IncomingCallScreen: Incoming call UI with pulsing animation
- CallViewModel: Call state management with Hilt DI
- Call button added to MessagingScreen TopAppBar

- CallNotificationHelper: Incoming/ongoing call notifications with full-screen intent
- CallActionReceiver: Notification action handling (answer/decline/end)
- AudioCoordinator: Audio focus, wake locks, proximity sensor

- RECORD_AUDIO for microphone access
- MODIFY_AUDIO_SETTINGS for audio routing
- USE_FULL_SCREEN_INTENT for incoming call overlay

Supports all LXST quality profiles (Opus/Codec2) for bandwidth-adaptive calls.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 22:04:40 -05:00