Skip to content

Add QRP Labs QMX via the hamlib backend - #925

Open
laniol8926 wants to merge 1 commit into
patrickrb:devfrom
laniol8926:feat/qmx-hamlib-rig
Open

laniol8926 wants to merge 1 commit into
patrickrb:devfrom
laniol8926:feat/qmx-hamlib-rig

Conversation

@laniol8926

@laniol8926 laniol8926 commented Oct 20, 2026 •

Copy link
Copy Markdown

Summary

  • The rig catalog (rigaddress.txt) had no entry for the QMX at all, so it never showed up as a selectable rig in Settings.
  • Its sibling QDX is listed using the generic Kenwood TS-590/480 command set, but that path sets MD2 (SSB-USB voice mode) rather than DATA mode + the TX-rise-threshold tuning the QMX specifically needs to transmit — the same bug danleetw/FT8TW's native app had to hand-patch around (MD6;Q41;).
  • FT8AF already vendors and statically compiles hamlib 4.7.2, which has a dedicated QRP Labs QMX backend (qrplabs_qmx_caps / RIG_MODEL_QRPLABS_QMX), exposed through the existing HamlibRig bridge — the same mechanism already used for "Yaesu FT-891 (Hamlib)". This adds the matching catalog line: 0x809 = RIG_MAKE_MODEL(RIG_KENWOOD, 57), hamlib's real QMX model ID, at its minimum supported baud rate (9600).
  • Per QRP Labs' own current QMX firmware guidance, the native hamlib definition is the recommended path over Kenwood emulation, which matches this change.
  • Appended at the end of the list (after ICOM IC-7300MK2), per RigAddressAssetTest's documented append-only invariant — the selected rig is persisted as a raw list index, so inserting it mid-file (my first push here, now fixed) would have silently reassigned every existing user's selected rig on upgrade. Updated GOLDEN_ORDER, moved the "last entry" pin test off the MK2 entry onto this one, and added qmxHamlib_isListedWithHamlibModelId mirroring the existing ic7300mk2_isListedWithIcomDefaults pattern.

