diff --git a/serial.go b/serial.go index d50e1f7..08f227a 100644 --- a/serial.go +++ b/serial.go @@ -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 @@ -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 { @@ -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 @@ -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: diff --git a/serial_unix.go b/serial_unix.go index f9e887c..0143aa2 100644 --- a/serial_unix.go +++ b/serial_unix.go @@ -14,6 +14,7 @@ import ( "strings" "sync" "sync/atomic" + "time" "go.bug.st/serial/unixutils" "golang.org/x/sys/unix" @@ -22,6 +23,7 @@ import ( type unixPort struct { handle int + readTimeout time.Duration closeLock sync.RWMutex closeSignal *unixutils.Pipe opened uint32 @@ -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 } @@ -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 } @@ -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 { @@ -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 diff --git a/serial_windows.go b/serial_windows.go index ec328f9..6b6d0ee 100644 --- a/serial_windows.go +++ b/serial_windows.go @@ -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) { @@ -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 @@ -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. @@ -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 @@ -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 }