chore: release v0.21.3
This commit is contained in:
+162
-47
@@ -34,6 +34,11 @@ type OmniRig struct {
|
||||
lastSig string // last logged Split/VFO signature — only log on change
|
||||
rigType string // OmniRig's RigType string (the .ini title), e.g. "IC-7610"
|
||||
|
||||
// connLogged holds the connect failure already written to the log, so the
|
||||
// 5-second reconnect loop reports a persistent problem once instead of
|
||||
// forever. Cleared on success.
|
||||
connLogged string
|
||||
|
||||
// lastSetFreq is the frequency most recently COMMANDED via SetFrequency.
|
||||
// SetMode uses it to pick USB vs LSB for "SSB" instead of reading OmniRig's
|
||||
// async Freq property, which still reports the OLD band for a poll or two
|
||||
@@ -53,8 +58,55 @@ func NewOmniRig(rigNum int) *OmniRig {
|
||||
|
||||
func (o *OmniRig) Name() string { return "omnirig" }
|
||||
|
||||
// elevationHint recognises the COM refusal that happens when OmniRig runs
|
||||
// elevated (as administrator) and OpsLog does not — or the reverse. Windows keeps
|
||||
// the two integrity levels apart, so the client cannot bind to the running
|
||||
// server's object and COM falls back to launching a fresh one, which then needs
|
||||
// elevation the client cannot grant.
|
||||
//
|
||||
// It is worth naming explicitly: the operator SEES OmniRig running, with its
|
||||
// settings window open, so "OmniRig not found" reads as nonsense and sends them
|
||||
// hunting for a driver or COM-port problem that does not exist. The fix is thirty
|
||||
// seconds of work once you know what to look for.
|
||||
func elevationHint(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
msg := strings.ToLower(err.Error())
|
||||
// Matched on the HRESULT text in whatever language Windows is running in, so
|
||||
// the code is checked too: 0x800702E4 = ERROR_ELEVATION_REQUIRED.
|
||||
if strings.Contains(msg, "elevation") || strings.Contains(msg, "élévation") ||
|
||||
strings.Contains(msg, "0x800702e4") || strings.Contains(msg, "access denied") ||
|
||||
strings.Contains(msg, "accès refusé") {
|
||||
return "OmniRig and OpsLog are running at different privilege levels — Windows keeps them apart, " +
|
||||
"so OpsLog cannot reach OmniRig even though it is running. Start BOTH the same way: either " +
|
||||
"un-tick \"Run as administrator\" on the OmniRig shortcut (and its Compatibility tab), or run " +
|
||||
"OpsLog as administrator too"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// logConnFailure writes a connect failure once per distinct cause. The reconnect
|
||||
// loop retries every 5 seconds forever, and a station whose OmniRig was simply
|
||||
// elevated had this filling its log at roughly 1500 lines an hour — which buries
|
||||
// the very diagnostics someone would go looking for.
|
||||
func (o *OmniRig) logConnFailure(msg string) {
|
||||
if o.connLogged == msg {
|
||||
return
|
||||
}
|
||||
o.connLogged = msg
|
||||
debugLog.Printf("OmniRig Rig%d: %s", o.RigNum, msg)
|
||||
}
|
||||
|
||||
func (o *OmniRig) Connect() error {
|
||||
debugLog.Printf("OmniRig.Connect Rig%d — log path: %s", o.RigNum, DebugLogPath())
|
||||
// This used to announce DebugLogPath() on every attempt — the path of the
|
||||
// FALLBACK cat.log, which nothing writes to once the app has wired LogSink and
|
||||
// everything goes to data\opslog.log. It pointed operators at an empty file in
|
||||
// %APPDATA% while the lines they wanted were somewhere else entirely. Dropped;
|
||||
// and logged once per failure run rather than every 5-second retry.
|
||||
if o.connLogged == "" {
|
||||
debugLog.Printf("OmniRig.Connect Rig%d", o.RigNum)
|
||||
}
|
||||
if err := ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED); err != nil {
|
||||
// 0x1 = S_FALSE → COM already initialised on this thread, fine.
|
||||
if oerr, ok := err.(*ole.OleError); !ok || oerr.Code() != 0x00000001 {
|
||||
@@ -62,15 +114,43 @@ func (o *OmniRig) Connect() error {
|
||||
}
|
||||
}
|
||||
|
||||
unk, err := oleutil.CreateObject("Omnirig.OmnirigX")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Omnirig.OmnirigX not available — is OmniRig installed and running?: %w", err)
|
||||
}
|
||||
omnirig, err := unk.QueryInterface(ole.IID_IDispatch)
|
||||
unk.Release()
|
||||
if err != nil {
|
||||
return fmt.Errorf("query interface: %w", err)
|
||||
const progID = "Omnirig.OmnirigX"
|
||||
var omnirig *ole.IDispatch
|
||||
unk, err := oleutil.CreateObject(progID)
|
||||
if err == nil {
|
||||
omnirig, err = unk.QueryInterface(ole.IID_IDispatch)
|
||||
unk.Release()
|
||||
if err != nil {
|
||||
return fmt.Errorf("query interface: %w", err)
|
||||
}
|
||||
} else {
|
||||
// A privilege mismatch is final — retrying, or trying the 32-bit server,
|
||||
// cannot cross an integrity boundary. Say what to do instead of dressing it
|
||||
// up as "not installed", which is what sends operators looking in the wrong
|
||||
// place entirely.
|
||||
if hint := elevationHint(err); hint != "" {
|
||||
o.logConnFailure(hint)
|
||||
return fmt.Errorf("%s (Windows said: %v)", hint, err)
|
||||
}
|
||||
// Otherwise it may be a partial registration; try activating the 32-bit
|
||||
// server explicitly before giving up (see omnirig_activate32.go).
|
||||
disp, err32 := createOmniRig32(progID)
|
||||
if err32 != nil {
|
||||
o.logConnFailure(fmt.Sprintf("CreateObject(%s) failed: %v; 32-bit activation also failed: %v", progID, err, err32))
|
||||
// Name the version requirement. HB9RYZ's OmniRig v2.1 is a different
|
||||
// product that its own author states is not compatible with v1, and it
|
||||
// does not provide v1's IOmniRigX interface — so an operator who has
|
||||
// only v2 installed sees OmniRig running and OpsLog failing, with
|
||||
// nothing to connect the two facts.
|
||||
return fmt.Errorf("OmniRig (v1) not reachable: %w — OpsLog needs OmniRig v1.19/v1.20 "+
|
||||
"(VE3NEA/Alex), the interface every logger uses. HB9RYZ's OmniRig v2.1 is a separate, "+
|
||||
"incompatible product and cannot serve OpsLog; the two may be installed side by side, "+
|
||||
"but v1 must be present and running", err)
|
||||
}
|
||||
debugLog.Printf("OmniRig: reached via explicit 32-bit activation after CreateObject failed (%v)", err)
|
||||
omnirig = disp
|
||||
}
|
||||
o.connLogged = "" // connected: re-arm the one-shot failure logging
|
||||
|
||||
rigVar, err := oleutil.GetProperty(omnirig, fmt.Sprintf("Rig%d", o.RigNum))
|
||||
if err != nil {
|
||||
@@ -80,10 +160,26 @@ func (o *OmniRig) Connect() error {
|
||||
o.omnirig = omnirig
|
||||
o.rig = rigVar.ToIDispatch()
|
||||
|
||||
// Log WHICH OmniRig answered. There are two incompatible products called
|
||||
// OmniRig: v1.19/1.20 (Alex, VE3NEA), whose IOmniRigX interface every logger
|
||||
// including OpsLog uses, and HB9RYZ's v2.1, which its own author states is "not
|
||||
// compatible" with v1 and works only with programs written for it. They can
|
||||
// coexist, and OmniRig's own window looks much the same either way — so an
|
||||
// operator running only v2 sees OmniRig on screen, sees OpsLog fail, and has no
|
||||
// way to know the two were never going to talk. These two version numbers
|
||||
// settle it in one line of a bug report.
|
||||
var iv, sv int64 = -1, -1
|
||||
if v, err := oleutil.GetProperty(o.omnirig, "InterfaceVersion"); err == nil {
|
||||
iv = v.Val
|
||||
}
|
||||
if v, err := oleutil.GetProperty(o.omnirig, "SoftwareVersion"); err == nil {
|
||||
sv = v.Val
|
||||
}
|
||||
if rt, err := oleutil.GetProperty(o.rig, "RigType"); err == nil {
|
||||
o.rigType = rt.ToString()
|
||||
debugLog.Printf("OmniRig connected to Rig%d type=%q", o.RigNum, o.rigType)
|
||||
}
|
||||
debugLog.Printf("OmniRig connected: Rig%d type=%q (OmniRig interface=%d software=%d)",
|
||||
o.RigNum, o.rigType, iv, sv)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -169,50 +265,69 @@ func (o *OmniRig) ReadState() (RigState, error) {
|
||||
}())
|
||||
}
|
||||
|
||||
// A genuine split: the rig explicitly flags PM_SPLITON, the two VFOs are
|
||||
// distinct and non-zero, AND they're in the same band. The same-band test
|
||||
// kills the common false positive where VFO B just holds a leftover from
|
||||
// another band (a "28 MHz / 7 MHz split" is nonsensical), which on the
|
||||
// FT-710 / TS-570 otherwise froze the main/TX freq on the wrong VFO.
|
||||
genuineSplit := splitRaw == pmSplitOn &&
|
||||
freqA != 0 && freqB != 0 && freqA != freqB &&
|
||||
BandFromHz(freqA) == BandFromHz(freqB)
|
||||
s.FreqHz, s.RxFreqHz, s.Split = resolveOmniRigVFOs(freqMain, freqA, freqB, s.Vfo, splitRaw)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
if genuineSplit {
|
||||
// ADIF: FreqHz = TX, RxFreqHz = RX. Determine which VFO is RX from the
|
||||
// ACTIVE frequency (OmniRig's generic Freq — the VFO you're listening on):
|
||||
// RX = the active VFO, TX = the other one. This is far more reliable than
|
||||
// trusting OmniRig's Vfo AB/BA enum, which several rigs (e.g. Yaesu FTDX10)
|
||||
// report inverted — the split then showed TX/RX swapped.
|
||||
s.Split = true
|
||||
// resolveOmniRigVFOs turns OmniRig's four readings into the ADIF pair
|
||||
// (FreqHz = TX, RxFreqHz = RX) plus a split flag.
|
||||
//
|
||||
// Pure and separate from ReadState because it encodes rig-specific rules that
|
||||
// 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) {
|
||||
// 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
|
||||
|
||||
// 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
|
||||
// leftover from another band (a "28 MHz / 7 MHz split" is nonsensical), which
|
||||
// on the FT-710 / TS-570 otherwise froze the TX freq on the wrong VFO.
|
||||
if splitFlagged && freqA != 0 && freqB != 0 && freqA != freqB &&
|
||||
BandFromHz(freqA) == BandFromHz(freqB) {
|
||||
// RX is the VFO being listened on — identified from the generic Freq rather
|
||||
// than from the Vfo AB/BA enum, which several rigs (Yaesu FTDX10) report
|
||||
// inverted, showing TX and RX swapped.
|
||||
switch {
|
||||
case freqMain != 0 && freqMain == freqA:
|
||||
s.RxFreqHz, s.FreqHz = freqA, freqB // listening on A → TX on B
|
||||
return freqB, freqA, true // listening on A → TX on B
|
||||
case freqMain != 0 && freqMain == freqB:
|
||||
s.RxFreqHz, s.FreqHz = freqB, freqA // listening on B → TX on A
|
||||
case s.Vfo == "BA":
|
||||
s.FreqHz, s.RxFreqHz = freqA, freqB // fall back to the Vfo enum
|
||||
return freqA, freqB, true // listening on B → TX on A
|
||||
case vfo == "BA":
|
||||
return freqA, freqB, true // fall back to the Vfo enum
|
||||
default:
|
||||
s.FreqHz, s.RxFreqHz = freqB, freqA
|
||||
}
|
||||
} else {
|
||||
// Simplex: read VFO A first, fall back to the generic Freq — exactly like
|
||||
// DXHunter/WSJT-X. PM_FREQA rigs (Yaesu, Kenwood) populate FreqA; some
|
||||
// Icoms (IC-9100 etc.) only populate the generic Freq. On the IC-7610
|
||||
// OmniRig's generic Freq reports VFO B (its Main/Sub model confuses the
|
||||
// stock ini), so keying off FreqA gives the operator the VFO they expect.
|
||||
s.Split = false
|
||||
s.RxFreqHz = 0
|
||||
switch {
|
||||
case freqA != 0:
|
||||
s.FreqHz = freqA
|
||||
case freqMain != 0:
|
||||
s.FreqHz = freqMain
|
||||
default:
|
||||
s.FreqHz = freqB
|
||||
return freqB, freqA, true
|
||||
}
|
||||
}
|
||||
return s, nil
|
||||
|
||||
// Simplex. The VFO the rig says is ACTIVE comes first: preferring freqA
|
||||
// unconditionally (as this did) meant the displayed frequency never left VFO
|
||||
// A — press SUB VFO on an FTDX101D and the radio receives on B while OpsLog
|
||||
// went on showing A, taking the band and the logged frequency with it.
|
||||
//
|
||||
// Only a VFO OmniRig explicitly names is honoured, so a rig that does not
|
||||
// report the enum keeps exactly the previous fallback order. That matters for
|
||||
// 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.
|
||||
switch {
|
||||
case (vfo == "B" || vfo == "BB") && freqB != 0:
|
||||
return freqB, 0, false
|
||||
case (vfo == "A" || vfo == "AA") && freqA != 0:
|
||||
return freqA, 0, false
|
||||
case freqA != 0:
|
||||
return freqA, 0, false
|
||||
case freqMain != 0:
|
||||
return freqMain, 0, false
|
||||
default:
|
||||
return freqB, 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OmniRig) SetFrequency(hz int64) error {
|
||||
|
||||
Reference in New Issue
Block a user