Skip to content

Commit 1b6dde0

Browse files
Add transaction reset option
1 parent 49d3cc1 commit 1b6dde0

File tree

1 file changed

+57
-6
lines changed

1 file changed

+57
-6
lines changed

mcp2210/mcp2210.py

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ class Mcp2210CommandResponseDesyncException(Exception):
9696
pass
9797

9898

99+
class Mcp2210CommandResponseTimeoutException(Exception):
100+
"""
101+
Timed out waiting for a response from the MCP2210
102+
"""
103+
pass
104+
105+
99106
class Mcp2210UsbBusyException(Exception):
100107
"""
101108
A USB transaction is in progress, so the MCP2210 cannot execute the command.
@@ -377,6 +384,8 @@ class Mcp2210(object):
377384
:param vendor_id: The vendor ID of the device (defaults to 0x04d8)
378385
:param product_id: The product ID of the device (defaults to 0x00de)
379386
:param immediate_gpio_update: If `True`, immediately send any GPIO configuration changes to the device
387+
:param reset_transactions_on_init: If 'True', performs a reset of the HID transaction statemachine on init
388+
(defaults to False to maintain backwards compatibility)
380389
381390
A usage example is below.
382391
@@ -392,6 +401,11 @@ class Mcp2210(object):
392401
# You can also connect either VCC or GND to pins 5-8.
393402
# Note that when unconnected the pins will read as False.
394403
404+
# In some circumstances, closing at the wrong time (through an exception, etc) can cause the
405+
# MCP2210 to get stuck mid-transaction. If this happens, the hidapi library will block forever. To resolve this,
406+
# pass reset_transactions_on_init=True to the constructor - this will attempt to reset the transaction
407+
# statemachine by performing a dummy read before doing anything meaningful.
408+
395409
# connect to the device by serial number
396410
mcp = Mcp2210(serial_number="0000992816")
397411
@@ -445,7 +459,7 @@ class Mcp2210(object):
445459
"""
446460

447461
def __init__(self, serial_number: str, vendor_id: int = 0x04d8, product_id: int = 0x00de,
448-
immediate_gpio_update: bool = True):
462+
immediate_gpio_update: bool = True, reset_transactions_on_init: bool = False):
449463
if not serial_number.isdigit():
450464
raise ValueError("Serial number must be numbers only")
451465
if len(serial_number) != 10:
@@ -468,6 +482,8 @@ def __init__(self, serial_number: str, vendor_id: int = 0x04d8, product_id: int
468482
self._gpio_direction_needs_update = False
469483
self._gpio_output_needs_update = False
470484

485+
if reset_transactions_on_init:
486+
self._transaction_flow_reset()
471487
self._get_spi_configuration()
472488
self._get_gpio_configuration()
473489

@@ -502,19 +518,37 @@ def _hid_write(self, payload: bytes, pad_with_zeros: bool = True):
502518
else:
503519
self._hid.write(bytes(request))
504520

505-
def _hid_read(self, size: int) -> bytes:
521+
def _hid_read(self, size: int, timeout_sec: float = 0) -> bytes:
506522
"""
507523
Internal function to perform a HID read from the device.
508524
509525
:param size: Number of bytes to read
526+
:param timeout_sec: How long to wait before timing out (zero means no timeout)
510527
:return: the bytes which were read
511528
"""
512-
read_data = bytes(self._hid.read(size))
529+
if timeout_sec <= 0:
530+
read_data = bytes(self._hid.read(size))
531+
else:
532+
self._hid.set_nonblocking(True)
533+
534+
read_data = b""
535+
start = time.monotonic()
536+
while start + timeout_sec >= time.monotonic():
537+
tmp = self._hid.read(size - len(read_data))
538+
if tmp:
539+
read_data += bytes(tmp)
540+
else:
541+
time.sleep(0.05)
542+
543+
if len(read_data) == size:
544+
break
545+
546+
self._hid.set_nonblocking(False)
513547

514548
logger.debug("MCP2210: HID read: " + bytes_to_hex_string(read_data))
515549
return read_data
516550

517-
def _execute_command(self, request: bytes, pad_with_zeros: bool = True, check_return_code: bool = True) -> bytes:
551+
def _execute_command(self, request: bytes, pad_with_zeros: bool = True, check_return_code: bool = True, timeout_sec: float = 0) -> bytes:
518552
"""
519553
Internal function to execute a command on the MCP2210.
520554
@@ -525,7 +559,11 @@ def _execute_command(self, request: bytes, pad_with_zeros: bool = True, check_re
525559
"""
526560
self._hid_write(request, pad_with_zeros=pad_with_zeros)
527561

528-
response = self._hid.read(64)
562+
response = self._hid_read(64, timeout_sec)
563+
564+
if not response or len(response) != 64:
565+
raise Mcp2210CommandResponseTimeoutException
566+
529567
if response[0] != request[0]:
530568
raise Mcp2210CommandResponseDesyncException
531569

@@ -539,6 +577,18 @@ def _execute_command(self, request: bytes, pad_with_zeros: bool = True, check_re
539577

540578
return response
541579

580+
def _transaction_flow_reset(self):
581+
"""
582+
Internal function for (hopefully) resetting the statemachine of the MCP2210. May be needed in case of a weird
583+
glitch state where a transaction failed but the device has not yet been reinitialised.
584+
585+
This reads the status with a short timeout and ignores the result.
586+
"""
587+
try:
588+
self._execute_command(bytes([Mcp2210Commands.GET_STATUS]), timeout_sec=1)
589+
except Mcp2210CommandResponseTimeoutException:
590+
pass
591+
542592
def _get_spi_configuration(self):
543593
"""
544594
Internal function which gets the SPI configuration from the MCP2210.
@@ -777,4 +827,5 @@ def set_spi_mode(self, mode: int):
777827
"""
778828

779829
self._spi_settings.mode = mode
780-
self._set_spi_configuration()
830+
self._set_spi_configuration()
831+

0 commit comments

Comments
 (0)