feat: native Yaesu CAT backend (FTDX10 / FTDX101), no OmniRig

Every Yaesu fault reported so far came from OmniRig's interpretation layer, not
from the radio: a rig file that never exposes the VFO, a Freq property meaning A
on one model and B on another, a split flag that alternates between polls. This
talks to the rig directly, so what the radio answers is what is shown.

Modern Yaesu CAT is plain ASCII with a ';' terminator — FA/FB for the VFOs, MD0
for the mode, VS for the selected VFO, TX to key. Frequency is written to the VFO
the operator is actually on, not always to A, which is the failure that made a
display disagree with the radio.

Two things are genuinely uncertain across the family and are treated as such
rather than guessed. SPLIT is read through ST, then FT if the rig ignores ST —
whichever answers wins and the choice is remembered, because the two commands say
DIFFERENT things (a split flag vs which VFO transmits). If neither answers, split
is reported OFF and the fact is logged, rather than invented. Unknown model ids
and mode bytes are logged raw for the same reason.

Split resolution, frequency parsing and the mode mapping are pure functions with
a table test — the OmniRig equivalent is where every Yaesu bug lived, and it had
no test until late.

Written from the CAT reference; NOT yet verified on a radio.
This commit is contained in:
2026-07-29 10:33:26 +02:00
parent 9cfa7a4dd1
commit 67005a8d50
7 changed files with 553 additions and 3 deletions
+21 -1
View File
@@ -108,6 +108,8 @@ const (
keyCATPollMs = "cat.poll_ms"
keyCATDelayMs = "cat.delay_ms" // pause between commands
keyCATDigitalDefault = "cat.digital_default" // mode to use when CAT reports DATA
keyCATYaesuPort = "cat.yaesu.port" // Yaesu CAT serial port (e.g. COM4)
keyCATYaesuBaud = "cat.yaesu.baud" // Yaesu CAT baud (FTDX10/101 default 38400)
keyCATIcomPort = "cat.icom.port" // Icom USB CI-V serial port (e.g. COM5)
keyCATIcomBaud = "cat.icom.baud" // Icom CI-V baud (default 115200)
keyCATIcomAddr = "cat.icom.addr" // Icom CI-V address, decimal (IC-7610 = 152 / 0x98)
@@ -342,6 +344,8 @@ type CATSettings struct {
FlexSpots bool `json:"flex_spots"` // push cluster spots to the panadapter
FlexDecodeSpots bool `json:"flex_decode_spots"` // push WSJT-X decodes (heard stations) to the panadapter
FlexDecodeSecs int `json:"flex_decode_secs"` // decode spot display duration (s) before removal (default 120)
YaesuPort string `json:"yaesu_port"` // Yaesu CAT serial port (e.g. COM4)
YaesuBaud int `json:"yaesu_baud"` // Yaesu CAT baud (FTDX10/101 default 38400)
IcomPort string `json:"icom_port"` // Icom USB CI-V serial port (e.g. COM5)
IcomBaud int `json:"icom_baud"` // Icom CI-V baud (default 115200)
IcomAddr int `json:"icom_addr"` // Icom CI-V address, decimal (IC-7610 = 152)
@@ -6562,7 +6566,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
if a.settings == nil {
return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
}
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault)
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATOmniRigVFO, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATFlexDecodeSpots, keyCATFlexDecodeSecs, keyCATYaesuPort, keyCATYaesuBaud, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATIcomNetHost, keyCATIcomNetUser, keyCATIcomNetPass, keyCATIcomNetAudio, keyCATTCIHost, keyCATTCIPort, keyCATTCISpots, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault)
if err != nil {
return CATSettings{}, err
}
@@ -6575,6 +6579,8 @@ func (a *App) GetCATSettings() (CATSettings, error) {
FlexSpots: m[keyCATFlexSpots] == "1",
FlexDecodeSpots: m[keyCATFlexDecodeSpots] == "1",
FlexDecodeSecs: 120,
YaesuPort: m[keyCATYaesuPort],
YaesuBaud: 38400,
IcomPort: m[keyCATIcomPort],
IcomBaud: 115200,
IcomAddr: 0x98, // IC-7610 default
@@ -6598,6 +6604,9 @@ func (a *App) GetCATSettings() (CATSettings, error) {
if n, _ := strconv.Atoi(m[keyCATTCIPort]); n > 0 && n <= 65535 {
out.TCIPort = n
}
if n, _ := strconv.Atoi(m[keyCATYaesuBaud]); n > 0 {
out.YaesuBaud = n
}
if n, _ := strconv.Atoi(m[keyCATIcomBaud]); n > 0 {
out.IcomBaud = n
}
@@ -6639,6 +6648,9 @@ func (a *App) SaveCATSettings(s CATSettings) error {
if s.FlexPort <= 0 || s.FlexPort > 65535 {
s.FlexPort = 4992
}
if s.YaesuBaud <= 0 {
s.YaesuBaud = 38400
}
if s.IcomBaud <= 0 {
s.IcomBaud = 115200
}
@@ -6687,6 +6699,8 @@ func (a *App) SaveCATSettings(s CATSettings) error {
keyCATFlexSpots: flexSpots,
keyCATFlexDecodeSpots: flexDecodeSpots,
keyCATFlexDecodeSecs: strconv.Itoa(s.FlexDecodeSecs),
keyCATYaesuPort: strings.TrimSpace(s.YaesuPort),
keyCATYaesuBaud: strconv.Itoa(s.YaesuBaud),
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
keyCATIcomBaud: strconv.Itoa(s.IcomBaud),
keyCATIcomAddr: strconv.Itoa(s.IcomAddr),
@@ -12059,6 +12073,12 @@ func (a *App) reloadCAT() {
}
}
a.cat.Start(fb)
case "yaesu":
// Native Yaesu CAT over the rig's USB/serial port — no OmniRig. Every
// Yaesu fault reported so far came from OmniRig's interpretation layer
// (a rig file that hides the VFO, a Freq property meaning A on one model
// and B on another); talking to the radio directly removes it.
a.cat.Start(cat.NewYaesu(s.YaesuPort, s.YaesuBaud, s.DigitalDefault))
case "icom":
// Native Icom CI-V over the radio's USB serial port (local control).
// Same civ protocol the network backend reuses for remote.