feat(elecraft): a K3/K4 console — power, volume, S-meter, SWR, MOX, tune

The Elecraft backend already existed; what was missing was somewhere to
operate the radio from. This adds the panel, on the Kenwood-dialect
client the K3 already speaks, in a tab of its own beside the Yaesu and
Icom consoles.

Scope is the six controls asked for and nothing else. Every extra command
added without a radio to test it against is a control that may or may not
do what its label says, and a K3 exposes dozens.

No K3 was available while writing this, so the two halves are treated
differently. The setters are the commands the reference documents
unambiguously and whose effect is visible and reversible (PC, AG,
TX/RX). The meters are the opposite: their scaling differs by model and
firmware, so the raw answers are logged next to the power setting, the
panel says the scaling is provisional, and an unmeasured SWR shows as
'—' rather than as a perfect 1.0 — a good match on an antenna nobody
measured is the reading that costs a radio.

ATU tune sends SWT20 (the K3 front-panel tap) and logs exactly what it
sent, so a wrong mapping names itself instead of leaving an operator
guessing which button OpsLog pressed.
This commit is contained in:
2026-08-24 14:09:28 +02:00
parent 1b63483596
commit 60dad353ce
11 changed files with 669 additions and 6 deletions
+55
View File
@@ -0,0 +1,55 @@
package main
// Elecraft K3/K4 panel bindings.
//
// The controls the operator asked for and nothing else: power, volume, the
// S-meter and SWR, MOX, and an ATU tune. A K3 exposes dozens of commands, but a
// panel earns its place by covering what is reached for during a QSO — and each
// one added without a radio to test it against is a control that may or may not
// do what its label says. See internal/cat/kenwood_panel.go.
import (
"fmt"
"hamlog/internal/cat"
)
// GetKenwoodState returns the K3/K4 panel snapshot. Zero value when the active
// CAT backend is something else, which is how the UI knows to hide the panel.
func (a *App) GetKenwoodState() cat.KenwoodTXState {
if a.cat == nil {
return cat.KenwoodTXState{}
}
st, _ := a.cat.KenwoodState()
return st
}
func (a *App) SetKenwoodPower(w int) error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodPower(w) })
}
func (a *App) SetKenwoodAFGain(p int) error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodAFGain(p) })
}
// SetKenwoodTX is the panel's MOX.
func (a *App) SetKenwoodTX(on bool) error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodTX(on) })
}
func (a *App) TuneKenwoodATU() error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.TuneKenwoodATU() })
}
// RefreshKenwood re-reads the settings on the next poll — for when a knob was
// turned on the radio itself.
func (a *App) RefreshKenwood() error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.RefreshKenwood() })
}
func (a *App) kenwoodPanelDo(fn func(cat.KenwoodPanelController) error) error {
if a.cat == nil {
return fmt.Errorf("CAT not initialized")
}
return a.cat.KenwoodPanelDo(fn)
}