Known gaps — please check before merging

  • No final APK built, and the JVM unit tests couldn't be run locally either. Developed on an aarch64 Linux board; the AGP-bundled aapt2 for Linux is x86_64-only, and processDebugResources (a dependency of both assembleDebug and testDebugUnitTest) needs it to compile AAR resources. qemu-user-static emulation was attempted but blocked by an unrelated, unsafe apt dependency conflict in that machine's repo config (would have required upgrading libc6 system-wide, so it was not forced through). The new/changed test assertions were traced by hand against RigNameList's parser instead (confirmed "809" parses as hex → 2057 = 0x809) — CI on a real x86_64 runner is this PR's actual first execution.
  • No QMX hardware available to test TX/RX against. The fix is verified at the source level (correct hamlib model ID, baud within the backend's declared range, reusing already-working HamlibRig wiring) but not verified against a real radio.

Test plan

  • Let CI run (testDebugUnitTest should now pass with the golden-order fix)
  • Build an APK (./gradlew assembleDebug) on a supported dev machine (Windows/macOS per AGENTS.md)
  • Select "Qrp Labs QMX (Hamlib)" in Settings and confirm it connects over USB CAT
  • Confirm TX actually keys/transmits (not just that CAT connects) — this is exactly where the Kenwood-emulation path silently failed before
  • Confirm frequency read/set and PTT work as expected

🤖 Generated with Claude Code

https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 41.51%. Comparing base (0677e40) to head (1d138cd).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##                dev     #925   +/-   ##
=========================================
  Coverage     41.51%   41.51%           
  Complexity      252      252           
=========================================
  Files           280      280           
  Lines         33993    33993           
  Branches       4034     4034           
=========================================
  Hits          14112    14112           
  Misses        19592    19592           
  Partials        289      289           
Flag Coverage Δ
android 17.63% <ø> (ø)
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@laniol8926

Copy link
Copy Markdown
Author

@patrickrb — flagging this directly in case it gets buried under the Codecov bot comment.

This change is small (one line in rigaddress.txt) and verified correct at the source level against the hamlib headers/binary already vendored in this repo, but I couldn't get a final APK out of my build environment to actually test it — I'm on an aarch64 Linux board, and the AGP-bundled aapt2 for Linux is x86_64-only, with no safe emulation path available here (see the PR description for what I tried and why I backed off).

Would you be able to either:

  1. Build a debug APK from this branch yourself and test it against a QMX, or
  2. If you're set up to build it but don't have a QMX handy, publish a debug build somewhere (a GitHub Release / Actions artifact) so I can test it directly on real hardware and report back?

No rush — happy to wait on whichever's easier on your end.

The rig catalog (rigaddress.txt) had no entry for the QMX at all, so it
never showed up as a selectable rig. Its sibling QDX is listed using the
generic Kenwood TS-590/480 command set, but that path sets MD2 (SSB-USB
voice mode) rather than the DATA mode + TX-rise-threshold tuning the QMX
specifically needs to transmit -- same bug danleetw/FT8TW's native app
had to hand-patch around.

FT8AF already vendors and statically compiles hamlib 4.7.2, which has a
dedicated QRP Labs QMX backend (qrplabs_qmx_caps / RIG_MODEL_QRPLABS_QMX),
exposed through the existing HamlibRig bridge -- the same mechanism
already used for "Yaesu FT-891 (Hamlib)". This adds the matching catalog
line: 0x809 = RIG_MAKE_MODEL(RIG_KENWOOD, 57), hamlib's real QMX model ID,
at its minimum supported baud rate (9600).

Per QRP Labs' own current QMX firmware guidance, the native hamlib
definition is the recommended path over Kenwood emulation, which matches
this change.

Appended at the end of rigaddress.txt (after ICOM IC-7300MK2), per
RigAddressAssetTest's documented append-only invariant -- the selected
rig is persisted as a raw list index, so inserting anywhere else would
silently reassign every existing user's selected rig on upgrade. Added
a matching GOLDEN_ORDER entry, moved the "last entry" pin from the MK2
test to this one, and added a dedicated test (qmxHamlib_isListedWith
HamlibModelId) mirroring the existing ic7300mk2_isListedWithIcomDefaults
pattern.

Verified: builds cleanly through resource merge, manifest processing,
and Java/Kotlin compilation on this checkout; the new/changed test
assertions were traced by hand against RigNameList's parser (confirmed
"809" parses as hex -> 2057 = 0x809) since this environment can't
execute them. Could not produce a final APK or run the JVM unit tests
locally -- the AGP-bundled aapt2 for Linux is x86_64-only and this build
machine is aarch64 (processDebugResources, a dependency of both
assembleDebug and testDebugUnitTest, needs it to compile AAR resources).
qemu-user-static emulation was attempted but blocked by an unrelated,
unsafe apt dependency conflict in this machine's repo configuration
(would have required upgrading libc6 system-wide, so it was not forced
through). No QMX hardware was available to test TX/RX against either.
All three are red flags a reviewer with access to any of them should
check before merge -- CI (real x86_64 runners) is this PR's actual first
verification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
@laniol8926

Copy link
Copy Markdown
Author

Tested this on real QMX hardware (arm64 debug build from this branch, sideloaded).

CAT control works — confirms the fix itself:
```
serial.prepare: matched 0483:a34c deviceId=2005 port=0
serial.open: OK port=0 baud=9600
setOperationBand: setting freq=7074000 (rig.getFreq=7074000)
```
Frequency set/read-back matches via the hamlib QMX backend. VID `0483` = STMicroelectronics, consistent with the QMX's STM32-based USB implementation.

