【python】pyserial 在windows 下卡住的bug

使用当前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 下卡住的现象了。

1. 已下载fastbot-android开源组件所需要依赖的jar包和libs包(jar文件夹下有3个jar文件,libs文件夹下有4个文件夹每个文件夹下有一个so文件)。 2. Push文件,.jar文件到/sdcard目录下,libs文件夹到 /data/local/tmp/下。通过adb push *.jar /sdcard和adb push libs/. /data/local/tmp/命令实现(注意这里我要求的是直接推送libs文件夹而不是其下的文件)。 3. 通过adb命令来启动fastbot测试: adb -s 你的设备号 shell CLASSPATH=/sdcard/monkeyq.jar:/sdcard/framework.jar:/sdcard/fastbot-thirdpart.jar exec app_process /system/bin com.android.commands.monkey.Monkey -p 包名(测试包名) –agent reuseq –running-minutes 遍历时长 –throttle 事件频率(500-800) -v -v --bugreport --output-directory /sdcard/test/log/crash 4. 停止测试的指令:adb shell pkill -f "monkey”/ adb shell ps | grep monkey。 5. fastbot具体的参数意义如下: -s 设备号 多个设备需要指定设备号,单独设备无需此-s参数 -p 包名 遍历app的包名,-p+包名 –agent reuseq 遍历模式,无需更改 –running-minutes 遍历时长(分钟) # 遍历时间:–running-minutes 时间 –throttle 事件频率 遍历事件频率,建议为500-800 可选参数 –bugreport 崩溃时保存bug report log –output-directory /sdcard/xxx log/crash 另存目录 6. 启动测试后,fastbot将会在终端启动,app将会进行自动遍历activity的操作。 7. 结果获取: (1) Crash、ANR 捕获会存放在/sdcard/crash-dump.log文件 (2) 捕获的Anr 同时也会写入 /sdcard/oom-traces.log 文件 (3) 如果你在命令里指定了。–output-directory /sdcard/xxx 路径,那么结果运行完之后来对应的路径获取即可。 (4) 正常跑完Fastbot会在当前shell中打印totalActivity(总activity列表),ExploredActivity(遍历到的activity列表)以及本次遍历的总覆盖率. 我希望根据fastbot_android这个开源组件以及以上测试步骤方法,开发一个可视化的测试工具,我准备用PyCharm工具python语言进行开发。希望能实现以下功能: 1. UI页面显示获取当前连接设备按钮,可以获取到设备,并且可以点击选择测试设备,可单选/可多选,可取消选择,选择后设备显示 “√”的UI效果。 2. UI页面显示配置测试包名称、测试时间、测试时间间隔参数设置的文本框。 3. UI页面显示开始测试按钮-点击并且开始执行测试后,按钮变为执行中,执行中状态时可选择停止测试,测试停止后按钮重新变为“开始测试”。 4. UI界面显示一个cmd信息窗口打印区域,显示终端打印的测试内容及推送的adb测试命令详细信息。 5. 点击开始测试时自动将依赖文件按照要求push到设备中的对应路径,push完成才开始推送测试命令。 6. 测试完成,获取存储的闪退数据等日志信息、根据测试覆盖率、测试时长、测试机型等进行总结,出具txt类型的测试报告。并且我希望能对获取到的存储的闪退问题等进行初步分析,分析属于什么类型问题闪退/无响应等,并且在测试报告中体现。 7. 将该工具进行打包,要求在无python环境的电脑上也能运行。 ————根据以上内容要求,分析可视化工具开发实现的全部流程步骤,将环境配置到最终实现的所有步骤都列举出来,包括完整详细的的代码设计(假设我是一个没有开发经验的测试人员,所以必须保证你的步骤足够详细,并且代码必须是完整和全面的、不需要自己再行修改和添加)。
04-03
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值