fix: Yaesu showed MAIN when the operator had moved RX and TX to SUB

The operator's description separates the two cases precisely, and that is what
identifies the fault. RX alone on SUB displayed VFO B correctly; RX AND TX on
SUB — entirely on the sub receiver — displayed VFO A.

VS was being read as "which VFO is in use". On an FTDX101 it does not answer that
question: with both RX and TX on sub it still reported the main VFO. FR is the
command that selects the RECEIVE VFO, and it is now asked first, VS remaining the
fallback for models that lack it.

Split falls out of the same correction: split means the TRANSMIT VFO differs from
the RECEIVE one, so a wrong receive VFO made the comparison wrong too — which is
the second half of what was reported. A test covers all four RX/TX combinations,
including the one that is NOT split (both on sub) and reads as split if you take
the transmit VFO alone.
This commit is contained in:
2026-07-29 17:26:00 +02:00
parent 35db1440e4
commit ccceab9d02
3 changed files with 71 additions and 7 deletions
+4 -2
View File
@@ -5,12 +5,14 @@
"en": [
"Clicking a cluster spot while listening on the SUB VFO no longer pulls the radio back to MAIN. The command used to tune sets the main VFO by definition; on SUB the sub VFO is now written directly and the VFO selection is left alone.",
"Split showed the opposite of the radio on some Yaesus. Where the rig reports which VFO transmits rather than a split flag, that has to be compared with the VFO you are listening to — so it read backwards for an operator working on SUB and correctly for one on MAIN. Setting split had the same fault.",
"A shared MySQL logbook could fail to build its schema, leaving OpsLog on the local SQLite database with a message that only said the connection failed. Column declarations that are quoted, or share a line with others, escaped the SQLite-to-MySQL type conversion — so MySQL refused an index and a default value. A database already left in that state is now repaired at startup instead of failing at every launch."
"A shared MySQL logbook could fail to build its schema, leaving OpsLog on the local SQLite database with a message that only said the connection failed. Column declarations that are quoted, or share a line with others, escaped the SQLite-to-MySQL type conversion — so MySQL refused an index and a default value. A database already left in that state is now repaired at startup instead of failing at every launch.",
"Yaesu: moving both RX and TX to the SUB receiver showed the MAIN frequency. The receive VFO is now read from the command that actually reports it, which also puts split right — split means the transmit VFO differs from the receive one."
],
"fr": [
"Cliquer sur un spot du cluster en écoutant sur le VFO SUB ne ramène plus la radio sur le VFO principal. La commande utilisée pour s'accorder agit par définition sur le VFO principal ; sur SUB, c'est désormais le VFO secondaire qui est écrit, sans toucher à la sélection.",
"Le split affichait l'inverse de la radio sur certains Yaesu. Quand la radio indique quel VFO émet plutôt qu'un indicateur de split, il faut le comparer au VFO écouté — l'affichage était donc inversé pour un opérateur travaillant sur SUB et correct pour un autre sur MAIN. Le réglage du split souffrait du même défaut.",
"Un carnet MySQL partagé pouvait échouer à créer son schéma, laissant OpsLog sur la base SQLite locale avec un message indiquant seulement l'échec de la connexion. Les déclarations de colonnes entre guillemets, ou partageant une ligne avec d'autres, échappaient à la conversion de types SQLite vers MySQL — d'où le refus d'un index et d'une valeur par défaut. Une base déjà dans cet état est désormais réparée au démarrage au lieu d'échouer à chaque lancement."
"Un carnet MySQL partagé pouvait échouer à créer son schéma, laissant OpsLog sur la base SQLite locale avec un message indiquant seulement l'échec de la connexion. Les déclarations de colonnes entre guillemets, ou partageant une ligne avec d'autres, échappaient à la conversion de types SQLite vers MySQL — d'où le refus d'un index et d'une valeur par défaut. Une base déjà dans cet état est désormais réparée au démarrage au lieu d'échouer à chaque lancement.",
"Yaesu : passer RX et TX sur le récepteur SUB affichait la fréquence du MAIN. Le VFO de réception est désormais lu sur la commande qui l'indique réellement, ce qui corrige aussi le split — être en split, c'est émettre sur un VFO différent de celui qu'on écoute."
]
},
{
+31 -5
View File
@@ -18,7 +18,8 @@ package cat
// FA; → FA014074000; VFO A frequency, 9 digits, Hz
// FB; → FB014100000; VFO B frequency
// MD0; → MD02; operating mode of the main receiver
// VS; → VS0; which VFO is selected (0=A, 1=B)
// FR; → FR1; RECEIVE VFO (0=main/A, 1=sub/B)
// VS; → VS0; selected VFO, on models without FR
// ST; → ST1; split (FTDX10/FTDX101)
// FT; → FT1; TX VFO (FT-991A/FT-710/FT-891 family)
// TX1; / TX0; key / unkey
@@ -93,6 +94,9 @@ type Yaesu struct {
// answers. Empty means the rig answered neither, and split is reported as
// off rather than invented.
splitCmd string
// rxVFOCmd is "FR" when the rig reports its receive VFO that way, else empty
// and VS is used — see ReadState.
rxVFOCmd string
curFreq int64
curRXFreq int64
@@ -159,6 +163,16 @@ func (y *Yaesu) Connect() error {
// remembering the answer keeps the poll loop from paying for two round trips
// per cycle, and makes "neither answered" an explicit, logged state instead
// of a silent assumption that split is off.
// Which command reports the RECEIVE VFO. FR is the right one where it exists;
// VS is a weaker substitute that an FTDX101 answers with the main VFO even
// when the operator has moved both RX and TX to sub.
if r, err := y.ask("FR;"); err == nil && strings.HasPrefix(r, "FR") {
y.rxVFOCmd = "FR"
debugLog.Printf("yaesu: receive VFO is read through FR (answered %q)", r)
} else {
debugLog.Printf("yaesu: no FR; — falling back to VS for the receive VFO")
}
for _, c := range []string{"ST", "FT"} {
if r, err := y.ask(c + ";"); err == nil && strings.HasPrefix(r, c) {
y.splitCmd = c
@@ -212,11 +226,23 @@ func (y *Yaesu) ReadState() (RigState, error) {
freqB, _ = parseYaesuFreq(r, "FB")
}
// Which VFO the operator is listening on. Unlike OmniRig there is no
// interpretation to do: VS answers 0 or 1.
// Which VFO the operator is LISTENING on.
//
// FR is the command that answers that — it selects the receive VFO — and VS
// does not: on an FTDX101 with both RX and TX moved to SUB, VS still reported
// the main VFO, so OpsLog displayed VFO A while the operator was entirely on
// B. Reported 2026-07-29. FR is asked first and VS is the fallback for models
// that do not implement it.
vfo := "A"
if r, err := y.ask("VS;"); err == nil && len(r) >= 3 && r[2] == '1' {
vfo = "B"
switch {
case y.rxVFOCmd != "":
if r, err := y.ask(y.rxVFOCmd + ";"); err == nil && len(r) >= len(y.rxVFOCmd)+1 && r[len(y.rxVFOCmd)] == '1' {
vfo = "B"
}
default:
if r, err := y.ask("VS;"); err == nil && len(r) >= 3 && r[2] == '1' {
vfo = "B"
}
}
y.curVFO = vfo
+36
View File
@@ -186,3 +186,39 @@ func TestYaesuSplitCommand(t *testing.T) {
}
}
}
// Which VFO the operator is listening on, on a rig with separate RX and TX
// selection.
//
// Reported on an FTDX101 (2026-07-29): moving RX alone to SUB displayed VFO B
// correctly, but moving BOTH RX and TX to SUB displayed VFO A — the operator was
// entirely on B and OpsLog showed the other one. FR reports the receive VFO; VS
// does not answer that question on this rig.
//
// With FR read correctly the split follows too, since split is "transmit VFO
// differs from receive VFO".
func TestYaesuReceiveVFOAndSplit(t *testing.T) {
cases := []struct {
name string
fr, ft string // replies
wantVFO string
wantSplit bool
}{
{"everything on main", "FR0;", "FT0;", "A", false},
{"RX on sub, TX still on main — split", "FR1;", "FT0;", "B", true},
{"RX and TX both on sub — NOT split", "FR1;", "FT1;", "B", false},
{"RX on main, TX on sub — split", "FR0;", "FT1;", "A", true},
}
for _, c := range cases {
vfo := "A"
if len(c.fr) >= 3 && c.fr[2] == '1' {
vfo = "B"
}
if vfo != c.wantVFO {
t.Errorf("%s: receive VFO = %s, want %s", c.name, vfo, c.wantVFO)
}
if got := yaesuSplitFromReply(c.ft, "FT", vfo); got != c.wantSplit {
t.Errorf("%s: split = %v, want %v", c.name, got, c.wantSplit)
}
}
}