But TX audio write fails — separate, pre-existing bug, not introduced by this PR's diff:
```
QSO: TX slot=1 order=6 msg=[CQ AI5II EM71]
playFT8Signal: TX path branch: audioOutputDeviceId=-1 usbAudioOutputVidPid=0483:A34C volume=100% autoVolume=false
playFT8Signal: using USB audio (direct) output
UsbAudioDevice: kernel audio driver detach via AudioControl iface 2: OK
playViaUsbAudio: device opened, output activated at 48000 Hz
UsbAudioDevice: trying libusb native write fd=209 iface=4 alt=1 ep=0x03 maxPkt=300 bytes=2426880 outputRate=48000 ch=2
UsbAudioDevice: libusb native write FAILED (rc=-1 IO), trying UsbRequest fallback
playViaUsbAudio: writeAudio returned FAILED — TX DROPPED, no audio sent this cycle (rig keyed but silent)
```
PTT fires (rig keys) but the audio transfer itself fails with a low-level libusb I/O error, so nothing actually transmits.

Also saw the whole USB connection to the QMX fully detach and reattach twice during the session, independent of the TX attempt:
```
CAT drop: IOException: USB get_status request failed [kind=TRANSIENT action=RECONNECT attempt=0]
usbDetach: vid=483 pid=a34c
CAT auto-reconnect: device left the bus, stopping (attach broadcast will restart)
```

I initially suspected old-phone/cable flakiness (this is a 2015 Moto G 3rd Gen), but ruling that out: both the cable and this exact phone have already done reliable, sustained live QMX work — same cable is the one in daily use with a different always-on QMX setup, and this same phone has already done live FT8 decoding over USB with a different native Android FT8 app with no equivalent dropouts. So this looks like something specific to FT8AF's direct-libusb audio path or its interaction with the CAT connection on this device, not the hardware.

Given CAT itself works end to end, I think this PR's change is sound — the TX-audio-write and USB-detach issues look like a separate, pre-existing bug surfaced by QMX now being reachable at all. Happy to pull more of debug.log or try narrower repros if useful.

@laniol8926

Copy link
Copy Markdown
Author

One more real-hardware finding: changing bands in the app did not actually retune the radio. Confirmed by direct observation (watching the QMX itself), not just the log — the first band change (to 7074000/40m) did take effect, but a later change to 14074000/20m did not; the radio stayed on 40m.

I can't fully root-cause this from `debug.log` alone, and want to flag why precisely: `HamlibRig.setFreqToRig()` submits `HamlibNative.nativeSetFreq()` to an async worker and only logs a failure via `Log.w` (logcat) if `rc != 0` — that line never reaches `fileLog()`/`debug.log`. I don't have `adb`/logcat access in this environment, so I can't see whether the native call actually returned a nonzero `rc` for that second band change or silently no-op'd for some other reason. The `debug.log` lines around it:
```
bandSelect: index=20, band=14074000, controlMode=1, rigConnected=true
setOperationBand: sending USB mode, then freq=14074000 in 800ms (controlMode=1)
setOperationBand: setting freq=14074000 (rig.getFreq=7074000)
```
...only show the app's own intent/cached state, not hamlib's actual return code — so this is a real symptom I can confirm, but not something I can pin down further without logcat. If it'd help, `fileLog()`-ing the `rc` from `nativeSetMode`/`nativeSetFreq` (not just `Log.w`) would make this kind of QMX-hamlib issue debuggable from the phone alone next time, since logcat access isn't always available.

@laniol8926

Copy link
Copy Markdown
Author

Found the precise mechanism behind "transmitting but no audio reaches the radio" (and probably the earlier "good audio level but no waterfall/decodes" too): after a USB detach/reattach, the serial/CAT side reconnects correctly, but the USB audio side silently falls back to the phone's own mic/speaker and never reacquires the QMX.

