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
9 changes: 6 additions & 3 deletions ft8cn/app/src/main/java/com/bg7yoz/ft8cn/Ft8Message.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,16 @@ public String getMessageText() {
}

if (i3 == 0 && (n3 == 1)) {//this is DXpedition

// Fox combo: "<acked> RR73; <invited> <foxHash> <report>". The report
// is already signed, so format its magnitude after the sign char —
// otherwise a negative report renders a double minus ("--18"). A zero
// report formats as "+0" (>= 0), matching WSJT-X, not "-0".
return String.format("%s RR73; %s %s %s%d"
,callsignTo
,dx_call_to2
,hashList.getCallsign(new long[]{callFromHash10})
,report > 0 ? "+" : "-"
,report
,report >= 0 ? "+" : "-"
,Math.abs(report)
);
Comment thread
patrickrb marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,14 @@ public static String getMyMaidenheadGrid() {
return myMaidenheadGrid;
}

// ===== FT8 DXpedition "Hound" mode =====
// When true, the TX engine runs the Hound QSO variant (call Fox high at
// 1000-4000 Hz, auto-QSY down to where Fox calls us, reply R+rpt, log on
// RR73) instead of the standard auto-sequencer. Mutually exclusive with the
// Hunt auto-answer-CQ mode. houndFoxCall is the Fox's base callsign.
public static boolean houndMode = false;
public static String houndFoxCall = "";

public static float getBaseFrequency() {
return baseFrequency;
}
Expand Down
29 changes: 29 additions & 0 deletions ft8cn/app/src/main/java/com/bg7yoz/ft8cn/MainViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,35 @@ public void callStation(Ft8Message message) {
ft8TransmitSignal.transmitNow();
}

// ===== FT8 DXpedition Hound mode =====

/**
* Enter DXpedition Hound mode and start calling the Fox. Mutually exclusive
* with Hunt (auto-answer-CQ), which is disabled here. The rig should already
* be tuned to the Fox's published dial frequency.
*
* @param foxCall the Fox's base callsign (the DXpedition)
* @param callFreqHz initial Hound TX audio frequency, 1000-4000 Hz
*/
public void startHoundMode(String foxCall, float callFreqHz) {
if (foxCall == null || foxCall.trim().length() < 3) return;
if (GeneralVariables.myCallsign == null
|| GeneralVariables.myCallsign.length() < 3) return;
GeneralVariables.houndMode = true;
GeneralVariables.houndFoxCall = foxCall.trim().toUpperCase();
GeneralVariables.autoFollowCQ = false;// Hound and Hunt are mutually exclusive
GeneralVariables.resetLaunchSupervision();
ft8TransmitSignal.startHound(GeneralVariables.houndFoxCall, callFreqHz);
}

/** Leave Hound mode and return to the normal idle/CQ state. */
public void stopHoundMode() {
GeneralVariables.houndMode = false;
GeneralVariables.houndFoxCall = "";
ft8TransmitSignal.setActivated(false);
ft8TransmitSignal.resetToCQ();
}


/**
* Get the followed callsign list from the database
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import com.bg7yoz.ft8cn.FT8Common;
import com.bg7yoz.ft8cn.Ft8Message;
import com.bg7yoz.ft8cn.ft8signal.FT8Package;
import com.bg7yoz.ft8cn.GeneralVariables;
import com.bg7yoz.ft8cn.R;
import com.bg7yoz.ft8cn.connector.ConnectMode;
Expand Down Expand Up @@ -1065,6 +1066,10 @@ public void parseMessageToFunction(ArrayList<Ft8Message> msgList) {
}
ArrayList<Ft8Message> messages = new ArrayList<>(msgList);// prevent thread conflicts

if (GeneralVariables.houndMode) {// DXpedition Hound: dedicated QSO handler
handleHoundCycle(messages);
return;
}

int newOrder = checkFunctionOrdFromMessages(messages);// check reply message sequence from the other party; -1 means not received
// Per-cycle spine of the QSO trace: current state, what (if anything) the
Expand Down Expand Up @@ -1207,6 +1212,132 @@ public void parseMessageToFunction(ArrayList<Ft8Message> msgList) {

}

// ==================== FT8 DXpedition Hound ====================

/**
* Begin DXpedition Hound operation: call the Fox high (1000-4000 Hz) and let
* {@link #handleHoundCycle} auto-QSY and sequence the QSO. The caller sets
* {@code GeneralVariables.houndMode = true} first.
*
* @param foxCall the Fox's base callsign
* @param callFreqHz initial Hound TX audio frequency (1000-4000 Hz)
*/
public void startHound(String foxCall, float callFreqHz) {
int i3 = GenerateFT8.checkI3ByCallsign(GeneralVariables.myCallsign);
// Seed the Fox callsign's hashes. DXpedition combo messages carry the Fox
// only as a 10-bit hash, so without this the combo renders "<...>" in the
// UI and handleHoundCycle can't tell whose combo it is. (No-op for a
// compound Fox whose combo hashes its full call while the user enters the
// base call — handleHoundCycle degrades gracefully in that case.)
Ft8Message.hashList.addHash(FT8Package.getHash22(foxCall), foxCall);
Ft8Message.hashList.addHash(FT8Package.getHash12(foxCall), foxCall);
Ft8Message.hashList.addHash(FT8Package.getHash10(foxCall), foxCall);
// Fox transmits in the even/1st slot (sequential 0); setTransmit derives
// our slot as (fox.sequential + 1) % 2 = 1 (odd), which is where Hounds
// are required to transmit.
resetTargetReport();
setTransmit(new TransmitCallsign(i3, 0, foxCall, callFreqHz, 0, 0), 1, "");
setBaseFrequency(callFreqHz);// call Fox at the chosen high frequency
Comment thread
patrickrb marked this conversation as resolved.
setActivated(true);
GeneralVariables.fileLog("HOUND: start, fox=" + foxCall
+ " callFreq=" + Math.round(callFreqHz) + "Hz slot=" + sequential);
}

/**
* DXpedition Hound per-cycle handler. Reacts to the Fox's reply to us:
* <ul>
* <li>RR73 to me (combo where I'm the acknowledged call, or an explicit
* standard RR73 from Fox) -&gt; log + complete.</li>
* <li>Report/invite to me (combo where I'm the invited second call, or a
* standard "&lt;me&gt; &lt;fox&gt; -rpt") -&gt; QSY to the frequency Fox
* called me on and reply "&lt;fox&gt; &lt;me&gt; R-rpt".</li>
* <li>Otherwise keep calling at the current order (grid-call or R+rpt).</li>
* </ul>
*/
private void handleHoundCycle(ArrayList<Ft8Message> messages) {
if (toCallsign == null || !toCallsign.haveTargetCallsign()) return;
String fox = toCallsign.callsign;

for (int i = messages.size() - 1; i >= 0; i--) {
Ft8Message msg = messages.get(i);
if (msg.getSequence() == sequential) continue;// our own slot
if (msg.band != GeneralVariables.band) continue;

boolean combo = (msg.i3 == 0 && msg.n3 == 1);// Fox DXpedition combo
boolean toMe = GeneralVariables.checkIsMyCallsign(msg.getCallsignTo());

// A combo names the Fox only by a 10-bit hash. If that hash resolves
// to a known callsign that isn't our Fox, it belongs to a different
// Fox's pileup — skip it so we don't QSY/log against the wrong station.
if (combo && !comboFromOurFox(msg, fox)) continue;

// 1) RR73 to me => QSO complete & logged. Combo: I'm the acknowledged
// (first) call. Standard: an explicit RR73 addressed to me by Fox.
if ((combo && toMe)
|| (msg.i3 == 1 && toMe
&& checkCallsignIsCallTo(msg.getCallsignFrom(), fox)
&& GeneralVariables.checkFun4(msg.extraInfo))) {
GeneralVariables.fileLog("HOUND: RR73 from " + fox + " -> complete");
houndComplete();
return;
}

// 2) Fox reported / invited me => reply R+rpt at Fox's frequency.
// Combo: I'm the invited second call (dx_call_to2), report = msg.report.
// Standard: "<me> <fox> -rpt" or "<me> <fox> R-rpt".
String invited = msg.dx_call_to2 == null ? ""
: msg.dx_call_to2.replace("<", "").replace(">", "");
boolean invitedByCombo = combo && GeneralVariables.checkIsMyCallsign(invited);
Comment thread
patrickrb marked this conversation as resolved.
boolean reportStd = msg.i3 == 1 && toMe
&& checkCallsignIsCallTo(msg.getCallsignFrom(), fox)
&& (GeneralVariables.checkFun2(msg.extraInfo)
|| GeneralVariables.checkFun3(msg.extraInfo));
if (invitedByCombo || reportStd) {
int rpt = invitedByCombo ? msg.report : getReportFromExtraInfo(msg.extraInfo);
if (rpt != -100) {
receivedReport = rpt;
receiveTargetReport = rpt;
}
toCallsign.snr = msg.snr;// Fox's SNR as I hear it -> goes in my R+rpt
setBaseFrequency(msg.freq_hz);// auto-QSY to where Fox called me
Comment thread
patrickrb marked this conversation as resolved.
functionOrder = 3;// "<fox> <me> R-rpt"
generateFun();
setCurrentFunctionOrder(functionOrder);
mutableFunctionOrder.postValue(functionOrder);
GeneralVariables.fileLog(String.format(
"HOUND: report from %s rpt=%d -> QSY %.0fHz, send R%s",
fox, rpt, msg.freq_hz, toCallsign.getSnr()));
return;
}
}
// Nothing relevant this cycle: keep calling at the current order.
}

/** Hound QSO complete: log it, fire the celebration, and stop transmitting. */
private void houndComplete() {
if (toCallsign == null) return;
doComplete();// saves the QSL record + posts mutableQsoCompletedAt
setActivated(false);// worked the Fox; stop calling
GeneralVariables.fileLog("HOUND: QSO logged with " + toCallsign.callsign);
}

/**
* Whether a DXpedition combo can be attributed to our Fox. The combo names
* the Fox only by a 10-bit hash; if that hash resolves to a known callsign
* that doesn't match our Fox (base or compound), it's a different Fox's combo.
* Returns true when the hash is unknown/unresolved, so we never reject a valid
* QSO just because the Fox's (possibly compound) call hasn't been hashed yet.
*/
private boolean comboFromOurFox(Ft8Message msg, String fox) {
String resolved = Ft8Message.hashList
.getCallsign(new long[]{msg.callFromHash10})
.replace("<", "").replace(">", "");
if (resolved.isEmpty() || resolved.equals("...")) return true;// unknown -> allow
return resolved.equals(fox) || resolved.contains(fox) || fox.contains(resolved);
}

// ==================== End FT8 DXpedition Hound ====================

/**
* Check watch list for active CQ messages that are not my current target callsign.
*
Expand Down
42 changes: 41 additions & 1 deletion ft8cn/app/src/main/kotlin/radio/ks3ckc/ft8us/FT8USApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import radio.ks3ckc.ft8us.theme.BgApp
import radio.ks3ckc.ft8us.ui.components.ActiveQsoPanel
import radio.ks3ckc.ft8us.ui.components.FT8USTab
import radio.ks3ckc.ft8us.ui.components.FrequencyPickerSheet
import radio.ks3ckc.ft8us.ui.components.HoundSetupSheet
import radio.ks3ckc.ft8us.ui.components.formatMhz
import radio.ks3ckc.ft8us.ui.components.QsoCelebration
import radio.ks3ckc.ft8us.ui.components.SlotTimerBar
Expand Down Expand Up @@ -77,6 +78,11 @@ fun FT8USApp(mainViewModel: MainViewModel) {
// editable in Settings, which provides the persisted default at startup).
var huntEnabled by remember { mutableStateOf(GeneralVariables.autoFollowCQ) }

// DXpedition Hound mode. Mirrors GeneralVariables.houndMode; the setup sheet
// collects the Fox call + call frequency before starting.
var dxEnabled by remember { mutableStateOf(GeneralVariables.houndMode) }
var showHoundSetup by remember { mutableStateOf(false) }

// Frequency picker sheet state
var showFrequencyPicker by rememberSaveable { mutableStateOf(false) }

Expand Down Expand Up @@ -212,6 +218,7 @@ fun FT8USApp(mainViewModel: MainViewModel) {
frequencyLabel = frequencyLabel,
txSlot = txSlot,
huntEnabled = huntEnabled,
dxEnabled = dxEnabled,
expanded = qsoPanelExpanded,
onCallCQ = {
if (GeneralVariables.myCallsign.isNullOrEmpty()) {
Expand All @@ -223,7 +230,22 @@ fun FT8USApp(mainViewModel: MainViewModel) {
}
},
onStop = {
mainViewModel.ft8TransmitSignal.setActivated(false)
// In Hound mode the STOP button leaves Hound entirely;
// otherwise it just deactivates the normal sequencer.
if (GeneralVariables.houndMode) {
mainViewModel.stopHoundMode()
dxEnabled = false
} else {
mainViewModel.ft8TransmitSignal.setActivated(false)
}
},
onToggleDx = {
if (dxEnabled || GeneralVariables.houndMode) {
mainViewModel.stopHoundMode()
dxEnabled = false
} else {
showHoundSetup = true
}
},
onToggleSlot = {
val current = mainViewModel.ft8TransmitSignal.sequential
Expand Down Expand Up @@ -274,5 +296,23 @@ fun FT8USApp(mainViewModel: MainViewModel) {
showFrequencyPicker = false
},
)

// DXpedition Hound setup — collects the Fox call + call frequency, then
// starts calling (disabling Hunt, which is mutually exclusive).
HoundSetupSheet(
visible = showHoundSetup,
initialFoxCall = GeneralVariables.houndFoxCall,
onDismiss = { showHoundSetup = false },
onStart = { foxCall, callFreqHz ->
if (GeneralVariables.myCallsign.isNullOrEmpty()) {
Toast.makeText(context, context.getString(R.string.app_set_callsign_first), Toast.LENGTH_SHORT).show()
} else {
mainViewModel.startHoundMode(foxCall, callFreqHz)
dxEnabled = true
huntEnabled = false
showHoundSetup = false
}
},
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package radio.ks3ckc.ft8us.ui.components

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.text.KeyboardOptions

/**
* Setup dialog for FT8 DXpedition "Hound" mode. The user tunes the rig to the
* Fox's published dial frequency (via the normal frequency picker), then enters
* the Fox's base callsign here and an initial call frequency in the 1000-4000 Hz
* Hound band. On Start, the app begins calling the Fox and auto-QSYs down when
* answered.
*/
@Composable
fun HoundSetupSheet(
visible: Boolean,
initialFoxCall: String,
onDismiss: () -> Unit,
onStart: (foxCall: String, callFreqHz: Float) -> Unit,
) {
if (!visible) return

var foxCall by remember { mutableStateOf(initialFoxCall) }
var callFreq by remember { mutableStateOf("2500") }

AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Work a DXpedition (Hound)") },
text = {
Column {
Text(
"Tune the rig to the Fox's published dial frequency first. " +
"You call high (1000–4000 Hz); the app auto-QSYs you " +
"down to where the Fox answers and replies with your report.",
)
Spacer(Modifier.height(12.dp))
OutlinedTextField(
value = foxCall,
onValueChange = { foxCall = it.uppercase().trim() },
label = { Text("Fox callsign") },
singleLine = true,
)
Spacer(Modifier.height(8.dp))
OutlinedTextField(
value = callFreq,
onValueChange = { v -> callFreq = v.filter { it.isDigit() }.take(4) },
label = { Text("Call frequency (Hz, 1000–4000)") },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)
}
},
confirmButton = {
TextButton(
onClick = {
val f = (callFreq.toFloatOrNull() ?: 2500f).coerceIn(1000f, 4000f)
if (foxCall.trim().length >= 3) onStart(foxCall.trim(), f)
},
) { Text("Start") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
},
)
}
Loading
Loading