You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Jupyter部件重运行时Libusb出现Windows访问拒绝错误(Error 5)的问题求助

Jupyter部件重运行时Libusb出现Windows访问拒绝错误(Error 5)的问题求助

大家好,我现在遇到一个头疼的USB IO问题,想请教下懂USB设备或者Jupyter部件开发的朋友:

问题背景

我正在Jupyter Notebook里开发一个部件,用来读取一款旧旋转运动传感器的USB数据。为了满足20Hz的传感器数据刷新率,我试过matplotlib ipywidgets、Dash-plotly和Bokeh——其中只有Bokeh的性能能跟上需求,但现在遇到了一个棘手的问题:每次重运行部件所在的单元格时,对USB设备的配置请求都会抛出Windows访问拒绝错误(Error 5),只有重启Jupyter内核才能解决

直接运行连接和读取逻辑(不封装在部件里)时完全正常,Dash-plotly的实现也不会出现这个错误,但它性能不够,没法处理20Hz的数据流。

设备连接逻辑

这款传感器的连接流程比较特殊:

  1. 刚插上电脑时会识别为引导加载器设备
  2. 发送配置指令后,设备会自动重启进入应用状态(设备管理器里能看到状态变化,只要不拔掉设备就会保持这个状态)
  3. 找到重启后的应用态设备,配置为默认的配置1

我的核心连接代码如下:

import time
import logging
import usb.core as libusb_package
import usb.util

# 设备VID/PID(示例占位)
ROTATION_BOOT_VID = 0xXXXX
ROTATION_BOOT_PID = 0xXXXX
ROTATION_APP_VID = 0xYYYY
ROTATION_APP_PID = 0xYYYY

class RotationSensor:
    def __init__(self):
        self.__dev: libusb_package.Device | None = None

    def connect(self, timeout_s=10):
        t0 = time.time()
        # 第一步:连接引导加载器设备并触发重启
        while time.time() - t0 < timeout_s:
            try:
                self.__dev = libusb_package.find(
                    idVendor=ROTATION_BOOT_VID,
                    idProduct=ROTATION_BOOT_PID
                )
                if self.__dev:
                    logging.info("找到旋转传感器引导加载器设备")
                    self.__initialize_configuration()
                    logging.info("已配置引导加载器设备")
                    self.__write_firmware()  # 发送指令触发设备重启
                    self.__wait_for_disconnect()
                    self.__dev = None  # 清空引导加载器设备引用
                    break
            except usb.core.USBError as e:
                time.sleep(0.05)

        # 第二步:连接重启后的应用态设备
        t0 = time.time()
        while time.time() - t0 < timeout_s:
            try:
                self.__dev = libusb_package.find(
                    idVendor=ROTATION_APP_VID,
                    idProduct=ROTATION_APP_PID
                )
                if self.__dev:
                    logging.info("找到旋转传感器应用态设备")
                    active_config = self.__dev.get_active_configuration()
                    logging.info(f"当前激活配置:{active_config}")
                    self.__initialize_configuration()
                    logging.info("已连接到应用态设备")
                    return
            except (usb.core.USBError, NotImplementedError) as e:
                if isinstance(e, usb.core.USBError) and e.errno == 13:
                    logging.warning("此处触发访问拒绝错误")
                    return
                self.__handle_usb_error(e)
                time.sleep(0.05)
        
        raise ValueError("超时未找到可用的旋转传感器设备")

    def __del__(self):
        """析构时确保释放USB资源"""
        if self.__dev is not None:
            logging.info("正在清理传感器设备资源...")
            usb.util.dispose_resources(self.__dev)
            self.__dev = None

    # 以下为省略的辅助方法
    def __initialize_configuration(self):
        # 设备配置逻辑(略)
        pass
    def __write_firmware(self):
        # 发送重启指令逻辑(略)
        pass
    def __wait_for_disconnect(self):
        # 等待设备重启断开逻辑(略)
        pass
    def __handle_usb_error(self, e):
        # USB错误处理逻辑(略)
        pass

Bokeh部件实现(出问题的版本)

为了实现实时绘图,我把传感器类封装到了Bokeh的周期性回调里,核心代码如下:

from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, Button
from bokeh.layouts import column, row
from bokeh.io import show
import time

def gyroscope_app(doc):
    source = ColumnDataSource(data=dict(time=[], radial_velocity=[]))
    rotation_sensor = RotationSensor()
    rotation_sensor.connect()

    # 初始化绘图
    plot = figure(
        x_axis_label='时间 (s)',
        y_axis_label='径向速度 (arcsec / s)',
        min_width=800,
        min_height=400
    )
    plot.line('time', 'radial_velocity', source=source)

    callback_id = {'value': None}

    # 周期性读取传感器数据并更新绘图
    def update(t):
        reading = rotation_sensor.read()
        if reading is None or len(reading) != 4:
            print("收到无效读数,跳过本次更新")
            return
        reading_data = reading[0] if reading[1] > 0 else -reading[0]
        new_data = dict(time=[t], radial_velocity=[reading_data])
        source.stream(new_data, rollover=30)

    # 控制按钮逻辑
    def start():
        if callback_id['value'] is None:
            rotation_sensor.start_sensor_reading()
            callback_id['value'] = doc.add_periodic_callback(update, 100)

    def stop():
        cid = callback_id['value']
        if cid is not None:
            doc.remove_periodic_callback(cid)
            callback_id['value'] = None
            rotation_sensor.stop_sensor_reading()

    def reset():
        stop()
        source.data = dict(time=[], radial_velocity=[])

    # 组装部件布局
    start_btn = Button(label="开始读取", button_type="success")
    start_btn.on_click(start)
    stop_btn = Button(label="停止读取", button_type="danger")
    stop_btn.on_click(stop)
    reset_btn = Button(label="重置数据", button_type="warning")
    reset_btn.on_click(reset)

    doc.add_root(
        column(
            row(start_btn, stop_btn, reset_btn),
            plot
        )
    )

show(gyroscope_app)

我已经尝试过的方法

  1. 在传感器类的错误处理和__del__析构方法里调用usb.util.dispose_resources,尝试强制释放设备资源
  2. 调试时确认:重运行单元格后,Python侧已经没有残留的设备对象
  3. 检查设备状态:设备重启后的应用态在设备管理器里显示正常,不会自动变回引导加载器

具体疑问

  1. 为什么重运行Jupyter部件单元格会导致USB设备被占用,直接运行代码却不会?
  2. 有没有可能是Bokeh的周期性回调或者Jupyter的部件机制,导致USB资源没有被正确释放?
  3. 除了usb.util.dispose_resources,还有什么方法能确保USB设备资源被完全释放,让我可以重运行代码而不用重启内核?
  4. Windows下有没有办法检查USB设备的内核驱动状态,或者强制释放设备占用?

麻烦各位大佬帮忙看看,感激不尽!

内容来源于stack exchange

火山引擎 最新活动