feat: Support for Antenna Genius
This commit is contained in:
@@ -40,6 +40,7 @@ import (
|
||||
"hamlog/internal/qso"
|
||||
"hamlog/internal/rotator/pst"
|
||||
"hamlog/internal/settings"
|
||||
"hamlog/internal/antgenius"
|
||||
"hamlog/internal/ultrabeam"
|
||||
"hamlog/internal/winkeyer"
|
||||
|
||||
@@ -83,6 +84,9 @@ const (
|
||||
keyCATPollMs = "cat.poll_ms"
|
||||
keyCATDelayMs = "cat.delay_ms" // pause between commands
|
||||
keyCATDigitalDefault = "cat.digital_default" // mode to use when CAT reports DATA
|
||||
keyCATIcomPort = "cat.icom.port" // Icom USB CI-V serial port (e.g. COM5)
|
||||
keyCATIcomBaud = "cat.icom.baud" // Icom CI-V baud (default 115200)
|
||||
keyCATIcomAddr = "cat.icom.addr" // Icom CI-V address, decimal (IC-7610 = 152 / 0x98)
|
||||
|
||||
// Audio (Digital Voice Keyer + QSO recorder). Machine-local hardware, so
|
||||
// global (not per-profile) like CAT/rotator. Device fields store the
|
||||
@@ -137,6 +141,11 @@ const (
|
||||
keyUltrabeamFollow = "ultrabeam.follow" // "1" → re-tune to the rig frequency
|
||||
keyUltrabeamStep = "ultrabeam.step_khz" // re-tune hysteresis: 25 | 50 | 100 kHz
|
||||
|
||||
// Antenna Genius (4O3A) antenna switch — Hardware → Antenna Genius. TCP
|
||||
// port is fixed at 9007, so only the IP is configurable.
|
||||
keyAntGeniusEnabled = "antgenius.enabled"
|
||||
keyAntGeniusHost = "antgenius.host"
|
||||
|
||||
// WinKeyer CW keyer (serial) — Hardware → CW Keyer.
|
||||
keyWKEnabled = "winkeyer.enabled"
|
||||
keyWKPort = "winkeyer.port"
|
||||
@@ -241,11 +250,14 @@ type QSLDefaults struct {
|
||||
// individual key/value pairs to keep the settings table flat.
|
||||
type CATSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Backend string `json:"backend"` // "omnirig" | "flex"
|
||||
Backend string `json:"backend"` // "omnirig" | "flex" | "icom"
|
||||
OmniRigNum int `json:"omnirig_rig"` // 1 or 2 (OmniRig "Rig1"/"Rig2" slot)
|
||||
FlexHost string `json:"flex_host"` // FlexRadio IP (native backend)
|
||||
FlexPort int `json:"flex_port"` // FlexRadio TCP port (default 4992)
|
||||
FlexSpots bool `json:"flex_spots"` // push cluster spots to the panadapter
|
||||
IcomPort string `json:"icom_port"` // Icom USB CI-V serial port (e.g. COM5)
|
||||
IcomBaud int `json:"icom_baud"` // Icom CI-V baud (default 115200)
|
||||
IcomAddr int `json:"icom_addr"` // Icom CI-V address, decimal (IC-7610 = 152)
|
||||
PollMs int `json:"poll_ms"` // poll interval in ms (default 250)
|
||||
DelayMs int `json:"delay_ms"` // pause between commands (default 0)
|
||||
DigitalDefault string `json:"digital_default"` // when CAT says DATA, surface this mode (FT8/FT4/RTTY/…)
|
||||
@@ -377,6 +389,7 @@ type App struct {
|
||||
clublog *clublog.Manager
|
||||
ultrabeam *ultrabeam.Client // Ultrabeam antenna (TCP); nil when disabled
|
||||
ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off
|
||||
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
|
||||
audioMgr *audio.Manager
|
||||
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
|
||||
cwMu sync.Mutex // guards the CW decoder lifecycle
|
||||
@@ -809,6 +822,8 @@ func (a *App) startup(ctx context.Context) {
|
||||
|
||||
// Ultrabeam antenna: connect in the background if enabled.
|
||||
a.startUltrabeam()
|
||||
// Antenna Genius switch: connect in the background if enabled.
|
||||
a.startAntGenius()
|
||||
|
||||
// Autostart: launch the active profile's configured external programs that
|
||||
// aren't already running (WSJT-X, JTAlert, rotator control, …). Background
|
||||
@@ -3786,7 +3801,7 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
||||
if a.settings == nil {
|
||||
return CATSettings{Backend: "omnirig", OmniRigNum: 1, PollMs: 250}, fmt.Errorf("db not initialized")
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault)
|
||||
m, err := a.settings.GetMany(a.ctx, keyCATEnabled, keyCATBackend, keyCATOmniRigNum, keyCATFlexHost, keyCATFlexPort, keyCATFlexSpots, keyCATIcomPort, keyCATIcomBaud, keyCATIcomAddr, keyCATPollMs, keyCATDelayMs, keyCATDigitalDefault)
|
||||
if err != nil {
|
||||
return CATSettings{}, err
|
||||
}
|
||||
@@ -3797,6 +3812,9 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
||||
FlexHost: m[keyCATFlexHost],
|
||||
FlexPort: 4992,
|
||||
FlexSpots: m[keyCATFlexSpots] == "1",
|
||||
IcomPort: m[keyCATIcomPort],
|
||||
IcomBaud: 115200,
|
||||
IcomAddr: 0x98, // IC-7610 default
|
||||
PollMs: 250,
|
||||
DelayMs: 0,
|
||||
DigitalDefault: m[keyCATDigitalDefault],
|
||||
@@ -3804,6 +3822,12 @@ func (a *App) GetCATSettings() (CATSettings, error) {
|
||||
if n, _ := strconv.Atoi(m[keyCATFlexPort]); n > 0 && n <= 65535 {
|
||||
out.FlexPort = n
|
||||
}
|
||||
if n, _ := strconv.Atoi(m[keyCATIcomBaud]); n > 0 {
|
||||
out.IcomBaud = n
|
||||
}
|
||||
if n, _ := strconv.Atoi(m[keyCATIcomAddr]); n > 0 && n <= 0xFF {
|
||||
out.IcomAddr = n
|
||||
}
|
||||
if out.Backend == "" {
|
||||
out.Backend = "omnirig"
|
||||
}
|
||||
@@ -3836,6 +3860,12 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
||||
if s.FlexPort <= 0 || s.FlexPort > 65535 {
|
||||
s.FlexPort = 4992
|
||||
}
|
||||
if s.IcomBaud <= 0 {
|
||||
s.IcomBaud = 115200
|
||||
}
|
||||
if s.IcomAddr <= 0 || s.IcomAddr > 0xFF {
|
||||
s.IcomAddr = 0x98
|
||||
}
|
||||
if s.PollMs < 50 || s.PollMs > 2000 {
|
||||
s.PollMs = 250
|
||||
}
|
||||
@@ -3860,6 +3890,9 @@ func (a *App) SaveCATSettings(s CATSettings) error {
|
||||
keyCATFlexHost: strings.TrimSpace(s.FlexHost),
|
||||
keyCATFlexPort: strconv.Itoa(s.FlexPort),
|
||||
keyCATFlexSpots: flexSpots,
|
||||
keyCATIcomPort: strings.TrimSpace(s.IcomPort),
|
||||
keyCATIcomBaud: strconv.Itoa(s.IcomBaud),
|
||||
keyCATIcomAddr: strconv.Itoa(s.IcomAddr),
|
||||
keyCATPollMs: strconv.Itoa(s.PollMs),
|
||||
keyCATDelayMs: strconv.Itoa(s.DelayMs),
|
||||
keyCATDigitalDefault: strings.ToUpper(strings.TrimSpace(s.DigitalDefault)),
|
||||
@@ -6769,6 +6802,100 @@ func (a *App) GetFlexState() cat.FlexTXState {
|
||||
return st
|
||||
}
|
||||
|
||||
// ── Icom CI-V control panel (receive DSP) ──────────────────────────────────
|
||||
|
||||
func (a *App) GetIcomState() cat.IcomTXState {
|
||||
if a.cat == nil {
|
||||
return cat.IcomTXState{}
|
||||
}
|
||||
st, _ := a.cat.IcomState()
|
||||
return st
|
||||
}
|
||||
|
||||
func (a *App) IcomRefresh() error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.RefreshIcom() })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetAFGain(p int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetAFGain(p) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetRFGain(p int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetRFGain(p) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetNB(on bool) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetNB(on) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetNBLevel(p int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetNBLevel(p) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetNR(on bool) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetNR(on) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetNRLevel(p int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetNRLevel(p) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetANF(on bool) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetANF(on) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetAGC(mode string) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetAGC(mode) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetPreamp(n int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetPreamp(n) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetAtt(db int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetAtt(db) })
|
||||
}
|
||||
|
||||
func (a *App) IcomSetFilter(n int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
}
|
||||
return a.cat.IcomDo(func(ic cat.IcomController) error { return ic.SetIcomFilter(n) })
|
||||
}
|
||||
|
||||
func (a *App) FlexSetPower(p int) error {
|
||||
if a.cat == nil {
|
||||
return fmt.Errorf("cat not initialized")
|
||||
@@ -7053,6 +7180,10 @@ func (a *App) reloadCAT() {
|
||||
}
|
||||
}
|
||||
a.cat.Start(fb)
|
||||
case "icom":
|
||||
// Native Icom CI-V over the radio's USB serial port (local control).
|
||||
// Same civ protocol a future network backend will reuse for remote.
|
||||
a.cat.Start(cat.NewIcomSerial(s.IcomPort, s.IcomBaud, s.IcomAddr, s.DigitalDefault))
|
||||
default:
|
||||
// Unknown backend → stop and emit a dummy state so the UI shows it.
|
||||
a.cat.Stop()
|
||||
@@ -7712,6 +7843,86 @@ func (a *App) TestUltrabeam(s UltrabeamSettings) error {
|
||||
return fmt.Errorf("no response from %s:%d", s.Host, s.Port)
|
||||
}
|
||||
|
||||
// ── Antenna Genius (4O3A) antenna switch (TCP, port fixed 9007) ─────────────
|
||||
|
||||
// AntGeniusSettings is the JSON shape for the Hardware → Antenna Genius panel.
|
||||
// The TCP port is fixed at 9007 on the device, so only the IP is configurable.
|
||||
type AntGeniusSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// GetAntGeniusSettings returns the persisted Antenna Genius config.
|
||||
func (a *App) GetAntGeniusSettings() (AntGeniusSettings, error) {
|
||||
out := AntGeniusSettings{}
|
||||
if a.settings == nil {
|
||||
return out, fmt.Errorf("db not initialized")
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx, keyAntGeniusEnabled, keyAntGeniusHost)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Enabled = m[keyAntGeniusEnabled] == "1"
|
||||
out.Host = m[keyAntGeniusHost]
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SaveAntGeniusSettings persists the config and (re)starts or stops the client.
|
||||
func (a *App) SaveAntGeniusSettings(s AntGeniusSettings) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
for k, v := range map[string]string{
|
||||
keyAntGeniusEnabled: boolStr(s.Enabled),
|
||||
keyAntGeniusHost: strings.TrimSpace(s.Host),
|
||||
} {
|
||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.startAntGenius()
|
||||
return nil
|
||||
}
|
||||
|
||||
// startAntGenius stops any existing client and starts a fresh one if enabled.
|
||||
func (a *App) startAntGenius() {
|
||||
if a.antgenius != nil {
|
||||
a.antgenius.Stop()
|
||||
a.antgenius = nil
|
||||
}
|
||||
s, err := a.GetAntGeniusSettings()
|
||||
if err != nil || !s.Enabled || strings.TrimSpace(s.Host) == "" {
|
||||
return
|
||||
}
|
||||
a.antgenius = antgenius.New(s.Host, 9007)
|
||||
_ = a.antgenius.Start()
|
||||
}
|
||||
|
||||
// GetAntGeniusStatus returns the switch's current state for the UI poll
|
||||
// (connection, active antenna per port, and the configured antenna list).
|
||||
func (a *App) GetAntGeniusStatus() antgenius.Status {
|
||||
if a.antgenius == nil {
|
||||
return antgenius.Status{}
|
||||
}
|
||||
return a.antgenius.GetStatus()
|
||||
}
|
||||
|
||||
// AntGeniusActivate selects an antenna on a port (1 = A, 2 = B).
|
||||
func (a *App) AntGeniusActivate(port, antenna int) error {
|
||||
if a.antgenius == nil {
|
||||
return fmt.Errorf("Antenna Genius not connected — enable it in Settings → Antenna Genius")
|
||||
}
|
||||
return a.antgenius.Activate(port, antenna)
|
||||
}
|
||||
|
||||
// AntGeniusDeselect clears the active antenna on a port (sets it to "None").
|
||||
func (a *App) AntGeniusDeselect(port int) error {
|
||||
if a.antgenius == nil {
|
||||
return fmt.Errorf("Antenna Genius not connected")
|
||||
}
|
||||
return a.antgenius.Activate(port, 0)
|
||||
}
|
||||
|
||||
// --- WinKeyer (CW keyer) bindings ---
|
||||
|
||||
// WKMacro is one CW message slot (F1…): a short label + the macro text, which
|
||||
|
||||
Reference in New Issue
Block a user