417 lines
13 KiB
Go
417 lines
13 KiB
Go
package cat
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/go-ole/go-ole"
|
|
"github.com/go-ole/go-ole/oleutil"
|
|
)
|
|
|
|
// OmniRig talks to the user's installed OmniRig server over COM.
|
|
//
|
|
// All methods MUST be called from the same OS thread (the one Manager.run
|
|
// locks). COM is thread-affine on Windows — calling these from random
|
|
// goroutines will return E_FAIL or crash.
|
|
//
|
|
// The user must install OmniRig separately and configure their rig (COM port,
|
|
// baud rate) in OmniRig's own GUI. HamLog just reads/writes through it.
|
|
type OmniRig struct {
|
|
RigNum int // 1 (Rig1) or 2 (Rig2)
|
|
|
|
omnirig *ole.IDispatch
|
|
rig *ole.IDispatch
|
|
}
|
|
|
|
// NewOmniRig creates a non-connected backend. Call Connect before use.
|
|
func NewOmniRig(rigNum int) *OmniRig {
|
|
if rigNum < 1 || rigNum > 2 {
|
|
rigNum = 1
|
|
}
|
|
return &OmniRig{RigNum: rigNum}
|
|
}
|
|
|
|
func (o *OmniRig) Name() string { return "omnirig" }
|
|
|
|
func (o *OmniRig) Connect() error {
|
|
debugLog.Printf("OmniRig.Connect Rig%d — log path: %s", o.RigNum, DebugLogPath())
|
|
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 {
|
|
return fmt.Errorf("CoInitializeEx: %w", err)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
rigVar, err := oleutil.GetProperty(omnirig, fmt.Sprintf("Rig%d", o.RigNum))
|
|
if err != nil {
|
|
omnirig.Release()
|
|
return fmt.Errorf("get Rig%d: %w", o.RigNum, err)
|
|
}
|
|
o.omnirig = omnirig
|
|
o.rig = rigVar.ToIDispatch()
|
|
|
|
if rt, err := oleutil.GetProperty(o.rig, "RigType"); err == nil {
|
|
debugLog.Printf("OmniRig connected to Rig%d type=%q", o.RigNum, rt.ToString())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *OmniRig) Disconnect() {
|
|
if o.rig != nil {
|
|
o.rig.Release()
|
|
o.rig = nil
|
|
}
|
|
if o.omnirig != nil {
|
|
o.omnirig.Release()
|
|
o.omnirig = nil
|
|
}
|
|
ole.CoUninitialize()
|
|
}
|
|
|
|
func (o *OmniRig) ReadState() (RigState, error) {
|
|
if o.rig == nil {
|
|
return RigState{}, fmt.Errorf("not connected")
|
|
}
|
|
var s RigState
|
|
s.Backend = o.Name()
|
|
s.RigNum = o.RigNum
|
|
|
|
// Status: 0 = NOTCONFIGURED, 1 = DISABLED, 2 = PORTBUSY,
|
|
// 3 = NOTRESPONDING, 4 = ONLINE.
|
|
if statusVar, err := oleutil.GetProperty(o.rig, "Status"); err == nil {
|
|
s.Connected = statusVar.Val == 4
|
|
}
|
|
|
|
if rigTypeVar, err := oleutil.GetProperty(o.rig, "RigType"); err == nil {
|
|
s.Rig = rigTypeVar.ToString()
|
|
}
|
|
|
|
if !s.Connected {
|
|
// Status string from OmniRig is informative for the user.
|
|
if statusStrVar, err := oleutil.GetProperty(o.rig, "StatusStr"); err == nil {
|
|
s.Error = statusStrVar.ToString()
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
if modeVar, err := oleutil.GetProperty(o.rig, "Mode"); err == nil {
|
|
s.Mode = omniRigMode(modeVar.Val)
|
|
}
|
|
if vfoVar, err := oleutil.GetProperty(o.rig, "Vfo"); err == nil {
|
|
s.Vfo = omniRigVfo(vfoVar.Val)
|
|
}
|
|
|
|
// Read both VFO frequencies separately so we can expose split TX/RX.
|
|
// Fall back to generic Freq if the rig only exposes the merged property.
|
|
freqA, freqB := int64(0), int64(0)
|
|
if v, err := oleutil.GetProperty(o.rig, "FreqA"); err == nil {
|
|
freqA = v.Val
|
|
}
|
|
if v, err := oleutil.GetProperty(o.rig, "FreqB"); err == nil {
|
|
freqB = v.Val
|
|
}
|
|
|
|
// Split detection: trust the explicit Split property when it's set,
|
|
// BUT only call it a real split if both VFO frequencies are non-zero
|
|
// and distinct. Bridges like SmartSDR-OmniRig report Split=ON by
|
|
// default (raw bit PM_SPLITON = 65536) with FreqB=0 because the Flex's
|
|
// slice model doesn't map to VFO A/B — that would yield a useless
|
|
// permanent SPLIT badge.
|
|
if v, err := oleutil.GetProperty(o.rig, "Split"); err == nil && v.Val != 0 {
|
|
s.Split = true
|
|
}
|
|
if s.Split && (freqB == 0 || freqA == freqB) {
|
|
s.Split = false
|
|
s.RxFreqHz = 0
|
|
}
|
|
|
|
// OmniRig's Vfo enum is RX-letter then TX-letter (AB = RX A, TX B).
|
|
// We follow ADIF: FreqHz = TX, RxFreqHz = RX (only meaningful in split).
|
|
switch s.Vfo {
|
|
case "AB":
|
|
s.FreqHz = freqB // TX
|
|
s.RxFreqHz = freqA // RX
|
|
case "BA":
|
|
s.FreqHz = freqA // TX
|
|
s.RxFreqHz = freqB // RX
|
|
case "B", "BB":
|
|
s.FreqHz = freqB
|
|
default: // "A", "AA", "" — single VFO on A or unknown
|
|
s.FreqHz = freqA
|
|
}
|
|
if s.FreqHz == 0 {
|
|
// Last resort — some rigs only update generic Freq.
|
|
if v, err := oleutil.GetProperty(o.rig, "Freq"); err == nil {
|
|
s.FreqHz = v.Val
|
|
}
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
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)
|
|
|
|
// 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
|
|
}
|
|
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"
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// SetMode maps an ADIF mode to the OmniRig PM_* bit and pushes it to the rig.
|
|
// For SSB, the USB/LSB side is chosen from the rig's current frequency
|
|
// following worldwide convention (LSB below 14 MHz, USB above).
|
|
//
|
|
// IMPORTANT: OmniRig's Mode property is typed as Long (VT_I4). go-ole would
|
|
// otherwise wrap a Go int64 into a VT_I8 variant which COM marshalling can
|
|
// reject silently or misinterpret — passing the wrong bit. Always cast to
|
|
// int32 explicitly.
|
|
//
|
|
// Logs each call to stdout so the user can cross-check what HamLog sent
|
|
// against OmniRig's Monitor window (right-click systray → Monitor) to find
|
|
// rig-specific mismatches (e.g. a Kenwood without FM on HF, an .ini with the
|
|
// wrong CAT command for a mode, etc.).
|
|
func (o *OmniRig) SetMode(mode string) error {
|
|
if o.rig == nil {
|
|
return fmt.Errorf("not connected")
|
|
}
|
|
var (
|
|
bit int64
|
|
bitName string
|
|
)
|
|
switch strings.ToUpper(strings.TrimSpace(mode)) {
|
|
case "CW":
|
|
bit, bitName = pmCWU, "PM_CW_U"
|
|
case "SSB":
|
|
// Read current freq to decide USB vs LSB.
|
|
var freq int64
|
|
if freqVar, err := oleutil.GetProperty(o.rig, "Freq"); err == nil {
|
|
freq = freqVar.Val
|
|
}
|
|
if freq > 0 && freq < 10_000_000 {
|
|
bit, bitName = pmSSBL, "PM_SSB_L"
|
|
} else {
|
|
bit, bitName = pmSSBU, "PM_SSB_U"
|
|
}
|
|
case "AM":
|
|
bit, bitName = pmAM, "PM_AM"
|
|
case "FM":
|
|
bit, bitName = pmFM, "PM_FM"
|
|
case "RTTY", "FSK":
|
|
// OmniRig has no specific RTTY/FSK mode — falls back to generic
|
|
// digital USB. Many rigs need RTTY selected manually on the panel.
|
|
bit, bitName = pmDIGU, "PM_DIG_U"
|
|
case "FT8", "FT4", "PSK31", "MFSK", "JS8", "JT65", "JT9", "OLIVIA", "DIGITALVOICE", "DATA":
|
|
bit, bitName = pmDIGU, "PM_DIG_U"
|
|
default:
|
|
return fmt.Errorf("OmniRig: unsupported mode %q", mode)
|
|
}
|
|
debugLog.Printf("OmniRig.SetMode(%q) → %s = 0x%08X (%d)", mode, bitName, bit, bit)
|
|
_, err := oleutil.PutProperty(o.rig, "Mode", int32(bit))
|
|
if err != nil {
|
|
debugLog.Printf("OmniRig.SetMode error: %v", err)
|
|
return fmt.Errorf("SetMode(%s) → %s: %w", mode, bitName, err)
|
|
}
|
|
|
|
// Read back what OmniRig now thinks the rig is on (best-effort —
|
|
// OmniRig is async so this may still be the old value for one poll).
|
|
if mv, err := oleutil.GetProperty(o.rig, "Mode"); err == nil {
|
|
debugLog.Printf("OmniRig.Mode immediately after Put = 0x%08X (%d) → %s",
|
|
mv.Val, mv.Val, omniRigMode(mv.Val))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetPTT keys or unkeys the rig via OmniRig's SetTx(PM_RX|PM_TX). Used by the
|
|
// Digital Voice Keyer to put the rig into TX while a voice message plays.
|
|
func (o *OmniRig) SetPTT(on bool) error {
|
|
if o.rig == nil {
|
|
debugLog.Printf("OmniRig.SetPTT(%v): NOT CONNECTED", on)
|
|
return fmt.Errorf("not connected")
|
|
}
|
|
status, statusStr, writeable := int64(-1), "", int64(-1)
|
|
if v, err := oleutil.GetProperty(o.rig, "Status"); err == nil {
|
|
status = v.Val
|
|
}
|
|
if v, err := oleutil.GetProperty(o.rig, "StatusStr"); err == nil {
|
|
statusStr = v.ToString()
|
|
}
|
|
if v, err := oleutil.GetProperty(o.rig, "WriteableParams"); err == nil {
|
|
writeable = v.Val
|
|
}
|
|
txWriteable := writeable != -1 && writeable&pmTX != 0
|
|
param, name := pmRX, "PM_RX"
|
|
if on {
|
|
param, name = pmTX, "PM_TX"
|
|
}
|
|
debugLog.Printf("OmniRig.SetPTT(%v): status=%d(%s) writeableParams=0x%X PM_TX-writeable=%v → Tx=%s",
|
|
on, status, statusStr, writeable, txWriteable, name)
|
|
if on && !txWriteable {
|
|
debugLog.Printf("OmniRig.SetPTT: ⚠ this rig's OmniRig .ini does NOT expose TX keying (PM_TX not writeable). " +
|
|
"Use VOX or serial RTS/DTR PTT instead.")
|
|
}
|
|
// OmniRig has NO SetTx method (that returns "unknown name"); the Tx
|
|
// parameter is set via the writeable Tx PROPERTY (PM_TX / PM_RX).
|
|
if _, err := oleutil.PutProperty(o.rig, "Tx", int32(param)); err != nil {
|
|
debugLog.Printf("OmniRig.SetPTT error: %v", err)
|
|
return fmt.Errorf("set Tx=%s: %w", name, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ===== OmniRig enum decoders =====
|
|
|
|
// Bit flags from OmniRig type library (RigParamX enum in OmniRig_TLB.pas).
|
|
//
|
|
// Cross-checked against https://github.com/VE3NEA/OmniRig — be careful when
|
|
// referencing other people's writeups online, several have these one bit
|
|
// too low which causes every mode to map to the slot below it (AM → DIG_L,
|
|
// FT8 → SSB_L, etc.).
|
|
const (
|
|
pmRX int64 = 1 << 20 // 0x00100000 — PM_RX (receive)
|
|
pmTX int64 = 1 << 21 // 0x00200000 — PM_TX (transmit / PTT on)
|
|
pmCWU int64 = 1 << 23 // 0x00800000
|
|
pmCWL int64 = 1 << 24 // 0x01000000
|
|
pmSSBU int64 = 1 << 25 // 0x02000000
|
|
pmSSBL int64 = 1 << 26 // 0x04000000
|
|
pmDIGU int64 = 1 << 27 // 0x08000000
|
|
pmDIGL int64 = 1 << 28 // 0x10000000
|
|
pmAM int64 = 1 << 29 // 0x20000000
|
|
pmFM int64 = 1 << 30 // 0x40000000 — still fits in int32 (max 2^31-1)
|
|
)
|
|
|
|
// omniRigMode maps the OmniRig Mode bit-flag to an ADIF mode string.
|
|
// OmniRig only reports rough categories; specific digital modes
|
|
// (FT8, RTTY, PSK31…) can't be inferred — DATA is returned and the user
|
|
// can keep / override the mode they already had in the entry form.
|
|
func omniRigMode(m int64) string {
|
|
switch {
|
|
case m&(pmCWU|pmCWL) != 0:
|
|
return "CW"
|
|
case m&(pmSSBU|pmSSBL) != 0:
|
|
return "SSB"
|
|
case m&(pmDIGU|pmDIGL) != 0:
|
|
return "DATA"
|
|
case m&pmAM != 0:
|
|
return "AM"
|
|
case m&pmFM != 0:
|
|
return "FM"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func omniRigVfo(v int64) string {
|
|
switch {
|
|
case v&1024 != 0:
|
|
return "A"
|
|
case v&2048 != 0:
|
|
return "B"
|
|
case v&64 != 0:
|
|
return "AA"
|
|
case v&128 != 0:
|
|
return "AB"
|
|
case v&256 != 0:
|
|
return "BA"
|
|
case v&512 != 0:
|
|
return "BB"
|
|
}
|
|
return ""
|
|
}
|