Files
go-serial/serial_unix.go
liulong f10848f47f
Some checks failed
test / native-os-build (macOS-latest) (push) Has been cancelled
test / native-os-build (ubuntu-latest) (push) Has been cancelled
test / native-os-build (windows-latest) (push) Has been cancelled
test / cross-os-build (freebsd amd64) (push) Has been cancelled
test / cross-os-build (linux ppc64le) (push) Has been cancelled
test / cross-os-build (openbsd 386) (push) Has been cancelled
test / cross-os-build (openbsd amd64) (push) Has been cancelled
test / cross-os-build (openbsd arm) (push) Has been cancelled
fix: 修复串口 read timeout 未生效的问题
- 将串口文件描述符设置为非阻塞模式,确保 read() 不会阻塞
- 修改 VMIN 从 1 改为 0,让 select() 正确控制读取超时
- 添加 Timeout 错误代码,超时时正确返回错误而不是 (0, nil)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:47:17 +08:00

555 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Copyright 2014-2024 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.
//
//go:build linux || darwin || freebsd || openbsd
package serial
import (
"fmt"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"go.bug.st/serial/unixutils"
"golang.org/x/sys/unix"
)
type unixPort struct {
handle int
readTimeout time.Duration
closeLock sync.RWMutex
closeSignal *unixutils.Pipe
opened uint32
}
func (port *unixPort) Close() error {
// 原子操作检查并设置端口关闭状态
if !atomic.CompareAndSwapUint32(&port.opened, 1, 0) {
return nil // 端口已经关闭,直接返回
}
var firstErr error
// 发送关闭信号以取消所有待处理的读操作
if port.closeSignal != nil {
if _, err := port.closeSignal.Write([]byte{0}); err != nil && firstErr == nil {
firstErr = err
}
}
// 检查句柄是否有效,然后执行关闭操作
if port.handle != -1 {
// 释放独占访问权 - 应在关闭handle之前完成
if err := port.releaseExclusiveAccess(); err != nil && firstErr == nil {
firstErr = err
}
// 关闭实际的端口句柄 - 这将解除任何挂起的读写操作
if err := unix.Close(port.handle); err != nil && firstErr == nil {
firstErr = err
}
port.handle = -1 // 标记句柄无效
}
// 关闭信号管道
if port.closeSignal != nil {
if err := port.closeSignal.Close(); err != nil && firstErr == nil {
firstErr = err
}
port.closeSignal = nil // 清除指针
}
return firstErr
}
func (port *unixPort) Read(p []byte) (int, error) {
// 首先检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
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 {
// 在每次select之前再次检查端口状态非阻塞检查
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
timeout := time.Duration(-1)
if port.readTimeout != NoTimeout {
timeout = time.Until(deadline)
if timeout < 0 {
// 负超时值在Select(...)中表示"无超时"
timeout = 0
}
}
res, err := unixutils.Select(fds, nil, fds, timeout)
if err == unix.EINTR {
continue // 系统调用被中断,重试
}
if err != nil {
// 如果在端口关闭后遇到错误返回PortClosed错误
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
return 0, &PortError{code: FunctionNotImplemented, causedBy: err}
}
if res.IsReadable(port.closeSignal.ReadFD()) {
// 收到关闭信号
return 0, &PortError{code: PortClosed}
}
if !res.IsReadable(port.handle) {
// 超时
if port.readTimeout == NoTimeout {
continue // 无超时设置,继续等待
}
// 超时应返回 Timeout 错误,而不是 (0, nil)
return 0, &PortError{code: Timeout, causedBy: unix.ETIMEDOUT}
}
n, err := unix.Read(port.handle, p)
if err == unix.EINTR {
continue // 系统调用被中断,重试
}
// Linux系统特性当读取操作期间端口断开连接时
// 端口会进入"可读但返回零长度数据"的状态。
// https://stackoverflow.com/a/34945814/1655275
if n == 0 && err == nil {
return 0, &PortError{code: PortClosed}
}
if n < 0 { // 确保不返回负数
n = 0
}
// 检查读取操作期间端口是否被关闭
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
if err != nil {
return n, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return n, nil
}
}
func (port *unixPort) Write(p []byte) (n int, err error) {
// 首先检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return 0, &PortError{code: PortClosed}
}
n, err = unix.Write(port.handle, p)
if n < 0 { // 确保不返回负数
n = 0
}
if err != nil {
return n, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return n, nil
}
func (port *unixPort) Break(t time.Duration) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
if err := unix.IoctlSetInt(port.handle, ioctlTiocsbrk, 0); err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
time.Sleep(t)
if err := unix.IoctlSetInt(port.handle, ioctlTioccbrk, 0); err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
return nil
}
func (port *unixPort) SetMode(mode *Mode) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
settings, err := port.getTermSettings()
if err != nil {
return &PortError{code: InvalidSerialPort, causedBy: err}
}
if err := setTermSettingsParity(mode.Parity, settings); err != nil {
return err
}
if err := setTermSettingsDataBits(mode.DataBits, settings); err != nil {
return err
}
if err := setTermSettingsStopBits(mode.StopBits, settings); err != nil {
return err
}
requireSpecialBaudrate := false
if err, special := setTermSettingsBaudrate(mode.BaudRate, settings); err != nil {
return err
} else if special {
requireSpecialBaudrate = true
}
if err := port.setTermSettings(settings); err != nil {
return &PortError{code: InvalidSerialPort, causedBy: err}
}
if requireSpecialBaudrate {
// MacOSX要求这是最后一个操作否则会产生'Invalid serial port'错误
if err := port.setSpecialBaudrate(uint32(mode.BaudRate)); err != nil {
return &PortError{code: InvalidSpeed, causedBy: err}
}
}
return nil
}
func (port *unixPort) SetDTR(dtr bool) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus()
if err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
if dtr {
status |= unix.TIOCM_DTR
} else {
status &^= unix.TIOCM_DTR
}
return port.setModemBitsStatus(status)
}
func (port *unixPort) SetRTS(rts bool) error {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus()
if err != nil {
return &PortError{code: FunctionNotImplemented, causedBy: err}
}
if rts {
status |= unix.TIOCM_RTS
} else {
status &^= unix.TIOCM_RTS
}
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) {
// 检查端口是否已经关闭
if atomic.LoadUint32(&port.opened) != 1 {
return nil, &PortError{code: PortClosed}
}
status, err := port.getModemBitsStatus()
if err != nil {
return nil, &PortError{code: FunctionNotImplemented, causedBy: err}
}
return &ModemStatusBits{
CTS: (status & unix.TIOCM_CTS) != 0,
DCD: (status & unix.TIOCM_CD) != 0,
DSR: (status & unix.TIOCM_DSR) != 0,
RI: (status & unix.TIOCM_RI) != 0,
}, nil
}
func nativeOpen(portName string, mode *Mode) (*unixPort, error) {
h, err := unix.Open(portName, unix.O_RDWR|unix.O_NOCTTY|unix.O_NDELAY, 0)
if err != nil {
switch err {
case unix.EBUSY:
return nil, &PortError{code: PortBusy}
case unix.EACCES:
return nil, &PortError{code: PermissionDenied}
}
return nil, err
}
port := &unixPort{
handle: h,
opened: 1,
readTimeout: NoTimeout,
}
// Setup serial port
settings, err := port.getTermSettings()
if err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting term settings: %w", err)}
}
// Set raw mode
setRawMode(settings)
// Explicitly disable RTS/CTS flow control
setTermSettingsCtsRts(false, settings)
if err = port.setTermSettings(settings); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting term settings: %w", err)}
}
if mode.InitialStatusBits != nil {
status, err := port.getModemBitsStatus()
if err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error getting modem bits status: %w", err)}
}
if mode.InitialStatusBits.DTR {
status |= unix.TIOCM_DTR
} else {
status &^= unix.TIOCM_DTR
}
if mode.InitialStatusBits.RTS {
status |= unix.TIOCM_RTS
} else {
status &^= unix.TIOCM_RTS
}
if err := port.setModemBitsStatus(status); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error setting modem bits status: %w", err)}
}
}
// MacOSX require that this operation is the last one otherwise an
// 'Invalid serial port' error is returned... don't know why...
if err := port.SetMode(mode); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error configuring port: %w", err)}
}
// Set non-blocking mode to ensure read() doesn't block after select() returns
// The select() call provides the timeout mechanism, not the read() call
unix.SetNonblock(h, true)
port.acquireExclusiveAccess()
// This pipe is used as a signal to cancel blocking Read
pipe := &unixutils.Pipe{}
if err := pipe.Open(); err != nil {
port.Close()
return nil, &PortError{code: InvalidSerialPort, causedBy: fmt.Errorf("error opening signaling pipe: %w", err)}
}
port.closeSignal = pipe
return port, nil
}
func nativeGetPortsList() ([]string, error) {
files, err := os.ReadDir(devFolder)
if err != nil {
return nil, err
}
ports := make([]string, 0, len(files))
for _, f := range files {
// Skip folders
if f.IsDir() {
continue
}
// Keep only devices with the correct name
if !osPortFilter.MatchString(f.Name()) {
continue
}
portName := devFolder + "/" + f.Name()
// Check if serial port is real or is a placeholder serial port "ttySxx" or "ttyHSxx"
if strings.HasPrefix(f.Name(), "ttyS") || strings.HasPrefix(f.Name(), "ttyHS") {
port, err := nativeOpen(portName, &Mode{})
if err != nil {
continue
} else {
port.Close()
}
}
// Save serial port in the resulting list
ports = append(ports, portName)
}
return ports, nil
}
// termios manipulation functions
func setTermSettingsParity(parity Parity, settings *unix.Termios) error {
switch parity {
case NoParity:
settings.Cflag &^= unix.PARENB
settings.Cflag &^= unix.PARODD
settings.Cflag &^= tcCMSPAR
settings.Iflag &^= unix.INPCK
case OddParity:
settings.Cflag |= unix.PARENB
settings.Cflag |= unix.PARODD
settings.Cflag &^= tcCMSPAR
settings.Iflag |= unix.INPCK
case EvenParity:
settings.Cflag |= unix.PARENB
settings.Cflag &^= unix.PARODD
settings.Cflag &^= tcCMSPAR
settings.Iflag |= unix.INPCK
case MarkParity:
if tcCMSPAR == 0 {
return &PortError{code: InvalidParity}
}
settings.Cflag |= unix.PARENB
settings.Cflag |= unix.PARODD
settings.Cflag |= tcCMSPAR
settings.Iflag |= unix.INPCK
case SpaceParity:
if tcCMSPAR == 0 {
return &PortError{code: InvalidParity}
}
settings.Cflag |= unix.PARENB
settings.Cflag &^= unix.PARODD
settings.Cflag |= tcCMSPAR
settings.Iflag |= unix.INPCK
default:
return &PortError{code: InvalidParity}
}
return nil
}
func setTermSettingsDataBits(bits int, settings *unix.Termios) error {
databits, ok := databitsMap[bits]
if !ok {
return &PortError{code: InvalidDataBits}
}
// Remove previous databits setting
settings.Cflag &^= unix.CSIZE
// Set requested databits
settings.Cflag |= databits
return nil
}
func setTermSettingsStopBits(bits StopBits, settings *unix.Termios) error {
switch bits {
case OneStopBit:
settings.Cflag &^= unix.CSTOPB
case OnePointFiveStopBits:
return &PortError{code: InvalidStopBits}
case TwoStopBits:
settings.Cflag |= unix.CSTOPB
default:
return &PortError{code: InvalidStopBits}
}
return nil
}
func setTermSettingsCtsRts(enable bool, settings *unix.Termios) {
if enable {
settings.Cflag |= tcCRTSCTS
} else {
settings.Cflag &^= tcCRTSCTS
}
}
func setRawMode(settings *unix.Termios) {
// Set local mode
settings.Cflag |= unix.CREAD
settings.Cflag |= unix.CLOCAL
// Set raw mode
settings.Lflag &^= unix.ICANON
settings.Lflag &^= unix.ECHO
settings.Lflag &^= unix.ECHOE
settings.Lflag &^= unix.ECHOK
settings.Lflag &^= unix.ECHONL
settings.Lflag &^= unix.ECHOCTL
settings.Lflag &^= unix.ECHOPRT
settings.Lflag &^= unix.ECHOKE
settings.Lflag &^= unix.ISIG
settings.Lflag &^= unix.IEXTEN
settings.Iflag &^= unix.IXON
settings.Iflag &^= unix.IXOFF
settings.Iflag &^= unix.IXANY
settings.Iflag &^= unix.INPCK
settings.Iflag &^= unix.IGNPAR
settings.Iflag &^= unix.PARMRK
settings.Iflag &^= unix.ISTRIP
settings.Iflag &^= unix.IGNBRK
settings.Iflag &^= unix.BRKINT
settings.Iflag &^= unix.INLCR
settings.Iflag &^= unix.IGNCR
settings.Iflag &^= unix.ICRNL
settings.Iflag &^= tcIUCLC
settings.Oflag &^= unix.OPOST
// Block reads until at least one char is available (no timeout)
// VMIN=1: Block until at least 1 byte is available
// VTIME=0: No inter-character timeout (not used with VMIN=1)
//
// NOTE: When readTimeout is set, the select() system call provides the timeout
// and VMIN/VTIME settings are not used for timeout purposes.
// However, VMIN=1 can cause issues with select() on some platforms/
// terminal drivers. Using VMIN=0 allows select() to work reliably.
settings.Cc[unix.VMIN] = 0
settings.Cc[unix.VTIME] = 0
}
// native syscall wrapper functions
func (port *unixPort) getTermSettings() (*unix.Termios, error) {
return unix.IoctlGetTermios(port.handle, ioctlTcgetattr)
}
func (port *unixPort) setTermSettings(settings *unix.Termios) error {
return unix.IoctlSetTermios(port.handle, ioctlTcsetattr, settings)
}
func (port *unixPort) getModemBitsStatus() (int, error) {
return unix.IoctlGetInt(port.handle, unix.TIOCMGET)
}
func (port *unixPort) setModemBitsStatus(status int) error {
return unix.IoctlSetPointerInt(port.handle, unix.TIOCMSET, status)
}
func (port *unixPort) acquireExclusiveAccess() error {
return unix.IoctlSetInt(port.handle, unix.TIOCEXCL, 0)
}
func (port *unixPort) releaseExclusiveAccess() error {
return unix.IoctlSetInt(port.handle, unix.TIOCNXCL, 0)
}