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") } }