fix: serial CW keyer PTT dropping between words on CP210x (SCU-17)
Confirmed from HB9HBY's log: ptt=true, yet the FT-891 dropped to RX between words during a CQ macro — while the same SCU-17 works in N1MM. Root cause: go.bug.st/serial drives DTR/RTS through GetCommState/SetCommState, which on the SCU-17's Silicon Labs CP210x drops the asserted RTS (PTT) the moment DTR (CW) is toggled. N1MM/WSJT use EscapeCommFunction, which sets one line without disturbing the other. OpsLog now drives the lines the same way: a new lineCtl abstraction with a Windows implementation (linectl_windows.go) that opens the COM port via CreateFile and toggles DTR/RTS with windows.EscapeCommFunction (SETDTR/CLRDTR/ SETRTS/CLRRTS). linectl_other.go keeps go.bug.st/serial for non-Windows builds. The serial keyer holds a lineCtl instead of a serial.Port. RTS (PTT) now stays asserted for the whole macro, so the rig holds TX between words. Promotes golang.org/x/sys to a direct dependency.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
//go:build windows
|
||||
|
||||
package winkeyer
|
||||
|
||||
// linectl_windows.go — DTR/RTS line control via EscapeCommFunction, the direct
|
||||
// Win32 method N1MM/WSJT/fldigi use. It sets ONE line at a time without a
|
||||
// GetCommState/SetCommState round-trip, so a held RTS (PTT) is NOT disturbed when
|
||||
// DTR (CW) is toggled. go.bug.st/serial drives the lines via SetCommState, which
|
||||
// on USB-serial adapters (the SCU-17's CP210x, FTDI) drops the held RTS the moment
|
||||
// DTR changes — the "rig drops to RX between words" bug this fixes.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type winLineCtl struct {
|
||||
h windows.Handle
|
||||
}
|
||||
|
||||
// openLineCtl opens a COM port for line control only (no data I/O).
|
||||
func openLineCtl(port string) (lineCtl, error) {
|
||||
name := strings.TrimSpace(port)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("no serial port")
|
||||
}
|
||||
// The \\.\ prefix is required for COM10+ and harmless for COM1-9.
|
||||
if !strings.HasPrefix(name, `\\.\`) {
|
||||
name = `\\.\` + name
|
||||
}
|
||||
p, err := windows.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h, err := windows.CreateFile(p,
|
||||
windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil,
|
||||
windows.OPEN_EXISTING, 0, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %s: %w", port, err)
|
||||
}
|
||||
// A minimal DCB so the driver is in a sane state. Baud is irrelevant for pure
|
||||
// line control, but leaving fDtr/fRtsControl at DISABLE means only our explicit
|
||||
// EscapeCommFunction calls drive the lines.
|
||||
var dcb windows.DCB
|
||||
if windows.GetCommState(h, &dcb) == nil {
|
||||
dcb.BaudRate = 9600
|
||||
dcb.ByteSize = 8
|
||||
dcb.Parity = 0 // NOPARITY
|
||||
dcb.StopBits = 0 // ONESTOPBIT
|
||||
// Clear DTR (bits 4-5) and RTS (bits 12-13) control fields → DISABLE, so the
|
||||
// lines idle low until we assert them, and no hardware flow control fights us.
|
||||
dcb.Flags &^= 0x00000030
|
||||
dcb.Flags &^= 0x00003000
|
||||
_ = windows.SetCommState(h, &dcb)
|
||||
}
|
||||
c := &winLineCtl{h: h}
|
||||
// Start both lines low (inactive) — the keyer's idle() will do this too.
|
||||
_ = c.setDTR(false)
|
||||
_ = c.setRTS(false)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *winLineCtl) setDTR(on bool) error {
|
||||
f := uint32(windows.CLRDTR)
|
||||
if on {
|
||||
f = windows.SETDTR
|
||||
}
|
||||
return windows.EscapeCommFunction(c.h, f)
|
||||
}
|
||||
|
||||
func (c *winLineCtl) setRTS(on bool) error {
|
||||
f := uint32(windows.CLRRTS)
|
||||
if on {
|
||||
f = windows.SETRTS
|
||||
}
|
||||
return windows.EscapeCommFunction(c.h, f)
|
||||
}
|
||||
|
||||
func (c *winLineCtl) close() error { return windows.CloseHandle(c.h) }
|
||||
Reference in New Issue
Block a user