Reported on an IC-9100: the MOX button does not transmit, it sets split. The source sends the PTT command (0x1C 0x00) and nowhere sends the split one (0x0F), so the discrepancy is between what OpsLog sends and what the rig acts on — and that link has already been shown, in the same operator's log, to lose frame sync. Rather than guess from the command table, this adds what settled the WinKeyer bug in one line: the actual bytes. Settings → CAT → Log the CI-V protocol writes every frame in both directions as hex. RX is traced BEFORE framing, since the bytes as they arrived are what reveals a lost boundary — a decoded view would hide exactly the fault being hunted. Session-only, like the keyer trace: it is a diagnostic, and a log full of hex helps nobody who forgot it was on. I am not claiming a cause yet. The last time I inferred one from a protocol document rather than from evidence, I inverted a digit and broke the case that worked.
112 lines
3.8 KiB
Go
112 lines
3.8 KiB
Go
package cat
|
|
|
|
import (
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// LogSink, when set by the host app at startup, receives every CAT debug
|
|
// line so they land in the unified app log (opslog.log) alongside the rest
|
|
// of OpsLog's diagnostics. Until it's wired we fall back to a dedicated
|
|
// cat.log file so early-startup lines aren't lost.
|
|
var LogSink func(format string, args ...any)
|
|
|
|
// catLogger forwards Printf either to the host LogSink (preferred) or to a
|
|
// local file/stderr fallback. Keeps the call sites (debugLog.Printf(...))
|
|
// unchanged.
|
|
type catLogger struct {
|
|
once sync.Once
|
|
fallback *log.Logger
|
|
}
|
|
|
|
func (c *catLogger) Printf(format string, args ...any) {
|
|
if LogSink != nil {
|
|
LogSink("cat: "+format, args...)
|
|
return
|
|
}
|
|
// Only now, on a line that genuinely has nowhere else to go, is the fallback
|
|
// file opened. It used to be created at package init, so every installation
|
|
// grew an %APPDATA%\OpsLog\cat.log that nothing ever wrote to once the app
|
|
// wired LogSink — a decoy for anyone told to "check the CAT log".
|
|
c.once.Do(func() { c.fallback = openFallbackLog() })
|
|
if c.fallback != nil {
|
|
c.fallback.Printf(format, args...)
|
|
}
|
|
}
|
|
|
|
// debugLog writes CAT debug events so users can diagnose mode/freq mismatches
|
|
// without rebuilding with a console. Once LogSink is set, lines flow into the
|
|
// main opslog.log.
|
|
var debugLog = &catLogger{}
|
|
|
|
func openFallbackLog() *log.Logger {
|
|
base, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return log.Default()
|
|
}
|
|
dir := filepath.Join(base, "OpsLog")
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return log.Default()
|
|
}
|
|
f, err := os.OpenFile(filepath.Join(dir, "cat.log"),
|
|
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return log.Default()
|
|
}
|
|
return log.New(f, "", log.LstdFlags|log.Lmicroseconds)
|
|
}
|
|
|
|
// DebugLogPath returns where the fallback cat.log lives, or "" when CAT lines are
|
|
// going to the app log instead — which is the normal case, and the answer callers
|
|
// actually need. It previously returned the path unconditionally, so the one place
|
|
// that displayed it sent operators to an empty file in %APPDATA% while their CAT
|
|
// diagnostics sat in data\opslog.log.
|
|
func DebugLogPath() string {
|
|
if LogSink != nil {
|
|
return "" // lines go to the unified app log; there is no separate cat.log
|
|
}
|
|
base, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return filepath.Join(base, "OpsLog", "cat.log")
|
|
}
|
|
|
|
// ── CI-V byte trace ────────────────────────────────────────────────────────
|
|
//
|
|
// Opt-in, off by default. Turning it on logs every CI-V frame sent and received
|
|
// as hex, exactly like the WinKeyer protocol trace.
|
|
//
|
|
// That trace exists because a fault nobody could reason about — "the keyer sends
|
|
// one element then stalls" — was settled in one line the moment the actual bytes
|
|
// were visible. The CI-V link has now produced the same class of report: a MOX
|
|
// button that sets split instead of transmitting, on a rig whose PTT command is
|
|
// demonstrably correct in the source. Guessing at that from the command table
|
|
// has already cost this project a wrong fix; the bytes will say what the rig was
|
|
// really asked.
|
|
var civTrace atomic.Bool
|
|
|
|
// SetCIVTrace turns the CI-V byte trace on or off.
|
|
func SetCIVTrace(on bool) {
|
|
civTrace.Store(on)
|
|
if on {
|
|
debugLog.Printf("civ trace ON — every CI-V frame to and from the rig is logged")
|
|
} else {
|
|
debugLog.Printf("civ trace OFF")
|
|
}
|
|
}
|
|
|
|
// CIVTraceEnabled reports whether the trace is running (for the settings UI).
|
|
func CIVTraceEnabled() bool { return civTrace.Load() }
|
|
|
|
// traceCIV logs one frame. dir is "TX" (to the rig) or "RX" (from it).
|
|
func traceCIV(dir string, b []byte) {
|
|
if !civTrace.Load() || len(b) == 0 {
|
|
return
|
|
}
|
|
debugLog.Printf("civ %s % X", dir, b)
|
|
}
|