Sequence from `debug.log`:
```
usbDetach: vid=483 pid=a34c
MicRecorder: reinit AudioRecord(VOICE_RECOGNITION) <- phone's own mic, not the radio
MicRecorder: stereo capture open, channel select=1
...
onNewIntent delayed: rigConnected=true <- CAT reconnected fine
```
No `openUsbAudioInput`/`UsbAudioDevice` lines appear anywhere after that detach — compare to every other reconnect earlier in the same log, which all show:
```
openUsbAudioInput: found device 0483:A34C name=QMX Transceiver
UsbAudioDevice: kernel audio driver detach via AudioControl iface 2: OK
UsbAudioDevice: trying libusb native capture fd=... iface=3 alt=1 ep=0x83 ...
UsbAudioDevice: libusb capture started OK
```
Then on the next TX attempt:
```
playFT8Signal: TX path branch: audioOutputDeviceId=54 usbAudioOutputVidPid=0000:0000 volume=100% autoVolume=false
playFT8Signal: using AudioTrack output (Android default sink)
```
`usbAudioOutputVidPid=0000:0000` confirms it: the app no longer thinks the QMX is an audio device at all, so it routes TX through Android's default sink (the phone speaker) instead of over USB. No error is shown to the user — the app just reports "transmitting" normally the whole time.

So the earlier "libusb native write FAILED (rc=-1 IO)" report and this one may be two different faces of the same underlying USB-audio-device-tracking issue (one still attempting and failing the libusb write, this one having given up on USB audio entirely and not noticing) — or two separate bugs. Either way, the common thread across all three things reported on this PR is: the USB audio side of the connection is noticeably less resilient to reattach/reconnect events than the CAT/serial side, and it fails silently rather than surfacing to the user or retrying.

@laniol8926

Copy link
Copy Markdown
Author

Follow-up on the band-retune issue above, with a harder confirmation: watching the QMX's own display directly (not just the app or the log), the radio's actual frequency stayed at 7074 (40m) when the app was commanded to switch to the 20m FT8 frequency (14074). This matches the `debug.log` sequence from that attempt:
```
bandSelect: index=20, band=14074000, controlMode=1, rigConnected=true
setOperationBand: setting freq=14074000 (rig.getFreq=7074000)
```
...with no subsequent log line ever showing the cached frequency updating to 14074000 afterward, consistent with the radio's display never actually moving. So this isn't just "the log doesn't prove it worked" — it's a direct, repeatable observation that the command had no effect on the radio at least some of the time.

@laniol8926

Copy link
Copy Markdown
Author

Ruling out the hardware definitively: the same QMX and same USB cable work completely normally under WSJT-X (running directly on the machine that was hosting the phone's MTP connection during this testing) — CAT, frequency control, and audio all fine there. So the three issues reported above (TX audio silently falling back to the phone's own speaker after a USB reattach, the radio not always retuning on a band change, and the earlier intermittent full USB detaches) are software-side in FT8AF, not the radio or cable.

@laniol8926

Copy link
Copy Markdown
Author

One more hardware variable ruled out: all of the phone-side testing above was repeated across two different USB-OTG cables/adapters between the phone and the QMX, with the same symptoms both times. Combined with the WSJT-X result above, that's the radio, the Quadra-side cable, and now both OTG cables all cleared — this is squarely software in FT8AF.

@laniol8926

Copy link
Copy Markdown
Author

Last piece of hardware-elimination: installed a different native Android FT8 app (danleetw/FT8TW) on a different, older Samsung Galaxy phone against the same QMX, and it's getting real decodes with no issues. So this isn't specific to the Moto G 3rd Gen used in the testing above either — two different phones, two different apps, same radio: only FT8AF shows the symptoms reported in this thread. That's about as clean a "software, not hardware" signal as this setup can produce.

@laniol8926

Copy link
Copy Markdown
Author

Correction/update to everything above — new evidence changes the picture significantly, and I want to flag that clearly rather than let the "it's FT8AF software, not hardware" framing in my earlier comments stand as the final word.

Summary of a cleaner round of testing:

App Phone (Android ver.) Result
FT8AF (this PR) Samsung Galaxy (Android 13) TS-480 works, TX and RX both fine
FT8AF (this PR) Moto G 3rd Gen (Android 15) CAT connects; audio does not work
danleetw/FT8TW (unrelated native app) Samsung Galaxy (Android 13) Works, real decodes
danleetw/FT8TW (unrelated native app) Moto G 3rd Gen (Android 15) CAT connects; audio does not work

