Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of read timeouts #109

Merged
merged 4 commits into from
Jun 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions serial.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

package serial

import "time"

//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
Expand Down Expand Up @@ -40,10 +42,17 @@ type Port interface {
// modem status bits for the serial port (CTS, DSR, etc...)
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() 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...).
// It can be retrieved with the Port.GetModemStatusBits() method.
type ModemStatusBits struct {
Expand Down Expand Up @@ -125,6 +134,8 @@ const (
InvalidParity
// InvalidStopBits the selected number of stop bits is not valid or not supported
InvalidStopBits
// InvalidTimeoutValue the timeout value is not valid or not supported
InvalidTimeoutValue
// ErrorEnumeratingPorts an error occurred while listing serial port
ErrorEnumeratingPorts
// PortClosed the port has been closed while the operation is in progress
Expand Down Expand Up @@ -152,6 +163,8 @@ func (e PortError) EncodedErrorString() string {
return "Port parity invalid or not supported"
case InvalidStopBits:
return "Port stop bits invalid or not supported"
case InvalidTimeoutValue:
return "Timeout value invalid or not supported"
case ErrorEnumeratingPorts:
return "Could not enumerate serial ports"
case PortClosed:
Expand Down
36 changes: 33 additions & 3 deletions serial_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"sync"
"sync/atomic"
"time"

"go.bug.st/serial/unixutils"
"golang.org/x/sys/unix"
Expand All @@ -22,6 +23,7 @@ import (
type unixPort struct {
handle int

readTimeout time.Duration
closeLock sync.RWMutex
closeSignal *unixutils.Pipe
opened uint32
Expand Down Expand Up @@ -61,9 +63,18 @@ func (port *unixPort) Read(p []byte) (int, error) {
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())
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 {
continue
}
Expand All @@ -73,10 +84,20 @@ func (port *unixPort) Read(p []byte) (int, error) {
if res.IsReadable(port.closeSignal.ReadFD()) {
return 0, &PortError{code: PortClosed}
}
if !res.IsReadable(port.handle) {
// Timeout happened
return 0, nil
}
n, err := unix.Read(port.handle, p)
if err == unix.EINTR {
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
n = 0
}
Expand Down Expand Up @@ -146,6 +167,14 @@ func (port *unixPort) SetRTS(rts bool) error {
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) {
status, err := port.getModemBitsStatus()
if err != nil {
Expand All @@ -171,8 +200,9 @@ func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
return nil, err
}
port := &unixPort{
handle: h,
opened: 1,
handle: h,
opened: 1,
readTimeout: NoTimeout,
}

// Setup serial port
Expand Down
65 changes: 47 additions & 18 deletions serial_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ package serial
*/

import (
"syscall"
"sync"
"syscall"
"time"
)

type windowsPort struct {
mu sync.Mutex
handle syscall.Handle
mu sync.Mutex
handle syscall.Handle
readTimeoutCycles int64
}

func nativeGetPortsList() ([]string, error) {
Expand Down Expand Up @@ -80,16 +82,19 @@ func (port *windowsPort) Read(p []byte) (int, error) {
return 0, err
}
defer syscall.CloseHandle(ev.HEvent)

cycles := int64(0)
for {
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_IO_PENDING:
// wait for overlapped I/O to complete
if err := getOverlappedResult(port.handle, ev, &readed, true); err != nil {
return int(readed), err
}
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
Expand All @@ -102,6 +107,14 @@ func (port *windowsPort) Read(p []byte) (int, error) {
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
// a serial port is alive in Windows is to check if the SetCommState
// function fails.
Expand Down Expand Up @@ -369,6 +382,31 @@ func (port *windowsPort) GetModemStatusBits() (*ModemStatusBits, error) {
}, 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) {
h, err := createEvent(nil, true, false, nil)
return &syscall.Overlapped{HEvent: h}, err
Expand Down Expand Up @@ -434,18 +472,9 @@ func nativeOpen(portName string, mode *Mode) (*windowsPort, error) {
return nil, &PortError{code: InvalidSerialPort}
}

// Set timeouts to 1 second
timeouts := &commTimeouts{
ReadIntervalTimeout: 0xFFFFFFFF,
ReadTotalTimeoutMultiplier: 0xFFFFFFFF,
ReadTotalTimeoutConstant: 1000, // 1 sec
WriteTotalTimeoutConstant: 0,
WriteTotalTimeoutMultiplier: 0,
}
if setCommTimeouts(port.handle, timeouts) != nil {
if port.SetReadTimeout(NoTimeout) != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort}
}

return port, nil
}