diag(icom net): name the last CI-V commands before the silence

Audio off, and the IC-7760 still answers at connect and then never again:
twenty-eight commands sent, not one reply, packets still arriving on the
stream. A count says how much went unanswered and never which command
went out last — which is the one thing that can identify a frame this rig
does not tolerate, on a radio nobody here has.

The transport now keeps the command headers of the last eight frames and
prints them with the silence report.
This commit is contained in:
2026-09-06 17:49:32 +02:00
parent f2168d339b
commit e8f9e68759
2 changed files with 40 additions and 2 deletions
+38
View File
@@ -132,6 +132,13 @@ type icomNet struct {
// loop, not the rig — and RS-BA1 showing no such dropouts points that way.
txCiv atomic.Uint32
txAtData atomic.Uint32
// WHAT was asked, not just how much. A rig that answers at connect and then
// never again has usually been sent something it does not like, and counting
// the unanswered commands says nothing about which one that was. The last few
// command headers are kept so the silence report can name them — the only way
// to find a poison command on a radio nobody here has.
cmdMu sync.Mutex
lastCmd []string
rx chan []byte // CI-V byte chunks from civPump → Read (control replies)
scopeRx chan []byte // scope (0x27) frames, kept off rx so the panadapter
@@ -268,6 +275,7 @@ func (n *icomNet) Write(p []byte) (int, error) {
n.vCivSeq++
n.seqMu.Unlock()
n.txCiv.Add(1)
n.noteCmd(p)
pkt := icnCivData(seq, n.vID, n.vRemote, civSeq, p)
n.sentMu.Lock()
n.sentBuf[seq] = pkt
@@ -280,6 +288,35 @@ func (n *icomNet) Write(p []byte) (int, error) {
return len(p), nil
}
// noteCmd remembers the command bytes of a CI-V frame — everything after the
// preamble and the two addresses, up to four bytes, which is command,
// sub-command and the first of the data.
func (n *icomNet) noteCmd(p []byte) {
if len(p) < 5 {
return
}
body := p[4:]
if len(body) > 4 {
body = body[:4]
}
n.cmdMu.Lock()
n.lastCmd = append(n.lastCmd, fmt.Sprintf("% X", body))
if len(n.lastCmd) > 8 {
n.lastCmd = n.lastCmd[len(n.lastCmd)-8:]
}
n.cmdMu.Unlock()
}
// recentCmds is what noteCmd collected, oldest first.
func (n *icomNet) recentCmds() string {
n.cmdMu.Lock()
defer n.cmdMu.Unlock()
if len(n.lastCmd) == 0 {
return "none"
}
return strings.Join(n.lastCmd, " | ")
}
// icnTrace toggles verbose per-frame CI-V request/reply logging for diagnosing
// the network transport. Off by default (the connect-step logs stay); flip to
// true to trace every TX/RX again.
@@ -496,6 +533,7 @@ func (n *icomNet) civPump() {
}
debugLog.Printf("icom net: no CI-V DATA for 10 s (transport last heard %s ago; last scope frame %s ago; last socket error: %v; missing-seq backlog: %d; CI-V commands SENT since the last answer: %d)",
time.Since(lastPkt).Round(time.Second), scopeAge, lastErr, len(n.rxMissing), n.txCiv.Load()-n.txAtData.Load())
debugLog.Printf("icom net: the last CI-V commands sent, oldest first: %s", n.recentCmds())
// THE AUDIO STREAM IS THE FIRST SUSPECT, AND ONLY THE LOG CAN SAY SO.
//
// The RX audio stream is experimental and shares the rig's session with