diff --git a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py index f45b0dc33..f215701da 100644 --- a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py +++ b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py @@ -751,67 +751,73 @@ def open(self): self._interface.open() - # If this probe has already been opened and examined previously, we don't need to examine it again. - if self._has_opened_once: - self._init_deferred_buffers() - if self._has_swo_uart: - self._swo_disable() - self._swo_status = SWOStatus.DISABLED - self._is_open = True - return + # Ensure the interface is closed if any subsequent setup step fails, so that + # re-open attempts don't hit "already open" from the HID layer. + try: + # If this probe has already been opened and examined previously, we don't need to examine it again. + if self._has_opened_once: + self._init_deferred_buffers() + if self._has_swo_uart: + self._swo_disable() + self._swo_status = SWOStatus.DISABLED + self._is_open = True + return - if session.Session.get_current().options['cmsis_dap.limit_packets'] or DAPSettings.limit_packets: - self._packet_count = 1 - LOG.debug("Limiting packet count to %d", self._packet_count) - else: - self._packet_count = self.identify(self.ID.MAX_PACKET_COUNT) - assert isinstance(self._packet_count, int) - - # Get the protocol version. - self._read_protocol_version() - - # Read the firmware version if the protocol supports it. - # THe PRODUCT_FW_VERSION ID was added in versions 1.3.0 (HID) and 2.1.0 (bulk). - if (self._cmsis_dap_version >= CMSISDAPVersion.V2_1_0) or (self._cmsis_dap_version >= CMSISDAPVersion.V1_3_0 - and self._cmsis_dap_version < CMSISDAPVersion.V2_0_0): - fw_version_value = self.identify(self.ID.PRODUCT_FW_VERSION) - assert isinstance(fw_version_value, (str, NoneType)) - self._fw_version = fw_version_value - - # Major protocol version based on use of bulk endpoints. - proto_major = (2 if self._interface.is_bulk else 1) - - # Log probe's firmware version. - if self._fw_version: - LOG.debug("CMSIS-DAP v%d probe %s: firmware version %s, protocol version %i.%i.%i", - proto_major, self._unique_id, self._fw_version, *self._cmsis_dap_version) - else: - LOG.debug("CMSIS-DAP v%d probe %s: protocol version %i.%i.%i", - proto_major, self._unique_id, *self._cmsis_dap_version) - - self._interface.set_packet_count(self._packet_count) - self._packet_size = self.identify(self.ID.MAX_PACKET_SIZE) - assert isinstance(self._packet_size, int) - self._interface.set_packet_size(self._packet_size) - self._capabilities = self.identify(self.ID.CAPABILITIES) - assert isinstance(self._capabilities, int) - self._has_swo_uart = (self._capabilities & Capabilities.SWO_UART) != 0 - if self._has_swo_uart: - swo_buffer_size_value = self.identify(self.ID.SWO_BUFFER_SIZE) - if isinstance(swo_buffer_size_value, int) and swo_buffer_size_value > 0: - self._swo_buffer_size = swo_buffer_size_value + if session.Session.get_current().options['cmsis_dap.limit_packets'] or DAPSettings.limit_packets: + self._packet_count = 1 + LOG.debug("Limiting packet count to %d", self._packet_count) else: - LOG.debug("CMSIS-DAP probe %s reported invalid SWO_BUFFER_SIZE (%d)", - self._unique_id, swo_buffer_size_value) - self._has_swo_uart = False - else: - self._swo_buffer_size = 0 - self._swo_status = SWOStatus.DISABLED + self._packet_count = self.identify(self.ID.MAX_PACKET_COUNT) + assert isinstance(self._packet_count, int) + + # Get the protocol version. + self._read_protocol_version() + + # Read the firmware version if the protocol supports it. + # THe PRODUCT_FW_VERSION ID was added in versions 1.3.0 (HID) and 2.1.0 (bulk). + if (self._cmsis_dap_version >= CMSISDAPVersion.V2_1_0) or (self._cmsis_dap_version >= CMSISDAPVersion.V1_3_0 + and self._cmsis_dap_version < CMSISDAPVersion.V2_0_0): + fw_version_value = self.identify(self.ID.PRODUCT_FW_VERSION) + assert isinstance(fw_version_value, (str, NoneType)) + self._fw_version = fw_version_value + + # Major protocol version based on use of bulk endpoints. + proto_major = (2 if self._interface.is_bulk else 1) + + # Log probe's firmware version. + if self._fw_version: + LOG.debug("CMSIS-DAP v%d probe %s: firmware version %s, protocol version %i.%i.%i", + proto_major, self._unique_id, self._fw_version, *self._cmsis_dap_version) + else: + LOG.debug("CMSIS-DAP v%d probe %s: protocol version %i.%i.%i", + proto_major, self._unique_id, *self._cmsis_dap_version) + + self._interface.set_packet_count(self._packet_count) + self._packet_size = self.identify(self.ID.MAX_PACKET_SIZE) + assert isinstance(self._packet_size, int) + self._interface.set_packet_size(self._packet_size) + self._capabilities = self.identify(self.ID.CAPABILITIES) + assert isinstance(self._capabilities, int) + self._has_swo_uart = (self._capabilities & Capabilities.SWO_UART) != 0 + if self._has_swo_uart: + swo_buffer_size_value = self.identify(self.ID.SWO_BUFFER_SIZE) + if isinstance(swo_buffer_size_value, int) and swo_buffer_size_value > 0: + self._swo_buffer_size = swo_buffer_size_value + else: + LOG.debug("CMSIS-DAP probe %s reported invalid SWO_BUFFER_SIZE (%d)", + self._unique_id, swo_buffer_size_value) + self._has_swo_uart = False + else: + self._swo_buffer_size = 0 + self._swo_status = SWOStatus.DISABLED - self._init_deferred_buffers() + self._init_deferred_buffers() - self._has_opened_once = True - self._is_open = True + self._has_opened_once = True + self._is_open = True + except Exception: + self._interface.close() + raise @locked def close(self): diff --git a/pyocd/probe/pydapaccess/interface/__init__.py b/pyocd/probe/pydapaccess/interface/__init__.py index 3b04ed7b5..386349993 100644 --- a/pyocd/probe/pydapaccess/interface/__init__.py +++ b/pyocd/probe/pydapaccess/interface/__init__.py @@ -55,9 +55,10 @@ # Default to hidapi for OS X. elif system == "Darwin": USB_BACKEND = "hidapiusb" - # Default to pyUSB for Linux. + # Prefer hidapi on Linux — pyusb sends control transfers that crash HID-only probes + # (e.g. pico2-debug) which have no string descriptors / langid support. elif system == "Linux": - USB_BACKEND = "pyusb" + USB_BACKEND = "hidapiusb" if HidApiUSB.isAvailable else "pyusb" elif "BSD" in system: USB_BACKEND = "pyusb" else: diff --git a/pyocd/probe/pydapaccess/interface/common.py b/pyocd/probe/pydapaccess/interface/common.py index a02b89846..7d003fcf9 100644 --- a/pyocd/probe/pydapaccess/interface/common.py +++ b/pyocd/probe/pydapaccess/interface/common.py @@ -79,6 +79,7 @@ NXP_MCULINK_ID, (0x1a86, 0x8011), # WCH-Link (0x2a86, 0x8011), # WCH-Link clone + (0x1209, 0x2488), # pico2-debug / Dapper Miser CMSIS-DAP (RP2350 software debug probe) ] ## List of substrings to look for in product and interface name strings. diff --git a/pyocd/probe/pydapaccess/interface/hidapi_backend.py b/pyocd/probe/pydapaccess/interface/hidapi_backend.py index 6382cfc48..a6c5b7570 100644 --- a/pyocd/probe/pydapaccess/interface/hidapi_backend.py +++ b/pyocd/probe/pydapaccess/interface/hidapi_backend.py @@ -215,6 +215,11 @@ def _update_report_sizes(self) -> bool: def open(self): + # Always use a fresh hid.device() object. The device opened during + # enumeration (hid.device(vid, pid, path) auto-opens) or a previously + # closed object cannot be safely re-opened via open_path() — the + # underlying hid_device* pointer is freed after close(). + self.device = hid.device() try: self.device.open_path(self.device_info['path']) except IOError as exc: @@ -294,11 +299,11 @@ def get_all_connected_interfaces(): if filter_device_by_usage_page(vid, pid, deviceInfo['usage_page']): continue - try: - dev = hid.device(vendor_id=vid, product_id=pid, path=deviceInfo['path']) - except IOError as exc: - LOG.debug("Failed to open USB device: %s", exc) - continue + # Create an unopened device object. open() will call open_path() + # when the probe is actually used — don't open here because the + # auto-open constructor leaks a file descriptor that open() would + # then double-open, breaking the close→reopen lifecycle. + dev = hid.device() # Create the USB interface object for this device. new_board = HidApiUSB(dev, deviceInfo) diff --git a/pyocd/probe/pydapaccess/interface/pyusb_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_backend.py index 56d5dc31a..b226e4d96 100644 --- a/pyocd/probe/pydapaccess/interface/pyusb_backend.py +++ b/pyocd/probe/pydapaccess/interface/pyusb_backend.py @@ -64,9 +64,19 @@ def __init__(self, dev): super().__init__() self.vid = dev.idVendor self.pid = dev.idProduct - self.product_name = dev.product or f"{dev.idProduct:#06x}" - self.vendor_name = dev.manufacturer or f"{dev.idVendor:#06x}" - self.serial_number = dev.serial_number \ + try: + self.product_name = dev.product or f"{dev.idProduct:#06x}" + except Exception: + self.product_name = f"{dev.idProduct:#06x}" + try: + self.vendor_name = dev.manufacturer or f"{dev.idVendor:#06x}" + except Exception: + self.vendor_name = f"{dev.idVendor:#06x}" + try: + _serial = dev.serial_number + except Exception: + _serial = None + self.serial_number = _serial \ or generate_device_unique_id(dev.idProduct, dev.idVendor, dev.bus, dev.address) self.ep_out = None self.ep_in = None @@ -358,7 +368,11 @@ def __call__(self, dev): config = dev.get_active_configuration() # Now read the product name string. - device_string = dev.product + # Some devices (e.g. pico2-debug) expose no string descriptors; treat as empty. + try: + device_string = dev.product + except Exception: + device_string = None if ((device_string is None) or (not is_known_device_string(device_string))) and (not known_cmsis_dap): return False @@ -391,11 +405,15 @@ def __call__(self, dev): if cmsis_dap_interface is None: return False if self._serial is not None: - if dev.serial_number is None: + try: + dev_serial = dev.serial_number + except Exception: + dev_serial = None + if dev_serial is None: if self._serial == "": return True if self._serial == generate_device_unique_id(dev.idProduct, dev.idVendor, dev.bus, dev.address): return True - if self._serial != dev.serial_number: + if self._serial != dev_serial: return False return True