Merge pull request #109 from cmaglie/timeouts

Implementation of read timeouts
This commit is contained in:
Cristian Maglie
2021-06-29 17:00:47 +02:00
committed by GitHub
3 changed files with 93 additions and 21 deletions

View File

@@ -6,6 +6,8 @@
package serial package serial
import "time"
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go //go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go syscall_windows.go
// Port is the interface for a serial Port // Port is the interface for a serial Port
@@ -40,10 +42,17 @@ type Port interface {
// modem status bits for the serial port (CTS, DSR, etc...) // modem status bits for the serial port (CTS, DSR, etc...)
GetModemStatusBits() (*ModemStatusBits, error) GetModemStatusBits() (*ModemStatusBits, error)
// SetReadTimeout sets the timeout for the Read operation or use serial.NoTimeout
// to disable read timeout.
SetReadTimeout(t time.Duration) error
// Close the serial port // Close the serial port
Close() error Close() error
} }
// NoTimeout should be used as a parameter to SetReadTimeout to disable timeout.
var NoTimeout time.Duration = -1
// ModemStatusBits contains all the modem status bits for a serial port (CTS, DSR, etc...). // ModemStatusBits contains all the modem status bits for a serial port (CTS, DSR, etc...).
// It can be retrieved with the Port.GetModemStatusBits() method. // It can be retrieved with the Port.GetModemStatusBits() method.
type ModemStatusBits struct { type ModemStatusBits struct {
@@ -125,6 +134,8 @@ const (
InvalidParity InvalidParity
// InvalidStopBits the selected number of stop bits is not valid or not supported // InvalidStopBits the selected number of stop bits is not valid or not supported
InvalidStopBits InvalidStopBits
// InvalidTimeoutValue the timeout value is not valid or not supported
InvalidTimeoutValue
// ErrorEnumeratingPorts an error occurred while listing serial port // ErrorEnumeratingPorts an error occurred while listing serial port
ErrorEnumeratingPorts ErrorEnumeratingPorts
// PortClosed the port has been closed while the operation is in progress // PortClosed the port has been closed while the operation is in progress
@@ -152,6 +163,8 @@ func (e PortError) EncodedErrorString() string {
return "Port parity invalid or not supported" return "Port parity invalid or not supported"
case InvalidStopBits: case InvalidStopBits:
return "Port stop bits invalid or not supported" return "Port stop bits invalid or not supported"
case InvalidTimeoutValue:
return "Timeout value invalid or not supported"
case ErrorEnumeratingPorts: case ErrorEnumeratingPorts:
return "Could not enumerate serial ports" return "Could not enumerate serial ports"
case PortClosed: case PortClosed:

View File

@@ -14,6 +14,7 @@ import (
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time"
"go.bug.st/serial/unixutils" "go.bug.st/serial/unixutils"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
@@ -22,6 +23,7 @@ import (
type unixPort struct { type unixPort struct {
handle int handle int
readTimeout time.Duration
closeLock sync.RWMutex closeLock sync.RWMutex
closeSignal *unixutils.Pipe closeSignal *unixutils.Pipe
opened uint32 opened uint32
@@ -61,9 +63,18 @@ func (port *unixPort) Read(p []byte) (int, error) {
return 0, &PortError{code: PortClosed} return 0, &PortError{code: PortClosed}
} }
var deadline time.Time
if port.readTimeout != NoTimeout {
deadline = time.Now().Add(port.readTimeout)
}
fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD()) fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD())
for { for {
res, err := unixutils.Select(fds, nil, fds, -1) timeout := time.Duration(-1)
if port.readTimeout != NoTimeout {
timeout = deadline.Sub(time.Now())
}
res, err := unixutils.Select(fds, nil, fds, timeout)
if err == unix.EINTR { if err == unix.EINTR {
continue continue
} }
@@ -73,10 +84,20 @@ func (port *unixPort) Read(p []byte) (int, error) {
if res.IsReadable(port.closeSignal.ReadFD()) { if res.IsReadable(port.closeSignal.ReadFD()) {
return 0, &PortError{code: PortClosed} return 0, &PortError{code: PortClosed}
} }
if !res.IsReadable(port.handle) {
// Timeout happened
return 0, nil
}
n, err := unix.Read(port.handle, p) n, err := unix.Read(port.handle, p)
if err == unix.EINTR { if err == unix.EINTR {
continue continue
} }
// Linux: when the port is disconnected during a read operation
// the port is left in a "readable with zero-length-data" state.
// https://stackoverflow.com/a/34945814/1655275
if n == 0 && err == nil {
return 0, &PortError{code: PortClosed}
}
if n < 0 { // Do not return -1 unix errors if n < 0 { // Do not return -1 unix errors
n = 0 n = 0
} }
@@ -146,6 +167,14 @@ func (port *unixPort) SetRTS(rts bool) error {
return port.setModemBitsStatus(status) return port.setModemBitsStatus(status)
} }
func (port *unixPort) SetReadTimeout(timeout time.Duration) error {
if timeout < 0 && timeout != NoTimeout {
return &PortError{code: InvalidTimeoutValue}
}
port.readTimeout = timeout
return nil
}
func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) { func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) {
status, err := port.getModemBitsStatus() status, err := port.getModemBitsStatus()
if err != nil { if err != nil {
@@ -171,8 +200,9 @@ func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
return nil, err return nil, err
} }
port := &unixPort{ port := &unixPort{
handle: h, handle: h,
opened: 1, opened: 1,
readTimeout: NoTimeout,
} }
// Setup serial port // Setup serial port

View File

@@ -18,13 +18,15 @@ package serial
*/ */
import ( import (
"syscall"
"sync" "sync"
"syscall"
"time"
) )
type windowsPort struct { type windowsPort struct {
mu sync.Mutex mu sync.Mutex
handle syscall.Handle handle syscall.Handle
readTimeoutCycles int64
} }
func nativeGetPortsList() ([]string, error) { func nativeGetPortsList() ([]string, error) {
@@ -80,16 +82,19 @@ func (port *windowsPort) Read(p []byte) (int, error) {
return 0, err return 0, err
} }
defer syscall.CloseHandle(ev.HEvent) defer syscall.CloseHandle(ev.HEvent)
cycles := int64(0)
for { for {
err := syscall.ReadFile(port.handle, p, &readed, ev) err := syscall.ReadFile(port.handle, p, &readed, ev)
if err == syscall.ERROR_IO_PENDING {
err = getOverlappedResult(port.handle, ev, &readed, true)
}
switch err { switch err {
case nil: case nil:
// operation completed successfully // operation completed successfully
case syscall.ERROR_IO_PENDING: case syscall.ERROR_OPERATION_ABORTED:
// wait for overlapped I/O to complete // port may have been closed
if err := getOverlappedResult(port.handle, ev, &readed, true); err != nil { return int(readed), &PortError{code: PortClosed, causedBy: err}
return int(readed), err
}
default: default:
// error happened // error happened
return int(readed), err return int(readed), err
@@ -102,6 +107,14 @@ func (port *windowsPort) Read(p []byte) (int, error) {
return 0, err return 0, err
} }
if port.readTimeoutCycles != -1 {
cycles++
if cycles == port.readTimeoutCycles {
// Timeout
return 0, nil
}
}
// At the moment it seems that the only reliable way to check if // At the moment it seems that the only reliable way to check if
// a serial port is alive in Windows is to check if the SetCommState // a serial port is alive in Windows is to check if the SetCommState
// function fails. // function fails.
@@ -369,6 +382,31 @@ func (port *windowsPort) GetModemStatusBits() (*ModemStatusBits, error) {
}, nil }, nil
} }
func (port *windowsPort) SetReadTimeout(timeout time.Duration) error {
var cycles, cycleDuration int64
if timeout == NoTimeout {
cycles = -1
cycleDuration = 1000
} else {
cycles = timeout.Milliseconds()/1000 + 1
cycleDuration = timeout.Milliseconds() / cycles
}
err := setCommTimeouts(port.handle, &commTimeouts{
ReadIntervalTimeout: 0xFFFFFFFF,
ReadTotalTimeoutMultiplier: 0xFFFFFFFF,
ReadTotalTimeoutConstant: uint32(cycleDuration),
WriteTotalTimeoutConstant: 0,
WriteTotalTimeoutMultiplier: 0,
})
if err != nil {
return &PortError{code: InvalidTimeoutValue, causedBy: err}
}
port.readTimeoutCycles = cycles
return nil
}
func createOverlappedEvent() (*syscall.Overlapped, error) { func createOverlappedEvent() (*syscall.Overlapped, error) {
h, err := createEvent(nil, true, false, nil) h, err := createEvent(nil, true, false, nil)
return &syscall.Overlapped{HEvent: h}, err return &syscall.Overlapped{HEvent: h}, err
@@ -434,18 +472,9 @@ func nativeOpen(portName string, mode *Mode) (*windowsPort, error) {
return nil, &PortError{code: InvalidSerialPort} return nil, &PortError{code: InvalidSerialPort}
} }
// Set timeouts to 1 second if port.SetReadTimeout(NoTimeout) != nil {
timeouts := &commTimeouts{
ReadIntervalTimeout: 0xFFFFFFFF,
ReadTotalTimeoutMultiplier: 0xFFFFFFFF,
ReadTotalTimeoutConstant: 1000, // 1 sec
WriteTotalTimeoutConstant: 0,
WriteTotalTimeoutMultiplier: 0,
}
if setCommTimeouts(port.handle, timeouts) != nil {
port.Close() port.Close()
return nil, &PortError{code: InvalidSerialPort} return nil, &PortError{code: InvalidSerialPort}
} }
return port, nil return port, nil
} }