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:
2026-08-30 11:42:48 +02:00
parent 7b15b534cd
commit 88c9756edf
3 changed files with 51 additions and 3 deletions
+4 -2
View File
@@ -38,7 +38,8 @@
"Cluster: spots on the standard FT8/FT4 frequencies of every band now read FT8/FT4 when the comment names no mode — 30 m, 60 m, 80 m FT4, 17 m FT4 and 12 m were falling into the generic DATA bucket.",
"OmniRig (Yaesu): with split engaged, tuning to a spot moves BOTH VFOs — on an FT-2000 the write landed on the TX VFO only, so the transmitter QSYed and the receiver stayed behind.",
"Icom network: Wake-on-LAN — a rig switched fully off (LAN server down) is woken with a magic packet to its remembered MAC whenever a session is wanted, so the console and its ON button come back without a walk to the shack.",
"Worked before: a slow lookup can no longer overwrite a newer one — typing a second callsign quickly could leave the previous calls QSOs displayed under the new calls name."
"Worked before: a slow lookup can no longer overwrite a newer one — typing a second callsign quickly could leave the previous calls QSOs displayed under the new calls name.",
"Icom network: OpsLog now sends its own pings every 500 ms on all three streams, as RS-BA1 and wfview do — a client that only ever replied was judged absent by the rig, which stopped serving CI-V data about a minute into every session: the recurring sound/CAT dropouts."
],
"fr": [
"Console Elecraft : le S-mètre est calibré sur un vrai K3 — S9 et les +dB correspondent désormais à laffichage de la radio (il lisait environ deux points S trop bas).",
@@ -76,7 +77,8 @@
"Cluster : les spots sur les fréquences standard FT8/FT4 de chaque bande lisent désormais FT8/FT4 quand le commentaire ne nomme pas de mode — 30 m, 60 m, FT4 80 m, FT4 17 m et 12 m tombaient dans le bloc DATA générique.",
"OmniRig (Yaesu) : avec le split actif, se rendre sur un spot déplace LES DEUX VFO — sur un FT-2000 l’écriture natteignait que le VFO TX, donc l’émetteur QSYait et le récepteur restait derrière.",
"Réseau Icom : Wake-on-LAN — une radio complètement éteinte (serveur LAN coupé) est réveillée par un paquet magique vers sa MAC mémorisée dès quune session est voulue, et la console avec son bouton ON revient sans aller au shack.",
"Déjà contacté : une recherche lente ne peut plus écraser une plus récente — taper un second indicatif rapidement pouvait laisser les QSO du call précédent affichés sous le nom du nouveau."
"Déjà contacté : une recherche lente ne peut plus écraser une plus récente — taper un second indicatif rapidement pouvait laisser les QSO du call précédent affichés sous le nom du nouveau.",
"Réseau Icom : OpsLog envoie désormais ses propres pings toutes les 500 ms sur les trois flux, comme RS-BA1 et wfview — un client qui ne faisait que répondre était jugé absent par la radio, qui cessait de servir le CI-V au bout dune minute : les coupures récurrentes de son/CAT."
]
},
{
+10 -1
View File
@@ -57,7 +57,9 @@ type icomAudio struct {
rxLastSeq uint16
rxMissing map[uint16]int
dumped int // packets hex-dumped so far (≤ icaDumpFirst)
dumped int // packets hex-dumped so far (≤ icaDumpFirst)
pingSeq uint16 // client-ping counter — the rig wants OUR pings too (see icnPing)
started time.Time
// Live-TX state — see SendTXChunk.
txMu sync.Mutex
@@ -109,6 +111,7 @@ func dialIcomAudio(host string, sink func([]byte), cancel <-chan struct{}) (*ico
_ = conn.SetReadBuffer(1 << 20)
a := &icomAudio{
conn: conn, aID: aID, aRemote: aRemote,
started: time.Now(),
sink: sink,
rxMissing: make(map[uint16]int),
done: make(chan struct{}),
@@ -124,6 +127,7 @@ func dialIcomAudio(host string, sink func([]byte), cancel <-chan struct{}) (*ico
func (a *icomAudio) audioPump() {
buf := make([]byte, 8192)
lastIdle := time.Now()
lastPing := time.Now()
lastReq := time.Now()
for {
select {
@@ -163,6 +167,11 @@ func (a *icomAudio) audioPump() {
_, _ = a.conn.Write(icnCtrl(0x00, 0, a.aID, a.aRemote))
lastIdle = time.Now()
}
if time.Since(lastPing) > 500*time.Millisecond {
a.pingSeq++
_, _ = a.conn.Write(icnPing(a.pingSeq, a.aID, a.aRemote, uint32(time.Since(a.started).Milliseconds())))
lastPing = time.Now()
}
if time.Since(lastReq) > 100*time.Millisecond {
a.sendRetransmitReq()
lastReq = time.Now()
+37
View File
@@ -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)