Tested on Mac OS, Linux and Windows with UART emulated on USB. I wrote general BSD code, because it should work, but I am not able to test it. On Mac OS I am quite sure that drain behaves as expected because it fixed an issue with buffer overflow (which OS does not report but fails). For Windows it seems that drain is actually part of `Write`, because `Write` was long and `Drain` was fast. But I have not found any mention in Win32 docs about buffering and asynchronous write, so I put there flushing code to be sure, and this code did not break anything. For Linux `Drain` is also taking more time than writing so it looks working too.
482 lines
12 KiB
Go
482 lines
12 KiB
Go
//
|
|
// Copyright 2014-2021 Cristian Maglie. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
//
|
|
|
|
package serial
|
|
|
|
/*
|
|
|
|
// MSDN article on Serial Communications:
|
|
// http://msdn.microsoft.com/en-us/library/ff802693.aspx
|
|
// (alternative link) https://msdn.microsoft.com/en-us/library/ms810467.aspx
|
|
|
|
// Arduino Playground article on serial communication with Windows API:
|
|
// http://playground.arduino.cc/Interfacing/CPPWindows
|
|
|
|
*/
|
|
|
|
import (
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
type windowsPort struct {
|
|
mu sync.Mutex
|
|
handle syscall.Handle
|
|
}
|
|
|
|
func nativeGetPortsList() ([]string, error) {
|
|
subKey, err := syscall.UTF16PtrFromString("HARDWARE\\DEVICEMAP\\SERIALCOMM\\")
|
|
if err != nil {
|
|
return nil, &PortError{code: ErrorEnumeratingPorts}
|
|
}
|
|
|
|
var h syscall.Handle
|
|
if err := syscall.RegOpenKeyEx(syscall.HKEY_LOCAL_MACHINE, subKey, 0, syscall.KEY_READ, &h); err != nil {
|
|
if errno, isErrno := err.(syscall.Errno); isErrno && errno == syscall.ERROR_FILE_NOT_FOUND {
|
|
return []string{}, nil
|
|
}
|
|
return nil, &PortError{code: ErrorEnumeratingPorts}
|
|
}
|
|
defer syscall.RegCloseKey(h)
|
|
|
|
var valuesCount uint32
|
|
if syscall.RegQueryInfoKey(h, nil, nil, nil, nil, nil, nil, &valuesCount, nil, nil, nil, nil) != nil {
|
|
return nil, &PortError{code: ErrorEnumeratingPorts}
|
|
}
|
|
|
|
list := make([]string, valuesCount)
|
|
for i := range list {
|
|
var data [1024]uint16
|
|
dataSize := uint32(len(data))
|
|
var name [1024]uint16
|
|
nameSize := uint32(len(name))
|
|
if regEnumValue(h, uint32(i), &name[0], &nameSize, nil, nil, &data[0], &dataSize) != nil {
|
|
return nil, &PortError{code: ErrorEnumeratingPorts}
|
|
}
|
|
list[i] = syscall.UTF16ToString(data[:])
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
func (port *windowsPort) Close() error {
|
|
port.mu.Lock()
|
|
defer func() {
|
|
port.handle = 0
|
|
port.mu.Unlock()
|
|
}()
|
|
if port.handle == 0 {
|
|
return nil
|
|
}
|
|
return syscall.CloseHandle(port.handle)
|
|
}
|
|
|
|
func (port *windowsPort) Read(p []byte) (int, error) {
|
|
var readed uint32
|
|
ev, err := createOverlappedEvent()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer syscall.CloseHandle(ev.HEvent)
|
|
|
|
err = syscall.ReadFile(port.handle, p, &readed, ev)
|
|
if err == syscall.ERROR_IO_PENDING {
|
|
err = getOverlappedResult(port.handle, ev, &readed, true)
|
|
}
|
|
switch err {
|
|
case nil:
|
|
// operation completed successfully
|
|
case syscall.ERROR_OPERATION_ABORTED:
|
|
// port may have been closed
|
|
return int(readed), &PortError{code: PortClosed, causedBy: err}
|
|
default:
|
|
// error happened
|
|
return int(readed), err
|
|
}
|
|
if readed > 0 {
|
|
return int(readed), nil
|
|
}
|
|
|
|
// Timeout
|
|
return 0, nil
|
|
}
|
|
|
|
func (port *windowsPort) Write(p []byte) (int, error) {
|
|
var writed uint32
|
|
ev, err := createOverlappedEvent()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer syscall.CloseHandle(ev.HEvent)
|
|
err = syscall.WriteFile(port.handle, p, &writed, ev)
|
|
if err == syscall.ERROR_IO_PENDING {
|
|
// wait for write to complete
|
|
err = getOverlappedResult(port.handle, ev, &writed, true)
|
|
}
|
|
return int(writed), err
|
|
}
|
|
|
|
func (port *windowsPort) Drain() (err error) {
|
|
return syscall.FlushFileBuffers(port.handle)
|
|
}
|
|
|
|
const (
|
|
purgeRxAbort uint32 = 0x0002
|
|
purgeRxClear = 0x0008
|
|
purgeTxAbort = 0x0001
|
|
purgeTxClear = 0x0004
|
|
)
|
|
|
|
func (port *windowsPort) ResetInputBuffer() error {
|
|
return purgeComm(port.handle, purgeRxClear|purgeRxAbort)
|
|
}
|
|
|
|
func (port *windowsPort) ResetOutputBuffer() error {
|
|
return purgeComm(port.handle, purgeTxClear|purgeTxAbort)
|
|
}
|
|
|
|
const (
|
|
dcbBinary uint32 = 0x00000001
|
|
dcbParity = 0x00000002
|
|
dcbOutXCTSFlow = 0x00000004
|
|
dcbOutXDSRFlow = 0x00000008
|
|
dcbDTRControlDisableMask = ^uint32(0x00000030)
|
|
dcbDTRControlEnable = 0x00000010
|
|
dcbDTRControlHandshake = 0x00000020
|
|
dcbDSRSensitivity = 0x00000040
|
|
dcbTXContinueOnXOFF = 0x00000080
|
|
dcbOutX = 0x00000100
|
|
dcbInX = 0x00000200
|
|
dcbErrorChar = 0x00000400
|
|
dcbNull = 0x00000800
|
|
dcbRTSControlDisbaleMask = ^uint32(0x00003000)
|
|
dcbRTSControlEnable = 0x00001000
|
|
dcbRTSControlHandshake = 0x00002000
|
|
dcbRTSControlToggle = 0x00003000
|
|
dcbAbortOnError = 0x00004000
|
|
)
|
|
|
|
type dcb struct {
|
|
DCBlength uint32
|
|
BaudRate uint32
|
|
|
|
// Flags field is a bitfield
|
|
// fBinary :1
|
|
// fParity :1
|
|
// fOutxCtsFlow :1
|
|
// fOutxDsrFlow :1
|
|
// fDtrControl :2
|
|
// fDsrSensitivity :1
|
|
// fTXContinueOnXoff :1
|
|
// fOutX :1
|
|
// fInX :1
|
|
// fErrorChar :1
|
|
// fNull :1
|
|
// fRtsControl :2
|
|
// fAbortOnError :1
|
|
// fDummy2 :17
|
|
Flags uint32
|
|
|
|
wReserved 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
|
|
}
|
|
|
|
const (
|
|
noParity = 0
|
|
oddParity = 1
|
|
evenParity = 2
|
|
markParity = 3
|
|
spaceParity = 4
|
|
)
|
|
|
|
var parityMap = map[Parity]byte{
|
|
NoParity: noParity,
|
|
OddParity: oddParity,
|
|
EvenParity: evenParity,
|
|
MarkParity: markParity,
|
|
SpaceParity: spaceParity,
|
|
}
|
|
|
|
const (
|
|
oneStopBit = 0
|
|
one5StopBits = 1
|
|
twoStopBits = 2
|
|
)
|
|
|
|
var stopBitsMap = map[StopBits]byte{
|
|
OneStopBit: oneStopBit,
|
|
OnePointFiveStopBits: one5StopBits,
|
|
TwoStopBits: twoStopBits,
|
|
}
|
|
|
|
const (
|
|
commFunctionSetXOFF = 1
|
|
commFunctionSetXON = 2
|
|
commFunctionSetRTS = 3
|
|
commFunctionClrRTS = 4
|
|
commFunctionSetDTR = 5
|
|
commFunctionClrDTR = 6
|
|
commFunctionSetBreak = 8
|
|
commFunctionClrBreak = 9
|
|
)
|
|
|
|
const (
|
|
msCTSOn = 0x0010
|
|
msDSROn = 0x0020
|
|
msRingOn = 0x0040
|
|
msRLSDOn = 0x0080
|
|
)
|
|
|
|
func (port *windowsPort) SetMode(mode *Mode) error {
|
|
params := dcb{}
|
|
if getCommState(port.handle, ¶ms) != nil {
|
|
port.Close()
|
|
return &PortError{code: InvalidSerialPort}
|
|
}
|
|
if mode.BaudRate == 0 {
|
|
params.BaudRate = 9600 // Default to 9600
|
|
} else {
|
|
params.BaudRate = uint32(mode.BaudRate)
|
|
}
|
|
if mode.DataBits == 0 {
|
|
params.ByteSize = 8 // Default to 8 bits
|
|
} else {
|
|
params.ByteSize = byte(mode.DataBits)
|
|
}
|
|
params.StopBits = stopBitsMap[mode.StopBits]
|
|
params.Parity = parityMap[mode.Parity]
|
|
if setCommState(port.handle, ¶ms) != nil {
|
|
port.Close()
|
|
return &PortError{code: InvalidSerialPort}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (port *windowsPort) SetDTR(dtr bool) error {
|
|
// Like for RTS there are problems with the escapeCommFunction
|
|
// observed behaviour was that DTR is set from false -> true
|
|
// when setting RTS from true -> false
|
|
// 1) Connect -> RTS = true (low) DTR = true (low) OKAY
|
|
// 2) SetDTR(false) -> RTS = true (low) DTR = false (heigh) OKAY
|
|
// 3) SetRTS(false) -> RTS = false (heigh) DTR = true (low) ERROR: DTR toggled
|
|
//
|
|
// In addition this way the CommState Flags are not updated
|
|
/*
|
|
var res bool
|
|
if dtr {
|
|
res = escapeCommFunction(port.handle, commFunctionSetDTR)
|
|
} else {
|
|
res = escapeCommFunction(port.handle, commFunctionClrDTR)
|
|
}
|
|
if !res {
|
|
return &PortError{}
|
|
}
|
|
return nil
|
|
*/
|
|
|
|
// The following seems a more reliable way to do it
|
|
|
|
params := &dcb{}
|
|
if err := getCommState(port.handle, params); err != nil {
|
|
return &PortError{causedBy: err}
|
|
}
|
|
params.Flags &= dcbDTRControlDisableMask
|
|
if dtr {
|
|
params.Flags |= dcbDTRControlEnable
|
|
}
|
|
if err := setCommState(port.handle, params); err != nil {
|
|
return &PortError{causedBy: err}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (port *windowsPort) SetRTS(rts bool) error {
|
|
// It seems that there is a bug in the Windows VCP driver:
|
|
// it doesn't send USB control message when the RTS bit is
|
|
// changed, so the following code not always works with
|
|
// USB-to-serial adapters.
|
|
//
|
|
// In addition this way the CommState Flags are not updated
|
|
|
|
/*
|
|
var res bool
|
|
if rts {
|
|
res = escapeCommFunction(port.handle, commFunctionSetRTS)
|
|
} else {
|
|
res = escapeCommFunction(port.handle, commFunctionClrRTS)
|
|
}
|
|
if !res {
|
|
return &PortError{}
|
|
}
|
|
return nil
|
|
*/
|
|
|
|
// The following seems a more reliable way to do it
|
|
|
|
params := &dcb{}
|
|
if err := getCommState(port.handle, params); err != nil {
|
|
return &PortError{causedBy: err}
|
|
}
|
|
params.Flags &= dcbRTSControlDisbaleMask
|
|
if rts {
|
|
params.Flags |= dcbRTSControlEnable
|
|
}
|
|
if err := setCommState(port.handle, params); err != nil {
|
|
return &PortError{causedBy: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (port *windowsPort) GetModemStatusBits() (*ModemStatusBits, error) {
|
|
var bits uint32
|
|
if !getCommModemStatus(port.handle, &bits) {
|
|
return nil, &PortError{}
|
|
}
|
|
return &ModemStatusBits{
|
|
CTS: (bits & msCTSOn) != 0,
|
|
DCD: (bits & msRLSDOn) != 0,
|
|
DSR: (bits & msDSROn) != 0,
|
|
RI: (bits & msRingOn) != 0,
|
|
}, nil
|
|
}
|
|
|
|
func (port *windowsPort) SetReadTimeout(timeout time.Duration) error {
|
|
commTimeouts := &commTimeouts{
|
|
ReadIntervalTimeout: 0xFFFFFFFF,
|
|
ReadTotalTimeoutMultiplier: 0xFFFFFFFF,
|
|
ReadTotalTimeoutConstant: 0xFFFFFFFE,
|
|
WriteTotalTimeoutConstant: 0,
|
|
WriteTotalTimeoutMultiplier: 0,
|
|
}
|
|
if timeout != NoTimeout {
|
|
ms := timeout.Milliseconds()
|
|
if ms > 0xFFFFFFFE || ms < 0 {
|
|
return &PortError{code: InvalidTimeoutValue}
|
|
}
|
|
commTimeouts.ReadTotalTimeoutConstant = uint32(ms)
|
|
}
|
|
|
|
if err := setCommTimeouts(port.handle, commTimeouts); err != nil {
|
|
return &PortError{code: InvalidTimeoutValue, causedBy: err}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (port *windowsPort) Break(d time.Duration) error {
|
|
if err := setCommBreak(port.handle); err != nil {
|
|
return &PortError{causedBy: err}
|
|
}
|
|
|
|
time.Sleep(d)
|
|
|
|
if err := clearCommBreak(port.handle); err != nil {
|
|
return &PortError{causedBy: err}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func createOverlappedEvent() (*syscall.Overlapped, error) {
|
|
h, err := createEvent(nil, true, false, nil)
|
|
return &syscall.Overlapped{HEvent: h}, err
|
|
}
|
|
|
|
func nativeOpen(portName string, mode *Mode) (*windowsPort, error) {
|
|
portName = "\\\\.\\" + portName
|
|
path, err := syscall.UTF16PtrFromString(portName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handle, err := syscall.CreateFile(
|
|
path,
|
|
syscall.GENERIC_READ|syscall.GENERIC_WRITE,
|
|
0, nil,
|
|
syscall.OPEN_EXISTING,
|
|
syscall.FILE_FLAG_OVERLAPPED,
|
|
0)
|
|
if err != nil {
|
|
switch err {
|
|
case syscall.ERROR_ACCESS_DENIED:
|
|
return nil, &PortError{code: PortBusy}
|
|
case syscall.ERROR_FILE_NOT_FOUND:
|
|
return nil, &PortError{code: PortNotFound}
|
|
}
|
|
return nil, err
|
|
}
|
|
// Create the serial port
|
|
port := &windowsPort{
|
|
handle: handle,
|
|
}
|
|
|
|
// Set port parameters
|
|
if port.SetMode(mode) != nil {
|
|
port.Close()
|
|
return nil, &PortError{code: InvalidSerialPort}
|
|
}
|
|
|
|
params := &dcb{}
|
|
if getCommState(port.handle, params) != nil {
|
|
port.Close()
|
|
return nil, &PortError{code: InvalidSerialPort}
|
|
}
|
|
params.Flags &= dcbDTRControlDisableMask
|
|
params.Flags &= dcbRTSControlDisbaleMask
|
|
if mode.InitialStatusBits == nil {
|
|
params.Flags |= dcbDTRControlEnable
|
|
params.Flags |= dcbRTSControlEnable
|
|
} else {
|
|
if mode.InitialStatusBits.DTR {
|
|
params.Flags |= dcbDTRControlEnable
|
|
}
|
|
if mode.InitialStatusBits.RTS {
|
|
params.Flags |= dcbRTSControlEnable
|
|
}
|
|
}
|
|
params.Flags &^= dcbOutXCTSFlow
|
|
params.Flags &^= dcbOutXDSRFlow
|
|
params.Flags &^= dcbDSRSensitivity
|
|
params.Flags |= dcbTXContinueOnXOFF
|
|
params.Flags &^= dcbInX
|
|
params.Flags &^= dcbOutX
|
|
params.Flags &^= dcbErrorChar
|
|
params.Flags &^= dcbNull
|
|
params.Flags &^= dcbAbortOnError
|
|
params.XonLim = 2048
|
|
params.XoffLim = 512
|
|
params.XonChar = 17 // DC1
|
|
params.XoffChar = 19 // C3
|
|
if setCommState(port.handle, params) != nil {
|
|
port.Close()
|
|
return nil, &PortError{code: InvalidSerialPort}
|
|
}
|
|
|
|
if port.SetReadTimeout(NoTimeout) != nil {
|
|
port.Close()
|
|
return nil, &PortError{code: InvalidSerialPort}
|
|
}
|
|
return port, nil
|
|
}
|