fix(icom): the client pings too — the dropouts' likely root cause
wfview sends a ping (0x07, 0x15 bytes, its own seq and a monotonic timestamp) every 500 ms on every stream, and so does RS-BA1. OpsLog only ever REPLIED to the rig's pings — and the loaner IC-7760 stopped serving CI-V data about a minute into nearly every session while the transport stayed alive: the rig had concluded nobody was listening. Scope, TX audio and idle numbering were each eliminated in turn before wfview's source settled it. Pings now go out on control, CI-V and audio alike.
This commit is contained in:
@@ -138,6 +138,11 @@ type icomNet struct {
|
||||
vTracked uint16
|
||||
vCivSeq uint16
|
||||
seqMu sync.Mutex // guards vTracked/vCivSeq: the command loop AND the pump's quiet-recovery both send
|
||||
// Client-ping sequence counters, one per stream, and the connection's epoch
|
||||
// for the ping timestamps. Owned by their pump goroutines — no locking.
|
||||
civPingSeq uint16
|
||||
ctrlPingSeq uint16
|
||||
started time.Time
|
||||
// txCiv counts CI-V command packets sent, and txAtData snapshots it at the
|
||||
// last received CI-V data. Their difference during a silence answers the
|
||||
// question the reconnects cannot: were we still ASKING when the answers
|
||||
@@ -332,6 +337,7 @@ func (n *icomNet) Close() error {
|
||||
func (n *icomNet) ctrlPump() {
|
||||
buf := make([]byte, 4096)
|
||||
lastIdle := time.Now()
|
||||
lastCtrlPing := time.Now()
|
||||
lastToken := time.Now() // token was just granted during dial
|
||||
for {
|
||||
select {
|
||||
@@ -372,6 +378,11 @@ func (n *icomNet) ctrlPump() {
|
||||
}
|
||||
// Renew well inside the rig's ~2-min token timeout. 30 s (was 45) leaves room
|
||||
// for one lost renewal + its retransmit before the token would lapse.
|
||||
if time.Since(lastCtrlPing) > 500*time.Millisecond {
|
||||
n.ctrlPingSeq++
|
||||
_, _ = n.ctrl.Write(icnPing(n.ctrlPingSeq, n.cID, n.cRemote, uint32(time.Since(n.started).Milliseconds())))
|
||||
lastCtrlPing = time.Now()
|
||||
}
|
||||
if time.Since(lastToken) > 30*time.Second {
|
||||
n.renewToken()
|
||||
lastToken = time.Now()
|
||||
@@ -416,6 +427,7 @@ func (n *icomNet) civPump() {
|
||||
// and a rig that stopped talking are different repairs, and the 30 s
|
||||
// watchdog that follows cannot tell them apart from where it sits.
|
||||
lastPkt := time.Now()
|
||||
lastPing := time.Now()
|
||||
lastData := time.Now() // CI-V payload packets (replies + transceive)
|
||||
lastScope := time.Time{} // scope frames within those
|
||||
var lastErr error
|
||||
@@ -551,6 +563,11 @@ func (n *icomNet) civPump() {
|
||||
n.sendRetransmitReq()
|
||||
lastReq = time.Now()
|
||||
}
|
||||
if time.Since(lastPing) > 500*time.Millisecond {
|
||||
n.civPingSeq++
|
||||
_, _ = n.civ.Write(icnPing(n.civPingSeq, n.vID, n.vRemote, uint32(time.Since(n.started).Milliseconds())))
|
||||
lastPing = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,6 +816,7 @@ func dialIcomNet(host, user, pass, compName string, rigAddr byte, cancel <-chan
|
||||
cTracked: cTracked, cAuthSeq: cInner,
|
||||
cToken: token, cTokReq: tokReq,
|
||||
cSentBuf: make(map[uint16][]byte),
|
||||
started: time.Now(),
|
||||
}
|
||||
n.markRx() // the successful handshake counts as initial rig activity
|
||||
// openClose(open) starts the CI-V data flow. We intentionally DO NOT power the
|
||||
@@ -1068,6 +1086,25 @@ func icnWakeOnLAN(mac string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// icnPing builds a CLIENT ping request (wfview's ping_packet: 0x15 bytes,
|
||||
// type 0x07, reply=0, a monotonic time at 0x11). The rig answers each one —
|
||||
// and, decisively, treats them as the client's sign of life: wfview sends one
|
||||
// every 500 ms on every stream, and OpsLog, which only ever REPLIED to the
|
||||
// rig's pings, watched the IC-7760 stop serving CI-V data about a minute into
|
||||
// every session. Same socket answered, same transport alive: the rig had
|
||||
// simply concluded nobody was listening.
|
||||
func icnPing(seq uint16, sentid, rcvdid uint32, ms uint32) []byte {
|
||||
b := make([]byte, 0x15)
|
||||
icnLE.PutUint32(b[0:], 0x15)
|
||||
icnLE.PutUint16(b[4:], 0x07)
|
||||
icnLE.PutUint16(b[6:], seq)
|
||||
icnLE.PutUint32(b[8:], sentid)
|
||||
icnLE.PutUint32(b[12:], rcvdid)
|
||||
b[0x10] = 0x00
|
||||
icnLE.PutUint32(b[0x11:], ms)
|
||||
return b
|
||||
}
|
||||
|
||||
func icnOpenClose(seq uint16, sentid, rcvdid uint32, civSeq uint16, magic byte) []byte {
|
||||
b := make([]byte, 0x16)
|
||||
icnLE.PutUint32(b[0:], 0x16)
|
||||
|
||||
Reference in New Issue
Block a user