Skip to content

Release: promote dev → staging (Android Auto status → Play internal)#452

Merged
patrickrb merged 91 commits into
stagingfrom
dev
Jul 7, 2026
Merged

Release: promote dev → staging (Android Auto status → Play internal)#452
patrickrb merged 91 commits into
stagingfrom
dev

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Why

Promotes devstaging, which publishes to the Play internal track (per android.yml). The motivating change is the Android Auto read-only QSO status feature (PR #431 + follow-up #443/fix-436): a sideloaded/adb-installed build is denied by Android Auto's CAR.VALIDATOR on a real car, so the feature can only reach a physical head unit once the app is Play-installed. Landing it on the internal track is what makes it appear in the car for testers.

Scope

This is a standard promotion (like #397), so it carries everything merged to dev since the last staging promotion — 87 commits — not just the Android Auto work. Notable inclusions:

Note on the Android Auto category

The car app is declared androidx.car.app.category.IOT. Validation (the sideload gate) happens before category, so IOT vs POI is irrelevant to launcher visibility — left as-is.

Publish behavior

Merging this triggers the internal-track release (signed AAB via CI). No publish happens until merge.

After it's live on internal: uninstall the debug-signed sideload on the test phone first (signature mismatch blocks the upgrade), then install from Play.

Reid-n0rc and others added 30 commits July 3, 2026 11:35
… <...>

Issue #392: answering a station running a compound/prefixed call (e.g.
SV8/DM5HF) showed the received call only as "<...>", making QSOs
impossible.

A compound call doesn't fit FT8's 28-bit standard-callsign field, so on
the air it is sent as a short hash — a 12-bit hash in a Type-4
nonstandard frame or a 22-bit hash in a standard frame. ft8_lib's
decoder resolves those against a hash table it accumulates from calls
heard in full over the air, but that table is never seeded with the
operator's own call, so an answer to our own compound CQ comes back as
"<...>". The JNI layer then zeroed the message's hash fields (a "<...>"
string has no computable hash), discarding the one number the Java side
could have resolved: MessageHashMap is already seeded with the
operator's own call and its hash (DatabaseOpr / ConfigFragment).

Fix: when the decoder yields "<...>", recover the raw hash straight from
the frame payload and hand it to Java so the seeded MessageHashMap
resolves it. New pure helpers in ft8_call_hash.h extract the 12-bit
(nonstandard) and 22-bit (standard) hashes, mirroring ft8_lib's bit
layout; both ft8_decode_jni.cpp and ft2_decode_jni.cpp populate the
corresponding hash field for any unresolved call.

Tests:
- test_call_hash.c (host): encodes real frames with the shipped ft8_lib
  packer and asserts the helpers recover exactly the WSJT-X hash, wired
  into run_host_tests.ps1.
- Ft8MessageTest: the copy constructor resolves a seeded compound call
  from callToHash22 and leaves an unseeded hash as "<...>".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add WSJT-X's own reference-packer (ft8code) 77-bit payloads for the two
answer frames to CQ from SV8/DM5HF — the compound call as a 22-bit hash
in the call_to and call_de positions — and assert ft8af_std_hash22
recovers 711279, matching WSJT-X's hash22calc and the app's own hash.

Verified separately that a WAV of "<SV8/DM5HF> K1ABC JN35" generated by
WSJT-X's ft8sim, decoded through the app's exact RX pipeline (monitor +
ft8_find_sync + ft8_decode), yields "<...>" (as WSJT-X's jt9 does with an
unseeded hash table) and then resolves to "SV8/DM5HF K1ABC JN35" via the
seeded hash list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a JVM test proving the dynamic case: a compound station heard only as
a hash shows "<...>", then once its full call is decoded (e.g. it calls
"CQ <compound>") the hash list learns hash->call and later hashed frames
resolve. This is mode-independent (FT8 and FT4 share the message/hash
layer).

Add docs/wsjtx-interop-compound-calls-392.md — a reproducible runbook that
validates interop against WSJT-X's own reference tools (hash22calc,
ft8code, ft8sim, jt9): hash agreement, golden-vector extraction, FT8 RX
end-to-end on ft8sim audio (config-seeded and learn-from-CQ), and FT8+FT4
TX decoded by jt9. Every command in the runbook was executed and produces
the documented output.

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

