Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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-<run_number> 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"
Expand All @@ -93,10 +100,21 @@ jobs:
echo "Latest tag: ${latest:-<none>} -> 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
Expand Down Expand Up @@ -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: |

Expand Down
31 changes: 31 additions & 0 deletions .github/workflows/main-gate.yml
Original file line number Diff line number Diff line change
@@ -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."
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public class GeneralVariables {
public static int usbAudioOutputProductId = 0;

public static MutableLiveData<Float> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand All @@ -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();
Expand Down
Loading
Loading