Skip to content

Promote dev to staging#323

Merged
patrickrb merged 8 commits into
stagingfrom
dev
Jun 22, 2026
Merged

Promote dev to staging#323
patrickrb merged 8 commits into
stagingfrom
dev

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Promote latest dev to staging (cuts an android-dev.<run#> prerelease + Play internal upload).

Included since the last promotion (#321)

🤖 Generated with Claude Code

patrickrb and others added 6 commits June 22, 2026 12:58
The park picker bottom sheet was emitted as a trailing sibling of the
fillMaxSize Activate content inside PotaScreen's Column. In a Column the
greedy fillMaxSize child consumes all the height, so the sheet (itself a
fillMaxSize scrim + sheet) laid out into zero leftover height: visible
flipped true on tap but nothing appeared on screen.

Host the Activate content and ParkPickerSheet together in a
Box(fillMaxSize) so the sheet overlays the content, mirroring how
FrequencyPickerSheet is hosted as a sibling overlay in FT8AFApp.

Add BottomSheetOverlayHostTest, a Compose/Robolectric regression test
pinning the geometry: an FT8AFBottomSheet placed after a fillMaxSize
sibling in a Column is not displayed, while the same wrapped in a Box is.

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

* Fix POTA Nearby parks showing far-away regions (bad upstream centers)

POTA's /locations API ships wrong center coordinates for a chunk of
foreign regions: ZA-FS, RU-VL, RO-OT (and others) all report centers
sitting in Kansas. nearbyParks picked only the 3 nearest region centers
and trusted them, so a US user got those mislocated regions and their
parks rendered their true ~5000-mile distance at the top of Nearby.

Broaden the candidate net from 3 to 12 nearest region codes so the
user's real region(s) are always included despite the bad rows, fetch
those park lists concurrently, then rank purely by each park's own
(correct) coordinates. Add a NEARBY_MAX_DISTANCE_KM cap (1000 km) so a
mislocated region's genuinely-distant parks can never surface as nearby.

Tests: a regression case using the real ZA-FS/RU-VL/RO-OT-in-Kansas data
showing the wider candidate count rescues US-KS/US-MO, plus max-distance
cap coverage on sortParksByDistance.

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

* Address review: pin nearby-candidate test to production value (12)

The wide candidate-count assertion used 5; use 12 to mirror the
production NEARBY_LOCATION_CANDIDATES constant so the test documents the
intended value. With 5 test rows the result is unchanged (all returned).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Found while auditing the on-device debug.log: nothing functionally broken,
but two log defects made a real failure mode hard to spot.

1. Dropped TX is invisible. When the libusb native write fails (seen ~10x in
   a session as rc=5 / rc=-1), the UsbRequest fallback also fails for the same
   stale-handle reason and writeAudio returns false in ~2ms. The rig is
   already keyed, so it transmits dead air for the whole 15s slot. The log
   only said "writeAudio returned FAILED", which reads as benign. The QSO
   sequencer is RX-driven and self-corrects next cycle, so this isn't fatal --
   but the operator can't tell from the rig, so the log now spells out
   "TX DROPPED, no audio sent this cycle (rig keyed but silent)" and names the
   libusb error code instead of a bare rc.

2. sinceLastReinit printed the epoch. lastReinitMs starts at 0, so a capture
   session that stopped before any reinit logged now-0, i.e. the wall-clock
   epoch (~1.78e12 ms, "56,000 years"). Now shows "n/a (no reinit yet)".

No behavior change beyond logging. Decision logic extracted into pure,
package-visible helpers (formatSinceReinit, describeLibusbWriteError,
buildWriteAudioResultLog) with unit tests.


Claude-Session: https://claude.ai/code/session_01CDu3dtuN89JsRqxFsq1ow9

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

* Improve RX decode sensitivity: deep decode default, level meter, FIR downsample

A user reported the app decoding far fewer signals than WSJT-X from the same
radio (TX500). Three independent sensitivity fixes:

1. Deep decode on by default. The subtract-and-redecode loop (the thing that
   pulls weak signals out from under strong ones, like WSJT-X's default depth)
   was gated behind a setting that defaulted off, so out of the box the app did
   a single decode pass. A persisted "deepMode" config still overrides; only
   installs that never touched the setting pick up the new default.

   Also replace the flat 7s deep-decode loop timeout with ModeProfile
   #deepDecodeBudgetMillis (0.75x the slot). The old constant was ~2x FT2's
   entire 3.75s slot (so it never bounded fast modes) yet left FT8 well short of
   its 15s cycle, cutting weak-signal passes WSJT-X would still run.

2. RX input-level meter. The radio's USB audio is often set too quiet (weak
   signals fall below threshold) or hot enough to clip (intermod kills decodes),
   and the app gave no feedback. Add AudioInputLevel (pure peak/RMS -> dBFS +
   coarse status) + AudioLevelDisplay (label/colour), metered off the same
   160ms waterfall buffer and shown on the spectrum screen.

3. Anti-aliasing FIR downsample. The 48k->12k USB capit path used a 4-tap box
   filter (~10 dB stopband), folding out-of-band hiss into the FT8 passband and
   raising the noise floor. Replace with a header-only windowed-sinc polyphase
   decimator (fir_decimator.h, ~80 dB rejection at the alias frequencies). Host
   test demonstrates the 9kHz alias tone drops from 0.32 (box) to 0.0001 (FIR).

Tests: ModeProfileTest (timeout scaling), AudioInputLevelTest +
AudioLevelDisplayTest (level math/mapping), test_fir_decimator.cpp wired into
run_host_tests. JVM tests + native host tests pass; debug build installs on the
Pixel 8.

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

* Address Copilot review on FIR decimator (#319)

Two review comments on the anti-aliasing downsample:

1. Guard the decimation-ratio division. nativeStart computed
   inputSampleRate / targetSampleRate directly, which divides by zero (UB)
   if the device streams a 0 target rate and silently produces the wrong
   output rate when the input isn't an exact multiple. Extracted the
   computation into a pure, tested helper decimationRatioFor() that clamps
   to >= 1 (pass-through on a bad pair), and the caller now logs the two
   degenerate cases instead of feeding the decoder mis-rated audio.

2. Fix misleading "polyphase" wording. The implementation is a straight
   FIR convolution followed by decimation, not a polyphase structure.
   Renamed the tapsPerPhase parameter to tapsPerRatio and reworded the
   comments to describe what the code actually does.

Adds host-test coverage for decimationRatioFor (exact multiple, zero/
negative target, input-below-target, non-integer multiple, equal rates).
Host FIR suite passes; assembleDebug builds all ABIs.

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

---------

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

Two review comments on the merged #319 code:

1. FT8SignalListener deep-decode budget used `timeSec` (total elapsed since the
   whole decode began), so on a slow device the initial fast + first deep pass
   could spend the budget and abort the subtract-and-redecode loop before it ran
   once. Budget the loop by its own elapsed time, measured from the loop start.
   Extracted DeepDecodeBudget.loopExhausted so the decision is unit-tested
   without loading the native decoder (DeepDecodeBudgetTest).

2. usb_audio_capture onTransferComplete heap-allocated a fresh std::vector<float>
   for the decimated output on every iso callback (~8ms). Reuse a persistent
   `outScratch` member (clear() keeps capacity), reserved once in nativeStart,
   so the hot capture path allocates nothing per transfer.

JVM tests + native host tests pass; debug build installs on the Pixel 8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t run numbers (#322)

The per-project CI split replaced build-release.yml with android.yml, a new
workflow file. GITHUB_RUN_NUMBER is per-workflow, so the counter reset from
~680 (under the old workflow, versionCode ~780 on the Play internal track) back
to a low number. With the old "+ 100" offset the staging->internal publish
produced versionCode 167, below the 780 already on the track, so Google Play
refused the rollout: "You cannot rollout this release because it does not allow
any existing users to upgrade to the newly added APKs."

Bump the offset to + 1000 so the next staging push clears the historical max
with margin. The publish step is already continue-on-error, so the run was
green and only the Play upload was skipped; this restores the upload.

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

codecov Bot commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 106 lines in your changes missing coverage. Please review.
✅ Project coverage is 11.51%. Comparing base (8f53360) to head (bc6c21e).

Files with missing lines Patch % Lines
...in/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaScreen.kt 0.00% 106 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             staging     #323   +/-   ##
==========================================
  Coverage      11.51%   11.51%           
  Complexity       113      113           
==========================================
  Files             88       88           
  Lines          12466    12466           
  Branches        2230     2230           
==========================================
  Hits            1435     1435           
  Misses         10900    10900           
  Partials         131      131           
Files with missing lines Coverage Δ
...in/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaScreen.kt 0.77% <0.00%> (ø)
🚀 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 latest dev branch to staging, producing an android-dev.<run#> prerelease and attempting a Play internal upload, while pulling in the most recent functional changes (decoder sensitivity, POTA picker/nearby logic, USB logging, and USB capture DSP).

Changes:

  • Fix Play internal-track rollout rejection by increasing the CI-derived versionCode offset (GITHUB_RUN_NUMBER + 1000).
  • Improve decode sensitivity/observability: deep-decode defaults + slot-scaled loop budget, RX input-level meter, and clearer USB TX/capture logging.
  • Fix POTA park picker UX and nearby-park correctness/perf (overlay hosting, broader candidate set, concurrent fetch, distance cap).

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ft8af/app/build.gradle Bumps CI versionCode offset to avoid Play “no upgrade path” rejection after workflow run-number reset.
ft8af/app/src/main/java/com/k1af/ft8af/FT8Common.java Removes the flat deep-decode timeout constant in favor of slot-scaled budgeting.
ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java Enables deep decode by default (with config override) and documents the behavior.
ft8af/app/src/main/java/com/k1af/ft8af/ModeProfile.java Adds deepDecodeBudgetMillis() scaling deep-decode loop time with mode slot length.
ft8af/app/src/main/java/com/k1af/ft8af/ft8listener/DeepDecodeBudget.java Extracts deep-decode loop budget decision into a unit-testable helper.
ft8af/app/src/main/java/com/k1af/ft8af/ft8listener/FT8SignalListener.java Budgets subtract-and-redecode loop by its own elapsed time using the new helper/budget.
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java Makes USB-direct TX failures explicit (“TX DROPPED”) via a testable log builder.
ft8af/app/src/main/java/com/k1af/ft8af/spectrum/AudioInputLevel.java Adds pure-logic peak/RMS→dBFS metering + status classification for RX level display.
ft8af/app/src/main/java/com/k1af/ft8af/spectrum/AudioLevelDisplay.java Maps meter reading to label + ARGB color (unit-testable, no Android deps).
ft8af/app/src/main/java/com/k1af/ft8af/ui/SpectrumFragment.java Updates spectrum screen to display the RX input-level meter each buffer.
ft8af/app/src/main/java/com/k1af/ft8af/wave/MicRecorder.java Fixes “sinceLastReinit” logging (shows sentinel instead of epoch) via helper.
ft8af/app/src/main/java/com/k1af/ft8af/wave/UsbAudioDevice.java Improves libusb write failure logs by naming libusb error codes (test-covered).
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/pota/PotaParkRepository.kt Fixes nearby parks by broadening region candidates, fetching concurrently, and adding a max-distance cap.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaScreen.kt Hosts ParkPickerSheet in a Box overlay so the picker is visible when toggled.
ft8af/app/src/main/res/layout/fragment_spectrum.xml Adds a TextView to render the RX input-level meter on the spectrum screen.
ft8af/app/src/main/cpp/fir_decimator.h Introduces a header-only FIR decimator to replace box-filter downsampling.
ft8af/app/src/main/cpp/usb_audio_capture.cpp Switches USB capture downsample to FIR decimation; reuses buffers to avoid per-transfer allocations.
ft8af/app/src/main/cpp/ft8af_glue/test_fir_decimator.cpp Adds host tests for FIR decimator ratio, unity gain, and alias rejection.
ft8af/app/src/main/cpp/ft8af_glue/run_host_tests.sh Builds/runs the new FIR decimator host test.
ft8af/app/src/main/cpp/ft8af_glue/run_host_tests.ps1 Builds/runs the new FIR decimator host test (and propagates its exit code).
ft8af/app/src/test/java/com/k1af/ft8af/ModeProfileTest.java Adds unit coverage for slot-scaled deep-decode budget and constraints.
ft8af/app/src/test/java/com/k1af/ft8af/ft8listener/DeepDecodeBudgetTest.java Adds unit coverage for the deep-decode subtraction-loop budgeting helper.
ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/TxResultLogTest.java Adds unit coverage for “TX DROPPED” vs OK logging.
ft8af/app/src/test/java/com/k1af/ft8af/spectrum/AudioInputLevelTest.java Adds unit coverage for RX meter math and status thresholds.
ft8af/app/src/test/java/com/k1af/ft8af/spectrum/AudioLevelDisplayTest.java Adds unit coverage for RX meter label/color mapping.
ft8af/app/src/test/java/com/k1af/ft8af/wave/MicRecorderSinceReinitTest.java Adds unit coverage for “since last reinit” log formatting sentinel vs duration.
ft8af/app/src/test/java/com/k1af/ft8af/wave/UsbAudioWriteErrorTest.java Adds unit coverage for libusb error code labeling.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/pota/PotaParkPickerLogicTest.kt Adds regression coverage for widened region candidates + max-distance capping.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/components/BottomSheetOverlayHostTest.kt Adds Compose/Robolectric regression test for bottom-sheet overlay hosting bug.

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

Comment thread ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaScreen.kt Outdated
patrickrb and others added 2 commits June 22, 2026 16:02
The #316 fix wrapped PotaScreen's content in a Box(fillMaxSize) overlay
host but left the if/else and ParkPickerSheet blocks at their original
indentation, so they sat at the same column as `Box(...) {` instead of
inside its lambda. ktlint flags this (Copilot review on #323) and it
reads as if the blocks aren't part of the Box.

Indent the whole Box body one level. Whitespace-only; no behavior change
(BottomSheetOverlayHostTest still covers the overlay-hosting geometry).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/pota/PotaScreen.kt
@patrickrb patrickrb merged commit 6b2b059 into staging Jun 22, 2026
34 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.

2 participants