From 4af9f6dc0088f4244d213678766e06a9df592018 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Tue, 19 Feb 2019 19:02:50 -0600 Subject: [PATCH 1/6] Fixed CMSIS-DAP SWO. --- pyocd/probe/pydapaccess/dap_access_cmsis_dap.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py index e19dccfd6..27700dce2 100644 --- a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py +++ b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py @@ -678,7 +678,7 @@ def swo_configure(self, enabled, rate): if self._protocol.swo_transport(DAPSWOTransport.DAP_SWO_DATA) != 0: self._swo_disable() return False - if self._protocol.swo_mode(DAP_SWO_MODE.UART) != 0: + if self._protocol.swo_mode(DAPSWOMode.UART) != 0: self._swo_disable() return False if self._protocol.swo_baudrate(rate) == 0: @@ -721,7 +721,8 @@ def get_swo_status(self): def swo_read(self, count=None): if count is None: count = self._packet_size - return self._protocol.swo_data(count) + status, count, data = self._protocol.swo_data(count) + return bytearray(data) def write_reg(self, reg_id, value, dap_index=0): assert reg_id in self.REG From 2e2bb4a182bf92f6522387b44c6f21bd309ee821 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Tue, 19 Feb 2019 18:38:14 -0600 Subject: [PATCH 2/6] CMSIS-DAPv2 support. --- pyocd/probe/pydapaccess/cmsis_dap_core.py | 1 + .../probe/pydapaccess/dap_access_cmsis_dap.py | 35 ++- pyocd/probe/pydapaccess/interface/__init__.py | 3 + .../pydapaccess/interface/pyusb_v2_backend.py | 292 ++++++++++++++++++ 4 files changed, 323 insertions(+), 8 deletions(-) create mode 100644 pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py diff --git a/pyocd/probe/pydapaccess/cmsis_dap_core.py b/pyocd/probe/pydapaccess/cmsis_dap_core.py index daa289ed0..2790be6f3 100644 --- a/pyocd/probe/pydapaccess/cmsis_dap_core.py +++ b/pyocd/probe/pydapaccess/cmsis_dap_core.py @@ -84,6 +84,7 @@ class Pin: class DAPSWOTransport: NONE = 0 DAP_SWO_DATA = 1 + DAP_SWO_EP = 2 # SWO mode options. class DAPSWOMode: diff --git a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py index 27700dce2..c9dbdbe55 100644 --- a/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py +++ b/pyocd/probe/pydapaccess/dap_access_cmsis_dap.py @@ -24,7 +24,7 @@ from .dap_settings import DAPSettings from .dap_access_api import DAPAccessIntf from .cmsis_dap_core import CMSISDAPProtocol -from .interface import (INTERFACE, USB_BACKEND, WS_BACKEND) +from .interface import (INTERFACE, USB_BACKEND, USB_BACKEND_V2, WS_BACKEND) from .cmsis_dap_core import (Command, Pin, Capabilities, DAP_TRANSFER_OK, DAP_TRANSFER_FAULT, DAP_TRANSFER_WAIT, DAPSWOTransport, DAPSWOMode, DAPSWOControl, @@ -50,10 +50,16 @@ class SWOStatus: def _get_interfaces(): """Get the connected USB devices""" + # Get CMSIS-DAPv2 interfaces. if DAPSettings.use_ws: - return INTERFACE[WS_BACKEND].get_all_connected_interfaces(DAPSettings.ws_host, DAPSettings.ws_port) + interfaces = INTERFACE[WS_BACKEND].get_all_connected_interfaces(DAPSettings.ws_host, DAPSettings.ws_port) else: - return INTERFACE[USB_BACKEND].get_all_connected_interfaces() + interfaces = INTERFACE[USB_BACKEND].get_all_connected_interfaces() + + # Add in CMSIS-DAPv2 interfaces. + interfaces += INTERFACE[USB_BACKEND_V2].get_all_connected_interfaces() + + return interfaces def _get_unique_id(interface): @@ -675,7 +681,13 @@ def swo_configure(self, enabled, rate): try: if enabled: - if self._protocol.swo_transport(DAPSWOTransport.DAP_SWO_DATA) != 0: + # Select the streaming SWO endpoint if available. + if self._interface.has_swo_ep: + transport = DAPSWOTransport.DAP_SWO_EP + else: + transport = DAPSWOTransport.DAP_SWO_DATA + + if self._protocol.swo_transport(transport) != 0: self._swo_disable() return False if self._protocol.swo_mode(DAPSWOMode.UART) != 0: @@ -709,9 +721,13 @@ def swo_control(self, start): if start: self._protocol.swo_control(DAPSWOControl.START) + if self._interface.has_swo_ep: + self._interface.start_swo() self._swo_status = SWOStatus.RUNNING else: self._protocol.swo_control(DAPSWOControl.STOP) + if self._interface.has_swo_ep: + self._interface.stop_swo() self._swo_status = SWOStatus.CONFIGURED return True @@ -719,10 +735,13 @@ def get_swo_status(self): return self._protocol.swo_status() def swo_read(self, count=None): - if count is None: - count = self._packet_size - status, count, data = self._protocol.swo_data(count) - return bytearray(data) + if self._interface.has_swo_ep: + return self._interface.read_swo() + else: + if count is None: + count = self._packet_size + status, count, data = self._protocol.swo_data(count) + return bytearray(data) def write_reg(self, reg_id, value, dap_index=0): assert reg_id in self.REG diff --git a/pyocd/probe/pydapaccess/interface/__init__.py b/pyocd/probe/pydapaccess/interface/__init__.py index f884d9340..1b227b250 100644 --- a/pyocd/probe/pydapaccess/interface/__init__.py +++ b/pyocd/probe/pydapaccess/interface/__init__.py @@ -19,12 +19,14 @@ import logging from .hidapi_backend import HidApiUSB from .pyusb_backend import PyUSB +from .pyusb_v2_backend import PyUSBv2 from .pywinusb_backend import PyWinUSB from .ws_backend import WebSocketInterface INTERFACE = { 'hidapiusb': HidApiUSB, 'pyusb': PyUSB, + 'pyusb_v2': PyUSBv2, 'pywinusb': PyWinUSB, 'ws': WebSocketInterface } @@ -58,3 +60,4 @@ else: raise Exception("No USB backend found") +USB_BACKEND_V2 = "pyusb_v2" diff --git a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py new file mode 100644 index 000000000..dc74ea4ad --- /dev/null +++ b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py @@ -0,0 +1,292 @@ +""" + mbed CMSIS-DAP debugger + Copyright (c) 2006-2013 ARM Limited + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" + +from .interface import Interface +from ..dap_access_api import DAPAccessIntf +import logging +import os +import threading +import six +from time import sleep + +LOG = logging.getLogger(__name__) + +try: + import usb.core + import usb.util +except: + IS_AVAILABLE = False +else: + IS_AVAILABLE = True + +class PyUSBv2(Interface): + """! + @brief CMSIS-DAPv2 interface using pyUSB. + """ + + isAvailable = IS_AVAILABLE + + def __init__(self): + super(PyUSBv2, self).__init__() + self.vid = 0 + self.pid = 0 + self.product_name = None + self.vendor_name = None + self.ep_out = None + self.ep_in = None + self.ep_swo = None + self.dev = None + self.intf_number = None + self.serial_number = None + self.kernel_driver_was_attached = False + self.closed = True + self.thread = None + self.rx_stop_event = None + self.swo_thread = None + self.swo_stop_event = None + self.rcv_data = [] + self.swo_data = [] + self.read_sem = threading.Semaphore(0) + self.packet_size = 512 + self.is_swo_running = False + + @property + def has_swo_ep(self): + return self.ep_swo is not None + + def open(self): + assert self.closed is True + + # Get device handle + dev = usb.core.find(custom_match=HasCmsisDapv2Interface(self.serial_number)) + if dev is None: + raise DAPAccessIntf.DeviceError("Device %s not found" % + self.serial_number) + + # get active config + config = dev.get_active_configuration() + + # Get CMSIS-DAPv2 interface + interface = usb.util.find_descriptor(config, custom_match=match_cmsis_dap_interface_name) + if interface is None: + raise DAPAccessIntf.DeviceError("Device %s has no CMSIS-DAPv2 interface" % + self.serial_number) + interface_number = interface.bInterfaceNumber + + # Find endpoints. CMSIS-DAPv2 endpoints are in a fixed order. + try: + ep_out = interface.endpoints()[0] + ep_in = interface.endpoints()[1] + ep_swo = interface.endpoints()[2] if len(interface.endpoints()) > 2 else None + except IndexError: + raise DAPAccessIntf.DeviceError("CMSIS-DAPv2 device %s is missing endpoints" % + self.serial_number) + + # Explicitly claim the interface + try: + usb.util.claim_interface(dev, interface_number) + except usb.core.USBError as exc: + raise six.raise_from(DAPAccessIntf.DeviceError("Unable to open device"), exc) + + # Update all class variables if we made it here + self.ep_out = ep_out + self.ep_in = ep_in + self.ep_swo = ep_swo + self.dev = dev + self.intf_number = interface_number + + # Start RX thread as the last step + self.closed = False + self.start_rx() + + def start_rx(self): + # Flush the RX buffers by reading until timeout exception + try: + while True: + self.ep_in.read(self.ep_in.wMaxPacketSize, 1) + except usb.core.USBError: + # USB timeout expected + pass + + # Start RX thread + self.rx_stop_event = threading.Event() + thread_name = "CMSIS-DAP receive (%s)" % self.serial_number + self.thread = threading.Thread(target=self.rx_task, name=thread_name) + self.thread.daemon = True + self.thread.start() + + def start_swo(self): + self.swo_stop_event = threading.Event() + thread_name = "SWO receive (%s)" % self.serial_number + self.swo_thread = threading.Thread(target=self.swo_rx_task, name=thread_name) + self.swo_thread.daemon = True + self.swo_thread.start() + self.is_swo_running = True + + def stop_swo(self): + self.swo_stop_event.set() + self.swo_thread.join() + self.swo_thread = None + self.swo_stop_event = None + self.is_swo_running = False + + def rx_task(self): + try: + while not self.rx_stop_event.is_set(): + self.read_sem.acquire() + if not self.rx_stop_event.is_set(): + self.rcv_data.append(self.ep_in.read(self.ep_in.wMaxPacketSize, 10 * 1000)) + finally: + # Set last element of rcv_data to None on exit + self.rcv_data.append(None) + + def swo_rx_task(self): + try: + while not self.swo_stop_event.is_set(): + try: + self.swo_data.append(self.ep_swo.read(self.ep_swo.wMaxPacketSize, 10 * 1000)) + except usb.core.USBError: + pass + finally: + # Set last element of swo_data to None on exit + self.swo_data.append(None) + + @staticmethod + def get_all_connected_interfaces(): + """! @brief Returns all the connected devices with a CMSIS-DAPv2 interface.""" + # find all cmsis-dap devices + all_devices = usb.core.find(find_all=True, custom_match=HasCmsisDapv2Interface()) + + # iterate on all devices found + boards = [] + for board in all_devices: + new_board = PyUSBv2() + new_board.vid = board.idVendor + new_board.pid = board.idProduct + new_board.product_name = board.product + new_board.vendor_name = board.manufacturer + new_board.serial_number = board.serial_number + boards.append(new_board) + + return boards + + def write(self, data): + """! @brief Write data on the OUT endpoint.""" + + report_size = self.packet_size + if self.ep_out: + report_size = self.ep_out.wMaxPacketSize + + for _ in range(report_size - len(data)): + data.append(0) + + self.read_sem.release() + + self.ep_out.write(data) + #logging.debug('sent: %s', data) + + def read(self): + """! @brief Read data on the IN endpoint.""" + while len(self.rcv_data) == 0: + sleep(0) + + if self.rcv_data[0] is None: + raise DAPAccessIntf.DeviceError("Device %s read thread exited unexpectedly" % self.serial_number) + return self.rcv_data.pop(0) + + def read_swo(self): + # Accumulate all available SWO data. + data = bytearray() + while len(self.swo_data): + if self.swo_data[0] is None: + raise DAPAccessIntf.DeviceError("Device %s SWO thread exited unexpectedly" % self.serial_number) + data += self.swo_data.pop(0) + + return data + + def set_packet_count(self, count): + # No interface level restrictions on count + self.packet_count = count + + def set_packet_size(self, size): + self.packet_size = size + + def get_serial_number(self): + return self.serial_number + + def close(self): + """! @brief Close the USB interface.""" + assert self.closed is False + + if self.is_swo_running: + self.stop_swo() + self.closed = True + self.rx_stop_event.set() + self.read_sem.release() + self.thread.join() + assert self.rcv_data[-1] is None + self.rcv_data = [] + self.swo_data = [] + usb.util.release_interface(self.dev, self.intf_number) + usb.util.dispose_resources(self.dev) + self.ep_out = None + self.ep_in = None + self.ep_swo = None + self.dev = None + self.intf_number = None + self.thread = None + +def match_cmsis_dap_interface_name(desc): + interface_name = usb.util.get_string(desc.device, desc.iInterface) + return (interface_name is not None) and ("CMSIS-DAP" in interface_name) + +class HasCmsisDapv2Interface(object): + """! @brief CMSIS-DAPv2 match class to be used with usb.core.find""" + + def __init__(self, serial=None): + """! @brief Create a new FindDap object with an optional serial number""" + self._serial = serial + + def __call__(self, dev): + """! @brief Return True if this is a CMSIS-DAPv2 device, False otherwise""" + try: + config = dev.get_active_configuration() + cmsis_dap_interface = usb.util.find_descriptor(config, custom_match=match_cmsis_dap_interface_name) + except ValueError as error: + # Permission denied error gets reported as ValueError (langid) + LOG.debug(("ValueError \"{}\" while trying to access USB device strings " + "for VID=0x{:04x} PID=0x{:04x}. " + "This is probably a permission issue.").format(error, dev.idVendor, dev.idProduct)) + return False + except usb.core.USBError as error: + LOG.warning("Exception getting product string: %s", error) + return False + except IndexError as error: + LOG.warning("Internal pyusb error: %s", error) + return False + + if cmsis_dap_interface is None: + return False + + # Check the class and subclass are vendor-specific. + if (cmsis_dap_interface.bInterfaceClass != 0xff) or (cmsis_dap_interface.bInterfaceSubClass != 0): + return False + + if self._serial is not None: + if self._serial != dev.serial_number: + return False + return True From 3ed9e94c49afc35268d9a28b49d7674e51ba1092 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Tue, 19 Feb 2019 19:14:26 -0600 Subject: [PATCH 3/6] Updated README.md to mention CMSIS-DAP v2 support. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aea70bd04..69083a061 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ Requirements - macOS, Linux, or Windows 7 or newer - Microcontroller with an Arm Cortex-M CPU - Supported debug probe - - [CMSIS-DAP](http://www.keil.com/pack/doc/CMSIS/DAP/html/index.html), such as an on-board debug - probe using [DAPLink](https://os.mbed.com/handbook/DAPLink) firmware. + - [CMSIS-DAP](http://www.keil.com/pack/doc/CMSIS/DAP/html/index.html) v1 (HID) and v2 (WinUSB), + such as an on-board debug probe using [DAPLink](https://os.mbed.com/handbook/DAPLink) firmware. - STLinkV2, either on-board or the standalone version. From 69f2ba2417ca977901fa7de6976e5f85b52f7d93 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Wed, 20 Feb 2019 09:09:19 -0600 Subject: [PATCH 4/6] Added ULINKplus to udev rules. --- udev/50-pyocd.rules | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/udev/50-pyocd.rules b/udev/50-pyocd.rules index 6206d58a2..e68967913 100644 --- a/udev/50-pyocd.rules +++ b/udev/50-pyocd.rules @@ -16,5 +16,6 @@ SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374e", MODE:="666" SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374f", MODE:="666" SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3753", MODE:="666" - +# c251:2750 Keil ULINKplus +SUBSYSTEM=="usb", ATTR{idVendor}=="c251", ATTR{idProduct}=="2750", MODE:="666" From 1f6dada09f5e03adcff9e055673f5b06c48a0a46 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Wed, 20 Feb 2019 11:23:24 -0600 Subject: [PATCH 5/6] Gracefully handle missing libusb for CMSIS-DAPv2 with a warning. --- pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py index dc74ea4ad..925b91602 100644 --- a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py +++ b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py @@ -169,7 +169,12 @@ def swo_rx_task(self): def get_all_connected_interfaces(): """! @brief Returns all the connected devices with a CMSIS-DAPv2 interface.""" # find all cmsis-dap devices - all_devices = usb.core.find(find_all=True, custom_match=HasCmsisDapv2Interface()) + try: + all_devices = usb.core.find(find_all=True, custom_match=HasCmsisDapv2Interface()) + except usb.core.NoBackendError: + # Print a warning if pyusb cannot find a backend, and return no probes. + LOG.warning("CMSIS-DAPv2 probes are not supported because no libusb library was found.") + return [] # iterate on all devices found boards = [] From a9628bccbec049071a495f28a6ed9e85e0096a95 Mon Sep 17 00:00:00 2001 From: Chris Reed Date: Thu, 21 Feb 2019 08:05:09 -0600 Subject: [PATCH 6/6] Improve filtering for CMSIS-DAP USB devices. - Added initial check of the bDeviceClass before attempting to read a string, which may fail due to permissions on Linux. This leads to much fewer messages. - Improved warning messages for CMSIS-DAPv2 interface. --- pyocd/probe/pydapaccess/interface/common.py | 24 +++++++++ .../pydapaccess/interface/pyusb_backend.py | 5 ++ .../pydapaccess/interface/pyusb_v2_backend.py | 49 +++++++++++-------- 3 files changed, 57 insertions(+), 21 deletions(-) create mode 100644 pyocd/probe/pydapaccess/interface/common.py diff --git a/pyocd/probe/pydapaccess/interface/common.py b/pyocd/probe/pydapaccess/interface/common.py new file mode 100644 index 000000000..e28581c34 --- /dev/null +++ b/pyocd/probe/pydapaccess/interface/common.py @@ -0,0 +1,24 @@ +# pyOCD debugger +# Copyright (c) 2019 Arm Limited +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +USB_CLASS_COMPOSITE = 0x00 +USB_CLASS_MISCELLANEOUS = 0xef + +CMSIS_DAP_USB_CLASSES = [ + USB_CLASS_COMPOSITE, + USB_CLASS_MISCELLANEOUS, + ] + diff --git a/pyocd/probe/pydapaccess/interface/pyusb_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_backend.py index cace950a0..39f9dadfb 100644 --- a/pyocd/probe/pydapaccess/interface/pyusb_backend.py +++ b/pyocd/probe/pydapaccess/interface/pyusb_backend.py @@ -16,6 +16,7 @@ """ from .interface import Interface +from .common import CMSIS_DAP_USB_CLASSES from ..dap_access_api import DAPAccessIntf import logging import os @@ -256,6 +257,10 @@ def __init__(self, serial=None): def __call__(self, dev): """Return True if this is a DAP device, False otherwise""" + # Check if the device class is a valid one for CMSIS-DAP. + if dev.bDeviceClass not in CMSIS_DAP_USB_CLASSES: + return False + try: device_string = dev.product except ValueError as error: diff --git a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py index 925b91602..2811db77b 100644 --- a/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py +++ b/pyocd/probe/pydapaccess/interface/pyusb_v2_backend.py @@ -1,27 +1,28 @@ -""" - mbed CMSIS-DAP debugger - Copyright (c) 2006-2013 ARM Limited - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" +# pyOCD debugger +# Copyright (c) 2019 Arm Limited +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. from .interface import Interface +from .common import CMSIS_DAP_USB_CLASSES from ..dap_access_api import DAPAccessIntf import logging import os import threading import six from time import sleep +import errno LOG = logging.getLogger(__name__) @@ -268,14 +269,20 @@ def __init__(self, serial=None): def __call__(self, dev): """! @brief Return True if this is a CMSIS-DAPv2 device, False otherwise""" + # Check if the device class is a valid one for CMSIS-DAP. + if dev.bDeviceClass not in CMSIS_DAP_USB_CLASSES: + return False + try: config = dev.get_active_configuration() cmsis_dap_interface = usb.util.find_descriptor(config, custom_match=match_cmsis_dap_interface_name) - except ValueError as error: - # Permission denied error gets reported as ValueError (langid) - LOG.debug(("ValueError \"{}\" while trying to access USB device strings " - "for VID=0x{:04x} PID=0x{:04x}. " - "This is probably a permission issue.").format(error, dev.idVendor, dev.idProduct)) + except OSError as error: + if error.errno == errno.EACCES: + LOG.debug(("Error \"{}\" while trying to access the USB device configuration " + "for VID=0x{:04x} PID=0x{:04x}. This can probably be remedied with a udev rule.") + .format(error, dev.idVendor, dev.idProduct)) + else: + LOG.warning("OS error getting USB interface string: %s", error) return False except usb.core.USBError as error: LOG.warning("Exception getting product string: %s", error)