From 7e6c0b4f7e4b1d49431a59e5f632f46105e1aaff Mon Sep 17 00:00:00 2001 From: rouggy Date: Sun, 30 Aug 2026 19:14:35 +0200 Subject: [PATCH] fix(kpa): stop switching the KPA500 off, and answer its operator instantly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three faults, one report. The slow poll asks ^TP — the KPA1500's ATU, which a KPA500 (no ATU) never answers — and ask() dropped the whole connection on any read timeout: a two-second stall and a reconnect every slow cycle, which is why buttons lagged and the status read wrong. Worse, every serial reopen toggled DTR/RTS — and those lines are the KPA500's POWER SWITCH (that is how the Elecraft utility turns it on), so the amplifier obediently switched off twenty seconds after its operator pressed nothing but Standby. Silence is no longer a dead link (write errors still are), ^TP is never asked again after one silence, the control lines are asserted once and held, and the baud field becomes a list of the rates these amplifiers actually speak. --- changelog.json | 10 ++++++++ frontend/src/components/SettingsModal.tsx | 13 ++++++++-- internal/kpa/kpa.go | 31 ++++++++++++++++++++--- 3 files changed, 48 insertions(+), 6 deletions(-) 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") } }