Skip to content

Promote dev → staging: compact decode view, QRP Labs QMX, Xiegu G106, Yaesu FTX-1, Wavelog sync, audio fixes - #967

Merged
patrickrb merged 181 commits into
stagingfrom
dev
Oct 27, 2026
Merged

patrickrb merged 181 commits into
stagingfrom
dev

Conversation

@patrickrb

@patrickrb patrickrb commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

Promotes everything merged into dev since the last staging promotion (33 PRs, 181 commits).

Device testing for this batch: several PRs added after this promotion opened touch TX, CAT, or USB audio (#922, #925, #946, #949, #952, #968, #969, #971). Check these on hardware with the staging internal build.

Decode screen

Rig / TX

Audio

Bands / settings

Logging

iOS / desktop

CI / tooling

🤖 Generated with Claude Code

patrickrb and others added 30 commits October 10, 2026 07:57
iOS TX kept silent on air — the rig's Data-VOX never keyed and TUNE was
silent at the radio too, even though RX decoded fine and the output route
was the USB interface at full volume. The waveform (full-scale GFSK) and
session routing were both correct; the audio simply never left the engine.

Root cause: RX and TX share one AVAudioEngine that AudioCaptureService starts
for *input* capture only. The first thing to touch mainMixerNode is the TX
path's `engine.connect(playerNode, to: mainMixerNode)` in ensureConnected().
Forming that mixer -> output link on an already-running engine leaves the new
output chain un-pulled by the render thread: the node keys without error but
no samples reach the hardware. play() only (re)started the engine when it was
stopped, and the capture engine is always running, so the freshly-formed
output connection was never brought up rendering. (PR #836 fixed the earlier
crash on this same path; this is the silent-TX state it left behind.)

Fix: ensureConnected() now reports whether it actually (re)connected, and
play() cycles the engine (stop -> start) once after a fresh connect so the
whole graph — player -> mixer -> output — comes up rendering. Gated on an
actual (re)connect via the pure, unit-tested
TxConnectionState.mustCycleEngineAfterConnect(didConnect:), so steady-state
transmit slots don't stutter the RX capture tap. This also covers the
post-config-change reconnect (DigiRig replug), which returns through the same
path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Wavelog user whose Station ID belongs to a different user than the API
key passes Test Connection and then has every QSO rejected with
HTTP 401 "station id does not belong to the API key owner." The app showed
"0 accepted" and nothing else; the reason only lived in debug.log.

- Catch-up sync dialog: show each service's rejection reason (JSON wrapper
  stripped) and, for Cloudlog-family servers, a hint naming the setting to
  fix (Station ID / API key).
- Post-QSO upload and background auto-sync: toast the same reason + hint.
- Cloudlog settings dialog: after Test Connection, warn when the entered
  Station ID is blank or not in the station list the server returned for
  this key.
- debug.log: log the station_profile_id on every rejected upload.

