feat: follow the SUB VFO over OmniRig; distance column in the QSO grids
OmniRig reports the VFO pair (AA/AB/BA/BB) on the whole Yaesu range and never the single-letter form. Only the latter was honoured, so every rig in that family stayed pinned to VFO A: pressing SUB moved the radio but not OpsLog, and a QSO worked on SUB was logged on the main VFO's frequency. The first letter of the pair is the VFO being listened on — Log4OM reads it and gets the right frequency on the same rigs, which is what showed the data was there. The enum now wins over the Yaesu Freq==FreqB inference, which is only a fallback for a rig file that names no VFO at all (the stock FTDX10 one answers neither VS; nor FR; usably — verified on the air). Split: the ON flag is still latched to survive a rig file that flips it on its own, but the latch is now ARMED only after 8 flips in 30 s. A first cut at 3-in-15s was armed by the operator toggling split while testing, imposing the 6 s clearing delay on a radio that did not need it; a misreading file flips a dozen times in that window untouched, so the two cases separate cleanly. Distance (km) column added to Recent QSOs and Worked before (shared catalog). Computed from the QSO's OWN my_grid/my_lat/lon first, falling back to the current profile's locator: a log spans years and portable outings, so the station a QSO was made from is not necessarily today's. Locator: a precise QRZ/HamQTH grid is no longer overwritten by the cty.dat entity centroid. The lookup runs several times per QSO and the provider gets 2 s; a slow second answer fell back to cty.dat and downgraded JN05JG to JN16 while name and QTH survived (they are only written when non-empty). The OmniRig diagnostic line now logs what OpsLog concluded, not just what OmniRig reported. icomnet.go: gofmt alignment only.
This commit is contained in:
+100
-14
@@ -46,6 +46,19 @@ type OmniRig struct {
|
||||
// the sideband (freq moved, but mode read the old band → wrong sideband).
|
||||
lastSetFreq int64
|
||||
lastSetFreqAt time.Time
|
||||
|
||||
// lastSplitOnAt is when OmniRig last reported PM_SPLITON cleanly. See the
|
||||
// FTDX101D note in ReadState — some .ini files alternate between ON and OFF
|
||||
// on consecutive polls, so the flag has to be latched to be usable.
|
||||
lastSplitOnAt time.Time
|
||||
|
||||
// splitFlaky records that THIS rig's .ini flips the split flag on its own,
|
||||
// which is what arms the latch. lastSplitFlag / splitFlips / splitFlipWindow
|
||||
// count the flips inside a rolling window to detect it.
|
||||
lastSplitFlag bool
|
||||
splitFlaky bool
|
||||
splitFlips int
|
||||
splitFlipWindow time.Time
|
||||
}
|
||||
|
||||
// NewOmniRig creates a non-connected backend. Call Connect before use.
|
||||
@@ -251,21 +264,61 @@ func (o *OmniRig) ReadState() (RigState, error) {
|
||||
splitRaw = v.Val
|
||||
}
|
||||
|
||||
// Diagnostic logged ONLY when Split or VFO changes (not on a timer), so
|
||||
// normal operation stays quiet but toggling split on the radio is captured —
|
||||
// needed to pin down this rig's PM_SPLITON value.
|
||||
// FTDX101D field capture: OmniRig alternates between two contradictory
|
||||
// readings on consecutive polls — "Vfo=AB Split=0x10000(OFF)" then
|
||||
// "Vfo=BA Split=0x8000(ON)", ~1.5 s apart, with the rig untouched. The stock
|
||||
// .ini evidently has two status commands that each write these params. A
|
||||
// sample-by-sample test therefore reports split for half the polls and no
|
||||
// split for the other half, which the UI shows as no split at all. Latch the
|
||||
// ON flag briefly so one truthful sample survives the contradicting one; the
|
||||
// latch expires on its own once the rig stops reporting ON, so cancelling
|
||||
// split on the radio still clears within a few seconds.
|
||||
//
|
||||
// The latch is ARMED ONLY for a rig that actually oscillates, because it costs
|
||||
// ~6 s before split clears on screen. A correct .ini (FTDX10 with VS; and FT;
|
||||
// read as separate frames, confirmed on the air 2026-07-26) never flips
|
||||
// unprompted, and there the latch would be a pure delay on a reading that was
|
||||
// already right.
|
||||
//
|
||||
// 8 flips in 30 s: a first cut at 3-in-15s was armed by the OPERATOR toggling
|
||||
// split three times while testing, which then imposed the 6 s delay on a rig
|
||||
// that did not need it. A misreading .ini flips every 1.5–3 s without being
|
||||
// touched — a dozen in the same window — so the gap is wide. The arming also
|
||||
// expires after 30 s without a flip, so a rig that behaves is never stuck with
|
||||
// the delay because of one burst.
|
||||
const flipWindow, flipsToArm = 30 * time.Second, 8
|
||||
now := time.Now()
|
||||
flagOn := splitRaw&pmSplitOn != 0 && splitRaw&pmSplitOff == 0
|
||||
if flagOn {
|
||||
o.lastSplitOnAt = now
|
||||
}
|
||||
if flagOn != o.lastSplitFlag {
|
||||
o.lastSplitFlag = flagOn
|
||||
if now.Sub(o.splitFlipWindow) > flipWindow {
|
||||
o.splitFlipWindow, o.splitFlips, o.splitFlaky = now, 0, false
|
||||
}
|
||||
o.splitFlips++
|
||||
if o.splitFlips >= flipsToArm {
|
||||
o.splitFlaky = true
|
||||
}
|
||||
} else if o.splitFlaky && now.Sub(o.splitFlipWindow) > flipWindow {
|
||||
o.splitFlaky, o.splitFlips = false, 0 // stopped oscillating — drop the delay
|
||||
}
|
||||
splitRecentOn := o.splitFlaky && !o.lastSplitOnAt.IsZero() && now.Sub(o.lastSplitOnAt) < 6*time.Second
|
||||
|
||||
s.FreqHz, s.RxFreqHz, s.Split = resolveOmniRigVFOs(o.rigType, freqMain, freqA, freqB, s.Vfo, splitRaw, splitRecentOn)
|
||||
|
||||
// Diagnostic logged ONLY when Split or VFO changes (not on a timer), so normal
|
||||
// operation stays quiet but toggling split or SUB VFO on the radio is
|
||||
// captured. It logs the RESOLVED tx/rx/split too: with the raw values alone a
|
||||
// user's log showed what OmniRig said but not what OpsLog concluded, which is
|
||||
// the half that was wrong.
|
||||
if sig := fmt.Sprintf("%x:%x", splitRaw, rawVfo); sig != o.lastSig {
|
||||
o.lastSig = sig
|
||||
debugLog.Printf("OmniRig Rig%d raw: Freq=%d FreqA=%d FreqB=%d Vfo=%q(raw=0x%X) Split=0x%X status=%d",
|
||||
o.RigNum, freqMain, freqA, freqB, s.Vfo, rawVfo, splitRaw, func() int64 {
|
||||
if v, e := oleutil.GetProperty(o.rig, "Status"); e == nil {
|
||||
return v.Val
|
||||
}
|
||||
return -1
|
||||
}())
|
||||
debugLog.Printf("OmniRig Rig%d raw: rig=%q Freq=%d FreqA=%d FreqB=%d Vfo=%q(raw=0x%X) Split=0x%X sticky=%v → tx=%d rx=%d split=%v",
|
||||
o.RigNum, o.rigType, freqMain, freqA, freqB, s.Vfo, rawVfo, splitRaw, splitRecentOn,
|
||||
s.FreqHz, s.RxFreqHz, s.Split)
|
||||
}
|
||||
|
||||
s.FreqHz, s.RxFreqHz, s.Split = resolveOmniRigVFOs(freqMain, freqA, freqB, s.Vfo, splitRaw)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -276,14 +329,14 @@ func (o *OmniRig) ReadState() (RigState, error) {
|
||||
// contradict each other — what fixes a Yaesu can break an Icom — and the only way
|
||||
// to change it safely is with every known rig's behaviour pinned in a test. COM
|
||||
// cannot be exercised from a test; this can.
|
||||
func resolveOmniRigVFOs(freqMain, freqA, freqB int64, vfo string, splitRaw int64) (txHz, rxHz int64, split bool) {
|
||||
func resolveOmniRigVFOs(rigType string, freqMain, freqA, freqB int64, vfo string, splitRaw int64, splitRecentOn bool) (txHz, rxHz int64, split bool) {
|
||||
// PM_SPLITON is tested as a BIT, not by equality. OmniRig's Split is a flag
|
||||
// word: an exact `== 0x8000` holds only for a rig whose ini sets that bit and
|
||||
// nothing else, and silently reports "no split" for any rig reporting the bit
|
||||
// alongside another. Requiring ON set and OFF clear keeps the two states apart
|
||||
// (both flags are non-zero, so a bare `!= 0` would read OFF as split) while
|
||||
// tolerating extra bits.
|
||||
splitFlagged := splitRaw&pmSplitOn != 0 && splitRaw&pmSplitOff == 0
|
||||
splitFlagged := (splitRaw&pmSplitOn != 0 && splitRaw&pmSplitOff == 0) || splitRecentOn
|
||||
|
||||
// A genuine split also needs two distinct, non-zero VFOs in the SAME band. The
|
||||
// band test kills the common false positive where VFO B merely holds a
|
||||
@@ -316,6 +369,32 @@ func resolveOmniRigVFOs(freqMain, freqA, freqB int64, vfo string, splitRaw int64
|
||||
// the IC-7610, whose stock ini reports the generic Freq as VFO B; and for the
|
||||
// PM_FREQA rigs (Yaesu, Kenwood) versus the Icoms (IC-9100) that populate only
|
||||
// the generic Freq.
|
||||
// The PAIR enums name BOTH VFOs at once — first letter = the one being
|
||||
// listened on, second = the one that transmits. Only the single-letter forms
|
||||
// were honoured here, so a rig that reports nothing but pairs (the whole Yaesu
|
||||
// family) always fell through to freqA and never followed the operator to SUB.
|
||||
// Log4OM reads that first letter and logs the right frequency on the same
|
||||
// rigs, which is what showed this was readable data and not a dead end.
|
||||
//
|
||||
// Checked BEFORE the Yaesu fallback below: an enum that names a VFO is the
|
||||
// rig speaking, the fallback is only an inference.
|
||||
switch {
|
||||
case (vfo == "BA" || vfo == "BB") && freqB != 0:
|
||||
return freqB, 0, false
|
||||
case (vfo == "AA" || vfo == "AB") && freqA != 0:
|
||||
return freqA, 0, false
|
||||
}
|
||||
|
||||
// Yaesu fallback, for a rig file that names no VFO at all (the stock FTDX10
|
||||
// one: it answers neither VS; nor FR; usably, verified on the air 2026-07-26).
|
||||
// There the generic Freq is the last clue — matching FreqB and not FreqA means
|
||||
// the operator is on SUB. Limited to Yaesu: the IC-7610's stock ini reports
|
||||
// the generic Freq as VFO B permanently, where this would name the wrong VFO —
|
||||
// that case is pinned in the test table.
|
||||
if isYaesuRig(rigType) && freqMain != 0 && freqMain == freqB && freqB != freqA {
|
||||
return freqB, 0, false
|
||||
}
|
||||
|
||||
switch {
|
||||
case (vfo == "B" || vfo == "BB") && freqB != 0:
|
||||
return freqB, 0, false
|
||||
@@ -601,6 +680,13 @@ func omniRigMode(m int64) string {
|
||||
|
||||
// omniRigVfo maps the OmniRig Vfo RigParamX enum to a short label, using the
|
||||
// documented PM_VFO* constants.
|
||||
// isYaesuRig recognises a Yaesu from OmniRig's RigType (the .ini title, e.g.
|
||||
// "FTDX101D", "FT-891"). Only used to gate rules that are true for Yaesu and
|
||||
// false for Icom, so a mis-titled ini simply keeps the generic behaviour.
|
||||
func isYaesuRig(rigType string) bool {
|
||||
return strings.HasPrefix(strings.ToUpper(strings.TrimSpace(rigType)), "FT")
|
||||
}
|
||||
|
||||
func omniRigVfo(v int64) string {
|
||||
switch {
|
||||
case v&0x40 != 0: // PM_VFOAA
|
||||
|
||||
Reference in New Issue
Block a user