bug
This commit is contained in:
@@ -339,6 +339,15 @@ func recordToQSO(rec Record) (qso.QSO, bool) {
|
||||
if hz, ok := parseFreqHz(rec["freq_rx"]); ok {
|
||||
q.FreqRXHz = &hz
|
||||
}
|
||||
// RX defaults to TX when the ADIF omits split info: an empty BAND_RX /
|
||||
// FREQ_RX means the contact wasn't cross-band/split, so RX = TX.
|
||||
if q.BandRX == "" {
|
||||
q.BandRX = q.Band
|
||||
}
|
||||
if q.FreqRXHz == nil && q.FreqHz != nil {
|
||||
v := *q.FreqHz
|
||||
q.FreqRXHz = &v
|
||||
}
|
||||
|
||||
q.RSTSent = rec["rst_sent"]
|
||||
q.RSTRcvd = rec["rst_rcvd"]
|
||||
|
||||
+30
-12
@@ -6,20 +6,38 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// debugLog writes CAT debug events to %APPDATA%/HamLog/cat.log so users can
|
||||
// diagnose mode/freq mismatches without rebuilding with -windowsconsole.
|
||||
//
|
||||
// Initialised lazily on first use. Falls back to the standard library
|
||||
// default logger (stderr, usually invisible in a Wails GUI build) if the
|
||||
// log file can't be opened.
|
||||
var debugLog = openDebugLog()
|
||||
// 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)
|
||||
|
||||
func openDebugLog() *log.Logger {
|
||||
// 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{ fallback *log.Logger }
|
||||
|
||||
func (c *catLogger) Printf(format string, args ...any) {
|
||||
if LogSink != nil {
|
||||
LogSink("cat: "+format, args...)
|
||||
return
|
||||
}
|
||||
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{fallback: openFallbackLog()}
|
||||
|
||||
func openFallbackLog() *log.Logger {
|
||||
base, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return log.Default()
|
||||
}
|
||||
dir := filepath.Join(base, "HamLog")
|
||||
dir := filepath.Join(base, "OpsLog")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return log.Default()
|
||||
}
|
||||
@@ -31,12 +49,12 @@ func openDebugLog() *log.Logger {
|
||||
return log.New(f, "", log.LstdFlags|log.Lmicroseconds)
|
||||
}
|
||||
|
||||
// DebugLogPath returns the path the cat.log file would be opened at, for
|
||||
// surfacing in the UI / docs.
|
||||
// DebugLogPath returns where the fallback cat.log lives, for surfacing in the
|
||||
// UI / docs. When LogSink is wired, CAT lines are in the main app log instead.
|
||||
func DebugLogPath() string {
|
||||
base, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(base, "HamLog", "cat.log")
|
||||
return filepath.Join(base, "OpsLog", "cat.log")
|
||||
}
|
||||
|
||||
+80
-24
@@ -160,42 +160,98 @@ func (o *OmniRig) ReadState() (RigState, error) {
|
||||
|
||||
func (o *OmniRig) SetFrequency(hz int64) error {
|
||||
if o.rig == nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency(%d): NOT CONNECTED", hz)
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
// OmniRig Freq is a Long (int32). Validate to avoid silent truncation.
|
||||
if hz < 0 || hz > 0x7fffffff {
|
||||
debugLog.Printf("OmniRig.SetFrequency(%d): out of int32 range", hz)
|
||||
return fmt.Errorf("frequency out of OmniRig int32 range")
|
||||
}
|
||||
hz32 := int32(hz)
|
||||
|
||||
// Pick the right OmniRig property. Many rig .ini files only define a
|
||||
// WRITE command for FreqA/FreqB but not the generic Freq — in which case
|
||||
// PutProperty(Freq) silently succeeds but the rig never moves. Write to
|
||||
// the active VFO's specific property when we know it; fall back to Freq.
|
||||
prop := "FreqA"
|
||||
if vfoVar, err := oleutil.GetProperty(o.rig, "Vfo"); err == nil {
|
||||
switch omniRigVfo(vfoVar.Val) {
|
||||
case "B", "BB", "BA":
|
||||
prop = "FreqB"
|
||||
case "A", "AA", "AB":
|
||||
prop = "FreqA"
|
||||
}
|
||||
// Log the rig's writable-params, status and VFO state up front so a
|
||||
// friend's session shows exactly what OmniRig reports for their rig.
|
||||
status, statusStr, rigType := int64(-1), "", ""
|
||||
if v, err := oleutil.GetProperty(o.rig, "Status"); err == nil {
|
||||
status = v.Val
|
||||
}
|
||||
debugLog.Printf("OmniRig.SetFrequency(%d Hz / %.6f MHz) → %s", hz, float64(hz)/1e6, prop)
|
||||
if _, err := oleutil.PutProperty(o.rig, prop, hz32); err != nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency(%s) error: %v — falling back to Freq", prop, err)
|
||||
if _, err2 := oleutil.PutProperty(o.rig, "Freq", hz32); err2 != nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency(Freq) also failed: %v", err2)
|
||||
return err2
|
||||
}
|
||||
if v, err := oleutil.GetProperty(o.rig, "StatusStr"); err == nil {
|
||||
statusStr = v.ToString()
|
||||
}
|
||||
if v, err := oleutil.GetProperty(o.rig, "RigType"); err == nil {
|
||||
rigType = v.ToString()
|
||||
}
|
||||
rawVfo, vfo := int64(-1), ""
|
||||
if vfoVar, err := oleutil.GetProperty(o.rig, "Vfo"); err == nil {
|
||||
rawVfo = vfoVar.Val
|
||||
vfo = omniRigVfo(vfoVar.Val)
|
||||
} else {
|
||||
debugLog.Printf("OmniRig.SetFrequency: Vfo read error: %v", err)
|
||||
}
|
||||
split := int64(0)
|
||||
if v, err := oleutil.GetProperty(o.rig, "Split"); err == nil {
|
||||
split = v.Val
|
||||
}
|
||||
// What can this rig's .ini actually write? OmniRig exposes a WriteableParams
|
||||
// bitmask — if FreqA/FreqB/Freq bits are missing, the write is a silent no-op.
|
||||
writeable := int64(-1)
|
||||
if v, err := oleutil.GetProperty(o.rig, "WriteableParams"); err == nil {
|
||||
writeable = v.Val
|
||||
}
|
||||
debugLog.Printf("OmniRig.SetFrequency(%d Hz / %.6f MHz): rig=%q status=%d(%s) vfo=%q(raw=%d) split=%d writeableParams=0x%X",
|
||||
hz, float64(hz)/1e6, rigType, status, statusStr, vfo, rawVfo, split, writeable)
|
||||
|
||||
// Pick the active VFO's specific property. Many rig .ini files only define
|
||||
// a WRITE command for FreqA/FreqB but not the generic Freq.
|
||||
prop := "FreqA"
|
||||
switch vfo {
|
||||
case "B", "BB", "BA":
|
||||
prop = "FreqB"
|
||||
case "A", "AA", "AB":
|
||||
prop = "FreqA"
|
||||
}
|
||||
|
||||
// Read back the active VFO freq after a short delay so the log shows
|
||||
// whether the rig actually moved. Useful when the .ini accepts the write
|
||||
// silently but the rig doesn't honour it (wrong WRITE command etc.).
|
||||
if fv, err := oleutil.GetProperty(o.rig, "Freq"); err == nil {
|
||||
debugLog.Printf("OmniRig.Freq immediately after Put = %d Hz", fv.Val)
|
||||
wroteOK := false
|
||||
if _, err := oleutil.PutProperty(o.rig, prop, hz32); err != nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(%s) error: %v", prop, err)
|
||||
} else {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(%s, %d) OK", prop, hz32)
|
||||
wroteOK = true
|
||||
}
|
||||
|
||||
// Belt-and-suspenders: when NOT in split, also write the generic Freq.
|
||||
// Icom .ini files commonly honour Freq (CI-V "set operating frequency")
|
||||
// but ignore FreqA/FreqB, so the rig changed mode but never moved — this
|
||||
// is exactly the IC-9100 "mode changes, freq doesn't" symptom.
|
||||
if split == 0 {
|
||||
if _, err := oleutil.PutProperty(o.rig, "Freq", hz32); err != nil {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(Freq) error: %v", err)
|
||||
if !wroteOK {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
debugLog.Printf("OmniRig.SetFrequency: PutProperty(Freq, %d) OK", hz32)
|
||||
}
|
||||
} else if !wroteOK {
|
||||
return fmt.Errorf("OmniRig: could not write %s and split is on (won't touch generic Freq)", prop)
|
||||
}
|
||||
|
||||
// Read back all three immediately. OmniRig is async (the CAT command is
|
||||
// queued + sent over serial), so these may still show the OLD value for
|
||||
// one poll cycle — but if they NEVER change in the next poll, the rig
|
||||
// isn't honouring the write (wrong .ini WRITE command for this model).
|
||||
fa, fb, fg := int64(-1), int64(-1), int64(-1)
|
||||
if v, err := oleutil.GetProperty(o.rig, "FreqA"); err == nil {
|
||||
fa = v.Val
|
||||
}
|
||||
if v, err := oleutil.GetProperty(o.rig, "FreqB"); err == nil {
|
||||
fb = v.Val
|
||||
}
|
||||
if v, err := oleutil.GetProperty(o.rig, "Freq"); err == nil {
|
||||
fg = v.Val
|
||||
}
|
||||
debugLog.Printf("OmniRig.SetFrequency: readback FreqA=%d FreqB=%d Freq=%d (target %d)", fa, fb, fg, hz)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package extsvc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -9,6 +10,29 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// baseCall extracts the operator's base callsign from a possibly-affixed call:
|
||||
// for slashed forms (F4BPO/P, FW/F4BPO, 9A/F4BPO/P) it returns the longest
|
||||
// token, which is the real call; otherwise the call itself. Upper-cased.
|
||||
func baseCall(s string) string {
|
||||
s = strings.ToUpper(strings.TrimSpace(s))
|
||||
if !strings.Contains(s, "/") {
|
||||
return s
|
||||
}
|
||||
best := ""
|
||||
for _, part := range strings.Split(s, "/") {
|
||||
if len(part) > len(best) {
|
||||
best = part
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// sameBaseCall reports whether two callsigns belong to the same operator,
|
||||
// ignoring portable prefixes/suffixes (F4BPO/P == F4BPO, FW/F4BPO == F4BPO).
|
||||
func sameBaseCall(a, b string) bool {
|
||||
return baseCall(a) == baseCall(b)
|
||||
}
|
||||
|
||||
// Deps are the host-app callbacks the Manager needs. Keeping them as
|
||||
// function fields decouples extsvc from the qso/adif/settings packages and
|
||||
// keeps the upload-scheduling logic testable.
|
||||
@@ -33,6 +57,11 @@ type Deps struct {
|
||||
// Upload flag ("N" or "R"), à la Log4OM. Returning false skips the QSO.
|
||||
ShouldUpload func(svc Service, id int64) bool
|
||||
|
||||
// StationCallOf returns the QSO's STATION_CALLSIGN. Used to guard against
|
||||
// uploading a QSO into a logbook for a different callsign (the force-call
|
||||
// option would otherwise silently relabel it). "" → no station call known.
|
||||
StationCallOf func(id int64) string
|
||||
|
||||
// Logf is an optional diagnostic logger.
|
||||
Logf func(format string, args ...any)
|
||||
}
|
||||
@@ -236,6 +265,33 @@ func (m *Manager) upload(svc Service, id int64, cfg ServiceConfig) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Station-callsign guard. Each logbook belongs to one callsign:
|
||||
// QRZ/LoTW → the ForceStationCallsign (the call this logbook signs as)
|
||||
// Club Log → the logbook Callsign param
|
||||
// If the QSO's own STATION_CALLSIGN is a DIFFERENT operator, uploading
|
||||
// would push it into the wrong logbook (and the force-call option would
|
||||
// silently relabel it). Block it with a clear error. Portable variants of
|
||||
// the SAME call (F4BPO/P, FW/F4BPO…) are allowed.
|
||||
owner := ""
|
||||
switch svc {
|
||||
case ServiceQRZ, ServiceLoTW:
|
||||
owner = cfg.ForceStationCallsign
|
||||
case ServiceClublog:
|
||||
owner = cfg.Callsign
|
||||
}
|
||||
if owner != "" && m.deps.StationCallOf != nil {
|
||||
qcall := m.deps.StationCallOf(id)
|
||||
if qcall != "" && !sameBaseCall(qcall, owner) {
|
||||
err := fmt.Errorf("station callsign %s does not match %s logbook %s — not uploaded",
|
||||
strings.ToUpper(qcall), svc, strings.ToUpper(owner))
|
||||
m.logf("extsvc: %s upload of QSO %d BLOCKED: %v", svc, id, err)
|
||||
if m.deps.NotifyError != nil {
|
||||
m.deps.NotifyError(svc, id, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
+15
-2
@@ -175,7 +175,7 @@ func TestQRZ(ctx context.Context, client *http.Client, apiKey string) (string, e
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("qrz: bad response: %w", err)
|
||||
}
|
||||
status := strings.ToUpper(strings.TrimSpace(vals.Get("STATUS")))
|
||||
status := qrzStatusField(vals)
|
||||
if status == "AUTH" || status == "FAIL" {
|
||||
reason := strings.TrimSpace(vals.Get("REASON"))
|
||||
if reason == "" {
|
||||
@@ -201,7 +201,11 @@ func parseQRZResponse(body string) (UploadResult, error) {
|
||||
if err != nil {
|
||||
return UploadResult{}, fmt.Errorf("qrz: bad response %q: %w", body, err)
|
||||
}
|
||||
status := strings.ToUpper(strings.TrimSpace(vals.Get("STATUS")))
|
||||
// The QRZ Logbook API returns the outcome in RESULT (=OK/FAIL/AUTH).
|
||||
// Accept STATUS as a fallback for robustness, but RESULT is the real
|
||||
// field — reading only STATUS made every INSERT (incl. successful ones)
|
||||
// look like it failed with an empty status.
|
||||
status := qrzStatusField(vals)
|
||||
reason := strings.TrimSpace(vals.Get("REASON"))
|
||||
logID := strings.TrimSpace(vals.Get("LOGID"))
|
||||
|
||||
@@ -222,6 +226,15 @@ func parseQRZResponse(body string) (UploadResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// qrzStatusField returns the QRZ outcome code, preferring RESULT (the
|
||||
// Logbook API's real field) and falling back to STATUS.
|
||||
func qrzStatusField(vals url.Values) string {
|
||||
if v := strings.ToUpper(strings.TrimSpace(vals.Get("RESULT"))); v != "" {
|
||||
return v
|
||||
}
|
||||
return strings.ToUpper(strings.TrimSpace(vals.Get("STATUS")))
|
||||
}
|
||||
|
||||
// isDuplicateReason recognises the various phrasings QRZ uses when a QSO is
|
||||
// already present.
|
||||
func isDuplicateReason(reason string) bool {
|
||||
|
||||
Reference in New Issue
Block a user