feat: Support for Power Genius XL

This commit is contained in:
2026-06-21 20:27:53 +02:00
parent b302d4d87b
commit 5d9765be09
8 changed files with 459 additions and 2 deletions
+86 -1
View File
@@ -41,6 +41,7 @@ import (
"hamlog/internal/rotator/pst"
"hamlog/internal/settings"
"hamlog/internal/antgenius"
"hamlog/internal/powergenius"
"hamlog/internal/ultrabeam"
"hamlog/internal/winkeyer"
@@ -146,6 +147,11 @@ const (
keyAntGeniusEnabled = "antgenius.enabled"
keyAntGeniusHost = "antgenius.host"
// PowerGenius XL (4O3A) amplifier fan-mode control — Hardware → PowerGenius.
keyPGXLEnabled = "pgxl.enabled"
keyPGXLHost = "pgxl.host"
keyPGXLPort = "pgxl.port"
// WinKeyer CW keyer (serial) — Hardware → CW Keyer.
keyWKEnabled = "winkeyer.enabled"
keyWKPort = "winkeyer.port"
@@ -389,7 +395,8 @@ 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
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
audioMgr *audio.Manager
qsoRec *audio.Recorder // continuous QSO recorder (rolling pre-roll)
cwMu sync.Mutex // guards the CW decoder lifecycle
@@ -824,6 +831,8 @@ func (a *App) startup(ctx context.Context) {
a.startUltrabeam()
// Antenna Genius switch: connect in the background if enabled.
a.startAntGenius()
// PowerGenius XL amp fan control: connect in the background if enabled.
a.startPGXL()
// Autostart: launch the active profile's configured external programs that
// aren't already running (WSJT-X, JTAlert, rotator control, …). Background
@@ -7923,6 +7932,82 @@ func (a *App) AntGeniusDeselect(port int) error {
return a.antgenius.Activate(port, 0)
}
// ── PowerGenius XL (4O3A) amplifier fan control (TCP, default port 9006) ─────
// PGXLSettings is the JSON shape for the Hardware → PowerGenius panel.
type PGXLSettings struct {
Enabled bool `json:"enabled"`
Host string `json:"host"`
Port int `json:"port"`
}
func (a *App) GetPGXLSettings() (PGXLSettings, error) {
out := PGXLSettings{Port: 9006}
if a.settings == nil {
return out, fmt.Errorf("db not initialized")
}
m, err := a.settings.GetMany(a.ctx, keyPGXLEnabled, keyPGXLHost, keyPGXLPort)
if err != nil {
return out, err
}
out.Enabled = m[keyPGXLEnabled] == "1"
out.Host = m[keyPGXLHost]
if p, _ := strconv.Atoi(m[keyPGXLPort]); p > 0 && p <= 65535 {
out.Port = p
}
return out, nil
}
func (a *App) SavePGXLSettings(s PGXLSettings) error {
if a.settings == nil {
return fmt.Errorf("db not initialized")
}
if s.Port <= 0 || s.Port > 65535 {
s.Port = 9006
}
for k, v := range map[string]string{
keyPGXLEnabled: boolStr(s.Enabled),
keyPGXLHost: strings.TrimSpace(s.Host),
keyPGXLPort: strconv.Itoa(s.Port),
} {
if err := a.settings.Set(a.ctx, k, v); err != nil {
return err
}
}
a.startPGXL()
return nil
}
// startPGXL stops any existing client and starts a fresh one if enabled.
func (a *App) startPGXL() {
if a.pgxl != nil {
a.pgxl.Stop()
a.pgxl = nil
}
s, err := a.GetPGXLSettings()
if err != nil || !s.Enabled || strings.TrimSpace(s.Host) == "" {
return
}
a.pgxl = powergenius.New(s.Host, s.Port)
_ = a.pgxl.Start()
}
// GetPGXLStatus returns the amp's fan/connection state for the UI poll.
func (a *App) GetPGXLStatus() powergenius.Status {
if a.pgxl == nil {
return powergenius.Status{}
}
return a.pgxl.GetStatus()
}
// PGXLSetFanMode sets the amplifier fan mode (STANDARD | CONTEST | BROADCAST).
func (a *App) PGXLSetFanMode(mode string) error {
if a.pgxl == nil {
return fmt.Errorf("PowerGenius not connected — enable it in Settings → PowerGenius")
}
return a.pgxl.SetFanMode(mode)
}
// --- WinKeyer (CW keyer) bindings ---
// WKMacro is one CW message slot (F1…): a short label + the macro text, which