From 25228475c731510f6b411850eeef88d28897878b Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 1 Jun 2026 15:50:03 -0500 Subject: [PATCH 1/5] ci: release pipeline for dev / main flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the release pipeline so dev is the integration target and main is the production candidate. Every push to either branch publishes to Play internal so testers always have the latest; production promotion remains a manual step in Play Console. Changes to build-release.yml: - Push triggers: drop the legacy capital-D "Dev"; add lowercase "dev". - PR triggers: now fire for PRs to either main or dev (was main only) so feature → dev PRs get unit tests. - Tag determination grows a dev branch lane: push to dev produces a run-numbered "dev-" tag, marks the GitHub release as prerelease, and uploads to Play internal alongside the main-branch v* releases. Distinct tag prefix keeps the two streams visually separate in both the GitHub releases list and the Play Console releaseName column. - softprops/action-gh-release now honors the is_prerelease output so dev-* releases sort under the latest v* and don't shadow "latest". New main-gate.yml workflow: - A required PR status check that enforces PRs to main must come from the dev branch. Failing fast on any other source means the release discipline (feature → dev → main) is mechanically enforced rather than left to convention. Once this lands on main, the repo owner enables branch protection in GitHub Settings → Branches: - main: require PR, require status checks (test, build, enforce-source-is-dev), restrict who can push to the repo owner only. - dev: require PR, require status checks (test, build). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build-release.yml | 34 ++++++++++++++++++++++++----- .github/workflows/main-gate.yml | 31 ++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/main-gate.yml diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index f9fa0d1e..68b89a56 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -2,11 +2,11 @@ name: Build & Release APK on: push: - branches: [main, Dev] + branches: [main, dev] tags: - 'v*' pull_request: - branches: [main] + branches: [main, dev] permissions: contents: write @@ -56,7 +56,7 @@ jobs: build: name: Build APK needs: test - # Run when tests pass (PR), or when tests were skipped (push to main/Dev/tags). + # Run when tests pass (PR), or when tests were skipped (push to main/dev/tags). # Block only on actual test failure or cancellation. if: ${{ always() && needs.test.result != 'failure' && needs.test.result != 'cancelled' }} runs-on: ubuntu-latest @@ -74,12 +74,19 @@ jobs: id: tag run: | set -euo pipefail + # Three release lanes (everything else is build-only): + # v* tag push -> production-named release, NOT prerelease + # push to main -> auto-bumped v* tag, NOT prerelease + # push to dev -> dev- tag, PRERELEASE + # Prereleases are still uploaded to Play internal but with a + # distinct releaseName so they're easy to tell apart from the + # v*-named candidates we'd actually promote to production. if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then - # Manual tag push — release as that tag echo "release_tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" elif [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/main" ]]; then - # Push to main — auto-bump patch from latest v* tag + # Auto-bump patch from latest v* tag. latest=$(git tag --list 'v*' --sort=-v:refname | head -n1) if [[ -z "$latest" ]]; then new_tag="v0.1" @@ -93,10 +100,21 @@ jobs: echo "Latest tag: ${latest:-} -> new tag: $new_tag" echo "release_tag=$new_tag" >> "$GITHUB_OUTPUT" echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" + elif [[ "${GITHUB_EVENT_NAME}" == "push" && "${GITHUB_REF}" == "refs/heads/dev" ]]; then + # dev pushes get a run-numbered tag and are flagged as prereleases. + # Distinct prefix so they sort separately from v* in GitHub releases + # and so the Play Console releaseName disambiguates them from + # main-branch release candidates. + new_tag="dev-${GITHUB_RUN_NUMBER}" + echo "release_tag=$new_tag" >> "$GITHUB_OUTPUT" + echo "should_release=true" >> "$GITHUB_OUTPUT" + echo "is_prerelease=true" >> "$GITHUB_OUTPUT" else - # PR or Dev push — build only, no release + # PR or other branch push — build only, no release echo "release_tag=" >> "$GITHUB_OUTPUT" echo "should_release=false" >> "$GITHUB_OUTPUT" + echo "is_prerelease=false" >> "$GITHUB_OUTPUT" fi - name: Set up JDK 17 @@ -186,6 +204,10 @@ jobs: tag_name: ${{ steps.tag.outputs.release_tag }} files: ft8cn/app/build/outputs/apk/release/FT8CN-*.apk generate_release_notes: true + # dev-* releases are flagged as prerelease so they sort under the + # latest v* and don't get picked up by downstream tooling that + # looks for "latest release". + prerelease: ${{ steps.tag.outputs.is_prerelease == 'true' }} append_body: true body: | diff --git a/.github/workflows/main-gate.yml b/.github/workflows/main-gate.yml new file mode 100644 index 00000000..8e8f6621 --- /dev/null +++ b/.github/workflows/main-gate.yml @@ -0,0 +1,31 @@ +name: Main branch source gate + +# Enforces release discipline: PRs targeting main must come from `dev`. +# Feature branches are expected to land on dev first; only dev → main is +# allowed as the "promote to production" step, gated by branch protection +# requiring this status check. +# +# To enforce it: GitHub → Settings → Branches → Branch protection rule for +# `main` → Require status checks to pass → add "Main branch source gate / +# enforce-source-is-dev" to the required list. + +on: + pull_request: + branches: [main] + +jobs: + enforce-source-is-dev: + name: enforce-source-is-dev + runs-on: ubuntu-latest + steps: + - name: Check PR head branch + env: + HEAD_REF: ${{ github.head_ref }} + run: | + set -euo pipefail + if [[ "${HEAD_REF}" != "dev" ]]; then + echo "::error title=PR source must be dev::PRs to main are only allowed from the dev branch. Source branch: ${HEAD_REF}" + echo "Land your changes on dev first (PR from your feature branch into dev), then open dev → main." + exit 1 + fi + echo "OK — PR source is dev." From 896200dd5a091c8f09587a91d722e87bf77a8fcb Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 1 Jun 2026 16:50:13 -0500 Subject: [PATCH 2/5] docs: surface ft8af.app site link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the new project site (https://ft8af.app) in three places where it's natural for a user to land: - README.md: a prominent "🌐 ft8af.app" line right under the tagline, so anyone browsing the repo on GitHub gets the link before the feature list. - About dialog: new "Website" section above "Built by", clickable via LocalUriHandler in the same style as the existing QRZ author links. Tap to open in the browser. - Splash screen: small "ft8af.app" line under the version footer. Same monospace family, slightly fainter so it doesn't fight the version number. Decorative only — splash is transient. Intentionally left alone: - The "FT8 · MOBILE COMPANION" subtitle on the splash. Three URLs near the top of the splash would be loud; the version footer is the natural place for the domain. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 ++ .../ks3ckc/ft8us/ui/settings/SettingsScreen.kt | 15 +++++++++++++++ .../radio/ks3ckc/ft8us/ui/splash/SplashScreen.kt | 10 ++++++++++ 3 files changed, 27 insertions(+) diff --git a/README.md b/README.md index 1fe54dbf..e9ae7849 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ **FT8 on Android — modernized.** +🌐 **[ft8af.app](https://ft8af.app)** + A fork of [FT8CN](https://github.com/N0BOY/FT8CN) that takes the excellent original and brings it forward: a Jetpack Compose UI, full English localization, dozens of bug fixes, and a pile of new operating features built for real on-the-air use. Run FT8 natively on your Android phone or tablet, drive your radio over USB CAT, decode the band, and work the world from anywhere. diff --git a/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt b/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt index e61e269f..c03c5768 100644 --- a/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt +++ b/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt @@ -1842,6 +1842,21 @@ private fun AboutDialog( }, ) + Text( + text = "Website", + color = TextPrimary, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + ) + Text( + text = "ft8af.app", + color = Accent, + fontSize = 14.sp, + modifier = Modifier + .fillMaxWidth() + .clickable { uriHandler.openUri("https://ft8af.app") }, + ) + Text( text = "Built by", color = TextPrimary, diff --git a/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/splash/SplashScreen.kt b/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/splash/SplashScreen.kt index 58e20f47..6a4d29cd 100644 --- a/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/splash/SplashScreen.kt +++ b/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/splash/SplashScreen.kt @@ -158,6 +158,16 @@ fun FT8USplashScreen( letterSpacing = 0.08.em, ) + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "ft8af.app", + color = Color(0xFF8A96B1).copy(alpha = 0.45f), + fontSize = 10.sp, + fontFamily = GeistMonoFamily, + letterSpacing = 0.08.em, + ) + Spacer(modifier = Modifier.height(96.dp)) } } From 0eae250b66dd817b07e1295bc510fe84420338b4 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 1 Jun 2026 18:52:10 -0500 Subject: [PATCH 3/5] FT8TransmitSignal: instrument TX output path with fileLog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The car-dash TX-abort symptom (PTT keys → 250-300ms → PTT releases, no audio sent) is still happening despite #84 shipping the libusb nativeWrite. But the full TX output path is logged only to logcat, so from debug.log there's no way to tell: 1. Which output branch ran (USB direct vs. AudioTrack). 2. If USB direct ran, which step failed before reaching writeAudio (device lookup, USB permission, open, hasOutput, activateOutput). Same instrumentation pattern as the input side that surfaced the real bugs in #82 / #83. Changes: - playFT8Signal: log the branch decision with audioOutputDeviceId and the saved USB output VID:PID. Log which branch was taken. - playViaUsbAudio: fileLog at each failure point and at the success pivots (device opened + output activated, before writeAudio, writeAudio return value). Every failure path has a distinct message so the failure point is obvious from one log line. After the next car-dash repro we'll know whether the abort is: - audioOutputDeviceId not being -1 at TX time (picker reset?), - device lookup failing (UsbManager state), - permission lost on re-attach, - activateOutput failing on the car-dash kernel, - or actually reaching writeAudio and libusb dying inside. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ft8cn/ft8transmit/FT8TransmitSignal.java | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ft8transmit/FT8TransmitSignal.java b/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ft8transmit/FT8TransmitSignal.java index e66fdd68..9a6c2461 100644 --- a/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ft8transmit/FT8TransmitSignal.java +++ b/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/ft8transmit/FT8TransmitSignal.java @@ -419,12 +419,25 @@ private void playFT8Signal(Ft8Message msg) { return; } - // USB audio output path + // USB audio output path. Logged so a dev-screen reader can see + // which branch was taken — the difference between TX going out + // the USB radio (this branch) vs. Android's default sink (the + // AudioTrack branch below) was the whole reason for #84, and we + // currently have no way to tell from debug.log which one fired. + GeneralVariables.fileLog(String.format( + "playFT8Signal: TX path branch: audioOutputDeviceId=%d " + + "usbAudioOutputVidPid=%04X:%04X", + GeneralVariables.audioOutputDeviceId, + GeneralVariables.usbAudioOutputVendorId, + GeneralVariables.usbAudioOutputProductId)); if (GeneralVariables.audioOutputDeviceId == -1 && GeneralVariables.usbAudioOutputVendorId != 0) { + GeneralVariables.fileLog("playFT8Signal: using USB audio (direct) output"); playViaUsbAudio(buffer); return; } + GeneralVariables.fileLog( + "playFT8Signal: using AudioTrack output (Android default sink)"); Log.d(TAG, String.format("playFT8Signal: Preparing sound card playback... bit depth: %s, sample rate: %d" , GeneralVariables.audioOutput32Bit ? "Float32" : "Int16" @@ -506,14 +519,15 @@ public void onPeriodicNotification(AudioTrack audioTrack) { * Play FT8 signal through USB audio device. */ private void playViaUsbAudio(float[] buffer) { - Log.d(TAG, String.format("playFT8Signal: USB audio output, VID=%04X PID=%04X, samples=%d, rate=%d", + GeneralVariables.fileLog(String.format( + "playViaUsbAudio: start, VID=%04X PID=%04X samples=%d rate=%d", GeneralVariables.usbAudioOutputVendorId, GeneralVariables.usbAudioOutputProductId, buffer.length, GeneralVariables.audioSampleRate)); Context context = GeneralVariables.getMainContext(); if (context == null) { - Log.e(TAG, "No context for USB audio"); + GeneralVariables.fileLog("playViaUsbAudio: ABORT no main context"); afterPlayAudio(); return; } @@ -522,38 +536,56 @@ private void playViaUsbAudio(float[] buffer) { GeneralVariables.usbAudioOutputVendorId, GeneralVariables.usbAudioOutputProductId); if (device == null) { - Log.e(TAG, "USB audio output device not found"); + GeneralVariables.fileLog(String.format( + "playViaUsbAudio: ABORT USB audio output device not found " + + "by VID:PID %04X:%04X", + GeneralVariables.usbAudioOutputVendorId, + GeneralVariables.usbAudioOutputProductId)); afterPlayAudio(); return; } UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE); - if (usbManager == null || !usbManager.hasPermission(device)) { - Log.e(TAG, "No USB permission for audio output device"); + if (usbManager == null) { + GeneralVariables.fileLog("playViaUsbAudio: ABORT UsbManager is null"); + afterPlayAudio(); + return; + } + if (!usbManager.hasPermission(device)) { + GeneralVariables.fileLog( + "playViaUsbAudio: ABORT no USB permission for output device " + + "(re-pick (USB direct) in Settings to re-grant)"); afterPlayAudio(); return; } UsbAudioDevice usbDev = new UsbAudioDevice(); if (!usbDev.open(context, device)) { - Log.e(TAG, "Failed to open USB audio output device"); + GeneralVariables.fileLog( + "playViaUsbAudio: ABORT UsbAudioDevice.open() failed " + + "(descriptor parse or claimInterface failed)"); afterPlayAudio(); return; } if (!usbDev.hasOutput()) { - Log.e(TAG, "USB audio device has no output endpoint"); + GeneralVariables.fileLog( + "playViaUsbAudio: ABORT device has no output endpoint"); usbDev.close(); afterPlayAudio(); return; } if (!usbDev.activateOutput(48000)) { - Log.e(TAG, "Failed to activate USB audio output"); + GeneralVariables.fileLog( + "playViaUsbAudio: ABORT activateOutput(48000) failed " + + "(alt-setting select or rate setup failed)"); usbDev.close(); afterPlayAudio(); return; } + GeneralVariables.fileLog( + "playViaUsbAudio: device opened, output activated at 48000 Hz"); // Skip leading samples if we started transmitting late, so audio still ends on the cycle boundary. int skipSamples = (lateStartSkipMs * GeneralVariables.audioSampleRate) / 1000; @@ -567,10 +599,12 @@ private void playViaUsbAudio(float[] buffer) { volumeAdjusted[i] = buffer[skipSamples + i] * GeneralVariables.volumePercent; } + GeneralVariables.fileLog(String.format( + "playViaUsbAudio: calling writeAudio playLength=%d rate=%d", + playLength, GeneralVariables.audioSampleRate)); boolean success = usbDev.writeAudio(volumeAdjusted, GeneralVariables.audioSampleRate); - if (!success) { - Log.e(TAG, "USB audio write failed"); - } + GeneralVariables.fileLog( + "playViaUsbAudio: writeAudio returned " + (success ? "OK" : "FAILED")); usbDev.close(); afterPlayAudio(); From 691a09a99e935e822fa3a45274ce910b9e0887e4 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Mon, 1 Jun 2026 20:42:54 -0500 Subject: [PATCH 4/5] Settings: TX volume becomes a live slider in the AUDIO section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You can now actually dial in TX drive while watching your radio's ALC. Two changes, no behavior breakage: 1. The TX Volume editor is now a slider with a big live readout instead of a number-input dialog. Dragging updates GeneralVariables.volumePercent immediately (and posts to the LiveData) so the next TX uses the new level without you having to tap Save first. We persist to the config DB on dismiss. The hardware volume key handler in ComposeMainActivity still works as a ±5% quick-adjust — they share the same state. 2. Moved the row from the TRANSMISSION section (where it sat alongside watchdog / stop-after) into the AUDIO section right under Audio Output. That's where someone tuning audio levels actually expects to find it. Why this matters: on the car-dash with the libusb output path now working, the TX audio is reaching the rig but a touch hot on ALC. Number-input UX was unusable for that — open dialog, type number, save, test, repeat. Slider-with-live-update lets you drag a couple of percent at a time and watch the rig's meter respond in real time. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ft8us/ui/settings/SettingsScreen.kt | 121 ++++++++++++++---- 1 file changed, 99 insertions(+), 22 deletions(-) diff --git a/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt b/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt index c03c5768..078c98ee 100644 --- a/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt +++ b/ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/ui/settings/SettingsScreen.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -418,22 +420,23 @@ fun SettingsScreen( } // -- TX Volume Editor -- + // Slider-based dialog with live update: dragging the thumb updates the + // in-memory volumePercent immediately so the next TX uses the new level, + // and we persist to the config DB on dismiss. Tapping outside the dialog + // or "Done" both commit. There's no "Cancel" because muscle-memory adjust + // is the whole point — if you dragged it, you meant it. if (showTxVolume) { - NumberInputDialog( - title = "TX Volume", - suffix = "%", + TxVolumeSliderDialog( initialValue = txVolume, - min = 0, - max = 100, - onDismiss = { showTxVolume = false }, - onSave = { value -> + onChange = { value -> + txVolume = value + GeneralVariables.volumePercent = value / 100f + GeneralVariables.mutableVolumePercent.postValue(value / 100f) + }, + onDismiss = { showTxVolume = false - val clamped = value.coerceIn(0, 100) - txVolume = clamped - GeneralVariables.volumePercent = clamped / 100f - GeneralVariables.mutableVolumePercent.postValue(clamped / 100f) - mainViewModel.databaseOpr.writeConfig("volumeValue", clamped.toString(), null) - mainViewModel.baseRig?.connector?.setRFVolume(clamped) + mainViewModel.databaseOpr.writeConfig("volumeValue", txVolume.toString(), null) + mainViewModel.baseRig?.connector?.setRFVolume(txVolume) }, ) } @@ -866,6 +869,14 @@ fun SettingsScreen( showAudioOutputPicker = true }, ) + SectionDivider() + SettingsRow( + label = "TX Volume", + description = "Transmit audio level (hardware buttons ±5%)", + value = "$txVolume%", + showChevron = true, + onClick = { showTxVolume = true }, + ) } } } @@ -905,14 +916,6 @@ fun SettingsScreen( showChevron = true, onClick = { showStopAfter = true }, ) - SectionDivider() - SettingsRow( - label = "TX Volume", - description = "Transmit audio level (hardware buttons ±5%)", - value = "$txVolume%", - showChevron = true, - onClick = { showTxVolume = true }, - ) } } } @@ -1751,7 +1754,81 @@ private fun SerialPortPickerDialog( } /** - * Simple informational dialog with a dismiss button. + * Slider-based TX volume editor with live update. Dragging commits the new + * level immediately (no Save/Cancel friction — the next TX uses what you + * dialed); we persist to the config DB only on dismiss. + */ +@Composable +private fun TxVolumeSliderDialog( + initialValue: Int, + onChange: (Int) -> Unit, + onDismiss: () -> Unit, +) { + var current by remember { mutableIntStateOf(initialValue) } + Dialog(onDismissRequest = onDismiss) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(BgSurface2) + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + text = "TX Volume", + color = TextPrimary, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + ) + + // Big live readout so you can dial it in at a glance — useful in + // the car where the slider thumb is small relative to the radio's + // ALC meter you're watching at the same time. + Text( + text = "$current%", + color = Accent, + fontWeight = FontWeight.SemiBold, + fontSize = 48.sp, + ) + + Slider( + value = current.toFloat(), + onValueChange = { v -> + val clamped = v.toInt().coerceIn(0, 100) + if (clamped != current) { + current = clamped + onChange(clamped) + } + }, + valueRange = 0f..100f, + colors = SliderDefaults.colors( + thumbColor = Accent, + activeTrackColor = Accent, + ), + ) + + Text( + text = "Adjust live — your next TX uses this level. " + + "Watch your radio's ALC and dial down until it's just kissing the line.", + color = TextMuted, + fontSize = 12.sp, + lineHeight = 16.sp, + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton(onClick = onDismiss) { + Text("Done", color = Accent, fontWeight = FontWeight.SemiBold) + } + } + } + } +} + +/** + * Generic informational dialog: title + body text + dismiss. */ @Composable private fun InfoDialog( From e9fc51c0b30b8df159ed9a6d73ff5098d16035bf Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Wed, 3 Jun 2026 10:04:22 -0500 Subject: [PATCH 5/5] TX volume: bump default to 80% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pairs with the live slider in Settings. 50% was conservative — on the libusb USB-audio output path the rig input stage typically wants more drive than that, and asking new users to discover the slider before their first TX makes the rig look broken. 80% is a reasonable starting point that produces audible drive on a typical rig without immediately pegging ALC; users who want a different default tune it from the slider and we persist. Co-Authored-By: Claude Opus 4.7 (1M context) --- ft8cn/app/src/main/java/com/bg7yoz/ft8cn/GeneralVariables.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/GeneralVariables.java b/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/GeneralVariables.java index 07c55cd3..7a9fa230 100644 --- a/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/GeneralVariables.java +++ b/ft8cn/app/src/main/java/com/bg7yoz/ft8cn/GeneralVariables.java @@ -56,7 +56,7 @@ public class GeneralVariables { public static int usbAudioOutputProductId = 0; public static MutableLiveData mutableVolumePercent = new MutableLiveData<>(); - public static float volumePercent = 0.5f;//Audio playback volume, as a percentage + public static float volumePercent = 0.8f;//Audio playback volume, as a percentage public static int flexMaxRfPower = 10;//Flex radio max transmit power public static int flexMaxTunePower = 10;//Flex radio max tune power