Conversation
The volume slider had no effect when TX audio was routed to the rig's USB sound card via AudioTrack: the rig overdrove at any slider position, including 0%. The AudioTrack path applied volume only via AudioTrack.setVolume(), but when a track is routed to a USB Audio Class device, Android frequently delegates level control to the device's hardware volume and the per-track setVolume() is a no-op, so the samples left at full scale regardless of the slider. Bake the gain into the float samples before write() (mirroring the USB-direct playViaUsbAudio path) and keep the track at unity to avoid double-attenuation on routes where setVolume() does work. Remove the now-redundant mid-cycle setVolume() observer (volume is fixed per cycle, matching the ALC auto-volume model). Also log the applied volume and autoVolume state on the TX-path branch line so this is diagnosable from debug.log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the inline volume-scaling loop (used by both the AudioTrack and USB-direct paths) into a pure static FT8TransmitSignal.applyVolume(), so the level logic behind the overdrive-at-0% fix is unit-testable, and make float2Short package-private static. Wrap the static System.loadLibrary in try/catch (mirroring GenerateFT8) so the class loads on the bare JVM and JaCoCo can instrument it — previously the class registered no coverage. New FT8TransmitSignalTest (plain JUnit + Truth) covers both helpers, including the key case that 0% volume yields digital silence. applyVolume and float2Short are now at 100% line/instruction coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oftware-scale Fix TX volume slider having no effect on USB-routed audio (overdrive at 0%)
When the rig monitors TX audio back to line-in, the decoder hears and decodes our own transmission. That loopback echo was leaking into the message list and the QSO conversation panel, producing a duplicate of every TX: the panel's synthesized key-up entry (text " (NNNNHz) MSG", wall-clock timestamp) plus a decoded-loopback TX row (plain getMessageText, cycle-boundary timestamp). The two never deduped because their strings differ by the frequency prefix, so both rendered ~1s apart and the stray loopback row also made the exchange look out of order. Fix: drop decodes whose sender is our own callsign in MainViewModel.afterDecode (own callsign in the "from" field can only be loopback), before they reach the message list, QSO panel, or SWL database. Remove the now-dead loopback TX branch in ActiveQsoPanel so TX rows come solely from the synthesized key-up entry. PSKReporter and the auto-sequence already ignored own-callsign messages, so behavior there is unchanged. Also add a per-cycle DECODE diagnostic (kept/ownEcho/replyToMe/slot) to help confirm the separate, unverified "missing other station responses" report from a pulled debug.log. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the two new codepaths from the loopback-echo fix into pure, testable units and cover them: - OwnTxEchoFilter (new): the decode-cycle filter that drops own-callsign loopback echoes, pulled out of MainViewModel.afterDecode. Covers dropping/counting echoes, the replyToMe diagnostic flag, empty input, unset callsign (drops nothing), compound-callsign base-call matching, and input immutability. 9 tests. - buildQsoLog (extracted in ActiveQsoPanel): the QSO conversation panel's RX/BUSY/TX classification and time ordering. Covers RX/BUSY/ignore, that own loopback never becomes a TX row, synth entries as the sole TX source, ascending utc ordering, and case-insensitive target match. 8 tests. No behavior change — afterDecode now delegates to OwnTxEchoFilter.filter and the composable delegates to buildQsoLog. All 17 tests green via testDebugUnitTest (Robolectric + Truth, matching the existing suite). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the Codecov patch-coverage gaps by moving the remaining logic into the pure helpers and covering every branch: - OwnTxEchoFilter: add decodeLogLine(slot), moving the DECODE diagnostic String.format out of the (untestable) MainViewModel.afterDecode body so the format is covered and afterDecode shrinks to a single fileLog glue line. Now 100% line + branch (2 new tests). - buildQsoLog: drop the dead `?:` fallback on getMessageText() (the Java method never returns null, so the fallback was an uncoverable partial), collapse the when into an early-return + RX/BUSY ternary, and cover the null-message-list, null-callsign, null-recipient, and checkIsMyCallsign-recipient branches. Now 100% line + branch (5 new tests). The residual uncovered lines are in MainViewModel.afterDecode and the ActiveQsoPanel @composable body, which cannot run in JVM unit tests: constructing MainViewModel chains into FT8SignalListener's `System.loadLibrary("ft8cn")`. All decision logic now lives in the two covered helpers; what remains there is LiveData / native-bound glue. 23 tests total, all green via testDebugUnitTest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hide own-TX loopback echoes from QSO panel and decode log
POTA's activation-log upload is gated by AWS Cognito (SRP) then a plain multipart POST to api.pota.app/adif. The pool/client IDs are public (they ship in pota.app's JS bundle and the open-source pota-adif-upload crate), so the app can log in with the user's pota.app account and upload directly instead of bouncing through the website. The pota module already produced correct per-park ADIF and hit the public spot endpoints; the only missing piece was the Cognito login. - PotaAuth: USER_SRP_AUTH login via aws-android-sdk-cognitoidentityprovider, refresh token persisted in a private SharedPreferences, fresh ID tokens minted via REFRESH_TOKEN_AUTH (raw JSON POST, no SDK). - PotaClient.uploadAdif()/getJobs(): multipart POST /adif with the raw ID token in Authorization (no Bearer prefix), plus job-status read. - PotaAdifExporter.buildActivationAdif(): extracted so the share-sheet and upload paths emit identical bytes. - PotaScreen History tab: primary "Upload to POTA" button + sign-in dialog; first upload prompts login, then it is silent. Share-ADIF / Open-pota.app fallbacks are unchanged. Spike scope: refresh token is stored in plaintext (matches the existing QRZ password storage) -- EncryptedSharedPreferences is a planned follow-up. Rides POTA's undocumented API, so the manual fallbacks stay. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Volume was baked into the per-cycle audio buffer, so a slider change only applied on the next over — there was no way to pull drive down during a transmission, a hardware-damage risk users reported. Move volume from a once-per-cycle bake to a live, per-chunk read on every TX path that streams to a radio, so dragging the slider down ramps the on-air level within tens of milliseconds. - USB-direct (libusb): add a g_outputVolume atomic + nativeSetTxVolume JNI in usb_audio_capture.cpp; scale each int16 sample as it is copied into the iso transfer buffers (prime loop + onOutputComplete). Java now passes full-scale PCM. Latency ~= buffered-ahead window (~32ms). - AudioTrack (Android sink): MODE_STATIC one-shot write -> MODE_STREAM with a small (~200ms) buffer, written in ~50ms chunks each scaled by the current volumePercent. Worker thread owns stop()/release(); STOP flips a cancel flag and pause()+flush()es for instant silence, which also unblocks a worker stuck in a blocking write (no release race / deadlock). - USB-direct UsbRequest fallback: scale each chunk live as it is sent. - truSDX CAT: apply volume around the 8-bit midpoint (128) per 256-byte send chunk, re-applying the 0x3B->0x3A escape after scaling. - Wire every volumePercent change (slider, hardware buttons, ALC auto-volume) to the native loop via one observeForever in ComposeMainActivity. ICOM/XieGu UDP paths already scaled live per packet and are unchanged. Tests: add coverage for floatToInt16NoPad (no per-chunk pad) and the chunked per-offset slicing (a mid-stream volume change attenuates only later chunks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Workflow note directing every separate line of work to its own git worktree instead of branch-switching the primary checkout, which collides with in-flight builds and adb installs. Includes the fresh-worktree gotchas: the untracked cpp/ native sources must be copied over or the NDK build fails, and build/install still goes through the Windows gradlew.bat wrapper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
State explicitly that no work — including one-line docs/config changes — lands via direct commit to dev or main, and that all PRs target dev. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document git worktree workflow in CLAUDE.md
The QSO bottom sheet now surfaces how far away the station you're calling is and where it is in the world: - New Distance stat card sits between Signal and Azimuth, formatted in the operator's preferred unit (mi/km) via MaidenheadGrid. - A compact equirectangular path map auto-zooms to frame the operator's grid and the remote grid, draws Natural Earth land outlines (reusing WorldOutlines), and connects the two stations with a dashed line. Longitudes are normalized across the antemeridian so trans-Pacific paths take the short way and wrapped continents still render. - The map card footer shows the DX country/state line (reusing formatLocationLine over message.fromWhere + UsStateLookup). The map and its trailing spacer render only when both grids are known, so nothing shifts when a decoded message lacks a grid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…code Extract the path-map framing math out of the QsoSheet DrawScope code into a pure, no-Android `QsoPathProjection` class so it can be unit-tested without a Canvas. `drawQsoPath` now constructs it instead of inlining the antemeridian normalization, bbox centering, span clamping, and uniform-scale math. Tests: - QsoPathProjectionTest (plain JUnit): longitude normalization across the date line, midpoint-centers-the-canvas, north-is-up, minimum-span clamp, and that both endpoints stay on-canvas for a trans-Pacific path. - QsoSheetLogicTest (Robolectric): computeDistanceText placeholder/format branches and gridToLatLon null/parse/decode branches. The two helpers were made `internal` for test visibility. Also add a Testing section to CLAUDE.md: every new code path requires a new test, with the extract-logic-from-Composable pattern and how to run the suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add res/raw/us_states.json (a ~90 KB GeoJSON FeatureCollection of the 50 states + DC) and a UsStateOutlines loader alongside WorldOutlines. The shared FeatureCollection parsing is factored out into parseGeoJsonRings so both land and state datasets use one code path. drawQsoPath now takes a stateRings list and strokes the state boundaries (no fill, fainter/thinner than the coastline) over the land fill, using the same projection and -360/0/+360 lon-offset repetition as the land so the Aleutians don't drop out. State lines only matter when a US endpoint is in frame; off-frame they clip away. Test: GeoOutlinesParseTest covers parseGeoJsonRings — Polygon, MultiPolygon, hole-skipping, non-polygon-skipping, and the empty case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The decode page filter (All / CQ Calls / CQ POTA / New DXCC / Needed / For Me) was stored in a local rememberSaveable, so it reset to "All" every time the user navigated to another tab and back. The tab switcher in FT8USApp swaps screens with a `when` block, which disposes and recreates DecodeScreen, discarding composable-local state. Hoist the selection into MainViewModel.decodeFilter (MutableLiveData), mirroring how the QSO bottom-sheet state already survives navigation. DecodeScreen now observes it and writes via postValue. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A new button in the decode top bar (next to Compact and Clear) toggles "clear every cycle" mode. When on, the decode list is wiped at the start of each cycle so it only ever shows the current slot; when off, decodes accumulate as before. - GeneralVariables.clearDecodesEveryCycle: new persisted flag. - DatabaseOpr: load the flag from config key "clearDecodesEveryCycle". - MainViewModel: clear ft8Messages before adding a non-deep cycle's decodes when the flag is on (deep decodes augment the cleared cycle, so they don't re-wipe it). - FT8USIcons.AutoClear: circular-arrow icon for the toggle, tinted Accent when on / TextMuted when off. - DecodeScreen: the toggle button, persisting via writeConfig like the Compact (msgMode) button. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HUNT (auto-answer CQ) and the CQ button were independent, so both could be active at once. Disable each while the other is selected: the CQ button greys out and stops responding while HUNT is on, and the HUNT pill greys out while you're actively running CQ. Pressing STOP returns to the neutral state where either mode can be chosen again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SRP email+password path can only authenticate accounts that have a Cognito password. POTA users who sign in with Google / Facebook / Login-with-Amazon are federated identities with no pool password, so SRP fails for them with no in-app recourse. Add the Cognito hosted-UI authorization-code + PKCE flow, which covers every login method POTA offers (the managed login page presents email, Google, Facebook and Amazon together). It produces an ordinary refresh token, so the existing idToken()/refreshIdToken() + /adif upload path is unchanged downstream. POTA's Cognito app client registers exactly one redirect URI (https://pota.app/) and rejects custom schemes / localhost, so Chrome Custom Tabs can't intercept the code. A WebView we control can: it watches navigation and lifts ?code= out the instant Cognito redirects, before pota.app loads. The default WebView UA ("; wv") trips Google's disallowed_useragent block, so the WebView uses a plain Chrome UA. - PotaAuth: newPkce()/authorizeUrl()/exchangeCode() + token-endpoint POST and id_token email-claim parsing for display. - PotaOAuthLogin.kt (new): full-screen WebView dialog driving the flow. - PotaScreen: "Sign in with Google / Facebook / Amazon" button in the existing login dialog hands off to the WebView; success resumes the pending upload. assembleDebug green. Live federated round-trip still needs on-device verification with a real federated pota.app account. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
observeForever was registered in onCreate and never removed, so every activity recreation (rotation, theme/locale change, process restart) added another observer and fired a redundant native setTxVolume per change. Switch to observe(this) so the observer is auto-removed on destroy. TX runs with the activity foregrounded (STARTED), so STARTED-only delivery loses no updates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tap now writes through mainViewModel.decodeFilter rather than directly into selectedFilter; reword the comment accordingly (addresses Copilot review note on PR #158). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- computeDistanceText: go through gridToLatLng + getDistLatLngStr so two stations in the same grid square show "0 mi/km" instead of "--". Only unparseable grids now fall back to the placeholder (getDistStr collapsed the real-zero and unknown cases together). Adds a regression test. - QSO path map: project the land + US-state ring sets into screen-space Paths once via Modifier.drawWithCache instead of rebuilding the whole geometry on every draw pass. The per-frame draw (recompose + sheet slide-in animation) now only issues drawPath calls, avoiding jank. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address Copilot review on PR #159 and a long-standing Clear-button lag. Clear-every-cycle: move the per-cycle wipe from afterDecode() to beforeListen() so it fires at the start of every slot, before decoding. The old placement only cleared when a non-deep cycle reached the append block, so a silent slot (zero decodes, or all decodes filtered as own-TX echoes -- both return early from afterDecode) left the previous slot on screen, contradicting the "only the current slot" intent. Clear button: the decode screen observes mutableFt8MessageList with structural equality, but every post handed back the same mutated ft8Messages instance, so Compose saw no change and only refreshed when the 1 Hz clock incidentally recomposed -- up to a second later. Tapping Clear felt dead. Add publishFt8MessageList(), which posts a defensive copy so Compose gets a structurally distinct value and the UI updates immediately; route afterDecode, clear, and the QTH-enrichment runnable through it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the Hound side of FT8 DXpedition (Fox/Hound) mode: call a
DXpedition "Fox" high in the 1000-4000 Hz band, auto-QSY down to where the
Fox answers, reply with the report, and log on RR73.
The Fox combo message ("CALL RR73; CALL2 <hash> rpt", i3=0/n3=1) already
decodes and surfaces to Java on this build (verified on-device against a
WSJT-X ft8code reference frame), so no native/decoder work is needed. The
Hound only ever transmits standard i3=1 messages (grid-call + R+rpt), which
already encode, so there are no DSP changes.
- Ft8Message: fix cosmetic double-sign in the i3=0/n3=1 combo formatter.
- GeneralVariables: houndMode + houndFoxCall flags.
- FT8TransmitSignal: startHound() + handleHoundCycle(), a dedicated Hound
QSO handler gated behind houndMode (standard sequencer untouched). Locks
TX to the odd slot, reuses getFunctionCommand orders 1 (grid) and 3
(R+rpt), auto-QSYs to the Fox frequency on invite, logs on RR73.
- MainViewModel: startHoundMode()/stopHoundMode() (disables Hunt, which is
mutually exclusive).
- TxStrip: new "DX" chip; FT8USApp: HoundSetupSheet (Fox call + call freq).
Verified on-device: builds, installs, launches without crash; the DX chip
opens the setup sheet; Start enters Hound mode and transmits the grid-call
in the odd slot each cycle with Hunt auto-disabled. Full QSO sequencing
(invite -> QSY -> reply, RR73 -> log) reuses proven primitives but awaits
on-air validation against a live Fox.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds FT4 as a selectable operating mode alongside FT8, driven by a new ModeProfile descriptor so future modes (e.g. FT2) are a one-entry add rather than another FT8/FT4 boolean. - ModeProfile enum: per-mode timing/symbol/protocol params keyed off the existing FT8Common.*_MODE ints; GeneralVariables.operatingMode + config persistence in DatabaseOpr. - Encode: parameterize GenerateFT8.generateFt8ByA91 by ModeProfile (FT8 output unchanged). The prebuilt libft8cn.so exposes no FT4 encode JNI, so a shim (cpp/ft4_encode_jni.cpp) in the CMake-built libft8af_usb.so bridges to the prebuilt's raw ft4_encode; GenerateFT8 now loads ft8af_usb too. - Decode: pass the mode's protocol to InitDecoder, tag messages, use the mode's RX window. - Timing: UtcTimer.sequential(utc, slotMillis) generalized (FT8 identical); FT8SignalListener/FT8TransmitSignal rebuild their cycle timers on mode change; late-start/Costas-clip slack now per-mode; SlotTimerBar parameterized by slot length. - UI: mode pill on the decode page (TxStrip), disabled mid-TX; cycles FT8<->FT4 via MainViewModel.setOperatingMode, which retunes the dial WITHIN the same band only (never auto-QSY to an untuned band). - Bands: mode-tagged FT4 dials in bands.txt; pickers filter by mode; OperationBand.getModeBandFreq for in-band retune. - Mode strings (QSL log, SWL log, PSKReporter query, POTA self-spot) now derive from ModeProfile.displayName. - Tests: ModeProfileTest, OperationBand mode parsing/getModeBandFreq, UtcTimer.sequential per-mode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Real-time TX volume: slider takes effect mid-transmission
Add distance, DX location, and a path map to the QSO panel
Persist decode-screen filter across navigation
Add decode-page toggle to clear decodes every cycle
Add federated (Google/Facebook/Amazon) POTA sign-in via hosted-UI OAuth
Add FT4 mode (extensible to future modes)
There was a problem hiding this comment.
Pull request overview
Release merge promoting dev to main, rolling up multiple recently-merged features/fixes across POTA upload/auth, FT4 mode support, DXpedition “Hound” mode, QSO path mapping, decode-list UX improvements, and TX-volume correctness.
Changes:
- Add FT4 as a first-class operating mode (mode profile descriptor, timers, band dials, UI mode pill, PSKReporter/POTA mode strings).
- Add/finish in-app POTA upload and hosted-UI federated sign-in (OAuth WebView flow, token storage, ADIF builder reuse).
- Add QSO distance + path map UI, plus decode-list behavior improvements (clear-every-cycle, instant Clear, persistent filter), and TX-volume live-scaling fixes/tests.
Reviewed changes
Copilot reviewed 52 out of 52 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ft8cn/app/src/test/kotlin/radio/ks3ckc/ft8us/ui/pota/StripWebViewTokenTest.kt | Unit coverage for WebView UA sanitization helper used by hosted-UI OAuth. |
| ft8cn/app/src/test/kotlin/radio/ks3ckc/ft8us/ui/pota/BuildActivationAdifTest.kt | Robolectric coverage for per-activation ADIF builder, including “no QSOs => empty list”. |
| ft8cn/app/src/test/kotlin/radio/ks3ckc/ft8us/ui/map/GeoOutlinesParseTest.kt | Coverage for shared GeoJSON outlines parser used by map rendering. |
| ft8cn/app/src/test/kotlin/radio/ks3ckc/ft8us/ui/decode/QsoSheetLogicTest.kt | Coverage for pure helpers backing QSO sheet distance/grid decode logic. |
| ft8cn/app/src/test/kotlin/radio/ks3ckc/ft8us/ui/decode/QsoPathProjectionTest.kt | Coverage for pure projection/framing math used by the QSO path inset map. |
| ft8cn/app/src/test/kotlin/radio/ks3ckc/ft8us/ui/components/ActiveQsoPanelLogicTest.kt | Coverage for extracted QSO-panel conversation classification/sorting logic. |
| ft8cn/app/src/test/java/com/bg7yoz/ft8cn/timer/UtcTimerTest.java | Adds tests for slot-length-aware sequential() (FT8 + FT4). |
| ft8cn/app/src/test/java/com/bg7yoz/ft8cn/OwnTxEchoFilterTest.java | Coverage for filtering own-TX loopback echoes out of decodes. |
| ft8cn/app/src/test/java/com/bg7yoz/ft8cn/ModeProfileTest.java | Coverage for FT8/FT4 mode descriptor table and fromId() behavior. |
| ft8cn/app/src/test/java/com/bg7yoz/ft8cn/ft8transmit/FT8TransmitSignalTest.java | Coverage for TX audio helper functions (volume scaling + PCM conversions). |
| ft8cn/app/src/test/java/com/bg7yoz/ft8cn/Ft8MessageTest.java | Coverage for DXpedition combo message formatting edge cases. |
| ft8cn/app/src/test/java/com/bg7yoz/ft8cn/database/OperationBandTest.java | Coverage for FT4-tagged band parsing and mode-specific dial lookup. |
| ft8cn/app/src/main/res/values/strings_compose.xml | Adds strings for mode switching, distance stat, and POTA upload/login UI. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/pota/PotaScreen.kt | Adds per-activation “Upload to POTA” flow with login handling and spinners. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/pota/PotaOAuthLogin.kt | New hosted-UI OAuth WebView dialog + UA sanitization helper. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/pota/PotaAdifExporter.kt | Extracts shared ADIF builder + prevents header-only ADIF exports. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/map/WorldOutlines.kt | Extracts shared GeoJSON ring parser; adds US state outlines loader. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/decode/QsoSheet.kt | Adds distance stat and a cached-draw path map inset to QSO sheet. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/decode/QsoPathProjection.kt | New pure geometry class/constants for map projection/framing. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/decode/DecodeScreen.kt | Persistent filter (ViewModel), clear-every-cycle toggle, and instant Clear wiring. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/components/TxStrip.kt | Adds mode pill + DX toggle; enforces CQ/HUNT mutual exclusivity/accessibility semantics. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/components/SlotTimerBar.kt | Makes slot length configurable (FT8 vs FT4) and aligns slot index computation. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/components/HoundSetupSheet.kt | New setup dialog for DXpedition Hound mode parameters. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/components/FT8USIcons.kt | Adds AutoClear icon for decode “clear every cycle” toggle. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/components/FrequencyPickerSheet.kt | Filters band/dial picker by operating mode (FT8 vs FT4). |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/components/ActiveQsoPanel.kt | Extracts QSO log builder; removes decoded-loopback TX branch reliance. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/pskreporter/PskReporterClient.kt | Uses current operating mode name in PSKReporter query. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/pota/PotaClient.kt | Adds authenticated ADIF upload and job polling helpers. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/pota/PotaAuth.kt | New Cognito SRP + hosted-UI OAuth auth manager with encrypted refresh-token storage. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/FT8USApp.kt | Wires mode pill/timer, DX (Hound) setup sheet, and mode switching UX. |
| ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ComposeMainActivity.kt | Forwards live TX volume updates to native USB output path; applies loaded operating mode. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/wave/UsbAudioNative.java | Adds native live TX volume setter API. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/wave/UsbAudioDevice.java | Seeds native volume and applies live scaling in Java fallback USB output loop. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/timer/UtcTimer.java | Adds slot-length-aware sequential() and routes legacy sequential() through current mode. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/rigs/TrUSDXRig.java | Moves TX volume to live per-chunk scaling in truSDX path. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/OwnTxEchoFilter.java | New filter utility for dropping own-TX loopback decodes + diagnostics. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ModeProfile.java | New FT8/FT4 mode descriptor table and encode dispatch. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/MainViewModel.java | Adds mode LiveData + mode switching; decode list snapshot publishing; echo filter; clear-every-cycle logic. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/GeneralVariables.java | Adds operatingMode/currentMode, clear-every-cycle flag, and Hound mode globals. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ft8transmit/GenerateFT8.java | Adds FT4 encode binding + mode-aware waveform generation. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ft8transmit/FT8TransmitSignal.java | Implements MODE_STREAM chunked AudioTrack playback, mode-aware timers, Hound mode handler, and late-start math fixes. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/Ft8Message.java | Fixes DXpedition combo report formatting (no “--18”; “+0” for zero). |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ft8listener/FT8SignalListener.java | Makes listener timer mode-aware and rebuildable; passes mode flag into decoder init. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/database/OperationBand.java | Adds per-mode band entries + mode-specific dial lookup + mode filtering. |
| ft8cn/app/src/main/java/com/bg7yoz/ft8cn/database/DatabaseOpr.java | Persists mode names in SWL messages; loads clear-every-cycle + operatingMode config keys. |
| ft8cn/app/src/main/cpp/usb_audio_capture.cpp | Adds live TX gain scaling and JNI setter for USB-direct output drain loop. |
| ft8cn/app/src/main/cpp/ft4_encode_jni.cpp | New JNI shim bridging FT4 encode into the prebuilt DSP library. |
| ft8cn/app/src/main/cpp/CMakeLists.txt | Imports prebuilt libft8cn.so and links ft8af_usb against it (FT4 encode shim). |
| ft8cn/app/src/main/assets/bands.txt | Adds FT4 dial entries tagged by mode. |
| ft8cn/app/build.gradle | Adds AWS Cognito SRP dependency + AndroidX security-crypto for token storage. |
| CLAUDE.md | Documents worktree-based workflow and test expectations for contributions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
367
to
+370
| public void afterDecode(long utc, float time_sec, int sequential | ||
| , ArrayList<Ft8Message> messages, boolean isDeep) { | ||
| if (messages.size() == 0) return;//no messages decoded, don't trigger action | ||
| , ArrayList<Ft8Message> decoded, boolean isDeep) { | ||
| if (decoded.size() == 0) return;//no messages decoded, don't trigger action | ||
|
|
Comment on lines
+161
to
+163
| // Volume is baked into the TX samples per-cycle (see playFT8Signal / playViaUsbAudio), | ||
| // so there is no live mid-cycle setVolume() observer here: a volume change takes effect | ||
| // on the next cycle. This matches the ALC auto-volume model (MeterProtectionController). |
Two pushes to dev landed 19s apart (merges of #161 and #163) and both ran the "Publish AAB to Play Internal track" step concurrently. Google Play permits only one active edit per app, so the second run's edit was invalidated mid-upload and the job failed with "This edit has expired, please create a new Edit." Add a workflow-level concurrency group so all release-publishing runs (pushes to main/dev and v* tags) share a single "play-publish" queue and never overlap on the Play API. cancel-in-progress is false so an in-flight upload finishes instead of being killed. PRs and feature-branch pushes get a unique per-run group and are never queued. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The TX strip packed six controls — status, mode, frequency, DX, HUNT, CQ, and the TX-slot toggle — into a single non-wrapping Row with SpaceBetween. On a Pixel 8 in portrait (~411 dp) their combined intrinsic width overflowed the right edge, clipping the CQ and TX1/TX2 buttons off screen so CQ couldn't be tapped. The row got tighter once the FT4 mode pill (#163) and DX/Hound toggle (#162) were added. Convert the strip to a FlowRow (matching the ExperimentalLayoutApi FlowRow already used in ActiveQsoPanel) so overflow controls wrap onto a second line instead of running off the edge. The six pills are now direct FlowRow children, each centered within its line via Modifier.align, with 8.dp horizontal and 6.dp vertical spacing. In portrait this lays out as status/mode/frequency/DX on line one and HUNT/CQ/TX on line two; on wider screens it stays a single line. The status text is capped to one line so a long localized label can't balloon its row. No control logic, colors, or callbacks changed. This is a declarative layout change with no extractable decision/geometry logic to unit-test (the project's tests are JUnit4 + Truth logic tests; Compose layout is verified on-device). Verified on a Pixel 8 in portrait: all six controls visible and tappable, CQ no longer clipped. Existing unit suite still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FT2 is the new ultra-fast HF digital mode from ft2.it (IU8LMC). It is structurally identical to FT4 — 4-GFSK, four Costas sync blocks, 87 data symbols, 77-bit payload, LDPC(174,91) — but runs at double the FT4 baud: 0.024s symbol period (NSPS 288 @12kHz), ~41.7Hz tone spacing, ~167Hz BW, 3.8s T/R cycle, ~2.52s audio. Confirmed against Decodium's FtxFt2Stage7.cpp. TX + mode plumbing (mirrors the FT4 PR #163): - FT8Common.FT2_MODE + ModeProfile.FT2 (one-entry add). Because FT2's tones are bit-identical to FT4, encode() reuses ft4Encode and synth runs at the FT2 symbol period — no new encode code. - bands.txt FT2-tagged dials (PROVISIONAL: FT8 sub-band placeholders pending the official ft2.it/HamPass list); OperationBand parser resolves the mode tag via ModeProfile.displayName so future modes need no new branch. - Mode pill cycles FT8->FT4->FT2 (iterates ModeProfile entries); PSKReporter mode string derives from displayName. RX — parallel from-source decoder (the prebuilt libft8cn.so has no FT2): - Adds FTX_PROTOCOL_FT2 to the in-tree kgoba ft8_lib (pinned at 6f528128, the same commit the prebuilt was built from): constants.h period/slot, monitor.c symbol-period switch, decode.c FT4 branches broadened to FT2 (shared 4-Costas/105-symbol/XOR layout). - ft2_decode_jni.cpp: FT2 decode JNI wrapper (adapted from the ft8_decoder.cpp reconstruction) with distinct *Ft2 entry points and protocol fixed to FT2. - CMake compiles the from-source decode slice + wrapper into libft8af_usb.so with -fvisibility=hidden so its symbols stay internal and never clash with the prebuilt's exported copies; FT8/FT4 keep using the prebuilt unchanged. - FT8SignalListener routes the decode loop to the FT2 backend when ModeProfile.usesFt2Decoder(); FT8/FT4 paths untouched. This newly tracks the vendored kgoba ft8_lib (previously untracked reference, "reconstruction in progress") because the FT2 build now compiles it. Tests: ModeProfile/OperationBand/UtcTimer FT2 cases. Verified on a Pixel 8: unit tests pass, native decoder links across all 4 ABIs, and a native FT2 round-trip (ft4_encode -> GFSK@0.024s -> FT2 decode) recovers the message on-device (Block size=288, score 40). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
FT8 needs the device clock within ~1 s of UTC, but the only sync paths today fail in the field: NTP auto-sync needs internet, and the legacy +/-7.5 s spinner lives only in the dead ConfigFragment (the Compose Settings UI had no time control at all). Add a "Time Sync" settings category with a +/- stepper (+/-0.1 s and +/-0.5 s, range +/-2.0 s) plus a Reset, driving UtcTimer.delay -- the single offset every RX window, TX start, and the slot-timer bar reads through. The correction is persisted under a new "timeCorrectionMs" config key and re-applied to UtcTimer.delay at startup, so an offline nudge survives a relaunch. A suggestion card surfaces the most recent cycle's average decode DT (mainViewModel.mutableTimerOffset) -- the only time reference available offline -- with one-tap apply via suggestedCorrectionMs(). Decision/format logic is extracted into pure functions in TimeCorrection.kt (clamp/step/suggested/format) and unit-tested in TimeCorrectionTest.kt, keeping the Composable a thin wrapper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CAT connection state previously surfaced only as transient Toasts, with no persistent indicator. Bluetooth in particular usually only connects on the second attempt: the first socketConnect() fails and silently drops back to disconnected, and a later CAT command auto-retries — so it works eventually, but the operator gets no signal in between. Add an always-visible status chip to the bottom TX strip (grey=disconnected, amber pulsing=connecting, green=connected, red=error) that is tappable to re-trigger the connection, turning the second-attempt friction into one tap. - New CatConnectionState enum exposed as LiveData on MainViewModel, driven from the rig-state callbacks plus a new onConnecting() default-method callback (no existing implementer changes). BluetoothRigConnector.socketConnect() and CableConnector.connect() fire onConnecting(), covering the auto-retry path. - MainViewModel.reconnectRig() reuses the connector connect() path on tap. - CatStatusChip with pure, tested catChipVisuals()/shouldShowCatChip(); chip is hidden for VOX/audio-only setups. - Strings added across all 6 locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues flagged by Copilot on the dev->main release PR (#165): 1. MainViewModel.afterDecode() returned early on a zero-decode slot without clearing mutableIsDecoding. beforeListen() sets it true every cycle, so a silent slot left the spectrum-display decoding marker stuck on until a later non-empty cycle. Route all three afterDecode() exit paths through a new pure helper decodingMarkerAfterPass() so they stay in agreement, and cover it with DecodingMarkerTest. 2. FT8TransmitSignal's constructor comment claimed TX volume is baked per-cycle and only takes effect next cycle. The live-TX-volume change (#155) made the MODE_STREAM AudioTrack loop re-read volumePercent per chunk and the USB-direct path apply gain live via setTxVolume, so the comment was stale. Updated it to describe the live behavior. 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>
- PskReporterSender: drop spots with an unknown/corrupt signalFormat instead of mislabelling them as FT8 (ModeProfile.fromId falls back to FT8); restores the prior drop-on-unknown behavior while still supporting FT2. - FT8SignalListener / ReBuildSignal: wrap the native loadLibrary calls in try/catch (mirroring GenerateFT8) so class init doesn't crash when native libs aren't on the path (e.g. JVM unit tests). - monitor.c: include <math.h> (sinf/log10f) and common.h (M_PI fallback) so the vendored source builds on toolchains where <math.h> doesn't define M_PI. - Add ft8_lib/fft/COPYING (KISS FFT BSD-3-Clause) referenced by the kiss_fft headers but previously missing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues raised on PR #170: 1. ERROR was unobservable. A failed Bluetooth connect calls onRunError() (ERROR) immediately followed by socketDisconnect() -> onDisconnected(), which overwrote the state back to DISCONNECTED, so the chip never stayed red. Track a synchronous catConnectionState mirror (postValue/getValue would race across the two callbacks) and add CatConnectionState.afterDisconnect() which preserves ERROR until the next connect attempt (onConnecting) or a success clears it. 2. showCatChip read GeneralVariables.controlMode, which isn't observable Compose state, so switching VOX <-> CAT/RTS/DTR in Settings wouldn't update the chip until an unrelated recomposition. Add GeneralVariables.mutableControlMode LiveData, post it from setControlMode() and the connect paths that force CAT, and derive showCatChip from the observed value in FT8USApp. Adds unit tests for afterDisconnect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix stuck decoding marker on silent slots; correct TX-volume comment
Serialize Play publishing to prevent edit-expired races
Wrap TX strip controls so CQ isn't clipped in portrait
Add FT2 mode (TX + RX)
Apply-suggested computed suggestedCorrectionMs() off the live UtcTimer.delay while the rest of the screen reads the local correctionMs state. If NTP sync updates UtcTimer.delay asynchronously while the Time Sync screen is open, the suggestion would be applied against a different base than what the UI shows. Use correctionMs so the suggestion is consistent with the displayed value (addresses Copilot review on #169). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add tappable CAT connection status chip to TX strip
Add a compact equirectangular map to POTA activations that frames the operator plus every plottable contact, with a dashed line out to each. It generalizes the QSO-distance map's projection (QsoPathProjection) from two endpoints to N contacts, reusing the same padding, min-span floor, and antemeridian short-path normalization. - PotaActivationProjection: pure N-point framing math, unit-tested without a Canvas (parity with the 2-point QSO projection, short-path lon, min-span clamp, centering). - buildActivationMapData: turns an activation's QSOs into map inputs; drops contacts with no/unparseable grid, sources the operator from the first QSO's my_gridsquare (so historical activations plot where the op was) with a fallback to the live grid, returns null when nothing is plottable. - PotaActivationMap: drawWithCache Compose map (land + US state outlines, operator/contact dots, operator label). Rework History: replace the inline expand/collapse row with a tap-through ActivationDetailScreen (back nav, park pills, contacts map, Share-ADIF / Open-pota.app actions, full QSO list). The map also renders live in the Activate tab. Plumbing: PotaActivationDao now selects my_gridsquare; PotaQso gains a myGrid field. New strings pota_contacts_map / pota_back across all locales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve PotaScreen.kt conflict between dev's direct upload-to-POTA feature (Cognito/OAuth sign-in + inline history-row upload button) and this branch's tap-through ActivationDetailScreen. Since the detail screen replaces the inline row and PotaScreen unmounts HistoryTab when it shows, the upload button and login/OAuth dialogs move into ActivationDetailScreen; HistoryTab becomes pure navigation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add manual time correction for offline operating
- PotaActivationMap: hoist the dashed-line PathEffect out of the per-contact draw loop into the drawWithCache cache phase to avoid per-frame allocation. - ActivationDetailScreen: fall back to the live grid only for active activations, so an in-progress activation map shows before the first QSO carries my_gridsquare, while finished activations stay null (no misplacement from a stale current grid). - PotaActivationProjection: fix a duplicated-word KDoc typo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
POTA: activation contacts map + tap-through history detail
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.
Release:
dev→mainRolls up 20 merged PRs (#151–#172) since the last release (#150). Highlights: in-app POTA upload with federated sign-in and an activation map, FT4 and FT2 modes, FT8 DXpedition Hound mode, a QSO distance/path map, manual time correction for offline operating, and several TX-audio, decode-list, and layout fixes. +11,838 / −542 across 113 files, with extensive new unit coverage.
✨ New features
ModeProfile.🎛️ UX & decode improvements
🐛 Fixes
🧰 Docs & tooling
✅ Testing
New unit coverage added across the release: FT4/FT2
ModeProfile, own-TX echo filter, QSO path projection / distance helpers, QSO panel + sheet log builders, POTA activation ADIF builder and activation-map history, manual time correction, WebView token stripping, geo-outline parsing,FT8TransmitSignal, andUtcTimer.🤖 Generated with Claude Code