Conversation
… <...> 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
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>
There was a problem hiding this comment.
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.
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Promotes
dev→staging, which publishes to the Play internal track (perandroid.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'sCAR.VALIDATORon 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
devsince the last staging promotion — 87 commits — not just the Android Auto work. Notable inclusions:car/package,FT8AFCarAppService, status + recent-decodes screens)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.