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") } // ── CAT wire trace ───────────────────────────────────────── // // Opt-in, off by default. Turning it on logs every frame sent and received — // CI-V as hex, the ASCII backends (Kenwood, Yaesu) as the text they exchange. // // It 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, and the CAT links have since produced the same class of report: // a MOX button that sets split instead of transmitting, and a split the Kenwood // backend does not recognise on a rig whose frequency it reads perfectly. // // Both are questions about what the RIG actually said. Guessing at that from a // protocol document has already cost this project a wrong fix that broke the // case which already worked. var catTrace atomic.Bool // SetCIVTrace turns the CAT wire trace on or off. Named for the CI-V link it // was written for; it now covers the text backends too. func SetCIVTrace(on bool) { catTrace.Store(on) if on { debugLog.Printf("wire trace ON — every CAT frame to and from the rig is logged") } else { debugLog.Printf("wire trace OFF") } } // CIVTraceEnabled reports whether the trace is running (for the settings UI). func CIVTraceEnabled() bool { return catTrace.Load() } // traceCIV logs one CI-V frame. dir is "TX" (to the rig) or "RX" (from it). func traceCIV(dir string, b []byte) { if !catTrace.Load() || len(b) == 0 { return } debugLog.Printf("civ %s % X", dir, b) } // traceText logs one ASCII exchange, for the Kenwood and Yaesu backends. // // The text is QUOTED. A truncated or empty reply must be visible as such // rather than vanish into the line: "" and ";" look identical unquoted, and // telling them apart is the whole question when a rig answers a command it // does not really support. func traceText(backend, dir, s string) { if !catTrace.Load() || s == "" { return } debugLog.Printf("%s %s %q", backend, dir, s) }