Skip to content

Commit

Permalink
CMSIS-DAP: pydapaccess: hidapi_backend: read thread, max packet count…
Browse files Browse the repository at this point in the history
… limit.

This change introduces a background read thread for the HidApiUSB
backend for CMSIS-DAP. Similar to the other backends, it reads and
queues incoming command responses as soon as they are available.
Also added a packet count maximum to deal with the arbitrary limit
of 30 queued IN reports in the Mac version of hidapi.
  • Loading branch information
flit committed Dec 20, 2021
1 parent 0446fed commit bddbd19
Showing 1 changed file with 64 additions and 5 deletions.
69 changes: 64 additions & 5 deletions pyocd/probe/pydapaccess/interface/hidapi_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import collections
import logging
import six
import threading

from .interface import Interface
from .common import (
Expand All @@ -25,6 +27,7 @@
)
from ..dap_access_api import DAPAccessIntf
from ....utility.compatibility import to_str_safe
from ....utility.timeout import Timeout

LOG = logging.getLogger(__name__)
TRACE = LOG.getChild("trace")
Expand All @@ -43,18 +46,53 @@ class HidApiUSB(Interface):

isAvailable = IS_AVAILABLE

HIDAPI_MAX_PACKET_COUNT = 30

def __init__(self):
super(HidApiUSB, self).__init__()
super().__init__()
# Vendor page and usage_id = 2
self.device = None
self.device_info = None
self.thread = None
self.read_sem = threading.Semaphore(0)
self.closed_event = threading.Event()
self.received_data = collections.deque()

def set_packet_count(self, count):
# hidapi for macos has an arbitrary limit on the number of packets it will queue for reading.
# Even though we have a read thread, it doesn't hurt to limit the packet count since the limit
# is fairly high.
self.packet_count = min(count, self.HIDAPI_MAX_PACKET_COUNT)

def open(self):
try:
self.device.open_path(self.device_info['path'])
except IOError as exc:
raise DAPAccessIntf.DeviceError("Unable to open device: " + str(exc)) from exc

self.closed_event.clear()

# Start RX thread
self.thread = threading.Thread(target=self.rx_task)
self.thread.daemon = True
self.thread.start()

def rx_task(self):
try:
while not self.closed_event.is_set():
self.read_sem.acquire()
if not self.closed_event.is_set():
read_data = self.device.read(self.packet_size)

if TRACE.isEnabledFor(logging.DEBUG):
# Strip off trailing zero bytes to reduce clutter.
TRACE.debug(" USB IN < (%d) %s", len(read_data), ' '.join([f'{i:02x}' for i in bytes(read_data).rstrip(b'\x00')]))

self.received_data.append(read_data)
finally:
# Set last element of rcv_data to None on exit
self.received_data.append(None)

@staticmethod
def get_all_connected_interfaces():
"""! @brief Returns all the connected devices with CMSIS-DAP in the name.
Expand Down Expand Up @@ -109,19 +147,40 @@ def write(self, data):
if TRACE.isEnabledFor(logging.DEBUG):
TRACE.debug(" USB OUT> (%d) %s", len(data), ' '.join([f'{i:02x}' for i in data]))
data.extend([0] * (self.packet_size - len(data)))
self.read_sem.release()
self.device.write([0] + data)

def read(self, timeout=-1):
def read(self, timeout=Interface.DEFAULT_READ_TIMEOUT):
"""! @brief Read data on the IN endpoint associated to the HID interface
"""
data = self.device.read(self.packet_size)
# Spin for a while if there's not data available yet. 100 µs sleep between checks.
with Timeout(timeout, sleeptime=0.0001) as t_o:
while t_o.check():
if len(self.received_data) != 0:
break
else:
raise DAPAccessIntf.DeviceError(f"Timeout reading from device {self.serial_number}")

if self.received_data[0] is None:
raise DAPAccessIntf.DeviceError(f"Device {self.serial_number} read thread exited")

# Trace when the higher layer actually gets a packet previously read.
if TRACE.isEnabledFor(logging.DEBUG):
# Strip off trailing zero bytes to reduce clutter.
TRACE.debug(" USB IN < (%d) %s", len(data), ' '.join([f'{i:02x}' for i in bytes(data).rstrip(b'\x00')]))
return data
TRACE.debug(" USB RD < (%d) %s", len(self.received_data[0]),
' '.join([f'{i:02x}' for i in bytes(self.received_data[0]).rstrip(b'\x00')]))

return self.received_data.popleft()


def close(self):
"""! @brief Close the interface
"""
assert not self.closed_event.is_set()

LOG.debug("closing interface")
self.closed_event.set()
self.read_sem.release()
self.thread.join()
self.thread = None
self.device.close()

0 comments on commit bddbd19

Please sign in to comment.