feat(kpa): the Elecraft amplifiers in the Settings, the card and the widget

Wired into the multi-amplifier support that already carries the SPE, the
ACOM and the PowerGenius: a fourth brand rather than a fourth mechanism.
The linking, the fan-out and the poll all work on it unchanged.

Three things the KPA does differently, and each is handled where it shows
rather than explained in a hint:

  - A KPA500 has no network side at all. Choosing it puts the entry on
    serial, and a configuration still asking for TCP is refused with a
    line saying so instead of retrying an address that cannot answer.
  - The amplifier answers ^ON while its main supplies are OFF — a sleeping
    microcontroller stays awake for exactly that — so power-on stays
    available over the network, unlike the SPE and ACOM which need their
    serial control lines.
  - Going to OPERATE clears the current fault, everything except
    temperature. The widget says so under the fault, because the button
    that fixes it is the one already on screen.

Defaults that leave a working configuration when the model is changed:
38400 baud, TCP port 1500, and a full-scale power mark of 500 or 1500 W
by model — a KPA500 read against a 1500 W scale looks idle at full
output.
This commit is contained in:
2026-08-26 17:50:30 +02:00
parent 55a643d9a4
commit 85872f8379
5 changed files with 142 additions and 16 deletions
+31
View File
@@ -43,6 +43,7 @@ import (
"hamlog/internal/geo"
"hamlog/internal/gridcache"
"hamlog/internal/integrations/udp"
"hamlog/internal/kpa"
"hamlog/internal/lookup"
"hamlog/internal/lotwusers"
"hamlog/internal/netctl"
@@ -18075,6 +18076,7 @@ type ampInst struct {
pgxl *powergenius.Client
spe *spe.Client
acom *acom.Client
kpa *kpa.Client
catemu *catemu.Server // Kenwood-format responder for band-follow (ACOM)
}
@@ -18088,6 +18090,9 @@ func (i *ampInst) stopAll() {
if i.acom != nil {
i.acom.Stop()
}
if i.kpa != nil {
i.kpa.Stop()
}
if i.catemu != nil {
i.catemu.Stop()
}
@@ -18102,6 +18107,8 @@ func ampTypeLabel(t string) string {
return "SPE " + map[string]string{"spe13": "1.3K-FA", "spe15": "1.5K-FA", "spe2k": "2K-FA"}[t]
case strings.HasPrefix(t, "acom"):
return "ACOM " + strings.TrimPrefix(t, "acom") + "S"
case strings.HasPrefix(t, "kpa"):
return "Elecraft " + strings.ToUpper(t)
}
return t
}
@@ -18213,6 +18220,19 @@ func (a *App) startAmps() {
if a.acom == nil {
a.acom = inst.acom
}
case strings.HasPrefix(c.Type, "kpa"):
// A KPA500 has no network port at all, so a configuration asking for
// one is a mistake worth naming rather than a connection that never
// succeeds.
if strings.EqualFold(c.Type, "kpa500") && c.Transport == "tcp" {
applog.Printf("amp %s: a KPA500 has no network connection — use its serial port", c.Name)
continue
}
inst.kpa = kpa.New(kpa.Config{
Model: strings.ToUpper(c.Type), Transport: c.Transport,
ComPort: c.ComPort, Baud: c.Baud, Host: c.Host, Port: c.Port,
})
_ = inst.kpa.Start()
default: // spe*
inst.spe = spe.New(spe.Config{Transport: c.Transport, ComPort: c.ComPort, Baud: c.Baud, Host: c.Host, Port: c.Port})
_ = inst.spe.Start()
@@ -18284,6 +18304,7 @@ type AmpStatus struct {
PGXL *powergenius.Status `json:"pgxl,omitempty"`
SPE *spe.Status `json:"spe,omitempty"`
ACOM *acom.Status `json:"acom,omitempty"`
KPA *kpa.Status `json:"kpa,omitempty"`
}
// GetAmpStatuses returns the live state of every ENABLED amplifier, in the
@@ -18310,6 +18331,9 @@ func (a *App) GetAmpStatuses() []AmpStatus {
case inst.acom != nil:
v := inst.acom.GetStatus()
st.ACOM = &v
case inst.kpa != nil:
v := inst.kpa.GetStatus()
st.KPA = &v
}
}
out = append(out, st)
@@ -18391,6 +18415,8 @@ func (a *App) ampOperateOne(id string, on bool) error {
return inst.spe.Operate(on)
case inst.acom != nil:
return inst.acom.Operate(on)
case inst.kpa != nil:
return inst.kpa.Operate(on)
}
return fmt.Errorf("amplifier not running")
}
@@ -18427,6 +18453,11 @@ func (a *App) ampPowerOne(id string, on, linked bool) error {
return inst.acom.PowerOn()
}
return inst.acom.PowerOff()
case inst.kpa != nil:
// OFF is a real power-down on a KPA1500: the main supplies drop and the
// way back on is the front panel or Wake-on-LAN. The button that reaches
// this asks first — see the UI — because "off" here is not standby.
return inst.kpa.PowerOn(on)
}
// Not an error worth surfacing when linked: a PGXL alongside two SPEs simply
// has no power command on its direct link, and reporting that as a failure