Two independently-written apps, same split, same phones, same radio. That's a strong signal this is an Android 15 platform-level change, not a bug in either app's code.

The specific clue: on Android 15, plugging in the QMX only shows the standard "which app do you want to open this with" chooser — no separate USB-device-access permission dialog appears, for either app. On Android 13, the explicit permission prompt does show. CAT/serial still works on Android 15 (likely covered by whatever implicit grant comes from the app-chooser/intent-filter match), but USB Audio Class access needs its own explicit UsbManager.requestPermission() round-trip, and that dialog apparently never fires on Android 15 for this composite (CDC-ACM + UAC) device — so audio access silently has no permission and fails, with no error surfaced to the user either way.

I don't have logcat access in my environment to confirm the exact permission-API-level mechanism, but given it reproduces identically in two unrelated codebases, I'd treat this as an Android-version compatibility issue to track (and maybe worth a GitHub issue of its own, separate from this PR) rather than something to chase inside FT8AF's own USB-audio code. Apologies for the earlier, more confident "it's definitely FT8AF" framing — should have held that more loosely given how early those comments were in the testing.

@laniol8926

Copy link
Copy Markdown
Author

One more refinement, and this is good news: tried a third app on the same Android 15 Moto G — QFT8, which was written specifically for the QMX — and it gets real decodes, no issue.

So this isn't an unconditional Android 15 platform block after all. It's narrower than that: whatever FT8AF and danleetw/FT8TW are both doing (or not doing) to request/claim the USB Audio Class interface doesn't work on Android 15, but QFT8's approach does. That means there's a working reference implementation to compare against — this is a fixable Android-15 compatibility bug in the USB-audio permission/claim flow, not something to just wait out. I don't have QFT8's source to compare against, but if anyone has access to it (or wants to sniff what permission calls it makes vs. what UsbAudioDevice/openUsbAudioInput do here), that'd be the fastest way to close this out. Still think it's worth its own tracking issue separate from this PR, now with "QFT8 works, so there's a known-working pattern" as a starting point.

@laniol8926

Copy link
Copy Markdown
Author

Closing summary of the real-hardware testing on this PR, pulling the thread above together:

  • This PR's actual change (QMX via hamlib) is validated. CAT control, frequency set/read, and PTT all work correctly through the hamlib QMX backend on Android 13.
  • The audio-related symptoms reported above (TX silently routing to the phone speaker, no waterfall/decodes, "rig keyed but silent") are not caused by this PR, and not a radio/cable/OTG-adapter problem — ruled out via WSJT-X on the same radio/cable, two different OTG cables, and a second phone.
  • The real cause is an Android-OS-version compatibility gap, not a platform dead-end. Confirmed Android 13 works and Android 15 doesn't for USB audio access, reproducing identically in both FT8AF and the unrelated danleetw/FT8TW app — same radio, same phones. (Android 14 wasn't tested, so "broken on anything newer than 13" is an extrapolation, not confirmed directly.) But a third app, QFT8 (written specifically for the QMX), gets clean decodes on the very same Android 15 phone — so whatever changed in newer Android isn't an unconditional block, it's that FT8AF's (and FT8TW's) current approach to claiming the USB Audio Class interface doesn't survive it, while QFT8's does.

Conclusion: this isn't something to wait out — it's a known-fixable gap. FT8AF (and FT8TW, separately) would benefit from updating USB-audio permission/claim handling to work on current Android versions, ideally informed by comparing against how QFT8 does it. Worth its own tracking issue separate from this PR, since it's pre-existing and affects every USB-audio rig, not just the QMX. Happy to help narrow it down further if useful — thanks for bearing with the long comment thread while this got untangled.

@patrickrb

Copy link
Copy Markdown
Owner

I love this, thank you!

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