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.
82 lines
2.4 KiB
Go
82 lines
2.4 KiB
Go
//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) }
|