feat: share the CAT link with other programs (Hamlib NET rigctl server)
A native CAT backend owns the rig's serial port, and Windows gives a COM port to one process — so choosing native CAT locked WSJT-X, MSHV and JTDX out of the radio entirely. That is the cost of dropping OmniRig, which was itself a sharing layer, and it has to be paid back. OpsLog now becomes the server, as wfview does. It speaks the Hamlib net rigctl protocol, which every one of those programs supports natively (rig model "Hamlib NET rigctl", 127.0.0.1:4532) with no driver to install. It sits in front of the MANAGER, not a backend, so an operator on OmniRig, Flex, Icom or TCI gets the same server. Two details that decide whether a client works at all rather than degrading: dump_state is parsed positionally and WSJT-X refuses to proceed without a well-formed block, so it is written out in full and its shape is pinned by a test; and set_vfo / set_split_vfo answer RPRT 0 rather than an error, because OpsLog follows the rig's own VFO and a refusal makes WSJT-X abandon the connection. Unknown commands answer RPRT -11 — never silence, which hangs a client instead. The whole protocol is tested against a fake rig, plus one end-to-end exchange over a real socket, since the framing is as much the contract as the text. Also: the Yaesu backend is confirmed working on a real FTDX10 (frequency, mode, VFO, split), so its "not yet verified" note is now wrong and is corrected.
This commit is contained in:
@@ -51,6 +51,7 @@ import (
|
||||
"hamlog/internal/qslcard"
|
||||
"hamlog/internal/qso"
|
||||
"hamlog/internal/relaydev"
|
||||
"hamlog/internal/rigctld"
|
||||
"hamlog/internal/rotator/gs232"
|
||||
"hamlog/internal/rotator/pst"
|
||||
"hamlog/internal/rotgenius"
|
||||
@@ -108,6 +109,8 @@ const (
|
||||
keyCATPollMs = "cat.poll_ms"
|
||||
keyCATDelayMs = "cat.delay_ms" // pause between commands
|
||||
keyCATDigitalDefault = "cat.digital_default" // mode to use when CAT reports DATA
|
||||
keyCATShareEnabled = "cat.share.enabled" // expose CAT to other programs (Hamlib NET rigctl)
|
||||
keyCATSharePort = "cat.share.port" // TCP port for that server (rigctld default 4532)
|
||||
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)
|
||||
@@ -359,6 +362,8 @@ type CATSettings struct {
|
||||
PollMs int `json:"poll_ms"` // poll interval in ms (default 250)
|
||||
DelayMs int `json:"delay_ms"` // pause between commands (default 0)
|
||||
DigitalDefault string `json:"digital_default"` // when CAT says DATA, surface this mode (FT8/FT4/RTTY/…)
|
||||
ShareEnabled bool `json:"share_enabled"` // serve CAT to other programs (Hamlib NET rigctl)
|
||||
SharePort int `json:"share_port"` // TCP port for it (default 4532)
|
||||
}
|
||||
|
||||
// ModePreset is a mode entry with default RST values to auto-populate
|
||||
@@ -474,6 +479,11 @@ type App struct {
|
||||
lookup *lookup.Manager
|
||||
cache *lookup.Cache
|
||||
cat *cat.Manager
|
||||
// catShare serves OpsLog's CAT link to other programs over the Hamlib NET
|
||||
// rigctl protocol. It exists because a native backend OWNS the rig's serial
|
||||
// port: without it, choosing native CAT locks WSJT-X and friends out of the
|
||||
// radio entirely. nil when the operator has not enabled sharing.
|
||||
catShare *rigctld.Server
|
||||
dxcc *dxcc.Manager
|
||||
cluster *cluster.Manager
|
||||
// Cluster spots/lines are processed OFF the socket-read goroutine. Enriching a
|
||||
@@ -1431,6 +1441,12 @@ func (a *App) shutdown(ctx context.Context) {
|
||||
// backend: without this the rig never gets a disconnect and holds its single
|
||||
// control session for minutes, refusing every new login (even from the Icom
|
||||
// Remote Utility) until it times out on its own.
|
||||
if a.catShare != nil {
|
||||
// Before the CAT stop, so no client is mid-command against a backend that
|
||||
// is disconnecting — and so the port is free for the next launch.
|
||||
a.catShare.Stop()
|
||||
a.catShare = nil
|
||||
}
|
||||
if a.cat != nil {
|
||||
a.cat.Stop()
|
||||
}
|
||||
@@ -6566,7 +6582,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, keyCATYaesuPort, keyCATYaesuBaud, 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, keyCATShareEnabled, keyCATSharePort)
|
||||
if err != nil {
|
||||
return CATSettings{}, err
|
||||
}
|
||||
@@ -6594,6 +6610,8 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
||||
PollMs: 250,
|
||||
DelayMs: 0,
|
||||
DigitalDefault: m[keyCATDigitalDefault],
|
||||
ShareEnabled: m[keyCATShareEnabled] == "1",
|
||||
SharePort: 4532,
|
||||
}
|
||||
if n, _ := strconv.Atoi(m[keyCATFlexPort]); n > 0 && n <= 65535 {
|
||||
out.FlexPort = n
|
||||
@@ -6604,6 +6622,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[keyCATSharePort]); n > 0 && n <= 65535 {
|
||||
out.SharePort = n
|
||||
}
|
||||
if n, _ := strconv.Atoi(m[keyCATYaesuBaud]); n > 0 {
|
||||
out.YaesuBaud = n
|
||||
}
|
||||
@@ -6648,6 +6669,9 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
||||
if s.FlexPort <= 0 || s.FlexPort > 65535 {
|
||||
s.FlexPort = 4992
|
||||
}
|
||||
if s.SharePort <= 0 || s.SharePort > 65535 {
|
||||
s.SharePort = 4532
|
||||
}
|
||||
if s.YaesuBaud <= 0 {
|
||||
s.YaesuBaud = 38400
|
||||
}
|
||||
@@ -6689,6 +6713,10 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
||||
if s.DigitalDefault == "" {
|
||||
s.DigitalDefault = "FT8"
|
||||
}
|
||||
shareEnabled := "0"
|
||||
if s.ShareEnabled {
|
||||
shareEnabled = "1"
|
||||
}
|
||||
for k, v := range map[string]string{
|
||||
keyCATEnabled: enabled,
|
||||
keyCATBackend: s.Backend,
|
||||
@@ -6714,6 +6742,8 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
||||
keyCATPollMs: strconv.Itoa(s.PollMs),
|
||||
keyCATDelayMs: strconv.Itoa(s.DelayMs),
|
||||
keyCATDigitalDefault: strings.ToUpper(strings.TrimSpace(s.DigitalDefault)),
|
||||
keyCATShareEnabled: shareEnabled,
|
||||
keyCATSharePort: strconv.Itoa(s.SharePort),
|
||||
} {
|
||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||
return err
|
||||
@@ -12051,6 +12081,7 @@ func (a *App) reloadCAT() {
|
||||
a.catFlexSpots = s.Enabled && ((s.Backend == "flex" && s.FlexSpots) || (s.Backend == "tci" && s.TCISpots))
|
||||
a.catFlexDecodeSpots = s.Enabled && s.Backend == "flex" && s.FlexDecodeSpots
|
||||
a.catFlexDecodeSecs = s.FlexDecodeSecs
|
||||
a.reloadCATShare(s)
|
||||
if !s.Enabled {
|
||||
a.cat.Stop()
|
||||
return
|
||||
@@ -14776,3 +14807,54 @@ func (a *App) ClusterSpotStatuses(spots []SpotQuery) []SpotStatus {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── CAT sharing (Hamlib NET rigctl server) ────────────────────────────────
|
||||
|
||||
// catShareRig adapts the CAT manager to what the rigctld server needs. A thin
|
||||
// interface rather than a direct dependency, so the protocol stays testable
|
||||
// without a radio — and so sharing works with EVERY backend, not just the
|
||||
// native ones: an operator on OmniRig or Flex gets the same server.
|
||||
type catShareRig struct{ a *App }
|
||||
|
||||
func (r catShareRig) Freq() int64 { return r.a.cat.State().FreqHz }
|
||||
func (r catShareRig) Mode() string { return r.a.cat.State().Mode }
|
||||
|
||||
// Split reports the flag and the OTHER VFO's frequency. RigState follows ADIF —
|
||||
// FreqHz is where we TRANSMIT and RxFreqHz where we listen — while a rigctl
|
||||
// client asks for the split TX frequency, which is FreqHz. Getting this pair
|
||||
// backwards would make a client transmit on the listening frequency.
|
||||
func (r catShareRig) Split() (bool, int64) {
|
||||
st := r.a.cat.State()
|
||||
if !st.Split {
|
||||
return false, 0
|
||||
}
|
||||
return true, st.FreqHz
|
||||
}
|
||||
|
||||
func (r catShareRig) SetFreq(hz int64) error { return r.a.cat.SetFrequency(hz) }
|
||||
func (r catShareRig) SetMode(m string) error { return r.a.cat.SetMode(m) }
|
||||
func (r catShareRig) SetPTT(on bool) error { return r.a.cat.SetPTT(on) }
|
||||
|
||||
// reloadCATShare starts, stops or restarts the sharing server to match the
|
||||
// settings. Called from reloadCAT so one "Save & Close" settles both.
|
||||
func (a *App) reloadCATShare(s CATSettings) {
|
||||
want := s.Enabled && s.ShareEnabled
|
||||
// Always tear down first: the port may have changed, and a listener bound to
|
||||
// the old one would keep answering while the client is told to use the new.
|
||||
if a.catShare != nil {
|
||||
a.catShare.Stop()
|
||||
a.catShare = nil
|
||||
}
|
||||
if !want {
|
||||
return
|
||||
}
|
||||
srv := rigctld.New(s.SharePort, catShareRig{a: a}, applog.Printf)
|
||||
if err := srv.Start(); err != nil {
|
||||
// The usual cause is another rigctld — or a previous OpsLog — already on
|
||||
// the port. Logged rather than surfaced: CAT itself is unaffected, and the
|
||||
// operator finds it in the log the moment a client fails to connect.
|
||||
applog.Printf("cat share: %v", err)
|
||||
return
|
||||
}
|
||||
a.catShare = srv
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user