feat: serial CW keyer — add invert (active-LOW) polarity option

Some CW interfaces key on the opposite line level (transmit at rest / key
backwards). New "Invert keying (active-LOW)" toggle flips the asserted level for
both the CW and PTT lines, and the idle state drives both lines to their
inactive level accordingly so an active-low rig isn't left keyed at rest.
Default off = N1MM convention (line HIGH = active).
This commit is contained in:
2026-07-23 18:50:01 +02:00
parent 2c5416500f
commit 43f15f1a2c
5 changed files with 33 additions and 13 deletions
+20 -8
View File
@@ -39,6 +39,7 @@ var morseTable = map[rune]string{
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
@@ -58,6 +59,7 @@ func newSerialKeyer(port serial.Port, cfg Config, onBusy func(bool)) *serialKeye
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,
@@ -69,17 +71,21 @@ func newSerialKeyer(port serial.Port, cfg Config, onBusy func(bool)) *serialKeye
stop: make(chan struct{}),
done: make(chan struct{}),
}
k.allLow() // start idle: key up, PTT off
k.idle() // start inactive: key up, PTT off
go k.run()
return k
}
// 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 }
// setKey raises/lowers the CW key line; setPTT the PTT line (only when enabled).
func (k *serialKeyer) setKey(down bool) {
if k.keyDTR {
_ = k.port.SetDTR(down)
_ = k.port.SetDTR(k.level(down))
} else {
_ = k.port.SetRTS(down)
_ = k.port.SetRTS(k.level(down))
}
}
func (k *serialKeyer) setPTT(on bool) {
@@ -87,12 +93,18 @@ func (k *serialKeyer) setPTT(on bool) {
return
}
if k.keyDTR {
_ = k.port.SetRTS(on)
_ = k.port.SetRTS(k.level(on))
} else {
_ = k.port.SetDTR(on)
_ = k.port.SetDTR(k.level(on))
}
}
func (k *serialKeyer) allLow() { _ = k.port.SetDTR(false); _ = k.port.SetRTS(false) }
// idle drives BOTH lines to their inactive level (key up, PTT off), respecting
// the invert option — so an active-LOW interface isn't left keyed at rest.
func (k *serialKeyer) idle() {
_ = k.port.SetDTR(k.level(false))
_ = k.port.SetRTS(k.level(false))
}
// SetSpeed / Send / Stop mirror the WinKeyer manager's control surface.
func (k *serialKeyer) SetSpeed(wpm int) {
@@ -131,7 +143,7 @@ func (k *serialKeyer) Close() {
func (k *serialKeyer) run() {
defer close(k.done)
defer k.allLow()
defer k.idle()
for {
select {
case <-k.stop:
@@ -149,7 +161,7 @@ func (k *serialKeyer) run() {
for {
select {
case <-k.stop:
k.allLow()
k.idle()
k.onBusy(false)
return
case s2 := <-k.in:
+1
View File
@@ -58,6 +58,7 @@ type Config struct {
// apply; the paddle/sidetone/ratio fields are WinKeyer-only.
Type string `json:"type"`
CWKeyLine string `json:"cw_key_line"` // serial: "dtr" (CW on DTR, PTT on RTS — default) | "rts"
CWInvert bool `json:"cw_invert"` // serial: invert line polarity (active-LOW) for both key and PTT
}
func (c Config) normalised() Config {