fix: accept Hamlib's VFO-prefixed commands — JTDX could not set the frequency

MSHV worked immediately, JTDX answered "Hamlib error: Invalid parameter while
setting frequency". The two use different Hamlib dialects: MSHV sends
"F 14074000", JTDX names the target first — "F VFOA 14074000". The VFO name
landed in the slot the frequency was read from, the parse failed, and we returned
RPRT -1, which is exactly the error JTDX reported.

A leading VFO name is now stripped from every command's arguments. It costs
nothing: OpsLog follows the rig's own VFO selection, so the name carries no
information we act on — but refusing it locked out a whole family of clients.
Both dialects are covered by a test, the plain one included, since this is an
addition and must not become a swap.

A rejected frequency is also logged with the raw line now. The client only shows
"Invalid parameter", which says nothing about what it actually sent — that is
why this took a screenshot to diagnose rather than a log.
This commit is contained in:
2026-07-29 10:58:16 +02:00
parent 38b480a985
commit 753d8d2ffa
3 changed files with 63 additions and 3 deletions
+27 -1
View File
@@ -179,7 +179,7 @@ func (s *Server) handle(line string) (resp string, quit bool) {
if len(fields) == 0 {
return "", false
}
cmd, args := fields[0], fields[1:]
cmd, args := fields[0], stripVFOArg(fields[1:])
switch cmd {
case "\\dump_state", "dump_state":
@@ -200,6 +200,10 @@ func (s *Server) handle(line string) (resp string, quit bool) {
}
hz, err := parseFreq(args[0])
if err != nil {
// Logged with the RAW line: a client that phrases a command in a
// dialect we don't accept shows only "Invalid parameter" on its side,
// which says nothing about what it actually sent.
s.log("rigctld: cannot read a frequency from %q — client dialect not handled", line)
return rprt(-1), false
}
if err := s.rig.SetFreq(hz); err != nil {
@@ -275,6 +279,28 @@ func (s *Server) handle(line string) (resp string, quit bool) {
func rprt(code int) string { return fmt.Sprintf("RPRT %d\n", code) }
// stripVFOArg drops a leading VFO name from a command's arguments.
//
// Hamlib has two dialects. In the plain one a client sends "F 14074000"; in VFO
// mode it names the target first — "F VFOA 14074000". MSHV uses the first and
// worked immediately; JTDX uses the second, so the frequency landed in the
// argument slot where a VFO was expected, the parse failed, and JTDX showed
// "Hamlib error: Invalid parameter while setting frequency" (our RPRT -1).
//
// Accepting both costs nothing here: OpsLog follows the rig's own VFO
// selection, so the name carries no information we act on — dropping it is not
// losing anything, and refusing it locks out a whole family of clients.
func stripVFOArg(args []string) []string {
if len(args) == 0 {
return args
}
switch strings.ToUpper(args[0]) {
case "VFOA", "VFOB", "VFOC", "VFO", "CURRVFO", "CURR", "MAIN", "SUB", "MEM", "A", "B":
return args[1:]
}
return args
}
// parseFreq accepts both the integer Hz and the "14074000.000000" form clients
// send interchangeably.
func parseFreq(s string) (int64, error) {
+34
View File
@@ -93,6 +93,40 @@ func TestHandleCommands(t *testing.T) {
}
}
// Hamlib's VFO dialect. JTDX names the target VFO before the value — "F VFOA
// 14074000" — where MSHV sends "F 14074000". Reading the VFO name as the
// frequency is what produced "Hamlib error: Invalid parameter while setting
// frequency" on JTDX while MSHV worked perfectly.
func TestHandleAcceptsVFOPrefixedCommands(t *testing.T) {
rig := &fakeRig{freq: 7074000, mode: "SSB"}
s := New(0, rig, nil)
if got, _ := s.handle("F VFOA 14074000"); got != "RPRT 0\n" {
t.Fatalf("handle(\"F VFOA 14074000\") = %q, want RPRT 0", got)
}
if got := rig.Freq(); got != 14074000 {
t.Errorf("frequency = %d, want 14074000 — the VFO name swallowed the value", got)
}
if got, _ := s.handle("M VFOA USB 2400"); got != "RPRT 0\n" {
t.Errorf("handle(\"M VFOA USB 2400\") = %q, want RPRT 0", got)
}
if got, _ := s.handle("T VFOA 1"); got != "RPRT 0\n" {
t.Errorf("handle(\"T VFOA 1\") = %q, want RPRT 0", got)
}
// A read with the VFO named must still answer the value, not an error.
if got, _ := s.handle("f VFOA"); got != "14074000\n" {
t.Errorf("handle(\"f VFOA\") = %q, want the frequency", got)
}
// And the plain dialect must keep working — this is an ADDITION, not a swap.
if got, _ := s.handle("F 21074000"); got != "RPRT 0\n" {
t.Errorf("plain set_freq broke: %q", got)
}
// "S 1 VFOB" starts with the split flag, not a VFO: nothing must be eaten.
if got, _ := s.handle("S 1 VFOB"); got != "RPRT 0\n" {
t.Errorf("handle(\"S 1 VFOB\") = %q, want RPRT 0", got)
}
}
// A rig that refuses must produce an error report, not a success — a client told
// "RPRT 0" believes the radio moved and will log the wrong frequency.
func TestHandleReportsBackendFailure(t *testing.T) {