From 64c294acc7bad3a31145af4ab9b51ada61469da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Stelmach?= Date: Wed, 12 Nov 2025 22:21:43 +0100 Subject: [PATCH 1/4] Do not reattach a driver if freeze requested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using FTDI as a GPIO adapter in bitbanging mode to control some external hardware, it is highly undesirable to leave ttyUSBn available for opening and possibly ruining the state of output pins. Signed-off-by: Łukasz Stelmach --- pyftdi/ftdi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index 891546e2..b1495b0a 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -579,7 +579,8 @@ def close(self, freeze: bool = False) -> None: except FtdiError as exc: self.log.warning('FTDI device may be gone: %s', exc) try: - self._usb_dev.attach_kernel_driver(self._index - 1) + if not freeze: + self._usb_dev.attach_kernel_driver(self._index - 1) except (NotImplementedError, USBError): pass self._usb_dev = None From 7e21195bdffe73cf37a244ea006e7901b666b1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Stelmach?= Date: Wed, 12 Nov 2025 22:26:51 +0100 Subject: [PATCH 2/4] Add claim_interface() to match release_interface() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release_interface() called from Ftdi.close() should be matched by claim_interface() in Ftdi.open_from_device(). Signed-off-by: Łukasz Stelmach --- pyftdi/ftdi.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index b1495b0a..76d7da19 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -16,8 +16,8 @@ from typing import Callable, Optional, List, Sequence, TextIO, Tuple, Union from usb.core import (Configuration as UsbConfiguration, Device as UsbDevice, USBError) -from usb.util import (build_request_type, release_interface, CTRL_IN, CTRL_OUT, - CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE) +from usb.util import (build_request_type, claim_interface, release_interface, + CTRL_IN, CTRL_OUT, CTRL_TYPE_VENDOR, CTRL_RECIPIENT_DEVICE) from .misc import to_bool from .usbtools import UsbDeviceDescriptor, UsbTools @@ -546,6 +546,8 @@ def open_from_device(self, device: UsbDevice, self._readbuffer = bytearray() # Drain input buffer self.purge_buffers() + # Claim the interface + claim_interface(device, self._index - 1) # Shallow reset self._reset_device() # Reset feature mode From 9c9e7e85c913c44afe0157ef7f3122dcb5575489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Stelmach?= Date: Wed, 12 Nov 2025 23:01:10 +0100 Subject: [PATCH 3/4] Don't reset the bitmode in bitbanging mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When using Ftdi.open_bintbang_*() functions, don't reset the bitmode because it causes some pins to twich briefley before the BITBANG mode is set. Signed-off-by: Łukasz Stelmach --- pyftdi/ftdi.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index 76d7da19..e21060df 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -522,7 +522,8 @@ def open(self, vendor: int, product: int, bus: Optional[int] = None, self.open_from_device(device, interface) def open_from_device(self, device: UsbDevice, - interface: int = 1) -> None: + interface: int = 1, + reset_bitmode: bool = True) -> None: """Open a new interface from an existing USB device. :param device: FTDI USB device (PyUSB instance) @@ -551,7 +552,8 @@ def open_from_device(self, device: UsbDevice, # Shallow reset self._reset_device() # Reset feature mode - self.set_bitmode(0, Ftdi.BitMode.RESET) + if reset_bitmode: + self.set_bitmode(0, Ftdi.BitMode.RESET) # Init latency self._latency_threshold = None self.set_latency_timer(self.LATENCY_MIN) @@ -841,7 +843,7 @@ def open_bitbang_from_device(self, device: UsbDevice, :param sync: whether to use synchronous or asynchronous bitbang :return: actual bitbang baudrate in bps """ - self.open_from_device(device, interface) + self.open_from_device(device, interface, reset_bitmode=False) # Set latency timer self.set_latency_timer(latency) # Set chunk size From 95b79eb07eb35700acf24f9ffb6b8fc8feb02842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Stelmach?= Date: Wed, 12 Nov 2025 23:04:39 +0100 Subject: [PATCH 4/4] Don't set configuration if not necessary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit According to libusb documentation [1] it isn't always desirable to call SET_CONFIGURAITON if a device has already been configured. The same approach can be found in libftdi[2]. I have observed that devices didn't work properly in BITBANG mode after the configuration was set to the one the device had alredy been using. [1] https://libusb.sourceforge.io/api-1.0/libusb_caveats.html#configsel [2] http://developer.intra2net.com/git/?p=libftdi;a=blob;f=src/ftdi.c;h=f5b70188f91fc1d4c329d891698ec74566ebeb94;hb=5c2c58e03ea999534e8cb64906c8ae8b15536c30#l631 Signed-off-by: Łukasz Stelmach --- pyftdi/ftdi.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index e21060df..75460ae4 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -532,12 +532,10 @@ def open_from_device(self, device: UsbDevice, if not isinstance(device, UsbDevice): raise FtdiError(f"Device '{device}' is not a PyUSB device") self._usb_dev = device - try: - self._usb_dev.set_configuration() - except USBError: - pass + cfg0 = device[0].bConfigurationValue # detect invalid interface as early as possible config = self._usb_dev.get_active_configuration() + cfg = config.bConfigurationValue if interface > config.bNumInterfaces: raise FtdiError(f'No such FTDI port: {interface}') self._set_interface(config, interface) @@ -548,6 +546,11 @@ def open_from_device(self, device: UsbDevice, # Drain input buffer self.purge_buffers() # Claim the interface + if device.bNumConfigurations > 0 and cfg0 != cfg: + try: + device.set_configuration() + except USBError: + pass claim_interface(device, self._index - 1) # Shallow reset self._reset_device()