New pure helpers (CloudlogFailureHint.kt, StationIdCheck.kt) with unit
tests; verified on the emulator against a mock Wavelog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every PR that touches ft8af/** now gets an installable debug APK, uploaded
as an artifact and linked from a sticky PR comment. The PR build in
android.yml is an unsigned release APK, which phones refuse to install.

Also adds a PR description template (APK link, manual test checklist,
what changed, risks, automated tests) and branch-name and PR rules in
AGENTS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
Copilot review is enabled but can run out of credits. PRs now request
Copilot first, fall back to Claude's code-review skill, address every
finding before QA, and record which reviewer was used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From a Claude code-review pass (Copilot is out of credits):
- Sign with a committed debug keystore so each new PR APK installs over
  the previous one instead of failing with a signature mismatch.
- Check out the PR head SHA so the APK matches the commit it is named after.
- Retry assembleDebug like android.yml, since Maven Central rate-limits runners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copying it to ~/.android/debug.keystore was ignored on the runner (the APK
still carried a fresh runner key). Use AGP's injected signing properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds a 'Save Input Volume per Band' toggle that mirrors the per-band TX
output level from #355: the Input Volume set on a band is saved and
restored on band change, with bands that have no saved value keeping the
current level. Own toggle and config keys (perBandInputLevel /
perBandInputLevels); reuses the shared per-band helpers, which now take a
max level so the RX map can hold 0..200%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The alternates row laid chips out in a plain Row, so on narrow screens the
last chip in a band got squeezed and its label wrapped one character per
line. Use a FlowRow that fills the space after the band label so whole chips
move to the next line, and keep each chip label on a single line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FTX-1 owners had to guess which Yaesu entry works. A user confirmed CAT
works with the FT-891/991(USB) profile (38400 baud, 9-digit Yaesu gen-3
command set), so add a named "YAESU FTX-1(USB)" entry with that profile.

Appended at the end of rigaddress.txt because the selected rig is saved
as a list index; extend the golden-order test and pin the new entry's
profile to the FT-891/991(USB) one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "N of M enabled" label was computed as total - excludedBands.size, but
the persisted exclusion set can hold names that are no longer listed, so it
read "1 of 20 enabled" with six bands on. The same size check guarded the
last band, which blocked turning bands off while several were still on.

- Count enabled bands against the listed names, and prune stale names
  from the exclusion set on toggle.
- Keep rig-reported off-plan frequencies (appended by getIndexByFreq) out
  of the toggle list, and order the list by frequency.
- Show a message when the user tries to hide the last enabled band.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto clear TX offset could pick offsets as low as 200 Hz. FT8 TX audio
is not a pure sine, so at a low offset its 2nd and 3rd harmonics land
inside the SSB passband and waste RF power as spurs.

Add a "Lowest auto offset" setting (200-2000 Hz, 50 Hz steps, default
200 Hz so nothing changes until the operator raises it). It appears
under the auto-clear toggle in Transmission settings and limits both
the CQ-start pick and the mid-CQ relocation. Offsets the operator
chose by hand are not touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Enabled Bands list is now sorted by frequency, so the old
file-order expectation no longer holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ft8_snr() took the other 7 Costas tones of each sync symbol as the noise.
With our 2-symbol Hann window and time_osr=4, neighbouring symbols and
leakage put signal energy into those tones, so the noise estimate rose
with the signal: reports were squeezed into about -14..-2 dB (a +20 dB
station read -3). That value is also the report we transmit.

Noise now comes from the spectral baseline around the candidate: the
20th percentile of the time-averaged linear power over +/-48 bins
(+/-300 Hz). Signal is the mean linear power of the expected Costas tone.
SNR = 10*log10((sig - noise) / noise) - 31.5 dB, clamped to [-24, +40].
The 31.5 dB constant is fitted against the testdata/ft8 jt9 truth.
FT4/FT2 are unchanged.

decode_bench now pairs each matched decode's ft8_snr() with jt9's SNR
(keeping the highest SNR over duplicate decodes, as the app does) and
--assert-snr fails the run outside testdata/ft8/snr_bounds.txt, which
run_host_tests.sh/.ps1 now pass. Against jt9 (n=462):

  before: slope 0.19, r 0.48, MAE 8.10 dB, jt9>=+10 mean -6.3
  after:  slope 0.93, r 0.85, MAE 4.07 dB, jt9>=+10 mean +13.7

Recall and floors are unchanged (462/473). ft8_snr costs ~6.5 us per
call on the host, about 1.7 ms per slot, which is within bench noise.

Fixes #937

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups on #941:

- ft8_snr: measure the noise percentile separately below and above the
  candidate (its own 8 tones excluded) and keep the louder side. When a
  rig's audio filter edge falls inside the +/-300 Hz window, the rolled-off
  bins dragged the single-window percentile down: a synthetic 15 dB
  roll-off read +24 instead of +10. Percentile 0.2 -> 0.1 per side (as
  WSJT-X's baseline uses), cal re-fitted 31.5 -> 30.5 dB. Corpus vs jt9:
  slope 1.00, r 0.89, MAE 3.87 dB, bias -0.08 (was 0.93/0.85/4.07/+0.18).
- decode_bench: truth duplicates keep the highest SNR like the decoded
  side; a truth line without an SNR column still counts as truth (it is
  only left out of the SNR fit); --snr-table covers up to +49.
- snr_bounds.txt: add bias_min/bias_max (+/-1.5 dB) so a calibration
  shift can't pass; tighten r_min 0.85, mae_max 4.5, strong mean >= +10.
- Tests: filter edge on either side, top-edge clamp, too-narrow and
  out-of-range windows.
- monitor.c: note that kMagToPower depends on its byte encoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dence

The maintainer tests on a device once per release batch (the staging Play
internal build) instead of installing every PR's APK. Per-PR device testing
is limited to PRs labeled needs-device-test (transmit/PTT, CAT, USB audio or
serial, audio routing, permissions, signing/release config). QA posts
emulator screenshots or a recording as evidence before handoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four androidTest flows run on the CI emulator so QA can check a PR without a
phone (BUR-25): launch to the main screen, change band and see the new
frequency, feed a recorded FT8 WAV through the decoder into the decode list,
and a callsign surviving an activity restart. Each saves a screenshot; the
instrumented job uploads them (plus logcat on failure) as `emulator-flows`.

SlotTimerBar's endless per-frame loop now uses withInfiniteAnimationFrameMillis
so Compose UI tests can reach idle (no change on a device).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `instrumented` to android-gate's needs so a failing emulator flow
blocks the merge. The flows passed 5 runs in a row (run 39515242176,
attempts 1-5).

Also harden two flows: BandChangeFlowTest derives the expected dial from
the band sheet rows for the current mode instead of hardcoding FT8
frequencies, and both band/setting cleanup steps are best-effort so a
teardown throw can't mask the real failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude code-review on #942: the flows drive live app state (decode
pipeline incl. PSKReporter/auto-sequencer, band, operator identity) and
connectedAndroidTest uninstalls the app, so running them on a personal
phone is harmful. Skip every flow unless on an emulator, fail CI if any
flow's screenshot is missing (so a misfiring check can't pass as skipped),
document emulator-only in AGENTS.md, and bound-check RIFF chunk sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Real-hardware testing on PR #925 found that FA; (frequency) commands
from the Hamlib QMX backend never reach the radio after the initial
rig_open() handshake, despite hamlib logging the write and timing out
waiting for an ack. The sink callback that forwards hamlib's CAT bytes
to the real serial connector silently returns when getConnector() is
null, with no log anywhere -- making this exact failure mode
undiagnosable from the app's own logs. This adds a temporary warning
log so the next real-hardware test can confirm or rule out this path
directly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Ruled out the null-connector theory from the previous diagnostic commit:
hamlib's ID; probe does reach the real serial port, but FA; (sent moments
later in the same rig_open() call) never does, despite hamlib's own
write_block() logging a successful write in both cases. CableSerialPort
had zero logging on the receive path, so "the radio never replies" was
never actually verified either.

This adds:
- CableSerialPort.onNewData: logs every byte actually read off the wire
  (serial.recv[...]), before any rig-specific listener sees it.
- hamlib_jni.cpp's pump thread: logs exactly what it reads from hamlib's
  loopback socket on every read(), to confirm whether FA; ever reaches
  the pump thread at all.

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

Real-hardware testing on PR #925 found the QMX keeps falling back from
Digital to USB mode under the Hamlib backend, even though
setUsbModeToRig() correctly asks hamlib for PKTUSB. hamlib's QMX
backend (qrplabs_qmx_caps) uses the generic kenwood_set_mode(), which
only sends MD6; -- it has no knowledge of Q41;, the QRP Labs-specific
follow-up this exact radio needs to actually commit to and stay in
Digital mode. This is the same gap Dan Lee's native app previously had
to hand-patch for the same radio (see project history).

Testing also showed the mode reverts after any frequency change alone
(FA...;), not just at mode-set time, so Q41; is re-sent after
setFreqToRig() too, scoped to the QMX's model ID only so it never
reaches any other Hamlib-backed rig (currently just the FT-891 entry).

Not unit-testable: this path is gated behind ensureOpen(), which
HamlibRigTest's own doc comment already notes always returns false in
a JVM unit test (no native lib) -- same as every other hamlib call in
this class. Needs real-hardware re-verification, same as the rest of
this PR's hamlib-backed behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Real-hardware testing of the previous Q41; fix found the QMX still
reverts from Digital to USB after a frequency change, despite
setFreqToRig() now re-sending Q41;. rig_get_freq() sends the same FA;
command as a query (no parameter) -- if that alone also triggers the
reversion, and the app polls frequency periodically (CAT-liveness/dial
sync), mode would keep reverting on every poll regardless of the
set-path fix. Mirrors the same Q41; follow-up onto the read path.

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

The Q41; fix (e046e0a, 3789712) was based on a wrong premise. Checked
the official QMX CAT manual (qrp-labs.com/images/qmx/manuals/
cat_1_04_003.pdf): there is no Q41 command. The Q-series commands are
Q0-QB; Q4 is "Get/Set TX Rise Threshold" (0-99%, default 80), so
"Q41;" was actually setting TX Rise Threshold to 1% on every mode-set,
freq-set, and freq-read -- an unrelated, actively harmful side effect,
not a mode fix.

Root cause, verified against hamlib's vendored source: the QMX backend
(qrplabs_qmx_caps) sets no mode_table override, so kenwood_set_mode()
falls back to the generic kenwood_mode_table, where RIG_MODE_PKTUSB is
table index 13. kenwood_set_mode() encodes any index > 9 as a letter
('A' + index - 10), so PKTUSB becomes "MDD;" -- not a valid QMX mode
character. The manual only documents single-digit MD values (1, 2, 3,
5, 6, 7, 8, 9), so "MDD;" is silently rejected; setUsbModeToRig() was
never actually setting Digital mode via this path at all.

Table index 6 holds RIG_MODE_RTTY (commented "// FSK Mode" in hamlib's
own source) -- exactly the QMX manual's own mode 6, "FSK", its real
digital mode. hamlib's string table maps "RTTY" to RIG_MODE_RTTY
directly, so requesting that mode produces the valid "MD6;" through
hamlib's normal, standard code path -- no raw command bypass needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Real-hardware testing keeps showing "hamlib model 9" open instead of
2057 when "Qrp Labs QMX (Hamlib)" is selected, even right after a
fresh install. Traced every writeConfig("civ", ...) call site and
found none of them apply to a Hamlib-backed rig selection -- the only
writers are the ICOM-gated repair path (repairCivAddressAgainstModel,
which explicitly returns early for any non-ICOM instructionSet) and
the manual hex-edit field. So the earlier civAddress/CivAddressConfig
byte-masking theory doesn't hold up on inspection and needs direct
verification rather than more static reading.

Logs the exact rigName/address applied in setAddrAndBauRate() (rig
selection) and the exact civAddress value used to construct HamlibRig
in connectRig(), to see empirically where 2057 becomes 9.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Root-caused via real hardware testing + diagnostic logging: selecting
"Qrp Labs QMX (Hamlib)" always opened hamlib model 9 instead of 2057
after any app restart. RadioAudioSettings.kt (the active Compose rig
picker) persists civAddress through CivAddressConfig.encode(), which
masks with "& 0xFF" since a real ICOM CI-V address is always a byte --
2057 (0x809) round-trips through that as 9. Confirmed on-device:
connectRig() logged "constructing HamlibRig(civAddress=9)", and hamlib
model 9 (SDR#) opened, sending its own unrelated handshake bytes that
put the QMX into its firmware debug terminal menu instead of CAT mode.

Fix: a new GeneralVariables.hamlibModelId field and "hamlibModel"
config key, written as a plain unmasked decimal string and loaded the
same way, used instead of civAddress to construct HamlibRig in
MainViewModel.connectRig(). civAddress and its existing byte-range
persistence/repair logic (issue #753) are untouched -- that contract
is relied on elsewhere for genuine ICOM addresses and didn't need
widening. Updated both the active picker (RadioAudioSettings.kt) and
the legacy one (ConfigFragment.setAddrAndBauRate) to keep the new
field in sync; added hydration tests covering the fix and a malformed
value.

This also explains several earlier rounds of real-hardware testing on
this PR that looked like CAT/mode bugs in the Hamlib backend itself
(including the reverted Q41; mistake) -- they were actually testing
the wrong hamlib backend (SDR#) the whole time, not QMX at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
The previous fix (47c8fc4) left a real gap: an install that had
already selected a Hamlib rig before "hamlibModel" existed loads
hamlibModelId=0 on first launch after updating. Confirmed on real
hardware: HamlibNative.nativeOpen(0, ...) -> hamlib's rig_init(0)
fails, and ~0.5s later the whole process aborts with a native SIGABRT
("pthread_mutex_lock called on a destroyed mutex") -- not a graceful
failure, a hard crash loop on every launch.

Two fixes:
1. HamlibRig.ensureOpen() now refuses to call nativeOpen at all when
   modelId <= 0 -- a bad model ID can never reach native code again,
   regardless of how it got that way.
2. connectRig() self-heals a <= 0 hamlibModelId from the currently
   selected rig's own rigaddress.txt entry (by modelNo) and persists
   it, so an existing install recovers on its next connect instead of
   needing the user to manually re-select the rig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
Found via real-hardware testing: every single Hamlib CAT failure in
this whole PR's testing reported "0 chars" from the radio, even when
serial.recv showed real traffic arriving in the same window. Root
cause: HamlibRig.onReceiveData() only forwards radio bytes into the
native bridge when handle != 0, but the original single nativeOpen()
call set `handle` only *after* it returned -- and nativeOpen() itself
ran rig_open() internally, including its own synchronous ID;/FA;
handshake, before returning. So every byte the radio sent back during
that handshake was silently dropped on the Java side, and hamlib
always timed out waiting for an ack that could never reach it, even
against a radio that was genuinely replying.

Split the single native call into nativeCreate() (builds the bridge,
starts the pump thread, returns a handle -- does NOT call rig_open())
and nativeRigOpen(handle) (runs rig_open() on an already-created
bridge). HamlibRig.ensureOpen() now publishes `handle` between the two
calls, so nativeFeedFromRig can deliver the radio's handshake replies
in real time, matching the threading model this file's own top
comment already documented (the serial read thread feeding acks while
the worker thread blocks in a synchronous rig_* call) -- the original
combined nativeOpen() just violated it for its own internal rig_open().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SaooA8HLsiETQ12Tbwwyzf
patrickrb and others added 11 commits October 27, 2026 09:58
#950 raised LogFileImport.MAX_IMPORT_BYTES from 50 to 100 MB.
readFully_throwsWhenExceedingCap pushes one byte past the cap through
readFully(), which buffers everything up to the cap first: over 100 MB in
a ByteArrayOutputStream, plus its growth copies, inside the 512 MB test
JVM. Depending on what else the run left on the heap it threw
OutOfMemoryError instead of the expected IOException, failing "Unit tests
& coverage" on dev (cc3a006) and on unrelated PRs (#966).

readFully() now takes the cap as a parameter; the one-argument form the
import calls still passes MAX_IMPORT_BYTES, so the app is unchanged. The
test uses a 16 KB cap, with a new at-the-cap case and a check that the
default stays 100 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop the readFully cap test from running CI out of heap
New setting under Logging > Cloudlog/Wavelog, "Download QSOs from Wavelog
when syncing", off by default. With it on, Sync (Logbook button and
auto-sync) reads the Wavelog logbook once and uses it both ways: insert
the remote QSOs no local row accounts for, then upload the pending rows
the logbook doesn't have.

- The download asks get_contacts_adif for the fields a full row needs;
  the duplicate-only check keeps its 7 fields. Older Wavelog sends full
  ADIF, so kept records are trimmed to the same field list.
- Remote QSOs are paired with every local row (synced or not) by #936's
  one-to-one matcher, so a second sync inserts nothing.
- Rows go in through the ADIF import's QSLRecord(map) +
  doInsertQSLData(..., false), in batches of 200 per transaction. A record
  the insert treats as an existing row (same call/date/time/mode) is not
  counted or flagged, so a pending local row stays pending.
- Inserted rows are marked synced for Cloudlog, QRZ and WRL, so a Wavelog
  download never pushes another logger's QSOs to QRZ/WRL by itself.
- The worked-before cache is reloaded after inserts.
- Plain Cloudlog: the settings row is greyed out with a hint once a probe
  says the server can't list QSOs; a sync just skips the download.
- Sync dialog line "Cloudlog / Wavelog / Nextlog: N downloaded" in all
  locales; auto-sync debug.log gets cloudlogDownloaded=N and also runs
  with nothing pending when the download is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…port note (#965 review round 1)

- Downloaded QSOs skip PotaSessionManager.stampQso (new doInsertQSLData overload).
- A remote QSO with the same call/date/time_on/mode as an unpaired local row is
  skipped instead of overwriting that row's grid and reports.
- A remote QSO without COMMENT gets an empty comment, not 'imported at ...'.
- The toggle stays switchable while on, even when the server can't list QSOs.
- A local-log read failure falls back to the upload pass's own duplicate check;
  download read/insert failures show in the sync dialog's Cloudlog error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…host

Review fixes for the two-way sync:

- Memory: at most MAX_KEPT_RECORDS (20,000) downloaded QSOs keep their
  fields. Past that the kept fields are dropped, nothing is inserted, and
  the sync says the logbook is too big to download (the duplicate check
  still sees every QSO). Reading stops at 250,000 QSOs overall, pages are
  2,000 QSOs, and an OutOfMemoryError or StackOverflowError while reading
  the logbook drops the partial log instead of crashing the app. Uncaught,
  that crash repeated on every launch, since auto-sync retries at start.
- Hostile or broken replies: response bodies are read in chunks and capped
  at 8M chars (readLine() buffered one endless line whole). Deeply nested
  JSON no longer overflows the stack in the page parser, the station_info
  parser or the Logging settings probe. Downloaded field values are cut to
  1 KB (COMMENT 4 KB), so one huge field can't make a row too big for a
  CursorWindow and break every later read of the logbook.
- Redirects: a POST follows a redirect only to the same host and never
  from https down to http. The body carries the API key.
- A Station ID / API key rejection now replaces an earlier download error
  as the reported error, so its fix-it hint still shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add QRP Labs QMX via the hamlib backend
Download QSOs Wavelog has and the phone doesn't during Sync (#965)
QMX: transmit by CAT tone commands (stacked on #925)
…on-grant

Restart mic capture when RECORD_AUDIO is granted on first launch
Resolves the rig-list conflict with #946 and #925 (both QMX entries are on
dev now): "QRP Labs QMX" and "Qrp Labs QMX (Hamlib)" keep their indexes and
"XIEGU G106(USB)" is appended after them in rigaddress.txt and
GOLDEN_ORDER. InstructionSet's comment no longer calls 27 an open PR's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#952 builds the TX AudioTrack with setPerformanceMode(LOW_LATENCY) and
logs getPerformanceMode(). Both exist only from Android 8.0 (API 26), and
minSdk is 23. On Android 6.0-7.1 they throw NoSuchMethodError, an Error
the TX paths' catch (Exception) doesn't catch, so every sound-card over
and TUNE crashed the app there.

Request the mode only on API 26+, with the SDK_INT check inline so lint's
NewApi check sees it. Older Android builds the same track without it, as
before #952. TxAudioTrackApiLevelTest builds the track under Robolectric
on API 25, 26 and the current runtime; against the unguarded code the
API 25 case fails with NoSuchMethodError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix TX crash on Android 6–7: guard AudioTrack performance mode behind API 26
Add safe Xiegu G106/DE-19 support and fixed receive timing
@patrickrb patrickrb changed the title Promote dev → staging: compact decode view, Yaesu FTX-1, SNR fixes, ADIF import Promote dev → staging: compact decode view, QRP Labs QMX, Xiegu G106, Yaesu FTX-1, Wavelog sync, audio fixes Oct 27, 2026
@patrickrb
patrickrb merged commit 9adb71b into staging Oct 27, 2026
43 checks passed
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.

4 participants