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

Timeouts #33

Closed
wants to merge 14 commits into from
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ language: go
go:
- 1.7.x
- 1.8.x
- 1.9.x

go_import_path: go.bug.st/serial.v1

Expand Down
57 changes: 56 additions & 1 deletion serial.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,52 @@ type Port interface {
// SetMode sets all parameters of the serial port
SetMode(mode *Mode) error

// SetReadTimeout sets the whole packet read timeout.
// Values:
// t < 0: Blocking mode
// `Read` function wait until requested number of bytes are received (possible forever).
// t = 0: Non-blocking mode
// `Read` function returns immediately in any case, returning up to the requested number of bytes.
// t > 0: set timeout to `t` milliseconds.
// `Read` function returns immediately when the requested number of bytes are available,
// otherwise wait until the timeout expires and return all bytes that were received until them.
SetReadTimeout(t int) error

// SetReadTimeoutEx — Sets whole package read timeout similar to general purpose function SetReadTimeout(),
// and also sets interbyte timeout.
//
// Generally interbyte timeout is not needed, but in some special cases this function cat help you.
SetReadTimeoutEx(t, i uint32) error

// SetLegacyReadTimeout — Very special function.
//
// Based on https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa363190(v=vs.85).aspx:
// If there are any bytes in the input buffer, ReadFile returns immediately with the bytes in the buffer.
// If there are no bytes in the input buffer, ReadFile waits until a byte arrives and then returns immediately.
// If no bytes arrive within the time specified by ReadTotalTimeoutConstant, ReadFile times out.
//
// Use it to configure read timeout in legacy manner. (Legacy for this library).
SetFirstByteReadTimeout(t uint32) error

// SetWriteTimeout set whole packet write timeout
// Values:
// Values:
// t < 0: Blocking mode
// `Write` function will block until complete or error.
// Depending of OS layer it can call multiple subsequent os-level write calls until done.
// t = 0: Non-blocking mode
// `Write` function will write some data and returns even not all data has been written.
// Depending of OS layer it makes only signle subsequent os-level write call.
// t > 0: set timeout to `t` milliseconds.
// `Write` function will write untile complete, error or timeout.
// Depending of OS layer it can call multiple subsequent os-levek write calls until done.
SetWriteTimeout(t int) error

// Stores data received from the serial port into the provided byte array
// buffer. The function returns the number of bytes read.
//
// The Read function blocks until (at least) one byte is received from
// the serial port or an error occurs.
// the serial port or a timeout reached or an error occurs.
Read(p []byte) (n int, err error)

// Send the content of the data byte array to the serial port.
Expand Down Expand Up @@ -125,12 +166,20 @@ const (
InvalidParity
// InvalidStopBits the selected number of stop bits is not valid or not supported
InvalidStopBits
// Invalid timeout value passed
InvalidTimeoutValue
// ErrorEnumeratingPorts an error occurred while listing serial port
ErrorEnumeratingPorts
// PortClosed the port has been closed while the operation is in progress
PortClosed
// FunctionNotImplemented the requested function is not implemented
FunctionNotImplemented
// Operating system function error
OsError
// Port write failed
WriteFailed
// Port read failed
ReadFailed
)

// EncodedErrorString returns a string explaining the error code
Expand All @@ -152,12 +201,18 @@ 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:
return "Port has been closed"
case FunctionNotImplemented:
return "Function not implemented"
case OsError:
return "Operating system error"
case WriteFailed:
return "Write failed"
default:
return "Other error"
}
Expand Down
144 changes: 135 additions & 9 deletions serial_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"regexp"
"strings"
"sync"
"time"
"unsafe"

