Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8dd0103
Fix crash: guard executor submits against teardown race
patrickrb Jun 23, 2026
4ac7575
Flex picker: refresh discovered radios live while dialog is open
patrickrb Jun 23, 2026
c706c4c
Add pull-down meters HUD (ALC/SWR) from top edge
patrickrb Jun 23, 2026
32ef256
Meters HUD: adapt per rig + configurable meter set (fix Flex blank HUD)
patrickrb Jun 23, 2026
a67d2c2
Meters HUD: in-app TX power control + PWR meter on by default
patrickrb Jun 23, 2026
2479f7e
Flex network: auto-discover and connect, no IP typing required
patrickrb Jun 23, 2026
46c4526
Flex discovery: stop auto-connect closing the dialog / breaking live …
patrickrb Jun 23, 2026
c9cc3b4
CAT chip: one-tap connect to saved Flex (and remember its address)
patrickrb Jun 23, 2026
89f64a6
Flex: show discovered radio in picker, auto-connect it, toast on connect
patrickrb Jun 23, 2026
3e99265
Meters HUD: replace invisible top-edge swipe with a visible pull-tab
patrickrb Jun 23, 2026
d9e8b87
Merge remote-tracking branch 'origin/feat/flex-discovery' into feat/m…
patrickrb Jun 23, 2026
4ce13ec
Merge remote-tracking branch 'origin/fix/qth-pool-rejected-execution'…
patrickrb Jun 23, 2026
c9e3860
Merge remote-tracking branch 'origin/fix/flex-picker-live-discovery' …
patrickrb Jun 23, 2026
a0675d9
Flex meters: force meter subscription + poll live values so the HUD p…
patrickrb Jun 23, 2026
527d5c0
Flex meters: drive the poll from the HUD so bars animate
patrickrb Jun 23, 2026
a249a42
Flex meters: drop diagnostic value logging (kept the meterDataReceive…
patrickrb Jun 23, 2026
33a50c2
Flex: force NETWORK connect mode so TX/RX audio routes over DAX
patrickrb Jun 23, 2026
539881b
Flex meters: re-subscribe to the meter stream each time the HUD opens
patrickrb Jun 23, 2026
c11161e
Flex: meter mapping, connection reliability, and DAX transmit groundwork
patrickrb Jun 24, 2026
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
4 changes: 4 additions & 0 deletions ft8af/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- For GPS location access -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Required to hold a WifiManager.MulticastLock so the Wi-Fi chip delivers
FlexRadio UDP discovery broadcasts to us (Android filters broadcast/
multicast packets without it). -->
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
Expand Down
45 changes: 45 additions & 0 deletions ft8af/app/src/main/java/com/k1af/ft8af/ExecutorUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.k1af.ft8af;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;

/**
* Helpers for submitting work to executors that may be torn down concurrently.
*
* <p>The decode pipeline runs on its own thread and calls back into
* {@code MainViewModel} after each pass to kick off background work (QTH/grid
* lookups, network TX audio). Those pools are shut down in
* {@code MainViewModel.onCleared()} when the ViewModel is destroyed (app close,
* connection reset/reconnect). A decode result that lands in the same instant
* the pool is shut down would otherwise reach {@link ExecutorService#execute}
* on a terminated pool and throw {@link RejectedExecutionException} on the
* decode thread, crashing the app. See the {@code afterDecode} race fixed
* alongside this helper.
*/
public final class ExecutorUtils {

private ExecutorUtils() {
}

/**
* Submit {@code task} to {@code pool}, tolerating a pool that is shutting
* down. The {@link ExecutorService#isShutdown()} check avoids the common
* case; the catch covers the narrow race where the pool is shut down
* between the check and {@link ExecutorService#execute}.
*
* @return {@code true} if the task was accepted, {@code false} if it was
* dropped because the pool (or task) was unavailable.
*/
public static boolean safeExecute(ExecutorService pool, Runnable task) {
if (pool == null || task == null || pool.isShutdown()) {
return false;
}
try {
pool.execute(task);
return true;
} catch (RejectedExecutionException e) {
// Pool was shut down concurrently (ViewModel teardown). Drop the task.
return false;
}
}
}
18 changes: 18 additions & 0 deletions ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public static boolean isConfiguredUsbAudioOutput(int vid, int pid) {

public static int flexMaxRfPower = 10;//Flex radio max transmit power
public static int flexMaxTunePower = 10;//Flex radio max tune power
// Last FlexRadio address connected to (discovered or typed). Persisted so the
// CAT chip can reconnect on a cold start without re-opening the picker.
public static String flexLastIp = "";

// Hidden debug mode (unlocked by tapping the version 7 times in About).
// When true, Settings exposes the Debug screen for log viewing/sharing.
Expand Down Expand Up @@ -366,6 +369,21 @@ public static Context getMainContext() {
public static int alcTargetLow = 60; // ALC target window low (0-255)
public static int alcTargetHigh = 100; // ALC target window high (0-255)

// Meters HUD: which meters the pull-down HUD shows. The HUD adapts per rig —
// only meters the connected rig actually reports are shown — and these flags
// gate which of the available ones the user wants. SWR + ALC are the two
// universal across CAT rigs and default on; the richer meters (only some
// rigs, e.g. FlexRadio/Xiegu network, report them) default off.
public static boolean meterShowSwr = true;
public static boolean meterShowAlc = true;
// Power defaults on: only the network rigs (Flex/Xiegu) report output watts,
// and on those it's the natural companion to the in-HUD TX-power control.
// Serial rigs don't report it, so "adapt per rig" hides it there anyway.
public static boolean meterShowPower = true;
public static boolean meterShowSMeter = false;
public static boolean meterShowVoltage = false;
public static boolean meterShowTemp = false;

public static MutableLiveData<Float> mutableBaseFrequency = new MutableLiveData<>();

private static int spectrumWidth = 3500;//Spectrum display width in Hz
Expand Down
81 changes: 77 additions & 4 deletions ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,10 @@ public void afterDecode(long utc, float time_sec, int sequential


getQTHRunnable.messages = messages;
getQTHThreadPool.execute(getQTHRunnable);//query location via thread pool
//query location via thread pool; guarded so a decode landing during
//ViewModel teardown (pool shut down in onCleared()) drops the lookup
//instead of crashing with RejectedExecutionException.
ExecutorUtils.safeExecute(getQTHThreadPool, getQTHRunnable);

//this variable also notifies message list changes
mutable_Decoded_Counter.postValue(
Expand Down Expand Up @@ -685,8 +688,8 @@ public void onTransmitByWifi(Ft8Message msg) {
if (baseRig.isConnected()) {
sendWaveDataRunnable.baseRig = baseRig;
sendWaveDataRunnable.message = msg;
//send network data packets via thread pool
sendWaveDataThreadPool.execute(sendWaveDataRunnable);
//send network data packets via thread pool (guarded against teardown race)
ExecutorUtils.safeExecute(sendWaveDataThreadPool, sendWaveDataRunnable);
}
}
}
Expand Down Expand Up @@ -714,7 +717,7 @@ public void onTransmitOverCAT(Ft8Message msg) {//send audio message via CAT
}
sendWaveDataRunnable.baseRig = baseRig;
sendWaveDataRunnable.message = msg;
sendWaveDataThreadPool.execute(sendWaveDataRunnable);
ExecutorUtils.safeExecute(sendWaveDataThreadPool, sendWaveDataRunnable);
}

}, new OnTransmitSuccess() {//when QSO is successful
Expand Down Expand Up @@ -1274,7 +1277,32 @@ public void run() {
* @param context context
* @param flexRadio FlexRadio object
*/
// IP of a Flex connect currently in flight (set when a connect starts, cleared
// when it succeeds or fails), so two near-simultaneous connect requests to the
// same radio don't both run the connect sequence. See the guard below.
private volatile String flexConnectInProgressIp = null;

public void connectFlexRadioRig(Context context, FlexRadio flexRadio) {
final String requestedIp = (flexRadio != null) ? flexRadio.getIp() : null;
// Guard against a DUPLICATE connect to the same Flex. Two UI paths can each
// fire a connect — the network picker's auto-connect and the CAT chip / a
// manual tap — and a second connect re-runs `client disconnect` + `slice
// create`, which spawns a SECOND slice on the radio (duplicate LEVEL/ALC
// meters) and tears down the GUI client whose meter subscription is live, so
// meters stream briefly then die. If we're already connected to this IP, or a
// connect to it is already in flight, make this call a no-op.
if (requestedIp != null && !requestedIp.isEmpty()) {
if (requestedIp.equals(flexConnectInProgressIp)) {
return;
}
if (baseRig != null && baseRig.getConnector() instanceof FlexConnector
&& baseRig.getConnector().isConnected()
&& requestedIp.equals(((FlexConnector) baseRig.getConnector()).getFlexIp())) {
setCatConnectionState(CatConnectionState.CONNECTED);
return;
}
}
flexConnectInProgressIp = requestedIp;
if (GeneralVariables.connectMode == ConnectMode.NETWORK) {
if (baseRig != null) {
if (baseRig.getConnector() != null) {
Expand All @@ -1283,13 +1311,49 @@ public void connectFlexRadioRig(Context context, FlexRadio flexRadio) {
}
}
Comment on lines 1306 to 1312
GeneralVariables.controlMode = ControlMode.CAT;//network control mode
// A FlexRadio always uses network (DAX) audio — force NETWORK connect mode
// so TX and RX audio route over the network. Otherwise a stale connectMode
// (e.g. left as USB_CABLE from a previously-attached USB sound card) sends
// TX audio to that USB output instead of the Flex (radio transmits 0 W) and
// pulls RX from the phone mic instead of DAX. Also clear any stale USB-audio
// output override for the same reason.
GeneralVariables.connectMode = ConnectMode.NETWORK;
databaseOpr.writeConfig("connectMode", String.valueOf(ConnectMode.NETWORK), null);
GeneralVariables.usbAudioOutputVendorId = 0;
GeneralVariables.usbAudioOutputProductId = 0;
// Remember the address so the CAT chip can reconnect on a cold start
// without re-opening the picker.
if (flexRadio != null && flexRadio.getIp() != null && !flexRadio.getIp().isEmpty()) {
GeneralVariables.flexLastIp = flexRadio.getIp();
databaseOpr.writeConfig("flexLastIp", flexRadio.getIp(), null);
}
// Reflect the in-progress attempt on the CAT chip immediately; the Flex
// connect is async (TCP), so without this the chip looks idle until/unless
// the link comes up.
setCatConnectionState(CatConnectionState.CONNECTING);
FlexConnector flexConnector = new FlexConnector(context, flexRadio, GeneralVariables.controlMode);
flexConnector.setOnWaveDataReceived(new FlexConnector.OnWaveDataReceived() {
@Override
public void OnDataReceived(int bufferLen, float[] buffer) {
hamRecorder.doOnWaveDataReceived(bufferLen, buffer);
}
});
// Surface success/failure: the Flex path doesn't fire the BaseRig
// onConnected() callback, so without this the CAT chip never turns
// connected and there's no "connected" confirmation toast.
flexConnector.setOnConnectionResult(new FlexConnector.OnConnectionResult() {
@Override
public void onConnected() {
flexConnectInProgressIp = null;
setCatConnectionState(CatConnectionState.CONNECTED);
ToastMessage.show(getStringFromResource(R.string.connected_rig));
}
@Override
public void onFailed() {
flexConnectInProgressIp = null;
setCatConnectionState(CatConnectionState.ERROR);
}
});
flexConnector.connect();
connectRig();

Expand Down Expand Up @@ -1527,6 +1591,15 @@ public void reconnectRig() {
if (baseRig == null || baseRig.getConnector() == null) {
return;
}
// Don't tear down a live or in-progress connection. Repeated CAT-chip taps would
// otherwise call connect() again on the Flex, opening a parallel TCP session whose
// "client gui" mints a new handle and orphans the slice/streams of the prior
// session — leaving the radio "connected" but owning nothing. Only reconnect when
// genuinely disconnected.
if (catConnectionState == CatConnectionState.CONNECTING
|| catConnectionState == CatConnectionState.CONNECTED) {
return;
}
setCatConnectionState(CatConnectionState.CONNECTING);
baseRig.getConnector().connect();
}
Expand Down
Loading
Loading