diff --git a/changelog.json b/changelog.json
index 4f7d571..52b05f1 100644
--- a/changelog.json
+++ b/changelog.json
@@ -1,4 +1,14 @@
[
+ {
+ "version": "0.27.3",
+ "date": "",
+ "en": [
+ "KPA500: the amplifier no longer switches itself off and commands respond instantly. A command this model does not know (the KPA1500’s ATU poll) was tearing the link down every cycle, and each reconnect toggled the serial control lines — which are the KPA500’s power switch. The lines are now held steady, silence is not treated as a dead link, and the baud is picked from a list."
+ ],
+ "fr": [
+ "KPA500 : l’ampli ne s’éteint plus tout seul et les commandes répondent instantanément. Une commande inconnue de ce modèle (le poll ATU du KPA1500) détruisait le lien à chaque cycle, et chaque reconnexion basculait les lignes de contrôle série — qui sont l’interrupteur du KPA500. Les lignes sont désormais tenues stables, le silence n’est plus traité comme un lien mort, et le baud se choisit dans une liste."
+ ]
+ },
{
"version": "0.27.2",
"date": "",
diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx
index 6633eb0..ec137dc 100644
--- a/frontend/src/components/SettingsModal.tsx
+++ b/frontend/src/components/SettingsModal.tsx
@@ -4416,8 +4416,17 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
- patchAmp(i, { baud: parseInt(e.target.value) || 115200 })} className="font-mono" />
+ {/* A list, not a free number: the KPA500 report that
+ began this had its operator wondering whether a typed
+ baud was the whole problem. These are the rates the
+ supported amplifiers actually speak. */}
+
) : (
diff --git a/internal/kpa/kpa.go b/internal/kpa/kpa.go
index 788503f..2651549 100644
--- a/internal/kpa/kpa.go
+++ b/internal/kpa/kpa.go
@@ -79,9 +79,10 @@ type Config struct {
type Client struct {
cfg Config
- mu sync.Mutex // serialises the connection: one question at a time
- conn io.ReadWriteCloser
- rd *bufio.Reader
+ mu sync.Mutex // serialises the connection: one question at a time
+ conn io.ReadWriteCloser
+ rd *bufio.Reader
+ skipTP bool // ^TP went unanswered once — a KPA500, no ATU; never ask again
statusMu sync.RWMutex
status Status
@@ -183,6 +184,13 @@ func (c *Client) connectLocked() error {
if err != nil {
return fmt.Errorf("cannot open %s: %w", c.cfg.ComPort, err)
}
+ // The KPA500 is POWER-CONTROLLED by these lines: the Elecraft utility
+ // switches the amplifier on by raising them. Held asserted, once, and
+ // never touched again — reconnect cycles that toggled them were
+ // switching a KPA500 OFF twenty seconds after its operator pressed
+ // nothing but Standby.
+ _ = p.SetDTR(true)
+ _ = p.SetRTS(true)
_ = p.SetReadTimeout(ioTimeout)
c.conn = p
}
@@ -214,7 +222,13 @@ func (c *Client) ask(cmd string) (string, error) {
// the frame.
line, err := c.rd.ReadString(';')
if err != nil {
- c.dropLocked()
+ // NOT dropped. A command this model simply does not know (^TP is the
+ // KPA1500's ATU — a KPA500 never answers it) is silence, not a dead
+ // link, and dropping here tore the connection down on every slow poll
+ // cycle: two seconds of stalled commands, a reconnect, and a DTR
+ // toggle the amplifier read as the off switch. Nothing arrived, so
+ // nothing is left to desynchronise the next exchange. Write errors —
+ // the genuinely dead link — still drop, above.
return "", fmt.Errorf("no answer to %s: %w", cmd, err)
}
return strings.TrimSpace(line), nil
@@ -364,12 +378,21 @@ func (c *Client) pollOnce(n uint64) {
c.statusMu.Unlock()
}
}
+ if c.skipTP {
+ return
+ }
if reply, err := c.ask("^TP;"); err == nil {
if v, err := parseInt(reply, "^TP"); err == nil {
c.statusMu.Lock()
c.status.Tuning = v == 1
c.statusMu.Unlock()
}
+ } else {
+ // One silence is the model's answer for good: a KPA500 has no ATU and
+ // will never answer ^TP — asking again every cycle cost a two-second
+ // stall each time.
+ c.skipTP = true
+ applog.Printf("kpa: ^TP unanswered — no ATU on this model, not asking again")
}
}