diff --git a/app.go b/app.go index ac73d65..299ce49 100644 --- a/app.go +++ b/app.go @@ -13245,6 +13245,14 @@ func (a *App) SaveWinkeyerSettings(s WinkeyerSettings) error { return err } } + // Apply line-control changes (PTT keying, key line, invert, lead/tail, speed) + // to a running serial keyer immediately — no reconnect needed. Harmless when + // the keyer isn't the serial type or isn't connected. + if a.winkeyer != nil && s.Engine == "serial" { + cfg := s.Config + cfg.Type = "serial" + a.winkeyer.ApplySerialConfig(cfg) + } return nil } diff --git a/changelog.json b/changelog.json index 0722c65..8e3682f 100644 --- a/changelog.json +++ b/changelog.json @@ -3,10 +3,12 @@ "version": "0.20.12", "date": "2026-07-23", "en": [ + "Serial CW keyer: 'Key PTT line' now defaults on when you pick the Serial engine (without it the rig drops to RX between words on semi-break-in), and changing the PTT / key-line / speed options now applies immediately without disconnecting the keyer. It also logs each transmission's PTT state to help diagnose a rig that still won't hold TX.", "New (Settings → General): show the ClubLog 'Most Wanted' rank in the entry-strip band matrix. When on, a 'MW #rank' pill appears next to the DXCC entity name (1 = the most wanted entity), coloured hotter the more wanted it is. The list is fetched from ClubLog and personalised to your callsign, refreshed daily.", "Fixed the colour theme sometimes resetting to light after an update/relaunch: an update can clear the WebView's localStorage, and the fallback that restores the theme from the local settings database gave up after ~2.4s — occasionally too soon during the brief startup window before that store is ready. It now keeps retrying until the store actually answers, so a dark theme is reliably restored." ], "fr": [ + "Keyer CW série : « Key PTT line » est maintenant activé par défaut quand on choisit le moteur Série (sans lui la radio retombe en RX entre les mots en semi-break-in), et changer les options PTT / ligne de manip / vitesse s'applique immédiatement sans déconnecter le keyer. Il logue aussi l'état PTT de chaque émission pour diagnostiquer une radio qui ne tient toujours pas le TX.", "Nouveau (Réglages → Général) : afficher le rang « Most Wanted » de ClubLog dans la matrice de bandes de la barre de saisie. Activé, une pastille « MW #rang » apparaît à côté du nom de l'entité DXCC (1 = l'entité la plus recherchée), d'autant plus colorée qu'elle est recherchée. La liste est récupérée depuis ClubLog et personnalisée selon ton indicatif, rafraîchie quotidiennement.", "Correction du thème de couleur qui revenait parfois au clair après une mise à jour/relancement : une mise à jour peut vider le localStorage de la WebView, et le repli qui restaure le thème depuis la base de réglages locale abandonnait après ~2,4s — parfois trop tôt pendant le court instant de démarrage avant que ce store soit prêt. Il réessaie maintenant jusqu'à ce que le store réponde vraiment, donc un thème sombre est restauré de façon fiable." ] diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 08dbb7a..f2c42ba 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -3027,7 +3027,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
- setWkField(v === 'serial' ? { engine: v, use_ptt: true } : { engine: v })}> WinKeyer (K1EL, serial) diff --git a/internal/winkeyer/serialcw.go b/internal/winkeyer/serialcw.go index 909938a..2ed503b 100644 --- a/internal/winkeyer/serialcw.go +++ b/internal/winkeyer/serialcw.go @@ -14,9 +14,12 @@ package winkeyer import ( "strings" "sync" + "sync/atomic" "time" "go.bug.st/serial" + + "hamlog/internal/applog" ) // morseTable maps a keyable character to its Morse element string (. = dit, @@ -35,14 +38,16 @@ var morseTable = map[rune]string{ } // serialKeyer bit-bangs CW on a serial control line. Owned by a winkeyer.Manager -// while Type == "serial"; all keying happens in run(). +// while Type == "serial"; all keying happens in run(). The line-control options +// (key line, invert, PTT, lead/tail) are atomic so ApplyConfig can update them +// LIVE — e.g. ticking "Key PTT line" takes effect without reconnecting the keyer. type serialKeyer struct { port serial.Port - keyDTR bool // true: CW on DTR, PTT on RTS (N1MM default). false: swapped. - invert bool // true: active-LOW — asserting a line means driving it LOW - usePTT bool - leadMs int - tailMs int + keyDTR atomic.Bool // true: CW on DTR, PTT on RTS (N1MM default). false: swapped. + invert atomic.Bool // true: active-LOW — asserting a line means driving it LOW + usePTT atomic.Bool // hold the PTT line for the whole transmission + leadMs atomic.Int32 // PTT lead-in + tailMs atomic.Int32 // PTT hold (hang) after the last element onBusy func(bool) mu sync.Mutex @@ -58,11 +63,6 @@ type serialKeyer struct { func newSerialKeyer(port serial.Port, cfg Config, onBusy func(bool)) *serialKeyer { k := &serialKeyer{ port: port, - keyDTR: !strings.EqualFold(strings.TrimSpace(cfg.CWKeyLine), "rts"), // default DTR=CW - invert: cfg.CWInvert, - usePTT: cfg.UsePTT, - leadMs: cfg.LeadInMs, - tailMs: cfg.TailMs, onBusy: onBusy, wpm: cfg.WPM, farns: cfg.Farnsworth, @@ -71,28 +71,49 @@ func newSerialKeyer(port serial.Port, cfg Config, onBusy func(bool)) *serialKeye stop: make(chan struct{}), done: make(chan struct{}), } + k.keyDTR.Store(!strings.EqualFold(strings.TrimSpace(cfg.CWKeyLine), "rts")) // default DTR=CW + k.invert.Store(cfg.CWInvert) + k.usePTT.Store(cfg.UsePTT) + k.leadMs.Store(int32(cfg.LeadInMs)) + k.tailMs.Store(int32(cfg.TailMs)) k.idle() // start inactive: key up, PTT off go k.run() return k } +// ApplyConfig live-updates the line-control options (no port reopen), so a +// settings change is felt from the next character without a reconnect. Speed and +// Farnsworth are updated too. keyText re-reads these per element. +func (k *serialKeyer) ApplyConfig(cfg Config) { + k.keyDTR.Store(!strings.EqualFold(strings.TrimSpace(cfg.CWKeyLine), "rts")) + k.invert.Store(cfg.CWInvert) + k.usePTT.Store(cfg.UsePTT) + k.leadMs.Store(int32(cfg.LeadInMs)) + k.tailMs.Store(int32(cfg.TailMs)) + k.mu.Lock() + k.wpm = cfg.WPM + k.farns = cfg.Farnsworth + k.mu.Unlock() + k.idle() // re-assert idle lines with any new polarity/key-line choice +} + // level maps a logical "active" state to the physical line level, honouring the // invert (active-LOW) option: normally active = HIGH, inverted active = LOW. -func (k *serialKeyer) level(active bool) bool { return active != k.invert } +func (k *serialKeyer) level(active bool) bool { return active != k.invert.Load() } // setKey raises/lowers the CW key line; setPTT the PTT line (only when enabled). func (k *serialKeyer) setKey(down bool) { - if k.keyDTR { + if k.keyDTR.Load() { _ = k.port.SetDTR(k.level(down)) } else { _ = k.port.SetRTS(k.level(down)) } } func (k *serialKeyer) setPTT(on bool) { - if !k.usePTT { + if !k.usePTT.Load() { return } - if k.keyDTR { + if k.keyDTR.Load() { _ = k.port.SetRTS(k.level(on)) } else { _ = k.port.SetDTR(k.level(on)) @@ -150,10 +171,16 @@ func (k *serialKeyer) run() { return case s := <-k.in: k.onBusy(true) + // Diagnostic: confirms whether PTT is actually held for the whole macro + // (a rig dropping to RX between words = usePTT off, or the interface's + // PTT line isn't wired / honoured in CW). + applog.Printf("winkeyer serial: TX %.40q ptt=%v line=%s lead=%dms tail=%dms", + s, k.usePTT.Load(), map[bool]string{true: "DTR-CW/RTS-PTT", false: "RTS-CW/DTR-PTT"}[k.keyDTR.Load()], + k.leadMs.Load(), k.tailMs.Load()) k.setPTT(true) - k.sleep(time.Duration(k.leadMs) * time.Millisecond) + k.sleep(time.Duration(k.leadMs.Load()) * time.Millisecond) k.keyText(s) - hang := time.Duration(k.tailMs) * time.Millisecond + hang := time.Duration(k.tailMs.Load()) * time.Millisecond if hang <= 0 { hang = 5 * time.Millisecond } diff --git a/internal/winkeyer/winkeyer.go b/internal/winkeyer/winkeyer.go index 7cb1927..8c28168 100644 --- a/internal/winkeyer/winkeyer.go +++ b/internal/winkeyer/winkeyer.go @@ -189,6 +189,21 @@ func (m *Manager) Connect(cfg Config) error { return nil } +// ApplySerialConfig live-updates a running serial-line keyer's control options +// (PTT keying, key line, invert, lead/tail, speed) WITHOUT reopening the port — +// so ticking "Key PTT line" (or changing the key line) takes effect from the next +// character, no reconnect needed. No-op unless a serial keyer is running. +func (m *Manager) ApplySerialConfig(cfg Config) { + cfg = cfg.normalised() + m.mu.Lock() + sk := m.serial + m.cfg = cfg + m.mu.Unlock() + if sk != nil { + sk.ApplyConfig(cfg) + } +} + // connectSerial opens the port for line-keying (DTR=CW / RTS=PTT) — no K1EL // handshake, no read loop. The PC bit-bangs the Morse itself (see serialcw.go). func (m *Manager) connectSerial(cfg Config) error {