fix(rigctld): never leave the rig keyed when the client goes away
The Kenwood/Elecraft backend deliberately suspends its wire poll while PTT is held — a K3 answers "?;" to IF; during transmit, and treating that as a fault used to drop the whole CAT link. The consequence was that nothing watched the transmitter: a client that crashed, was closed, or simply had its socket shut under it left the rig on air. And shutting the socket is routine. reloadCATShare tears the sharing server down and rebuilds it on every settings save, so a Save while WSJT-X held PTT was enough. A K3 operator's log shows exactly that: "TX;" at 17:53:09, no "RX;" ever, the poll silent, and the rig still keyed 29 s later when the CAT link happened to be rebuilt. The server now drops PTT when a connection ends and when Stop() is called. Stop() runs before reloadCAT restarts the backend, so the unkey still reaches the radio. An atomic Swap keeps it once-only across the two paths.
This commit is contained in:
@@ -118,7 +118,32 @@ func (s *Server) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// releasePTT drops the transmitter when whoever was holding it goes away.
|
||||
//
|
||||
// Nothing else will. The Kenwood/Elecraft backend deliberately suspends its
|
||||
// wire poll while PTT is held (a K3 answers "?;" to IF; during transmit), so a
|
||||
// client that crashes, is killed, or simply has its socket closed under it
|
||||
// leaves the rig keyed with nobody watching — and closing the socket is exactly
|
||||
// what Stop() does on every settings save. Seen in the field: a K3 sat in
|
||||
// transmit for 29 s, until the CAT link happened to be rebuilt.
|
||||
//
|
||||
// Swap makes this once-only, so the Stop() path and the per-connection defer it
|
||||
// triggers can both call it without double-unkeying.
|
||||
func (s *Server) releasePTT(why string) {
|
||||
if !s.ptt.Swap(false) {
|
||||
return
|
||||
}
|
||||
s.log("rigctld: %s while the rig was keyed — dropping PTT", why)
|
||||
if err := s.rig.SetPTT(false); err != nil {
|
||||
s.log("rigctld: emergency unkey failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
// Before anything is torn down: reloadCAT calls us BEFORE it restarts the CAT
|
||||
// backend, so the rig is still reachable here and an unkey still lands.
|
||||
s.releasePTT("CAT sharing stopped")
|
||||
|
||||
s.mu.Lock()
|
||||
s.closed = true
|
||||
ln := s.ln
|
||||
@@ -146,6 +171,8 @@ func (s *Server) serve(c net.Conn) {
|
||||
delete(s.conns, c)
|
||||
s.mu.Unlock()
|
||||
_ = c.Close()
|
||||
// A client that walks away mid-over must not leave the rig transmitting.
|
||||
s.releasePTT(fmt.Sprintf("client %s left", c.RemoteAddr()))
|
||||
}()
|
||||
s.log("rigctld: client connected from %s", c.RemoteAddr())
|
||||
r := bufio.NewReader(c)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fakeRig stands in for the CAT manager.
|
||||
@@ -234,6 +235,66 @@ func TestServerOverTCP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A client that vanishes mid-over must not leave the transmitter keyed. Nothing
|
||||
// else would notice: the Kenwood/Elecraft backend stops polling while PTT is
|
||||
// held, so a K3 was seen sitting in transmit for 29 s after WSJT-X's socket was
|
||||
// closed under it by a settings save.
|
||||
func TestPTTIsDroppedWhenTheClientDisappears(t *testing.T) {
|
||||
rig := &fakeRig{freq: 14074000, mode: "FT8"}
|
||||
s := New(0, rig, nil)
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.ln = ln
|
||||
s.mu.Unlock()
|
||||
go func() {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go s.serve(c)
|
||||
}
|
||||
}()
|
||||
defer s.Stop()
|
||||
|
||||
c, err := net.DialTimeout("tcp", ln.Addr().String(), dialTimeout)
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
r := bufio.NewReader(c)
|
||||
// "T 3" is what WSJT-X sends for PTT_ON_DATA — the form seen in the field.
|
||||
if _, err := c.Write([]byte("T 3\n")); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
line, _ := r.ReadString('\n')
|
||||
if strings.TrimSpace(line) != "RPRT 0" {
|
||||
t.Fatalf("set_ptt = %q, want RPRT 0", strings.TrimSpace(line))
|
||||
}
|
||||
rig.mu.Lock()
|
||||
keyed := rig.ptt
|
||||
rig.mu.Unlock()
|
||||
if !keyed {
|
||||
t.Fatalf("rig is not keyed after T 3 — the test proves nothing")
|
||||
}
|
||||
|
||||
_ = c.Close()
|
||||
// The unkey happens on the serve goroutine's defer, so poll briefly.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
rig.mu.Lock()
|
||||
keyed = rig.ptt
|
||||
rig.mu.Unlock()
|
||||
if !keyed {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Error("rig still keyed after the client disconnected — the transmitter was left on air")
|
||||
}
|
||||
|
||||
func TestModeMapping(t *testing.T) {
|
||||
for _, c := range []struct{ adif, hamlib string }{
|
||||
{"SSB", "USB"}, {"LSB", "LSB"}, {"CW", "CW"}, {"RTTY", "RTTY"},
|
||||
|
||||
Reference in New Issue
Block a user