fix: serial CW keyer PTT — default on, live-apply, and diagnostic log
Reported: on a CQ macro the rig drops to RX between words instead of holding TX for the whole macro. Root cause is PTT not being held (usePTT off, or the RTS PTT line not wired/honoured). Improvements: - "Key PTT line" now defaults ON when the Serial engine is selected — without it the rig runs on semi-break-in and drops between words. - The serial keyer's line options (PTT, key line, invert, lead/tail, speed) are now atomic and live-updatable: SaveWinkeyerSettings -> ApplySerialConfig applies them from the next character, no reconnect needed. - Each transmission logs its PTT state / key line / lead/tail, so a rig that still won't hold TX can be diagnosed from the app log. The macro itself is sent as one string (not fragmented), so PTT is genuinely held across word gaps when usePTT is on — pending on-air confirmation via the log.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user