diff --git a/app.go b/app.go index b7567a5..31a16ce 100644 --- a/app.go +++ b/app.go @@ -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 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 66308b8..54197c3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3382,7 +3382,7 @@ export default function App() { {/* ===== CW decoder strip (only when enabled AND mode is CW) ===== */} {cwOn && ( -
+ Default port is 9006. Once enabled, a fan-mode dropdown (Standard / Contest / Broadcast) appears next to the amplifier Operate button in the FlexRadio tab. +
+