"golang.org/x/sys/unix"
Expand All @@ -23,6 +24,10 @@ import (
type unixPort struct {
handle int

firstByteTimeout bool
readTimeout int
writeTimeout int

closeLock sync.RWMutex
closeSignal *unixutils.Pipe
opened bool
Expand Down Expand Up @@ -52,26 +57,88 @@ func (port *unixPort) Close() error {
return nil
}

func (port *unixPort) Read(p []byte) (n int, err error) {
func (port *unixPort) Read(p []byte) (int, error) {
port.closeLock.RLock()
defer port.closeLock.RUnlock()
if !port.opened {
return 0, &PortError{code: PortClosed}
}

size, read := len(p), 0
fds := unixutils.NewFDSet(port.handle, port.closeSignal.ReadFD())
res, err := unixutils.Select(fds, nil, fds, -1)
if err != nil {
return 0, err
buf := make([]byte, size)

now := time.Now()
deadline := now.Add(time.Duration(port.readTimeout) * time.Millisecond)

for read < size {
res, err := unixutils.Select(fds, nil, fds, deadline.Sub(now))
if err != nil {
return read, err
}
if res.IsReadable(port.closeSignal.ReadFD()) {
return read, &PortError{code: PortClosed}
}
if !res.IsReadable(port.handle) {
break
}
n, err := unix.Read(port.handle, buf)
// read should always return some data as select reported it was ready to read when we get to this point.
if err == nil && n == 0 {
err = &PortError{code: ReadFailed}
}
if err != nil {
return read, err
}
copy(p[read:], buf[:n])
read += n

now = time.Now()
if !now.Before(deadline) || port.firstByteTimeout {
break
}
}
if res.IsReadable(port.closeSignal.ReadFD()) {
return read, nil
}

func (port *unixPort) Write(p []byte) (int, error) {
port.closeLock.RLock()
defer port.closeLock.RUnlock()
if !port.opened {
return 0, &PortError{code: PortClosed}
}
return unix.Read(port.handle, p)
}

func (port *unixPort) Write(p []byte) (n int, err error) {
return unix.Write(port.handle, p)
size, written := len(p), 0
fds := unixutils.NewFDSet(port.handle)
clFds := unixutils.NewFDSet(port.closeSignal.ReadFD())

deadline := time.Now().Add(time.Duration(port.writeTimeout) * time.Millisecond)

for written < size {
n, err := unix.Write(port.handle, p[written:])
if err != nil {
return written, err
}
if port.writeTimeout == 0 {
return n, nil
}
written += n
now := time.Now()
if port.writeTimeout > 0 && !now.Before(deadline) {
return written, nil
}
res, err := unixutils.Select(clFds, fds, fds, deadline.Sub(now))
if err != nil {
return written, err
}
if res.IsReadable(port.closeSignal.ReadFD()) {
return written, &PortError{code: PortClosed}
}
if !res.IsWritable(port.handle) {
return written, &PortError{code: WriteFailed}
}
}
return written, nil
}

func (port *unixPort) ResetInputBuffer() error {
Expand Down Expand Up @@ -128,6 +195,46 @@ func (port *unixPort) SetRTS(rts bool) error {
return port.setModemBitsStatus(status)
}

func (port *unixPort) SetReadTimeout(t int) error {
port.firstByteTimeout = false
port.readTimeout = t
return nil // timeout is done via select
}

func (port *unixPort) SetReadTimeoutEx(t, i uint32) error {
port.closeLock.RLock()
defer port.closeLock.RUnlock()
if !port.opened {
return &PortError{code: PortClosed}
}

port.firstByteTimeout = false
port.readTimeout = int(t)
settings, err := port.getTermSettings()
if err != nil {
return err
}
if err := setTermSettingsInterbyteTimeout(int(t), settings); err != nil {
return err
}
return port.setTermSettings(settings)
}

func (port *unixPort) SetFirstByteReadTimeout(t uint32) error {
if t > 0 && t < 0xFFFFFFFF {
port.firstByteTimeout = true
port.readTimeout = int(t)
return nil
} else {
return &PortError{code: InvalidTimeoutValue}
}
}

func (port *unixPort) SetWriteTimeout(t int) error {
port.writeTimeout = t
return nil // timeout is done via select
}

func (port *unixPort) GetModemStatusBits() (*ModemStatusBits, error) {
status, err := port.getModemBitsStatus()
if err != nil {
Expand Down Expand Up @@ -155,6 +262,10 @@ func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
port := &unixPort{
handle: h,
opened: true,

firstByteTimeout: true,
readTimeout: 1000, // Backward compatible default value
writeTimeout: 0,
}

// Setup serial port
Expand Down Expand Up @@ -368,6 +479,21 @@ func setRawMode(settings *unix.Termios) {
settings.Cc[unix.VTIME] = 0
}

func setTermSettingsInterbyteTimeout(timeout int, settings *unix.Termios) error {
vtime := timeout / 100 // VTIME tenths of a second elapses between bytes
if vtime > 255 || vtime*100 != timeout {
return &PortError{code: InvalidTimeoutValue}
}
if vtime > 0 {
settings.Cc[unix.VMIN] = 1
settings.Cc[unix.VTIME] = uint8(timeout)
} else {
settings.Cc[unix.VMIN] = 0
settings.Cc[unix.VTIME] = 0
}
return nil
}

// native syscall wrapper functions

func (port *unixPort) getTermSettings() (*unix.Termios, error) {
Expand Down
Loading