Skip to content

Detach kernel USB-audio driver so Android sounds can't kill a USB-direct TX - #777

Merged
patrickrb merged 3 commits into
devfrom
fix/usb-audio-detach-kernel-driver
Aug 25, 2026
Merged

Detach kernel USB-audio driver so Android sounds can't kill a USB-direct TX#777
patrickrb merged 3 commits into
devfrom
fix/usb-audio-detach-kernel-driver

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Problem

USB-direct TX intermittently goes out as dead air: the rig keys, debug.log shows
libusb native write FAILED (rc=5 TRANSFER_NO_DEVICE) after ~280ms, no usbDetach,
and RX capture on the same device keeps running. Reported by users as "TX stops
working until I unplug/replug" (the replug is a coincidence — see below).

Root cause (2026-08-25 bench log, Pixel 11 Pro XL + CM108 0d8c:0012)

claimInterface(force) on the UAC AudioStreaming interfaces is a silent no-op for
the kernel's snd-usb-audio driver: it binds the card at the AudioControl interface
and marks the streaming interfaces owned-but-unused. So the ALSA card survived our
claim and Android kept the rig's sound card registered as a usb_headset sink +
source
(dumpsys audiosetWiredDeviceConnectionState … from UsbAlsaDevice).

Every sound Android routed there made the kernel driver flip interface 1's
alt-setting under our in-flight iso URBs; the kernel completes those with
-ESHUTDOWN → libusb TRANSFER_NO_DEVICE. Correlation over the whole log:

write result alert ≤6 s before no alert
OK 1 302
FAIL (rc=5, device still on bus) 20 (all at dt ≈ 1.9 s) 2 (BT car-kit connect at 09:08, nav prompt in car)
FAIL (rc=-4, whole bus dropped) 0 2 (electrical, see below)

The dominant trigger is the app's own QSO-complete DX-alert notification ding
(ALERT fire … QSO complete 1.9 s before the TX slot). rc=-1 IO at submit is the
same thing when the alt-setting was already flipped back before our submit.

Iso OUT has no device-side handshake, so only host-side actions can fail an OUT URB
while the device stays attached; RX surviving pins it to an interface-1-only action,
i.e. the kernel class driver.

Fix

UsbAudioDevice.open() now calls detachKernelAudioDriver(), which force-claims
the AudioControl interface. That runs the real usb_audio_disconnect, retiring the
ALSA card, so nothing Android plays can reach the endpoint. The per-cycle TX open
skips the claim when the session-long RX capture already holds it on the same
device. No AudioControl interface / refused claim → logged, behaviour unchanged.

Side effect, by design: phone sounds Android would have routed into the rig's mic
input
are dropped instead (they were inaudible to the operator anyway and could have
been keyed on air). The kernel doesn't rebind the driver on release; the card
returns on the next replug.

Also: the UsbAudioWriteErrorTest Javadoc blamed rc=5 on RF knocking the device off
the bus — corrected. CLAUDE.md gains TX-pipeline gotcha #4 with the log signatures.

Not fixed here (electrical)

Two failures today were a real bus drop mid-TX: usbDetach for hub + CP2105 + C-Media
together, serial.send: port not open!, and dumpsys batterystats --history shows
the phone lost hub power (-plugged) at the same instant (08:37:02 at 35 % drive on
40 m, 09:00:12). That's the hub resetting under RF — ferrites / powered hub / shorter
cable, not software.

Tests

  • New UsbAudioControlInterfaceTest (7 tests) for audioControlInterfaceIndex().
  • testDebugUnitTest --tests com.k1af.ft8af.wave.*: 19 suites green.

Hardware verification — TODO before merge

Not yet run on hardware (the bench phone carries the non-debuggable Play build, so a
debug install would wipe its QSO log). To verify: plug in the rig interface, check
debug.log for UsbAudioDevice: kernel audio driver detach via AudioControl iface 0: OK,
then trigger a QSO-complete alert (or any notification sound) ~2 s before a TX slot
and confirm the next write logs libusb native write OK. Also confirm
dumpsys media.audio_flinger doesn't misbehave once the ALSA card is gone.

