fix: split read (and set) backwards on a Yaesu reporting the TX VFO

Reported on an FTDX101: the panel showed split ON with the radio OFF, and the
reverse — while the same model behaved correctly for another operator, and an
FTDX10 was fine throughout. That pattern is the clue: one operator was on MAIN,
the other on SUB.

The two commands say different things and were read alike:

  ST is a split FLAG   — ST1 means split, whatever VFO is in use.
  FT names the TX VFO  — FT0 = transmit on A, FT1 = transmit on B.

Split is on when the rig transmits on a DIFFERENT VFO from the one it listens to.
Reading FT1 as "split" is therefore correct only on VFO A, and exactly inverted
on SUB. It is now compared with the current VFO.

Writing had the identical fault, and it was worse: sending FT1 for "split on"
while the operator listened on SUB CLEARED the split it was asked to set. Both
directions are pinned by tests over each VFO state.
This commit is contained in:
2026-07-29 16:20:25 +02:00
parent c297f91ca8
commit a3fd32ec91
4 changed files with 124 additions and 26 deletions
+4 -2
View File
@@ -3,10 +3,12 @@
"version": "0.22.1", "version": "0.22.1",
"date": "2026-07-29", "date": "2026-07-29",
"en": [ "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." "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."
], ],
"fr": [ "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." "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."
] ]
}, },
{ {
+23 -8
View File
@@ -223,7 +223,7 @@ func (y *Yaesu) ReadState() (RigState, error) {
split := false split := false
if y.splitCmd != "" { if y.splitCmd != "" {
if r, err := y.ask(y.splitCmd + ";"); err == nil { if r, err := y.ask(y.splitCmd + ";"); err == nil {
split = yaesuSplitOn(r, y.splitCmd) split = yaesuSplitFromReply(r, y.splitCmd, vfo)
} }
} }
@@ -396,18 +396,33 @@ func parseYaesuFreq(reply, prefix string) (int64, bool) {
return hz, true return hz, true
} }
// yaesuSplitOn reads the split reply for whichever command the rig answers. // yaesuSplitFromReply turns the split reply into a yes or no.
// //
// ST is a split flag: ST1 means split. FT names the TX VFO: FT1 means transmit // The two commands say DIFFERENT things and reading them alike is a real fault,
// on VFO B, which IS split when the operator is listening on A. The two are not // reported on an FTDX101 (F4NBZ, 2026-07-29) where the panel showed split ON with
// the same statement, which is why the command in use is remembered rather than // the radio OFF and the reverse:
// both being tried and merged. //
func yaesuSplitOn(reply, cmd string) bool { // ST is a split FLAG — ST1 means split, whatever VFO is in use.
// FT names the TX VFO — FT0 = transmit on A, FT1 = transmit on B.
//
// Split is on when the rig TRANSMITS on a different VFO from the one it is
// LISTENING to. Reading FT1 as "split" is therefore only right for an operator
// on VFO A: on SUB it is exactly inverted, which is why the same model behaved
// correctly for one operator and backwards for another — one was on MAIN, the
// other on SUB.
func yaesuSplitFromReply(reply, cmd, vfo string) bool {
r := strings.TrimSpace(reply) r := strings.TrimSpace(reply)
if !strings.HasPrefix(r, cmd) || len(r) < len(cmd)+1 { if !strings.HasPrefix(r, cmd) || len(r) < len(cmd)+1 {
return false return false
} }
return r[len(cmd)] == '1' d := r[len(cmd)]
if cmd == "ST" {
return d == '1'
}
// FT: compare the transmit VFO with the one being listened to.
txOnB := d == '1'
rxOnB := strings.HasPrefix(strings.ToUpper(vfo), "B")
return txOnB != rxOnB
} }
// resolveYaesuVFOs turns the two frequencies plus the VFO and split flags into // resolveYaesuVFOs turns the two frequencies plus the VFO and split flags into
+28 -3
View File
@@ -397,12 +397,12 @@ func (y *Yaesu) SetYaesuSplit(on bool) error {
return y.SetYaesuSplitOffset(y.defaultSplitOffset()) return y.SetYaesuSplitOffset(y.defaultSplitOffset())
} }
y.mu.Lock() y.mu.Lock()
cmd := y.splitCmd cmd, vfo := y.splitCmd, y.curVFO
y.mu.Unlock() y.mu.Unlock()
if cmd == "" { if cmd == "" {
return fmt.Errorf("yaesu: this rig answered neither ST; nor FT; — split cannot be set") return fmt.Errorf("yaesu: this rig answered neither ST; nor FT; — split cannot be set")
} }
return y.setAndRefresh(fmt.Sprintf("%s0;", cmd)) return y.setAndRefresh(yaesuSplitCommand(cmd, vfo, false))
} }
// defaultSplitOffset is what SPLIT means on this mode: up 1 kHz on CW and the // defaultSplitOffset is what SPLIT means on this mode: up 1 kHz on CW and the
@@ -662,7 +662,7 @@ func (y *Yaesu) SetYaesuSplitOffset(offsetHz int64) error {
return fmt.Errorf("yaesu: this rig answered neither ST; nor FT; — split cannot be set") return fmt.Errorf("yaesu: this rig answered neither ST; nor FT; — split cannot be set")
} }
time.Sleep(30 * time.Millisecond) time.Sleep(30 * time.Millisecond)
if err := y.write(fmt.Sprintf("%s1;", y.splitCmd)); err != nil { if err := y.write(yaesuSplitCommand(y.splitCmd, y.curVFO, true)); err != nil {
return err return err
} }
y.panel.Split = true y.panel.Split = true
@@ -808,3 +808,28 @@ func (m *meterPeak) update(sample int, now time.Time) int {
} }
return m.val return m.val
} }
// yaesuSplitCommand builds the command that turns split on or off.
//
// Writing is as asymmetric as reading. ST takes the state directly — ST1 is
// split on. FT sets which VFO TRANSMITS, so "split on" means transmit on the
// OTHER VFO from the one being listened to, and "split off" means transmit on
// the same one. Sending FT1 for "on" regardless is right only for an operator on
// VFO A; on SUB it would clear the split it was asked to set.
func yaesuSplitCommand(cmd, vfo string, on bool) string {
if cmd == "ST" {
if on {
return "ST1;"
}
return "ST0;"
}
onSub := strings.HasPrefix(strings.ToUpper(vfo), "B")
txOnB := onSub // split off = transmit where we listen
if on {
txOnB = !onSub
}
if txOnB {
return cmd + "1;"
}
return cmd + "0;"
}
+68 -12
View File
@@ -54,24 +54,46 @@ func TestResolveYaesuVFOs(t *testing.T) {
} }
} }
// ST and FT say different things and must not be read the same way — see the // ST and FT say different things, and reading them alike inverted the split
// comment on yaesuSplitOn. // display on an FTDX101 (F4NBZ, 2026-07-29): the panel showed split ON with the
// radio OFF and the reverse.
//
// ST is a split FLAG — ST1 means split, whatever VFO is in use.
// FT names the TX VFO — FT0 = transmit on A, FT1 = transmit on B.
//
// Split is on when the rig transmits on a DIFFERENT VFO from the one it listens
// to, so FT has to be compared with the current VFO. That is why the same model
// behaved correctly for one operator and backwards for another: one was on MAIN,
// the other on SUB.
func TestYaesuSplitReply(t *testing.T) { func TestYaesuSplitReply(t *testing.T) {
cases := []struct { cases := []struct {
reply, cmd string reply, cmd, vfo string
want bool want bool
}{ }{
{"ST1;", "ST", true}, // The flag is absolute.
{"ST0;", "ST", false}, {"ST1;", "ST", "A", true},
{"FT1;", "FT", true}, {"ST0;", "ST", "A", false},
{"FT0;", "FT", false}, {"ST1;", "ST", "B", true},
{"ST1;", "FT", false}, // reply for the other command — not accepted {"ST0;", "ST", "B", false},
{"ST", "ST", false}, // truncated
{"", "ST", false}, // Listening on A: transmit on B is split, transmit on A is not.
{"FT1;", "FT", "A", true},
{"FT0;", "FT", "A", false},
// Listening on B: exactly the opposite — the reported inversion.
{"FT0;", "FT", "B", true},
{"FT1;", "FT", "B", false},
// The pair enums the Yaesus also report start with the listening VFO.
{"FT0;", "FT", "BA", true},
{"FT1;", "FT", "AB", true},
{"ST1;", "FT", "A", false}, // reply for the other command — not accepted
{"ST", "ST", "A", false}, // truncated
{"", "ST", "A", false},
} }
for _, c := range cases { for _, c := range cases {
if got := yaesuSplitOn(c.reply, c.cmd); got != c.want { if got := yaesuSplitFromReply(c.reply, c.cmd, c.vfo); got != c.want {
t.Errorf("yaesuSplitOn(%q,%q) = %v, want %v", c.reply, c.cmd, got, c.want) t.Errorf("yaesuSplitFromReply(%q, %q, vfo=%q) = %v, want %v",
c.reply, c.cmd, c.vfo, got, c.want)
} }
} }
} }
@@ -130,3 +152,37 @@ func TestYaesuCmdPrefix(t *testing.T) {
} }
} }
} }
// Writing split is as asymmetric as reading it.
//
// ST takes the state directly. FT sets which VFO TRANSMITS, so "split on" means
// transmit on the OTHER VFO from the one being listened to. Sending FT1 for "on"
// regardless is right only on VFO A — on SUB it would CLEAR the split it was
// asked to set, which is the same inversion that showed on the FTDX101 panel.
func TestYaesuSplitCommand(t *testing.T) {
cases := []struct {
cmd, vfo string
on bool
want string
}{
{"ST", "A", true, "ST1;"},
{"ST", "B", true, "ST1;"}, // the flag does not care which VFO
{"ST", "B", false, "ST0;"},
// Listening on A: split means transmit on B.
{"FT", "A", true, "FT1;"},
{"FT", "A", false, "FT0;"},
// Listening on B: split means transmit on A — the reverse.
{"FT", "B", true, "FT0;"},
{"FT", "B", false, "FT1;"},
// Pair enums start with the listening VFO.
{"FT", "BA", true, "FT0;"},
{"FT", "AB", true, "FT1;"},
}
for _, c := range cases {
if got := yaesuSplitCommand(c.cmd, c.vfo, c.on); got != c.want {
t.Errorf("yaesuSplitCommand(%q, vfo=%q, on=%v) = %q, want %q",
c.cmd, c.vfo, c.on, got, c.want)
}
}
}