Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
1d138cd
Add QRP Labs QMX via the hamlib backend
laniol8926 Oct 20, 2026
0421739
Add diagnostic log when HamlibRig's sink drops bytes on null connector
laniol8926 Oct 21, 2026
641baee
Add diagnostic logging on the serial receive path and pump thread read
laniol8926 Oct 21, 2026
e046e0a
Send QMX's vendor-specific Q41; after mode-set and after every freq c…
laniol8926 Oct 21, 2026
3789712
Also send Q41; after readFreqFromRig(), not just setFreqToRig()
laniol8926 Oct 21, 2026
9b5e878
Revert incorrect Q41; fix; use the mode string that actually produces…
laniol8926 Oct 21, 2026
fde49bf
Add diagnostic logging on rig selection and HamlibRig construction
laniol8926 Oct 21, 2026
47c8fc4
Fix hamlib model ID truncation: persist it separately from civAddress
laniol8926 Oct 21, 2026
4a76f2a
Fix crash: hamlibModelId=0 (migration gap) aborted the whole process
laniol8926 Oct 21, 2026
d78aebe
Fix: publish the handle before rig_open()'s handshake, not after
laniol8926 Oct 21, 2026
83cf1f7
Add status doc for the QMX hamlib backend work (PR #925)
laniol8926 Oct 21, 2026
6dfd6ce
Log AudioTrack preferredDevice outcome for TX (symmetric with RX)
laniol8926 Oct 22, 2026
5d89ca5
Fix RX: reopen MicRecorder when the audio input device is changed
laniol8926 Oct 22, 2026
e0ad4a3
Log reinit preferredDevice outcome + hydrated audioInputDeviceId
laniol8926 Oct 22, 2026
abb8ad9
Fix RX: request USB mode for FT8/FT4, not RTTY (literal FSK)
laniol8926 Oct 22, 2026
bc261b3
Revert FT_MODE to RTTY (Digi): mode was never the RX cause
laniol8926 Oct 22, 2026
5b76f4b
Fix RX: a transient AudioRecord failure permanently stopped capture
laniol8926 Oct 22, 2026
9ed68ff
Document the RX investigation (2026-10-21/22 overnight session)
laniol8926 Oct 22, 2026
12815ec
Fix RX: force QMX I/Q mode off on connect (Q90;)
laniol8926 Oct 22, 2026
c790e8d
Document the QMX I/Q-mode-off fix (Q90;) as the likely real RX fix
laniol8926 Oct 22, 2026
38f8e9d
Fix RX: capture PCM16 instead of float, convert in software
laniol8926 Oct 22, 2026
c7f2c4c
Document the real RX root cause: float capture breaks on this USB HAL
laniol8926 Oct 22, 2026
7142484
Confirm RX fix on real hardware: real FT8 decodes
laniol8926 Oct 22, 2026
59dbe26
Fully confirm RX fix (7 decode cycles, fmt=1 throughout); note follow-up
laniol8926 Oct 22, 2026
4f44820
Address Patrick's PR review: CI fix + findings 2-5
laniol8926 Oct 22, 2026
07dd884
Address findings 6, 9, 10: drop temp diagnostics, trim/merge comments…
laniol8926 Oct 22, 2026
a99d596
Add test coverage for the shouldBeRunning restart decision (finding 7)
laniol8926 Oct 22, 2026
60fa988
Merge remote-tracking branch 'origin/dev' into feat/qmx-hamlib-rig
laniol8926 Oct 22, 2026
d00353e
Merge branch 'fix/micrecorder-usb-pcm16-capture' into feat/qmx-hamlib…
laniol8926 Oct 22, 2026
9384822
Log the resolved preferredDevice id, not just the requested one
laniol8926 Oct 22, 2026
b84274a
Merge branch 'fix/adif-import-size-limit' into feat/qmx-hamlib-rig
laniol8926 Oct 22, 2026
89053ae
Merge branch 'fix/adif-rst-parse-and-invalid-tally' into feat/qmx-ham…
laniol8926 Oct 22, 2026
e707bfe
Merge branch 'fix/adif-rst-parse-and-invalid-tally' into feat/qmx-ham…
laniol8926 Oct 23, 2026
b30f7db
Back off native open retries against a persistently unresponsive radio
laniol8926 Oct 23, 2026
32848d9
Use SystemClock.elapsedRealtime(), not wall-clock, for the open-retry…
laniol8926 Oct 23, 2026
f8867e9
Fix the backoff's sentinel/clock test interaction: use Long.MIN_VALUE…
laniol8926 Oct 23, 2026
dd17241
Fix a test-only race: drain the worker before moving the fake clock
laniol8926 Oct 23, 2026
00fb7db
Default QMX TX audio to LEFT channel: fixes the TX-underdrive issue
laniol8926 Oct 23, 2026
091ec4f
Merge branch 'fix/tx-audio-attributes-bypass-enhancements' into feat/…
laniol8926 Oct 23, 2026
971660d
Merge corrected TX audio attributes fix (revert USAGE_VOICE_COMMUNICA…
laniol8926 Oct 23, 2026
27b31fd
Pause RX capture during TX when sharing one USB rig (fixes silent TX)
laniol8926 Oct 23, 2026
0ea5ab4
Merge dev into feat/qmx-hamlib-rig
patrickrb Oct 27, 2026
b0c37f9
QMX RX pause: gate on the QMX, nest pauses, retire the old reader
patrickrb Oct 27, 2026
0f638ee
Merge dev into feat/qmx-hamlib-rig
patrickrb Oct 27, 2026
67b2e93
Treat #946's native QMX entry as a QMX for the RX pause and TX channel
patrickrb Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,35 @@ interface (that runs the real `usb_audio_disconnect`). A genuine bus drop
looks different: `usbDetach` for the hub, serial and audio devices together
and `serial.send: port not open!` — that one is electrical (RF resetting the
hub), not software.

**5. On at least one phone's USB-audio HAL, RX capture and TX playback
cannot share one USB rig at the same time.** Opening TX's `AudioTrack` while
`MicRecorder`'s RX `AudioRecord` is still open on the same USB device fails
at the ALSA proxy level, and every subsequent write is then silently
dropped — the generated waveform can be full-scale all the way to
`AudioTrack.write()` and still never reach the rig, with no exception and no
error surfaced to the app. Tell from log (vendor tags, MediaTek-specific
spelling, but the pattern generalizes): `AudioALSAPlaybackHandlerUsb: open(),
proxy_open fail, ret -12` right after `alsa_device_proxy: proxy_open()
pcm_is_ready() failed: cannot set hw params: I/O error`, then
`AudioALSAPlaybackHandlerUsb: write(), mPcm is NULL!` repeated once per
buffer for the rest of the transmission (thousands of lines on a 13 s over).
Correlates exactly with whether `AudioALSACaptureHandlerUsb` is open at that
moment — a reference app (QFT8) that drives the same radio to full output
runs strictly half-duplex: it closes its USB capture, plays, then reopens
capture, and its opens never fail. Fixed by doing the same thing around
every "over"/TUNE key-down: `MainViewModel`'s shared `beginKeying()`/
`endKeying()` (already the single choke point for PTT and SCO around TX, see
items above) now calls `HamRecorder.pauseForTx()`/`resumeAfterTx()`, gated by
`MainViewModel.shouldPauseRxCaptureForTx()`: only for the QRP Labs QMX, and
only when RX is actually an Android capture session (not a LAN rig, not
already USB-direct) on the *same* USB VID:PID as TX's output (not USB-direct
output either, which is separately broken for unrelated reasons). Keep the
QMX gate: a matching VID:PID alone is true for every single-cable USB rig
(IC-7300, IC-705, IC-9700, FT-991A, Digirig, SignaLink...), and pausing RX
there blanks the waterfall during TX and breaks full-duplex satellite
monitoring for a HAL conflict that was only ever confirmed with a QMX.
Widen it only on a confirmed report from another rig. Pauses nest (a tune
carrier and an over can overlap), so `HamRecorder` counts them, and
`MicRecorder.stopRecord()` joins the old reader thread so a quick
resume can't leave two readers on one `AudioRecord`.
1 change: 1 addition & 0 deletions ft8af/app/src/main/assets/rigaddress.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ Yaesu FT-891 (Hamlib),40C,38400,26
ICOM IC-7300MK2,B6,115200,0
YAESU FTX-1(USB),00,38400,2
QRP Labs QMX,00,9600,27
Qrp Labs QMX (Hamlib),809,9600,26
78 changes: 52 additions & 26 deletions ft8af/app/src/main/cpp/ft8af_glue/hamlib_jni.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ struct HamlibBridge {
std::atomic<bool> running{false};
jobject sink = nullptr; // global ref to HamlibNative.HamlibSink
jmethodID onBytesToRig = nullptr;
int model_id = 0; // for logging only; set in nativeCreate, read in nativeRigOpen
bool opened = false; // rig_open() succeeded; nativeClose only rig_close()s then
};

// ---- pump thread: hamlib's outbound CAT bytes -> Java sink ------------------
Expand Down Expand Up @@ -118,12 +120,23 @@ static HamlibBridge *handle_to_bridge(jlong h) {
extern "C" {

/*
* nativeOpen: build the bridge, init+open the rig on the loopback socket.
* Returns an opaque handle (>0) or 0 on failure.
* nativeCreate: build the bridge and start the pump thread, but do NOT call
* rig_open() yet. Returns an opaque handle (>0) or 0 on failure.
*
* Split from the open call (see nativeRigOpen below) so Java can publish the
* handle to HamlibRig.handle BEFORE rig_open()'s own synchronous handshake
* runs. HamlibRig.onReceiveData() only forwards radio bytes into the bridge
* when handle != 0 — with the original single nativeOpen(), handle was only
* set after it returned, so every byte the radio sent during rig_open()'s own
* handshake (its ID;/FA; probes) was silently dropped on the Java side, and
* hamlib always saw 0 chars and timed out, even against a radio that was
* actually replying. Confirmed on real hardware: serial.recv showed real
* traffic arriving while hamlib's own trace reported "Timed out ... after 0
* chars" for the exact same window, every single time.
*/
JNIEXPORT jlong JNICALL
Java_com_k1af_ft8af_wave_HamlibNative_nativeOpen(JNIEnv *env, jclass,
jint modelId, jobject sink) {
Java_com_k1af_ft8af_wave_HamlibNative_nativeCreate(JNIEnv *env, jclass,
jint modelId, jobject sink) {
ensure_vm(env);
auto *b = new HamlibBridge();

Expand Down Expand Up @@ -173,30 +186,40 @@ Java_com_k1af_ft8af_wave_HamlibNative_nativeOpen(JNIEnv *env, jclass,
close(b->listen_fd); delete b; return 0;
}

// 5. open (connects to our listener; initial CAT handshake flows over the
// bridge to the real radio via Java)
b->model_id = modelId;
LOGI("hamlib bridge for model %d created on 127.0.0.1:%d (not yet opened)",
modelId, port);
return reinterpret_cast<jlong>(b);
}

/*
* nativeRigOpen: run rig_open() on an already-created bridge (see
* nativeCreate above for why this is split out). Java must publish the
* handle to HamlibRig.handle before calling this, so nativeFeedFromRig can
* deliver bytes the radio sends during rig_open()'s own handshake.
*
* Returns RIG_OK (0) on success, or a (negative) hamlib error code. Either
* way the bridge is left alive: the caller (HamlibRig) owns releasing it via
* nativeClose, exactly once, never concurrently with nativeFeedFromRig. A
* free done here on failure raced the serial read thread, which can still be
* inside nativeFeedFromRig(handle, …) on this same handle — the radio is
* actively replying to the handshake when rig_open() fails, so that race was
* not theoretical.
*/
JNIEXPORT jint JNICALL
Java_com_k1af_ft8af_wave_HamlibNative_nativeRigOpen(JNIEnv *, jclass,
jlong handle) {
auto *b = handle_to_bridge(handle);
if (b == nullptr) return -1;

int rc = rig_open(b->rig);
if (rc != RIG_OK) {
LOGE("rig_open failed: %d (%s)", rc, rigerror(rc));
b->running = false;
// The pump thread may still be blocked in accept() (hamlib never
// connected). Close the listener FIRST to release accept(); if it did
// connect, shutting down srv_fd releases a blocked read(). Only then is
// pthread_join safe — otherwise it would hang forever.
int lfd = b->listen_fd.exchange(-1);
if (lfd >= 0) { shutdown(lfd, SHUT_RDWR); close(lfd); }
int sfd = b->srv_fd.load();
if (sfd >= 0) shutdown(sfd, SHUT_RDWR);
pthread_join(b->pump, nullptr);
rig_cleanup(b->rig);
env->DeleteGlobalRef(b->sink);
sfd = b->srv_fd.load();
if (sfd >= 0) close(sfd);
delete b;
return 0;
return rc; // caller still owns the bridge and frees it via nativeClose
}
LOGI("hamlib rig %d opened via 127.0.0.1:%d", modelId, port);
return reinterpret_cast<jlong>(b);
b->opened = true;
LOGI("hamlib rig %d opened", b->model_id);
return RIG_OK;
}

/* Feed bytes received from the real radio into hamlib's read side. */
Expand Down Expand Up @@ -273,13 +296,16 @@ Java_com_k1af_ft8af_wave_HamlibNative_nativeClose(JNIEnv *env, jclass, jlong han
b->running = false;
// Unblock the pump whether it is parked in accept() (never connected) or
// read(): close the listener first, then shut down the accepted socket.
// Joining before this could deadlock. See nativeOpen's failure path.
// Joining before this could deadlock. See nativeRigOpen's failure path.
int lfd = b->listen_fd.exchange(-1);
if (lfd >= 0) { shutdown(lfd, SHUT_RDWR); close(lfd); }
int sfd = b->srv_fd.load();
if (sfd >= 0) shutdown(sfd, SHUT_RDWR);
if (b->pump) pthread_join(b->pump, nullptr);
if (b->rig) { rig_close(b->rig); rig_cleanup(b->rig); }
if (b->rig) {
if (b->opened) rig_close(b->rig);
rig_cleanup(b->rig);
}
sfd = b->srv_fd.load();
if (sfd >= 0) close(sfd);
if (b->sink) env->DeleteGlobalRef(b->sink);
Expand Down
Loading
Loading