feat: amplifier band-follow — answer the amp's frequency polls

On its CAT/AUX connector an ACOM is the MASTER: it polls a transceiver and
changes band from the reply, so nothing can be pushed to it. internal/catemu
answers those polls on a second serial port, in ACOM command set 5 (Kenwood /
Elecraft): FA;, FB;, IF; and ID;, and nothing else — a wrong-length reply is
worse than none, it desynchronises the amp's parser for the following poll.

Also offered for SPE. Some amps do not poll at all but read the radio↔PC CAT
line in parallel; those never hear a responder, so an optional unprompted send
(500/1000 ms) covers them.

The TX frequency is what is sent: in split the amp must be tuned where we
transmit. Frame lengths are pinned by tests — the failure they guard against is
silent and only shows up as an amp that mistunes.

Untested on hardware.
This commit is contained in:
2026-07-27 11:56:30 +02:00
parent 05d64024ef
commit 5394b55bb7
8 changed files with 573 additions and 7 deletions
+45
View File
@@ -0,0 +1,45 @@
package catemu
import "testing"
// The reply LENGTHS are the contract: an amplifier parses these frames by
// fixed offsets, so a frame one character short desynchronises its parser for
// every following poll. Pin them.
func TestReplyShapes(t *testing.T) {
s := New(Config{ComPort: "COM99", Baud: 9600}, nil)
s.SetFrequency(14025000)
s.SetMode("CW")
if got := s.ifFrame(14025000); len(got) != 38 {
t.Errorf("IF frame is %d chars, want 38: %q", len(got), got)
}
// TS-2000 IF: "IF" then the 11-digit frequency in Hz.
if got := s.ifFrame(14025000); got[:13] != "IF00014025000" {
t.Errorf("IF frame frequency field = %q", got[:13])
}
if got := s.ifFrame(14025000); got[len(got)-1] != ';' {
t.Errorf("IF frame not terminated by ';': %q", got)
}
// Mode digit sits at P9, right after freq(11)+step(4)+rit(6)+3+2+1 — index 29 in the frame.
if got := s.ifFrame(14025000)[29]; got != '3' {
t.Errorf("CW mode digit = %q, want '3'", got)
}
s.SetMode("FT8") // data rides on SSB as far as an amp cares
if got := s.ifFrame(14074000)[29]; got != '2' {
t.Errorf("FT8 mode digit = %q, want '2'", got)
}
}
func TestModeDigit(t *testing.T) {
cases := map[string]byte{
"CW": '3', "CW-R": '3', "LSB": '1', "USB": '2', "SSB": '2',
"FM": '4', "AM": '5', "RTTY": '6', "": '2', "FT8": '2',
}
for mode, want := range cases {
s := New(Config{}, nil)
s.SetMode(mode)
if got := s.modeDigit(); got != want {
t.Errorf("mode %q → %q, want %q", mode, got, want)
}
}
}