@@ -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+
99106class 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