refactor(winkeyer): learn the slow boot instead of naming the keyer
The K3NG entry added an hour ago is gone. It named one clone among many — WKmini, home-built Arduinos, unbranded boxes — for hardware that speaks exactly the same protocol, and it was the only line in the engine list that picked a boot delay rather than a protocol. An operator with an unlabelled clone would have had to guess. The delay is now learnt per port. The first connect finds out by failing the quick attempt and succeeding on the slow one; that fact is written to a global setting keyed by the port, and every connect afterwards goes straight to the slow attempt. Global rather than per profile on purpose: which keyer is plugged into COM3 belongs to the computer, and switching profiles for a different rig does not change the keyer on the desk. Two tests hold the contract from both sides — a slow keyer must be reported as slow, and a keyer that answers at once must not be, or every K1EL connect would inherit seconds it never needed.
This commit is contained in:
@@ -1326,6 +1326,7 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
a.winkeyer.OnSlowBoot(a.noteKeyerSlowBoot)
|
||||||
|
|
||||||
// Live space-weather (solar flux, sunspots, A/K indices) for the header strip
|
// Live space-weather (solar flux, sunspots, A/K indices) for the header strip
|
||||||
// and per-QSO stamping. Refreshes in the background; pushes a UI event on each
|
// and per-QSO stamping. Refreshes in the background; pushes a UI event on each
|
||||||
@@ -16677,6 +16678,43 @@ func (a *App) CIVTraceEnabled() bool { return cat.CIVTraceEnabled() }
|
|||||||
// WinkeyerTraceEnabled — same, for the keyer trace.
|
// WinkeyerTraceEnabled — same, for the keyer trace.
|
||||||
func (a *App) WinkeyerTraceEnabled() bool { return winkeyer.TraceEnabled() }
|
func (a *App) WinkeyerTraceEnabled() bool { return winkeyer.TraceEnabled() }
|
||||||
|
|
||||||
|
// keyerSlowBootKey names the "this port holds a keyer that reboots when opened"
|
||||||
|
// flag. GLOBAL, not per profile: which keyer is plugged into COM3 is a property
|
||||||
|
// of the computer, and an operator who switches profiles for a different rig has
|
||||||
|
// not changed the keyer on the desk.
|
||||||
|
func keyerSlowBootKey(port string) string {
|
||||||
|
return "winkeyer.slow_boot." + strings.ToUpper(strings.TrimSpace(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyerNeedsSlowBoot reports whether this port has already been seen to need the
|
||||||
|
// long opening wait.
|
||||||
|
//
|
||||||
|
// A K3NG keyer — an Arduino running the WinKeyer protocol — reboots when the
|
||||||
|
// port opens, because DTR is wired to its reset pin. The first connect finds
|
||||||
|
// that out by failing the quick attempt and succeeding on the slow one; every
|
||||||
|
// connect afterwards reads this and goes straight to the slow one.
|
||||||
|
//
|
||||||
|
// Learnt rather than configured. The alternative was a "K3NG" entry beside
|
||||||
|
// "WinKeyer" in the engine list, which would name one clone among many — WKmini,
|
||||||
|
// home-built Arduinos, unbranded boxes — and leave every other operator guessing
|
||||||
|
// which of the two to pick for hardware that speaks exactly the same protocol.
|
||||||
|
func (a *App) keyerNeedsSlowBoot(port string) bool {
|
||||||
|
if a.settings == nil || strings.TrimSpace(port) == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
v, _ := a.settings.GetGlobal(a.ctx, keyerSlowBootKey(port))
|
||||||
|
return v == "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
// noteKeyerSlowBoot records that this port's keyer needed the long wait.
|
||||||
|
func (a *App) noteKeyerSlowBoot(port string) {
|
||||||
|
if a.settings == nil || strings.TrimSpace(port) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = a.settings.SetGlobal(a.ctx, keyerSlowBootKey(port), "1")
|
||||||
|
applog.Printf("winkeyer: %s remembered as a slow-booting keyer — it will open directly next time", port)
|
||||||
|
}
|
||||||
|
|
||||||
// WinkeyerConnect opens the serial link using the saved config.
|
// WinkeyerConnect opens the serial link using the saved config.
|
||||||
func (a *App) WinkeyerConnect() error {
|
func (a *App) WinkeyerConnect() error {
|
||||||
if a.winkeyer == nil {
|
if a.winkeyer == nil {
|
||||||
@@ -16689,14 +16727,10 @@ func (a *App) WinkeyerConnect() error {
|
|||||||
cfg := s.Config
|
cfg := s.Config
|
||||||
// The "serial" engine keys CW on the port's DTR/RTS lines (SCU-17 style)
|
// The "serial" engine keys CW on the port's DTR/RTS lines (SCU-17 style)
|
||||||
// instead of talking the K1EL WinKeyer protocol — flag it for the manager.
|
// instead of talking the K1EL WinKeyer protocol — flag it for the manager.
|
||||||
// "k3ng" speaks the same protocol as a K1EL and differs only in how long it
|
if s.Engine == "serial" {
|
||||||
// takes to answer after the port opens; the manager needs to know which.
|
|
||||||
switch s.Engine {
|
|
||||||
case "serial":
|
|
||||||
cfg.Type = "serial"
|
cfg.Type = "serial"
|
||||||
case "k3ng":
|
|
||||||
cfg.Type = "k3ng"
|
|
||||||
}
|
}
|
||||||
|
cfg.SlowBoot = a.keyerNeedsSlowBoot(cfg.Port)
|
||||||
return a.winkeyer.Connect(cfg)
|
return a.winkeyer.Connect(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@
|
|||||||
"Appearance: new Sahara theme, warm sand tones for long sessions in daylight.",
|
"Appearance: new Sahara theme, warm sand tones for long sessions in daylight.",
|
||||||
"US counties: Connecticut returned the census planning regions, and San Francisco, Doña Ana, Baltimore city and several Alaska boroughs matched nothing at all. The cluster now shows the same county as the Info panel. Re-download the county database.",
|
"US counties: Connecticut returned the census planning regions, and San Francisco, Doña Ana, Baltimore city and several Alaska boroughs matched nothing at all. The cluster now shows the same county as the Info panel. Re-download the county database.",
|
||||||
"QSL: the manager (QSL_VIA) and the sending method (QSL_SENT_VIA / QSL_RCVD_VIA) are separate fields at last, as ADIF defines them. Imports keep both, exports write both, and a log where they were mixed is offered a correction at startup.",
|
"QSL: the manager (QSL_VIA) and the sending method (QSL_SENT_VIA / QSL_RCVD_VIA) are separate fields at last, as ADIF defines them. Imports keep both, exports write both, and a log where they were mixed is offered a correction at startup.",
|
||||||
"WinKeyer: the opening handshake follows K1EL’s own sequence (RTS off, parser resync, echo test), which wakes keyers that stayed silent, and a port where nothing answers now says so instead of showing “connected”. K3NG keyers have their own entry in the list — they reboot when the port opens and need longer to answer."
|
"WinKeyer: the opening handshake follows K1EL’s own sequence (RTS off, parser resync, echo test), which wakes keyers that stayed silent, and a port where nothing answers now says so instead of showing “connected”. A keyer that reboots when the port opens — K3NG and other Arduino builds — is waited out and remembered, so it opens directly next time."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Amplificateurs : cochez ceux qui partagent un combineur et ON, OFF et OPERATE agissent sur tous à la fois. Chacun garde ses propres mesures.",
|
"Amplificateurs : cochez ceux qui partagent un combineur et ON, OFF et OPERATE agissent sur tous à la fois. Chacun garde ses propres mesures.",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
"Apparence : nouveau thème Sahara, tons sable chauds pour les longues sessions en plein jour.",
|
"Apparence : nouveau thème Sahara, tons sable chauds pour les longues sessions en plein jour.",
|
||||||
"Comtés US : le Connecticut renvoyait les planning regions du recensement, et San Francisco, Doña Ana, Baltimore city et plusieurs districts d’Alaska ne correspondaient à rien. Le cluster affiche maintenant le même comté que le panneau Info. Rechargez la base des comtés.",
|
"Comtés US : le Connecticut renvoyait les planning regions du recensement, et San Francisco, Doña Ana, Baltimore city et plusieurs districts d’Alaska ne correspondaient à rien. Le cluster affiche maintenant le même comté que le panneau Info. Rechargez la base des comtés.",
|
||||||
"QSL : le manager (QSL_VIA) et le mode d’envoi (QSL_SENT_VIA / QSL_RCVD_VIA) sont enfin deux champs distincts, comme le veut l’ADIF. Les imports gardent les deux, les exports les écrivent, et un log où ils étaient mélangés se voit proposer une correction au démarrage.",
|
"QSL : le manager (QSL_VIA) et le mode d’envoi (QSL_SENT_VIA / QSL_RCVD_VIA) sont enfin deux champs distincts, comme le veut l’ADIF. Les imports gardent les deux, les exports les écrivent, et un log où ils étaient mélangés se voit proposer une correction au démarrage.",
|
||||||
"WinKeyer : la séquence d’ouverture suit celle de K1EL (RTS bas, resynchronisation, test d’écho), ce qui réveille les manipulateurs muets, et un port où rien ne répond le dit au lieu d’afficher « connecté ». Les K3NG ont leur propre entrée dans la liste : ils redémarrent à l’ouverture du port et mettent plus longtemps à répondre."
|
"WinKeyer : la séquence d’ouverture suit celle de K1EL (RTS bas, resynchronisation, test d’écho), ce qui réveille les manipulateurs muets, et un port où rien ne répond le dit au lieu d’afficher « connecté ». Un manipulateur qui redémarre à l’ouverture du port — K3NG et autres montages Arduino — est attendu puis mémorisé, pour s’ouvrir directement ensuite."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3884,7 +3884,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
|
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="winkeyer">{t('wk.engWinkeyer')}</SelectItem>
|
<SelectItem value="winkeyer">{t('wk.engWinkeyer')}</SelectItem>
|
||||||
<SelectItem value="k3ng">{t('wk.engK3ng')}</SelectItem>
|
|
||||||
<SelectItem value="serial">{t('wk.engSerial')}</SelectItem>
|
<SelectItem value="serial">{t('wk.engSerial')}</SelectItem>
|
||||||
<SelectItem value="icom">{t('wk.engIcom')}</SelectItem>
|
<SelectItem value="icom">{t('wk.engIcom')}</SelectItem>
|
||||||
<SelectItem value="yaesu">{t('wk.engYaesu')}</SelectItem>
|
<SelectItem value="yaesu">{t('wk.engYaesu')}</SelectItem>
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const en: Dict = {
|
|||||||
'wk.escClears': 'ESC clears the callsign too (otherwise ESC only stops transmission)',
|
'wk.escClears': 'ESC clears the callsign too (otherwise ESC only stops transmission)',
|
||||||
'wk.esm': 'ESM — Enter Sends Message (CW)',
|
'wk.esm': 'ESM — Enter Sends Message (CW)',
|
||||||
'wk.esmHint': 'In CW with the keyer on, Enter fires a macro by QSO stage instead of logging: empty callsign → F1 (CQ); callsign entered → F2 (report) and focus jumps to RST; Enter in RST → F3 (TU), which logs if the macro contains <LOGQSO>.',
|
'wk.esmHint': 'In CW with the keyer on, Enter fires a macro by QSO stage instead of logging: empty callsign → F1 (CQ); callsign entered → F2 (report) and focus jumps to RST; Enter in RST → F3 (TU), which logs if the macro contains <LOGQSO>.',
|
||||||
'wk.engK3ng': 'K3NG (WinKeyer emulation)', 'wk.engWinkeyer': 'WinKeyer (K1EL, serial)', 'wk.engSerial': 'Serial port (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (rig keyer)', 'wk.engYaesu': 'Yaesu (rig keyer)', 'wk.yaesuHint': 'Keying goes over the CAT link already configured in Settings → CAT — no second COM port. For the FTDX101 / FT-991A / FT-710 family. An FTDX10 does NOT accept it (DAKY included): on that one use the "Serial port (DTR=CW / RTS=PTT)" keyer on its other COM port (the standard one) with PC KEYING on DTR — confirmed working.', 'wk.catWarnYaesu': 'The Yaesu keyer needs the Yaesu CAT backend — CAT is currently {backend}.', 'wk.engKenwood': 'Kenwood / Elecraft (rig keyer)', 'wk.kenwoodHint': 'Keying goes over the CAT link already configured in Settings → CAT (the KY command) — no second COM port. Ideal for an Elecraft K3/K4 whose single USB port is the CAT port.', 'wk.catWarnKenwood': 'The Kenwood/Elecraft keyer needs the Kenwood/Elecraft CAT backend — CAT is currently {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (coming soon)',
|
'wk.engWinkeyer': 'WinKeyer (K1EL, serial)', 'wk.engSerial': 'Serial port (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (rig keyer)', 'wk.engYaesu': 'Yaesu (rig keyer)', 'wk.yaesuHint': 'Keying goes over the CAT link already configured in Settings → CAT — no second COM port. For the FTDX101 / FT-991A / FT-710 family. An FTDX10 does NOT accept it (DAKY included): on that one use the "Serial port (DTR=CW / RTS=PTT)" keyer on its other COM port (the standard one) with PC KEYING on DTR — confirmed working.', 'wk.catWarnYaesu': 'The Yaesu keyer needs the Yaesu CAT backend — CAT is currently {backend}.', 'wk.engKenwood': 'Kenwood / Elecraft (rig keyer)', 'wk.kenwoodHint': 'Keying goes over the CAT link already configured in Settings → CAT (the KY command) — no second COM port. Ideal for an Elecraft K3/K4 whose single USB port is the CAT port.', 'wk.catWarnKenwood': 'The Kenwood/Elecraft keyer needs the Kenwood/Elecraft CAT backend — CAT is currently {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (coming soon)',
|
||||||
'wk.icomNote': "Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode… are configured on the radio; only the speed is set from here (the rig's KEY SPEED).",
|
'wk.icomNote': "Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode… are configured on the radio; only the speed is set from here (the rig's KEY SPEED).",
|
||||||
'wk.flexNote': "FlexRadio keys CW through the radio's CWX keyer over the existing SmartSDR CAT connection — no WinKeyer or SmartCAT needed. It reuses the connection set in Settings → CAT, so there's nothing else to wire up here. Put a slice in CW mode. Only the speed is set from here; weight, sidetone and break-in are configured on the radio (break-in must be on for CW to actually transmit).",
|
'wk.flexNote': "FlexRadio keys CW through the radio's CWX keyer over the existing SmartSDR CAT connection — no WinKeyer or SmartCAT needed. It reuses the connection set in Settings → CAT, so there's nothing else to wire up here. Put a slice in CW mode. Only the speed is set from here; weight, sidetone and break-in are configured on the radio (break-in must be on for CW to actually transmit).",
|
||||||
'wk.catWarnIcom': 'Your CAT backend is set to {backend}. Icom CI-V CW needs the CAT backend set to Icom and connected — change it under Settings → CAT interface, otherwise sending CW will fail.',
|
'wk.catWarnIcom': 'Your CAT backend is set to {backend}. Icom CI-V CW needs the CAT backend set to Icom and connected — change it under Settings → CAT interface, otherwise sending CW will fail.',
|
||||||
@@ -593,7 +593,7 @@ const fr: Dict = {
|
|||||||
'wk.escClears': "ÉCHAP efface aussi l'indicatif (sinon ÉCHAP arrête seulement la transmission)",
|
'wk.escClears': "ÉCHAP efface aussi l'indicatif (sinon ÉCHAP arrête seulement la transmission)",
|
||||||
'wk.esm': 'ESM — Entrée envoie le message (CW)',
|
'wk.esm': 'ESM — Entrée envoie le message (CW)',
|
||||||
'wk.esmHint': "En CW avec le keyer actif, Entrée envoie un macro selon l'étape du QSO au lieu de loguer : indicatif vide → F1 (CQ) ; indicatif saisi → F2 (report) et le focus passe au RST ; Entrée dans le RST → F3 (TU), qui logue si le macro contient <LOGQSO>.",
|
'wk.esmHint': "En CW avec le keyer actif, Entrée envoie un macro selon l'étape du QSO au lieu de loguer : indicatif vide → F1 (CQ) ; indicatif saisi → F2 (report) et le focus passe au RST ; Entrée dans le RST → F3 (TU), qui logue si le macro contient <LOGQSO>.",
|
||||||
'wk.engK3ng': 'K3NG (émulation WinKeyer)', 'wk.engWinkeyer': 'WinKeyer (K1EL, série)', 'wk.engSerial': 'Port série (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (keyer de la radio)', 'wk.engYaesu': 'Yaesu (keyer de la radio)', 'wk.yaesuHint': "La manipulation passe par la liaison CAT déjà configurée dans Réglages → CAT — sans second port COM. Pour la famille FTDX101 / FT-991A / FT-710. Un FTDX10 ne l'accepte PAS (DAKY compris) : sur celui-ci, utilisez le keyer « Port série (DTR=CW / RTS=PTT) » sur son autre port COM (le standard) avec PC KEYING sur DTR — confirmé fonctionnel.", 'wk.catWarnYaesu': 'Le keyer Yaesu nécessite le backend CAT Yaesu — le CAT est actuellement : {backend}.', 'wk.engKenwood': 'Kenwood / Elecraft (keyer de la radio)', 'wk.kenwoodHint': "La manipulation passe par la liaison CAT déjà configurée dans Réglages → CAT (la commande KY) — sans second port COM. Idéal pour un Elecraft K3/K4 dont l'unique port USB est le port CAT.", 'wk.catWarnKenwood': 'Le keyer Kenwood/Elecraft nécessite le backend CAT Kenwood/Elecraft — le CAT est actuellement : {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (bientôt)',
|
'wk.engWinkeyer': 'WinKeyer (K1EL, série)', 'wk.engSerial': 'Port série (DTR=CW / RTS=PTT)', 'wk.engIcom': 'Icom CI-V (keyer de la radio)', 'wk.engYaesu': 'Yaesu (keyer de la radio)', 'wk.yaesuHint': "La manipulation passe par la liaison CAT déjà configurée dans Réglages → CAT — sans second port COM. Pour la famille FTDX101 / FT-991A / FT-710. Un FTDX10 ne l'accepte PAS (DAKY compris) : sur celui-ci, utilisez le keyer « Port série (DTR=CW / RTS=PTT) » sur son autre port COM (le standard) avec PC KEYING sur DTR — confirmé fonctionnel.", 'wk.catWarnYaesu': 'Le keyer Yaesu nécessite le backend CAT Yaesu — le CAT est actuellement : {backend}.', 'wk.engKenwood': 'Kenwood / Elecraft (keyer de la radio)', 'wk.kenwoodHint': "La manipulation passe par la liaison CAT déjà configurée dans Réglages → CAT (la commande KY) — sans second port COM. Idéal pour un Elecraft K3/K4 dont l'unique port USB est le port CAT.", 'wk.catWarnKenwood': 'Le keyer Kenwood/Elecraft nécessite le backend CAT Kenwood/Elecraft — le CAT est actuellement : {backend}.', 'wk.engFlex': 'FlexRadio (CWX)', 'wk.engTci': 'TCI (bientôt)',
|
||||||
'wk.icomNote': "L'Icom CI-V manipule la CW via le keyer interne de la radio sur la connexion CAT existante (commande 0x17) — il réutilise le port COM CAT défini dans Réglages → CAT, rien d'autre à câbler ici. Mets la radio en mode CW. Poids, ratio, sidetone, mode paddle… se règlent sur la radio ; seule la vitesse est définie ici (KEY SPEED de la radio).",
|
'wk.icomNote': "L'Icom CI-V manipule la CW via le keyer interne de la radio sur la connexion CAT existante (commande 0x17) — il réutilise le port COM CAT défini dans Réglages → CAT, rien d'autre à câbler ici. Mets la radio en mode CW. Poids, ratio, sidetone, mode paddle… se règlent sur la radio ; seule la vitesse est définie ici (KEY SPEED de la radio).",
|
||||||
'wk.flexNote': "FlexRadio manipule la CW via le keyer CWX de la radio sur la connexion SmartSDR CAT existante — pas besoin de WinKeyer ni de SmartCAT. Il réutilise la connexion définie dans Réglages → CAT, rien d'autre à câbler ici. Mets une slice en mode CW. Seule la vitesse est définie ici ; poids, sidetone et break-in se règlent sur la radio (le break-in doit être activé pour que la CW parte vraiment).",
|
'wk.flexNote': "FlexRadio manipule la CW via le keyer CWX de la radio sur la connexion SmartSDR CAT existante — pas besoin de WinKeyer ni de SmartCAT. Il réutilise la connexion définie dans Réglages → CAT, rien d'autre à câbler ici. Mets une slice en mode CW. Seule la vitesse est définie ici ; poids, sidetone et break-in se règlent sur la radio (le break-in doit être activé pour que la CW parte vraiment).",
|
||||||
'wk.catWarnIcom': "Ton backend CAT est réglé sur {backend}. La CW Icom CI-V nécessite le backend CAT réglé sur Icom et connecté — change-le dans Réglages → Interface CAT, sinon l'envoi CW échouera.",
|
'wk.catWarnIcom': "Ton backend CAT est réglé sur {backend}. La CW Icom CI-V nécessite le backend CAT réglé sur Icom et connecté — change-le dans Réglages → Interface CAT, sinon l'envoi CW échouera.",
|
||||||
|
|||||||
@@ -65,9 +65,10 @@ var errNoKeyer = errors.New("no WinKeyer answered on this port — check the cab
|
|||||||
// by whoever talked to it last (the first attempt's nulls clear that), and an
|
// by whoever talked to it last (the first attempt's nulls clear that), and an
|
||||||
// Arduino-based keyer still rebooting from the DTR edge (the second attempt
|
// Arduino-based keyer still rebooting from the DTR edge (the second attempt
|
||||||
// waits long enough for it).
|
// waits long enough for it).
|
||||||
// slowBoot skips straight to the long wait: set when the operator has told us
|
// slowBoot skips straight to the long wait, set when this port has already been
|
||||||
// the keyer is a K3NG, which reboots on every connect.
|
// seen to need it. The second return value says whether the long wait is what
|
||||||
func hostOpen(p serial.Port, slowBoot bool) (int, error) {
|
// worked, so the caller can remember it and open quickly next time.
|
||||||
|
func hostOpen(p serial.Port, slowBoot bool) (ver int, needsSlowBoot bool, err error) {
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for attempt := 1; attempt <= handshakeTry; attempt++ {
|
for attempt := 1; attempt <= handshakeTry; attempt++ {
|
||||||
wait := bootDelay
|
wait := bootDelay
|
||||||
@@ -77,16 +78,16 @@ func hostOpen(p serial.Port, slowBoot bool) (int, error) {
|
|||||||
ver, err := hostOpenOnce(p, wait)
|
ver, err := hostOpenOnce(p, wait)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if attempt > 1 {
|
if attempt > 1 {
|
||||||
applog.Printf("winkeyer: answered on attempt %d — the keyer needed %s to boot (a K3NG or other Arduino keyer with auto-reset enabled)", attempt, wait)
|
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)
|
||||||
}
|
}
|
||||||
return ver, nil
|
return ver, wait == resetDelay, nil
|
||||||
}
|
}
|
||||||
lastErr = err
|
lastErr = err
|
||||||
if attempt < handshakeTry {
|
if attempt < handshakeTry {
|
||||||
applog.Printf("winkeyer: handshake attempt %d failed (%v) — retrying after %s in case the keyer is rebooting", attempt, err, resetDelay)
|
applog.Printf("winkeyer: handshake attempt %d failed (%v) — retrying after %s in case the keyer is rebooting", attempt, err, resetDelay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0, lastErr
|
return 0, false, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func hostOpenOnce(p serial.Port, boot time.Duration) (int, error) {
|
func hostOpenOnce(p serial.Port, boot time.Duration) (int, error) {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (f *fakeKeyer) SetMode(*serial.Mode) error { return nil }
|
|||||||
// left mid-command simply absorbed.
|
// left mid-command simply absorbed.
|
||||||
func TestHostOpenFollowsK1ELSequence(t *testing.T) {
|
func TestHostOpenFollowsK1ELSequence(t *testing.T) {
|
||||||
f := &fakeKeyer{version: 23}
|
f := &fakeKeyer{version: 23}
|
||||||
ver, err := hostOpen(f, false)
|
ver, _, err := hostOpen(f, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("hostOpen: %v", err)
|
t.Fatalf("hostOpen: %v", err)
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ func TestHostOpenFollowsK1ELSequence(t *testing.T) {
|
|||||||
// having to unplug anything.
|
// having to unplug anything.
|
||||||
func TestHostOpenRecoversAConfusedParser(t *testing.T) {
|
func TestHostOpenRecoversAConfusedParser(t *testing.T) {
|
||||||
f := &fakeKeyer{version: 30, needsResync: true}
|
f := &fakeKeyer{version: 30, needsResync: true}
|
||||||
ver, err := hostOpen(f, false)
|
ver, _, err := hostOpen(f, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("hostOpen: %v", err)
|
t.Fatalf("hostOpen: %v", err)
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ func TestHostOpenRecoversAConfusedParser(t *testing.T) {
|
|||||||
// commands and a keyer that never made a sound.
|
// commands and a keyer that never made a sound.
|
||||||
func TestHostOpenFailsWhenNothingAnswers(t *testing.T) {
|
func TestHostOpenFailsWhenNothingAnswers(t *testing.T) {
|
||||||
f := &fakeKeyer{deaf: true}
|
f := &fakeKeyer{deaf: true}
|
||||||
if _, err := hostOpen(f, false); !errors.Is(err, errNoKeyer) {
|
if _, _, err := hostOpen(f, false); !errors.Is(err, errNoKeyer) {
|
||||||
t.Fatalf("want errNoKeyer, got %v", err)
|
t.Fatalf("want errNoKeyer, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ func TestHostOpenFailsWhenNothingAnswers(t *testing.T) {
|
|||||||
// reported as "no keyer".
|
// reported as "no keyer".
|
||||||
func TestHostOpenReportsMissingVersion(t *testing.T) {
|
func TestHostOpenReportsMissingVersion(t *testing.T) {
|
||||||
f := &fakeKeyer{mute: true}
|
f := &fakeKeyer{mute: true}
|
||||||
_, err := hostOpen(f, false)
|
_, _, err := hostOpen(f, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("want an error")
|
t.Fatal("want an error")
|
||||||
}
|
}
|
||||||
@@ -176,21 +176,36 @@ func (s *slowKeyer) Write(p []byte) (int, error) {
|
|||||||
func TestHostOpenWaitsOutAnArduinoReboot(t *testing.T) {
|
func TestHostOpenWaitsOutAnArduinoReboot(t *testing.T) {
|
||||||
f := &slowKeyer{ready: time.Now().Add(1500 * time.Millisecond)}
|
f := &slowKeyer{ready: time.Now().Add(1500 * time.Millisecond)}
|
||||||
f.version = 23
|
f.version = 23
|
||||||
ver, err := hostOpen(f, false)
|
ver, slow, err := hostOpen(f, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("hostOpen: %v", err)
|
t.Fatalf("hostOpen: %v", err)
|
||||||
}
|
}
|
||||||
if ver != 23 {
|
if ver != 23 {
|
||||||
t.Errorf("version = %d, want 23", ver)
|
t.Errorf("version = %d, want 23", ver)
|
||||||
}
|
}
|
||||||
|
// This second value is what gets remembered for the port, and it is the
|
||||||
|
// whole reason the operator is never asked what kind of keyer they own.
|
||||||
|
// Lose it and every later connect pays the same doomed quick attempt.
|
||||||
|
if !slow {
|
||||||
|
t.Error("the long wait is what worked, but it was not reported as needed")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telling OpsLog the keyer is a K3NG must skip the doomed fast attempt, so the
|
// A keyer that answers straight away must NOT be remembered as slow — that
|
||||||
// first try already allows for the reboot.
|
// would add seconds to every connect for a K1EL that never needed them.
|
||||||
|
func TestHostOpenDoesNotMarkAFastKeyerSlow(t *testing.T) {
|
||||||
|
f := &fakeKeyer{version: 23}
|
||||||
|
if _, slow, err := hostOpen(f, false); err != nil || slow {
|
||||||
|
t.Fatalf("hostOpen = slow %v, err %v — want a fast keyer left alone", slow, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A port already known to hold a slow keyer skips the doomed fast attempt, so
|
||||||
|
// the second connect is as quick as a K1EL's.
|
||||||
func TestHostOpenSlowBootSucceedsFirstTry(t *testing.T) {
|
func TestHostOpenSlowBootSucceedsFirstTry(t *testing.T) {
|
||||||
f := &slowKeyer{ready: time.Now().Add(1500 * time.Millisecond)}
|
f := &slowKeyer{ready: time.Now().Add(1500 * time.Millisecond)}
|
||||||
f.version = 23
|
f.version = 23
|
||||||
if _, err := hostOpen(f, true); err != nil {
|
if _, _, err := hostOpen(f, true); err != nil {
|
||||||
t.Fatalf("hostOpen: %v", err)
|
t.Fatalf("hostOpen: %v", err)
|
||||||
}
|
}
|
||||||
// One attempt: exactly one handshake on the wire, not two.
|
// One attempt: exactly one handshake on the wire, not two.
|
||||||
|
|||||||
@@ -51,10 +51,15 @@ type Config struct {
|
|||||||
UsePTT bool `json:"use_ptt"` // key PTT (Key/PTT output)
|
UsePTT bool `json:"use_ptt"` // key PTT (Key/PTT output)
|
||||||
SerialEcho bool `json:"serial_echo"` // device echoes sent chars back to host
|
SerialEcho bool `json:"serial_echo"` // device echoes sent chars back to host
|
||||||
|
|
||||||
|
// SlowBoot skips the quick opening attempt and waits for a keyer that reboots
|
||||||
|
// when the port opens — a K3NG or any other Arduino-based WinKeyer with its
|
||||||
|
// auto-reset still enabled. Not an operator setting: it is remembered per port
|
||||||
|
// the first time a keyer only answers the long attempt, so the second connect
|
||||||
|
// is as quick as a K1EL's and nobody has to know what is inside the box.
|
||||||
|
SlowBoot bool `json:"-"`
|
||||||
|
|
||||||
// Type selects the keyer engine on this serial port:
|
// Type selects the keyer engine on this serial port:
|
||||||
// "" / "k1el" → a K1EL WinKeyer chip (the default, everything above applies)
|
// "" / "k1el" → a K1EL WinKeyer chip (the default, everything above applies)
|
||||||
// "k3ng" → a K3NG keyer: an Arduino running the same protocol. Identical
|
|
||||||
// once open; it just needs time to reboot when the port opens.
|
|
||||||
// "serial" → the PC bit-bangs Morse on a control line (no WinKeyer chip):
|
// "serial" → the PC bit-bangs Morse on a control line (no WinKeyer chip):
|
||||||
// the "hardware CW keying" a Yaesu SCU-17 / generic interface
|
// the "hardware CW keying" a Yaesu SCU-17 / generic interface
|
||||||
// uses. WPM / Weight / Farnsworth / LeadIn / Tail / UsePTT still
|
// uses. WPM / Weight / Farnsworth / LeadIn / Tail / UsePTT still
|
||||||
@@ -110,12 +115,20 @@ type Manager struct {
|
|||||||
|
|
||||||
onStatus func(Status)
|
onStatus func(Status)
|
||||||
onEcho func(string) // chars the device echoes back as it keys them
|
onEcho func(string) // chars the device echoes back as it keys them
|
||||||
|
// onSlowBoot fires when a keyer answered only after the long wait, so the
|
||||||
|
// caller can persist that for this port. See Config.SlowBoot.
|
||||||
|
onSlowBoot func(port string)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(onStatus func(Status), onEcho func(string)) *Manager {
|
func NewManager(onStatus func(Status), onEcho func(string)) *Manager {
|
||||||
return &Manager{onStatus: onStatus, onEcho: onEcho}
|
return &Manager{onStatus: onStatus, onEcho: onEcho}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnSlowBoot registers the callback that persists "this port needs the long
|
||||||
|
// opening wait". Optional: without it a slow keyer still connects, it just
|
||||||
|
// pays the failed quick attempt on every connect.
|
||||||
|
func (m *Manager) OnSlowBoot(fn func(port string)) { m.onSlowBoot = fn }
|
||||||
|
|
||||||
// ListPorts returns the available serial port names (COM3, COM6, …).
|
// ListPorts returns the available serial port names (COM3, COM6, …).
|
||||||
func ListPorts() ([]string, error) {
|
func ListPorts() ([]string, error) {
|
||||||
ports, err := serial.GetPortsList()
|
ports, err := serial.GetPortsList()
|
||||||
@@ -166,13 +179,15 @@ func (m *Manager) Connect(cfg Config) error {
|
|||||||
return fmt.Errorf("winkeyer: open %s: %w", cfg.Port, err)
|
return fmt.Errorf("winkeyer: open %s: %w", cfg.Port, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A K3NG keyer reboots when the port opens (DTR is its reset line), so it is
|
ver, slowBoot, err := hostOpen(p, cfg.SlowBoot)
|
||||||
// given the long wait from the start rather than being failed once first.
|
|
||||||
ver, err := hostOpen(p, cfg.Type == "k3ng")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = p.Close()
|
_ = p.Close()
|
||||||
return fmt.Errorf("winkeyer: %s: %w", cfg.Port, err)
|
return fmt.Errorf("winkeyer: %s: %w", cfg.Port, err)
|
||||||
}
|
}
|
||||||
|
// Tell the caller to remember it, so this port opens quickly next time.
|
||||||
|
if slowBoot && !cfg.SlowBoot && m.onSlowBoot != nil {
|
||||||
|
m.onSlowBoot(cfg.Port)
|
||||||
|
}
|
||||||
_ = p.SetReadTimeout(200 * time.Millisecond)
|
_ = p.SetReadTimeout(200 * time.Millisecond)
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user