From 8f643b5b67bc9a2d2e705d4fa15b999d9597cc48 Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 9 Sep 2026 23:11:39 +0200 Subject: [PATCH] fix(winkeyer): wake a keyer that will not open, instead of giving up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An operator with a WinKey2 USB had to run K1EL's WKdemo and close it again before OpsLog could talk to the keyer at all. That workaround is the diagnosis: closing another program does something to the keyer that opening the port does not, and whatever state it was stuck in survives a failed connect. So the second handshake attempt now does what closing WKdemo does, in an order that survives each step failing: - Host Close, in case the keyer is still in host mode from a session that ended without one — a crash, a cable pulled, a machine switched off. It has been waiting ever since for a host that went away. - Admin Reset, which returns it to its power-up state. A parser stuck part-way through a command whose parameters will never arrive cannot be talked out of it any other way. - A DTR pulse, which is what closing a program actually does to the line. On a WKUSB and on every Arduino-based clone, DTR runs to the processor's reset: it is a power-on reset in all but name. RTS is left alone throughout — on a serial WinKeyer it is the negative rail the RS-232 swing comes from, and driving it starves the chip. A keyer that answers the echo and then refuses to open is the same leftover session seen from the other side, so that case sends Host Close and asks again rather than reporting a keyer that demonstrably just spoke to us as absent. And a port already known to need the slow path gets the wake-up on the FIRST attempt from then on: making the operator sit through a failure to earn it again doubles the connect time for no new information. The handshake bytes are already logged on every connect, so the next report of this shape says where it stopped. Co-Authored-By: Claude Opus 5 (1M context) --- changelog.json | 6 ++-- internal/winkeyer/hostopen.go | 63 +++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/changelog.json b/changelog.json index f0a61c8..329ade6 100644 --- a/changelog.json +++ b/changelog.json @@ -7,14 +7,16 @@ "FlexRadio, satellite: the per-band antennas you configured are now applied to the satellite slices. They were not — the entry form applied them on a band change, to the active slice, and a pass never goes through that path. The two slices are on two different bands, so each gets its own: the downlink takes the receive antenna for its band, the uplink the transmit antenna for its. On a station with transverters (XVTA on 2 m, XVTB on 70 cm) the downlink was left on whatever the radio last used, and heard nothing.", "FlexRadio, satellite: on an inverting transponder the uplink is set to LSB and the downlink to USB, instead of USB at both ends. The passband is turned over, so audio transmitted on the wrong sideband comes back through it upside down — which is every attempted contact on FO-29, RS-44 and AO-73.", "FlexRadio, satellite: the CTCSS tone is set on the uplink slice from the satellite's frequency plan. An FM bird does not answer without it, and it is the one setting an operator cannot reach from the front panel once a pass has started.", - "Changing satellite while tracking now moves the radio to the new one at once, and the antenna with it. The selection on the page was the display's; the tracker held its own and went on following whatever it was started with, so with two birds up at the same time the frequencies stayed on the first — the only way through was to stop tracking and start it again. The radio stays armed through the change, so a Flex no longer throws away and rebuilds both its slices for nothing." + "Changing satellite while tracking now moves the radio to the new one at once, and the antenna with it. The selection on the page was the display's; the tracker held its own and went on following whatever it was started with, so with two birds up at the same time the frequencies stayed on the first — the only way through was to stop tracking and start it again. The radio stays armed through the change, so a Flex no longer throws away and rebuilds both its slices for nothing.", + "WinKeyer: a keyer that will not connect is now woken up instead of given up on. An operator with a WinKey2 USB had to run K1EL's WKdemo and close it again before OpsLog could open the keyer at all — so the second attempt now does what closing WKdemo does: Host Close in case a session that ended badly left the keyer waiting for a host that went away, Admin Reset for a parser stuck part-way through a command, and a DTR pulse, which on a WKUSB or an Arduino clone is a power-on reset in all but name. A keyer that echoes but refuses to open is also closed and asked again, which is the same leftover-session case seen from the other side. A port known to need this gets it straight away next time." ], "fr": [ "La correction Doppler était fausse — d’un facteur d’environ 250, et dans le mauvais sens. La bibliothèque SGP4 renvoie une vitesse radiale qui n’en est pas une : l’ISS se rapprochant à 5,5 km/s était rapportée à +2036 km/s, ce qui déplaçait une descente 2 m de deux mégahertz au lieu de trois kilohertz, et dans la mauvaise direction. OpsLog mesure désormais cette vitesse à partir de la distance elle-même, ce qui ne peut pas contredire la physique. Une descente 2 m se décale d’environ ±3,5 kHz sur un passage et une 70 cm d’environ ±10 kHz, comme il se doit.", "FlexRadio, satellite : les antennes par bande que vous avez configurées sont désormais appliquées aux tranches satellite. Elles ne l’étaient pas — la fenêtre de saisie les appliquait au changement de bande, sur la tranche active, et un passage ne passe jamais par là. Les deux tranches sont sur deux bandes différentes, donc chacune reçoit la sienne : la descente prend l’antenne de réception de sa bande, la montée l’antenne d’émission de la sienne. Sur une station à transverters (XVTA en 2 m, XVTB en 70 cm), la descente restait sur ce que la radio utilisait en dernier, et n’entendait rien.", "FlexRadio, satellite : sur un transpondeur inverseur, la montée est mise en LSB et la descente en USB, au lieu d’USB des deux côtés. La bande passante est retournée : une audio émise sur la mauvaise bande latérale revient à l’envers — soit tous les QSO tentés sur FO-29, RS-44 et AO-73.", "FlexRadio, satellite : la tonalité CTCSS est réglée sur la tranche de montée depuis le plan de fréquences du satellite. Un satellite FM ne répond pas sans elle, et c’est le seul réglage qu’un OM ne peut pas atteindre en façade une fois le passage commencé.", - "Changer de satellite pendant le suivi déplace désormais la radio sur le nouveau immédiatement, et l’antenne avec. La sélection de la page était celle de l’affichage ; le tracker gardait la sienne et continuait de suivre celui avec lequel il avait démarré, donc avec deux satellites en passage simultané les fréquences restaient sur le premier — il fallait arrêter puis relancer le suivi. La radio reste armée pendant le changement : un Flex ne jette plus ses deux tranches pour les reconstruire inutilement." + "Changer de satellite pendant le suivi déplace désormais la radio sur le nouveau immédiatement, et l’antenne avec. La sélection de la page était celle de l’affichage ; le tracker gardait la sienne et continuait de suivre celui avec lequel il avait démarré, donc avec deux satellites en passage simultané les fréquences restaient sur le premier — il fallait arrêter puis relancer le suivi. La radio reste armée pendant le changement : un Flex ne jette plus ses deux tranches pour les reconstruire inutilement.", + "WinKeyer : un keyer qui refuse de se connecter est désormais réveillé au lieu d’être abandonné. Un OM avec un WinKey2 USB devait lancer le WKdemo de K1EL puis le refermer avant qu’OpsLog puisse ouvrir le keyer — la seconde tentative fait donc maintenant ce que fait la fermeture de WKdemo : Host Close au cas où une session mal terminée aurait laissé le keyer à attendre un hôte disparu, Admin Reset pour un analyseur bloqué au milieu d’une commande, et une impulsion sur DTR, qui sur un WKUSB ou un clone Arduino est une remise sous tension ou presque. Un keyer qui répond à l’écho mais refuse de s’ouvrir est également refermé puis redemandé — le même cas de session résiduelle, vu de l’autre côté. Un port connu pour en avoir besoin y a droit d’emblée la fois suivante." ] }, { diff --git a/internal/winkeyer/hostopen.go b/internal/winkeyer/hostopen.go index 4890c4a..25068c3 100644 --- a/internal/winkeyer/hostopen.go +++ b/internal/winkeyer/hostopen.go @@ -32,7 +32,9 @@ import ( const ( cmdNull = 0x13 cmdAdmin = 0x00 + adminReset = 0x01 adminOpen = 0x02 + adminClose = 0x03 adminEcho = 0x04 echoProbe = 0x55 // K1EL's own choice; any byte works, this one is 0b01010101 bootDelay = 400 * time.Millisecond @@ -75,7 +77,11 @@ func hostOpen(p serial.Port, slowBoot bool) (ver int, needsSlowBoot bool, err er if attempt > 1 || slowBoot { wait = resetDelay } - ver, err := hostOpenOnce(p, wait) + // A port already known to need the slow path gets the wake-up on the + // FIRST attempt too: it needed it last time, and making the operator + // wait through a failure to earn it again is a connect that takes twice + // as long for no new information. + ver, err := hostOpenOnce(p, wait, attempt > 1 || slowBoot) if err == nil { if attempt > 1 { applog.Printf("winkeyer: answered on attempt %d — this keyer needs %s to boot (a K3NG or another Arduino keyer with auto-reset on); remembering that for this port", attempt, wait) @@ -90,7 +96,10 @@ func hostOpen(p serial.Port, slowBoot bool) (ver int, needsSlowBoot bool, err er return 0, false, lastErr } -func hostOpenOnce(p serial.Port, boot time.Duration) (int, error) { +func hostOpenOnce(p serial.Port, boot time.Duration, hard bool) (int, error) { + if hard { + recoverKeyer(p) + } // The keyer may still be booting off the DTR line we just raised. time.Sleep(boot) drain(p) @@ -131,11 +140,59 @@ func hostOpenOnce(p serial.Port, boot time.Duration) (int, error) { ver, ok := readByte(p, openTimeout) traceHandshake("RX", nil, ver, ok) if !ok { - return 0, errors.New("host open: the keyer echoed but did not return its firmware version") + // It answered the echo, so there IS a keyer on this port — it simply + // will not open. A keyer already IN host mode does exactly that: a + // previous session that ended badly never sent Host Close, and it has + // been waiting ever since for a host that went away. Close it and ask + // again. + applog.Printf("winkeyer: echoed but did not open — closing a host session left over from last time, and asking again") + if _, err := p.Write([]byte{cmdAdmin, adminClose}); err != nil { + return 0, fmt.Errorf("host close: %w", err) + } + time.Sleep(250 * time.Millisecond) + drain(p) + traceHandshake("TX", open, 0, false) + if _, err := p.Write(open); err != nil { + return 0, fmt.Errorf("host open: %w", err) + } + ver, ok = readByte(p, openTimeout) + traceHandshake("RX", nil, ver, ok) + if !ok { + return 0, errors.New("host open: the keyer echoed but did not return its firmware version") + } } return int(ver), nil } +// recoverKeyer does to the keyer what running K1EL's WKdemo and closing it +// again does — which is the workaround an operator found for a WKUSB that +// OpsLog could not open until they had. +// +// Three things, in an order that survives each of them failing: +// +// - Host Close, in case the keyer is still in host mode from a session that +// ended without one: a crash, a cable pulled, a machine switched off. +// - Admin Reset, which returns it to its power-up state. A parser stuck +// part-way through a command whose parameters will never arrive cannot be +// talked out of it any other way. +// - A DTR pulse. That is what closing another program actually does to the +// line, and on the boxes that wire DTR to the processor's reset — a WKUSB, +// and every Arduino-based clone — it is a power-on reset in all but name. +// +// RTS is left alone throughout: on a serial WinKeyer it is the negative rail +// the RS-232 swing comes from, and driving it starves the chip. +func recoverKeyer(p serial.Port) { + applog.Printf("winkeyer: waking the keyer — host close, reset, then a DTR pulse") + _, _ = p.Write([]byte{cmdNull, cmdNull, cmdNull, cmdAdmin, adminClose}) + time.Sleep(150 * time.Millisecond) + _, _ = p.Write([]byte{cmdAdmin, adminReset}) + time.Sleep(150 * time.Millisecond) + _ = p.SetDTR(false) + time.Sleep(250 * time.Millisecond) + _ = p.SetDTR(true) + drain(p) +} + // traceHandshake puts the opening exchange in the log, ALWAYS — unlike the // running trace beside it, which is behind the diagnostic option. //