🤖 Generated with Claude Code

patrickrb and others added 2 commits August 25, 2026 14:40
…ect TX

Force-claim the device's UAC AudioControl interface (not just the streaming
interfaces) whenever the app opens a USB audio device for direct libusb use,
for RX and for each TX cycle.

Why: claiming only the AudioStreaming interfaces is a silent no-op for the
kernel's snd-usb-audio driver, which binds the card at the AudioControl
interface and treats the streaming interfaces as owned-but-unused. The ALSA
card therefore survived, Android kept the rig's CM108 registered as a
usb_headset sink+source, and every sound Android routed there -- the app's
own QSO-complete DX-alert ding, a BT car-kit connect re-route, a nav prompt
-- made the kernel driver flip the playback interface's alt-setting under
our in-flight iso URBs. The kernel completes those with -ESHUTDOWN, libusb
reports rc=5 TRANSFER_NO_DEVICE ~280 ms into the TX with the device still on
the bus, and the cycle goes out as dead air. In the 2026-08-25 bench log 20
of 22 such failures were preceded by a QSO-complete alert 1.9 s earlier; the
other two coincided with a Bluetooth car kit connecting.

Claiming the AudioControl interface runs the real usb_audio_disconnect, so
the ALSA card is retired and nothing Android plays can reach the endpoint.
The per-cycle TX open skips the claim when the session-long RX capture on
the same device already holds it. A missing AudioControl interface or a
refused claim is logged and ignored, so behaviour on odd devices is unchanged.

Side effect, by design: while the app holds the device, phone audio that
Android would have routed into the rig's mic input is dropped instead (it
was inaudible to the operator either way, and could have been keyed on
air). The kernel does not rebind the driver on release; the card returns on
the next unplug/replug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@patrickrb
patrickrb requested a balanced review from Copilot August 25, 2026 19:43
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 42.36%. Comparing base (551b0a2) to head (36d469f).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #777      +/-   ##
============================================
+ Coverage     42.29%   42.36%   +0.07%     
  Complexity      226      226              
============================================
  Files           267      267              
  Lines         31730    31819      +89     
  Branches       3634     3650      +16     
============================================
+ Hits          13419    13479      +60     
- Misses        18054    18082      +28     
- Partials        257      258       +1     
Flag Coverage Δ
android 16.91% <ø> (+0.25%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.
see 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Failed opens can leak a USB connection and control-interface claim, while the core claim behavior lacks regression coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Detaches Android’s kernel USB-audio driver to prevent routed sounds from interrupting direct USB FT8 transmission.

Changes:

  • Force-claims the UAC AudioControl interface.
  • Adds interface-selection unit tests.
  • Updates USB failure diagnostics and documentation.
File summaries
File Description
UsbAudioDevice.java Implements driver detachment and claim lifecycle.
UsbAudioControlInterfaceTest.java Tests AudioControl interface selection.
UsbAudioWriteErrorTest.java Corrects failure-cause documentation.
CLAUDE.md Documents the USB-direct TX failure mode.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ft8af/app/src/main/java/com/k1af/ft8af/wave/UsbAudioDevice.java Outdated
Comment thread ft8af/app/src/main/java/com/k1af/ft8af/wave/UsbAudioDevice.java
Comment thread ft8af/app/src/main/java/com/k1af/ft8af/wave/UsbAudioDevice.java
- open(): validate the endpoint scan first; on a device with no usable iso
  endpoint close the connection and return false instead of leaking it (and
  no longer detach Android audio from a device we can't use).
- Extract the claim decision + effect behind a KernelDetachPort so the
  stateful branches (claim on the AC interface, skip when the RX session
  already holds it, refused claim) are covered by UsbAudioKernelDetachTest.
- Correct the describeLibusbWriteError / shouldFallbackToUsbRequest docs:
  rc=5 is the kernel tearing down the endpoint, rc=-4 is a real bus removal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@patrickrb
patrickrb merged commit 9e8b040 into dev Aug 25, 2026
17 checks passed
@patrickrb
patrickrb deleted the fix/usb-audio-detach-kernel-driver branch August 25, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants