Repository: pyocd/pyocd
Issue: cannot find my WCH-Link

WCH-Link is a CMSIS-dap debugger made by WCH.

It can be found by Keil and OpenOCD when adding the command `cmsis_dap_vid_pid 0x1a86 0x8011`. 

However, it cannot be found by PyOCD. When I run `pyocd list`, it just shows `
No available debug probes are connected`.

Changed files:
pyocd/probe/pydapaccess/interface/pyusb_backend.py, pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py, pyocd/probe/pydapaccess/interface/pywinusb_backend.py, pyocd/probe/pydapaccess/interface/hidapi_backend.py, pyocd/probe/pydapaccess/interface/common.py

diff --git a/pyocd/probe/pydapaccess/interface/common.py b/pyocd/probe/pydapaccess/interface/common.py
index b8c2073d..0bdaf4aa 100644
--- a/pyocd/probe/pydapaccess/interface/common.py
+++ b/pyocd/probe/pydapaccess/interface/common.py
@@ -1,6 +1,7 @@
 # pyOCD debugger
 # Copyright (c) 2019-2021 Arm Limited
 # Copyright (c) 2021 mentha
+# Copyright (c) 2021 Chris Reed
 # SPDX-License-Identifier: Apache-2.0
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -18,6 +19,7 @@
 import usb.util
 from hashlib import sha1
 from base64 import b32encode
+from typing import (List, Union)
 
 # USB class codes.
 USB_CLASS_COMPOSITE = 0x00
@@ -110,17 +112,21 @@ def check_ep(interface, ep_index, ep_dir, ep_type):
     return (usb.util.endpoint_direction(ep.bEndpointAddress) == ep_dir) \\
         and (usb.util.endpoint_type(ep.bmAttributes) == ep_type)
 
-def generate_device_unique_id(dev):
-    """! @brief Generate a semi-stable unique ID from a pyusb device.
+def generate_device_unique_id(vid: int, pid: int, *locations: List[Union[int, str]]) -> str:
+    """! @brief Generate a semi-stable unique ID from USB device properties.
     
     This function is intended to be used in cases where a device does not provide a serial number
     string. pyocd still needs a valid unique ID so the device can be selected from amongst multiple
     connected devices. The algorithm used here generates an ID that is stable for a given device as
     long as it is connected to the same USB port.
     
-    @param dev pyusb device object.
-    @return 16-character string.
+    @param vid Vendor ID.
+    @param pid Product ID.
+    @param locations Additional parameters are expected to be int or string values that represent
+        parts of the bus location to which the device is connected. At least one location parameter
+        must be provided.
+    @return Unique ID string generated from parameeters.
     """
-    s = "{:4x},{:4x},{:s},{:s}".format(dev.idProduct, dev.idVendor, dev.bus, dev.address)
-    return b32encode(sha1(s.encode()).digest()).decode('ascii')[:16]
+    s = f"{vid:4x},{pid:4x}," + ",".join(str(locations))
+    return b32encode(sha1(s.encode()).digest()).decode('ascii')
 
diff --git a/pyocd/probe/pydapaccess/interface/hidapi_backend.py b/pyocd/probe/pydapaccess/interface/hidapi_backend.py
index b64f05e1..71f4a978 100644
--- a/pyocd/probe/pydapaccess/interface/hidapi_backend.py
+++ b/pyocd/probe/pydapaccess/interface/hidapi_backend.py
@@ -19,7 +19,10 @@ import logging
 import six
 
 from .interface import Interface
-from .common import filter_device_by_usage_page
+from .common import (
+    filter_device_by_usage_page,
+    generate_device_unique_id,
+    )
 from ..dap_access_api import DAPAccessIntf
 from ....utility.compatibility import to_str_safe
 
@@ -59,16 +62,17 @@ class HidApiUSB(Interface):
 
         devices = hid.enumerate()
 
-        if not devices:
-            return []
-
         boards = []
 
         for deviceInfo in devices:
             product_name = to_str_safe(deviceInfo['product_string'])
-            if (product_name.find("CMSIS-DAP") < 0):
-                # Skip non cmsis-dap devices
-                continue
+            if ("CMSIS-DAP" not in product_name):
+                # Check the device path as a backup. Even though we can't get the interface name from
+                # hidapi, it may appear in the path. At least, it does on macOS.
+                device_path = to_str_safe(deviceInfo['path'])
+                if "CMSIS-DAP" not in device_path:
+                    # Skip non cmsis-dap devices
+                    continue
             
             vid = deviceInfo['vendor_id']
             pid = deviceInfo['product_id']
@@ -85,11 +89,12 @@ class HidApiUSB(Interface):
 
             # Create the USB interface object for this device.
             new_board = HidApiUSB()
-            new_board.vendor_name = deviceInfo['manufacturer_string']
-            new_board.product_name = deviceInfo['product_string']
-            new_board.serial_number = deviceInfo['serial_number']
             new_board.vid = vid
             new_board.pid = pid
+            new_board.vendor_name = deviceInfo['manufacturer_string'] or f"{vid:#06x}"
+            new_board.product_name = deviceInfo['product_string'] or f"{pid:#06x}"
+            new_board.serial_number = deviceInfo['serial_number'] \\
+                    or generate_device_unique_id(vid, pid, six.ensure_str(deviceInfo['path']))
             new_board.device_info = deviceInfo
             new_board.device = dev
             boards.append(new_board)
diff --git a/pyocd/probe/pydapaccess/interface/pyusb_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_backend.py
index b0087b05..13098b4d 100644
--- a/pyocd/probe/pydapaccess/interface/pyusb_backend.py
+++ b/pyocd/probe/pydapaccess/interface/pyusb_backend.py
@@ -2,6 +2,7 @@
 # Copyright (c) 2006-2021 Arm Limited
 # Copyright (c) 2020 Patrick Huesmann
 # Copyright (c) 2021 mentha
+# Copyright (c) Chris Reed
 # SPDX-License-Identifier: Apache-2.0
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -167,9 +168,10 @@ class PyUSB(Interface):
             new_board = PyUSB()
             new_board.vid = board.idVendor
             new_board.pid = board.idProduct
-            new_board.product_name = board.product or hex(board.idProduct)
-            new_board.vendor_name = board.manufacturer or hex(board.idVendor)
-            new_board.serial_number = board.serial_number or generate_device_unique_id(board)
+            new_board.product_name = board.product or f"{board.idProduct:#06x}"
+            new_board.vendor_name = board.manufacturer or f"{board.idVendor:#06x}"
+            new_board.serial_number = board.serial_number \\
+                    or generate_device_unique_id(board.idProduct, board.idVendor, board.bus, board.address)
             boards.append(new_board)
 
         return boards
diff --git a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py
index aab8474d..f14914c4 100644
--- a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py
+++ b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py
@@ -1,6 +1,7 @@
 # pyOCD debugger
 # Copyright (c) 2019-2021 Arm Limited
 # Copyright (c) 2021 mentha
+# Copyright (c) Chris Reed
 # SPDX-License-Identifier: Apache-2.0
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -187,9 +188,10 @@ class PyUSBv2(Interface):
             new_board = PyUSBv2()
             new_board.vid = board.idVendor
             new_board.pid = board.idProduct
-            new_board.product_name = board.product or hex(board.idProduct)
-            new_board.vendor_name = board.manufacturer or hex(board.idVendor)
-            new_board.serial_number = board.serial_number or generate_device_unique_id(board)
+            new_board.product_name = board.product or f"{board.idProduct:#06x}"
+            new_board.vendor_name = board.manufacturer or f"{board.idVendor:#06x}"
+            new_board.serial_number = board.serial_number \\
+                    or generate_device_unique_id(board.idProduct, board.idVendor, board.bus, board.address)
             boards.append(new_board)
 
         return boards
diff --git a/pyocd/probe/pydapaccess/interface/pywinusb_backend.py b/pyocd/probe/pydapaccess/interface/pywinusb_backend.py
index 2427f3ee..c0d9aeed 100644
--- a/pyocd/probe/pydapaccess/interface/pywinusb_backend.py
+++ b/pyocd/probe/pydapaccess/interface/pywinusb_backend.py
@@ -21,7 +21,10 @@ from time import sleep
 import six
 
 from .interface import Interface
-from .common import filter_device_by_usage_page
+from .common import (
+    filter_device_by_usage_page,
+    generate_device_unique_id,
+    )
 from ..dap_access_api import DAPAccessIntf
 from ....utility.timeout import Timeout
 
@@ -100,7 +103,7 @@ class PyWinUSB(Interface):
         # find devices with good vid/pid
         all_mbed_devices = []
         for d in all_devices:
-            if (d.product_name.find("CMSIS-DAP") >= 0):
+            if ("CMSIS-DAP" in d.product_name):
                 all_mbed_devices.append(d)
 
         boards = []
@@ -120,17 +123,18 @@ class PyWinUSB(Interface):
                 new_board = PyWinUSB()
                 new_board.report = report[0]
                 new_board.packet_size = len(new_board.report.get_raw_data()) - 1
-                new_board.vendor_name = dev.vendor_name
-                new_board.product_name = dev.product_name
-                new_board.serial_number = dev.serial_number
+                new_board.vendor_name = dev.vendor_name or f"{dev.vendor_id:#06x}"
+                new_board.product_name = dev.product_name or f"{dev.product_id:#06x}"
+                new_board.serial_number = dev.serial_number \\
+                        or generate_device_unique_id(dev.vendor_id, dev.product_id, dev.device_path)
                 new_board.vid = dev.vendor_id
                 new_board.pid = dev.product_id
                 new_board.device = dev
-                dev.close()
                 boards.append(new_board)
             except Exception as e:
                 if (str(e) != "Failure to get HID pre parsed data"):
                     LOG.error("Receiving Exception: %s", e)
+            finally:
                 dev.close()
 
         return boards