Files
picoclaw/pkg/tools/hardware/serial_windows.go
T

248 lines
5.8 KiB
Go

//go:build windows
package hardwaretools
import (
"context"
"sort"
"strings"
"time"
"unsafe"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGetCommState = kernel32.NewProc("GetCommState")
procSetCommState = kernel32.NewProc("SetCommState")
procSetCommTimeouts = kernel32.NewProc("SetCommTimeouts")
procPurgeComm = kernel32.NewProc("PurgeComm")
)
const (
purgeTxClear = 0x0004
purgeRxClear = 0x0008
dcbFlagBinary = 0x00000001
dcbFlagParity = 0x00000002
dcbFlagOutxCtsFlow = 0x00000004
dcbFlagOutxDsrFlow = 0x00000008
dcbFlagDtrControlMask = 0x00000030
dcbFlagDsrSensitivity = 0x00000040
dcbFlagTXContinueOnXoff = 0x00000080
dcbFlagOutX = 0x00000100
dcbFlagInX = 0x00000200
dcbFlagRtsControlMask = 0x00003000
)
type dcb struct {
DCBlength uint32
BaudRate uint32
Flags uint32
Reserved uint16
XonLim uint16
XoffLim uint16
ByteSize byte
Parity byte
StopBits byte
XonChar byte
XoffChar byte
ErrorChar byte
EofChar byte
EvtChar byte
wReserved1 uint16
}
type commTimeouts struct {
ReadIntervalTimeout uint32
ReadTotalTimeoutMultiplier uint32
ReadTotalTimeoutConstant uint32
WriteTotalTimeoutMultiplier uint32
WriteTotalTimeoutConstant uint32
}
func serialListPorts() ([]serialPortInfo, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `HARDWARE\DEVICEMAP\SERIALCOMM`, registry.QUERY_VALUE)
if err != nil {
if err == registry.ErrNotExist {
return nil, nil
}
return nil, err
}
defer key.Close()
names, err := key.ReadValueNames(-1)
if err != nil {
return nil, err
}
ports := make([]serialPortInfo, 0, len(names))
seen := make(map[string]struct{})
for _, name := range names {
value, _, err := key.GetStringValue(name)
if err != nil {
continue
}
portName := strings.TrimSpace(value)
if portName == "" {
continue
}
normalized := strings.ToUpper(portName)
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
ports = append(ports, serialPortInfo{
Name: normalized,
Path: normalized,
})
}
sort.Slice(ports, func(i, j int) bool {
return ports[i].Path < ports[j].Path
})
return ports, nil
}
func serialRead(ctx context.Context, cfg serialConfig, length int, timeout time.Duration) ([]byte, error) {
if err := serialContextErr(ctx); err != nil {
return nil, err
}
handle, err := openAndConfigureWindowsSerial(cfg, timeout)
if err != nil {
return nil, err
}
defer windows.CloseHandle(handle)
if err := serialContextErr(ctx); err != nil {
return nil, err
}
buf := make([]byte, length)
var read uint32
// Synchronous serial I/O on Windows cannot be interrupted once the syscall starts.
// COMMTIMEOUTS bounds how long turn cancellation may take to surface.
if err := windows.ReadFile(handle, buf, &read, nil); err != nil {
return nil, err
}
return buf[:read], nil
}
func serialWrite(ctx context.Context, cfg serialConfig, data []byte, timeout time.Duration) (int, error) {
if err := serialContextErr(ctx); err != nil {
return 0, err
}
handle, err := openAndConfigureWindowsSerial(cfg, timeout)
if err != nil {
return 0, err
}
defer windows.CloseHandle(handle)
if err := serialContextErr(ctx); err != nil {
return 0, err
}
return serialWriteAll(ctx, data, timeout, time.Now, func(chunk []byte) (int, error) {
var written uint32
// Like ReadFile above, this synchronous WriteFile call relies on COMMTIMEOUTS
// rather than context preemption once the syscall is in flight.
if err := windows.WriteFile(handle, chunk, &written, nil); err != nil {
return int(written), err
}
return int(written), nil
})
}
func openAndConfigureWindowsSerial(cfg serialConfig, timeout time.Duration) (windows.Handle, error) {
handle, err := windows.CreateFile(
windows.StringToUTF16Ptr(cfg.Port),
windows.GENERIC_READ|windows.GENERIC_WRITE,
0,
nil,
windows.OPEN_EXISTING,
0,
0,
)
if err != nil {
return 0, err
}
if err := configureWindowsSerialPort(handle, cfg, timeout); err != nil {
windows.CloseHandle(handle)
return 0, err
}
return handle, nil
}
func configureWindowsSerialPort(handle windows.Handle, cfg serialConfig, timeout time.Duration) error {
state := dcb{DCBlength: uint32(unsafe.Sizeof(dcb{}))}
r1, _, err := procGetCommState.Call(uintptr(handle), uintptr(unsafe.Pointer(&state)))
if r1 == 0 {
return err
}
state.BaudRate = uint32(cfg.Baud)
state.ByteSize = byte(cfg.DataBits)
state.Flags = sanitizeWindowsSerialFlags(state.Flags)
state.Flags |= dcbFlagBinary
switch cfg.Parity {
case "even":
state.Parity = 2
state.Flags |= dcbFlagParity
case "odd":
state.Parity = 1
state.Flags |= dcbFlagParity
default:
state.Parity = 0
state.Flags &^= dcbFlagParity
}
switch cfg.StopBits {
case 2:
state.StopBits = 2
default:
state.StopBits = 0
}
r1, _, err = procSetCommState.Call(uintptr(handle), uintptr(unsafe.Pointer(&state)))
if r1 == 0 {
return err
}
timeoutMS := uint32(timeout / time.Millisecond)
if timeoutMS == 0 {
timeoutMS = 1
}
timeouts := commTimeouts{
ReadIntervalTimeout: timeoutMS,
ReadTotalTimeoutConstant: timeoutMS,
WriteTotalTimeoutConstant: timeoutMS,
ReadTotalTimeoutMultiplier: 0,
WriteTotalTimeoutMultiplier: 0,
}
r1, _, err = procSetCommTimeouts.Call(uintptr(handle), uintptr(unsafe.Pointer(&timeouts)))
if r1 == 0 {
return err
}
procPurgeComm.Call(uintptr(handle), uintptr(purgeRxClear|purgeTxClear))
return nil
}
func sanitizeWindowsSerialFlags(flags uint32) uint32 {
flags &^= dcbFlagOutxCtsFlow |
dcbFlagOutxDsrFlow |
dcbFlagDtrControlMask |
dcbFlagDsrSensitivity |
dcbFlagTXContinueOnXoff |
dcbFlagOutX |
dcbFlagInX |
dcbFlagRtsControlMask
return flags
}