feat: control the 4O3A Tuner Genius XL directly over TCP (port 9010)
New internal/tunergenius client speaking the same "Genius Series" text API as the Antenna Genius / PowerGenius XL: banner on connect (with optional "AUTH" for remote access), "C<seq>|<cmd>\n" commands, "R<seq>|<code>|<msg>" replies and the "S<seq>|status k=v …" snapshot; async "M|<msg>" info lines are consumed inline. Polls status ~1.5s and exposes SWR (return-loss dB converted to a VSWR ratio), forward power (dBm→W), and the operate/bypass/ tuning/active-channel state. Actions: autotune, global bypass, operate/standby. Controlled directly (not via the radio) so OpsLog uses only one of the box's four connection slots, per the device's protocol doc. Wiring: - app.go: tunergenius.* settings keys, Get/Save/start, GetTunerGeniusStatus, TunerGeniusAutotune/SetBypass/SetOperate; started in the background at launch. - Settings → Tuner Genius panel (enable + IP + optional remote code). - Docked TunerGeniusPanel widget (SWR/power readouts + Tune/Bypass/Operate), top-bar toggle, shown when enabled — mirrors the Antenna Genius widget. - i18n EN/FR (sec.tunergenius, tg2.*, tgp.*). Command verbs (operate/bypass/autotune/status/auth) come straight from the 4O3A "Tuner Genius XL — Protocol Description"; UNTESTED on hardware.
This commit is contained in:
@@ -57,6 +57,7 @@ import (
|
||||
"hamlog/internal/solar"
|
||||
"hamlog/internal/spe"
|
||||
"hamlog/internal/steppir"
|
||||
"hamlog/internal/tunergenius"
|
||||
"hamlog/internal/uls"
|
||||
"hamlog/internal/ultrabeam"
|
||||
"hamlog/internal/winkeyer"
|
||||
@@ -190,6 +191,12 @@ const (
|
||||
keyAntGeniusHost = "antgenius.host"
|
||||
keyAntGeniusPassword = "antgenius.password" // remote/AUTH password (blank on LAN)
|
||||
|
||||
// Tuner Genius XL (4O3A) — Hardware → Tuner Genius. TCP port is fixed at 9010
|
||||
// on the device, so only the IP + optional remote code are configurable.
|
||||
keyTunerGeniusEnabled = "tunergenius.enabled"
|
||||
keyTunerGeniusHost = "tunergenius.host"
|
||||
keyTunerGeniusPassword = "tunergenius.password" // remote/AUTH code (blank on LAN)
|
||||
|
||||
// Amplifier control — Hardware → Amplifier (PowerGenius XL over TCP; SPE Expert
|
||||
// over USB serial or an RS232-to-Ethernet bridge). Keys keep the pgxl.* prefix
|
||||
// for backward compatibility with existing saved settings.
|
||||
@@ -475,6 +482,7 @@ type App struct {
|
||||
motorMoveCmdNs atomic.Int64 // unixnano of the last commanded antenna move (grace window)
|
||||
motorInhibited atomic.Bool // TX currently inhibited by the motor-antenna watcher
|
||||
antgenius *antgenius.Client // Antenna Genius (4O3A) switch (TCP); nil when disabled
|
||||
tunergenius *tunergenius.Client // Tuner Genius XL (4O3A) ATU (TCP); nil when disabled
|
||||
pgxl *powergenius.Client // PowerGenius XL (4O3A) amp fan control (TCP); nil when disabled
|
||||
spe *spe.Client // legacy pointer: FIRST enabled SPE amp (kept for the pre-multi bindings)
|
||||
acom *acom.Client // legacy pointer: FIRST enabled ACOM amp
|
||||
@@ -1139,6 +1147,8 @@ func (a *App) startup(ctx context.Context) {
|
||||
a.startUltrabeam()
|
||||
// Antenna Genius switch: connect in the background if enabled.
|
||||
a.startAntGenius()
|
||||
// Tuner Genius XL ATU: connect in the background if enabled.
|
||||
a.startTunerGenius()
|
||||
// PowerGenius XL amp fan control: connect in the background if enabled.
|
||||
a.startAmps()
|
||||
|
||||
@@ -12913,6 +12923,96 @@ func (a *App) AntGeniusDeselect(port int) error {
|
||||
return a.antgenius.Activate(port, 0)
|
||||
}
|
||||
|
||||
// ── Tuner Genius XL (4O3A) ATU control (TCP, fixed port 9010) ────────────────
|
||||
|
||||
// TunerGeniusSettings is the JSON shape for the Hardware → Tuner Genius panel.
|
||||
// The TCP port is fixed at 9010 on the device, so only the IP is configurable.
|
||||
type TunerGeniusSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"` // remote-access code; leave blank on LAN (no AUTH)
|
||||
}
|
||||
|
||||
// GetTunerGeniusSettings returns the persisted Tuner Genius config.
|
||||
func (a *App) GetTunerGeniusSettings() (TunerGeniusSettings, error) {
|
||||
out := TunerGeniusSettings{}
|
||||
if a.settings == nil {
|
||||
return out, fmt.Errorf("db not initialized")
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx, keyTunerGeniusEnabled, keyTunerGeniusHost, keyTunerGeniusPassword)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out.Enabled = m[keyTunerGeniusEnabled] == "1"
|
||||
out.Host = m[keyTunerGeniusHost]
|
||||
out.Password = m[keyTunerGeniusPassword]
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SaveTunerGeniusSettings persists the config and (re)starts or stops the client.
|
||||
func (a *App) SaveTunerGeniusSettings(s TunerGeniusSettings) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
for k, v := range map[string]string{
|
||||
keyTunerGeniusEnabled: boolStr(s.Enabled),
|
||||
keyTunerGeniusHost: strings.TrimSpace(s.Host),
|
||||
keyTunerGeniusPassword: s.Password,
|
||||
} {
|
||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
a.startTunerGenius()
|
||||
return nil
|
||||
}
|
||||
|
||||
// startTunerGenius stops any existing client and starts a fresh one if enabled.
|
||||
func (a *App) startTunerGenius() {
|
||||
if a.tunergenius != nil {
|
||||
go a.tunergenius.Stop() // background teardown so saving Settings doesn't block
|
||||
a.tunergenius = nil
|
||||
}
|
||||
s, err := a.GetTunerGeniusSettings()
|
||||
if err != nil || !s.Enabled || strings.TrimSpace(s.Host) == "" {
|
||||
return
|
||||
}
|
||||
a.tunergenius = tunergenius.New(s.Host, tunergenius.DefaultPort, s.Password)
|
||||
_ = a.tunergenius.Start()
|
||||
}
|
||||
|
||||
// GetTunerGeniusStatus returns the ATU's current state for the UI poll.
|
||||
func (a *App) GetTunerGeniusStatus() tunergenius.Status {
|
||||
if a.tunergenius == nil {
|
||||
return tunergenius.Status{}
|
||||
}
|
||||
return a.tunergenius.GetStatus()
|
||||
}
|
||||
|
||||
// TunerGeniusAutotune starts an automatic tuning cycle on the active channel.
|
||||
func (a *App) TunerGeniusAutotune() error {
|
||||
if a.tunergenius == nil {
|
||||
return fmt.Errorf("Tuner Genius not connected — enable it in Settings → Tuner Genius")
|
||||
}
|
||||
return a.tunergenius.Autotune()
|
||||
}
|
||||
|
||||
// TunerGeniusSetBypass engages (true) or clears (false) the global bypass.
|
||||
func (a *App) TunerGeniusSetBypass(on bool) error {
|
||||
if a.tunergenius == nil {
|
||||
return fmt.Errorf("Tuner Genius not connected")
|
||||
}
|
||||
return a.tunergenius.SetBypass(on)
|
||||
}
|
||||
|
||||
// TunerGeniusSetOperate puts the tuner in OPERATE (true) or STANDBY (false).
|
||||
func (a *App) TunerGeniusSetOperate(on bool) error {
|
||||
if a.tunergenius == nil {
|
||||
return fmt.Errorf("Tuner Genius not connected")
|
||||
}
|
||||
return a.tunergenius.SetOperate(on)
|
||||
}
|
||||
|
||||
// ── PowerGenius XL (4O3A) amplifier fan control (TCP, default port 9008) ─────
|
||||
|
||||
// PGXLSettings is the JSON shape for the Hardware → Amplifier panel. It covers
|
||||
|
||||
Reference in New Issue
Block a user