Field report (POTA activation 2026-07-03): QSOs stalled repeating the
previous message while the partner's reply was visible on screen, and
some stations calling K1AF were never answered. debug.log shows the
cause: MainViewModel only handed fast-pass decodes to
parseMessageToFunction, so any reply found only by a deep, subtraction
or late/cross-slot pass (routine since the decode-gap work, PRs
#367-#370) never reached the sequencer. Both stalled QSOs in the report
show newOrder=-1 for two cycles while the partner's R-report was
deep-pass-only (one cycle even logged replyToMe=false while the R-report
was displayed in the QSO panel).

Fix: deep passes now drive the sequencer via an evidence-only parse
variant - they can advance the QSO, trigger the RR73->73 reply,
complete on a received 73, and enqueue callers, but never make
absence-of-evidence decisions (no-reply counting, give-up, "partner
went silent" completions), which stay fast-pass-only so a deep pass
finding nothing new is not double-counted as a second no-reply.

Deep results that land while TX is playing are stashed in
PendingSequencerDecodes (age-capped) and replayed at the first delivery
after key-up, so evidence arriving mid-transmission still advances the
next cycle instead of being dropped. Results landing before TX keys
(~0.9s window) upgrade the very next transmission.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Field report (POTA activation 2026-07-03): 5 of 265 transmissions
logged "libusb native write FAILED (rc=5 UNKNOWN) ... TX DROPPED" - the
rig keyed and transmitted dead air. rc=5 is LIBUSB_TRANSFER_NO_DEVICE:
onOutputComplete stores the failing transfer's (positive)
libusb_transfer_status as nativeWrite's return value, but
describeLibusbWriteError only mapped negative libusb_error codes, so
the actual failure mode (device falling off the bus mid-TX, typically
RF into the USB link at power) was logged as UNKNOWN. Two of the five
drops were load-bearing: a first RR73 and a first R-report went out
silent, stretching both QSOs.

Changes:
- describeLibusbWriteError now names the positive transfer statuses
  (TRANSFER_NO_DEVICE etc.) alongside the negative error codes.
- The UsbRequest fallback is gated by shouldFallbackToUsbRequest(): it
  restarts the message from byte 0, which is only sane for failures
  before streaming began (its original purpose - kernels where libusb
  can't run). Mid-stream deaths (>1s in), device-gone and
  user-cancelled failures now drop the cycle cleanly instead of ever
  keying an off-grid, overlapping restart.
- The operator gets an on-screen warning when a TX write drops (rig
  keys but sends silence - previously invisible in the field); the
  user's own STOP press does not warn. String added to all 16 locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot review on PR #406: Ft8Message.utcTime is in the NTP/GPS-corrected
time base (UtcTimer.getSystemTime() = delay + currentTimeMillis), while
the stash/drain calls passed the raw system clock. On a phone with a
badly-set clock (the reason the app has clock sync at all) the mixed
bases would skew age eviction by the correction offset - keeping stale
evidence or evicting fresh evidence. Pass the corrected time and
document the expected base on PendingSequencerDecodes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot review findings, both valid:
- writeElapsedMs was measured with System.currentTimeMillis(); a wall
  clock step (NTP set, user change) could fake a fast failure and
  wrongly allow the restart-from-zero UsbRequest fallback after audio
  already went to air. Use SystemClock.elapsedRealtime().
- The tx_audio_dropped toast hard-coded "USB audio device dropped" as
  the cause, but writeAudio can fail for other reasons (libusb setup
  errors, UsbRequest path failures). Genericized to "USB audio error"
  in all 16 locales; the precise cause stays in debug.log via
  describeLibusbWriteError.

Also move the shouldWarnTxDropped tests into their own TxDropWarningTest
file so this PR no longer appends to the same FT8TransmitSignalTest
region as PR #406 (eliminates the guaranteed merge conflict between the
two open PRs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Feed deep/late-pass decodes to the QSO auto-sequencer
Handle USB TX writes that die mid-message (device off the bus)
shouldRun() and markStarted() were two separate @synchronized calls with
no lock held across them, so two triggers racing (app-start syncNow vs
the ConnectivityManager onAvailable callback) could both observe
inFlight == false and both start a sync pass, re-uploading the same QSO
rows to QRZ/Cloudlog. Collapse the check and the mark into a single
@synchronized tryStart(now, anyServiceEnabled) so exactly one caller can
win the gate.

Closes #399

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
…tate

MainViewModel.afterDecode read-modify-wrote currentMessages and
currentDecodeCount with no synchronization. Since the late full-slot
pass (#363), slot N's deep delivery routinely overlaps slot N+1's early
pass, so two decode threads run afterDecode concurrently: label-list
assignments clobber each other, the += on the decode count loses
increments, and a reader can iterate a list reference mid-reassignment.

Move both values into DecodeCycleState, whose synchronized methods
serialize every RMW on one monitor and give readers a happens-before on
the latest snapshot. countAfterPass returns the value it produced so the
caller posts its own update instead of re-reading a field another pass
may have advanced. The label lists themselves stay safe to iterate
because WaterfallLabelMessages.afterPass never mutates a published list.

External readers (LogHttpServer, GridTracker, CallingListFragment,
SpectrumView/Fragment, WaterfallScreen) go through the new accessors;
the Kotlin site binds to getCurrentMessages() via synthetic property
unchanged.

Closes #398

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
…nstant

findEndpoints() guessed the channel count as wMaxPacketSize / (48 * 2)
with truncating integer division. A 44.1 kHz stereo capture endpoint
(~180-byte packets) judged against the 48 kHz frame size counted as
mono, so when the UAC descriptors do not parse (the legacy fallback in
selectInputAltSetting, exactly the #364 device class) the capture loop
treated interleaved L/R frames as consecutive mono samples and nothing
decoded.

Extract a rate-aware, rounding channelsForMaxPacketSize(mps, rate)
helper (rounding matters: 44.1 kHz frames alternate 44/45 samples so
packets are not exact multiples), use it for the initial guess, and
re-derive the count once the real rate is settled: activateInput()
recomputes it whenever the descriptors did not supply a channel count,
and activateOutput() recomputes against the requested output rate.

Closes #400

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
sendFreeTextOnce armed transmitFreeText/freeTextOneShot and relied on
setActivated(true) failing to clean up — but setActivated only blocks on
SWR lockout, never the callsign, so with a short/empty callsign the
cleanup guard was skipped while setTransmitting()/transmitNow() refused
to key. afterPlayAudio()/shouldStopAfterOneShot(), the normal disarm
point, never ran, and the stale free text replaced the operator's next
CQ/standard message once they fixed their callsign.

Extract the keying guard into isCallsignReadyToTransmit() (now shared by
setTransmitting, transmitNow, restTransmitting), check it in
sendFreeTextOnce BEFORE arming (with the usual callsign-error toast),
disarm an armed one-shot whenever setTransmitting(true) is refused, and
correct the guard comment that wrongly claimed setActivated validates
the callsign.

Closes #401

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
… lock

forceCompleteAfterStall read dataCount/voiceData from the watchdog
thread with no happens-before against the capture thread, so a stall
delivery could race a nearly-complete fill: stale count, won CAS, and a
short/zero-tailed buffer handed to the decoder while the final samples
were still landing.

Add a per-monitor bufferLock: chunk copies (and the already-delivered
check) happen under it, and the watchdog takes it before inspecting
dataCount and claiming the completed CAS — waiting out an in-flight
chunk and deciding the winner on the true count before the buffer is
handed out. Delivery runs outside the lock; later chunks check
completed under the lock so the delivered buffer can no longer change.

Closes #404

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
The i3=0 n3=2 (EU VHF) and i3=0 n3=6 (contesting combo) decoders in
message.c are hand-rolled bit extraction with direct formatting and
shipped with no coverage — a wrong shift or mask would silently
mis-render every such contact.

test_contest_decode.c assembles 77-bit payloads with an independent
MSB-first bit writer (callsign bits mined from the vendored packer's own
standard-frame output, so this is an encode/decode consistency check)
and asserts the fully formatted decode: type routing, /P and R flags,
RST+serial rendering incl. zero padding and the s12 all-ones boundary,
grid6/grid4 extremes, and c28a/c28b field independence. Wired into
run_host_tests.ps1 and run_host_tests.sh.

Closes #403

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
The #392 backstop (recover the raw 22-/12-bit hash when the decoder
yields <...>) existed only in the STANDARD and NONSTD_CALL JNI
branches, so the new FD/RTTY/WWROF/EU_VHF/CONTESTING decoders (and
DXpedition) re-introduced the bug per message type: a hashed compound
call in a contest exchange came back as the placeholder with zeroed
hash fields.

Replace the special-cased blocks with one type-keyed pass:
ft8af_call_hash22 knows each message type's c28 field offsets (straight
from the decoders in message.c) and applies unpack28's NTOKENS/MAX22
hash test on a generic MSB-first field read; the JNI (ft8 + ft2) runs
it for every decoded type, writing the hash only when the decoder did
not produce a plain call for that field. ft8af_std_hash22 stays as a
thin wrapper. Future decoders with c28 fields only need a case in the
offset table.

Closes #402

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Two parallel stacks metered the same 12 kHz RX audio on different
thresholds: AudioInputLevel (spectrum meter, RMS/peak dBFS, 5 states)
and InputLevelIndicator's classifyInputLevel (Compose strip, linear
peak fractions 25/75/99%, 4 states) — so the two UIs could disagree on
the same audio and the healthy-gain window had to be tuned twice.

AudioInputLevel is now the single classifier: new fromPeakRms(peak,
rms) classifies an accumulated window pair through the exact
thresholds compute() uses (compute delegates to it, so they cannot
drift). The Compose strip's threshold constants and 4-state enum are
deleted; classifyInputLevel/inputLevelColor/inputLevelStatusText are
thin mappers over the shared 5-state Status (SILENT: muted color,
reuses the too-low wording so no new strings in 16 locales).
InputAudioLevel windowing and AudioLevelDisplay are unchanged.

Closes #405

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Implements issue #408 Phase 1: a latching TUNE control that keys the
rig through the existing PTT path and plays a steady, phase-continuous
sinusoid at the current TX offset until the operator stops it or the
code-enforced max-on timeout (default 10s, 2-60s configurable) fires.

- TuneToneGenerator: chunked, phase-continuous, raised-cosine ramped
  tone; amplitude re-read per chunk (live level changes land mid-tune).
- TuneController: injected-clock safety state machine — hard timeout,
  atomic single-flight start, first-stop-reason-wins.
- Keying: OnDoTransmitted.onTuneKeyDown/Up defaults; MainViewModel's
  CAT/RTS/DTR PTT+SCO logic factored into beginKeying/endKeying shared
  with the FT8 path (VOX keys on the tone itself).
- Playback: the playFT8Signal chunked MODE_STREAM AudioTrack pattern;
  PTT release + track release in a finally on every exit path; start/
  stop logged to debug.log with offset, level, duration, stop reason.
- Guards: armed/live FT8 TX, SWR lockout, and the WSPR blacklist block
  tune; unsupported routes (truSDX CAT-audio, network rigs, USB-direct)
  refuse with a toast (Phase 2). TX start preempts a running tune;
  onStop() force-stops it when the app is backgrounded.
- UI: TUNE chip in TxStrip with red active state + live countdown, the
  global TransmitGlow during tune, timeout toast; strings in 16 locales.
- Settings: tune timeout, independent tune level, and per-band tune
  levels (own store, gated on the existing per-band toggle, reusing the
  PerBandOutputLevel helpers).

Closes #408

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Replace the untimed join() with a completion latch and a 5s bounded
await, so a wedged worker fails the assertion instead of hanging the
whole suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
- DecodeCycleState javadoc no longer claims afterPass always builds
  fresh lists; state the real invariant (published lists are never
  mutated — passed through or replaced by a new merged list).
- The two concurrency tests now use a two-latch start (all workers
  signal ready before release) so every run actually overlaps, and
  bounded joins so a wedged worker fails instead of hanging the suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
channelsForMaxPacketSize now routes through the existing
isPlausibleRate guard: a positive-but-garbage sample rate (malformed
descriptor data) made the per-channel divisor tiny and rounded any
packet size up to stereo; outside the 8-768 kHz UAC range we now fall
back to mono, matching the documented degenerate behavior. Also fixed
the test javadoc arithmetic (the old guess truncated 180/96, not
180/192).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
- RecordingCallback.onGetDone is synchronized: the race tests call it
  from either thread, and a regression producing two near-simultaneous
  deliveries must fail the hasSize(1) assertion cleanly rather than
  corrupt an unsynchronized ArrayList.
- The final-chunk race test now asserts the last chunk is
  all-or-nothing (every sample real or every sample zero) instead of
  spot-checking the last element, so a torn half-copied chunk cannot
  slip through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Math.max(0f, NaN) is NaN, so a corrupted snapshot flowed NaN through
toDbfs/classify — where every comparison is false — and misreported
GOOD with NaN dB values. fromPeakRms now treats NaN/Infinity as digital
silence per field, with tests for all three poison values and a mixed
one-field case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
A config row with a NULL Value makes cursor.getString return null;
the three tune keys now guard it (null keeps the compiled-in default),
matching the perBand* keys next to them, and the boolean compare is
the null-safe 1.equals(result) form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Make QsoSyncGate check-and-mark atomic to close the TOCTOU race
Serialize afterDecode shared state behind a synchronized DecodeCycleState
Derive USB channel count from the actual sample rate, not a 48 kHz constant
Refuse to arm the free-text one-shot when the callsign can't transmit
…isibility

Order HamRecorder watchdog reads against capture writes with a buffer lock
Add host tests for the EU_VHF and CONTESTING native decoders
Reid-n0rc and others added 22 commits July 6, 2026 10:21
Upgrade the Compose toolchain to Compose 1.8, which requires the Compose
compiler that ships inside Kotlin 2.x, so this is a Kotlin 2.0 / K2
migration.

Root build.gradle:
- Kotlin 1.9.22 -> 2.0.21 (org.jetbrains.kotlin.android)
- Add org.jetbrains.kotlin.plugin.compose 2.0.21 (the K2 Compose compiler,
  version pinned to the Kotlin version)
- detekt 1.23.6 -> 1.23.8 and ktlint plugin 12.1.1 -> 12.1.2, the first
  releases that run against a Kotlin 2.0 build without the embedded-compiler
  mismatch that fails the build

app/build.gradle:
- Apply org.jetbrains.kotlin.plugin.compose; remove
  composeOptions.kotlinCompilerExtensionVersion '1.5.8' (standalone compiler,
  topped out at Compose 1.7)
- Compose BOM 2024.02.00 (Compose 1.6) -> 2025.04.01 (Compose 1.8.0)
- activity-compose 1.8.2 -> 1.10.1; navigation-compose 2.7.7 -> 2.8.9;
  lifecycle-{viewmodel,runtime}-compose 2.7.0 -> 2.8.7 (BOM-external, aligned
  to Compose 1.8 / Kotlin 2.0)

No K2 source fallout: the main + test Java/Kotlin interop surface compiles
clean under K2 (only pre-existing deprecation warnings). Robolectric 4.14.1
needs no bump.

Static analysis: detekt 1.23.8 detects a few SwallowedException /
TooGenericExceptionCaught cases in pre-existing, unchanged files that 1.23.6
missed — recorded in the detekt baseline so only new code is policed. ktlint's
pre-existing formatting violations are captured in a new ktlint baseline
(config/ktlint/baseline.xml), matching the detekt-baseline pattern already used
in this repo.

Verified on macOS/JDK 17: compileDebug{Kotlin,JavaWithJavac},
testDebugUnitTest, detekt, ktlintCheck, and assembleDebug all green.

The declarative autofill migration (issue #440 step 8, which unblocks #439) is
left as a follow-up: the experimental AutofillNode helper it replaces is not
present on this branch, so there is nothing to migrate here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The QSO status showed "Calling CQ" whenever TX was activated with no
target, but that state also covers pure hunt mode (autoFollowCQ) where the
app is silently listening for someone else's CQ rather than transmitting.
Only the Hunt+CQ hybrid (huntCallsCQ) actually transmits CQ.

Add an isHunting state (isCallingCq && autoFollowCQ && !huntCallsCQ) and a
new "Listening for CQ" string, rendered in the phone StationHeader and the
Android Auto status pane before the calling-CQ branch. The car mapper gains
huntEnabled/huntCallsCq params (defaulting false, so existing behavior and
tests are unchanged) wired from GeneralVariables at the call site.

Adds the qsopanel_hunting key to all 16 locales (English fallback value)
and unit tests covering the hunt-listening and Hunt+CQ-hybrid headlines.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
…ngth validation (#435)

Builds on the existing CQ options sheet (free-text field + FT8 charset/13-char
validation) with the remaining deltas from #435:

- Persist the custom/directed CQ across sessions via the SQLite config table
  (new GeneralVariables.cqFreeText, DatabaseOpr load branch, writeConfig on
  change), mirroring the Field Day persist pattern.
- Add a saved-CQ quick-select chip (shown only when a custom CQ is stored) that
  re-arms it, plus an X affordance to clear it.
- Validate the assembled "CQ <text> <call>" string length, not just the raw
  field: buildDirectedCq/directedCqFits helpers surface an inline error when the
  full on-air message would exceed 13 chars (the raw counter still shows
  otherwise). Config loads async, so the saved value is re-synced in the
  configLoaded effect.

Tests: 7 new pure-logic tests in CqOptionsLogicTest cover buildDirectedCq
(assembly, space collapsing, empty-text) and directedCqFits (13-char boundary,
over-length, invalid charset). Full suite green (1552 tests, 0 failures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Add the Inter variable font as the proportional UI typeface and enable
its `zero` OpenType feature app-wide so `0` renders slashed and is
unambiguous vs `O` for callsigns, grid squares, and frequencies.

- Type.kt: add InterFamily (variable TTF, per-weight wght variations),
  switch all 15 Material3 typography slots from Geist to Inter, and set
  fontFeatureSettings = "zero" on them. Add pure helper slashedZero() +
  DATA_FONT_FEATURES for testability.
- FT8AFTheme.kt: provide LocalTextStyle carrying the slashed-zero feature
  at the theme root, so the ~130 inline GeistMono data Text sites inherit
  it without editing them.
- Keep GeistMonoFamily for tabular data columns so numeric/column
  alignment is preserved (Inter is proportional).
- Bundle Inter (OFL-1.1); license at app/licenses/OFL_Inter.txt and a
  one-line credit in the About dialog.
- Add TypographyFeaturesTest (pure JVM).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
… work (#439)

Password managers (1Password, Bitwarden, Google Password Manager, …) work
through the Android Autofill framework, which classifies a field by the
autofill hints it advertises. The QRZ, ICOM, and POTA login dialogs use plain
Compose OutlinedTextFields with no autofill metadata, so the OS sees anonymous
text boxes and never offers to fill or save credentials.

Add a reusable `Modifier.autofill(role, onFill)` (ui/components/AutofillModifier.kt)
that registers an AutofillNode in the local autofill tree and requests/cancels
autofill on focus — the experimental-node approach for the current Compose 1.6
toolchain. The field→AutofillType decision is extracted into the pure, testable
`credentialAutofillTypes(role)`.

Applied to:
- QRZ Profile Lookup: username → Username, password → Password
- ICOM/Xiegu login: username → Username, password → Password
- POTA login: email → EmailAddress, password → Password

QRZ usernames are callsigns, so Username (not EmailAddress) is used. API-key
fields (QRZ Logbook, Cloudlog) are intentionally left as-is — the framework has
no API-key type (issue #439 open question, option a).

The experimental AutofillType is kept out of the public modifier signature (it
takes CredentialFieldRole) so its error-level opt-in doesn't spread to every
call site. Follow-up once the Compose BOM reaches ~1.8 (#440): replace the node
API with declarative `Modifier.semantics { contentType = … }`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he (#437)

Increment 1 of ~5 for issue #437 (auto per-location Wavelog station
profiles). This lands the dark, pure foundation only — it changes NO live
upload behavior. The feature is gated by a default-false flag and nothing
in the live upload path consults any of the new code yet.

- LocationSignature: pure value type computing a stable, normalized,
  canonical dedup-key string from a configurable subset of location fields
  (grid/state/DXCC/county/city/CQ/ITU zone) plus an optional POTA ref.
  No Android deps.
- ThirdPartyService.createCloudlogStation + pure helpers
  buildCreateStationRequestJson / parseCreateStationResponse. Wire shape
  is provisional (to be confirmed against a live Wavelog >= 2.1.2 server)
  and not called from any live path.
- LocationStationResolver: pure find-or-create decision over the signature
  cache and the existing station-profile list (validates stale cached ids).
- Persistent signature -> station_profile_id cache: new
  location_station_cache SQLite table (DB v18 -> v19) with thin DAO
  (put/get/getAll/clear) plus a small LocationStationCacheEntry model.
- GeneralVariables.perLocationStationEnabled flag, default false.

Tests (all new): signature computation/subsets/POTA/normalization/equality,
resolver branches, create_station JSON build+parse, cache DAO round-trip,
and the flag default. Full suite green (1583 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Match the codebase's CQ acronym casing (GeneralVariables.autoFollowCQ /
huntCallsCQ) for the car status parameter and its call sites/tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
…ttribution, de-stale comment

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
…e cqFreeText persistence

- Add contentDescription/onClickLabel ("Remove saved CQ") to the saved-CQ ✕ control
- Move cq_directed_too_long, cq_saved_label, and new cq_saved_remove to
  strings_compose.xml; reword cq_directed_too_long to cover both the length and
  the limited-character-set failure modes of directedCqFits
- Stop writing cqFreeText to SQLite on every keystroke: keep in-memory updates on
  change and persist once on sheet dismiss / Call CQ via new pure helper
  shouldPersistFreeText (covered by unit tests)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
…ast create_station body, redact API key in logs

- LocationSignature.ALL_FIELDS is now Collections.unmodifiableSet so callers
  can't mutate the shared constant.
- Compute the canonical signature once in the constructor (the type is
  immutable) and cache it; signature()/equals/hashCode/toString reuse it.
- buildCreateStationRequestJson returns null (not "{}") on failure, and
  createCloudlogStation aborts without POSTing when the body can't be built.
- Add redactUrlApiKey() and use it in sendPostRequest's URL log lines so the
  Cloudlog/Wavelog API-key path segment never reaches logcat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Fix #436: hunting status shows "Listening for CQ" instead of "Calling CQ"
Add free-text directed CQ: persist, quick-select, remove, combined-length validation (#435)
Adopt Inter UI font with slashed zeros (zero feature) (#438)
Address PR #446 review:

- AutofillModifier: remember() the AutofillNode instead of recreating it
  every recomposition, register/unregister it via DisposableEffect so
  orphaned nodes no longer accumulate in LocalAutofillTree, and capture
  onFill through rememberUpdatedState so the stable node runs the latest
  lambda.
- QRZ dialog: place the caret at end of text after autofill by setting
  TextFieldValue's selection to TextRange(length), matching normal typing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Add autofill hints to Compose login fields so password managers work (#439)
dev merged the #392 compound-callsign hash-recovery work separately (and
iterated it further: the static Ft8Message.hashList is now reset via an
@before clearHashList() so the resolution tests are order-independent, and
test_call_hash.c is wired into the POSIX run_host_tests.sh that CI runs).
Resolve every #392 conflict by taking dev's authoritative version and drop
the now-orphaned docs/wsjtx-interop-compound-calls-392.md, so this PR is
purely the Kotlin 2.0 / Compose 1.8 (K2) toolchain migration for #440.

Also correct the ft8af/build.gradle static-analysis comment: it claimed
ktlint 1.3.x is required to parse Kotlin 2.0, but only the Gradle plugin was
bumped (12.1.2); the ktlint engine stays pinned to 1.2.1 and parses the K2
sources cleanly.

Addresses Copilot review findings on #441. Verified: testDebugUnitTest green
under K2 on the merged tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
build: migrate to Kotlin 2.0 / Compose 1.8 (K2) — closes #440
…ature

Wavelog per-location station: signature + create_station client + cache (increment 1 of ~5) (#437)
buildActivationAdif (the shared core behind both the share-sheet export
and the authenticated in-app POTA upload) matched QSOs on park ref alone,
with no time bound. Exporting one activation of a park therefore pulled in
every QSO ever logged at that park, across all past sessions and dates.

Scope the query to the activation's [startedAtMs, endedAtMs] window, the
same filter PotaActivationDao.getActivationQsos already uses for the in-app
contacts list. App-logged QSOs store qso_date as yyyyMMdd and time_on as
HHMMSS, so `qso_date || time_on` compares directly against a 14-char
yyyyMMddHHmmss GMT stamp; an open (still-active) activation uses a
far-future upper bound.

Tests: updated the existing 7 BuildActivationAdifTest cases to store
realistic HHMMSS times inside the window, and added two regression tests
(out-of-window QSOs excluded; open activation still exports post-start QSOs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
Copilot review flagged that the `qso_date || time_on` window comparison
assumed a fixed-width 6-digit HHMMSS time_on. time_on is stored
variable-width: app-logged rows are HHMMSS, but imported or hand-edited
ADIF rows may be HHMM or drop a leading zero (e.g. "815" for 08:15). A
naive concatenation mis-sorts those against the 14-char yyyyMMddHHmmss
bounds, so an in-window QSO could be wrongly excluded.

Centralize the window SQL + stamp formatting into a shared PotaQsoWindow
object whose ROW_STAMP normalizes time_on to 6 digits (pad even-width,
prepend a zero to odd-width, same rule DatabaseOpr uses for its dedupe
ORDER BY). Route both PotaAdifExporter.buildActivationAdif and
PotaActivationDao.getActivationQsos through it so the export/upload and
the in-app contacts list can't drift apart again.

Test: variableWidthTimeOn_isNormalizedForTheWindowCompare covers "815"
(dropped leading zero), "0830" (HHMM), "084500" (HHMMSS) inside the
window and "930" (09:30) correctly excluded past the 09:00 end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA
…ndow

Fix POTA export lumping all park QSOs across activations
Replace the hardcoded "FT8US" typo on the Settings > About screen with
stringResource(R.string.app_name) so the label stays in sync with the
canonical app name.

Adds AboutAppNameTest (Robolectric) asserting R.string.app_name resolves
to "FT8AF", guarding against future resource renames or wrong ID references.

Re-lands the change from #448 on a clean branch off dev (that PR's branch
was cut from main and carried main's entire history).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.11007% with 596 lines in your changes missing coverage. Please review.
✅ Project coverage is 21.45%. Comparing base (019d908) to head (635ce27).
⚠️ Report is 10 commits behind head on staging.

Files with missing lines Patch % Lines
...o/ks3ckc/ft8af/ui/settings/TransmissionSettings.kt 0.00% 112 Missing ⚠️
...n/kotlin/radio/ks3ckc/ft8af/car/QsoStatusScreen.kt 0.00% 94 Missing ⚠️
...radio/ks3ckc/ft8af/ui/settings/AdvancedSettings.kt 18.68% 74 Missing ⚠️
...radio/ks3ckc/ft8af/ui/components/CqOptionsSheet.kt 4.83% 59 Missing ⚠️
...app/src/main/kotlin/radio/ks3ckc/ft8af/FT8AFApp.kt 0.00% 42 Missing ⚠️
desktop/src-tauri/src/engine.rs 0.00% 35 Missing ⚠️
...tlin/radio/ks3ckc/ft8af/car/RecentDecodesScreen.kt 0.00% 30 Missing ⚠️
...kotlin/radio/ks3ckc/ft8af/ui/components/TxStrip.kt 5.00% 19 Missing ⚠️
...pp/src/main/kotlin/radio/ks3ckc/ft8af/TuneLevel.kt 18.18% 17 Missing and 1 partial ⚠️
...dio/ks3ckc/ft8af/ui/components/AutofillModifier.kt 29.16% 17 Missing ⚠️
... and 19 more
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff              @@
##             staging     #452      +/-   ##
=============================================
+ Coverage      20.79%   21.45%   +0.66%     
- Complexity       133      142       +9     
=============================================
  Files            148      156       +8     
  Lines          19291    20310    +1019     
  Branches        2873     2990     +117     
=============================================
+ Hits            4011     4357     +346     
- Misses         15114    15785     +671     
- Partials         166      168       +2     
Flag Coverage Δ
android 12.48% <23.01%> (+0.31%) ⬆️
desktop 39.86% <82.11%> (+5.01%) ⬆️
ios 96.38% <ø> (ø)
native 9.93% <ø> (ø)

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

Files with missing lines Coverage Δ
...in/kotlin/radio/ks3ckc/ft8af/pota/PotaQsoWindow.kt 100.00% <100.00%> (ø)
...main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt 15.25% <50.00%> (+0.25%) ⬆️
...p/src/main/kotlin/radio/ks3ckc/ft8af/theme/Type.kt 99.12% <97.95%> (-0.88%) ⬇️
...lin/radio/ks3ckc/ft8af/ui/pota/PotaAdifExporter.kt 66.03% <66.66%> (-0.31%) ⬇️
...otlin/radio/ks3ckc/ft8af/pota/PotaActivationDao.kt 0.00% <0.00%> (ø)
...radio/ks3ckc/ft8af/ui/components/ActiveQsoPanel.kt 9.39% <0.00%> (+0.11%) ⬆️
...ks3ckc/ft8af/ui/components/FrequencyPickerSheet.kt 1.02% <0.00%> (+1.02%) ⬆️
.../ks3ckc/ft8af/ui/components/InputLevelIndicator.kt 26.56% <88.23%> (+6.56%) ⬆️
desktop/src-tauri/src/main.rs 0.00% <0.00%> (ø)
...main/kotlin/radio/ks3ckc/ft8af/car/CarQsoStatus.kt 95.23% <95.23%> (ø)
... and 20 more

... and 18 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.

Pull request overview

Promotes the dev branch to staging to trigger a signed AAB release to the Google Play internal track, primarily to make the Android Auto read-only QSO status feature testable on real head units (where sideloaded builds are blocked by Android Auto validation).

Changes:

  • Upgrades the Android build toolchain to Kotlin 2.x and updates Compose + static-analysis plugin versions accordingly.
  • Adds Android Auto (car-app-library) templated UI entry points plus manifest wiring and Robolectric verification.
  • Includes a broad set of accumulated dev features/fixes with substantial new unit test coverage (FFT display knobs, tune/ATU support, decode-thread safety, POTA activation windowing, etc.).

Reviewed changes

Copilot reviewed 152 out of 155 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
ft8af/build.gradle Bumps Kotlin/Compose-related Gradle plugins and static-analysis plugin versions.
ft8af/app/build.gradle Updates Compose BOM + related deps; adds car-app-library dependency; removes old Compose compiler extension config.
ft8af/app/config/detekt/baseline.xml Updates detekt baseline formatting and adds newly suppressed findings.
ft8af/app/licenses/OFL_Inter.txt Adds Inter font license text.
ft8af/app/src/main/AndroidManifest.xml Declares Android Auto metadata + exported CarAppService wiring.
ft8af/app/src/main/cpp/usb_audio_capture.cpp Adds stop-reason diagnostics for USB capture session shutdown.
ft8af/app/src/main/cpp/ft8af_glue/run_host_tests.sh Extends native host tests (FFT window/avg + contest decode).
ft8af/app/src/main/cpp/ft8af_glue/run_host_tests.ps1 Windows equivalent updates for added native host tests.
ft8af/app/src/main/cpp/ft8af_glue/ft8_decode_jni.cpp Broadens post-decode callsign-hash recovery across message types.
ft8af/app/src/main/cpp/ft8af_glue/ft2_decode_jni.cpp Mirrors FT8 JNI hash-recovery broadening for FT2.
ft8af/app/src/main/cpp/ft8af_glue/fft_display.h Adds window/averaging APIs + sanitizers for display FFT pipeline.
ft8af/app/src/main/cpp/ft8af_glue/fft_display.c Implements window functions + magnitude EMA for display FFT pipeline.
ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java Adds new feature flags/settings, including tune + FFT display knobs.
ft8af/app/src/main/java/com/k1af/ft8af/DecodeCycleState.java Introduces synchronized holder for per-cycle decode state shared across threads.
ft8af/app/src/main/java/com/k1af/ft8af/PendingSequencerDecodes.java Adds thread-safe stash/drain for late/deep decodes during TX.
ft8af/app/src/main/java/com/k1af/ft8af/concurrent/SafeExecutor.java Adds helper to avoid decode-thread crashes on executor shutdown races.
ft8af/app/src/main/java/com/k1af/ft8af/html/LogHttpServer.java Switches to safe decode-cycle getters for message list and decode count.
ft8af/app/src/main/java/com/k1af/ft8af/log/LocationStationResolver.java Adds pure resolver logic for per-location Wavelog station selection (dark feature).
ft8af/app/src/main/java/com/k1af/ft8af/log/LocationStationCacheEntry.java Adds value type for persisted per-location station cache rows.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/BaseRig.java Adds ATU-tune capability API on base rig abstraction.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/IcomRigConstant.java Adds CI-V ATU start-tune frame builder.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/IcomRig.java Implements ATU tune support for Icom rigs.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/Yaesu3RigConstant.java Adds Yaesu ATU-tune command constant and accessor.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/KenwoodTK90RigConstant.java Adds Kenwood ATU-tune command constant and accessor.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/YaesuDX10Rig.java Enables ATU tune for Yaesu DX10 rig implementation.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/Yaesu39Rig.java Enables ATU tune for Yaesu 3/9 protocol rig implementation.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/XieGuRig.java Enables ATU tune for Xiegu rig implementation.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/XieGu6100Rig.java Enables ATU tune for Xiegu 6100 rig implementation.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/KenwoodTS590Rig.java Enables ATU tune for Kenwood TS-590 implementation.
ft8af/app/src/main/java/com/k1af/ft8af/rigs/KenwoodTS2000Rig.java Enables ATU tune for Kenwood TS-2000 implementation.
ft8af/app/src/main/java/com/k1af/ft8af/service/RxServiceController.java Adds EXIT action + intent-action mapping to service command enum.
ft8af/app/src/main/java/com/k1af/ft8af/service/RxForegroundService.java Adds notification Exit action and activity-provided exit handler hook.
ft8af/app/src/main/java/com/k1af/ft8af/spectrum/AudioInputLevel.java Adds fromPeakRms entry point and sanitization for non-finite inputs.
ft8af/app/src/main/java/com/k1af/ft8af/ui/BinAggregation.java Extracts spectrum bin aggregation logic into testable utility.
ft8af/app/src/main/java/com/k1af/ft8af/ui/ColumnarView.java Switches bin combining to use BinAggregation + new developer setting.
ft8af/app/src/main/java/com/k1af/ft8af/ui/SpectrumFragment.java Uses safe decode-cycle accessors and adds native FFT display params setter.
ft8af/app/src/main/java/com/k1af/ft8af/ui/SpectrumView.java Uses safe decode-cycle accessors for current messages.
ft8af/app/src/main/java/com/k1af/ft8af/ui/CallingListFragment.java Uses safe decode-cycle accessor for decode count.
ft8af/app/src/main/java/com/k1af/ft8af/grid_tracker/GridTrackerMainActivity.java Uses safe decode-cycle accessors for current messages and decode count.
ft8af/app/src/main/java/com/k1af/ft8af/wave/UsbCaptureRetryPolicy.java Adds pure policy for USB capture failure classification + backoff.
ft8af/app/src/main/java/com/k1af/ft8af/wave/HamRecorder.java Adds locking around voice buffer handoff to avoid watchdog/capture races.
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/OnDoTransmitted.java Adds default tune key up/down callbacks for PTT routing.
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/TuneMethod.java Adds Tune method selector (ATU vs tone) and decision logic.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt Registers RX notification exit handler; stops tune on background; clears handler on destroy.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/TuneLevel.kt Adds tune audio level resolution + per-band persistence helpers.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/car/FT8AFCarAppService.kt Adds Android Auto CarAppService entry point + host validation policy.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/car/RecentDecodesScreen.kt Adds Android Auto screen for recent decodes list.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/pota/PotaQsoWindow.kt Centralizes activation time-window SQL + stamping logic.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/pota/PotaActivationDao.kt Reuses shared activation time-window logic for QSO fetch.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaAdifExporter.kt Scopes ADIF export to activation time window using shared logic.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaScreen.kt Adds autofill hints to POTA login fields.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/AboutSettings.kt Fixes app-name label and adds font attribution link.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/ConnectionDialogs.kt Adds autofill hints to ICOM login dialog fields.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/LoggingSettings.kt Adds autofill hints to QRZ credential dialog fields.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/AutofillModifier.kt Adds Compose autofill modifier + role/type mapping for credential fields.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/ActiveQsoPanel.kt Adds “hunting/listening” status headline logic.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/FrequencyPickerSheet.kt Clears CQ-slot history on real band hops.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/InputLevelIndicator.kt Unifies RX input classification with AudioInputLevel peak/RMS thresholds.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/TxStrip.kt Adds TUNE chip UI and testable label helper.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/waterfall/WaterfallScreen.kt Pushes FFT display params to native bridge each frame.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/sync/QsoAutoSync.kt Fixes sync gate to be atomic (single lock) via tryStart.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/theme/FT8AFTheme.kt Applies slashed-zero OpenType feature via ambient LocalTextStyle.
ft8af/app/src/main/res/xml/automotive_app_desc.xml Adds automotive app descriptor resource for Android Auto discovery.
ft8af/app/src/main/res/values-zh-rTW/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-zh-rCN/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-uk/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-tr/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-ru/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-pl/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-nl/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-ko/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-ja/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-it/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-in/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-fr/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-es/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-cs/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
ft8af/app/src/main/res/values-ar/strings_compose.xml Adds new strings for multiple features incl. hunting/tune/FFT knobs.
desktop/src/ipc.ts Exposes waterfall FFT config types + IPC setter for desktop app.
desktop/src-tauri/src/main.rs Adds Tauri command to set/persist waterfall config via engine.
desktop/src-tauri/src/lib.rs Exposes new wf module.
desktop/README.md Documents desktop waterfall display pipeline + developer knobs.
docs/clear-cq-slot-selection.md Adds documentation for clear-CQ-slot selection algorithm and integration.
.gitignore Ignores per-contributor Claude Code local notes file.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/settings/FftDisplaySettingsTest.kt Tests FFT window/averaging/bin-aggregation label mappings and defaults.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/settings/CredentialAutofillTypesTest.kt Tests credential field role → autofill hint mapping.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/settings/AboutAppNameTest.kt Robolectric guard for app-name string used in About screen.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/components/TuneChipLabelTest.kt Tests tune chip label countdown formatting/clamping.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/components/CqOptionsLogicTest.kt Adds tests for directed CQ assembly/fits and free-text persistence.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/theme/TypographyFeaturesTest.kt Tests slashed-zero feature application and preservation of TextStyle fields.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/GeneralVariablesFftSettingsTest.kt Robolectric tests for FFT knob clamping setters in GeneralVariables.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/car/CarAppManifestWiringTest.kt Robolectric manifest wiring tests for Android Auto service discovery.
ft8af/app/src/test/java/com/k1af/ft8af/wave/UsbCaptureRetryPolicyTest.java Tests USB capture failure detection and backoff policy.
ft8af/app/src/test/java/com/k1af/ft8af/wave/MicRecorderCaptureHandoffTest.java Tests capture-thread generation and join predicates to prevent crashes/deadlocks.
ft8af/app/src/test/java/com/k1af/ft8af/UiSeedSnapshotTest.java Tests MainViewModel UI list seeding avoids live list reference leakage.
ft8af/app/src/test/java/com/k1af/ft8af/ui/BinAggregationTest.java Tests bin aggregation modes match legacy behavior and clamp correctly.
ft8af/app/src/test/java/com/k1af/ft8af/spectrum/AudioInputLevelTest.java Tests new fromPeakRms path + sanitization matches compute().
ft8af/app/src/test/java/com/k1af/ft8af/service/RxServiceControllerTest.java Tests new action→command mapping behavior.
ft8af/app/src/test/java/com/k1af/ft8af/rigs/AtuTuneCommandTest.java Byte-exact tests for rig ATU-start command frames/strings.
ft8af/app/src/test/java/com/k1af/ft8af/PendingSequencerDecodesTest.java Tests pending decode stash/drain ordering and age eviction.
ft8af/app/src/test/java/com/k1af/ft8af/GeneralVariablesPerLocationFlagTest.java Guards dark-launch flag default for per-location station feature.
ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/TxDropWarningTest.java Tests warning gating logic for dropped TX audio.
ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/TuneMethodTest.java Tests tune method decision and clamping behavior.
ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FreeTextOneShotArmingTest.java Tests free-text one-shot arming/disarming behavior across failure paths.
ft8af/app/src/test/java/com/k1af/ft8af/database/DatabaseOprParseConfigIntTest.java Tests safe parsing of config ints from potentially invalid DB strings.
ft8af/app/src/test/java/com/k1af/ft8af/database/DatabaseOprLocationStationCacheTest.java Tests DAO round-trips for per-location station cache.
ft8af/app/src/test/java/com/k1af/ft8af/concurrent/SafeExecutorTest.java Tests SafeExecutor behavior on shutdown/null/rejected execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ft8af/app/build.gradle Outdated
Comment thread ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ComposeMainActivity.kt
patrickrb and others added 3 commits July 6, 2026 19:40
* Fix CAT chip falsely turning red after FT8 transmit

The liveness watchdog suppresses probing during TX, so lastRigResponseMs
freezes for the whole over. An FT8 transmit is 12.64s — longer than the
8s stale timeout — so the first tick after TX ends judged the pre-TX
timestamp as stale and flipped the chip to ERROR (sticky red) even though
the rig was fully responsive. FT4 (~4.48s) stayed under the timeout, so
this only manifested on FT8.

Re-arm lastRigResponseMs on the TX->RX edge so the quiet window restarts
from the end of transmit, giving the probe sent that same tick time to
reply before any staleness judgement. Edge detection lives in a pure
CatLiveness.shouldRearmAfterTx predicate with unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014B1egpdNWafvAksJCn1tMA

* Sample wall clock once per liveness tick

Address review feedback: catLivenessTick() read System.currentTimeMillis()
twice (re-arm and staleness check). Capture a single nowMs at the top of the
tick and reuse it for both, so a clock change mid-tick can't skew the
comparison and the tick is easier to reason about.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@patrickrb patrickrb merged commit 4aca0dc into staging Jul 7, 2026
38 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.

3 participants