【python】pyserial 在windows 下卡住的bug

发布于:2025-07-09 ⋅ 阅读:(31) ⋅ 点赞:(0)

使用当前pyserial 在Windwos 下做的项目,移交到客户案场后, 客户发现程序会卡住,经过最终调查,是卡在了pyserial 的write 方法中了。

具体现象

程序卡在write 方法中,长时间无法返回,即使设置了write_timeout 也无法返回。

分析

下来我们将Send 方法的代码切出来:

   def write(self, data):
        """Output the given byte string over the serial port."""
        if not self.is_open:
            raise PortNotOpenError()
        #~ if not isinstance(data, (bytes, bytearray)):
            #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
        # convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
        data = to_bytes(data)
        if data:
            #~ win32event.ResetEvent(self._overlapped_write.hEvent)
            n = win32.DWORD()
            success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
            if self._write_timeout != 0:  # if blocking (None) or w/ write timeout (>0)
                if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
                    raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))

                # Wait for the write to complete.
                #~ win32.WaitForSingleObject(self._overlapped_write.hEvent, win32.INFINITE)
                win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), True)
                if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
                    return n.value  # canceled IO is no error
                if n.value != len(data):
                    raise SerialTimeoutException('Write timeout')
                return n.value
            else:
                errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
                if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
                                 win32.ERROR_OPERATION_ABORTED):
                    return 0
                elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
                    # no info on true length provided by OS function in async mode
                    return len(data)
                else:
                    raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
        else:
            return 0

这里有一个 Windows 串口编程的“坑”:即使你设置了 write_timeout,某些情况下,WriteFile + GetOverlappedResult() 仍可能卡住不返回。下面我从操作系统底层机制解释这个行为。
在 PySerial 的 Windows 实现中,串口写操作等价于如下 WinAPI 调用流程:

OVERLAPPED ov = {...};  // 重叠结构体(异步IO用)
DWORD written = 0;
BOOL success = WriteFile(hSerial, buffer, len, &written, &ov);

if (!success && GetLastError() == ERROR_IO_PENDING) {
    // 等待异步写完成
    GetOverlappedResult(hSerial, &ov, &written, TRUE);  // TRUE = 阻塞等待
}

问题发生点:GetOverlappedResult(…, TRUE)
这个调用是阻塞等待写完成。理论上,当写操作超过 WriteTotalTimeoutConstant + len * WriteTotalTimeoutMultiplier 时,系统应返回 SerialTimeoutException,但实际并不总是这样。

原因 解释
✅ 驱动未正确处理超时 某些串口驱动(尤其是 USB 转串口)忽略了 SetCommTimeouts 中的写超时设置。
✅ 缓冲区已满 写缓冲区写不进去,但驱动仍让 WriteFile 挂起等待,超时机制失效。
✅ GetOverlappedResult 不检查超时 它本身不带超时参数,除非你用 WaitForSingleObject 另设超时,pyserial 没这么做。
✅ 虚拟串口/硬件死锁 某些虚拟串口设备(如蓝牙串口、USB转串口)会因设备掉线导致永远不返回。

举个例子:

ser.write_timeout = 2.0
ser.write(b'X' * 102400)  # 写一个很大的包

如果目标串口设备已断开/没响应:

  • WriteFile() 会返回 ERROR_IO_PENDING
  • 然后 GetOverlappedResult(…, TRUE) 就会 永久阻塞
  • 此时 write_timeout 设置毫无作用

如果串口写缓冲区已满,驱动并不会立即失败, 而是等待硬件清空缓冲区(例如对端设备接受), 如果硬件永远不读(比如死机了),那它就永远挂住了。
WriteFile() 是异步的,但 GetOverlappedResult(…, TRUE) 是阻塞的。它等的是 overlapped 事件完成,但串口驱动只有在写成功/失败时才触发事件。

当使用 OVERLAPPED 结构进行串口异步写时,WriteFile() 返回后,并不会立刻知道是否成功,而是通过一个事件句柄 hEvent 在未来某个时间点通知“写操作完成”。

所以我们给出最终解决方案如下:

def write(self, data):
        """Output the given byte string over the serial port."""
        if not self.is_open:
            raise PortNotOpenError()
        #~ if not isinstance(data, (bytes, bytearray)):
            #~ raise TypeError('expected %s or bytearray, got %s' % (bytes, type(data)))
        # convert data (needed in case of memoryview instance: Py 3.1 io lib), ctypes doesn't like memoryview
        data = to_bytes(data)
        if data:
            #~ win32event.ResetEvent(self._overlapped_write.hEvent)
            n = win32.DWORD()
            success = win32.WriteFile(self._port_handle, data, len(data), ctypes.byref(n), self._overlapped_write)
            if self._write_timeout != 0:  # if blocking (None) or w/ write timeout (>0)
                if not success and win32.GetLastError() not in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
                    raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))

                # Wait for the write to complete.
				WAIT_TIMEOUT = 0x00000102
				if self._write_timeout is None:
    				timeout_ms = win32.INFINITE
				else:
    				timeout_ms = int(self._write_timeout * 1000)

				rc = win32.WaitForSingleObject(self._overlapped_write.hEvent, timeout_ms)
				if rc == WAIT_TIMEOUT:
    				self.cancel_write()
    				raise SerialTimeoutException('Write timeout due to device blocking.')

				win32.GetOverlappedResult(self._port_handle, self._overlapped_write, ctypes.byref(n), False)
				if win32.GetLastError() == win32.ERROR_OPERATION_ABORTED:
    				return n.value  # canceled IO is no error

				if n.value != len(data):
    				raise SerialTimeoutException('Write timeout')

				return n.value
		else:
                errorcode = win32.ERROR_SUCCESS if success else win32.GetLastError()
                if errorcode in (win32.ERROR_INVALID_USER_BUFFER, win32.ERROR_NOT_ENOUGH_MEMORY,
                                 win32.ERROR_OPERATION_ABORTED):
                    return 0
                elif errorcode in (win32.ERROR_SUCCESS, win32.ERROR_IO_PENDING):
                    # no info on true length provided by OS function in async mode
                    return len(data)
                else:
                    raise SerialException("WriteFile failed ({!r})".format(ctypes.WinError()))
        else:
            return 0

使用WaitForSingleObject:

  • 等待 overlapped 写事件完成
  • 最多等待 timeout_in_ms 毫秒
  • 返回值 rc 用于判断是成功、超时,还是其他错误
  • 如果超时,调用cancel_write() 掉取消一个挂起的 I/O 操作(如 ReadFile、WriteFile 等)(特别是在使用 重叠(Overlapped)I/O 时非常有用。)

如此,就完美的解决掉pyserial 在windows 下卡住的现象了。


网站公告

今日签到

点亮在社区的每一天
去签到