From f1646c0172a8e276077fcfa2a118bc2c4efa24c7 Mon Sep 17 00:00:00 2001 From: Henry Au-Yeung Date: Fri, 4 Feb 2022 16:21:52 -0800 Subject: [PATCH 01/62] After ACK, if reading next, make SDA an input to avoid shorts After the master sends the request to read from a slave and after the ACK from the slave, the _send_check_ack function ends with SDA being a HI output. During this period to when the master starts clocking in the data it wants to read, some slaves pull the SDA line low. This would short the SDA output to GND, causing it to draw its max current (16mA for the FT4232). This fix makes it so that if the next action after the ACK is a read, SDA remains as an input during this period of time. --- pyftdi/i2c.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index 563a59bc..ac08b18c 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -691,7 +691,7 @@ def read(self, address: int, readlen: int = 1, with self._lock: while True: try: - self._do_prolog(i2caddress) + self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=False) data = self._do_read(readlen) do_epilog = relax return data @@ -731,7 +731,7 @@ def write(self, address: int, out: Union[bytes, bytearray, Iterable[int]], with self._lock: while True: try: - self._do_prolog(i2caddress) + self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=True) self._do_write(out) do_epilog = relax return @@ -779,9 +779,9 @@ def exchange(self, address: int, with self._lock: while True: try: - self._do_prolog(i2caddress) + self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=True) self._do_write(out) - self._do_prolog(i2caddress | self.BIT0) + self._do_prolog(i2caddress | self.BIT0, fake_tristate_ack_end_as_output=False) if readlen: data = self._do_read(readlen) do_epilog = relax @@ -816,7 +816,7 @@ def poll(self, address: int, write: bool = False, do_epilog = True with self._lock: try: - self._do_prolog(i2caddress) + self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=True) do_epilog = relax return True except I2cNackError: @@ -859,7 +859,7 @@ def poll_cond(self, address: int, fmt: str, mask: int, value: int, while retry < count: retry += 1 size = scalc(fmt) - self._do_prolog(i2caddress) + self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=False) data = self._do_read(size) self.log.debug("Poll data: %s", hexlify(data).decode()) cond, = sunpack(fmt, data) @@ -1020,7 +1020,7 @@ def _write_raw(self, data: int, write_high: bool): cmd = bytes([Ftdi.SET_BITS_LOW, low_data, low_dir]) self._ftdi.write_data(cmd) - def _do_prolog(self, i2caddress: int) -> None: + def _do_prolog(self, i2caddress: int, fake_tristate_ack_end_as_output: bool) -> None: if i2caddress is None: return self.log.debug(' prolog 0x%x', i2caddress >> 1) @@ -1029,7 +1029,7 @@ def _do_prolog(self, i2caddress: int) -> None: cmd.extend(self._write_byte) cmd.append(i2caddress) try: - self._send_check_ack(cmd) + self._send_check_ack(cmd, fake_tristate_ack_end_as_output) except I2cNackError: self.log.warning('NACK @ 0x%02x', (i2caddress>>1)) raise @@ -1041,15 +1041,18 @@ def _do_epilog(self) -> None: # be sure to purge the MPSSE reply self._ftdi.read_data_bytes(1, 1) - def _send_check_ack(self, cmd: bytearray): + def _send_check_ack(self, cmd: bytearray, fake_tristate_ack_end_as_output: bool): # note: cmd is modified if self._fake_tristate: # SCL low, SDA high-Z (input) cmd.extend(self._clk_lo_data_input) # read SDA (ack from slave) cmd.extend(self._read_bit) - # leave SCL low, restore SDA as output - cmd.extend(self._clk_lo_data_hi) + # after ACK, if writing next, end as output. if reading next, end + # as an input to prevent a short as the slave may pull SDA low after ACK + if fake_tristate_ack_end_as_output: + # leave SCL low, restore SDA as output + cmd.extend(self._clk_lo_data_hi) else: # SCL low, SDA high-Z cmd.extend(self._clk_lo_data_hi) @@ -1152,4 +1155,4 @@ def _do_write(self, out: Union[bytes, bytearray, Iterable[int]]): for byte in out: cmd = bytearray(self._write_byte) cmd.append(byte) - self._send_check_ack(cmd) + self._send_check_ack(cmd, fake_tristate_ack_end_as_output=True) From 135072a22baf4520b45f76d58930be36298deb34 Mon Sep 17 00:00:00 2001 From: Henry Au-Yeung Date: Fri, 4 Feb 2022 16:21:52 -0800 Subject: [PATCH 02/62] Fake tri-state: After ACK, if reading next, make SDA an input to avoid shorts After the master sends the request to read from a slave and after the ACK from the slave, the _send_check_ack function ends with SDA being a HI output. During this period to when the master starts clocking in the data it wants to read, some slaves pull the SDA line low. This would short the SDA output to GND, causing it to draw its max current (16mA for the FT4232). This fix makes it so that if the next action after the ACK is a read, SDA remains as an input during this period of time. --- pyftdi/gpio.py | 2 +- pyftdi/i2c.py | 45 +++++++++++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/pyftdi/gpio.py b/pyftdi/gpio.py index 18353dd2..0ddbd1d6 100644 --- a/pyftdi/gpio.py +++ b/pyftdi/gpio.py @@ -481,7 +481,7 @@ def set_frequency(self, frequency: Union[int, float]) -> None: self._frequency = self._ftdi.set_frequency(float(frequency)) def _update_direction(self) -> None: - # nothing to do in MPSSE mode, as direction is udpated with each + # nothing to do in MPSSE mode, as direction is updated with each # GPIO command pass diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index ac08b18c..46f5dd2c 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -353,10 +353,10 @@ class I2cController: HIGH = 0xff BIT0 = 0x01 IDLE = HIGH - SCL_BIT = 0x01 #AD0 - SDA_O_BIT = 0x02 #AD1 - SDA_I_BIT = 0x04 #AD2 - SCL_FB_BIT = 0x80 #AD7 + SCL_BIT = 0x01 # AD0 + SDA_O_BIT = 0x02 # AD1 + SDA_I_BIT = 0x04 # AD2 + SCL_FB_BIT = 0x80 # AD7 PAYLOAD_MAX_LENGTH = 0xFF00 # 16 bits max (- spare for control) HIGHEST_I2C_ADDRESS = 0x7F DEFAULT_BUS_FREQUENCY = 100000.0 @@ -691,7 +691,7 @@ def read(self, address: int, readlen: int = 1, with self._lock: while True: try: - self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=False) + self._do_prolog(i2caddress) data = self._do_read(readlen) do_epilog = relax return data @@ -731,7 +731,7 @@ def write(self, address: int, out: Union[bytes, bytearray, Iterable[int]], with self._lock: while True: try: - self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=True) + self._do_prolog(i2caddress) self._do_write(out) do_epilog = relax return @@ -779,9 +779,9 @@ def exchange(self, address: int, with self._lock: while True: try: - self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=True) + self._do_prolog(i2caddress) self._do_write(out) - self._do_prolog(i2caddress | self.BIT0, fake_tristate_ack_end_as_output=False) + self._do_prolog(i2caddress | self.BIT0) if readlen: data = self._do_read(readlen) do_epilog = relax @@ -816,7 +816,7 @@ def poll(self, address: int, write: bool = False, do_epilog = True with self._lock: try: - self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=True) + self._do_prolog(i2caddress) do_epilog = relax return True except I2cNackError: @@ -859,7 +859,7 @@ def poll_cond(self, address: int, fmt: str, mask: int, value: int, while retry < count: retry += 1 size = scalc(fmt) - self._do_prolog(i2caddress, fake_tristate_ack_end_as_output=False) + self._do_prolog(i2caddress) data = self._do_read(size) self.log.debug("Poll data: %s", hexlify(data).decode()) cond, = sunpack(fmt, data) @@ -1020,7 +1020,7 @@ def _write_raw(self, data: int, write_high: bool): cmd = bytes([Ftdi.SET_BITS_LOW, low_data, low_dir]) self._ftdi.write_data(cmd) - def _do_prolog(self, i2caddress: int, fake_tristate_ack_end_as_output: bool) -> None: + def _do_prolog(self, i2caddress: int) -> None: if i2caddress is None: return self.log.debug(' prolog 0x%x', i2caddress >> 1) @@ -1029,9 +1029,9 @@ def _do_prolog(self, i2caddress: int, fake_tristate_ack_end_as_output: bool) -> cmd.extend(self._write_byte) cmd.append(i2caddress) try: - self._send_check_ack(cmd, fake_tristate_ack_end_as_output) + self._send_check_ack(cmd, is_do_prolog_cmd=True) except I2cNackError: - self.log.warning('NACK @ 0x%02x', (i2caddress>>1)) + self.log.warning('NACK @ 0x%02x', (i2caddress >> 1)) raise def _do_epilog(self) -> None: @@ -1041,16 +1041,25 @@ def _do_epilog(self) -> None: # be sure to purge the MPSSE reply self._ftdi.read_data_bytes(1, 1) - def _send_check_ack(self, cmd: bytearray, fake_tristate_ack_end_as_output: bool): + def _send_check_ack(self, cmd: bytearray, is_do_prolog_cmd: bool): # note: cmd is modified if self._fake_tristate: + # if it is a _do_prolog command, then check to see if the next + # action after ACK is a read by looking at the last bit of cmd + # which is the read/write bit. Write=0, Read=1 + end_as_input = False + if is_do_prolog_cmd: + if cmd[-1:][0] & 0x1: + # if reading after ACK, we want to end as an input later + end_as_input = True # SCL low, SDA high-Z (input) cmd.extend(self._clk_lo_data_input) # read SDA (ack from slave) cmd.extend(self._read_bit) - # after ACK, if writing next, end as output. if reading next, end - # as an input to prevent a short as the slave may pull SDA low after ACK - if fake_tristate_ack_end_as_output: + # if writing after ACK, end as an output. if reading after, end + # as an input to prevent a short as some slaves may pull SDA low + # after ACK + if not end_as_input: # leave SCL low, restore SDA as output cmd.extend(self._clk_lo_data_hi) else: @@ -1155,4 +1164,4 @@ def _do_write(self, out: Union[bytes, bytearray, Iterable[int]]): for byte in out: cmd = bytearray(self._write_byte) cmd.append(byte) - self._send_check_ack(cmd, fake_tristate_ack_end_as_output=True) + self._send_check_ack(cmd, is_do_prolog_cmd=False) From 381546ffff9e67b96651fe58da805af4e24155af Mon Sep 17 00:00:00 2001 From: Henry Au-Yeung Date: Tue, 15 Mar 2022 15:38:59 -0700 Subject: [PATCH 03/62] Simplify implementation For fake tr-state: Don't restore SDA as output after checking for ACK. Instead, make it an output right before when we want to write. --- pyftdi/i2c.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index 46f5dd2c..0ec64fae 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -1029,7 +1029,7 @@ def _do_prolog(self, i2caddress: int) -> None: cmd.extend(self._write_byte) cmd.append(i2caddress) try: - self._send_check_ack(cmd, is_do_prolog_cmd=True) + self._send_check_ack(cmd) except I2cNackError: self.log.warning('NACK @ 0x%02x', (i2caddress >> 1)) raise @@ -1041,27 +1041,13 @@ def _do_epilog(self) -> None: # be sure to purge the MPSSE reply self._ftdi.read_data_bytes(1, 1) - def _send_check_ack(self, cmd: bytearray, is_do_prolog_cmd: bool): + def _send_check_ack(self, cmd: bytearray): # note: cmd is modified if self._fake_tristate: - # if it is a _do_prolog command, then check to see if the next - # action after ACK is a read by looking at the last bit of cmd - # which is the read/write bit. Write=0, Read=1 - end_as_input = False - if is_do_prolog_cmd: - if cmd[-1:][0] & 0x1: - # if reading after ACK, we want to end as an input later - end_as_input = True # SCL low, SDA high-Z (input) cmd.extend(self._clk_lo_data_input) # read SDA (ack from slave) cmd.extend(self._read_bit) - # if writing after ACK, end as an output. if reading after, end - # as an input to prevent a short as some slaves may pull SDA low - # after ACK - if not end_as_input: - # leave SCL low, restore SDA as output - cmd.extend(self._clk_lo_data_hi) else: # SCL low, SDA high-Z cmd.extend(self._clk_lo_data_hi) @@ -1162,6 +1148,11 @@ def _do_write(self, out: Union[bytes, bytearray, Iterable[int]]): self.log.debug('- write %d byte(s): %s', len(out), hexlify(out).decode()) for byte in out: - cmd = bytearray(self._write_byte) + if self._fake_tristate: + # leave SCL low, restore SDA as output + cmd = bytearray(self._clk_lo_data_hi) + cmd.extend(self._write_byte) + else: + cmd = bytearray(self._write_byte) cmd.append(byte) - self._send_check_ack(cmd, is_do_prolog_cmd=False) + self._send_check_ack(cmd) From 5ad09becdc4395d62acc91aaf22db437a95c7870 Mon Sep 17 00:00:00 2001 From: Sebastian Zwietz Date: Thu, 5 May 2022 21:54:39 +0200 Subject: [PATCH 04/62] Fix start address of CRC word (16 bit) when checking the remaining space for USB descriptor strings --- pyftdi/eeprom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index e2d96fa6..b0b8b368 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -717,7 +717,7 @@ def _generate_var_strings(self, fill=True) -> None: self._eeprom[s1_vstr_start:s1_vstr_start+len(stream)] = stream self._eeprom[dynpos:dynpos+len(stream)] = stream mtp = self._ftdi.device_version == 0x1000 - crc_pos = 0x100 if mtp else self._size + crc_pos = ( 0x100 if mtp else self._size ) - 2 rem = crc_pos - (dynpos + len(stream)) if rem < 0: oversize = (-rem + 2) // 2 From 051ccb2d087fe43e92b9d688cf81649276db1d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Niew=C3=B6hner?= Date: Sun, 14 Aug 2022 12:11:54 +0200 Subject: [PATCH 05/62] i2c: relax the bus after epilog in fake-tristate mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set lines to high-z after epilog when fake-tristate is enabled Similiar to issue #46, FT2232H/FT4232H never *really* relax the bus after the epilog, because they don't support tristate in hardware. This leads to blocking the bus when another master (besides the FTDI) is present. Implement fake-tristate for both SCL and SDA after the epilog to unblock the bus. Signed-off-by: Michael Niewöhner --- pyftdi/i2c.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index 563a59bc..aebf0b6e 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -965,6 +965,12 @@ def _clk_lo_data_lo(self) -> Tuple[int]: self._gpio_low, self.I2C_DIR | (self._gpio_dir & 0xFF)) + @property + def _clk_input_data_input(self) -> Tuple[int]: + return (Ftdi.SET_BITS_LOW, + self._gpio_low, + (self._gpio_dir & 0xFF)) + @property def _idle(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, @@ -1037,6 +1043,9 @@ def _do_prolog(self, i2caddress: int) -> None: def _do_epilog(self) -> None: self.log.debug(' epilog') cmd = bytearray(self._stop) + if self._fake_tristate: + # SCL high-Z, SDA high-Z + cmd.extend(self._clk_input_data_input) self._ftdi.write_data(cmd) # be sure to purge the MPSSE reply self._ftdi.read_data_bytes(1, 1) From 73dfd82d781c1143d8aa8070feea917511b664a7 Mon Sep 17 00:00:00 2001 From: Taisuke Yamada Date: Sun, 6 Nov 2022 21:06:15 +0900 Subject: [PATCH 06/62] Added -F option to force clock mode (needed to force enable I2C on FT2232D). --- pyftdi/bin/i2cscan.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyftdi/bin/i2cscan.py b/pyftdi/bin/i2cscan.py index 3ef681b5..7d6ebfc3 100755 --- a/pyftdi/bin/i2cscan.py +++ b/pyftdi/bin/i2cscan.py @@ -33,7 +33,7 @@ class I2cBusScanner: HIGHEST_I2C_SLAVE_ADDRESS = 0x78 @classmethod - def scan(cls, url: str, smb_mode: bool = True) -> None: + def scan(cls, url: str, smb_mode: bool = True, force: bool = False) -> None: """Scan an I2C bus to detect slave device. :param url: FTDI URL @@ -45,6 +45,7 @@ def scan(cls, url: str, smb_mode: bool = True) -> None: getLogger('pyftdi.i2c').setLevel(ERROR) try: i2c.set_retry_count(1) + i2c.force_clock_mode(force) i2c.configure(url) for addr in range(cls.HIGHEST_I2C_SLAVE_ADDRESS+1): port = i2c.get_port(addr) @@ -102,6 +103,8 @@ def main(): help='increase verbosity') argparser.add_argument('-d', '--debug', action='store_true', help='enable debug mode') + argparser.add_argument('-F', '--force', action='store_true', + help='force clock mode (for FT2232D)') args = argparser.parse_args() debug = args.debug @@ -134,7 +137,7 @@ def main(): except ValueError as exc: argparser.error(str(exc)) - I2cBusScanner.scan(args.device, not args.no_smb) + I2cBusScanner.scan(args.device, not args.no_smb, args.force) except (ImportError, IOError, NotImplementedError, ValueError) as exc: print('\nError: %s' % exc, file=stderr) From 5fab92accd155b7be4690f38b9933fa4ed332049 Mon Sep 17 00:00:00 2001 From: Davide Toldo Date: Tue, 25 Apr 2023 15:49:43 +0200 Subject: [PATCH 07/62] Fix confusing typo --- pyftdi/doc/installation.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/doc/installation.rst b/pyftdi/doc/installation.rst index 942dbd96..e864e8fe 100644 --- a/pyftdi/doc/installation.rst +++ b/pyftdi/doc/installation.rst @@ -197,7 +197,7 @@ Open a *shell*, or a *CMD* on Windows should list all the FTDI devices available on your host. -Alternatively, you can invoke ``ftdu_urls.py`` script that lists all detected +Alternatively, you can invoke ``ftdi_urls.py`` script that lists all detected FTDI devices. See the :doc:`tools` chapter for details. * Example with 1 FT232H device with a serial number and 1 FT2232 device From 3c0e8d1d20b6e2881ab33a1298f338f221075288 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 5 Aug 2023 23:04:23 +0200 Subject: [PATCH 08/62] Update setup syntax --- .github/workflows/pythonchecksyntax.yml | 2 +- LICENSE | 2 +- pyftdi/__init__.py | 6 +-- pyftdi/ftdi.py | 2 +- pyftdi/serialext/logger.py | 2 +- setup.cfg | 2 +- setup.py | 53 ++++++++++--------------- test-requirements.txt | 3 ++ 8 files changed, 32 insertions(+), 40 deletions(-) create mode 100644 test-requirements.txt diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 93e81e5f..87ef7e95 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -21,7 +21,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install setuptools wheel ruamel.yaml + pip install -r test-requirements.txt - name: Check style run: | python setup.py check_style diff --git a/LICENSE b/LICENSE index ab67d8cf..89cfb454 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2008-2021 Emmanuel Blot +Copyright (c) 2008-2023 Emmanuel Blot All Rights Reserved. SPDX-License-Identifier: BSD-3-Clause diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 7433e95e..9551577e 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2022 Emmanuel Blot +# Copyright (c) 2010-2023 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # @@ -6,7 +6,7 @@ #pylint: disable-msg=missing-docstring -__version__ = '0.54.0' +__version__ = '0.54.1' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' @@ -15,7 +15,7 @@ # For all support requests, please open a new issue on GitHub __email__ = 'emmanuel.blot@free.fr' __license__ = 'Modified BSD' -__copyright__ = 'Copyright (c) 2011-2021 Emmanuel Blot' +__copyright__ = 'Copyright (c) 2011-2023 Emmanuel Blot' from logging import WARNING, NullHandler, getLogger diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index c0e3d768..d78a0244 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2020 Emmanuel Blot +# Copyright (c) 2010-2020 Emmanuel Blot # Copyright (c) 2016 Emmanuel Bouaziz # All rights reserved. # diff --git a/pyftdi/serialext/logger.py b/pyftdi/serialext/logger.py index e48c2eaa..6736c7f7 100644 --- a/pyftdi/serialext/logger.py +++ b/pyftdi/serialext/logger.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2016 Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # diff --git a/setup.cfg b/setup.cfg index fd242be7..28e40d98 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [bdist_wheel] [metadata] -license_file = pyftdi/doc/license.rst +license_files = pyftdi/doc/license.rst [build_sphinx] source-dir = pyftdi/doc diff --git a/setup.py b/setup.py index 2b3221a5..6075fc2d 100755 --- a/setup.py +++ b/setup.py @@ -1,28 +1,25 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2021 Emmanuel Blot +# Copyright (c) 2010-2023 Emmanuel Blot # Copyright (c) 2010-2016 Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=unused-variable -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=broad-except -#pylint: disable-msg=no-self-use +#pylint: disable=unused-variable +#pylint: disable=missing-docstring +#pylint: disable=broad-except from codecs import open as codec_open -from setuptools import find_packages, setup -from setuptools.command.build_py import build_py -from distutils.cmd import Command -from distutils.log import DEBUG, INFO from os import close, getcwd, unlink, walk from os.path import abspath, dirname, join as joinpath, relpath from py_compile import compile as pycompile, PyCompileError from re import split as resplit, search as research from sys import stderr, exit as sysexit from tempfile import mkstemp +from setuptools import Command, find_packages, setup +from setuptools.command.build_py import build_py NAME = 'pyftdi' @@ -38,10 +35,10 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Hardware :: Hardware Drivers', ] @@ -49,9 +46,6 @@ 'pyusb >= 1.0.0, != 1.2.0', 'pyserial >= 3.0', ] -TEST_REQUIRES = [ - 'ruamel.yaml >= 0.16', -] HERE = abspath(dirname(__file__)) @@ -80,13 +74,10 @@ def find_meta(meta): """ Extract __*meta*__ from META_FILE. """ - meta_match = research( - r"(?m)^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta), - META_FILE - ) + meta_match = research(rf"(?m)^__{meta}__ = ['\"]([^'\"]*)['\"]", META_FILE) if meta_match: return meta_match.group(1) - raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta)) + raise RuntimeError(f'Unable to find __{meta}__ string.') class BuildPy(build_py): @@ -118,7 +109,7 @@ def byte_compile(self, files): except PyCompileError as exc: # avoid chaining exceptions print(str(exc), file=stderr) - raise SyntaxError("Cannot byte-compile '%s'" % file) + raise SyntaxError(f"Cannot byte-compile '{file}'") from exc finally: unlink(pyc) super().byte_compile(files) @@ -137,7 +128,7 @@ def finalize_options(self): pass def run(self): - self.announce('checking coding style', level=INFO) + self.announce('checking coding style', level=2) filecount = 0 topdir = dirname(__file__) or getcwd() for dpath, dnames, fnames in walk(topdir): @@ -145,18 +136,17 @@ def run(self): if not d.startswith('.') and d != 'doc'] for filename in (joinpath(dpath, f) for f in fnames if f.endswith('.py')): - self.announce('checking %s' % relpath(filename, topdir), - level=INFO) - with open(filename, 'rt') as pfp: + self.announce(f'checking {relpath(filename, topdir)}', level=2) + with open(filename, 'rt', encoding='utf-8') as pfp: for lpos, line in enumerate(pfp, start=1): if len(line) > 80: - print('\n %d: %s' % (lpos, line.rstrip())) - raise RuntimeError("Invalid line width '%s'" % - relpath(filename, topdir)) + print(f'\n {lpos}: {line.rstrip()}') + toppath = relpath(filename, topdir) + raise RuntimeError(f"Invalid line width in " + f"{toppath}:{lpos}") filecount += 1 if not filecount: - raise RuntimeError('No Python file found from "%s"' % - topdir) + raise RuntimeError(f'No Python file found from "{topdir}"') def main(): @@ -187,14 +177,13 @@ def main(): 'pyftdi.serialext': ['*.rst', 'doc/api/uart.rst']}, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, - test_requires=TEST_REQUIRES, - python_requires='>=3.7', + python_requires='>=3.8', ) if __name__ == '__main__': try: main() - except Exception as exc: - print(exc, file=stderr) + except Exception as exc_: + print(exc_, file=stderr) sysexit(1) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..2031f04f --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +ruamel.yaml >= 0.16 +setuptools +wheel From 8654b6f6bdc9861b9a9d29bcdfbf3fdb2a3d6158 Mon Sep 17 00:00:00 2001 From: Kornel Swierzy Date: Sat, 5 Aug 2023 23:21:06 +0200 Subject: [PATCH 09/62] Support for FTDI FT4232HA added. --- README.md | 1 + pyftdi/__init__.py | 2 +- pyftdi/doc/api/i2c.rst | 18 +++++----- pyftdi/doc/api/spi.rst | 2 +- pyftdi/doc/authors.rst | 2 ++ pyftdi/doc/defs.rst | 25 ++++++------- pyftdi/doc/eeprom.rst | 20 +++++------ pyftdi/doc/features.rst | 10 +++--- pyftdi/doc/gpio.rst | 4 +-- pyftdi/doc/index.rst | 1 + pyftdi/doc/installation.rst | 2 ++ pyftdi/doc/pinout.rst | 14 ++++---- pyftdi/doc/urlscheme.rst | 3 +- pyftdi/eeprom.py | 13 +++---- pyftdi/ftdi.py | 54 +++++++++++++++------------- pyftdi/tests/backend/ftdivirt.py | 23 ++++++------ pyftdi/tests/gpio.py | 5 +-- pyftdi/tests/i2c.py | 4 +-- pyftdi/tests/mockusb.py | 23 +++++++----- pyftdi/tests/resources/ft4232ha.yaml | 16 +++++++++ pyftdi/tests/resources/ftmany.yaml | 11 ++++++ pyftdi/tracer.py | 5 +-- pyftdi/usbtools.py | 8 ++--- 23 files changed, 159 insertions(+), 107 deletions(-) create mode 100644 pyftdi/tests/resources/ft4232ha.yaml diff --git a/README.md b/README.md index a6637e15..12797740 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Suported FTDI devices include: * FT232H (single port, clock up to 30 MHz) * FT2232H (dual port, clock up to 30 MHz) * FT4232H (quad port, clock up to 30 MHz) + * FT4232HA (quad port, clock up to 30 MHz) ## Features diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 9551577e..3a350649 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -6,7 +6,7 @@ #pylint: disable-msg=missing-docstring -__version__ = '0.54.1' +__version__ = '0.55.0' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' diff --git a/pyftdi/doc/api/i2c.rst b/pyftdi/doc/api/i2c.rst index 838a7e5b..3c28e44b 100644 --- a/pyftdi/doc/api/i2c.rst +++ b/pyftdi/doc/api/i2c.rst @@ -132,8 +132,8 @@ Fortunately, FT232H device is fitted with real open collector outputs, and PyFtdi always enable this mode on SCL and SDA lines when a FT232H device is used. -Other FTDI devices such as FT2232H and FT4232H do not support open collector -mode, and source current to SCL and SDA lines. +Other FTDI devices such as FT2232H, FT4232H and FT4232HA do not support open +collector mode, and source current to SCL and SDA lines. Clock streching ``````````````` @@ -142,11 +142,11 @@ Clock stretching is supported through a hack that re-uses the JTAG adaptative clock mode designed for ARM devices. FTDI HW drives SCL on ``AD0`` (`BD0`), and samples the SCL line on : the 8\ :sup:`th` pin of a port ``AD7`` (``BD7``). -When a FTDI device without an open collector capability is used (FT2232H, -FT4232H) the current sourced from AD0 may prevent proper sampling of the SCL -line when the slave attempts to strech the clock. It is therefore recommended -to add a low forward voltage drop diode to `AD0` to prevent AD0 to source -current to the SCL bus. See the wiring section. +When a FTDI device without an open collector capability is used +(FT2232H, FT4232H, FT4232HA) the current sourced from AD0 may prevent proper +sampling ofthe SCL line when the slave attempts to strech the clock. It is +therefore recommended to add a low forward voltage drop diode to `AD0` to +prevent AD0 to source current to the SCL bus. See the wiring section. Speed ````` @@ -165,7 +165,7 @@ Use of PyFtdi_ should nevetherless carefully studied and is not recommended if you need to achieve medium to high speed write operations with a slave (relative to the I2C clock...). Dedicated I2C master such as FT4222H device is likely a better option, but is not currently supported with PyFtdi_ as it uses -a different communication protocol. +a different communication protocol. .. _i2c_wiring: @@ -187,5 +187,5 @@ Wiring *Fig.1*: * ``D1`` is only required when clock streching is used along with - FT2232H or FT4232H devices. It should not be fit with an FT232H. + FT2232H, FT4232H or FT4232HA devices. It should not be fit with an FT232H. * ``AD7`` may be used as a regular GPIO with clock stretching is not required. diff --git a/pyftdi/doc/api/spi.rst b/pyftdi/doc/api/spi.rst index 8c350ad8..3e40306c 100644 --- a/pyftdi/doc/api/spi.rst +++ b/pyftdi/doc/api/spi.rst @@ -158,7 +158,7 @@ Application Node 114: Support for mode 1 and mode 3 is implemented with some workarounds, but generated signals may not be reliable: YMMV. It is only available with -H -series (232H, 2232H, 4232H). +series (232H, 2232H, 4232H, 4232HA). The 3-clock phase mode which has initially be designed to cope with |I2C| signalling is used to delay the data lines from the clock signals. A direct diff --git a/pyftdi/doc/authors.rst b/pyftdi/doc/authors.rst index 27dca97e..3d65d6a3 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -44,3 +44,5 @@ Contributors * Amanita-muscaria * len0rd * Rod Whitby + * Kornel Swierzy + diff --git a/pyftdi/doc/defs.rst b/pyftdi/doc/defs.rst index 052f42c6..920a52da 100644 --- a/pyftdi/doc/defs.rst +++ b/pyftdi/doc/defs.rst @@ -1,26 +1,27 @@ .. |I2C| replace:: I\ :sup:`2`\ C -.. _FT232R: http://www.ftdichip.com/Products/ICs/FT232R.htm -.. _FT230X: http://www.ftdichip.com/Products/ICs/FT230X.html -.. _FT2232D: http://www.ftdichip.com/Products/ICs/FT2232D.htm -.. _FT232H: http://www.ftdichip.com/Products/ICs/FT232H.htm -.. _FT2232H: http://www.ftdichip.com/Products/ICs/FT2232H.html -.. _FT4232H: http://www.ftdichip.com/Products/ICs/FT4232H.htm -.. _FTDI_Recovery: http://www.ftdichip.com/Support/Documents/AppNotes/AN_136%20Hi%20Speed%20Mini%20Module%20EEPROM%20Disaster%20Recovery.pdf +.. _FT232R: https://www.ftdichip.com/Products/ICs/FT232R.htm +.. _FT230X: https://www.ftdichip.com/Products/ICs/FT230X.html +.. _FT2232D: https://www.ftdichip.com/Products/ICs/FT2232D.htm +.. _FT232H: https://www.ftdichip.com/Products/ICs/FT232H.htm +.. _FT2232H: https://www.ftdichip.com/Products/ICs/FT2232H.html +.. _FT4232H: https://www.ftdichip.com/Products/ICs/FT4232H.htm +.. _FT4232HA: http://ftdichip.com/products/ft4232haq/ +.. _FTDI_Recovery: https://www.ftdichip.com/Support/Documents/AppNotes/AN_136%20Hi%20Speed%20Mini%20Module%20EEPROM%20Disaster%20Recovery.pdf .. _PyFtdi: https://www.github.com/eblot/pyftdi .. _PyFtdiTools: https://github.com/eblot/pyftdi/tree/master/pyftdi/bin -.. _FTDI: http://www.ftdichip.com/ -.. _PyUSB: http://pyusb.github.io/pyusb/ +.. _FTDI: https://www.ftdichip.com/ +.. _PyUSB: https://pyusb.github.io/pyusb/ .. _Python: https://www.python.org/ .. _pyserial: https://pythonhosted.org/pyserial/ .. _libftdi: https://www.intra2net.com/en/developer/libftdi/ .. _pyspiflash: https://github.com/eblot/pyspiflash/ .. _pyi2cflash: https://github.com/eblot/pyi2cflash/ -.. _libusb: http://www.libusb.info/ +.. _libusb: https://www.libusb.info/ .. _Libusb on Windows: https://github.com/libusb/libusb/wiki/Windows .. _Libusb win32: https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/ -.. _Zadig: http://zadig.akeo.ie/ -.. _FTDI macOS guide: http://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf +.. _Zadig: https://zadig.akeo.ie/ +.. _FTDI macOS guide: https://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf .. _Libusb issue on macOs: https://github.com/libusb/libusb/commit/5e45e0741daee4fa295c6cc977edfb986c872152 .. _FT_PROG: https://www.ftdichip.com/Support/Utilities.htm#FT_PROG .. _fstring: https://www.python.org/dev/peps/pep-0498 diff --git a/pyftdi/doc/eeprom.rst b/pyftdi/doc/eeprom.rst index 3a31dbfa..dcda1448 100644 --- a/pyftdi/doc/eeprom.rst +++ b/pyftdi/doc/eeprom.rst @@ -44,15 +44,15 @@ EEPROM from the command line. See the :ref:`tools` chapter to locate this tool. [-s SERIAL_NUMBER] [-m MANUFACTURER] [-p PRODUCT] [-c CONFIG] [--vid VID] [--pid PID] [-e] [-E] [-u] [-v] [-d] [device] - + Simple FTDI EEPROM configurator. - + positional arguments: device serial port device name - + optional arguments: -h, --help show this help message and exit - + Files: -i INPUT, --input INPUT input ini file to load EEPROM content @@ -62,7 +62,7 @@ EEPROM from the command line. See the :ref:`tools` chapter to locate this tool. output ini file to save EEPROM content -V VIRTUAL, --virtual VIRTUAL use a virtual device, specified as YaML - + Device: -P VIDPID, --vidpid VIDPID specify a custom VID:PID device ID (search for FTDI devices) @@ -70,12 +70,12 @@ EEPROM from the command line. See the :ref:`tools` chapter to locate this tool. force an EEPROM model -S {128,256,1024}, --size {128,256,1024} force an EEPROM size - + Format: -x, --hexdump dump EEPROM content as ASCII -X HEXBLOCK, --hexblock HEXBLOCK dump EEPROM as indented hexa blocks - + Configuration: -s SERIAL_NUMBER, --serial-number SERIAL_NUMBER set serial number @@ -87,12 +87,12 @@ EEPROM from the command line. See the :ref:`tools` chapter to locate this tool. change/configure a property as key=value pair --vid VID shortcut to configure the USB vendor ID --pid PID shortcut to configure the USB product ID - + Action: -e, --erase erase the whole EEPROM content -E, --full-erase erase the whole EEPROM content, including the CRC -u, --update perform actual update, use w/ care - + Extras: -v, --verbose increase verbosity -d, --debug enable debug mode @@ -372,7 +372,7 @@ Examples TIME_STAMP, TRISTATE, TXDEN, TXLED, TXRXLED, VBUS_SENSE * Erase the whole EEPROM including its CRC. - + Once power cycle, the device should run as if no EEPROM was connected. Do not use this with internal, embedded EEPROMs such as FT230X. diff --git a/pyftdi/doc/features.rst b/pyftdi/doc/features.rst index 36094472..8080823b 100644 --- a/pyftdi/doc/features.rst +++ b/pyftdi/doc/features.rst @@ -34,14 +34,14 @@ SPI master Supported devices: -===== ===== ====== ==================================================== +===== ===== ====== ============================================================ Mode CPol CPha Status -===== ===== ====== ==================================================== +===== ===== ====== ============================================================ 0 0 0 Supported on all MPSSE devices 1 0 1 Workaround available for on -H series - 2 1 0 Supported on -H series (FT232H_/FT2232H_/FT4232H_) + 2 1 0 Supported on -H series (FT232H_/FT2232H_/FT4232H_/FT4232HA_) 3 1 1 Workaround available for on -H series -===== ===== ====== ==================================================== +===== ===== ====== ============================================================ PyFtdi_ can be used with pyspiflash_ module that demonstrates how to use the FTDI SPI master with a pure-Python serial flash device driver for @@ -61,7 +61,7 @@ Note: FTDI*232* devices cannot be used as an SPI slave. |I2C| master ```````````` -Supported devices: FT232H_, FT2232H_, FT4232H_ +Supported devices: FT232H_, FT2232H_, FT4232H_, FT4232HA_ For now, only 7-bit addresses are supported. diff --git a/pyftdi/doc/gpio.rst b/pyftdi/doc/gpio.rst index 4fdf0906..e2085cdc 100644 --- a/pyftdi/doc/gpio.rst +++ b/pyftdi/doc/gpio.rst @@ -76,8 +76,8 @@ the actual hardware, *i.e.* the FTDI model: `BDBUS/BCBUS`, * FT2232H features two ports, which are 16-bit wide each: `ADBUS/ACBUS` and `BDBUS/BCBUS`, -* FT4232H features four ports, which are 8-bit wide each: `ADBUS`, `BDBUS`, - `CDBUS` and `DDBUS`, +* FT4232H/FT4232HA features four ports, which are 8-bit wide each: `ADBUS`, + `BDBUS`, `CDBUS` and `DDBUS`, * FT230X features a single port, which is 4-bit wide, * FT231X feature a single port, which is 8-bit wide diff --git a/pyftdi/doc/index.rst b/pyftdi/doc/index.rst index 618755aa..689040bf 100644 --- a/pyftdi/doc/index.rst +++ b/pyftdi/doc/index.rst @@ -45,6 +45,7 @@ Supported FTDI devices include: * FT232H (single port, clock up to 30 MHz) * FT2232H (dual port, clock up to 30 MHz) * FT4232H (quad port, clock up to 30 MHz) + * FT4232HA (quad port, clock up to 30 MHz) Features -------- diff --git a/pyftdi/doc/installation.rst b/pyftdi/doc/installation.rst index e864e8fe..ad24212e 100644 --- a/pyftdi/doc/installation.rst +++ b/pyftdi/doc/installation.rst @@ -38,6 +38,8 @@ configure `udev`, here is a typical setup: SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6014", GROUP="plugdev", MODE="0664" # FT230X/FT231X/FT234X SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6015", GROUP="plugdev", MODE="0664" + # FT4232HA + SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6048", GROUP="plugdev", MODE="0664" .. note:: **Accessing FTDI devices with custom VID/PID** diff --git a/pyftdi/doc/pinout.rst b/pyftdi/doc/pinout.rst index 71a145c5..44a3fea9 100644 --- a/pyftdi/doc/pinout.rst +++ b/pyftdi/doc/pinout.rst @@ -3,9 +3,9 @@ FTDI device pinout ------------------ -============ ============= ======= ====== ============== ========== ====== ============ +============ ============= ======= ====== ============== ========== ====== ============= IF/1 [#ih]_ IF/2 [#if2]_ BitBang UART |I2C| SPI JTAG C232HD cable -============ ============= ======= ====== ============== ========== ====== ============ +============ ============= ======= ====== ============== ========== ====== ============= ``ADBUS0`` ``BDBUS0`` GPIO0 TxD SCK SCLK TCK Orange ``ADBUS1`` ``BDBUS1`` GPIO1 RxD SDA/O [#i2c]_ MOSI TDI Yellow ``ADBUS2`` ``BDBUS2`` GPIO2 RTS SDA/I [#i2c]_ MISO TDO Green @@ -22,7 +22,7 @@ FTDI device pinout ``ACBUS5`` ``BCBUS5`` GPIO13 GPIO13 ``ACBUS6`` ``BCBUS6`` GPIO14 GPIO14 ``ACBUS7`` ``BCBUS7`` GPIO15 GPIO15 -============ ============= ======= ====== ============== ========== ====== ============ +============ ============= ======= ====== ============== ========== ====== ============= .. [#ih] 16-bit port (ACBUS, BCBUS) is not available with FT4232H_ series, and FTDI2232C/D only support 12-bit ports. @@ -30,10 +30,10 @@ FTDI device pinout is bi-directional, two FTDI pins are required to provide the SDA feature, and they should be connected together and to the SDA |I2C| bus line. Pull-up resistors on SCK and SDA lines should be used. -.. [#if2] FT232H_ does not support a secondary MPSSE port, only FT2232H_ and - FT4232H_ do. Note that FT4232H_ has 4 serial ports, but only the - first two interfaces are MPSSE-capable. C232HD cable only exposes - IF/1 (ADBUS). +.. [#if2] FT232H_ does not support a secondary MPSSE port, only FT2232H_, + FT4232H_ and FT4232HA_ do. Note that FT4232H_/FT4232HA_ has 4 serial + ports, but only the first two interfaces are MPSSE-capable. C232HD + cable only exposes IF/1 (ADBUS). .. [#rck] In order to support I2C clock stretch mode, ADBUS7 should be connected to SCK. When clock stretching mode is not selected, ADBUS7 may be used as GPIO7. \ No newline at end of file diff --git a/pyftdi/doc/urlscheme.rst b/pyftdi/doc/urlscheme.rst index 6a03eb5e..a4e1f68e 100644 --- a/pyftdi/doc/urlscheme.rst +++ b/pyftdi/doc/urlscheme.rst @@ -27,7 +27,8 @@ where: ``0x6015`` * Supported product aliases: - * ``232``, ``232r``, ``232h``, ``2232d``, ``2232h``, ``4232h``, ``230x`` + * ``232``, ``232r``, ``232h``, ``2232d``, ``2232h``, ``4232h``, ``4232ha``, + ``230x`` * ``ft`` prefix for all aliases is also accepted, as for example ``ft232h`` * ``serial``: the serial number as a string. This is the preferred method to diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index e2d96fa6..1bb0759f 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2022, Emmanuel Blot +# Copyright (c) 2019-2023, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -70,6 +70,7 @@ class FtdiEeprom: 0x0800: _PROPS(256, 0x1A, 0x9A, 0x18), # FT4232H 0x0900: _PROPS(256, 0x1A, 0xA0, 0x1e), # FT232H 0x1000: _PROPS(1024, 0x1A, 0xA0, None), # FT230X/FT231X/FT234X + 0x3600: _PROPS(256, 0x1A, 0x9A, 0x18), # FT4232HA } """EEPROM properties.""" @@ -634,7 +635,7 @@ def commit(self, dry_run: bool = True, no_crc: bool = False) -> bool: the EEPROM content :param no_crc: do not compute EEPROM CRC. This should only be used to perform a full erasure of the EEPROM, as an attempt to recover - from a corrupted config. + from a corrupted config. :return: True if some changes have been committed to the EEPROM """ @@ -778,7 +779,7 @@ def _update_crc(self): crc_s1_start = self.mirror_sector - crc_size self._eeprom[crc_s1_start:crc_s1_start+crc_size] = spack(' Tuple[int, bool]: """ :return: Tuple of: @@ -937,7 +938,7 @@ def _set_bus_control(self, bus: str, control: str, def _set_group(self, group: int, control: str, value: Union[str, int, bool], out: Optional[TextIO]) \ -> None: - if self.device_version in (0x0700, 0x0800, 0x0900): + if self.device_version in (0x0700, 0x0800, 0x0900, 0x3600): self._set_group_x232h(group, control, value, out) return raise ValueError('Group not implemented for this device') @@ -951,7 +952,7 @@ def _set_bus_control_230x(self, bus: str, control: str, def _set_group_x232h(self, group: int, control: str, value: str, out: Optional[TextIO]) -> None: - if self.device_version in (0x0700, 0x800): # 2232H/4232H + if self.device_version in (0x0700, 0x800, 0x3600): # 2232H/4232H/4232HA offset = 0x0c + group//2 nibble = group & 1 else: # 232H @@ -1121,7 +1122,7 @@ def _decode_4232h(self): cfg['channel_%x_rs485' % (0xa+chix)] = bool(conf & (rs485 << chix)) def _decode_x232h(self, cfg): - # common code for 2232h and 4232h + # common code for 2232h, 4232h, 4232ha cfg0, cfg1 = self._eeprom[0x00], self._eeprom[0x01] cfg['channel_a_driver'] = 'VCP' if (cfg0 & (1 << 3)) else 'D2XX' cfg['channel_b_driver'] = 'VCP' if (cfg1 & (1 << 3)) else 'D2XX' diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index d78a0244..c6e147b6 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2020 Emmanuel Blot +# Copyright (c) 2010-2023 Emmanuel Blot # Copyright (c) 2016 Emmanuel Bouaziz # All rights reserved. # @@ -81,6 +81,7 @@ class Ftdi: ('230x', 0x6015), ('231x', 0x6015), ('234x', 0x6015), + ('4232ha', 0x6048), ('ft232', 0x6001), ('ft232r', 0x6001), ('ft232h', 0x6014), @@ -92,7 +93,8 @@ class Ftdi: ('ft4232h', 0x6011), ('ft230x', 0x6015), ('ft231x', 0x6015), - ('ft234x', 0x6015))) + ('ft234x', 0x6015), + ('ft4232ha', 0x6048))) } """Supported products, only FTDI officials ones. To add third parties and customized products, see @@ -110,20 +112,22 @@ class Ftdi: 0x0700: 'ft2232h', 0x0800: 'ft4232h', 0x0900: 'ft232h', - 0x1000: 'ft-x'} + 0x1000: 'ft-x', + 0x3600: 'ft4232ha'} """Common names of FTDI supported devices.""" # Note that the FTDI datasheets contradict themselves, so # the following values may not be the right ones... FIFO_SIZES = { - 0x0200: (128, 128), # FT232AM: TX: 128, RX: 128 - 0x0400: (128, 384), # FT232BM: TX: 128, RX: 384 - 0x0500: (128, 384), # FT2232C: TX: 128, RX: 384 - 0x0600: (256, 128), # FT232R: TX: 256, RX: 128 - 0x0700: (4096, 4096), # FT2232H: TX: 4KiB, RX: 4KiB - 0x0800: (2048, 2048), # FT4232H: TX: 2KiB, RX: 2KiB - 0x0900: (1024, 1024), # FT232H: TX: 1KiB, RX: 1KiB - 0x1000: (512, 512), # FT-X: TX: 512, RX: 512 + 0x0200: (128, 128), # FT232AM: TX: 128, RX: 128 + 0x0400: (128, 384), # FT232BM: TX: 128, RX: 384 + 0x0500: (128, 384), # FT2232C: TX: 128, RX: 384 + 0x0600: (256, 128), # FT232R: TX: 256, RX: 128 + 0x0700: (4096, 4096), # FT2232H: TX: 4KiB, RX: 4KiB + 0x0800: (2048, 2048), # FT4232H: TX: 2KiB, RX: 2KiB + 0x0900: (1024, 1024), # FT232H: TX: 1KiB, RX: 1KiB + 0x1000: (512, 512), # FT-X: TX: 512, RX: 512 + 0x3600: (2048, 2048), # FT4232HA: TX: 2KiB, RX: 2KiB } """FTDI chip internal FIFO sizes @@ -508,9 +512,9 @@ def open(self, vendor: int, product: int, bus: Optional[int] = None, the USB device in random order. serial argument is more reliable selector and should always be prefered. - Some FTDI devices support several interfaces/ports (such as FT2232H - and FT4232H). The interface argument selects the FTDI port to use, - starting from 1 (not 0). + Some FTDI devices support several interfaces/ports (such as FT2232H, + FT4232H and FT4232HA). The interface argument selects the FTDI port + to use, starting from 1 (not 0). :param int vendor: USB vendor id :param int product: USB product id @@ -654,10 +658,10 @@ def open_mpsse(self, vendor: int, product: int, bus: Optional[int] = None, the USB device in random order. serial argument is more reliable selector and should always be prefered. - Some FTDI devices support several interfaces/ports (such as FT2232H - and FT4232H). The interface argument selects the FTDI port to use, - starting from 1 (not 0). Note that not all FTDI ports are MPSSE - capable. + Some FTDI devices support several interfaces/ports (such as FT2232H, + FT4232H and FT4232HA). The interface argument selects the FTDI port + to use, starting from 1 (not 0). Note that not all FTDI ports are + MPSSE capable. :param vendor: USB vendor id :param product: USB product id @@ -707,10 +711,10 @@ def open_mpsse_from_device(self, device: UsbDevice, the USB device in random order. serial argument is more reliable selector and should always be prefered. - Some FTDI devices support several interfaces/ports (such as FT2232H - and FT4232H). The interface argument selects the FTDI port to use, - starting from 1 (not 0). Note that not all FTDI ports are MPSSE - capable. + Some FTDI devices support several interfaces/ports (such as FT2232H, + FT4232H and FT4232HA). The interface argument selects the FTDI port + to use, starting from 1 (not 0). Note that not all FTDI ports are + MPSSE capable. :param device: FTDI USB device :param interface: FTDI interface/port @@ -945,7 +949,7 @@ def has_mpsse(self) -> bool: """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') - return self.device_version in (0x0500, 0x0700, 0x0800, 0x0900) + return self.device_version in (0x0500, 0x0700, 0x0800, 0x0900, 0x3600) @property def has_wide_port(self) -> bool: @@ -1008,7 +1012,7 @@ def is_H_series(self) -> bool: """ if not self.is_connected: raise FtdiError('Device characteristics not yet known') - return self.device_version in (0x0700, 0x0800, 0x0900) + return self.device_version in (0x0700, 0x0800, 0x0900, 0x3600) @property @@ -1029,6 +1033,8 @@ def is_mpsse_interface(self, interface: int) -> bool: return False if self.device_version == 0x0800 and interface > 2: return False + if self.device_version == 0x3600 and interface > 2: + return False return True @property diff --git a/pyftdi/tests/backend/ftdivirt.py b/pyftdi/tests/backend/ftdivirt.py index 4797e50e..13875bb6 100644 --- a/pyftdi/tests/backend/ftdivirt.py +++ b/pyftdi/tests/backend/ftdivirt.py @@ -1,6 +1,6 @@ """PyUSB virtual FTDI device.""" -# Copyright (c) 2020-2021, Emmanuel Blot +# Copyright (c) 2020-2023, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -230,14 +230,15 @@ class BitMode(IntEnum): SYNCFF = 0x40 # Single Channel Synchronous FIFO mode FIFO_SIZES = { - 0x0200: (128, 128), # FT232AM: TX: 128, RX: 128 - 0x0400: (128, 384), # FT232BM: TX: 128, RX: 384 - 0x0500: (128, 384), # FT2232C: TX: 128, RX: 384 - 0x0600: (256, 128), # FT232R: TX: 256, RX: 128 - 0x0700: (4096, 4096), # FT2232H: TX: 4KiB, RX: 4KiB - 0x0800: (2048, 2048), # FT4232H: TX: 2KiB, RX: 2KiB - 0x0900: (1024, 1024), # FT232H: TX: 1KiB, RX: 1KiB - 0x1000: (512, 512), # FT-X: TX: 512, RX: 512 + 0x0200: (128, 128), # FT232AM: TX: 128, RX: 128 + 0x0400: (128, 384), # FT232BM: TX: 128, RX: 384 + 0x0500: (128, 384), # FT2232C: TX: 128, RX: 384 + 0x0600: (256, 128), # FT232R: TX: 256, RX: 128 + 0x0700: (4096, 4096), # FT2232H: TX: 4KiB, RX: 4KiB + 0x0800: (2048, 2048), # FT4232H: TX: 2KiB, RX: 2KiB + 0x0900: (1024, 1024), # FT232H: TX: 1KiB, RX: 1KiB + 0x1000: (512, 512), # FT-X: TX: 512, RX: 512 + 0x3600: (2048, 2048), # FT4232HA: TX: 2KiB, RX: 2KiB } """FTDI chip internal FIFO sizes. @@ -252,7 +253,8 @@ class BitMode(IntEnum): 0x0700: 16, 0x0800: 8, 0x0900: 16, - 0x1000: 8} + 0x1000: 8, + 0x3600: 8} """Interterface pin count.""" UART_PINS = IntEnum('UartPins', 'TXD RXD RTS CTS DTR DSR DCD RI', start=0) @@ -972,6 +974,7 @@ class Properties(NamedTuple): 0x0800: Properties(4, 8, 0), # FT4232H 0x0900: Properties(1, 8, 10), # FT232H 0x1000: Properties(1, 8, 4), # FT231X + 0x3600: Properties(4, 8, 0), # FT4232HA } """Width of port/bus (regular, cbus).""" diff --git a/pyftdi/tests/gpio.py b/pyftdi/tests/gpio.py index 3c1ca732..4d159d51 100755 --- a/pyftdi/tests/gpio.py +++ b/pyftdi/tests/gpio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2016-2020, Emmanuel Blot +# Copyright (c) 2016-2023, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -354,7 +354,8 @@ def test_gpio_baudate(self): class GpioMultiportTestCase(FtdiTestCase): - """FTDI GPIO test for multi-port FTDI devices, i.e. FT2232H/FT4232H. + """FTDI GPIO test for multi-port FTDI devices, + i.e. FT2232H/FT4232H/FT4232HA. Please ensure that the HW you connect to the FTDI port A does match the encoded configuration. Check your HW setup before running this test diff --git a/pyftdi/tests/i2c.py b/pyftdi/tests/i2c.py index 5b841957..64b7c73d 100755 --- a/pyftdi/tests/i2c.py +++ b/pyftdi/tests/i2c.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2017-2020, Emmanuel Blot +# Copyright (c) 2017-2023, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -241,7 +241,7 @@ def test(self): class I2cDualMaster(TestCase): """Check the behaviour of 2 I2C masters. Requires a multi port FTDI device, - i.e. FT2232H or FT4232H. See issue #159. + i.e. FT2232H, FT4232H or FT4232HA. See issue #159. """ def test(self): diff --git a/pyftdi/tests/mockusb.py b/pyftdi/tests/mockusb.py index 13ac3cd2..57d03e46 100755 --- a/pyftdi/tests/mockusb.py +++ b/pyftdi/tests/mockusb.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2020-2021, Emmanuel Blot +# Copyright (c) 2020-2023, Emmanuel Blot # All rights reserved. #pylint: disable-msg=empty-docstring @@ -101,16 +101,18 @@ def test_list_devices(self): '232h': 0x6014, '2232h': 0x6010, '4232h': 0x6011, + '4232ha': 0x6048, } } devs = UsbTools.list_devices('ftdi:///?', vids, pids, vid) - self.assertEqual(len(devs), 6) + self.assertEqual(len(devs), 7) ifmap = { 0x6001: 1, 0x6010: 2, 0x6011: 4, 0x6014: 1, - 0x6015: 1 + 0x6015: 1, + 0x6048: 4 } for dev, desc in devs: strings = UsbTools.build_dev_strings('ftdi', vids, pids, @@ -143,22 +145,25 @@ def setUpClass(cls): def test_list_devices(self): """List FTDI devices.""" devs = Ftdi.list_devices('ftdi:///?') - self.assertEqual(len(devs), 6) + self.assertEqual(len(devs), 7) devs = Ftdi.list_devices('ftdi://:232h/?') self.assertEqual(len(devs), 2) devs = Ftdi.list_devices('ftdi://:2232h/?') self.assertEqual(len(devs), 1) devs = Ftdi.list_devices('ftdi://:4232h/?') self.assertEqual(len(devs), 1) + devs = Ftdi.list_devices('ftdi://:4232ha/?') + self.assertEqual(len(devs), 1) out = StringIO() Ftdi.show_devices('ftdi:///?', out) lines = [l.strip() for l in out.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() - self.assertEqual(len(lines), 10) + self.assertEqual(len(lines), 14) portmap = defaultdict(int) - reference = {'232': 1, '2232': 2, '4232': 4, '232h': 2, 'ft-x': 1} + reference = {'232': 1, '2232': 2, '4232': 4, '232h': 2, 'ft-x': 1, + '4232ha': 4} for line in lines: url = line.split(' ')[0].strip() parts = urlsplit(url) @@ -250,7 +255,7 @@ def test_enumerate(self): class MockFourPortDeviceTestCase(FtdiTestCase): - """Test FTDI APIs with a quad-port FTDI device (FT4232H) + """Test FTDI APIs with a quad-port FTDI device (FT4232H, FT4232HA) """ @classmethod @@ -296,7 +301,7 @@ def test_enumerate(self): lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() - self.assertEqual(len(lines), 10) + self.assertEqual(len(lines), 14) for line in lines: self.assertTrue(line.startswith('ftdi://')) # skip description, i.e. consider URL only @@ -304,7 +309,7 @@ def test_enumerate(self): urlparts = urlsplit(url) self.assertEqual(urlparts.scheme, 'ftdi') parts = urlparts.netloc.split(':') - if parts[1] == '4232': + if (parts[1] == '4232') or (parts[1] == '4232ha'): # def file contains no serial number, so expect bus:addr syntax self.assertEqual(len(parts), 4) self.assertRegex(parts[2], r'^\d$') diff --git a/pyftdi/tests/resources/ft4232ha.yaml b/pyftdi/tests/resources/ft4232ha.yaml new file mode 100644 index 00000000..6825b052 --- /dev/null +++ b/pyftdi/tests/resources/ft4232ha.yaml @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause + +devices: + - bus: 1 + address: 1 + descriptor: + vid: 0x403 + pid: 0x6048 + version: 0x3600 + manufacturer: FTDI + product: FT4232HA + serialnumber: FT7E0S92 + configurations: + - interfaces: + - repeat: 4 + diff --git a/pyftdi/tests/resources/ftmany.yaml b/pyftdi/tests/resources/ftmany.yaml index 6a622cdc..9e678568 100644 --- a/pyftdi/tests/resources/ftmany.yaml +++ b/pyftdi/tests/resources/ftmany.yaml @@ -60,6 +60,17 @@ devices: manufacturer: FTDI product: FT2232R serialnumber: FT1OPQ + - bus: 3 + address: 5 + descriptor: + vid: 0x403 + pid: 0x6048 + version: 0x3600 + manufacturer: FTDI + product: FT4232HA + configurations: + - interfaces: + - repeat: 4 diff --git a/pyftdi/tracer.py b/pyftdi/tracer.py index 91d96f28..8b414da2 100644 --- a/pyftdi/tracer.py +++ b/pyftdi/tracer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017-2020, Emmanuel Blot +# Copyright (c) 2017-2023, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -29,7 +29,8 @@ class FtdiMpsseTracer: 0x0700: 2, 0x0800: 2, 0x0900: 1, - 0x1000: 0} + 0x1000: 0, + 0x3600: 2} """Count of MPSSE engines.""" def __init__(self, version): diff --git a/pyftdi/usbtools.py b/pyftdi/usbtools.py index b1508927..5a1d57bc 100644 --- a/pyftdi/usbtools.py +++ b/pyftdi/usbtools.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014-2020, Emmanuel Blot +# Copyright (c) 2014-2023, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # @@ -136,9 +136,9 @@ def get_device(cls, devdesc: UsbDeviceDescriptor) -> UsbDevice: the USB device in random order. serial argument is more reliable selector and should always be prefered. - Some FTDI devices support several interfaces/ports (such as FT2232H - and FT4232H). The interface argument selects the FTDI port to use, - starting from 1 (not 0). + Some FTDI devices support several interfaces/ports (such as FT2232H, + FT4232H and FT4232HA). The interface argument selects the FTDI port + to use, starting from 1 (not 0). :param devdesc: Device descriptor that identifies the device by constraints. From 543fb4b19050afd753bc6efe9a77d091b1f279ea Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 5 Aug 2023 23:58:17 +0200 Subject: [PATCH 10/62] Fix line width --- pyftdi/eeprom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index 1bb0759f..7f012053 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -952,7 +952,8 @@ def _set_bus_control_230x(self, bus: str, control: str, def _set_group_x232h(self, group: int, control: str, value: str, out: Optional[TextIO]) -> None: - if self.device_version in (0x0700, 0x800, 0x3600): # 2232H/4232H/4232HA + # 2232H/4232H/4232HA + if self.device_version in (0x0700, 0x800, 0x3600): offset = 0x0c + group//2 nibble = group & 1 else: # 232H From d58a78b13eafcac888deb5984d97966101fa3d52 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sun, 6 Aug 2023 00:03:12 +0200 Subject: [PATCH 11/62] Update GitHub actions for Python version and fix Sphinx RTD theme URL --- .github/workflows/pythonchecksyntax.yml | 2 +- .github/workflows/pythonmocktests.yml | 2 +- .github/workflows/pythonpackage.yml | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 87ef7e95..1dcac078 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pythonmocktests.yml b/.github/workflows/pythonmocktests.yml index 7011d78f..8c17b5f4 100644 --- a/.github/workflows/pythonmocktests.yml +++ b/.github/workflows/pythonmocktests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c46c128e..3c6675f4 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 @@ -20,9 +20,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt - pip install setuptools wheel sphinx sphinx_autodoc_typehints - # Shpinx Read the Doc theme seems to never get a release w/ fixed issues - pip install -U -e git+https://github.com/readthedocs/sphinx_rtd_theme.git@2b8717a3647cc650625c566259e00305f7fb60aa#egg=sphinx_rtd_theme + pip install setuptools wheel sphinx sphinx_rtd_theme sphinx_autodoc_typehints - name: Build package run: | python setup.py bdist From 800fccb21b80ce427a61f264174e48563c21e3de Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sun, 6 Aug 2023 00:07:28 +0200 Subject: [PATCH 12/62] Update Python requirements Although Python3.7 syntax is supported, PyFtdi does not want to support end-of-life'd Python versions --- pyftdi/doc/requirements.rst | 6 +++++- pyftdi/tests/toolsimport.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyftdi/doc/requirements.rst b/pyftdi/doc/requirements.rst index 51b79e99..0b0f3a51 100644 --- a/pyftdi/doc/requirements.rst +++ b/pyftdi/doc/requirements.rst @@ -3,7 +3,11 @@ Requirements ------------ -Python_ 3.7 or above is required. +Python_ 3.8 or above is required. + +* PyFtdi *v0.54* is the last PyFtdi version to support Python 3.7. + + * Python 3.7 has reached end-of-life on June 27rd, 2023. * PyFtdi *v0.53* is the last PyFtdi version to support Python 3.6. diff --git a/pyftdi/tests/toolsimport.py b/pyftdi/tests/toolsimport.py index 68491935..64a28eb5 100755 --- a/pyftdi/tests/toolsimport.py +++ b/pyftdi/tests/toolsimport.py @@ -23,7 +23,7 @@ class ToolsTestCase(TestCase): This is especially useful to find Python syntax version mismatch and other not-yet-supported modules/features. - PyFtdi and tools should support Python 3.7 onwards. + PyFtdi and tools should support Python 3.8 onwards. """ @classmethod From 27a1e24e441c25f7ad2c1d898d004b4469e3b87f Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 14 Mar 2022 10:00:41 +0100 Subject: [PATCH 13/62] eeprom: fix regression introduced in #301 --- pyftdi/eeprom.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index 7f012053..859f38d1 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -690,10 +690,11 @@ def _generate_var_strings(self, fill=True) -> None: # if a custom,small EEPROM device is used dynpos = 0x40 data_pos = dynpos - # start of var-strings in sector 1 (used for mirrored config) - s1_vstr_start = data_pos - self.mirror_sector tbl_pos = 0x0e - tbl_sector2_pos = self.mirror_sector + tbl_pos + if self.is_mirroring_enabled: + # start of var-strings in sector 1 (used for mirrored config) + s1_vstr_start = data_pos - self.mirror_sector + tbl_sector2_pos = self.mirror_sector + tbl_pos for name in self.VAR_STRINGS: try: ustr = self._config[name].encode('utf-16le') @@ -704,15 +705,15 @@ def _generate_var_strings(self, fill=True) -> None: stream.append(0x03) # string descriptor stream.extend(ustr) self._eeprom[tbl_pos] = data_pos + tbl_pos += 1 if self.is_mirroring_enabled: self._eeprom[tbl_sector2_pos] = data_pos - tbl_pos += 1 - tbl_sector2_pos += 1 + tbl_sector2_pos += 1 self._eeprom[tbl_pos] = length + tbl_pos += 1 if self.is_mirroring_enabled: self._eeprom[tbl_sector2_pos] = length - tbl_pos += 1 - tbl_sector2_pos += 1 + tbl_sector2_pos += 1 data_pos += length if self.is_mirroring_enabled: self._eeprom[s1_vstr_start:s1_vstr_start+len(stream)] = stream From 6c65a721274acafe905bc03c2616dab716747681 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 14 Mar 2022 10:01:00 +0100 Subject: [PATCH 14/62] eeprom: use f-strings --- pyftdi/eeprom.py | 116 +++++++++++++++++++++++------------------------ 1 file changed, 57 insertions(+), 59 deletions(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index 859f38d1..230e1ada 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -39,13 +39,13 @@ class FtdiEepromError(FtdiError): class Hex2Int(int): """Hexa representation of a byte.""" def __str__(self): - return '0x%02x' % int(self) + return f'0x{int(self):02x}' class Hex4Int(int): """Hexa representation of a half-word.""" def __str__(self): - return '0x%04x' % int(self) + return f'0x{int(self):04x}' class FtdiEeprom: @@ -125,7 +125,7 @@ def __init__(self): def __getattr__(self, name): if name in self._config: return self._config[name] - raise AttributeError('No such attribute: %s' % name) + raise AttributeError(f'No such attribute: {name}') @classproperty def eeprom_sizes(cls) -> List[int]: @@ -296,7 +296,7 @@ def cbus_pins(self) -> List[int]: :return: list of CBUS pins """ pins = [pin for pin in range(0, 10) - if self._config.get('cbus_func_%d' % pin, '') == 'GPIO'] + if self._config.get(f'cbus_func_{pin}', '') == 'GPIO'] return pins @property @@ -313,7 +313,7 @@ def cbus_mask(self) -> int: cbus = list(range(4)) mask = 0 for bix, pin in enumerate(cbus): - if self._config.get('cbus_func_%d' % pin, '') == 'GPIO': + if self._config.get(f'cbus_func_{pin}', '') == 'GPIO': mask |= 1 << bix return mask @@ -381,7 +381,7 @@ def save_config(self, file: TextIO) -> None: for i in range(0, len(self._eeprom), length): chunk = self._eeprom[i:i+length] hexa = hexlify(chunk).decode() - cfg.set('raw', '@%02x' % i, hexa) + cfg.set('raw', f'@{i:02x}', hexa) cfg.write(file) def load_config(self, file: TextIO, section: Optional[str] = None) -> None: @@ -411,7 +411,7 @@ def load_config(self, file: TextIO, section: Optional[str] = None) -> None: sect = 'raw' if sect in sections and section in (None, 'all', sect): if not cfg.has_section(sect): - raise FtdiEepromError("No '%s' section in INI file" % sect) + raise FtdiEepromError(f"No '{sect}' section in INI file") options = cfg.options(sect) try: for opt in options: @@ -422,11 +422,10 @@ def load_config(self, file: TextIO, section: Optional[str] = None) -> None: buf = unhexlify(hexval) self._eeprom[address:address+len(buf)] = buf except IndexError as exc: - raise ValueError("Invalid address in '%s'' section" % - sect) from exc + raise ValueError(f"Invalid address in '{sect}' " + f"section") from exc except ValueError as exc: - raise ValueError("Invalid line in '%s'' section" % - sect) from exc + raise ValueError(f"Invalid line in '{sect}' section") from exc self._compute_crc(self._eeprom, True) if not self._valid: raise ValueError('Loaded RAW section is invalid (CRC mismatch') @@ -439,12 +438,12 @@ def load_config(self, file: TextIO, section: Optional[str] = None) -> None: } if sect in sections and section in (None, 'all', sect): if not cfg.has_section(sect): - raise FtdiEepromError("No '%s' section in INI file" % sect) + raise FtdiEepromError(f"No '{sect}' section in INI file") options = cfg.options(sect) for opt in options: value = cfg.get(sect, opt).strip() if opt in vmap: - func = getattr(self, 'set_%s' % vmap[opt]) + func = getattr(self, f'set_{vmap[opt]}') func(value) else: self.log.debug('Assigning opt %s = %s', opt, value) @@ -454,7 +453,7 @@ def load_config(self, file: TextIO, section: Optional[str] = None) -> None: self.log.warning("Ignoring setting '%s': %s", opt, exc) loaded = True if not loaded: - raise ValueError('Invalid section: %s' % section) + raise ValueError(f'Invalid section: {section}') self._sync_eeprom() def set_serial_number(self, serial: str) -> None: @@ -487,7 +486,7 @@ def set_property(self, name: str, value: Union[str, int, bool], mobj = match(r'cbus_func_(\d)', name) if mobj: if not isinstance(value, str): - raise ValueError("'%s' should be specified as a string" % name) + raise ValueError("'{name}' should be specified as a string") self._set_cbus_func(int(mobj.group(1)), value, out) self._dirty.add(name) return @@ -522,7 +521,7 @@ def set_property(self, name: str, value: Union[str, int, bool], if name in hwords: val = to_int(value) if not 0 <= val <= 0xFFFF: - raise ValueError('Invalid value for %s' % name) + raise ValueError(f'Invalid value for {name}') offset = hwords[name] self._eeprom[offset:offset+2] = spack(' None: """Erase the whole EEPROM. @@ -603,7 +602,7 @@ def initialize(self) -> None: self.set_manufacturer_name('FTDI') self.set_product_name(dev_name.upper()) sernum = ''.join([chr(randint(ord('A'), ord('Z'))) for _ in range(5)]) - self.set_serial_number('FT%d%s' % (randint(0, 9), sernum)) + self.set_serial_number(f'FT{randint(0, 9)}{sernum}') self.set_property('vendor_id', vid) self.set_property('product_id', pid) self.set_property('type', dev_ver) @@ -626,7 +625,7 @@ def dump_config(self, file: Optional[BinaryIO] = None) -> None: if self._dirty: self._decode_eeprom() for name, value in self._config.items(): - print('%s: %s' % (name, value), file=file or sys.stdout) + print(f'{name}: {value}', file=file or sys.stdout) def commit(self, dry_run: bool = True, no_crc: bool = False) -> bool: """Commit any changes to the EEPROM. @@ -652,7 +651,7 @@ def commit(self, dry_run: bool = True, no_crc: bool = False) -> bool: if old != new: break pos &= ~0x1 - raise FtdiEepromError('Write to EEPROM failed @ 0x%02x' % pos) + raise FtdiEepromError(f'Write to EEPROM failed @ 0x{pos:02x}') self._modified = False return dry_run @@ -665,11 +664,11 @@ def _validate_string(cls, string): for invchr in ':/': # do not accept characters which are interpreted as URL seperators if invchr in string: - raise ValueError("Invalid character '%s' in string" % invchr) + raise ValueError(f"Invalid character '{invchr}' in string") def _update_var_string(self, name: str, value: str) -> None: if name not in self.VAR_STRINGS: - raise ValueError('%s is not a variable string' % name) + raise ValueError(f'{name} is not a variable string') try: if value == self._config[name]: return @@ -766,7 +765,7 @@ def _compute_crc(self, eeprom: Union[bytes, bytearray], check=False): if check: self._valid = not bool(crc) if not self._valid: - self.log.debug('CRC is now 0x%04x', crc) + self.log.debug(f'CRC is now 0x{crc:04x}') else: self.log.debug('CRC OK') return crc, crc_pos, crc_size @@ -811,8 +810,8 @@ def _read_eeprom(self) -> bytes: else: self.log.error('Invalid CRC or EEPROM content') if not self.is_empty and mirror_detected: - self.log.info("Detected a mirrored eeprom. " + - "Enabling mirrored writing") + self.log.info('Detected a mirrored eeprom. ' + 'Enabling mirrored writing') self._mirror = True return eeprom @@ -847,7 +846,7 @@ def _decode_eeprom(self): name = Ftdi.DEVICE_NAMES[cfg['type']].replace('-', '') if name.startswith('ft'): name = name[2:] - func = getattr(self, '_decode_%s' % name) + func = getattr(self, f'_decode_{name}') except (KeyError, AttributeError): self.log.warning('No EEPROM decoder for device %s', name or '?') else: @@ -874,7 +873,7 @@ def _set_cbus_func(self, cpin: int, value: str, raise ValueError('This property is not supported on this ' 'device') from exc pin_filter = getattr(self, - '_filter_cbus_func_x%x' % self.device_version, + f'_filter_cbus_func_x{self.device_version:x}', None) if value == '?' and out: items = {item.name for item in cbus} @@ -883,15 +882,15 @@ def _set_cbus_func(self, cpin: int, value: str, print(', '.join(sorted(items)) if items else '(none)', file=out) return if not 0 <= cpin < count: - raise ValueError("Unsupported CBUS pin '%d'" % cpin) + raise ValueError(f"Unsupported CBUS pin '{cpin}'") try: code = cbus[value.upper()].value except KeyError as exc: - raise ValueError("CBUS pin %d does not have function '%s'" % - (cpin, value)) from exc + raise ValueError(f"CBUS pin '{cpin}'' does not have function " + f"{value}'") from exc if pin_filter and not pin_filter(cpin, value.upper()): - raise ValueError("Unsupported CBUS function '%s' for pin '%d'" % - (value, cpin)) + raise ValueError(f"Unsupported CBUS function '{value}' for pin " + f"'{cpin}'") addr = offset + (cpin*width)//8 if width == 4: bitoff = 4 if cpin & 0x1 else 0 @@ -948,7 +947,7 @@ def _set_bus_control_230x(self, bus: str, control: str, value: Union[str, int, bool], out: Optional[TextIO]) -> None: if bus not in 'cd': - raise ValueError('Invalid bus: %s' % bus) + raise ValueError(f'Invalid bus: {bus}') self._set_bus_xprop(0x0c, bus == 'c', control, value, out) def _set_group_x232h(self, group: int, control: str, value: str, @@ -973,7 +972,7 @@ def _set_bus_xprop(self, offset: int, high_nibble: bool, control: str, return value = int(value) if value not in candidates: - raise ValueError('Invalid drive current: %d mA' % value) + raise ValueError(f'Invalid drive current: {value} mA') value //= 4 value -= 1 elif control in ('slow_slew', 'schmitt'): @@ -982,10 +981,9 @@ def _set_bus_xprop(self, offset: int, high_nibble: bool, control: str, return value = int(to_bool(value)) else: - raise ValueError('Unsupported control: %s' % control) + raise ValueError(f'Unsupported control: {control}') except (ValueError, TypeError) as exc: - raise ValueError('Invalid %s value: %s' % - (control, value)) from exc + raise ValueError(f'Invalid {control} value: {value}') from exc config = self._eeprom[offset] if not high_nibble: conf = config & 0x0F @@ -1014,7 +1012,7 @@ def _set_invert(self, name, value, out): print('off, on', file=out) return if name.upper() not in self.UART_BITS.__members__: - raise ValueError('Unknown property: %s' % name) + raise ValueError(f'Unknown property: {name}') value = to_bool(value, permissive=False) code = getattr(self.UART_BITS, name.upper()) invert = self._eeprom[0x0B] @@ -1031,20 +1029,20 @@ def _decode_x(self): cfg['channel_a_driver'] = 'VCP' if misc & (1 << 7) else 'D2XX' for bit in self.UART_BITS: value = self._eeprom[0x0B] - cfg['invert_%s' % self.UART_BITS(bit).name] = bool(value & bit) + cfg[f'invert_{self.UART_BITS(bit).name}'] = bool(value & bit) max_drive = self.DRIVE.LOW.value | self.DRIVE.HIGH.value value = self._eeprom[0x0c] for grp in range(2): conf = value & 0xF bus = 'c' if grp else 'd' - cfg['%sbus_drive' % bus] = 4 * (1+(conf & max_drive)) - cfg['%sbus_schmitt' % bus] = bool(conf & self.DRIVE.SCHMITT) - cfg['%sbus_slow_slew' % bus] = bool(conf & self.DRIVE.SLOW_SLEW) + cfg[f'{bus}bus_drive'] = 4 * (1+(conf & max_drive)) + cfg[f'{bus}bus_schmitt'] = bool(conf & self.DRIVE.SCHMITT) + cfg[f'{bus}bus_slow_slew'] = bool(conf & self.DRIVE.SLOW_SLEW) value >>= 4 for bix in range(4): value = self._eeprom[0x1A + bix] try: - cfg['cbus_func_%d' % bix] = self.CBUSX(value).name + cfg[f'cbus_func_{bix}'] = self.CBUSX(value).name except ValueError: pass @@ -1062,20 +1060,20 @@ def _decode_232h(self): max_drive = self.DRIVE.LOW.value | self.DRIVE.HIGH.value for grp in range(2): conf = self._eeprom[0x0c+grp] - cfg['group_%d_drive' % grp] = 4 * (1+(conf & max_drive)) - cfg['group_%d_schmitt' % grp] = \ + cfg[f'group_{grp}_drive'] = 4 * (1+(conf & max_drive)) + cfg[f'group_{grp}_schmitt'] = \ bool(conf & self.DRIVE.SCHMITT.value) - cfg['group_%d_slow_slew' % grp] = \ + cfg[f'group_{grp}_slow_slew'] = \ bool(conf & self.DRIVE.SLOW_SLEW.value) for bix in range(5): value = self._eeprom[0x18 + bix] low, high = value & 0x0F, value >> 4 try: - cfg['cbus_func_%d' % ((2*bix)+0)] = self.CBUSH(low).name + cfg[f'cbus_func_{(2*bix)+0}'] = self.CBUSH(low).name except ValueError: pass try: - cfg['cbus_func_%d' % ((2*bix)+1)] = self.CBUSH(high).name + cfg[f'cbus_func_{(2*bix)+1}'] = self.CBUSH(high).name except ValueError: pass @@ -1087,19 +1085,19 @@ def _decode_232r(self): cfg['external_oscillator'] = cfg0 & 0x02 for bit in self.UART_BITS: value = self._eeprom[0x0B] - cfg['invert_%s' % self.UART_BITS(bit).name] = bool(value & bit) + cfg[f'invert_{self.UART_BITS(bit).name}'] = bool(value & bit) bix = 0 while True: value = self._eeprom[0x14 + bix] low, high = value & 0x0F, value >> 4 try: - cfg['cbus_func_%d' % ((2*bix)+0)] = self.CBUS(low).name + cfg[f'cbus_func_{(2*bix)+0}'] = self.CBUS(low).name except ValueError: pass if bix == 2: break try: - cfg['cbus_func_%d' % ((2*bix)+1)] = self.CBUS(high).name + cfg[f'cbus_func_{(2*bix)+1}'] = self.CBUS(high).name except ValueError: pass bix += 1 @@ -1121,7 +1119,7 @@ def _decode_4232h(self): conf = self._eeprom[0x0B] rs485 = self.CHANNEL.RS485 for chix in range(4): - cfg['channel_%x_rs485' % (0xa+chix)] = bool(conf & (rs485 << chix)) + cfg[f'channel_{0xa+chix:x}_rs485'] = bool(conf & (rs485 << chix)) def _decode_x232h(self, cfg): # common code for 2232h, 4232h, 4232ha @@ -1134,8 +1132,8 @@ def _decode_x232h(self, cfg): val = self._eeprom[0x0c + bix//2] else: val >>= 4 - cfg['group_%d_drive' % bix] = 4 * (1+(val & max_drive)) - cfg['group_%d_schmitt' % bix] = \ + cfg[f'group_{bix}_drive'] = 4 * (1+(val & max_drive)) + cfg[f'group_{bix}_schmitt'] = \ bool(val & self.DRIVE.SCHMITT.value) - cfg['group_%d_slow_slew' % bix] = \ + cfg[f'group_{bix}_slow_slew'] = \ bool(val & self.DRIVE.SLOW_SLEW.value) From 579403f72e3ff54d2f76c71a9cb95f0ddcbbdf5d Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Mon, 14 Mar 2022 15:05:21 +0100 Subject: [PATCH 15/62] eeprom: add more eeprom_mock tests to verify EEPROM mirroring works correctly --- pyftdi/eeprom.py | 4 +- pyftdi/tests/eeprom_mock.py | 163 +++++++++++++++++++++++++++++++----- 2 files changed, 145 insertions(+), 22 deletions(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index 230e1ada..0e8b496d 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -686,7 +686,7 @@ def _generate_var_strings(self, fill=True) -> None: stream = bytearray() dynpos = self._PROPERTIES[self.device_version].dynoff if dynpos > self._size: - # if a custom,small EEPROM device is used + # if a custom, small EEPROM device is used dynpos = 0x40 data_pos = dynpos tbl_pos = 0x0e @@ -703,7 +703,7 @@ def _generate_var_strings(self, fill=True) -> None: stream.append(length) stream.append(0x03) # string descriptor stream.extend(ustr) - self._eeprom[tbl_pos] = data_pos + self._eeprom[tbl_pos] = data_pos | 0x80 tbl_pos += 1 if self.is_mirroring_enabled: self._eeprom[tbl_sector2_pos] = data_pos diff --git a/pyftdi/tests/eeprom_mock.py b/pyftdi/tests/eeprom_mock.py index 7acf99f3..14f9ee6b 100644 --- a/pyftdi/tests/eeprom_mock.py +++ b/pyftdi/tests/eeprom_mock.py @@ -22,6 +22,11 @@ class FtdiTestCase(TestCase): """Common features for all tests. """ + # manufacturer/product/serial number strings to use in tests + TEST_MANU_NAME = "MNAME" + TEST_PROD_NAME = "PNAME" + TEST_SN = "SN123" + @classmethod def setUpClass(cls): cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), permissive=False) @@ -45,11 +50,6 @@ class EepromMirrorTestCase(FtdiTestCase): commit any of their eeprom changes """ - # manufacturer name string to use in tests - TEST_MANU_NAME = "MNAME" - TEST_PROD_NAME = "PNAME" - TEST_SN = "SN123" - @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() @@ -72,6 +72,7 @@ def test_mirror_properties(self): eeprom = FtdiEeprom() eeprom.open(self.url, ignore=True) self.assertTrue(eeprom.has_mirroring) + self.assertFalse(eeprom.is_mirroring_enabled) self.assertEqual(eeprom.size // 2, eeprom.mirror_sector) eeprom.close() @@ -79,6 +80,7 @@ def test_mirror_properties(self): mirrored_eeprom.enable_mirroring(True) mirrored_eeprom.open(self.url, ignore=True) self.assertTrue(mirrored_eeprom.has_mirroring) + self.assertTrue(mirrored_eeprom.is_mirroring_enabled) self.assertEqual(mirrored_eeprom.size // 2, mirrored_eeprom.mirror_sector) mirrored_eeprom.close() @@ -179,20 +181,13 @@ def _check_for_mirrored_eeprom_contents(self, eeprom: FtdiEeprom): self.assertEqual(eeprom.data[ii], eeprom.data[ii + eeprom.mirror_sector]) - -class EepromMirrorFt232hTestCase(EepromMirrorTestCase): - TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' - - -class EepromMirrorFt2232hTestCase(EepromMirrorTestCase): - TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' - - -class EepromMirrorFt230xTestCase(FtdiTestCase): - """Test FTDI eeprom with non-mirroring capabilities to ensure it works as - expected. +class NonMirroredEepromTestCase(FtdiTestCase): + """Test FTDI EEPROM mirror features do not break FTDI devices that do + not use mirroring """ - TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft230x.yaml' + TEST_MANU_NAME = "MNAME" + TEST_PROD_NAME = "PNAME" + TEST_SN = "SN123" @classmethod def setUpClass(cls): @@ -206,16 +201,22 @@ def setUpClass(cls): ftdi.open_from_url(cls.url) count = ftdi.device_port_count ftdi.close() - + def test_mirror_properties(self): """Check FtdiEeprom properties are accurate for a device that can not mirror. + Only run this test if the device under test is incapable of + mirroring """ + if self.DEVICE_CAN_MIRROR: + self.skipTest("Mirror properties for devices capable of mirroring" + + " are tested in EepromMirrorTestCase") # properties should work regardless of if the mirror option is set # or not eeprom = FtdiEeprom() eeprom.open(self.url, ignore=True) self.assertFalse(eeprom.has_mirroring) + self.assertFalse(eeprom.is_mirroring_enabled) with self.assertRaises(FtdiError): eeprom.mirror_sector eeprom.close() @@ -224,14 +225,81 @@ def test_mirror_properties(self): mirrored_eeprom.enable_mirroring(True) mirrored_eeprom.open(self.url, ignore=True) self.assertFalse(mirrored_eeprom.has_mirroring) + self.assertFalse(mirrored_eeprom.is_mirroring_enabled) with self.assertRaises(FtdiError): - eeprom.mirror_sector + mirrored_eeprom.mirror_sector mirrored_eeprom.close() + def test_no_mirror_manufacturer(self): + """Verify manufacturer string is NOT duplicated/mirrored + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(False) + eeprom.open(self.url, ignore=True) + eeprom.erase() + eeprom.set_manufacturer_name(self.TEST_MANU_NAME) + self._check_for_non_mirrored_eeprom_contents(eeprom) + + def test_no_mirror_product(self): + """Verify product string is NOT duplicated/mirrored + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(False) + eeprom.open(self.url, ignore=True) + eeprom.erase() + eeprom.set_product_name(self.TEST_PROD_NAME) + self._check_for_non_mirrored_eeprom_contents(eeprom) + + def test_mirror_serial(self): + """Verify serial string is NOT duplicated/mirrored + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(False) + eeprom.open(self.url, ignore=True) + eeprom.erase() + eeprom.set_serial_number(self.TEST_SN) + self._check_for_non_mirrored_eeprom_contents(eeprom) + + def test_varstr_combinations(self): + """Verify various combinations of var strings are NOT + duplicated/mirrored + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(False) + eeprom.open(self.url, ignore=True) + + # manu + prod str + eeprom.erase() + eeprom.set_manufacturer_name(self.TEST_MANU_NAME) + eeprom.set_product_name(self.TEST_PROD_NAME) + self._check_for_non_mirrored_eeprom_contents(eeprom) + + # manu + sn str + eeprom.erase() + eeprom.set_manufacturer_name(self.TEST_MANU_NAME) + eeprom.set_serial_number(self.TEST_SN) + self._check_for_non_mirrored_eeprom_contents(eeprom) + + # prod + sn str + eeprom.erase() + eeprom.set_manufacturer_name(self.TEST_PROD_NAME) + eeprom.set_serial_number(self.TEST_SN) + self._check_for_non_mirrored_eeprom_contents(eeprom) + + # manu + prod + sn str + eeprom.erase() + eeprom.set_manufacturer_name(self.TEST_MANU_NAME) + eeprom.set_manufacturer_name(self.TEST_PROD_NAME) + eeprom.set_serial_number(self.TEST_SN) + self._check_for_non_mirrored_eeprom_contents(eeprom) + def test_compute_size_does_not_mirror(self): """Verify the eeproms internal _compute_size method returns the correct bool value when it detects no mirroring. """ + if self.DEVICE_CAN_MIRROR: + self.skipTest("Mirror properties for devices capable of mirroring" + + " are tested in EepromMirrorTestCase") eeprom = FtdiEeprom() eeprom.open(self.url, ignore=True) _, mirrored = eeprom._compute_size([]) @@ -244,12 +312,67 @@ def test_compute_size_does_not_mirror(self): self.assertFalse(mirrored) eeprom.close() + def _check_for_non_mirrored_eeprom_contents(self, eeprom: FtdiEeprom): + """Check that contents of the eeprom is not mirrored + """ + mirror_sector_start = eeprom.size // 2 + # split eeprom into 2 sectors as would be done if mirroring was enabled + # and verify the device is not mirrored + normal_mirror_s1 = eeprom.data[:mirror_sector_start] + normal_mirror_s2 = eeprom.data[mirror_sector_start:] + self.assertNotEqual(normal_mirror_s1, normal_mirror_s2) + + +class EepromMirrorFt232hTestCase(EepromMirrorTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' + + +class EepromMirrorFt2232hTestCase(EepromMirrorTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' + + +class EepromMirrorFt4232hTestCase(EepromMirrorTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft4232h.yaml' + + +class EepromMirrorFt232rTestCase(NonMirroredEepromTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232r.yaml' + DEVICE_CAN_MIRROR = False + + +class EepromMirrorFt230xTestCase(NonMirroredEepromTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft230x.yaml' + DEVICE_CAN_MIRROR = False + + +class EepromNonMirroredFt232hTestCase(NonMirroredEepromTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' + DEVICE_CAN_MIRROR = True + + +class EepromNonMirroredFt2232hTestCase(NonMirroredEepromTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' + DEVICE_CAN_MIRROR = True + + +class EepromNonMirroredFt4232hTestCase(NonMirroredEepromTestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft4232h.yaml' + DEVICE_CAN_MIRROR = True + def suite(): suite_ = TestSuite() + # Test devices that support the mirroring capability suite_.addTest(makeSuite(EepromMirrorFt232hTestCase, 'test')) suite_.addTest(makeSuite(EepromMirrorFt2232hTestCase, 'test')) + suite_.addTest(makeSuite(EepromMirrorFt4232hTestCase, 'test')) + # Test devices that do not support the mirror capability + suite_.addTest(makeSuite(EepromMirrorFt232rTestCase, 'test')) suite_.addTest(makeSuite(EepromMirrorFt230xTestCase, 'test')) + # test devices that support the mirroring capability, but have it disabled + suite_.addTest(makeSuite(EepromNonMirroredFt232hTestCase, 'test')) + suite_.addTest(makeSuite(EepromNonMirroredFt2232hTestCase, 'test')) + suite_.addTest(makeSuite(EepromNonMirroredFt4232hTestCase, 'test')) return suite_ From 9e35192a83b921c6bdbe4e73f4855b74440bbca0 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 12 Aug 2023 16:32:05 +0200 Subject: [PATCH 16/62] version: update to 0.55.1 --- README.md | 2 +- pyftdi/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 12797740..090f1d98 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,6 @@ PyFtdi currently supports the following features: ### Python support -PyFtdi requires Python 3.7+. +PyFtdi requires Python 3.8+. See `pyftdi/doc/requirements.rst` for more details. diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 3a350649..35e04ed4 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -6,7 +6,7 @@ #pylint: disable-msg=missing-docstring -__version__ = '0.55.0' +__version__ = '0.55.1' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' From 70f0ede6308e00140c8bcd7c7bea523bd93bda6e Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 12 Aug 2023 17:00:55 +0200 Subject: [PATCH 17/62] i2ctools: fix line width introduced w/ #326. --- pyftdi/bin/i2cscan.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyftdi/bin/i2cscan.py b/pyftdi/bin/i2cscan.py index 7d6ebfc3..1039c34d 100755 --- a/pyftdi/bin/i2cscan.py +++ b/pyftdi/bin/i2cscan.py @@ -33,7 +33,8 @@ class I2cBusScanner: HIGHEST_I2C_SLAVE_ADDRESS = 0x78 @classmethod - def scan(cls, url: str, smb_mode: bool = True, force: bool = False) -> None: + def scan(cls, url: str, smb_mode: bool = True, force: bool = False) \ + -> None: """Scan an I2C bus to detect slave device. :param url: FTDI URL From 9a46a3042cb024b6e8c2293cd538c10f243ebb43 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 12 Aug 2023 17:09:50 +0200 Subject: [PATCH 18/62] doc: update contributors list --- pyftdi/doc/authors.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyftdi/doc/authors.rst b/pyftdi/doc/authors.rst index 3d65d6a3..8c741030 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -45,4 +45,8 @@ Contributors * len0rd * Rod Whitby * Kornel Swierzy + * Taisuke Yamada + * Michael Niewöhner + * Kalofin + * Henry Au-Yeung From dc970f7b1ae2023954f5ca1dbb3c1f313017154a Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 12 Aug 2023 17:15:20 +0200 Subject: [PATCH 19/62] .gitignore: ignore local virtual env --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 58065b3e..42fbc6e9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ dist/ build/ sphinx/ .vscode/ +.venv/ From 9c845270e058262669be87bd2566c96f1a731c07 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 12 Aug 2023 17:19:33 +0200 Subject: [PATCH 20/62] github: update checkout action version --- .github/workflows/pythonchecksyntax.yml | 2 +- .github/workflows/pythonmocktests.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 1dcac078..4be50301 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -12,7 +12,7 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: diff --git a/.github/workflows/pythonmocktests.yml b/.github/workflows/pythonmocktests.yml index 8c17b5f4..7353ceef 100644 --- a/.github/workflows/pythonmocktests.yml +++ b/.github/workflows/pythonmocktests.yml @@ -14,7 +14,7 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3c6675f4..5db2f442 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,7 +11,7 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: From 20d55e91e2c039f9624e169fe82b1fbd4e78e7ef Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 12 Aug 2023 17:49:22 +0200 Subject: [PATCH 21/62] eeprom: improve tests --- pyftdi/eeprom.py | 9 +++++++++ pyftdi/tests/eeprom.py | 1 - pyftdi/tests/eeprom_mock.py | 21 ++++++++++++++++++--- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index a1f14edf..efa56763 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -121,6 +121,7 @@ def __init__(self): self._modified = False self._chip: Optional[int] = None self._mirror = False + self._test_mode = False def __getattr__(self, name): if name in self._config: @@ -659,6 +660,10 @@ def reset_device(self): """Execute a USB device reset.""" self._ftdi.reset(usb_reset=True) + def set_test_mode(self, enable: bool): + """Enable test mode (silence some warnings).""" + self._test_mode = enable + @classmethod def _validate_string(cls, string): for invchr in ':/': @@ -841,6 +846,10 @@ def _decode_eeprom(self): if cfg['use_usb_version']: cfg['usb_version'] = \ Hex4Int(sunpack(' Date: Sat, 12 Aug 2023 18:12:24 +0200 Subject: [PATCH 22/62] Revert "Merge pull request #308 from Kalofin/fix-eeprom-strings" This reverts commit f2f619c194d53f1d7b48b63f341d91efe3bb10a6, reversing changes made to 9914488bde2f92dba293f3e626734f7c67b51217. --- pyftdi/eeprom.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index efa56763..23ee9952 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -723,7 +723,7 @@ def _generate_var_strings(self, fill=True) -> None: self._eeprom[s1_vstr_start:s1_vstr_start+len(stream)] = stream self._eeprom[dynpos:dynpos+len(stream)] = stream mtp = self._ftdi.device_version == 0x1000 - crc_pos = ( 0x100 if mtp else self._size ) - 2 + crc_pos = 0x100 if mtp else self._size rem = crc_pos - (dynpos + len(stream)) if rem < 0: oversize = (-rem + 2) // 2 From 0d20ccd1ae4542d868a252c492ba6855b4e67906 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 12 Aug 2023 18:25:05 +0200 Subject: [PATCH 23/62] doc: update GitHub action badges because breaking compatibility is always nice and keeps devs busy. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 090f1d98..abc38fab 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ # PyFtdi -![Python package](https://github.com/eblot/pyftdi/workflows/Python%20package/badge.svg) -![Mock tests](https://github.com/eblot/pyftdi/workflows/Python%20mock%20tests/badge.svg) -![Syntax tests](https://github.com/eblot/pyftdi/workflows/Python%20syntax%20tests/badge.svg) +![Python package](https://github.com/eblot/pyftdi/actions/workflows/pythonpackage.yml/badge.svg) +![Mock tests](https://github.com/eblot/pyftdi/actions/workflows/pythonmocktests.yml/badge.svg) +![Syntax tests](https://github.com/eblot/pyftdi/actions/workflows/pythonchecksyntax.yml/badge.svg) [![StandWithUkraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://vshymanskyy.github.io/StandWithUkraine) [![PyPI](https://img.shields.io/pypi/v/pyftdi.svg?maxAge=2592000)](https://pypi.org/project/pyftdi/) From ea4d2f590228eff00f92e9bb798c29185e429c48 Mon Sep 17 00:00:00 2001 From: Tobias Alex-Petersen Date: Wed, 29 Nov 2023 14:36:08 +0100 Subject: [PATCH 24/62] Documentation fix of i2c docstrings --- pyftdi/i2c.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index 8e26b6f7..b13193a5 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -117,7 +117,7 @@ def write(self, out: Union[bytes, bytearray, Iterable[int]], def read_from(self, regaddr: int, readlen: int = 0, relax: bool = True, start: bool = True) -> bytes: - """Read one or more bytes from a remote slave + """Read one or more bytes from a given register at remote slave :param regaddr: slave register address to read from :param readlen: count of bytes to read out. @@ -134,7 +134,7 @@ def read_from(self, regaddr: int, readlen: int = 0, def write_to(self, regaddr: int, out: Union[bytes, bytearray, Iterable[int]], relax: bool = True, start: bool = True): - """Read one or more bytes from a remote slave + """Write one or more bytes to a given register at a remote slave :param regaddr: slave register address to write to :param out: the byte buffer to send From 14a901f3be798993ab17c9661ed9763128464d58 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Wed, 29 Nov 2023 14:49:05 +0100 Subject: [PATCH 25/62] .github: run all CI sessions on PR --- .github/workflows/pythonchecksyntax.yml | 7 +++++-- .github/workflows/pythonmocktests.yml | 2 +- .github/workflows/pythonpackage.yml | 7 +++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 4be50301..5cc96f43 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -1,7 +1,10 @@ name: Python syntax tests # check that there is no import issues with tool suite -on: [push] +on: + push: + pull_request: + types: [assigned, opened, synchronize, reopened] jobs: build: @@ -14,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/pythonmocktests.yml b/.github/workflows/pythonmocktests.yml index 7353ceef..216c14d9 100644 --- a/.github/workflows/pythonmocktests.yml +++ b/.github/workflows/pythonmocktests.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 5db2f442..a9dad9b5 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -1,6 +1,9 @@ name: Python package -on: [push] +on: + push: + pull_request: + types: [assigned, opened, synchronize, reopened] jobs: build: @@ -13,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 4301823249bef25da875ac2dd6b7201f6b7d239b Mon Sep 17 00:00:00 2001 From: Roman Dobrodii Date: Sat, 24 Feb 2024 13:37:16 +0100 Subject: [PATCH 26/62] eeprom: fix bug with setting single-bit options - memory offset for those was computed incorrectly when clearing the bit Signed-off-by: Roman Dobrodii --- pyftdi/eeprom.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index 23ee9952..f0e70170 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -535,15 +535,14 @@ def set_property(self, name: str, value: Union[str, int, bool], val = to_bool(value, permissive=False, allow_int=True) offset, bit = confs[name] mask = 1 << bit + idx = 0x08 + offset if val: - idx = 0x08 + offset self._eeprom[idx] |= mask if self.is_mirroring_enabled: # duplicate in 'sector 2' idx2 = self.mirror_sector + idx self._eeprom[idx2] |= mask else: - idx = 0x0a + offset self._eeprom[idx] &= ~mask if self.is_mirroring_enabled: # duplicate in 'sector 2' From e68e4c36a274233744cd07c1dd910a4604094c85 Mon Sep 17 00:00:00 2001 From: Roman Dobrodii Date: Sat, 24 Feb 2024 13:39:05 +0100 Subject: [PATCH 27/62] eeprom: add support for some ft232h/ft2232h features - support setting channel_type - support setting channel_driver - support setting suspend_dbus7 - support writing eeprom chip type to eeprom Signed-off-by: Roman Dobrodii --- pyftdi/eeprom.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index f0e70170..6e2c2e1e 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -501,6 +501,66 @@ def set_property(self, name: str, value: Union[str, int, bool], self._set_group(int(mobj.group(1)), mobj.group(2), value, out) self._dirty.add(name) return + mobj = match(r'channel_([abcd])_type', name) + if mobj: + chn = mobj.group(1) + if value == 'UART': + val = 0 + else: + val = self.CHANNEL[value] + if self.device_version == 0x0700 and chn in 'ab': + # FT2232H + idx = 0x00 if chn == 'a' else 0x01 + mask = 0x07 + elif self.device_version == 0x0900 and chn == 'a': + # FT232H + idx = 0x00 + mask = 0x0F + else: + raise ValueError( + f"Option '{name}' not supported by the device") + if val & ~mask: + raise ValueError( + f"Unsupported value for setting '{name}': {val}") + self._eeprom[idx] &= ~mask + self._eeprom[idx] |= val + if self.is_mirroring_enabled: + idx2 = self.mirror_sector + idx + self._eeprom[idx2] &= ~mask + self._eeprom[idx2] |= val + self._dirty.add(name) + return + mobj = match(r'channel_([abcd])_driver', name) + if mobj: + chn = mobj.group(1) + if value == 'VCP': + val = 1 + elif value == 'D2XX': + val = 0 + else: + raise ValueError( + f"Invalid value '{value} for '{name}'") + if self.device_version == 0x0700 and chn in 'ab': + # FT2232H + idx = 0x00 if chn == 'a' else 0x01 + mask = 1 << 3 + elif self.device_version == 0x0900 and chn == 'a': + # FT232H + idx = 0x00 + mask = 1 << 4 + else: + raise ValueError( + f"Option '{name}' not supported by the device") + self._eeprom[idx] &= ~mask + if val: + self._eeprom[idx] |= mask + if self.is_mirroring_enabled: + idx2 = self.mirror_sector + idx + self._eeprom[idx2] &= ~mask + if val: + self._eeprom[idx2] |= mask + self._dirty.add(name) + return confs = { 'remote_wakeup': (0, 5), 'self_powered': (0, 6), @@ -567,6 +627,37 @@ def set_property(self, name: str, value: Union[str, int, bool], self._set_invert(name[len('invert_'):], value, out) self._dirty.add(name) return + if name == 'chip': + val = to_int(value) + idx = self._PROPERTIES[self.device_version].chipoff + if idx is None: + raise ValueError( + f"Setting '{name}' is not supported by the chip") + self._eeprom[idx] = val + if self.is_mirroring_enabled: + idx2 = self.mirror_sector + idx + self._eeprom[idx2] = val + self._dirty.add(name) + return + if name == 'suspend_dbus7': + val = to_bool(value, permissive=False, allow_int=True) + if self.device_version == 0x0700: + # FT2232H + idx = 0x01 + mask = self.CFG1.SUSPEND_DBUS7.value + self._eeprom[idx] &= ~mask + if val: + self._eeprom[idx] |= mask + if self.is_mirroring_enabled: + idx2 = self.mirror_sector + idx + self._eeprom[idx2] &= ~mask + if val: + self._eeprom[idx2] |= mask + else: + raise ValueError( + f"Setting '{name}' is not supported by the chip") + self._dirty.add(name) + return if name in self.properties: if name not in self._config: raise NotImplementedError('Change is not supported') From 28605c3b381956e1063ecb22c48af2a94ff9cd13 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 5 Apr 2024 13:33:17 +0200 Subject: [PATCH 28/62] eeprom: update author & date Signed-off-by: Emmanuel Blot --- pyftdi/doc/authors.rst | 1 + pyftdi/eeprom.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyftdi/doc/authors.rst b/pyftdi/doc/authors.rst index 8c741030..fe41a469 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -49,4 +49,5 @@ Contributors * Michael Niewöhner * Kalofin * Henry Au-Yeung + * Roman Dobrodii diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index 6e2c2e1e..a9ab314b 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2023, Emmanuel Blot +# Copyright (c) 2019-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause From 8e1f4db5564fd223aaf448d690012875b42da181 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Wed, 16 Aug 2023 12:00:04 -0400 Subject: [PATCH 29/62] tests: use module_setup to establish virtual mock environment This makes it possible to run these virtual mock tests under pytest, which does not run `main`. --- pyftdi/tests/eeprom_mock.py | 6 +++++- pyftdi/tests/gpio.py | 6 +++++- pyftdi/tests/mockusb.py | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/pyftdi/tests/eeprom_mock.py b/pyftdi/tests/eeprom_mock.py index 74ebeda6..8df89758 100644 --- a/pyftdi/tests/eeprom_mock.py +++ b/pyftdi/tests/eeprom_mock.py @@ -407,7 +407,7 @@ def virtualize(): raise AssertionError('Cannot load virtual USB backend') -def main(): +def setup_module(): import doctest doctest.testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) @@ -426,6 +426,10 @@ def main(): FtdiLogger.set_level(loglevel) FtdiLogger.set_formatter(formatter) virtualize() + + +def main(): + setup_module() try: ut_main(defaultTest='suite') except KeyboardInterrupt: diff --git a/pyftdi/tests/gpio.py b/pyftdi/tests/gpio.py index 4d159d51..4fa1d0c5 100755 --- a/pyftdi/tests/gpio.py +++ b/pyftdi/tests/gpio.py @@ -633,7 +633,7 @@ def virtualize(): raise AssertionError('Cannot load virtual USB backend') from exc -def main(): +def setup_module(): import doctest doctest.testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) @@ -652,6 +652,10 @@ def main(): FtdiLogger.set_level(loglevel) FtdiLogger.set_formatter(formatter) virtualize() + + +def main(): + setup_module() try: ut_main(defaultTest='suite') except KeyboardInterrupt: diff --git a/pyftdi/tests/mockusb.py b/pyftdi/tests/mockusb.py index 57d03e46..a6bc2a4e 100755 --- a/pyftdi/tests/mockusb.py +++ b/pyftdi/tests/mockusb.py @@ -839,7 +839,7 @@ def suite(): return suite_ -def main(): +def setup_module(): testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) if debug: @@ -866,6 +866,10 @@ def main(): MockLoader = backend.create_loader() except AttributeError as exc: raise AssertionError('Cannot load virtual USB backend') from exc + + +def main(): + setup_module() ut_main(defaultTest='suite') From e2b4ac4e8fb2f7fd6dddc467d76acc8a421c8844 Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Wed, 16 Aug 2023 14:23:52 -0400 Subject: [PATCH 30/62] tests: don't present untestable base classes via pytest pytest collects tests in classes that either derive from `unittest.TestCase` or are named with a `Test` prefix, where functions have a `test` prefix. More details on this logic are given at https://docs.pytest.org/en/7.4.x/explanation/goodpractices.html#test-discovery. pyftdi's `eeprom_mock` test has two base classes, `EepromMirrorTestCase` and `NonMirroredEepromTestCase`, that derive from `unittest.TestCase` and contain `test`-prefixed functions. These tests cannot be run from within the base classes, because they depend on a variable, `TEST_CONFIG_FILENAME`, which is expected to be set in their subclasses. The tests can only run from within the subclasses, and will produce an error result when attempting to run them from the base classes. To prevent pytest from collecting the untestable base class tests, the base classes are changed to not derive from `unittest.TestCase`, preferring instead to use multiple inheritance to allow the subclasses to derive from that class. This makes it possible to easily run the `eeprom_mock` test via pytest without spurious errors related to the untestable base classes. --- pyftdi/tests/eeprom_mock.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyftdi/tests/eeprom_mock.py b/pyftdi/tests/eeprom_mock.py index 8df89758..a5b73cee 100644 --- a/pyftdi/tests/eeprom_mock.py +++ b/pyftdi/tests/eeprom_mock.py @@ -18,7 +18,7 @@ VirtLoader = None -class FtdiTestCase(TestCase): +class FtdiTestCase: """Common features for all tests. """ @@ -338,39 +338,39 @@ def _check_for_non_mirrored_eeprom_contents(self, eeprom: FtdiEeprom): self.assertNotEqual(normal_mirror_s1, normal_mirror_s2) -class EepromMirrorFt232hTestCase(EepromMirrorTestCase): +class EepromMirrorFt232hTestCase(EepromMirrorTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' -class EepromMirrorFt2232hTestCase(EepromMirrorTestCase): +class EepromMirrorFt2232hTestCase(EepromMirrorTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' -class EepromMirrorFt4232hTestCase(EepromMirrorTestCase): +class EepromMirrorFt4232hTestCase(EepromMirrorTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft4232h.yaml' -class EepromMirrorFt232rTestCase(NonMirroredEepromTestCase): +class EepromMirrorFt232rTestCase(NonMirroredEepromTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232r.yaml' DEVICE_CAN_MIRROR = False -class EepromMirrorFt230xTestCase(NonMirroredEepromTestCase): +class EepromMirrorFt230xTestCase(NonMirroredEepromTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft230x.yaml' DEVICE_CAN_MIRROR = False -class EepromNonMirroredFt232hTestCase(NonMirroredEepromTestCase): +class EepromNonMirroredFt232hTestCase(NonMirroredEepromTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' DEVICE_CAN_MIRROR = True -class EepromNonMirroredFt2232hTestCase(NonMirroredEepromTestCase): +class EepromNonMirroredFt2232hTestCase(NonMirroredEepromTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' DEVICE_CAN_MIRROR = True -class EepromNonMirroredFt4232hTestCase(NonMirroredEepromTestCase): +class EepromNonMirroredFt4232hTestCase(NonMirroredEepromTestCase, TestCase): TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft4232h.yaml' DEVICE_CAN_MIRROR = True From b06886a91a27e184c78901cb5ef77502596c1fec Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 5 Apr 2024 13:49:16 +0200 Subject: [PATCH 31/62] eeprom: update author & date Signed-off-by: Emmanuel Blot --- pyftdi/doc/authors.rst | 2 +- pyftdi/tests/eeprom.py | 2 +- pyftdi/tests/gpio.py | 2 +- pyftdi/tests/mockusb.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyftdi/doc/authors.rst b/pyftdi/doc/authors.rst index fe41a469..4a644be6 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -50,4 +50,4 @@ Contributors * Kalofin * Henry Au-Yeung * Roman Dobrodii - + * Mark Mentovai diff --git a/pyftdi/tests/eeprom.py b/pyftdi/tests/eeprom.py index 456e4ea9..e21b4742 100755 --- a/pyftdi/tests/eeprom.py +++ b/pyftdi/tests/eeprom.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2018, Stephen Goadhouse -# Copyright (c) 2019, Emmanuel Blot +# Copyright (c) 2019-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/pyftdi/tests/gpio.py b/pyftdi/tests/gpio.py index 4fa1d0c5..0185dbbb 100755 --- a/pyftdi/tests/gpio.py +++ b/pyftdi/tests/gpio.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2016-2023, Emmanuel Blot +# Copyright (c) 2016-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/pyftdi/tests/mockusb.py b/pyftdi/tests/mockusb.py index a6bc2a4e..37ac7919 100755 --- a/pyftdi/tests/mockusb.py +++ b/pyftdi/tests/mockusb.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2020-2023, Emmanuel Blot +# Copyright (c) 2020-2024, Emmanuel Blot # All rights reserved. #pylint: disable-msg=empty-docstring From bee78e3df837612f8fa3710ef86ee8214a19653d Mon Sep 17 00:00:00 2001 From: Alessandro Zini Date: Thu, 4 Apr 2024 16:49:14 +0200 Subject: [PATCH 32/62] eeprom: add support for some ft4232h features - support setting channel_type - support setting channel_driver - support writing eeprom chip type to eeprom Missing: - support setting suspend_dbus7 Signed-off-by: Alessandro Zini --- pyftdi/eeprom.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index a9ab314b..aede2090 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -512,6 +512,11 @@ def set_property(self, name: str, value: Union[str, int, bool], # FT2232H idx = 0x00 if chn == 'a' else 0x01 mask = 0x07 + elif self.device_version == 0x0800: + # FT4232H + idx = 0x0b + mask = 1 << {'a': 4, 'b': 5, 'c': 6, 'd': 7}.get(chn) + val = mask if val > 0 else 0 elif self.device_version == 0x0900 and chn == 'a': # FT232H idx = 0x00 @@ -544,6 +549,10 @@ def set_property(self, name: str, value: Union[str, int, bool], # FT2232H idx = 0x00 if chn == 'a' else 0x01 mask = 1 << 3 + elif self.device_version == 0x0800: + # FT4232H + idx = {'a': 0, 'b': 1, 'c': 0, 'd': 1}.get(chn) + mask = 1 << {'a': 3, 'b': 3, 'c': 7, 'd': 7}.get(chn) elif self.device_version == 0x0900 and chn == 'a': # FT232H idx = 0x00 @@ -1218,7 +1227,8 @@ def _decode_4232h(self): conf = self._eeprom[0x0B] rs485 = self.CHANNEL.RS485 for chix in range(4): - cfg[f'channel_{0xa+chix:x}_rs485'] = bool(conf & (rs485 << chix)) + cfg[f'channel_{0xa+chix:x}_type'] = ( + 'RS485' if conf & (rs485 << chix) else 'UART') def _decode_x232h(self, cfg): # common code for 2232h, 4232h, 4232ha From 9834c46498da79b3851bf9fc2252305badcab473 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 5 Apr 2024 14:06:02 +0200 Subject: [PATCH 33/62] eeprom: update authors --- pyftdi/doc/authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/pyftdi/doc/authors.rst b/pyftdi/doc/authors.rst index 4a644be6..00f3da7c 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -51,3 +51,4 @@ Contributors * Henry Au-Yeung * Roman Dobrodii * Mark Mentovai + * Alessandro Zini From 5ca35f42b28e5532c0c65961cbaf3e965653c9f2 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Fri, 5 Apr 2024 14:26:42 +0200 Subject: [PATCH 34/62] pylint: create a global configuration file Move all too-few / too-many arbitrary warnings into this config file. Signed-off-by: Emmanuel Blot --- .flake8 | 2 ++ .pylintrc | 12 ++++++++++++ pyftdi/__init__.py | 2 +- pyftdi/bin/ftconf.py | 8 ++++---- pyftdi/bin/ftdi_urls.py | 2 +- pyftdi/bin/i2cscan.py | 7 +++---- pyftdi/bin/pyterm.py | 16 +++++----------- pyftdi/bits.py | 11 +++++------ pyftdi/eeprom.py | 9 ++------- pyftdi/ftdi.py | 22 ++++++---------------- pyftdi/gpio.py | 3 +-- pyftdi/i2c.py | 11 +---------- pyftdi/jtag.py | 4 ++-- pyftdi/misc.py | 12 +++++------- pyftdi/serialext/logger.py | 12 ++++++------ pyftdi/serialext/protocol_ftdi.py | 8 ++++---- pyftdi/serialext/protocol_unix.py | 21 ++++++++++----------- pyftdi/serialext/tests/rl.py | 2 +- pyftdi/spi.py | 10 ++-------- pyftdi/term.py | 2 +- pyftdi/tests/backend/consts.py | 7 +++---- pyftdi/tests/backend/ftdivirt.py | 17 +++++------------ pyftdi/tests/backend/loader.py | 12 ++++++------ pyftdi/tests/backend/usbvirt.py | 11 ++++------- pyftdi/tests/bits.py | 2 +- pyftdi/tests/cbus.py | 4 ++-- pyftdi/tests/eeprom.py | 2 +- pyftdi/tests/gpio.py | 8 ++++---- pyftdi/tests/i2c.py | 6 +++--- pyftdi/tests/jtag.py | 2 +- pyftdi/tests/mockusb.py | 12 ++++++------ pyftdi/tests/spi.py | 6 +++--- pyftdi/tests/toolsimport.py | 10 +++++----- pyftdi/tests/uart.py | 12 ++++++------ pyftdi/tracer.py | 5 ++--- pyftdi/usbtools.py | 12 +++++------- setup.py | 6 +++--- 37 files changed, 134 insertions(+), 176 deletions(-) create mode 100644 .flake8 create mode 100644 .pylintrc diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..15fc7e33 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 80 diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..60b70871 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,12 @@ +[MESSAGES CONTROL] + +disable= + too-few-public-methods, + too-many-arguments, + too-many-branches, + too-many-instance-attributes, + too-many-lines, + too-many-locals, + too-many-nested-blocks, + too-many-public-methods, + too-many-statements diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 35e04ed4..2f39aca8 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -4,7 +4,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=missing-docstring +# pylint: disable=missing-docstring __version__ = '0.55.1' __title__ = 'PyFtdi' diff --git a/pyftdi/bin/ftconf.py b/pyftdi/bin/ftconf.py index cce9635e..bbe88fc9 100755 --- a/pyftdi/bin/ftconf.py +++ b/pyftdi/bin/ftconf.py @@ -19,9 +19,9 @@ from pyftdi.ftdi import Ftdi from pyftdi.misc import add_custom_devices, hexdump -#pylint: disable-msg=too-many-locals -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-statements +# pylint: disable=too-many-locals +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements def main(): @@ -107,7 +107,7 @@ def main(): FtdiLogger.log.addHandler(StreamHandler(stderr)) if args.virtual: - #pylint: disable-msg=import-outside-toplevel + # pylint: disable=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', ) diff --git a/pyftdi/bin/ftdi_urls.py b/pyftdi/bin/ftdi_urls.py index 1297e602..eccb9bbb 100755 --- a/pyftdi/bin/ftdi_urls.py +++ b/pyftdi/bin/ftdi_urls.py @@ -45,7 +45,7 @@ def main(): FtdiLogger.log.addHandler(StreamHandler(stderr)) if args.virtual: - #pylint: disable-msg=import-outside-toplevel + # pylint: disable=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', ) diff --git a/pyftdi/bin/i2cscan.py b/pyftdi/bin/i2cscan.py index 1039c34d..e062b42f 100755 --- a/pyftdi/bin/i2cscan.py +++ b/pyftdi/bin/i2cscan.py @@ -1,15 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2018-2022, Emmanuel Blot +# Copyright (c) 2018-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """Tiny I2C bus scanner.""" -#pylint: disable-msg=broad-except -#pylint: disable-msg=too-few-public-methods +# pylint: disable=broad-except from argparse import ArgumentParser, FileType from logging import Formatter, StreamHandler, getLogger, DEBUG, ERROR @@ -124,7 +123,7 @@ def main(): FtdiLogger.set_level(loglevel) if args.virtual: - #pylint: disable-msg=import-outside-toplevel + # pylint: disable=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('pyftdi.tests.backend.usbvirt', ) diff --git a/pyftdi/bin/pyterm.py b/pyftdi/bin/pyterm.py index 73ad2eb4..d9fa3c85 100755 --- a/pyftdi/bin/pyterm.py +++ b/pyftdi/bin/pyterm.py @@ -3,20 +3,14 @@ """Simple Python serial terminal """ -# Copyright (c) 2010-2022, Emmanuel Blot +# Copyright (c) 2010-2024, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=too-many-instance-attributes -#pylint: disable-msg=too-many-arguments -#pylint: disable-msg=too-many-nested-blocks -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-statements -#pylint: disable-msg=too-few-public-methods -#pylint: disable-msg=broad-except -#pylint: disable-msg=wrong-import-position +# pylint: disable=broad-except +# pylint: disable=wrong-import-position from argparse import ArgumentParser, FileType from atexit import register @@ -30,8 +24,8 @@ from traceback import format_exc from _thread import interrupt_main -#pylint: disable-msg=import-error -#pylint: disable-msg=import-outside-toplevel +# pylint: disable=import-error +# pylint: disable=import-outside-toplevel from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi diff --git a/pyftdi/bits.py b/pyftdi/bits.py index f646f618..8760b224 100644 --- a/pyftdi/bits.py +++ b/pyftdi/bits.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2019 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # @@ -9,11 +9,10 @@ from typing import Iterable, List, Optional, Tuple, Union from .misc import is_iterable, xor -#pylint: disable-msg=invalid-name -#pylint: disable-msg=unneeded-not -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-arguments -#pylint: disable-msg=duplicate-key +# pylint: disable=invalid-name +# pylint: disable=unneeded-not +# pylint: disable=duplicate-key + class BitSequenceError(Exception): """Bit sequence error""" diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index aede2090..c894c15f 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -5,13 +5,8 @@ """EEPROM management for PyFdti""" -#pylint: disable-msg=too-many-arguments -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-instance-attributes -#pylint: disable-msg=too-many-locals -#pylint: disable-msg=too-many-public-methods -#pylint: disable-msg=wrong-import-position -#pylint: disable-msg=import-error +# pylint: disable=wrong-import-position +# pylint: disable=import-error import sys from binascii import hexlify, unhexlify diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index c6e147b6..620624db 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2023 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2016 Emmanuel Bouaziz # All rights reserved. # @@ -21,17 +21,7 @@ from .misc import to_bool from .usbtools import UsbDeviceDescriptor, UsbTools -#pylint: disable-msg=invalid-name -#pylint: disable-msg=too-many-arguments -#pylint: disable=too-many-arguments -#pylint: disable=too-many-branches -#pylint: disable=too-many-statements -#pylint: disable=too-many-nested-blocks -#pylint: disable=too-many-instance-attributes -#pylint: disable=too-many-nested-blocks -#pylint: disable=too-many-public-methods -#pylint: disable=too-many-locals -#pylint: disable=too-many-lines +# pylint: disable=invalid-name class FtdiError(IOError): @@ -730,13 +720,13 @@ def open_mpsse_from_device(self, device: UsbDevice, :param bool debug: add more debug traces :return: actual bus frequency in Hz """ - # pylint: disable-msg=unused-argument + # pylint: disable=unused-argument self.open_from_device(device, interface) if not self.is_mpsse_interface(interface): self.close() raise FtdiMpsseError('This interface does not support MPSSE') if to_bool(tracer): # accept strings as boolean - #pylint: disable-msg=import-outside-toplevel + # pylint: disable=import-outside-toplevel from .tracer import FtdiMpsseTracer self._tracer = FtdiMpsseTracer(self.device_version) self.log.debug('Using MPSSE tracer') @@ -2044,7 +2034,7 @@ def _set_interface(self, config: UsbConfiguration, ifnum: int): except (NotImplementedError, USBError): pass -#pylint: disable-msg=protected-access +# pylint: disable=protected-access # need to access private member _ctx of PyUSB device (resource manager) # until PyUSB #302 is addressed @@ -2057,7 +2047,7 @@ def _is_pyusb_handle_active(self) -> bool: # and there is no public API for this. return bool(self._usb_dev._ctx.handle) -#pylint: enable-msg=protected-access +# pylint: enable-msg=protected-access def _reset_device(self): """Reset the FTDI device (FTDI vendor command)""" diff --git a/pyftdi/gpio.py b/pyftdi/gpio.py index 0ddbd1d6..5c794302 100644 --- a/pyftdi/gpio.py +++ b/pyftdi/gpio.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014-2020, Emmanuel Blot +# Copyright (c) 2014-2024, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # @@ -6,7 +6,6 @@ """GPIO/BitBang support for PyFdti""" -#pylint: disable-msg=too-few-public-methods from struct import calcsize as scalc, unpack as sunpack from typing import Iterable, Optional, Tuple, Union diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index b13193a5..b084e10d 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -1,19 +1,10 @@ -# Copyright (c) 2017-2021, Emmanuel Blot +# Copyright (c) 2017-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """I2C support for PyFdti""" -#pylint: disable-msg=too-many-lines -#pylint: disable-msg=too-many-locals -#pylint: disable-msg=too-many-instance-attributes -#pylint: disable-msg=too-many-public-methods -#pylint: disable-msg=too-many-arguments -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-statements - - from binascii import hexlify from collections import namedtuple from logging import getLogger diff --git a/pyftdi/jtag.py b/pyftdi/jtag.py index caf6b9a2..c6fe065e 100644 --- a/pyftdi/jtag.py +++ b/pyftdi/jtag.py @@ -6,8 +6,8 @@ """JTAG support for PyFdti""" -#pylint: disable-msg=invalid-name -#pylint: disable-msg=missing-function-docstring +# pylint: disable=invalid-name +# pylint: disable=missing-function-docstring from time import sleep from typing import List, Tuple, Union diff --git a/pyftdi/misc.py b/pyftdi/misc.py index a959a34b..b2e2427a 100644 --- a/pyftdi/misc.py +++ b/pyftdi/misc.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2021 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # @@ -6,10 +6,8 @@ """Miscellaneous helpers""" -#pylint: disable-msg=invalid-name -#pylint: disable-msg=import-outside-toplevel -#pylint: disable-msg=too-many-locals -#pylint: disable-msg=too-many-arguments +# pylint: disable=invalid-name +# pylint: disable=import-outside-toplevel from array import array from copy import deepcopy @@ -198,7 +196,7 @@ def xor(_a_: bool, _b_: bool) -> bool: :param _b_: second argument :return: xor-ed value """ - #pylint: disable-msg=superfluous-parens + # pylint: disable=superfluous-parens return bool((not(_a_) and _b_) or (_a_ and not(_b_))) @@ -311,7 +309,7 @@ def show_call_stack(): class classproperty(property): """Getter property decorator for a class""" - #pylint: disable=invalid-name + # pylint: disable=invalid-name def __get__(self, obj: Any, objtype=None) -> Any: return super().__get__(objtype) diff --git a/pyftdi/serialext/logger.py b/pyftdi/serialext/logger.py index 6736c7f7..80afece4 100644 --- a/pyftdi/serialext/logger.py +++ b/pyftdi/serialext/logger.py @@ -4,12 +4,12 @@ # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=no-member -#pylint: disable-msg=broad-except -#pylint: disable-msg=invalid-name -#pylint: disable-msg=super-with-arguments -#pylint: disable-msg=missing-function-docstring -#pylint: disable-msg=missing-module-docstring +# pylint: disable=no-member +# pylint: disable=broad-except +# pylint: disable=invalid-name +# pylint: disable=super-with-arguments +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring from sys import stderr from time import time diff --git a/pyftdi/serialext/protocol_ftdi.py b/pyftdi/serialext/protocol_ftdi.py index 4a59674a..3c409432 100644 --- a/pyftdi/serialext/protocol_ftdi.py +++ b/pyftdi/serialext/protocol_ftdi.py @@ -5,10 +5,10 @@ # SPDX-License-Identifier: BSD-3-Clause # this file has not been updated for a while, so coding style needs some love -#pylint: disable-msg=attribute-defined-outside-init -#pylint: disable-msg=invalid-name -#pylint: disable-msg=missing-class-docstring -#pylint: disable-msg=missing-module-docstring +# pylint: disable=attribute-defined-outside-init +# pylint: disable=invalid-name +# pylint: disable=missing-class-docstring +# pylint: disable=missing-module-docstring from io import RawIOBase from time import sleep, time as now diff --git a/pyftdi/serialext/protocol_unix.py b/pyftdi/serialext/protocol_unix.py index fa679952..1875212d 100644 --- a/pyftdi/serialext/protocol_unix.py +++ b/pyftdi/serialext/protocol_unix.py @@ -1,18 +1,17 @@ -# Copyright (c) 2008-2016, Emmanuel Blot +# Copyright (c) 2008-2024, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause # this file has not been updated for a while, so coding style needs some love -#pylint: disable-msg=broad-except -#pylint: disable-msg=attribute-defined-outside-init -#pylint: disable-msg=redefined-outer-name -#pylint: disable-msg=invalid-name -#pylint: disable-msg=too-few-public-methods -#pylint: disable-msg=missing-function-docstring -#pylint: disable-msg=missing-class-docstring -#pylint: disable-msg=missing-module-docstring +# pylint: disable=broad-except +# pylint: disable=attribute-defined-outside-init +# pylint: disable=redefined-outer-name +# pylint: disable=invalid-name +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=missing-module-docstring import errno import os @@ -70,7 +69,7 @@ def open(self): self.close() msg = "Could not open port: %s" % (str(e),) if isinstance(e, socket.error): - # pylint: disable-msg=no-member + # pylint: disable=no-member raise SerialExceptionWithErrno(msg, e.errno) from e raise SerialException(msg) from e self._set_open_state(True) @@ -91,7 +90,7 @@ def close(self): def in_waiting(self): """Return the number of characters currently in the input buffer.""" - #pylint: disable-msg=no-self-use + # pylint: disable=no-self-use return 0 def read(self, size=1): diff --git a/pyftdi/serialext/tests/rl.py b/pyftdi/serialext/tests/rl.py index 921fdca6..ea95c29b 100644 --- a/pyftdi/serialext/tests/rl.py +++ b/pyftdi/serialext/tests/rl.py @@ -8,7 +8,7 @@ path.append(dirname(dirname(dirname(dirname(__file__))))) -#pylint: disable-msg=wrong-import-position +# pylint: disable=wrong-import-position from pyftdi import serialext diff --git a/pyftdi/spi.py b/pyftdi/spi.py index a24d828e..b9c67e31 100644 --- a/pyftdi/spi.py +++ b/pyftdi/spi.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2020, Emmanuel Blot +# Copyright (c) 2010-2024, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # @@ -6,13 +6,7 @@ """SPI support for PyFdti""" -#pylint: disable-msg=too-many-arguments -#pylint: disable-msg=too-many-locals -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-statements -#pylint: disable-msg=too-many-instance-attributes -#pylint: disable-msg=too-many-public-methods -#pylint: disable-msg=invalid-name +# pylint: disable=invalid-name from logging import getLogger from struct import calcsize as scalc, pack as spack, unpack as sunpack diff --git a/pyftdi/term.py b/pyftdi/term.py index e179d6e1..ccd63bda 100755 --- a/pyftdi/term.py +++ b/pyftdi/term.py @@ -9,7 +9,7 @@ from os import environ, read as os_read from sys import platform, stderr, stdin, stdout -#pylint: disable-msg=import-error +# pylint: disable=import-error if platform == 'win32': import msvcrt from subprocess import call # ugly workaround for an ugly OS diff --git a/pyftdi/tests/backend/consts.py b/pyftdi/tests/backend/consts.py index 91016bdd..99adc30a 100644 --- a/pyftdi/tests/backend/consts.py +++ b/pyftdi/tests/backend/consts.py @@ -1,13 +1,12 @@ """Constant importer from existing modules.""" -# Copyright (c) 2020-2021, Emmanuel Blot +# Copyright (c) 2020-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=invalid-name -#pylint: disable-msg=too-many-instance-attributes +# pylint: disable=missing-docstring +# pylint: disable=invalid-name from enum import Enum from importlib import import_module diff --git a/pyftdi/tests/backend/ftdivirt.py b/pyftdi/tests/backend/ftdivirt.py index 13875bb6..8c5151b5 100644 --- a/pyftdi/tests/backend/ftdivirt.py +++ b/pyftdi/tests/backend/ftdivirt.py @@ -1,21 +1,14 @@ """PyUSB virtual FTDI device.""" -# Copyright (c) 2020-2023, Emmanuel Blot +# Copyright (c) 2020-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=unused-argument -#pylint: disable-msg=invalid-name -#pylint: disable-msg=too-many-arguments -#pylint: disable-msg=too-many-locals -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-statements -#pylint: disable-msg=too-many-instance-attributes -#pylint: disable-msg=too-many-public-methods -#pylint: disable-msg=too-few-public-methods -#pylint: disable-msg=no-self-use +# pylint: disable=missing-docstring +# pylint: disable=unused-argument +# pylint: disable=invalid-name +# pylint: disable=no-self-use import os from array import array diff --git a/pyftdi/tests/backend/loader.py b/pyftdi/tests/backend/loader.py index 4e0bfccd..dadb64f6 100644 --- a/pyftdi/tests/backend/loader.py +++ b/pyftdi/tests/backend/loader.py @@ -6,12 +6,12 @@ # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=too-few-public-methods -#pylint: disable-msg=too-many-branches -#pylint: disable-msg=too-many-statements -#pylint: disable-msg=too-many-nested-blocks -#pylint: disable-msg=no-self-use +# pylint: disable=missing-docstring +# pylint: disable=too-few-public-methods +# pylint: disable=too-many-branches +# pylint: disable=too-many-statements +# pylint: disable=too-many-nested-blocks +# pylint: disable=no-self-use from binascii import unhexlify from logging import getLogger diff --git a/pyftdi/tests/backend/usbvirt.py b/pyftdi/tests/backend/usbvirt.py index d3becd88..af5f472f 100644 --- a/pyftdi/tests/backend/usbvirt.py +++ b/pyftdi/tests/backend/usbvirt.py @@ -4,17 +4,14 @@ hardware. """ -# Copyright (c) 2020-2021, Emmanuel Blot +# Copyright (c) 2020-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=invalid-name -#pylint: disable-msg=attribute-defined-outside-init -#pylint: disable-msg=too-many-locals -#pylint: disable-msg=too-many-arguments -#pylint: disable-msg=too-many-instance-attributes +# pylint: disable=missing-docstring +# pylint: disable=invalid-name +# pylint: disable=attribute-defined-outside-init from array import array from binascii import hexlify diff --git a/pyftdi/tests/bits.py b/pyftdi/tests/bits.py index a3c4b6c7..9764c48b 100755 --- a/pyftdi/tests/bits.py +++ b/pyftdi/tests/bits.py @@ -7,7 +7,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=broad-except +# pylint: disable=broad-except import unittest from pyftdi.bits import BitSequence, BitZSequence, BitSequenceError diff --git a/pyftdi/tests/cbus.py b/pyftdi/tests/cbus.py index 46f2a7fd..726f47d5 100755 --- a/pyftdi/tests/cbus.py +++ b/pyftdi/tests/cbus.py @@ -13,8 +13,8 @@ from pyftdi.ftdi import Ftdi, FtdiError from pyftdi.eeprom import FtdiEeprom -#pylint: disable-msg=empty-docstring -#pylint: disable-msg=missing-docstring +# pylint: disable=empty-docstring +# pylint: disable=missing-docstring class CbusGpioTestCase(TestCase): diff --git a/pyftdi/tests/eeprom.py b/pyftdi/tests/eeprom.py index e21b4742..cd3e6e0d 100755 --- a/pyftdi/tests/eeprom.py +++ b/pyftdi/tests/eeprom.py @@ -16,7 +16,7 @@ from pyftdi.ftdi import Ftdi from pyftdi.misc import hexdump, to_bool -#pylint: disable-msg=missing-docstring +# pylint: disable=missing-docstring class EepromTestCase(unittest.TestCase): diff --git a/pyftdi/tests/gpio.py b/pyftdi/tests/gpio.py index 0185dbbb..979e83bf 100755 --- a/pyftdi/tests/gpio.py +++ b/pyftdi/tests/gpio.py @@ -6,10 +6,10 @@ # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=empty-docstring -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=invalid-name -#pylint: disable-msg=global-statement +# pylint: disable=empty-docstring +# pylint: disable=missing-docstring +# pylint: disable=invalid-name +# pylint: disable=global-statement import logging from collections import deque diff --git a/pyftdi/tests/i2c.py b/pyftdi/tests/i2c.py index 64b7c73d..24b786e3 100755 --- a/pyftdi/tests/i2c.py +++ b/pyftdi/tests/i2c.py @@ -16,9 +16,9 @@ from pyftdi.i2c import I2cController, I2cIOError from pyftdi.misc import pretty_size -#pylint: disable-msg=attribute-defined-outside-init -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=no-self-use +# pylint: disable=attribute-defined-outside-init +# pylint: disable=missing-docstring +# pylint: disable=no-self-use class I2cTca9555TestCase(TestCase): diff --git a/pyftdi/tests/jtag.py b/pyftdi/tests/jtag.py index ff13936f..a87db2a9 100755 --- a/pyftdi/tests/jtag.py +++ b/pyftdi/tests/jtag.py @@ -11,7 +11,7 @@ from pyftdi.jtag import JtagEngine, JtagTool from pyftdi.bits import BitSequence -#pylint: disable-msg=missing-docstring +# pylint: disable=missing-docstring # Should match the tested device diff --git a/pyftdi/tests/mockusb.py b/pyftdi/tests/mockusb.py index 37ac7919..e90477b8 100755 --- a/pyftdi/tests/mockusb.py +++ b/pyftdi/tests/mockusb.py @@ -4,12 +4,12 @@ # Copyright (c) 2020-2024, Emmanuel Blot # All rights reserved. -#pylint: disable-msg=empty-docstring -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=no-self-use -#pylint: disable-msg=invalid-name -#pylint: disable-msg=global-statement -#pylint: disable-msg=too-many-locals +# pylint: disable=empty-docstring +# pylint: disable=missing-docstring +# pylint: disable=no-self-use +# pylint: disable=invalid-name +# pylint: disable=global-statement +# pylint: disable=too-many-locals import logging diff --git a/pyftdi/tests/spi.py b/pyftdi/tests/spi.py index 1753c872..feaf0bbb 100755 --- a/pyftdi/tests/spi.py +++ b/pyftdi/tests/spi.py @@ -6,9 +6,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=empty-docstring -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=no-self-use +# pylint: disable=empty-docstring +# pylint: disable=missing-docstring +# pylint: disable=no-self-use import logging import unittest diff --git a/pyftdi/tests/toolsimport.py b/pyftdi/tests/toolsimport.py index 64a28eb5..2cc29128 100755 --- a/pyftdi/tests/toolsimport.py +++ b/pyftdi/tests/toolsimport.py @@ -4,11 +4,11 @@ # Copyright (c) 2020, Emmanuel Blot # All rights reserved. -#pylint: disable-msg=empty-docstring -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=no-self-use -#pylint: disable-msg=invalid-name -#pylint: disable-msg=global-statement +# pylint: disable=empty-docstring +# pylint: disable=missing-docstring +# pylint: disable=no-self-use +# pylint: disable=invalid-name +# pylint: disable=global-statement from doctest import testmod from importlib import import_module diff --git a/pyftdi/tests/uart.py b/pyftdi/tests/uart.py index 3a248029..f9e6b0e2 100755 --- a/pyftdi/tests/uart.py +++ b/pyftdi/tests/uart.py @@ -22,8 +22,8 @@ from pyftdi.misc import to_bool from pyftdi.serialext import serial_for_url -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=protected-access +# pylint: disable=missing-docstring +# pylint: disable=protected-access # Specify the second port for multi port device # Unfortunately, auto detection triggers some issue in multiprocess test @@ -174,9 +174,9 @@ def test2_uart_cross_talk_speed(self): sleep(0.5) sink.join() if isinstance(results[1], Exception): - #pylint: disable-msg=raising-bad-type + # pylint: disable=raising-bad-type raise results[1] - #pylint: disable-msg=unpacking-non-sequence + # pylint: disable=unpacking-non-sequence tsize, tdelta = results[0] rsize, rdelta = results[1] self.assertGreater(rsize, 0, 'Not data received') @@ -205,9 +205,9 @@ def test_loopback_talk_speed(self): sleep(0.5) sink.join() if isinstance(results[1], Exception): - #pylint: disable-msg=raising-bad-type + # pylint: disable=raising-bad-type raise results[1] - #pylint: disable-msg=unpacking-non-sequence + # pylint: disable=unpacking-non-sequence tsize, tdelta = results[0] rsize, rdelta = results[1] self.assertGreater(rsize, 0, 'Not data received') diff --git a/pyftdi/tracer.py b/pyftdi/tracer.py index 8b414da2..cf98cc12 100644 --- a/pyftdi/tracer.py +++ b/pyftdi/tracer.py @@ -1,12 +1,11 @@ -# Copyright (c) 2017-2023, Emmanuel Blot +# Copyright (c) 2017-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """MPSSE command debug tracer.""" -#pylint: disable-msg=missing-docstring -#pylint: disable-msg=too-many-instance-attributes +# pylint: disable=missing-docstring from binascii import hexlify from collections import deque diff --git a/pyftdi/usbtools.py b/pyftdi/usbtools.py index 5a1d57bc..951ecc9a 100644 --- a/pyftdi/usbtools.py +++ b/pyftdi/usbtools.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014-2023, Emmanuel Blot +# Copyright (c) 2014-2024, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # @@ -18,9 +18,7 @@ from usb.util import dispose_resources, get_string as usb_get_string from .misc import to_int -#pylint: disable-msg=broad-except -#pylint: disable-msg=too-many-locals,too-many-branches,too-many-statements -#pylint: disable-msg=too-many-arguments, too-many-nested-blocks +# pylint: disable=broad-except UsbDeviceDescriptor = NamedTuple('UsbDeviceDescriptor', (('vid', int), @@ -543,7 +541,7 @@ def get_string(cls, device: UsbDevice, stridx: int) -> str: :return: the string read from the USB device """ if cls.UsbApi is None: - #pylint: disable-msg=import-outside-toplevel + # pylint: disable=import-outside-toplevel import inspect args, _, _, _ = \ inspect.signature(UsbDevice.read).parameters @@ -644,11 +642,11 @@ def _get_backend_device(cls, device: UsbDevice) -> Any: :return: the implementation of any """ try: - #pylint: disable-msg=protected-access + # pylint: disable=protected-access # need to access private member _ctx of PyUSB device # (resource manager) until PyUSB #302 is addressed return device._ctx.dev - #pylint: disable-msg=protected-access + # pylint: disable=protected-access except AttributeError: return None diff --git a/setup.py b/setup.py index 6075fc2d..8d207332 100755 --- a/setup.py +++ b/setup.py @@ -7,9 +7,9 @@ # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable=unused-variable -#pylint: disable=missing-docstring -#pylint: disable=broad-except +# pylint: disable=unused-variable +# pylint: disable=missing-docstring +# pylint: disable=broad-except from codecs import open as codec_open from os import close, getcwd, unlink, walk From 6b2c74945ee2db23a2e4285c03f8bbdba09ab42c Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 6 Apr 2024 11:24:09 +0200 Subject: [PATCH 35/62] code cleanup remove deprecated Python syntax Signed-off-by: Emmanuel Blot --- .pylintrc | 8 +- LICENSE | 2 +- pyftdi/__init__.py | 6 +- pyftdi/bin/ftconf.py | 25 +++--- pyftdi/bin/ftdi_urls.py | 10 +-- pyftdi/bin/i2cscan.py | 16 ++-- pyftdi/bin/pyterm.py | 38 ++++----- pyftdi/bits.py | 15 ++-- pyftdi/eeprom.py | 32 ++++---- pyftdi/ftdi.py | 124 +++++++++++++++--------------- pyftdi/gpio.py | 10 +-- pyftdi/i2c.py | 13 ++-- pyftdi/jtag.py | 24 +++--- pyftdi/misc.py | 33 ++++---- pyftdi/serialext/__init__.py | 4 +- pyftdi/serialext/logger.py | 63 ++++++++------- pyftdi/serialext/protocol_ftdi.py | 12 +-- pyftdi/serialext/protocol_unix.py | 39 +++++----- pyftdi/spi.py | 15 ++-- pyftdi/term.py | 17 ++-- pyftdi/tests/backend/consts.py | 2 - pyftdi/tests/backend/ftdivirt.py | 15 ++-- pyftdi/tests/backend/loader.py | 13 ++-- pyftdi/tests/backend/mpsse.py | 8 +- pyftdi/tests/backend/usbvirt.py | 21 ++--- pyftdi/tests/bits.py | 56 +++++++------- pyftdi/tests/cbus.py | 4 +- pyftdi/tests/eeprom.py | 7 +- pyftdi/tests/eeprom_mock.py | 56 ++++++++------ pyftdi/tests/ftdi.py | 8 +- pyftdi/tests/gpio.py | 43 +++++------ pyftdi/tests/i2c.py | 25 +++--- pyftdi/tests/jtag.py | 22 +++--- pyftdi/tests/mockusb.py | 52 ++++++------- pyftdi/tests/spi.py | 5 +- pyftdi/tests/timearray.py | 88 --------------------- pyftdi/tests/toolsimport.py | 7 +- pyftdi/tests/uart.py | 16 ++-- pyftdi/tracer.py | 58 ++++++++------ pyftdi/usbtools.py | 99 +++++++++++------------- setup.py | 12 ++- 41 files changed, 527 insertions(+), 596 deletions(-) delete mode 100644 pyftdi/tests/timearray.py diff --git a/.pylintrc b/.pylintrc index 60b70871..05d31e49 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,3 +1,7 @@ +[MASTER] + +init-hook='import sys; sys.path.append(".")' + [MESSAGES CONTROL] disable= @@ -9,4 +13,6 @@ disable= too-many-locals, too-many-nested-blocks, too-many-public-methods, - too-many-statements + too-many-return-statements, + too-many-statements, + unspecified-encoding diff --git a/LICENSE b/LICENSE index 89cfb454..b262ab34 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2008-2023 Emmanuel Blot +Copyright (c) 2008-2024 Emmanuel Blot All Rights Reserved. SPDX-License-Identifier: BSD-3-Clause diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 2f39aca8..4596ccc3 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2023 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # @@ -6,7 +6,7 @@ # pylint: disable=missing-docstring -__version__ = '0.55.1' +__version__ = '0.55.2' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' @@ -15,7 +15,7 @@ # For all support requests, please open a new issue on GitHub __email__ = 'emmanuel.blot@free.fr' __license__ = 'Modified BSD' -__copyright__ = 'Copyright (c) 2011-2023 Emmanuel Blot' +__copyright__ = 'Copyright (c) 2011-2024 Emmanuel Blot' from logging import WARNING, NullHandler, getLogger diff --git a/pyftdi/bin/ftconf.py b/pyftdi/bin/ftconf.py index bbe88fc9..1fb36a7d 100755 --- a/pyftdi/bin/ftconf.py +++ b/pyftdi/bin/ftconf.py @@ -3,7 +3,7 @@ """Simple FTDI EEPROM configurator. """ -# Copyright (c) 2019-2022, Emmanuel Blot +# Copyright (c) 2019-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,7 +11,7 @@ from argparse import ArgumentParser, FileType from io import StringIO from logging import Formatter, StreamHandler, DEBUG, ERROR -from sys import modules, stderr, stdout +from sys import exit as sys_exit, modules, stderr, stdout from textwrap import fill from traceback import format_exc from pyftdi import FtdiLogger @@ -86,9 +86,9 @@ def main(): extra = argparser.add_argument_group(title='Extras') extra.add_argument('-v', '--verbose', action='count', default=0, - help='increase verbosity') + help='increase verbosity') extra.add_argument('-d', '--debug', action='store_true', - help='enable debug mode') + help='enable debug mode') args = argparser.parse_args() debug = args.debug @@ -138,13 +138,12 @@ def main(): helpstr = ', '.join(sorted(eeprom.properties)) print(fill(helpstr, initial_indent=' ', subsequent_indent=' ')) - exit(1) + sys_exit(1) for sep in ':=': if sep in conf: name, value = conf.split(sep, 1) if not value: - argparser.error('Configuration %s without value' % - conf) + argparser.error(f'Configuration {conf} without value') if value == 'help': value = '?' helpio = StringIO() @@ -153,10 +152,10 @@ def main(): if helpstr: print(fill(helpstr, initial_indent=' ', subsequent_indent=' ')) - exit(1) + sys_exit(1) break else: - argparser.error('Missing name:value separator in %s' % conf) + argparser.error(f'Missing name:value separator in {conf}') if args.vid: eeprom.set_property('vendor_id', args.vid) if args.pid: @@ -166,7 +165,7 @@ def main(): if args.hexblock is not None: indent = ' ' * args.hexblock for pos in range(0, len(eeprom.data), 16): - hexa = ' '.join(['%02x' % x for x in eeprom.data[pos:pos+16]]) + hexa = ' '.join([f'{x:02x}' for x in eeprom.data[pos:pos+16]]) print(indent, hexa, sep='') if args.update: if eeprom.commit(False, no_crc=args.full_erase): @@ -181,12 +180,12 @@ def main(): eeprom.save_config(ofp) except (ImportError, IOError, NotImplementedError, ValueError) as exc: - print('\nError: %s' % exc, file=stderr) + print(f'\nError: {exc}', file=stderr) if debug: print(format_exc(chain=False), file=stderr) - exit(1) + sys_exit(1) except KeyboardInterrupt: - exit(2) + sys_exit(2) if __name__ == '__main__': diff --git a/pyftdi/bin/ftdi_urls.py b/pyftdi/bin/ftdi_urls.py index eccb9bbb..b5993a08 100755 --- a/pyftdi/bin/ftdi_urls.py +++ b/pyftdi/bin/ftdi_urls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2022, Emmanuel Blot +# Copyright (c) 2019-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -9,7 +9,7 @@ from argparse import ArgumentParser, FileType from logging import Formatter, StreamHandler, DEBUG, ERROR -from sys import modules, stderr +from sys import exit as sys_exit, modules, stderr from traceback import format_exc from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi @@ -62,12 +62,12 @@ def main(): Ftdi.show_devices() except (ImportError, IOError, NotImplementedError, ValueError) as exc: - print('\nError: %s' % exc, file=stderr) + print(f'\nError: {exc}', file=stderr) if debug: print(format_exc(chain=False), file=stderr) - exit(1) + sys_exit(1) except KeyboardInterrupt: - exit(2) + sys_exit(2) if __name__ == '__main__': diff --git a/pyftdi/bin/i2cscan.py b/pyftdi/bin/i2cscan.py index e062b42f..41f3dbcd 100755 --- a/pyftdi/bin/i2cscan.py +++ b/pyftdi/bin/i2cscan.py @@ -12,7 +12,7 @@ from argparse import ArgumentParser, FileType from logging import Formatter, StreamHandler, getLogger, DEBUG, ERROR -from sys import modules, stderr +from sys import exit as sys_exit, modules, stderr from traceback import format_exc from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi @@ -75,12 +75,12 @@ def scan(cls, url: str, smb_mode: bool = True, force: bool = False) \ i2c.terminate() columns = 16 row = 0 - print(' %s' % ''.join(' %01X ' % col for col in range(columns))) + print(' ', ''.join(f' {col:01X} ' for col in range(columns))) while True: chunk = slaves[row:row+columns] if not chunk: break - print(' %1X:' % (row//columns), ' '.join(chunk)) + print(f' {row//columns:01X}:', ' '.join(chunk)) row += columns @@ -140,16 +140,16 @@ def main(): I2cBusScanner.scan(args.device, not args.no_smb, args.force) except (ImportError, IOError, NotImplementedError, ValueError) as exc: - print('\nError: %s' % exc, file=stderr) + print(f'\nError: {exc}', file=stderr) if debug: print(format_exc(chain=False), file=stderr) - exit(1) + sys_exit(1) except KeyboardInterrupt: - exit(2) + sys_exit(2) if __name__ == '__main__': try: main() - except Exception as exc: - print(str(exc), file=stderr) + except Exception as _exc: + print(str(_exc), file=stderr) diff --git a/pyftdi/bin/pyterm.py b/pyftdi/bin/pyterm.py index d9fa3c85..b8065faf 100755 --- a/pyftdi/bin/pyterm.py +++ b/pyftdi/bin/pyterm.py @@ -18,7 +18,7 @@ from logging import Formatter, StreamHandler, DEBUG, ERROR from os import environ, linesep, stat from re import search -from sys import exit as sysexit, modules, platform, stderr, stdout +from sys import exit as sys_exit, modules, platform, stderr, stdout from time import sleep from threading import Event, Thread from traceback import format_exc @@ -57,7 +57,7 @@ def run(self, fullmode=False, loopback=False, silent=False, """Switch to a pure serial terminal application""" self._terminal.init(fullmode) - print('Entering minicom mode @ %d bps' % self._port.baudrate) + print(f'Entering minicom mode @ { self._port.baudrate} bps') stdout.flush() self._resume = True # start the reader (target to host direction) within a dedicated thread @@ -109,6 +109,7 @@ def _get_from_port(self): self._resume = False print(str(ex), file=stderr) interrupt_main() + return bytearray() except Exception as ex: print(str(ex), file=stderr) return bytearray() @@ -132,7 +133,7 @@ def _reader(self, loopback, getfunc): except KeyboardInterrupt: return except Exception as exc: - print("Exception: %s" % exc) + print(f'Exception: {exc}') if self._debug: print(format_exc(chain=False), file=stderr) interrupt_main() @@ -182,7 +183,7 @@ def _writer(self, fullmode, silent, localecho, crlf=0): def _cleanup(self, *args): """Cleanup resource before exiting""" if args and args[0]: - print('%sAborting...' % linesep) + print(f'{linesep}Aborting...') try: self._resume = False if self._port: @@ -222,7 +223,7 @@ def _open_port(device, baudrate, parity, rtscts, debug=False): if not vmo: # unable to parse version raise ValueError() - if tuple([int(x) for x in vmo.groups()]) < (3, 0): + if tuple(int(x) for x in vmo.groups()) < (3, 0): # pysrial version is too old raise ValueError() except (ValueError, IndexError, ImportError) as exc: @@ -243,10 +244,10 @@ def _open_port(device, baudrate, parity, rtscts, debug=False): if not port.is_open: port.open() if not port.is_open: - raise IOError('Cannot open port "%s"' % device) + raise IOError(f"Cannot open port '{device}'") if debug: backend = port.BACKEND if hasattr(port, 'BACKEND') else '?' - print("Using serial backend '%s'" % backend) + print(f"Using serial backend '{backend}'") return port except SerialException as exc: raise IOError(str(exc)) from exc @@ -272,7 +273,6 @@ def get_default_device() -> str: return device - def main(): """Main routine""" debug = False @@ -280,16 +280,16 @@ def main(): default_device = get_default_device() argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('-f', '--fullmode', dest='fullmode', - action='store_true', - help='use full terminal mode, exit with ' - '[Ctrl]+B') + action='store_true', + help='use full terminal mode, exit with ' + '[Ctrl]+B') argparser.add_argument('device', nargs='?', default=default_device, - help='serial port device name (default: %s)' % - default_device) + help=f'serial port device name ' + f'(default: {default_device}') argparser.add_argument('-b', '--baudrate', - help='serial port baudrate (default: %d)' % - MiniTerm.DEFAULT_BAUDRATE, - default='%s' % MiniTerm.DEFAULT_BAUDRATE) + efault=str(MiniTerm.DEFAULT_BAUDRATE), + help=f'serial port baudrate ' + f'(default: {MiniTerm.DEFAULT_BAUDRATE})') argparser.add_argument('-w', '--hwflow', action='store_true', help='hardware flow control') @@ -355,12 +355,12 @@ def main(): args.crlf) except (IOError, ValueError) as exc: - print('\nError: %s' % exc, file=stderr) + print(f'\nError: {exc}', file=stderr) if debug: print(format_exc(chain=False), file=stderr) - sysexit(1) + sys_exit(1) except KeyboardInterrupt: - sysexit(2) + sys_exit(2) if __name__ == '__main__': diff --git a/pyftdi/bits.py b/pyftdi/bits.py index 8760b224..9dcac416 100644 --- a/pyftdi/bits.py +++ b/pyftdi/bits.py @@ -71,7 +71,7 @@ def __init__(self, value: Union['BitSequence', str, int] = None, elif value is None: pass else: - raise BitSequenceError("Cannot initialize from a %s" % type(value)) + raise BitSequenceError(f"Cannot initialize from '{type(value)}'") self._update_length(length, msb) def sequence(self) -> bytearray: @@ -272,7 +272,7 @@ def __str__(self): else: j = None chunks.append(srepr[-i-8:j]) - return '%d: %s' % (len(self), ' '.join(reversed(chunks))) + return f'{len(self)}: {" ".join(reversed(chunks))}' def __int__(self): value = 0 @@ -371,14 +371,15 @@ def invert(self): return self def tobyte(self, msb=False): - raise BitSequenceError("Type %s cannot be converted to byte" % - type(self)) + raise BitSequenceError(f'Type {type(self)} cannot be converted to ' + f'byte') def tobytes(self, msb=False, msby=False): - raise BitSequenceError("Type %s cannot be converted to bytes" % - type(self)) + raise BitSequenceError(f'Type {type(self)} cannot be converted to ' + f'bytes') def matches(self, other): + # pylint: disable=missing-function-docstring if not isinstance(self, BitSequence): raise BitSequenceError('Not a BitSequence instance') # the bit sequence should be of the same length @@ -494,7 +495,7 @@ def to_seq(self, msb=0, lsb=0): def __getitem__(self, index): if isinstance(index, slice): if index.stop == index.start: - return + return None if index.stop < index.start: offset = index.stop count = index.start-index.stop+1 diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index c894c15f..7e692504 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -12,16 +12,12 @@ from binascii import hexlify, unhexlify from collections import OrderedDict, namedtuple from configparser import ConfigParser -from enum import IntEnum -if sys.version_info[:2] > (3, 5): - from enum import IntFlag +from enum import IntEnum, IntFlag from logging import getLogger from random import randint from re import match from struct import calcsize as scalc, pack as spack, unpack as sunpack from typing import BinaryIO, List, Optional, Set, TextIO, Union, Tuple -if sys.version_info[:2] == (3, 5): - from aenum import IntFlag from usb.core import Device as UsbDevice from .ftdi import Ftdi, FtdiError from .misc import classproperty, to_bool, to_int @@ -69,7 +65,6 @@ class FtdiEeprom: } """EEPROM properties.""" - CBUS = IntEnum('CBus', 'TXDEN PWREN TXLED RXLED TXRXLED SLEEP CLK48 CLK24 CLK12 ' 'CLK6 GPIO BB_WR BB_RD', start=0) @@ -125,6 +120,7 @@ def __getattr__(self, name): @classproperty def eeprom_sizes(cls) -> List[int]: + # pylint: disable=no-self-argument """Return a list of supported EEPROM sizes. :return: the supported EEPROM sizes @@ -248,8 +244,8 @@ def storage_size(self) -> int: eeprom_storage_size = self.size if self.is_mirroring_enabled: eeprom_storage_size = self.mirror_sector - except FtdiError as fe: - raise fe + except FtdiError as exc: + raise exc return eeprom_storage_size @property @@ -262,7 +258,7 @@ def data(self) -> bytes: return bytes(self._eeprom) @property - def properties(self) -> Set[str]: + def properties(self) -> Set[str]: """Returns the supported properties for the current device. :return: the supported properies. @@ -342,7 +338,7 @@ def is_mirroring_enabled(self) -> bool: """ return self.has_mirroring and self._mirror - def enable_mirroring(self, enable : bool) -> None: + def enable_mirroring(self, enable: bool) -> None: """Enable EEPROM write mirroring. When enabled, this divides the EEPROM into 2 sectors and mirrors configuration data between them. @@ -625,7 +621,7 @@ def set_property(self, name: str, value: Union[str, int, bool], self._dirty.add(name) return if name.startswith('invert_'): - if not self.device_version in (0x600, 0x1000): + if self.device_version not in (0x600, 0x1000): raise ValueError('UART control line inversion not available ' 'with this device') self._set_invert(name[len('invert_'):], value, out) @@ -834,7 +830,7 @@ def _sync_eeprom(self, no_crc: bool = False): self.log.debug('No change detected for EEPROM content') return if not no_crc: - if any([x in self._dirty for x in self.VAR_STRINGS]): + if any(x in self._dirty for x in self.VAR_STRINGS): self._generate_var_strings() for varstr in self.VAR_STRINGS: self._dirty.discard(varstr) @@ -864,7 +860,7 @@ def _compute_crc(self, eeprom: Union[bytes, bytearray], check=False): if check: self._valid = not bool(crc) if not self._valid: - self.log.debug(f'CRC is now 0x{crc:04x}') + self.log.debug('CRC is now 0x%04x', crc) else: self.log.debug('CRC OK') return crc, crc_pos, crc_size @@ -878,8 +874,8 @@ def _update_crc(self): crc_s1_start = self.mirror_sector - crc_size self._eeprom[crc_s1_start:crc_s1_start+crc_size] = spack(' Tuple[int, bool]: + def _compute_size(self, eeprom: Union[bytes, bytearray]) \ + -> Tuple[int, bool]: """ :return: Tuple of: - int of usable size of the eeprom @@ -887,7 +883,7 @@ def _compute_size(self, """ if self._ftdi.is_eeprom_internal: return self._ftdi.max_eeprom_size, False - if all([x == 0xFF for x in eeprom]): + if all(x == 0xFF for x in eeprom): # erased EEPROM, size is unknown return self._ftdi.max_eeprom_size, False if eeprom[0:0x80] == eeprom[0x80:0x100]: @@ -989,8 +985,8 @@ def _set_cbus_func(self, cpin: int, value: str, try: code = cbus[value.upper()].value except KeyError as exc: - raise ValueError(f"CBUS pin '{cpin}'' does not have function " - f"{value}'") from exc + raise ValueError(f"CBUS pin '{cpin}' does not have function " + f"{value}'") from exc if pin_filter and not pin_filter(cpin, value.upper()): raise ValueError(f"Unsupported CBUS function '{value}' for pin " f"'{cpin}'") diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index 620624db..26e1da6a 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -182,18 +182,18 @@ class BitMode(IntEnum): ENABLE_CLK_DIV5 = 0x8b # Modem status - MODEM_CTS = (1 << 4) # Clear to send - MODEM_DSR = (1 << 5) # Data set ready - MODEM_RI = (1 << 6) # Ring indicator - MODEM_RLSD = (1 << 7) # Carrier detect - MODEM_DR = (1 << 8) # Data ready - MODEM_OE = (1 << 9) # Overrun error - MODEM_PE = (1 << 10) # Parity error - MODEM_FE = (1 << 11) # Framing error - MODEM_BI = (1 << 12) # Break interrupt - MODEM_THRE = (1 << 13) # Transmitter holding register - MODEM_TEMT = (1 << 14) # Transmitter empty - MODEM_RCVE = (1 << 15) # Error in RCVR FIFO + MODEM_CTS = 1 << 4 # Clear to send + MODEM_DSR = 1 << 5 # Data set ready + MODEM_RI = 1 << 6 # Ring indicator + MODEM_RLSD = 1 << 7 # Carrier detect + MODEM_DR = 1 << 8 # Data ready + MODEM_OE = 1 << 9 # Overrun error + MODEM_PE = 1 << 10 # Parity error + MODEM_FE = 1 << 11 # Framing error + MODEM_BI = 1 << 12 # Break interrupt + MODEM_THRE = 1 << 13 # Transmitter holding register + MODEM_TEMT = 1 << 14 # Transmitter empty + MODEM_RCVE = 1 << 15 # Error in RCVR FIFO # FTDI MPSSE commands SET_BITS_LOW = 0x80 # Change LSB GPIO output @@ -250,15 +250,15 @@ class BitMode(IntEnum): # Flow control arguments SIO_DISABLE_FLOW_CTRL = 0x0 - SIO_RTS_CTS_HS = (0x1 << 8) - SIO_DTR_DSR_HS = (0x2 << 8) - SIO_XON_XOFF_HS = (0x4 << 8) + SIO_RTS_CTS_HS = 0x1 << 8 + SIO_DTR_DSR_HS = 0x2 << 8 + SIO_XON_XOFF_HS = 0x4 << 8 SIO_SET_DTR_MASK = 0x1 - SIO_SET_DTR_HIGH = (SIO_SET_DTR_MASK | (SIO_SET_DTR_MASK << 8)) - SIO_SET_DTR_LOW = (0x0 | (SIO_SET_DTR_MASK << 8)) + SIO_SET_DTR_HIGH = SIO_SET_DTR_MASK | (SIO_SET_DTR_MASK << 8) + SIO_SET_DTR_LOW = 0x0 | (SIO_SET_DTR_MASK << 8) SIO_SET_RTS_MASK = 0x2 - SIO_SET_RTS_HIGH = (SIO_SET_RTS_MASK | (SIO_SET_RTS_MASK << 8)) - SIO_SET_RTS_LOW = (0x0 | (SIO_SET_RTS_MASK << 8)) + SIO_SET_RTS_HIGH = SIO_SET_RTS_MASK | (SIO_SET_RTS_MASK << 8) + SIO_SET_RTS_LOW = 0x0 | (SIO_SET_RTS_MASK << 8) # Parity bits PARITY_NONE, PARITY_ODD, PARITY_EVEN, PARITY_MARK, PARITY_SPACE = range(5) @@ -305,7 +305,7 @@ class BitMode(IntEnum): LATENCY_EEPROM_FT232R = 77 # EEPROM Properties - EXT_EEPROM_SIZES = (128, 256) # in bytes (93C66 seen as 93C56) + EXT_EEPROM_SIZES = (128, 256) # in bytes (93C66 seen as 93C56) INT_EEPROMS = { 0x0600: 0x80, # FT232R: 128 bytes, 1024 bits @@ -411,9 +411,9 @@ def add_custom_vendor(cls, vid: int, vidname: str = '') -> None: :raise ValueError: if the vendor id is already referenced """ if vid in cls.VENDOR_IDS.values(): - raise ValueError('Vendor ID 0x%04x already registered' % vid) + raise ValueError(f'Vendor ID 0x{vid:04x} already registered') if not vidname: - vidname = '0x%04x' % vid + vidname = f'0x{vid:04x}' cls.VENDOR_IDS[vidname] = vid @classmethod @@ -431,10 +431,10 @@ def add_custom_product(cls, vid: int, pid: int, pidname: str = '') -> None: if vid not in cls.PRODUCT_IDS: cls.PRODUCT_IDS[vid] = OrderedDict() elif pid in cls.PRODUCT_IDS[vid].values(): - raise ValueError('Product ID 0x%04x:0x%04x already registered' % - (vid, pid)) + raise ValueError(f'Product ID 0x{vid:04x}:0x{pid:04x} already ' + f'registered') if not pidname: - pidname = '0x%04x' % pid + pidname = f'0x{pid:04x}' cls.PRODUCT_IDS[vid][pidname] = pid @classmethod @@ -491,7 +491,7 @@ def open_from_url(self, url: str) -> None: def open(self, vendor: int, product: int, bus: Optional[int] = None, address: Optional[int] = None, index: int = 0, serial: Optional[str] = None, - interface: int = 1) -> None: + interface: int = 1) -> None: """Open a new interface to the specified FTDI device. If several FTDI devices of the same kind (vid, pid) are connected @@ -529,7 +529,7 @@ def open_from_device(self, device: UsbDevice, :param interface: FTDI interface to use (integer starting from 1) """ if not isinstance(device, UsbDevice): - raise FtdiError("Device '%s' is not a PyUSB device" % device) + raise FtdiError(f"Device '{device}' is not a PyUSB device") self._usb_dev = device try: self._usb_dev.set_configuration() @@ -538,7 +538,7 @@ def open_from_device(self, device: UsbDevice, # detect invalid interface as early as possible config = self._usb_dev.get_active_configuration() if interface > config.bNumInterfaces: - raise FtdiError('No such FTDI port: %d' % interface) + raise FtdiError(f'No such FTDI port: {interface}') self._set_interface(config, interface) self._max_packet_size = self._get_max_packet_size() # Invalidate data in the readbuffer @@ -1004,7 +1004,6 @@ def is_H_series(self) -> bool: raise FtdiError('Device characteristics not yet known') return self.device_version in (0x0700, 0x0800, 0x0900, 0x3600) - @property def is_mpsse(self) -> bool: """Tell whether the device is configured in MPSSE mode @@ -1082,8 +1081,8 @@ def fifo_sizes(self) -> Tuple[int, int]: try: return Ftdi.FIFO_SIZES[self.device_version] except KeyError as exc: - raise FtdiFeatureError('Unsupported device: 0x%04x' % - self.device_version) from exc + raise FtdiFeatureError(f'Unsupported device: ' + f'0x{self.device_version:04x}') from exc @property def mpsse_bit_delay(self) -> float: @@ -1219,8 +1218,7 @@ def read_data_set_chunksize(self, chunksize: int = 0) -> None: chunksize = min(self.fifo_sizes[0], self.fifo_sizes[1], self._max_packet_size) if platform == 'linux': - if chunksize > 16384: - chunksize = 16384 + chunksize = min(chunksize, 16384) self._readbuffer_chunksize = chunksize self.log.debug('RX chunksize: %d', self._readbuffer_chunksize) @@ -1262,9 +1260,9 @@ def set_cbus_direction(self, mask: int, direction: int) -> None: """ # sanity check: there cannot be more than 4 CBUS pins in bitbang mode if not 0 <= mask <= 0x0F: - raise ValueError('Invalid CBUS gpio mask: 0x%02x' % mask) + raise ValueError(f'Invalid CBUS gpio mask: 0x{mask:02x}') if not 0 <= direction <= 0x0F: - raise ValueError('Invalid CBUS gpio direction: 0x%02x' % direction) + raise ValueError(f'Invalid CBUS gpio direction: 0x{direction:02x}') self._cbus_pins = (mask, direction) def get_cbus_gpio(self) -> int: @@ -1295,7 +1293,7 @@ def set_cbus_gpio(self, pins: int) -> None: raise FtdiError('CBUS gpio not available from current mode') # sanity check: there cannot be more than 4 CBUS pins in bitbang mode if not 0 <= pins <= 0x0F: - raise ValueError('Invalid CBUS gpio pins: 0x%02x' % pins) + raise ValueError(f'Invalid CBUS gpio pins: 0x{pins:02x}') if not self._cbus_pins[0] & self._cbus_pins[1]: raise FtdiError('No CBUS IO configured as output') pins &= self._cbus_pins[0] & self._cbus_pins[1] @@ -1413,14 +1411,14 @@ def set_flowctrl(self, flowctrl: str) -> None: try: value = ctrl[flowctrl] | self._index except KeyError as exc: - raise ValueError('Unknown flow control: %s' % flowctrl) from exc + raise ValueError(f'Unknown flow control: {flowctrl}') from exc try: if self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, Ftdi.SIO_REQ_SET_FLOW_CTRL, 0, value, bytearray(), self._usb_write_timeout): raise FtdiError('Unable to set flow control') except USBError as exc: - raise FtdiError('UsbError: %s' % str(exc)) from exc + raise FtdiError(f'UsbError: {exc}') from exc def set_dtr(self, state: bool) -> None: """Set dtr line @@ -1663,15 +1661,15 @@ def read_eeprom(self, addr: int = 0, length: Optional[int] = None, Ftdi.REQ_IN, Ftdi.SIO_REQ_READ_EEPROM, 0, word_addr, 2, self._usb_read_timeout) if not buf: - raise FtdiEepromError('EEPROM read error @ %d' % - (word_addr << 1)) + err_addr = word_addr << 1 + raise FtdiEepromError(f'EEPROM read error @ {err_addr}') data.extend(buf) word_count -= 1 word_addr += 1 start = addr & 0x1 return bytes(data[start:start+length]) except USBError as exc: - raise FtdiError('UsbError: %s' % exc) from exc + raise FtdiError(f'UsbError: {exc}') from exc def write_eeprom(self, addr: int, data: Union[bytes, bytearray], eeprom_size: Optional[int] = None, @@ -1726,8 +1724,7 @@ def write_eeprom(self, addr: int, data: Union[bytes, bytearray], size += 1 if size & 0x1: size += 1 - if size > eeprom_size-2: - size = eeprom_size-2 + size = min(size, eeprom_size - 2) # finally, write new section of data and ... self._write_eeprom_raw(start, eeprom[start:start+size], dry_run=dry_run) @@ -1781,7 +1778,7 @@ def write_data(self, data: Union[bytes, bytearray]) -> int: offset += length return offset except USBError as exc: - raise FtdiError('UsbError: %s' % str(exc)) from exc + raise FtdiError(f'UsbError: {exc}') from exc def read_data_bytes(self, size: int, attempt: int = 1, request_gen: Optional[Callable[[int], bytes]] = None) \ @@ -1907,7 +1904,7 @@ def read_data_bytes(self, size: int, attempt: int = 1, self._readoffset += part_size return data except USBError as exc: - raise FtdiError('UsbError: %s' % str(exc)) from exc + raise FtdiError(f'UsbError: {exc}') from exc # never reached raise FtdiError("Internal error") @@ -1988,7 +1985,7 @@ def set_dynamic_latency(self, lmin: int, lmax: int, else: for lat in (lmin, lmax): if not self.LATENCY_MIN <= lat <= self.LATENCY_MAX: - raise ValueError("Latency out of range: %d" % lat) + raise ValueError(f'Latency out of range: {lat}') self._latency_min = lmin self._latency_max = lmax self._latency_threshold = threshold @@ -2004,7 +2001,7 @@ def validate_mpsse(self) -> None: # only useful in MPSSE mode bytes_ = self.read_data(2) if (len(bytes_) >= 2) and (bytes_[0] == '\xfa'): - raise FtdiError("Invalid command @ %d" % bytes_[1]) + raise FtdiError(f'Invalid command @ {bytes_[1]}') @classmethod def get_error_string(cls) -> str: @@ -2061,8 +2058,8 @@ def _ctrl_transfer_out(self, reqtype: int, value: int, data: bytes = b''): return self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, reqtype, value, self._index, bytearray(data), self._usb_write_timeout) - except USBError as ex: - raise FtdiError('UsbError: %s' % str(ex)) from None + except USBError as exc: + raise FtdiError(f'UsbError: {exc}') from None def _ctrl_transfer_in(self, reqtype: int, length: int): """Request for a control message from the device""" @@ -2070,8 +2067,8 @@ def _ctrl_transfer_in(self, reqtype: int, length: int): return self._usb_dev.ctrl_transfer( Ftdi.REQ_IN, reqtype, 0, self._index, length, self._usb_read_timeout) - except USBError as ex: - raise FtdiError('UsbError: %s' % str(ex)) from None + except USBError as exc: + raise FtdiError(f'UsbError: {exc}') from None def _write(self, data: Union[bytes, bytearray]) -> int: if self._debug_log: @@ -2084,15 +2081,15 @@ def _write(self, data: Union[bytes, bytearray]) -> int: try: return self._usb_dev.write(self._in_ep, data, self._usb_write_timeout) - except USBError as ex: - raise FtdiError('UsbError: %s' % str(ex)) from None + except USBError as exc: + raise FtdiError(f'UsbError: {exc}') from None def _read(self) -> bytes: try: data = self._usb_dev.read(self._out_ep, self._readbuffer_chunksize, self._usb_read_timeout) - except USBError as ex: - raise FtdiError('UsbError: %s' % str(ex)) from None + except USBError as exc: + raise FtdiError(f'UsbError: {exc}') from None if data: if self._debug_log: self.log.debug('< %s', hexlify(data).decode()) @@ -2129,13 +2126,13 @@ def _check_eeprom_size(self, eeprom_size: Optional[int]) -> int: if self.device_version in self.INT_EEPROMS: if (eeprom_size and eeprom_size != self.INT_EEPROMS[self.device_version]): - raise ValueError('Invalid EEPROM size: %d' % eeprom_size) + raise ValueError(f'Invalid EEPROM size: {eeprom_size}') eeprom_size = self.INT_EEPROMS[self.device_version] else: if eeprom_size is None: eeprom_size = self.max_eeprom_size if eeprom_size not in self.EXT_EEPROM_SIZES: - raise ValueError('Invalid EEPROM size: %d' % eeprom_size) + raise ValueError(f'Invalid EEPROM size: {eeprom_size}') return eeprom_size def _write_eeprom_raw(self, addr: int, data: Union[bytes, bytearray], @@ -2162,13 +2159,13 @@ def _write_eeprom_raw(self, addr: int, data: Union[bytes, bytearray], length = len(data) if addr & 0x1 or length & 0x1: raise ValueError('Address/length not even') - for word in sunpack('<%dH' % (length//2), data): + for word in sunpack(f'<{length//2}H', data): if not dry_run: out = self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, Ftdi.SIO_REQ_WRITE_EEPROM, word, addr >> 1, b'', self._usb_write_timeout) if out: - raise FtdiEepromError('EEPROM Write Error @ %d' % addr) + raise FtdiEepromError(f'EEPROM Write Error @ {addr}') self.log.debug('Write EEPROM [0x%02x]: 0x%04x', addr, word) else: self.log.info('Fake write EEPROM [0x%02x]: 0x%04x', @@ -2261,9 +2258,8 @@ def _set_baudrate(self, baudrate: int, constrain: bool) -> int: actual, delta, index, value) # return actual if constrain and delta > Ftdi.BAUDRATE_TOLERANCE: - raise ValueError('Baudrate tolerance exceeded: %.02f%% ' - '(wanted %d, achievable %d)' % - (delta, baudrate, actual)) + raise ValueError(f'Baudrate tolerance exceeded: {delta:.02f}% ' + f'(wanted {baudrate}, achievable {actual})') try: if self._usb_dev.ctrl_transfer( Ftdi.REQ_OUT, Ftdi.SIO_REQ_SET_BAUDRATE, value, index, @@ -2271,14 +2267,14 @@ def _set_baudrate(self, baudrate: int, constrain: bool) -> int: raise FtdiError('Unable to set baudrate') return actual except USBError as exc: - raise FtdiError('UsbError: %s' % str(exc)) from exc + raise FtdiError('UsbError: {exc}') from exc def _set_frequency(self, frequency: float) -> float: """Convert a frequency value into a TCK divisor setting""" if not self.is_mpsse: raise FtdiFeatureError('Cannot change frequency w/ current mode') if frequency > self.frequency_max: - raise FtdiFeatureError('Unsupported frequency: %f' % frequency) + raise FtdiFeatureError(f'Unsupported frequency: {frequency:.0f}') # Calculate base speed clock divider divcode = Ftdi.ENABLE_CLK_DIV5 divisor = int((Ftdi.BUS_CLOCK_BASE+frequency/2)/frequency)-1 diff --git a/pyftdi/gpio.py b/pyftdi/gpio.py index 5c794302..f55da6c4 100644 --- a/pyftdi/gpio.py +++ b/pyftdi/gpio.py @@ -427,8 +427,8 @@ class GpioMpsseController(GpioBaseController): MPSSE_PAYLOAD_MAX_LENGTH = 0xFF00 # 16 bits max (- spare for control) - def read(self, readlen: int = 1, peek: Optional[bool] = None) -> \ - Union[int, bytes, Tuple[int]]: + def read(self, readlen: int = 1, peek: Optional[bool] = None) \ + -> Union[int, bytes, Tuple[int]]: """Read the GPIO input pin electrical level. :param readlen: how many GPIO samples to retrieve. Each sample if @@ -498,7 +498,7 @@ def _configure(self, url: str, direction: int, def _read_mpsse(self, count: int) -> Tuple[int]: if self._width > 8: cmd = bytearray([Ftdi.GET_BITS_LOW, Ftdi.GET_BITS_HIGH] * count) - fmt = '<%dH' % count + fmt = f'<{count}H' else: cmd = bytearray([Ftdi.GET_BITS_LOW] * count) fmt = None @@ -509,8 +509,8 @@ def _read_mpsse(self, count: int) -> Tuple[int]: size = scalc(fmt) if fmt else count data = self._ftdi.read_data_bytes(size, 4) if len(data) != size: - raise FtdiError('Cannot read GPIO, recv %d out of %d bytes' % - (len(data), size)) + raise FtdiError(f'Cannot read GPIO, recv {len(data)} ' + f'out of {size} bytes') if fmt: return sunpack(fmt, data) return data diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index b084e10d..7231dea5 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -213,10 +213,10 @@ def address(self) -> int: return self._address def _make_buffer(self, regaddr: int, - out: Union[bytes, bytearray, Iterable[int], None] = None)\ - -> bytes: + out: Union[bytes, bytearray, Iterable[int], + None] = None) -> bytes: data = bytearray() - data.extend(spack('%s%s' % (self._endian, self._format), regaddr)) + data.extend(spack(f'{self._endian}{self._format}', regaddr)) if out: data.extend(out) return bytes(data) @@ -592,7 +592,7 @@ def validate_address(cls, address: Optional[int]) -> None: if address is None: return if address > cls.HIGHEST_I2C_ADDRESS: - raise I2cIOError("No such I2c slave: 0x%02x" % address) + raise I2cIOError(f'No such I2c slave: 0x{address:02x}') @property def frequency_max(self) -> float: @@ -900,8 +900,8 @@ def write_gpio(self, value: int) -> None: """ with self._lock: if (value & self._gpio_dir) != value: - raise I2cIOError('No such GPO pins: %04x/%04x' % - (self._gpio_dir, value)) + raise I2cIOError(f'No such GPO pins: ' + f'{self._gpio_dir:04x}/{value:04x}') # perform read-modify-write use_high = self._wide_port and (self.direction & 0xff00) data = self._read_raw(use_high) @@ -1100,6 +1100,7 @@ def _do_read(self, readlen: int) -> bytes: cmd_chunk = bytearray() cmd_chunk.extend(read_not_last * chunk_size) cmd_chunk.extend(self._immediate) + def write_command_gen(length: int): if length <= 0: # no more data diff --git a/pyftdi/jtag.py b/pyftdi/jtag.py index c6fe065e..4192246a 100644 --- a/pyftdi/jtag.py +++ b/pyftdi/jtag.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2020, Emmanuel Blot +# Copyright (c) 2010-2024, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # @@ -103,7 +103,7 @@ def reset(self): def find_path(self, target: Union[JtagState, str], source: Union[JtagState, str, None] = None) \ - -> List[JtagState]: + -> List[JtagState]: """Find the shortest event sequence to move from source state to target state. If source state is not specified, used the current state. @@ -136,8 +136,8 @@ def next_path(state, target, path): if npath: paths.append(npath) # keep the shortest path - return min([(len(l), l) for l in paths], - key=lambda x: x[0])[1] if paths else [] + return min(((len(p), p) for p in paths), key=lambda x: x[0])[1] \ + if paths else [] return next_path(source, target, []) @classmethod @@ -243,7 +243,7 @@ def sync(self) -> None: self._write_buff = bytearray() def write_tms(self, tms: BitSequence, - should_read: bool=False) -> None: + should_read: bool = False) -> None: """Change the TAP controller state""" if not isinstance(tms, BitSequence): raise JtagError('Expect a BitSequence') @@ -416,7 +416,7 @@ def _write_bytes(self, out: BitSequence): olen = len(bytes_)-1 # print("WRITE BYTES %s" % out) cmd = bytearray((Ftdi.WRITE_BYTES_NVE_LSB, olen & 0xff, - (olen >> 8) & 0xff)) + (olen >> 8) & 0xff)) cmd.extend(bytes_) self._stack_cmd(cmd) @@ -529,7 +529,7 @@ def sync(self) -> None: def shift_register(self, out: BitSequence) -> BitSequence: if not self._sm.state_of('shift'): - raise JtagError("Invalid state: %s" % self._sm.state()) + raise JtagError(f'Invalid state: {self._sm.state()}') if self._sm.state_of('capture'): bs = BitSequence(False) self._ctrl.write_tms(bs) @@ -546,7 +546,7 @@ def shift_and_update_register(self, out: BitSequence) -> BitSequence: """Shift a BitSequence into the current register and retrieve the register output, advancing the state to update_*r""" if not self._sm.state_of('shift'): - raise JtagError("Invalid state: %s" % self._sm.state()) + raise JtagError(f'Invalid state: {self._sm.state()}') if self._sm.state_of('capture'): bs = BitSequence(False) self._ctrl.write_tms(bs) @@ -612,7 +612,7 @@ def detect_register_size(self) -> int: # Freely inpired from UrJTAG stm = self._engine.state_machine if not stm.state_of('shift'): - raise JtagError("Invalid state: %s" % stm.state()) + raise JtagError(f'Invalid state: {stm.state()}') if stm.state_of('capture'): bs = BitSequence(False) self._engine.controller.write_tms(bs) @@ -621,9 +621,9 @@ def detect_register_size(self) -> int: PATTERN_LEN = 8 stuck = None for length in range(1, MAX_REG_LEN): - print("Testing for length %d" % length) + print(f'Testing for length {length}') if length > 5: - raise ValueError('Abort detection over reg length %d' % length) + raise ValueError(f'Abort detection over reg length {length}') zero = BitSequence(length=length) inj = BitSequence(length=length+PATTERN_LEN) inj.inc() @@ -647,7 +647,7 @@ def detect_register_size(self) -> int: break inj.inc() if ok: - print("Register detected length: %d" % length) + print(f'Register detected length: {length}') return length if stuck is not None: raise JtagError('TDO seems to be stuck') diff --git a/pyftdi/misc.py b/pyftdi/misc.py index b2e2427a..c7c06294 100644 --- a/pyftdi/misc.py +++ b/pyftdi/misc.py @@ -46,7 +46,7 @@ def hexdump(data: Union[bytes, bytearray, Iterable[int]], else: src = data except Exception as exc: - raise TypeError("Unsupported data type '%s'" % type(data)) from exc + raise TypeError(f"Unsupported data type '{type(data)}'") from exc length = 16 result = [] @@ -61,16 +61,15 @@ def hexdump(data: Union[bytes, bytearray, Iterable[int]], abv = True continue abv = False - hexa = ' '.join(["%02x" % x for x in s]) + hexa = ' '.join((f'{x:02x}' for x in s)) printable = s.translate(ASCIIFILTER).decode('ascii') if full: hx1, hx2 = hexa[:3*8], hexa[3*8:] hl = length//2 - result.append("%08x %-*s %-*s |%s|\n" % - (i, hl*3, hx1, hl*3, hx2, printable)) + result.append(f'{i:08x} {hx1:<{hl*3}} {hx2:<{hl*3}} ' + f'|{printable}|\n') else: - result.append("%06x %-*s %s\n" % - (i, length*3, hexa, printable)) + result.append(f'{i:06x} {hexa:<{length*3}} {printable}\n') last = s return ''.join(result) @@ -95,11 +94,11 @@ def hexline(data: Union[bytes, bytearray, Iterable[int]], else: src = data except Exception as exc: - raise TypeError("Unsupported data type '%s'" % type(data)) from exc + raise TypeError(f"Unsupported data type '{type(data)}'") from exc - hexa = sep.join(["%02x" % x for x in src]) + hexa = sep.join((f'{x:02x}' for x in src)) printable = src.translate(ASCIIFILTER).decode('ascii') - return "(%d) %s : %s" % (len(data), hexa, printable) + return f'({len(data)}) {hexa} : {printable}' def to_int(value: Union[int, str]) -> int: @@ -156,12 +155,12 @@ def to_bool(value: Union[int, bool, str], permissive: bool = True, return bool(value) if permissive: return False - raise ValueError("Invalid boolean value: '%d'" % value) + raise ValueError(f"Invalid boolean value: '{value}'") if value.lower() in TRUE_BOOLEANS: return True if permissive or (value.lower() in FALSE_BOOLEANS): return False - raise ValueError('"Invalid boolean value: "%s"' % value) + raise ValueError(f"Invalid boolean value: '{value}'") def to_bps(value: str) -> int: @@ -197,7 +196,7 @@ def xor(_a_: bool, _b_: bool) -> bool: :return: xor-ed value """ # pylint: disable=superfluous-parens - return bool((not(_a_) and _b_) or (_a_ and not(_b_))) + return bool((not (_a_) and _b_) or (_a_ and not (_b_))) def is_iterable(obj: Any) -> bool: @@ -237,12 +236,12 @@ def pretty_size(size, sep: str = ' ', if size > lim_m: ssize = size >> 20 if floor or (ssize << 20) == size: - return '%d%sMiB' % (ssize, sep) + return f'{ssize}{sep}MiB' if size > lim_k: ssize = size >> 10 if floor or (ssize << 10) == size: - return '%d%sKiB' % (ssize, sep) - return '%d%sbyte%s' % (size, sep, (plural and 's' or '')) + return f'{ssize}{sep}KiB' + return f'{size}{sep}byte{plural and "s" or ""}' def add_custom_devices(ftdicls=None, @@ -328,8 +327,8 @@ def __getattr__(self, name): try: return self.__getitem__(name) except KeyError as exc: - raise AttributeError("'%s' object has no attribute '%s'" % - (self.__class__.__name__, name)) from exc + raise AttributeError(f"'{self.__class__.__name__}' object has no " + f"attribute '{name}'") from exc def __setattr__(self, name, value): self.__setitem__(name, value) diff --git a/pyftdi/serialext/__init__.py b/pyftdi/serialext/__init__.py index 540f4a6b..1e5f677e 100644 --- a/pyftdi/serialext/__init__.py +++ b/pyftdi/serialext/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2008-2015, Neotion # All rights reserved. # @@ -13,7 +13,7 @@ raise ImportError("Python serial module not installed") from exc try: from serial import VERSION, serial_for_url as serial4url - version = tuple([int(x) for x in VERSION.split('.')]) + version = tuple(int(x) for x in VERSION.split('.')) if version < (3, 0): raise ValueError except (ValueError, IndexError, ImportError) as exc: diff --git a/pyftdi/serialext/logger.py b/pyftdi/serialext/logger.py index 80afece4..67a3b314 100644 --- a/pyftdi/serialext/logger.py +++ b/pyftdi/serialext/logger.py @@ -1,15 +1,15 @@ -# Copyright (c) 2010-2016 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -# pylint: disable=no-member # pylint: disable=broad-except # pylint: disable=invalid-name -# pylint: disable=super-with-arguments # pylint: disable=missing-function-docstring # pylint: disable=missing-module-docstring +# pylint: disable=no-member +# pylint: disable=super-with-arguments from sys import stderr from time import time @@ -28,10 +28,10 @@ def __init__(self, *args, **kwargs): if not logpath: raise ValueError('Missing logfile') try: + # pylint: disable=consider-using-with self._logger = open(logpath, "wt") - except IOError as e: - print("Cannot log data to %s: %s" % (logpath, str(e)), - file=stderr) + except IOError as exc: + print(f'Cannot log data to {logpath}: {exc}', file=stderr) self._last = time() self._log_init(*args, **kwargs) super(SerialLogger, self).__init__(*args, **kwargs) @@ -68,7 +68,7 @@ def reset_output_buffer(self): super(SerialLogger, self).reset_output_buffer() def send_break(self, duration=0.25): - self._log_signal('BREAK', 'for %.3f' % duration) + self._log_signal('BREAK', f'for {duration:.3f}') super(SerialLogger, self).send_break() def _update_break_state(self): @@ -117,63 +117,62 @@ def _print(self, header, string=None): now = time() delta = (now-self._last)*1000 self._last = now - print("%s (%3.3f ms):\n%s" % (header, delta, string or ''), + print(f'{header} ({delta:3.3f} ms):\n{string or ""}', file=self._logger) self._logger.flush() def _log_init(self, *args, **kwargs): try: - self._print( - 'NEW', ' args: %s %s' % - (', '.join(args), - ', '.join({'%s=%s' % it for it in kwargs.items()}))) - except Exception as e: - print('Cannot log init (%s)' % e, file=stderr) + sargs = ', '.join(args) + skwargs = ', '.join({f'{it[0]}={it[1]}' for it in kwargs.items()}) + self._print('NEW', f' args: {sargs} {skwargs}') + except Exception as exc: + print(f'Cannot log init ({exc})', file=stderr) def _log_open(self): try: self._print('OPEN') - except Exception as e: - print('Cannot log open (%s)' % e, file=stderr) + except Exception as exc: + print(f'Cannot log open ({exc})', file=stderr) def _log_close(self): try: self._print('CLOSE') - except Exception as e: - print('Cannot log close (%s)' % e, file=stderr) + except Exception as exc: + print(f'Cannot log close ({exc})', file=stderr) def _log_read(self, data): try: self._print('READ', hexdump(data)) - except Exception as e: - print('Cannot log input data (%s)' % e, file=stderr) + except Exception as exc: + print(f'Cannot log input data ({exc})', file=stderr) def _log_write(self, data): try: self._print('WRITE', hexdump(data)) - except Exception as e: - print('Cannot log output data (%s)' % e, data, file=stderr) + except Exception as exc: + print(f'Cannot log output data ({exc})', data, file=stderr) def _log_flush(self): try: self._print('FLUSH') - except Exception as e: - print('Cannot log flush action (%s)' % e, file=stderr) + except Exception as exc: + print(f'Cannot log flush action ({exc})', file=stderr) def _log_reset(self, type_): try: self._print('RESET BUFFER', type_) - except Exception as e: - print('Cannot log reset buffer (%s)' % e, file=stderr) + except Exception as exc: + print(f'Cannot log reset buffer ({exc})', file=stderr) def _log_waiting(self, count): try: - self._print('INWAITING', '%d' % count) - except Exception as e: - print('Cannot log inwaiting (%s)' % e, file=stderr) + self._print('INWAITING', f'{count}') + except Exception as exc: + print(f'Cannot log inwaiting ({exc})', file=stderr) def _log_signal(self, name, value): try: - self._print(name.upper(), '%s' % value) - except Exception as e: - print('Cannot log %s (%s)' % (name, e), file=stderr) + self._print(name.upper(), str(value)) + except Exception as exc: + print(f'Cannot log {name} ({exc})', file=stderr) diff --git a/pyftdi/serialext/protocol_ftdi.py b/pyftdi/serialext/protocol_ftdi.py index 3c409432..f5bf91e0 100644 --- a/pyftdi/serialext/protocol_ftdi.py +++ b/pyftdi/serialext/protocol_ftdi.py @@ -1,4 +1,4 @@ -# Copyright (c) 2008-2020, Emmanuel Blot +# Copyright (c) 2008-2024, Emmanuel Blot # Copyright (c) 2008-2016, Neotion # All rights reserved. # @@ -26,7 +26,7 @@ class FtdiSerial(SerialBase): list(range(115200, 1000000, 115200)) + list(range(1000000, 13000000, 100000))) - PYSERIAL_VERSION = tuple([int(x) for x in pyserialver.split('.')]) + PYSERIAL_VERSION = tuple(int(x) for x in pyserialver.split('.')) def open(self): """Open the initialized serial port""" @@ -34,9 +34,9 @@ def open(self): raise SerialException("Port must be configured before use.") try: device = Ftdi.create_from_url(self.port) - except (UsbToolsError, IOError) as ex: - raise SerialException('Unable to open USB port %s: %s' % - (self.portstr, str(ex))) from ex + except (UsbToolsError, IOError) as exc: + raise SerialException(f'Unable to open USB port {self.portstr}: ' + f'{exc}') from exc self.udev = device self._set_open_state(True) self._reconfigure_port() @@ -179,7 +179,7 @@ def _reconfigure_port(self): pass except IOError as exc: err = self.udev.get_error_string() - raise SerialException("%s (%s)" % (str(exc), err)) from exc + raise SerialException(f'{exc} ({err})') from exc def _set_open_state(self, open_): self.is_open = bool(open_) diff --git a/pyftdi/serialext/protocol_unix.py b/pyftdi/serialext/protocol_unix.py index 1875212d..e239573d 100644 --- a/pyftdi/serialext/protocol_unix.py +++ b/pyftdi/serialext/protocol_unix.py @@ -18,8 +18,8 @@ import select import socket from io import RawIOBase -from serial import (SerialBase, SerialException, portNotOpenError, - writeTimeoutError, VERSION as pyserialver) +from serial import (SerialBase, SerialException, PortNotOpenError, + VERSION as pyserialver) from ..misc import hexdump @@ -43,7 +43,7 @@ class SocketSerial(SerialBase): BACKEND = 'socket' VIRTUAL_DEVICE = True - PYSERIAL_VERSION = tuple([int(x) for x in pyserialver.split('.')]) + PYSERIAL_VERSION = tuple(int(x) for x in pyserialver.split('.')) def _reconfigure_port(self): pass @@ -65,13 +65,13 @@ def open(self): filename = os.path.join(home, filename[2:]) self._filename = filename self.sock.connect(self._filename) - except Exception as e: + except Exception as exc: self.close() - msg = "Could not open port: %s" % (str(e),) - if isinstance(e, socket.error): + msg = f'Could not open port: {exc}' + if isinstance(exc, socket.error): # pylint: disable=no-member - raise SerialExceptionWithErrno(msg, e.errno) from e - raise SerialException(msg) from e + raise SerialExceptionWithErrno(msg, exc.errno) from exc + raise SerialException(msg) from exc self._set_open_state(True) self._lastdtr = None @@ -90,7 +90,6 @@ def close(self): def in_waiting(self): """Return the number of characters currently in the input buffer.""" - # pylint: disable=no-self-use return 0 def read(self, size=1): @@ -98,7 +97,7 @@ def read(self, size=1): return less characters as requested. With no timeout it will block until the requested number of bytes is read.""" if self.sock is None: - raise portNotOpenError + raise PortNotOpenError read = bytearray() if size > 0: while len(read) < size: @@ -109,7 +108,7 @@ def read(self, size=1): if not buf: # Some character is ready, but none can be read # it is a marker for a disconnected peer - raise portNotOpenError + raise PortNotOpenError read += buf if self._timeout >= 0 and not buf: break # early abort on timeout @@ -118,24 +117,24 @@ def read(self, size=1): def write(self, data): """Output the given string over the serial port.""" if self.sock is None: - raise portNotOpenError + raise PortNotOpenError t = len(data) d = data while t > 0: try: - if self._writeTimeout is not None and self._writeTimeout > 0: + if self.writeTimeout is not None and self.writeTimeout > 0: _, ready, _ = select.select([], [self.sock], [], - self._writeTimeout) + self.writeTimeout) if not ready: - raise writeTimeoutError + raise TimeoutError() n = self.sock.send(d) if self._dump: print(hexdump(d[:n])) - if self._writeTimeout is not None and self._writeTimeout > 0: + if self.writeTimeout is not None and self.writeTimeout > 0: _, ready, _ = select.select([], [self.sock], [], - self._writeTimeout) + self.writeTimeout) if not ready: - raise writeTimeoutError + raise TimeoutError() d = d[n:] t = t - n except OSError as e: @@ -165,7 +164,7 @@ def _update_rts_state(self): def _update_dtr_state(self): """Set terminal status line: Data Terminal Ready""" - def setDTR(self, on=1): + def setDTR(self, value=1): """Set terminal status line: Data Terminal Ready""" @property @@ -193,7 +192,7 @@ def cd(self): def nonblocking(self): """internal - not portable!""" if self.sock is None: - raise portNotOpenError + raise PortNotOpenError self.sock.setblocking(0) def dump(self, enable): diff --git a/pyftdi/spi.py b/pyftdi/spi.py index b9c67e31..e7c98821 100644 --- a/pyftdi/spi.py +++ b/pyftdi/spi.py @@ -103,7 +103,7 @@ def read(self, readlen: int = 0, start: bool = True, stop: bool = True, def write(self, out: Union[bytes, bytearray, Iterable[int]], start: bool = True, stop: bool = True, droptail: int = 0) \ - -> None: + -> None: """Write bytes to the slave :param out: data to send to the SPI slave, may be empty to read out @@ -142,7 +142,7 @@ def set_mode(self, mode: int, cs_hold: Optional[int] = None) -> None: value) """ if not 0 <= mode <= 3: - raise SpiIOError('Invalid SPI mode: %d' % mode) + raise SpiIOError(f'Invalid SPI mode: {mode}') if (mode & 0x2) and not self._controller.is_inverted_cpha_supported: raise SpiIOError('SPI with CPHA high is not supported by ' 'this FTDI device') @@ -388,8 +388,7 @@ def configure(self, url: Union[str, UsbDevice], self._cs_count = int(kwargs['cs_count']) del kwargs['cs_count'] if not 1 <= self._cs_count <= 5: - raise ValueError('Unsupported CS line count: %d' % - self._cs_count) + raise ValueError(f'Unsupported CS line count: {self._cs_count}') if 'turbo' in kwargs: self._turbo = bool(kwargs['turbo']) del kwargs['turbo'] @@ -470,8 +469,8 @@ def get_port(self, cs: int, freq: Optional[float] = None, if cs >= len(self._spi_ports): if cs < 5: # increase cs_count (up to 4) to reserve more /CS channels - raise SpiIOError("/CS pin %d not reserved for SPI" % cs) - raise SpiIOError("No such SPI port: %d" % cs) + raise SpiIOError('/CS pin {cs} not reserved for SPI') + raise SpiIOError(f'No such SPI port: {cs}') if not self._spi_ports[cs]: freq = min(freq or self._frequency, self.frequency_max) hold = freq and (1+int(1E6/freq)) @@ -673,8 +672,8 @@ def write_gpio(self, value: int) -> None: """ with self._lock: if (value & self._gpio_dir) != value: - raise SpiIOError('No such GPO pins: %04x/%04x' % - (self._gpio_dir, value)) + raise SpiIOError(f'No such GPO pins: ' + f'{self._gpio_dir:04x}/{value:04x}') # perform read-modify-write use_high = self._wide_port and (self.direction & 0xff00) data = self._read_raw(use_high) diff --git a/pyftdi/term.py b/pyftdi/term.py index ccd63bda..fbf4ebe9 100755 --- a/pyftdi/term.py +++ b/pyftdi/term.py @@ -1,6 +1,6 @@ """Terminal management helpers""" -# Copyright (c) 2020-2021, Emmanuel Blot +# Copyright (c) 2020-2024, Emmanuel Blot # Copyright (c) 2020, Michael Pratt # All rights reserved. # @@ -17,15 +17,20 @@ from termios import (ECHO, ICANON, TCSAFLUSH, TCSANOW, VINTR, VMIN, VSUSP, VTIME, tcgetattr, tcsetattr) + # pylint workaround (disable=used-before-assignment) + def call(): + # pylint: disable=missing-function-docstring + pass + class Terminal: """Terminal management function """ FNKEYS = { - #Ctrl + Alt + Backspace + # Ctrl + Alt + Backspace 14: b'\x1b^H', - #Ctrl + Alt + Enter + # Ctrl + Alt + Enter 28: b'\x1b\r', # Pause/Break 29: b'\x1c', @@ -44,7 +49,7 @@ class Terminal: 145: b'\x1b[1;5B', 116: b'\x1b[1;5C', 115: b'\x1b[1;5D', - #Ctrl + Tab + # Ctrl + Tab 148: b'\x1b[2J', # Cursor (Home, Ins, Del...) 71: b'\x1b[1~', @@ -65,7 +70,7 @@ class Terminal: 146: b'\x1b[2;5~', 147: b'\x1b[3;5~', 117: b'\x1b[1;5F', - 134: b'\x1b[5;5~', + 114: b'\x1b[5;5~', 118: b'\x1b[6;5~', # Function Keys (F1 - F12) 59: b'\x1b[11~', @@ -141,7 +146,7 @@ def init(self, fullterm: bool) -> None: if not self.IS_MSWIN: self._termstates = [(t.fileno(), tcgetattr(t.fileno()) if t.isatty() else None) - for t in (stdin, stdout, stderr)] + for t in (stdin, stdout, stderr)] tfd, istty = self._termstates[0] if istty: new = tcgetattr(tfd) diff --git a/pyftdi/tests/backend/consts.py b/pyftdi/tests/backend/consts.py index 99adc30a..c413d2fd 100644 --- a/pyftdi/tests/backend/consts.py +++ b/pyftdi/tests/backend/consts.py @@ -10,7 +10,6 @@ from enum import Enum from importlib import import_module -from sys import version_info from pyftdi.ftdi import Ftdi from pyftdi.misc import EasyDict @@ -103,7 +102,6 @@ def dec_desc_type(self, desctype: int) -> str: return self._desc_type[desctype & self._desc_type_mask] - class FtdiConstants: """Expose useful constants defined in Ftdi and allow reverse search, i.e. retrieve constant literals from integral values. diff --git a/pyftdi/tests/backend/ftdivirt.py b/pyftdi/tests/backend/ftdivirt.py index 8c5151b5..b188f6ac 100644 --- a/pyftdi/tests/backend/ftdivirt.py +++ b/pyftdi/tests/backend/ftdivirt.py @@ -8,7 +8,6 @@ # pylint: disable=missing-docstring # pylint: disable=unused-argument # pylint: disable=invalid-name -# pylint: disable=no-self-use import os from array import array @@ -17,13 +16,15 @@ from enum import IntEnum, unique from logging import getLogger from struct import calcsize as scalc, pack as spack, unpack as sunpack -from sys import version_info from threading import Event, Lock, Thread from time import sleep, time as now -from typing import List, Mapping, NamedTuple, Optional, Sequence, Tuple +from typing import (TYPE_CHECKING, List, Mapping, NamedTuple, Optional, + Sequence, Tuple) from pyftdi.eeprom import FtdiEeprom # only for consts, do not use code from .consts import FTDICONST, USBCONST from .mpsse import VirtMpsseEngine, VirtMpsseTracer +if TYPE_CHECKING: + from .backend import VirtDeviceHandle class Pipe: @@ -204,7 +205,6 @@ class Fifos(NamedTuple): rx: Fifo # Host-to-FTDI tx: Fifo # FTDI-to-host - @unique class BitMode(IntEnum): """Function mode selection. @@ -898,10 +898,9 @@ def _tx_worker(self): else: _wait_delay = self.SLEEP_DELAY continue - else: - self.log.error('Unimplemented support for command %d', - command) - continue + self.log.error('Unimplemented support for command %d', + command) + continue self.log.debug('End of worker %s', self._tx_thread.name) except Exception as exc: self.log.error('Dead of worker %s: %s', self._tx_thread.name, exc) diff --git a/pyftdi/tests/backend/loader.py b/pyftdi/tests/backend/loader.py index dadb64f6..bf04cf0a 100644 --- a/pyftdi/tests/backend/loader.py +++ b/pyftdi/tests/backend/loader.py @@ -1,7 +1,7 @@ """Virtual USB backend loader. """ -# Copyright (c) 2020-2021, Emmanuel Blot +# Copyright (c) 2020-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -11,11 +11,10 @@ # pylint: disable=too-many-branches # pylint: disable=too-many-statements # pylint: disable=too-many-nested-blocks -# pylint: disable=no-self-use +# pylint: disable=import-error from binascii import unhexlify from logging import getLogger -from sys import version_info from typing import BinaryIO from ruamel.yaml import YAML from pyftdi.misc import to_bool @@ -207,7 +206,7 @@ def _build_device_descriptor(self, container) -> dict: try: dkey = kmap[ckey] except KeyError as exc: - raise ValueError(f'Unknown descriptor field {dkey}') from exc + raise ValueError(f'Unknown descriptor field {ckey}') from exc kwargs[dkey] = cval return kwargs @@ -287,11 +286,11 @@ def _build_interfaces(self, container): else: raise ValueError(f'Invalid interface entry {ikey}') ifaces = [] - while repeat: + while repeat: repeat -= 1 ifdesc, endpoints = self._build_alternative(altdef[0]) - self._last_ep_idx = max([ep.bEndpointAddress & 0x7F - for ep in endpoints]) + self._last_ep_idx = max(ep.bEndpointAddress & 0x7F + for ep in endpoints) iface = VirtInterface(ifdesc) for endpoint in endpoints: iface.add_endpoint(endpoint) diff --git a/pyftdi/tests/backend/mpsse.py b/pyftdi/tests/backend/mpsse.py index a1403d0c..38ea142b 100644 --- a/pyftdi/tests/backend/mpsse.py +++ b/pyftdi/tests/backend/mpsse.py @@ -8,8 +8,10 @@ from collections import deque from logging import getLogger from struct import unpack as sunpack -from typing import Union +from typing import TYPE_CHECKING, Union from pyftdi.tracer import FtdiMpsseEngine, FtdiMpsseTracer +if TYPE_CHECKING: + from .ftdivirt import VirtFtdiPort class VirtMpsseTracer(FtdiMpsseTracer): @@ -26,8 +28,8 @@ def _get_engine(self, iface: int): try: self._engines[iface] except IndexError as exc: - raise ValueError('No MPSSE engine available on interface %d' % - iface) from exc + raise ValueError(f'No MPSSE engine available on interface ' + f'{iface}') from exc if not self._engines[iface]: self._engines[iface] = VirtMpsseEngine(self, self._port) return self._engines[iface] diff --git a/pyftdi/tests/backend/usbvirt.py b/pyftdi/tests/backend/usbvirt.py index af5f472f..319087df 100644 --- a/pyftdi/tests/backend/usbvirt.py +++ b/pyftdi/tests/backend/usbvirt.py @@ -19,12 +19,13 @@ from importlib import import_module from logging import getLogger from struct import calcsize as scalc, pack as spack -from sys import version_info -from typing import List, Mapping, Optional, Tuple +from typing import TYPE_CHECKING, List, Mapping, Optional, Tuple from usb.backend import IBackend from pyftdi.misc import EasyDict from .consts import USBCONST from .ftdivirt import VirtFtdi +if TYPE_CHECKING: + from .loader import VirtLoader class VirtEndpoint: @@ -82,7 +83,7 @@ class InterfaceDescriptor(EasyDict): bInterfaceSubClass=0xFF, bInterfaceProtocol=0xFF, iInterface=0, # String desc index - extra_descriptors=extra or b'') + extra_descriptors=extra or b'') desc.update(defs) self.alt = 0 self.altsettings: List[Tuple[VirtInterface, @@ -160,10 +161,10 @@ class ConfigDescriptor(EasyDict): wTotalLength=0, bNumInterfaces=0, bConfigurationValue=0, - iConfiguration=0, # string index + iConfiguration=0, # string index bmAttributes=0x80, # bus-powered bMaxPower=150//2, # 150 mA - extra_descriptors=extra or b'') + extra_descriptors=extra or b'') self.desc.update(defs) self.interfaces: List[VirtInterface] = [] @@ -226,6 +227,7 @@ class DeviceDescriptor(EasyDict): eeprom=None) self.desc.update(defs) self._props = set() + # pylint: disable=consider-using-dict-items for key in kwargs: # be sure not to allow descriptor override by arbitrary properties if key not in defs: @@ -316,8 +318,8 @@ class VirtBackend(IBackend): def __init__(self): self.log = getLogger('pyftdi.virt.usb') - self._devices: List[VirtDevice] = list() - self._device_handles: Mapping[int, VirtDeviceHandle] = dict() + self._devices: List[VirtDevice] = [] + self._device_handles: Mapping[int, VirtDeviceHandle] = {} self._device_handle_count: int = 0 def add_device(self, device: VirtDevice): @@ -329,7 +331,7 @@ def flush_devices(self): self._devices.clear() @classmethod - def create_loader(cls) -> 'VirtLooader': + def create_loader(cls) -> 'VirtLoader': """Provide the loader class to configure this virtual backend instance. Using this method to retrieve a loader ensure both the virtual @@ -354,8 +356,7 @@ def get_virtual_ftdi(self, bus: int, address: int) -> VirtFtdi: raise ValueError('No FTDI @ {bus:address}') def enumerate_devices(self) -> VirtDevice: - for dev in self._devices: - yield dev + yield from self._devices def open_device(self, dev: VirtDevice) -> VirtDeviceHandle: self._device_handle_count += 1 diff --git a/pyftdi/tests/bits.py b/pyftdi/tests/bits.py index 9764c48b..ebd1ea3b 100755 --- a/pyftdi/tests/bits.py +++ b/pyftdi/tests/bits.py @@ -1,13 +1,18 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2010-2016 Emmanuel Blot +"""BitSequence unit tests.""" + +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause # pylint: disable=broad-except +# pylint: disable=invalid-name +# pylint: disable=missing-class-docstring +# pylint: disable=missing-function-docstring import unittest from pyftdi.bits import BitSequence, BitZSequence, BitSequenceError @@ -75,23 +80,22 @@ def test_cmp(self): self.assertTrue(bzs.matches(self.bs7)) def test_representation(self): - self.assertEqual("%s / %r" % (self.bs1, self.bs1), - "8: 10000000 / 10000000") - self.assertEqual("%s / %r" % (self.bs2, self.bs2), - "8: 01000000 / 01000000") - self.assertEqual("%s / %r" % (self.bs3, self.bs3), - "7: 0010000 / 0010000") - self.assertEqual("%s / %r" % (self.bs4, self.bs4), - "11: 001 00000000 / 00100000000") - self.assertEqual("%s / %r" % (self.bs5, self.bs5), - "49: 1 00010000 11011001 00110001 01101110 10111111 " - "11111110 / 100010000110110010011000101101110101111" - "1111111110") - self.assertEqual("%s / %r" % (self.bs6, self.bs6), - "49: 1 00010000 11011001 00110001 01101110 10111111 " - "11111111 / 100010000110110010011000101101110101111" - "1111111111") - + self.assertEqual(f'{self.bs1} / {self.bs1!r}' % (self.bs1, self.bs1), + '8: 10000000 / 10000000') + self.assertEqual(f'{self.bs2} / {self.bs2!r}' % (self.bs2, self.bs2), + '8: 01000000 / 01000000') + self.assertEqual(f'{self.bs3} / {self.bs3!r}' % (self.bs3, self.bs3), + '7: 0010000 / 0010000') + self.assertEqual(f'{self.bs4} / {self.bs4!r}' % (self.bs4, self.bs4), + '11: 001 00000000 / 00100000000') + self.assertEqual(f'{self.bs5} / {self.bs5!r}' % (self.bs5, self.bs5), + '49: 1 00010000 11011001 00110001 01101110 10111111 ' + '11111110 / 100010000110110010011000101101110101111' + '1111111110') + self.assertEqual(f'{self.bs6} / {self.bs6!r}' % (self.bs6, self.bs6), + '49: 1 00010000 11011001 00110001 01101110 10111111 ' + '11111111 / 100010000110110010011000101101110101111' + '1111111111') self.assertEqual(repr(self.bzs4), '11Z1Z010ZZ0100') self.assertEqual(repr(self.bzs5), '100Z01') @@ -117,8 +121,8 @@ def test_init(self): bs[8:12] = BitZSequence(value='ZZZZ') except BitSequenceError: pass - except Exception as e: - self.fail("Unexpected exception %s" % e) + except Exception as exc: + self.fail(f'Unexpected exception {exc}') else: self.fail("Error was expected") bs = BitZSequence('1111101010100111Z1Z010ZZ0100', msb=True) @@ -139,15 +143,15 @@ def test_init(self): bs = BitSequence(bytes_=[0x44, 0x666, 0xcc], msby=False) except BitSequenceError: pass - except Exception as e: - self.fail("Unexpected exception %s" % e) + except Exception as exc: + self.fail(f'Unexpected exception {exc}') else: self.fail("Error was expected") def test_conversion(self): bs = BitSequence(0xCA, msb=True, length=8) - self.assertEqual('%02x' % bs.tobyte(False), '53') - self.assertEqual('%02x' % bs.tobyte(True), 'ca') + self.assertEqual(f'{bs.tobyte(False):02x}', '53') + self.assertEqual(f'{bs.tobyte(True):02x}', 'ca') self.assertEqual(bs, BitSequence(bs.tobyte(True), msb=True, length=8)) self.assertRaises(BitSequenceError, BitZSequence.__int__, self.bzs5) self.assertRaises(BitSequenceError, BitZSequence.tobyte, self.bzs5) @@ -179,10 +183,10 @@ def test_misc(self): '0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15]') b = BitSequence(bytes_=[0xa0, '\x0f', 0x77], msb=False, msby=False) - self.assertEqual(str(['%02x' % x for x in b.tobytes(False)]), + self.assertEqual(str([f'{x:02x}' for x in b.tobytes(False)]), "['a0', '0f', '77']") b = BitSequence(bytes_=[0xa0, '\x0f', 0x77], msb=True, msby=True) - self.assertEqual(str(['%02x' % x for x in b.tobytes(True)]), + self.assertEqual(str([f'{x:02x}' for x in b.tobytes(True)]), "['a0', '0f', '77']") b = BitSequence(length=7) b[6] = '1' diff --git a/pyftdi/tests/cbus.py b/pyftdi/tests/cbus.py index 726f47d5..e6679d16 100755 --- a/pyftdi/tests/cbus.py +++ b/pyftdi/tests/cbus.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2020, Emmanuel Blot +"""CBUS unit tests.""" + +# Copyright (c) 2020-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/pyftdi/tests/eeprom.py b/pyftdi/tests/eeprom.py index cd3e6e0d..70dd0302 100755 --- a/pyftdi/tests/eeprom.py +++ b/pyftdi/tests/eeprom.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +"""EEPROM unit tests.""" + # Copyright (c) 2018, Stephen Goadhouse # Copyright (c) 2019-2024, Emmanuel Blot # All rights reserved. @@ -18,7 +20,6 @@ # pylint: disable=missing-docstring - class EepromTestCase(unittest.TestCase): """FTDI EEPROM access method test case""" @@ -42,7 +43,7 @@ def setUp(self): ftdi.open_bitbang_from_url(self.url, direction=out_pins) self.ftdi = ftdi except IOError as exc: - raise IOError('Unable to open USB port: %s' % str(exc)) from exc + raise IOError(f'Unable to open USB port: {exc}') from exc def tearDown(self): """Close the FTDI connection""" @@ -106,7 +107,7 @@ def main(): try: loglevel = getattr(logging, level) except AttributeError as exc: - raise ValueError('Invalid log level: %s' % level) from exc + raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.set_level(loglevel) testmod(modules[__name__]) unittest.main(defaultTest='suite') diff --git a/pyftdi/tests/eeprom_mock.py b/pyftdi/tests/eeprom_mock.py index a5b73cee..a3a528d0 100644 --- a/pyftdi/tests/eeprom_mock.py +++ b/pyftdi/tests/eeprom_mock.py @@ -1,20 +1,25 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2021, Emmanuel Blot +# Copyright (c) 2019-2024, Emmanuel Blot # All rights reserved. -# # SPDX-License-Identifier: BSD-3-Clause -# -# mock eeprom tests that can be run in CI without a device connected + +"""Mock eeprom tests that can be run in CI without a device connected.""" + import logging from os import environ from sys import modules, stdout -from unittest import TestCase, TestSuite, SkipTest, makeSuite, main as ut_main +from unittest import TestCase, TestSuite, makeSuite, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.eeprom import FtdiEeprom -from pyftdi.misc import to_bool, hexdump +from pyftdi.misc import to_bool from pyftdi.ftdi import FtdiError +# pylint: disable=invalid-name +# pylint: disable=missing-class-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=no-member + VirtLoader = None @@ -61,7 +66,7 @@ def setUpClass(cls): if cls.url == 'ftdi:///1': ftdi = Ftdi() ftdi.open_from_url(cls.url) - count = ftdi.device_port_count + _ = ftdi.device_port_count ftdi.close() def test_mirror_properties(self): @@ -84,7 +89,7 @@ def test_mirror_properties(self): self.assertTrue(mirrored_eeprom.has_mirroring) self.assertTrue(mirrored_eeprom.is_mirroring_enabled) self.assertEqual(mirrored_eeprom.size // 2, - mirrored_eeprom.mirror_sector) + mirrored_eeprom.mirror_sector) mirrored_eeprom.close() def test_mirror_manufacturer(self): @@ -161,6 +166,7 @@ def test_compute_size_detects_mirror(self): """Verify the eeproms internal _compute_size method returns the correct bool value when it detects an eeprom mirror """ + # pylint: disable=protected-access eeprom = FtdiEeprom() eeprom.set_test_mode(True) eeprom.open(self.url, ignore=True) @@ -185,11 +191,13 @@ def _check_for_mirrored_eeprom_contents(self, eeprom: FtdiEeprom): """ sector_size = eeprom.size // 2 for ii in range(0, sector_size): - self.assertEqual(eeprom.data[ii], + self.assertEqual( + eeprom.data[ii], eeprom.data[ii + eeprom.mirror_sector], f'Mismatch mirror data @ 0x{ii:02x}: 0x{eeprom.data[ii]:02x} ' f'!= 0x{eeprom.data[ii + eeprom.mirror_sector]:02x}') + class NonMirroredEepromTestCase(FtdiTestCase): """Test FTDI EEPROM mirror features do not break FTDI devices that do not use mirroring @@ -208,7 +216,7 @@ def setUpClass(cls): if cls.url == 'ftdi:///1': ftdi = Ftdi() ftdi.open_from_url(cls.url) - count = ftdi.device_port_count + _ = ftdi.device_port_count ftdi.close() def test_mirror_properties(self): @@ -218,8 +226,8 @@ def test_mirror_properties(self): mirroring """ if bool(getattr(self, 'DEVICE_CAN_MIRROR', None)): - self.skipTest("Mirror properties for devices capable of mirroring" - + " are tested in EepromMirrorTestCase") + self.skipTest('Mirror properties for devices capable of mirroring ' + 'are tested in EepromMirrorTestCase') # properties should work regardless of if the mirror option is set # or not eeprom = FtdiEeprom() @@ -228,7 +236,7 @@ def test_mirror_properties(self): self.assertFalse(eeprom.has_mirroring) self.assertFalse(eeprom.is_mirroring_enabled) with self.assertRaises(FtdiError): - eeprom.mirror_sector + _ = eeprom.mirror_sector eeprom.close() # even if mirroring is enabled, should still stay false mirrored_eeprom = FtdiEeprom() @@ -237,7 +245,7 @@ def test_mirror_properties(self): self.assertFalse(mirrored_eeprom.has_mirroring) self.assertFalse(mirrored_eeprom.is_mirroring_enabled) with self.assertRaises(FtdiError): - mirrored_eeprom.mirror_sector + _ = mirrored_eeprom.mirror_sector mirrored_eeprom.close() def test_no_mirror_manufacturer(self): @@ -311,9 +319,10 @@ def test_compute_size_does_not_mirror(self): """Verify the eeproms internal _compute_size method returns the correct bool value when it detects no mirroring. """ + # pylint: disable=protected-access if self.DEVICE_CAN_MIRROR: - self.skipTest("Mirror properties for devices capable of mirroring" - + " are tested in EepromMirrorTestCase") + self.skipTest('Mirror properties for devices capable of mirroring ' + 'are tested in EepromMirrorTestCase') eeprom = FtdiEeprom() eeprom.set_test_mode(True) eeprom.open(self.url, ignore=True) @@ -394,6 +403,7 @@ def suite(): def virtualize(): if not to_bool(environ.get('FTDI_VIRTUAL', 'off')): return + # pylint: disable=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('backend.usbvirt', ) @@ -401,15 +411,17 @@ def virtualize(): backend = UsbTools.find_backend() try: # obtain the loader class associated with the virtual backend + # pylint: disable=global-statement global VirtLoader VirtLoader = backend.create_loader() - except AttributeError: - raise AssertionError('Cannot load virtual USB backend') + except AttributeError as exc: + raise AssertionError('Cannot load virtual USB backend') from exc def setup_module(): - import doctest - doctest.testmod(modules[__name__]) + # pylint: disable=import-outside-toplevel + from doctest import testmod + testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) if debug: formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)-7s' @@ -420,8 +432,8 @@ def setup_module(): level = environ.get('FTDI_LOGLEVEL', 'warning').upper() try: loglevel = getattr(logging, level) - except AttributeError: - raise ValueError(f'Invalid log level: {level}') + except AttributeError as exc: + raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) FtdiLogger.set_level(loglevel) FtdiLogger.set_formatter(formatter) diff --git a/pyftdi/tests/ftdi.py b/pyftdi/tests/ftdi.py index 14d08d28..56600bde 100755 --- a/pyftdi/tests/ftdi.py +++ b/pyftdi/tests/ftdi.py @@ -1,11 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2010-2020, Emmanuel Blot +"""FTDI detection and connection unit tests.""" + +# Copyright (c) 2010-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause +# pylint: disable=missing-docstring + import logging from doctest import testmod from os import environ @@ -84,7 +88,7 @@ def test_dual_if_reset(self): ftdi1.close() raise SkipTest('FTDI device is not a multi-port device') next_port = (int(url1[-1]) % count) + 1 - url2 = 'ftdi:///%d' % next_port + url2 = f'ftdi:///{next_port}' ftdi2 = Ftdi() self.assertTrue(ftdi1.is_connected, 'Unable to connect to FTDI') ftdi2.open_from_url(url2) diff --git a/pyftdi/tests/gpio.py b/pyftdi/tests/gpio.py index 979e83bf..0a0a49eb 100755 --- a/pyftdi/tests/gpio.py +++ b/pyftdi/tests/gpio.py @@ -7,9 +7,9 @@ # SPDX-License-Identifier: BSD-3-Clause # pylint: disable=empty-docstring -# pylint: disable=missing-docstring -# pylint: disable=invalid-name # pylint: disable=global-statement +# pylint: disable=invalid-name +# pylint: disable=missing-docstring import logging from collections import deque @@ -103,16 +103,16 @@ def test_gpio_freeze(self): For now, it requires a logic analyzer to verify the output, this is not automatically validated by SW """ - direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In + direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio = GpioAsyncController() gpio.configure(self.url, direction=direction, frequency=1e3, initial=0x0) port = gpio.get_gpio() # emit a sequence as a visual marker on b3,b2,b1,b0 - port.write([x<<4 for x in range(16)]) + port.write([x << 4 for x in range(16)]) sleep(0.01) # write 0b0110 to the port - port.write(0x6<<4) + port.write(0x6 << 4) sleep(0.001) # close w/o freeze: all the outputs should be reset (usually 0b1111) # it might need pull up (or pull down) to observe the change as @@ -123,21 +123,20 @@ def test_gpio_freeze(self): initial=0x0) port = gpio.get_gpio() # emit a sequence as a visual marker with on b3 and b1 - port.write([(x<<4)&0x90 for x in range(16)]) + port.write([(x << 4) & 0x90 for x in range(16)]) sleep(0.01) # write 0b0110 to the port - port.write(0x6<<4) + port.write(0x6 << 4) sleep(0.01) # close w/ freeze: outputs should not be reset (usually 0b0110) gpio.close(True) - def test_gpio_values(self): """Simple test to demonstrate bit-banging. """ if self.skip_loopback: raise SkipTest('Skip loopback test on multiport device') - direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In + direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio = GpioAsyncController() gpio.configure(self.url, direction=direction, frequency=1e6, initial=0x0) @@ -182,7 +181,7 @@ def test_gpio_initial(self): raise SkipTest('Skip initial test on multiport device') if not self.loader: raise SkipTest('Skip initial test on physical device') - direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In + direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In vftdi = self.loader.get_virtual_ftdi(1, 1) vport = vftdi.get_port(1) gpio = GpioAsyncController() @@ -199,7 +198,7 @@ def test_gpio_loopback(self): if self.skip_loopback: raise SkipTest('Skip loopback test on multiport device') gpio = GpioAsyncController() - direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In + direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio.configure(self.url, direction=direction, frequency=800000) for out in range(16): # print(f'Write {out:04b} -> {out << 4:08b}') @@ -211,7 +210,7 @@ def test_gpio_loopback(self): self.assertEqual(lsbs, out) # check level of outputs match the ones written self.assertEqual(msbs, out) - outs = list([(out & 0xf) << 4 for out in range(1000)]) + outs = list((out & 0xf) << 4 for out in range(1000)) gpio.write(outs) gpio.ftdi.read_data(512) for _ in range(len(outs)): @@ -234,7 +233,7 @@ def test_gpio_baudate(self): # mesure their frequency. The EEPROM should be configured to enable # those signal on some of the CBUS pins, for example. gpio = GpioAsyncController() - direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In + direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio.configure(self.url, direction=direction) buf = bytes([0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00, 0xf0, 0x00]) freqs = [50e3, 200e3, 1e6, 3e6] @@ -307,10 +306,10 @@ def test_gpio_values(self): """ if self.skip_loopback: raise SkipTest('Skip loopback test on multiport device') - direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In + direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio = GpioSyncController() gpio.configure(self.url, direction=direction, initial=0xee) - outs = bytes([(out & 0xf)<<4 for out in range(1000)]) + outs = bytes([(out & 0xf) << 4 for out in range(1000)]) ins = gpio.exchange(outs) exp_in_count = min(len(outs), gpio.ftdi.fifo_sizes[0]) self.assertEqual(len(ins), exp_in_count) @@ -336,7 +335,7 @@ def test_gpio_baudate(self): # mesure their frequency. The EEPROM should be configured to enable # those signal on some of the CBUS pins, for example. gpio = GpioSyncController() - direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In + direction = 0xFF & ~((1 << 4) - 1) # 4 Out, 4 In gpio.configure(self.url, direction=direction) buf = bytes([0xf0, 0x00] * 64) freqs = [50e3, 200e3, 1e6, 3e6] @@ -444,12 +443,11 @@ def test_gpio_stream(self): qout.popleft() # offset is the count of missed bytes offset = len(ins)-len(qout) - self.assertGreater(offset, 0) # no more output than input - self.assertLess(offset, 16) # seems to be in the 6..12 range + self.assertGreater(offset, 0) # no more output than input + self.assertLess(offset, 16) # seems to be in the 6..12 range # print('Offset', offset) # check that the remaining sequence match for sout, sin in zip(qout, ins): - #print(f'{sout:08b} --> {sin:08b}') # check inputs match outputs self.assertEqual(sout, sin) gpio_in.close() @@ -580,7 +578,6 @@ def test_peek_gpio(self): inv = gpio_in.read(peek=True) # check inputs match outputs self.assertEqual(inv, out) - #print(f'{out} {inv}') # check level of outputs match the ones written self.assertEqual(outv, out) gpio_in.close() @@ -620,6 +617,7 @@ def suite(): def virtualize(): if not to_bool(environ.get('FTDI_VIRTUAL', 'off')): return + # pylint: disable=import-outside-toplevel from pyftdi.usbtools import UsbTools # Force PyUSB to use PyFtdi test framework for USB backends UsbTools.BACKENDS = ('backend.usbvirt', ) @@ -634,8 +632,9 @@ def virtualize(): def setup_module(): - import doctest - doctest.testmod(modules[__name__]) + # pylint: disable=import-outside-toplevel + from doctest import testmod + testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) if debug: formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)-7s' diff --git a/pyftdi/tests/i2c.py b/pyftdi/tests/i2c.py index 24b786e3..f74f9c19 100755 --- a/pyftdi/tests/i2c.py +++ b/pyftdi/tests/i2c.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2017-2023, Emmanuel Blot +"""I2C unit tests.""" + +# Copyright (c) 2017-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -12,13 +14,13 @@ from doctest import testmod from os import environ from sys import modules, stdout +from time import time as now from pyftdi import FtdiLogger from pyftdi.i2c import I2cController, I2cIOError from pyftdi.misc import pretty_size # pylint: disable=attribute-defined-outside-init # pylint: disable=missing-docstring -# pylint: disable=no-self-use class I2cTca9555TestCase(TestCase): @@ -137,9 +139,6 @@ def test_short(self): def test_long(self): port = self._i2c.get_port(self.address) # select start address - #print('RC', self._i2c.ftdi.read_data_get_chunksize()) - #print('WC', self._i2c.ftdi.write_data_get_chunksize()) - from time import time as now size = 4096 port.write(b'\x00\x00') start = now() @@ -248,7 +247,7 @@ def test(self): url1 = environ.get('FTDI_DEVICE', 'ftdi:///1') i2c1 = I2cController() i2c1.configure(url1, frequency=100000) - url2 = '%s%d' % (url1[:-1], int(url1[-1])+1) + url2 = f'{url1[:-1]}{int(url1[-1])+1}' i2c2 = I2cController() i2c2.configure(url2, frequency=100000) port = i2c2.get_port(0x76) @@ -270,11 +269,11 @@ def test(self): gpio = i2c.get_gpio() gpio.set_direction(0x0010, 0x0010) gpio.write(0) - gpio.write(1<<4) + gpio.write(1 << 4) gpio.write(0) slave.write([0x12, 0x34]) gpio.write(0) - gpio.write(1<<4) + gpio.write(1 << 4) gpio.write(0) @@ -291,13 +290,13 @@ def suite(): bridge -or any unsupported setup!! You've been warned. """ ste = TestSuite() - #ste.addTest(I2cTca9555TestCase('test')) - #ste.addTest(I2cAccelTest('test')) - #ste.addTest(I2cReadTest('test')) + # ste.addTest(I2cTca9555TestCase('test')) + # ste.addTest(I2cAccelTest('test')) + # ste.addTest(I2cReadTest('test')) ste.addTest(makeSuite(I2cEepromTest, 'test')) - #ste.addTest(I2cReadGpioTest('test')) + # ste.addTest(I2cReadGpioTest('test')) ste.addTest(I2cClockStrechingGpioCheck('test')) - #ste.addTest(I2cDualMaster('test')) + # ste.addTest(I2cDualMaster('test')) ste.addTest(I2cIssue143('test')) return ste diff --git a/pyftdi/tests/jtag.py b/pyftdi/tests/jtag.py index a87db2a9..d0e03265 100755 --- a/pyftdi/tests/jtag.py +++ b/pyftdi/tests/jtag.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2011-2020, Emmanuel Blot +"""JTAG unit test.""" + +# Copyright (c) 2011-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -38,7 +40,7 @@ def test_idcode_reset(self): self.jtag.reset() idcode = self.jtag.read_dr(32) self.jtag.go_idle() - print("IDCODE (reset): 0x%x" % int(idcode)) + print(f'IDCODE (reset): 0x{int(idcode):x}') def test_idcode_sequence(self): """Read the IDCODE using the dedicated instruction""" @@ -46,7 +48,7 @@ def test_idcode_sequence(self): self.jtag.write_ir(instruction) idcode = self.jtag.read_dr(32) self.jtag.go_idle() - print("IDCODE (idcode): 0x%08x" % int(idcode)) + print(f'IDCODE (idcode): 0x{int(idcode):08x}') def test_idcode_shift_register(self): """Read the IDCODE using the dedicated instruction with @@ -54,26 +56,26 @@ def test_idcode_shift_register(self): instruction = JTAG_INSTR['IDCODE'] self.jtag.change_state('shift_ir') retval = self.jtag.shift_and_update_register(instruction) - print("retval: 0x%x" % int(retval)) + print(f'retval: 0x{int(retval):x}') self.jtag.go_idle() self.jtag.change_state('shift_dr') idcode = self.jtag.shift_and_update_register(BitSequence('0'*32)) self.jtag.go_idle() - print("IDCODE (idcode): 0x%08x" % int(idcode)) + print(f'IDCODE (idcode): 0x{int(idcode):08x}') def test_bypass_shift_register(self): """Test the BYPASS instruction using shift_and_update_register""" instruction = JTAG_INSTR['BYPASS'] self.jtag.change_state('shift_ir') retval = self.jtag.shift_and_update_register(instruction) - print("retval: 0x%x" % int(retval)) + print(f'retval: 0x{int(retval):x}') self.jtag.go_idle() self.jtag.change_state('shift_dr') - _in = BitSequence('011011110000'*2, length=24) - out = self.jtag.shift_and_update_register(_in) + in_ = BitSequence('011011110000'*2, length=24) + out = self.jtag.shift_and_update_register(in_) self.jtag.go_idle() - print("BYPASS sent: %s, received: %s (should be left shifted by one)" - % (_in, out)) + print(f'BYPASS sent: {in_}, received: {out} ' + f' (should be left shifted by one)') def _test_detect_ir_length(self): """Detect the instruction register length""" diff --git a/pyftdi/tests/mockusb.py b/pyftdi/tests/mockusb.py index e90477b8..842eb196 100755 --- a/pyftdi/tests/mockusb.py +++ b/pyftdi/tests/mockusb.py @@ -5,13 +5,11 @@ # All rights reserved. # pylint: disable=empty-docstring -# pylint: disable=missing-docstring -# pylint: disable=no-self-use -# pylint: disable=invalid-name # pylint: disable=global-statement +# pylint: disable=invalid-name +# pylint: disable=missing-docstring # pylint: disable=too-many-locals - import logging from collections import defaultdict from contextlib import redirect_stdout @@ -19,7 +17,7 @@ from io import StringIO from os import environ from string import ascii_letters -from sys import modules, stdout, version_info +from sys import modules, stdout from unittest import TestCase, TestSuite, makeSuite, main as ut_main from urllib.parse import urlsplit from pyftdi import FtdiLogger @@ -156,7 +154,7 @@ def test_list_devices(self): self.assertEqual(len(devs), 1) out = StringIO() Ftdi.show_devices('ftdi:///?', out) - lines = [l.strip() for l in out.getvalue().split('\n')] + lines = [ln.strip() for ln in out.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() @@ -190,7 +188,7 @@ def test_enumerate(self): temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') - lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] + lines = [ln.strip() for ln in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() @@ -216,7 +214,7 @@ def test_enumerate(self): temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') - lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] + lines = [ln.strip() for ln in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() @@ -243,7 +241,7 @@ def test_enumerate(self): temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') - lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] + lines = [ln.strip() for ln in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() @@ -270,7 +268,7 @@ def test_enumerate(self): temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') - lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] + lines = [ln.strip() for ln in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() @@ -297,7 +295,7 @@ def test_enumerate(self): temp_stdout = StringIO() with redirect_stdout(temp_stdout): self.assertRaises(SystemExit, ftdi.open_from_url, 'ftdi:///?') - lines = [l.strip() for l in temp_stdout.getvalue().split('\n')] + lines = [ln.strip() for ln in temp_stdout.getvalue().split('\n')] lines.pop(0) # "Available interfaces" while lines and not lines[-1]: lines.pop() @@ -411,10 +409,10 @@ def _test_gpio(self): def test_baudrate(self): """Check simple GPIO write and read sequence.""" # load custom CBUS config, with: - # CBUS0: GPIO (gpio) - # CBUS1: GPIO (gpio) - # CBUS0: DRIVE1 (forced to high level) - # CBUS0: TXLED (eq. to highz for tests) + # - CBUS0: GPIO (gpio) + # - CBUS1: GPIO (gpio) + # - CBUS0: DRIVE1 (forced to high level) + # - CBUS0: TXLED (eq. to highz for tests) with open('pyftdi/tests/resources/ft230x_io.yaml', 'rb') as yfp: self.loader.load(yfp) gpio = GpioController() @@ -486,7 +484,6 @@ def test_baudrate_fs_dev(self): 460800, 490000, 921600, 1000000, 1200000, 1500000, 2000000, 3000000): port.baudrate = baudrate - #print(f'{baudrate} -> {port.ftdi.baudrate} -> {vport.baudrate}') self.assertEqual(port.ftdi.baudrate, vport.baudrate) port.close() @@ -504,7 +501,6 @@ def test_baudrate_hs_dev(self): 460800, 490000, 921600, 1000000, 1200000, 1500000, 2000000, 3000000, 4000000, 6000000): port.baudrate = baudrate - #print(f'{baudrate} -> {port.ftdi.baudrate} -> {vport.baudrate}') self.assertEqual(port.ftdi.baudrate, vport.baudrate) port.close() @@ -728,10 +724,10 @@ def tearDown(self): def test_230x(self): """Check simple GPIO write and read sequence.""" # load custom CBUS config, with: - # CBUS0: GPIO (gpio) - # CBUS1: GPIO (gpio) - # CBUS0: DRIVE1 (forced to high level) - # CBUS0: TXLED (eq. to highz for tests) + # - CBUS0: GPIO (gpio) + # - CBUS1: GPIO (gpio) + # - CBUS0: DRIVE1 (forced to high level) + # - CBUS0: TXLED (eq. to highz for tests) with open('pyftdi/tests/resources/ft230x_io.yaml', 'rb') as yfp: self.loader.load(yfp) ftdi = Ftdi() @@ -774,13 +770,13 @@ def test_230x(self): def test_lc231x(self): """Check simple GPIO write and read sequence.""" - # load custom CBUS config, with: - # CBUS0: GPIO (gpio) - # CBUS1: TXLED - # CBUS2: DRIVE0 (to light up RX green led) - # CBUS3: GPIO (gpio) - # only CBUS0 and CBUS3 are available on LC231X - # CBUS1 is connected to TX led, CBUS2 to RX led + # load custom CBUS config, with: + # - CBUS0: GPIO (gpio) + # - CBUS1: TXLED + # - CBUS2: DRIVE0 (to light up RX green led) + # - CBUS3: GPIO (gpio) + # only CBUS0 and CBUS3 are available on LC231X + # - CBUS1 is connected to TX led, CBUS2 to RX led with open('pyftdi/tests/resources/ft231x_cbus.yaml', 'rb') as yfp: self.loader.load(yfp) ftdi = Ftdi() diff --git a/pyftdi/tests/spi.py b/pyftdi/tests/spi.py index feaf0bbb..cfb1289b 100755 --- a/pyftdi/tests/spi.py +++ b/pyftdi/tests/spi.py @@ -8,7 +8,6 @@ # pylint: disable=empty-docstring # pylint: disable=missing-docstring -# pylint: disable=no-self-use import logging import unittest @@ -297,7 +296,7 @@ def test_bit_duplex(self): for loop in range(7): data = self._port.exchange(buf, duplex=True, droptail=loop+1) self.assertEqual(len(data), 1) - exp = buf[0] & ~((1<<(loop+1))-1) + exp = buf[0] & ~((1 << (loop+1))-1) # print(f'{data[0]:08b} {exp:08b}') self.assertEqual(data[0], exp) @@ -306,7 +305,7 @@ def test_bytebit_duplex(self): for loop in range(7): data = self._port.exchange(buf, duplex=True, droptail=loop+1) self.assertEqual(len(data), 2) - exp = buf[-1] & ~((1<<(loop+1))-1) + exp = buf[-1] & ~((1 << (loop+1))-1) # print(f'{data[-1]:08b} {exp:08b}') self.assertEqual(data[0], 0xFF) self.assertEqual(data[-1], exp) diff --git a/pyftdi/tests/timearray.py b/pyftdi/tests/timearray.py deleted file mode 100644 index a5b66d3c..00000000 --- a/pyftdi/tests/timearray.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 - -"""Quick and dirty bytearray vs. array('B') performance test.""" - -from array import array -from struct import pack -from timeit import timeit -from time import perf_counter - -def timing(f, n, a): - start = perf_counter() - for _ in range(n): - f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a); f(a) - finish = perf_counter() - return '%s\t%f' % (f.__name__, finish - start) - -def time_array(addr): - return array('B', addr) - -def time_bytearray(addr): - return bytearray(addr) - -def extend_array(addr): - b = bytearray() - b.extend(addr) - b.extend(b) - b.extend(b) - b.extend(b) - b.extend(b) - b.extend(b) - return b - -def extend_bytearray(addr): - b = bytearray() - b.extend(addr) - b.extend(b) - b.extend(b) - b.extend(b) - b.extend(b) - b.extend(b) - return b - -def array_tostring(addr): - return array('B', addr).tobytes() - -def str_bytearray(addr): - return str(bytearray(addr)) - -def struct_pack(addr): - return pack('4B', *addr) - -def main(): - count = 100000 - addr = '192.168.4.2' - addr = tuple([int(i) for i in addr.split('.')]) - print('\t\ttiming\t\tfunc\t\tno func') - print('%s\t%s\t%s' % (timing(time_array, count, addr), - timeit('time_array((192,168,4,2))', number=count, - setup='from __main__ import time_array'), - timeit("array('B', (192,168,4,2))", number=count, - setup='from array import array'))) - print('%s\t%s\t%s' % (timing(time_bytearray, count, addr), - timeit('time_bytearray((192,168,4,2))', number=count, - setup='from __main__ import time_bytearray'), - timeit('bytearray((192,168,4,2))', number=count))) - print('%s\t%s' % (timing(extend_array, count, addr), - timeit('extend_array((192,168,4,2))', number=count, - setup='from __main__ import extend_array'))) - print('%s\t%s' % (timing(extend_bytearray, count, addr), - timeit('extend_bytearray((192,168,4,2))', number=count, - setup='from __main__ import extend_bytearray'))) - print('%s\t%s\t%s' % (timing(array_tostring, count, addr), - timeit('array_tostring((192,168,4,2))', number=count, - setup='from __main__ import array_tostring'), - timeit("array('B', (192,168,4,2)).tostring()", number=count, - setup='from array import array'))) - print('%s\t%s\t%s' % (timing(str_bytearray, count, addr), - timeit('str_bytearray((192,168,4,2))', number=count, - setup='from __main__ import str_bytearray'), - timeit('str(bytearray((192,168,4,2)))', number=count))) - print('%s\t%s\t%s' % (timing(struct_pack, count, addr), - timeit('struct_pack((192,168,4,2))', number=count, - setup='from __main__ import struct_pack'), - timeit("pack('4B', *(192,168,4,2))", number=count, - setup='from struct import pack'))) - -if __name__ == '__main__': - main() diff --git a/pyftdi/tests/toolsimport.py b/pyftdi/tests/toolsimport.py index 2cc29128..a12241ae 100755 --- a/pyftdi/tests/toolsimport.py +++ b/pyftdi/tests/toolsimport.py @@ -1,14 +1,13 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2020, Emmanuel Blot +# Copyright (c) 2024, Emmanuel Blot # All rights reserved. # pylint: disable=empty-docstring -# pylint: disable=missing-docstring -# pylint: disable=no-self-use -# pylint: disable=invalid-name # pylint: disable=global-statement +# pylint: disable=invalid-name +# pylint: disable=missing-docstring from doctest import testmod from importlib import import_module diff --git a/pyftdi/tests/uart.py b/pyftdi/tests/uart.py index f9e6b0e2..333e3f82 100755 --- a/pyftdi/tests/uart.py +++ b/pyftdi/tests/uart.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2017-2020, Emmanuel Blot +"""UART unit tests.""" + +# Copyright (c) 2017-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -39,7 +41,7 @@ class FtdiTestCase(TestCase): @classmethod def setUpClass(cls): cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), - permissive=False) + permissive=False) def setUp(self): if self.debug: @@ -224,7 +226,7 @@ def _stream_source(cls, port, chunk, size, results): tx_size = 0 start = now() while tx_size < size: - samples = spack('>%dI' % chunk, *range(pos, pos+chunk)) + samples = spack(f'>{chunk}I', *range(pos, pos+chunk)) pos += chunk port.write(samples) tx_size += len(samples) @@ -251,7 +253,8 @@ def _stream_sink(cls, port, size, results): data.extend(buf) sample_count = len(data)//sample_size length = sample_count*sample_size - samples = sunpack('>%dI' % sample_count, data[:length]) + samples = sunpack(f'>{sample_count}I' % sample_count, + data[:length]) data = data[length:] for sample in samples: if first is None: @@ -316,7 +319,7 @@ def generate_bytes(cls, count=0): def build_next_url(cls, url): iface = int(url[-1]) iface = (iface + 1) % 3 - return '%s%d' % (url[:-1], iface) + return f'{url[:-1]}{iface}' class BaudrateTestCase(FtdiTestCase): @@ -352,10 +355,11 @@ def main(): try: loglevel = getattr(logging, level) except AttributeError as exc: - raise ValueError('Invalid log level: %s' % level) from exc + raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.set_level(loglevel) ut_main(defaultTest='suite') + if __name__ == '__main__': if platform == 'darwin': # avoid the infamous "The process has forked and you cannot use this diff --git a/pyftdi/tracer.py b/pyftdi/tracer.py index cf98cc12..50b295f5 100644 --- a/pyftdi/tracer.py +++ b/pyftdi/tracer.py @@ -9,12 +9,13 @@ from binascii import hexlify from collections import deque +from importlib import import_module from inspect import currentframe from logging import getLogger from string import ascii_uppercase from struct import unpack as sunpack +from sys import modules from typing import Union -from .ftdi import Ftdi class FtdiMpsseTracer: @@ -47,8 +48,8 @@ def _get_engine(self, iface: int): try: self._engines[iface] except IndexError as exc: - raise ValueError('No MPSSE engine available on interface %d' % - iface) from exc + raise ValueError(f'No MPSSE engine available on interface ' + f'{iface}') from exc if not self._engines[iface]: self._engines[iface] = FtdiMpsseEngine(iface) return self._engines[iface] @@ -63,22 +64,6 @@ class FtdiMpsseEngine: COMMAND_PREFIX = \ 'GET SET READ WRITE RW ENABLE DISABLE CLK LOOPBACK SEND DRIVE' - def build_commands(prefix: str): - commands = {} - for cmd in dir(Ftdi): - if cmd[0] not in ascii_uppercase: - continue - value = getattr(Ftdi, cmd) - if not isinstance(value, int): - continue - family = cmd.split('_')[0] - if family not in prefix.split(): - continue - commands[value] = cmd - return commands - - COMMANDS = build_commands(COMMAND_PREFIX) - ST_IDLE = range(1) def __init__(self, iface: int): @@ -92,17 +77,18 @@ def __init__(self, iface: int): self._resp_decoded = True self._last_codes = deque() self._expect_resp = deque() # positive: byte, negative: bit count + self._commands = self._build_commands() def send(self, buf: Union[bytes, bytearray]) -> None: self._trace_tx.extend(buf) while self._trace_tx: try: code = self._trace_tx[0] - cmd = self.COMMANDS[code] + cmd = self._commands[code] if self._cmd_decoded: self.log.debug('[%d]:[Command: %02X: %s]', self._if, code, cmd) - cmd_decoder = getattr(self, '_cmd_%s' % cmd.lower()) + cmd_decoder = getattr(self, f'_cmd_{cmd.lower()}') rdepth = len(self._expect_resp) try: self._cmd_decoded = cmd_decoder() @@ -120,7 +106,7 @@ def send(self, buf: Union[bytes, bytearray]) -> None: except KeyError: self.log.warning('[%d]:Unknown command code: %02X', self._if, code) - except AttributeError as exc: + except AttributeError: self.log.warning('[%d]:Decoder for command %s [%02X] is not ' 'implemented', self._if, cmd, code) except ValueError as exc: @@ -139,8 +125,8 @@ def receive(self, buf: Union[bytes, bytearray]) -> None: code = None try: code = self._last_codes.popleft() - cmd = self.COMMANDS[code] - resp_decoder = getattr(self, '_resp_%s' % cmd.lower()) + cmd = self._commands[code] + resp_decoder = getattr(self, f'_resp_{cmd.lower()}') self._resp_decoded = resp_decoder() if self._resp_decoded: continue @@ -159,6 +145,28 @@ def receive(self, buf: Union[bytes, bytearray]) -> None: self._trace_rx = bytearray() self._last_codes.clear() + @classmethod + def _build_commands(cls): + # pylint: disable=no-self-argument + commands = {} + fdti_mod_name = 'pyftdi.ftdi' + ftdi_mod = modules.get(fdti_mod_name) + if not ftdi_mod: + ftdi_mod = import_module(fdti_mod_name) + ftdi_type = getattr(ftdi_mod, 'Ftdi') + for cmd in dir(ftdi_type): + if cmd[0] not in ascii_uppercase: + continue + value = getattr(ftdi_type, cmd) + if not isinstance(value, int): + continue + family = cmd.split('_')[0] + # pylint: disable=no-member + if family not in cls.COMMAND_PREFIX.split(): + continue + commands[value] = cmd + return commands + def _cmd_enable_clk_div5(self): self.log.info(' [%d]:Enable clock divisor /5', self._if) self._clkdiv5 = True @@ -463,7 +471,7 @@ def bm2str(cls, value: int, mask: int, hiz: str = '_') -> str: @classmethod def bitfmt(cls, value, width): - return format(value, '0%db' % width) + return format(value, f'0{width}b') # rw_bytes_pve_pve_lsb # rw_bytes_pve_nve_lsb diff --git a/pyftdi/usbtools.py b/pyftdi/usbtools.py index 951ecc9a..b8a593fd 100644 --- a/pyftdi/usbtools.py +++ b/pyftdi/usbtools.py @@ -55,6 +55,7 @@ it may be degraded to (vid, pid) 2-uple. """ + class UsbToolsError(Exception): """UsbTools error.""" @@ -85,15 +86,14 @@ def find_all(cls, vps: Sequence[Tuple[int, int]], the host :return: a list of 2-tuple (UsbDeviceDescriptor, interface count) """ - cls.Lock.acquire() - try: + with cls.Lock: devs = set() for vid, pid in vps: # TODO optimize useless loops devs.update(UsbTools._find_devices(vid, pid, nocache)) devices = set() for dev in devs: - ifcount = max([cfg.bNumInterfaces for cfg in dev]) + ifcount = max(cfg.bNumInterfaces for cfg in dev) # TODO: handle / is serial number strings sernum = UsbTools.get_string(dev, dev.iSerialNumber) description = UsbTools.get_string(dev, dev.iProduct) @@ -102,8 +102,6 @@ def find_all(cls, vps: Sequence[Tuple[int, int]], sernum, None, description) devices.add((descriptor, ifcount)) return list(devices) - finally: - cls.Lock.release() @classmethod def flush_cache(cls, ): @@ -117,9 +115,8 @@ def flush_cache(cls, ): Failing to clear out the cache may lead to USB Error 19: ``Device may have been disconnected``. """ - cls.Lock.acquire() - cls.UsbDevices.clear() - cls.Lock.release() + with cls.Lock: + cls.UsbDevices.clear() @classmethod def get_device(cls, devdesc: UsbDeviceDescriptor) -> UsbDevice: @@ -142,8 +139,7 @@ def get_device(cls, devdesc: UsbDeviceDescriptor) -> UsbDevice: constraints. :return: PyUSB device instance """ - cls.Lock.acquire() - try: + with cls.Lock: if devdesc.index or devdesc.sn or devdesc.description: dev = None if not devdesc.vid: @@ -199,8 +195,6 @@ def get_device(cls, devdesc: UsbDeviceDescriptor) -> UsbDevice: else: cls.Devices[devkey][1] += 1 return cls.Devices[devkey][0] - finally: - cls.Lock.release() @classmethod def release_device(cls, usb_dev: UsbDevice): @@ -209,10 +203,9 @@ def release_device(cls, usb_dev: UsbDevice): :param usb_dev: a previously instanciated USB device instance """ # Lookup for ourselves in the class dictionary - cls.Lock.acquire() - try: - for devkey in cls.Devices: - dev, refcount = cls.Devices[devkey] + with cls.Lock: + # pylint: disable=unnecessary-dict-index-lookup + for devkey, (dev, refcount) in cls.Devices.items(): if dev == usb_dev: # found if refcount > 1: @@ -223,8 +216,6 @@ def release_device(cls, usb_dev: UsbDevice): dispose_resources(cls.Devices[devkey][0]) del cls.Devices[devkey] break - finally: - cls.Lock.release() @classmethod def release_all_devices(cls, devclass: Optional[Type] = None) -> int: @@ -233,9 +224,9 @@ def release_all_devices(cls, devclass: Optional[Type] = None) -> int: :param devclass: optional class to only release devices of one type :return: the count of device that have been released. """ - cls.Lock.acquire() - try: + with cls.Lock: remove_devs = set() + # pylint: disable=consider-using-dict-items for devkey in cls.Devices: if devclass: dev = cls._get_backend_device(cls.Devices[devkey][0]) @@ -246,8 +237,6 @@ def release_all_devices(cls, devclass: Optional[Type] = None) -> int: for devkey in remove_devs: del cls.Devices[devkey] return len(remove_devs) - finally: - cls.Lock.release() @classmethod def list_devices(cls, urlstr: str, @@ -295,7 +284,7 @@ def parse_url(cls, urlstr: str, scheme: str, """ urlparts = urlsplit(urlstr) if scheme != urlparts.scheme: - raise UsbToolsError("Invalid URL: %s" % urlstr) + raise UsbToolsError(f'Invalid URL: {urlstr}') try: if not urlparts.path: raise UsbToolsError('URL string is missing device port') @@ -306,7 +295,7 @@ def parse_url(cls, urlstr: str, scheme: str, interface = to_int(path) report_devices = False except (IndexError, ValueError) as exc: - raise UsbToolsError('Invalid device URL: %s' % urlstr) from exc + raise UsbToolsError(f'Invalid device URL: {urlstr}') from exc candidates, idx = cls.enumerate_candidates(urlparts, vdict, pdict, default_vendor) if report_devices: @@ -316,30 +305,30 @@ def parse_url(cls, urlstr: str, scheme: str, 'No USB-Serial device has been detected') if idx is None: if len(candidates) > 1: - raise UsbToolsError("%d USB devices match URL '%s'" % - (len(candidates), urlstr)) + raise UsbToolsError(f"{len(candidates)} USB devices match URL " + f"'{urlstr}'") idx = 0 try: desc, _ = candidates[idx] vendor, product = desc[:2] except IndexError: - raise UsbToolsError('No USB device matches URL %s' % - urlstr) from None + raise UsbToolsError(f'No USB device matches URL {urlstr}') \ + from None if not vendor: cvendors = {candidate[0] for candidate in candidates} if len(cvendors) == 1: vendor = cvendors.pop() if vendor not in pdict: - raise UsbToolsError('Vendor ID %s not supported' % - (vendor and '0x%04x' % vendor)) + vstr = '0x{vendor:04x}' if vendor is not None else '?' + raise UsbToolsError(f'Vendor ID {vstr} not supported') if not product: cproducts = {candidate[1] for candidate in candidates if candidate[0] == vendor} if len(cproducts) == 1: product = cproducts.pop() if product not in pdict[vendor].values(): - raise UsbToolsError('Product ID %s not supported' % - (product and '0x%04x' % product)) + pstr = '0x{vendor:04x}' if product is not None else '?' + raise UsbToolsError(f'Product ID {pstr} not supported') devdesc = UsbDeviceDescriptor(vendor, product, desc.bus, desc.address, desc.sn, idx, desc.description) return devdesc, interface @@ -375,13 +364,13 @@ def enumerate_candidates(cls, urlparts: SplitResult, try: product = to_int(plcomps[1]) except ValueError as exc: - raise UsbToolsError('Product %s is not referenced' % - plcomps[1]) from exc + raise UsbToolsError(f'Product {plcomps[1]} is not ' + f'referenced') from exc else: product = None except (IndexError, ValueError) as exc: - raise UsbToolsError('Invalid device URL: %s' % - urlunsplit(urlparts)) from exc + raise UsbToolsError(f'Invalid device URL: ' + f'{urlunsplit(urlparts)}') from exc sernum = None idx = None bus = None @@ -392,8 +381,8 @@ def enumerate_candidates(cls, urlparts: SplitResult, bus = int(locators[0], 16) address = int(locators[1], 16) except ValueError as exc: - raise UsbToolsError('Invalid bus/address: %s' % - ':'.join(locators)) from exc + raise UsbToolsError(f'Invalid bus/address: ' + f'{":".join(locators)}') from exc else: if locators and locators[0]: try: @@ -415,7 +404,7 @@ def enumerate_candidates(cls, urlparts: SplitResult, devices = cls.find_all(vps) if sernum: if sernum not in [dev.sn for dev, _ in devices]: - raise UsbToolsError("No USB device with S/N %s" % sernum) + raise UsbToolsError(f'No USB device with S/N {sernum}') for desc, ifcount in devices: if vendor and vendor != desc.vid: continue @@ -451,10 +440,10 @@ def show_devices(cls, scheme: str, if not out: out = sys.stdout devstrs = cls.build_dev_strings(scheme, vdict, pdict, devdescs) - max_url_len = max([len(url) for url, _ in devstrs]) - print("Available interfaces:", file=out) - for desc in devstrs: - print((' %%-%ds %%s' % max_url_len) % desc, file=out) + max_url_len = max(len(url) for url, _ in devstrs) + print('Available interfaces:', file=out) + for url, desc in devstrs: + print(f' {url:{max_url_len}s} {desc}', file=out) print('', file=out) @classmethod @@ -480,7 +469,7 @@ def build_dev_strings(cls, scheme: str, # try to find a matching string for the current vendor vendors = [] # fallback if no matching string for the current vendor is found - vendor = '%04x' % desc.vid + vendor = f'{desc.vid:04x}' for vidc in vdict: if vdict[vidc] == desc.vid: vendors.append(vidc) @@ -489,7 +478,7 @@ def build_dev_strings(cls, scheme: str, vendor = vendors[0] # try to find a matching string for the current vendor # fallback if no matching string for the current product is found - product = '%04x' % desc.pid + product = f'{desc.pid:04x}' try: products = [] productids = pdict[desc.vid] @@ -507,14 +496,14 @@ def build_dev_strings(cls, scheme: str, if not sernum: sernum = '' if [c for c in sernum if c not in printablechars or c == '?']: - serial = '%d' % indices[ikey] + serial = f'{indices[ikey]}' else: serial = sernum if serial: parts.append(serial) elif desc.bus is not None and desc.address is not None: - parts.append('%x' % desc.bus) - parts.append('%x' % desc.address) + parts.append(f'{desc.bus:x}') + parts.append(f'{desc.address:x}') # the description may contain characters that cannot be # emitted in the output stream encoding format try: @@ -524,7 +513,7 @@ def build_dev_strings(cls, scheme: str, port) try: if desc.description: - description = '(%s)' % desc.description + description = f'({desc.description})' else: description = '' except Exception: @@ -565,11 +554,8 @@ def find_backend(cls) -> IBackend: :return: PyUSB backend """ - cls.Lock.acquire() - try: + with cls.Lock: return cls._load_backend() - finally: - cls.Lock.release() @classmethod def _find_devices(cls, vendor: int, product: int, @@ -604,6 +590,7 @@ def _find_devices(cls, vendor: int, product: int, vpdict.setdefault(vendor, []) vpdict[vendor].append(product) for dev in backend.enumerate_devices(): + # pylint: disable=no-member device = UsbDevice(dev, backend) if device.idVendor in vpdict: products = vpdict[device.idVendor] @@ -617,17 +604,17 @@ def _find_devices(cls, vendor: int, product: int, # appears also as N device with as single interface. # We only keep the "device" that declares the most # interface count and discard the "virtual" ones. - filtered_devs = dict() + filtered_devs = {} for dev in devs: vid = dev.idVendor pid = dev.idProduct - ifc = max([cfg.bNumInterfaces for cfg in dev]) + ifc = max(cfg.bNumInterfaces for cfg in dev) k = (vid, pid, dev.bus, dev.address) if k not in filtered_devs: filtered_devs[k] = dev else: fdev = filtered_devs[k] - fifc = max([cfg.bNumInterfaces for cfg in fdev]) + fifc = max(cfg.bNumInterfaces for cfg in fdev) if fifc < ifc: filtered_devs[k] = dev devs = set(filtered_devs.values()) diff --git a/setup.py b/setup.py index 8d207332..8324f410 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2023 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # Copyright (c) 2010-2016 Neotion # All rights reserved. # @@ -131,6 +131,7 @@ def run(self): self.announce('checking coding style', level=2) filecount = 0 topdir = dirname(__file__) or getcwd() + error_count = 0 for dpath, dnames, fnames in walk(topdir): dnames[:] = [d for d in dnames if not d.startswith('.') and d != 'doc'] @@ -140,11 +141,14 @@ def run(self): with open(filename, 'rt', encoding='utf-8') as pfp: for lpos, line in enumerate(pfp, start=1): if len(line) > 80: - print(f'\n {lpos}: {line.rstrip()}') toppath = relpath(filename, topdir) - raise RuntimeError(f"Invalid line width in " - f"{toppath}:{lpos}") + print(f' invalid line width in {toppath}:{lpos}', + file=stderr) + print(f' {line.strip()}', file=stderr) + error_count += 1 filecount += 1 + if error_count: + raise RuntimeError(f'{error_count} errors') if not filecount: raise RuntimeError(f'No Python file found from "{topdir}"') From aeebeeac70143ca0e6f104e18815b6104acf254d Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 6 Apr 2024 17:19:12 +0200 Subject: [PATCH 36/62] .github: add PyLint linter to CI Signed-off-by: Emmanuel Blot --- .github/workflows/pythonchecksyntax.yml | 5 +++++ test-requirements.txt | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 5cc96f43..c9d3843f 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -28,6 +28,11 @@ jobs: - name: Check style run: | python setup.py check_style + - name: Linter + run: | + pylint --disable=fixme --disable=duplicate-code \ + pyftdi pyftdi/bin pyftdi/serialext \ + pyftdi/tests pyftdi/serialext/tests pyftdi/tests/backend - name: Install package run: | python setup.py install diff --git a/test-requirements.txt b/test-requirements.txt index 2031f04f..2edb271d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ -ruamel.yaml >= 0.16 setuptools wheel +pylint +ruamel.yaml >= 0.16 From 93954e248335004daa8b22297feb1224c3f59df4 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 6 Apr 2024 17:35:39 +0200 Subject: [PATCH 37/62] add Python 3.12 --- .github/workflows/pythonchecksyntax.yml | 2 +- .github/workflows/pythonmocktests.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- setup.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index c9d3843f..f405f341 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pythonmocktests.yml b/.github/workflows/pythonmocktests.yml index 216c14d9..6b82b114 100644 --- a/.github/workflows/pythonmocktests.yml +++ b/.github/workflows/pythonmocktests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index a9dad9b5..747368aa 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 diff --git a/setup.py b/setup.py index 8324f410..c83d2086 100755 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Hardware :: Hardware Drivers', ] From 3d2fed8a6ccf1fba5970af79f035ca5cb88dc4e2 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 6 Apr 2024 18:23:25 +0200 Subject: [PATCH 38/62] unit tests: replace deprecated makeSuite method Signed-off-by: Emmanuel Blot --- pyftdi/__init__.py | 2 +- pyftdi/tests/bits.py | 23 +++++++++++++---------- pyftdi/tests/cbus.py | 27 +++++++++++++++++++++------ pyftdi/tests/eeprom.py | 11 ++++++----- pyftdi/tests/eeprom_mock.py | 34 ++++++++++++++++++++-------------- pyftdi/tests/ftdi.py | 13 ++++++++----- pyftdi/tests/gpio.py | 7 ++----- pyftdi/tests/i2c.py | 37 +++++++++++++++++++------------------ pyftdi/tests/jtag.py | 7 +++++-- pyftdi/tests/mockusb.py | 18 ++---------------- pyftdi/tests/spi.py | 25 ++++++++++++++----------- 11 files changed, 111 insertions(+), 93 deletions(-) mode change 100644 => 100755 pyftdi/tests/eeprom_mock.py diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 4596ccc3..fa945b71 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -6,7 +6,7 @@ # pylint: disable=missing-docstring -__version__ = '0.55.2' +__version__ = '0.55.3' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' diff --git a/pyftdi/tests/bits.py b/pyftdi/tests/bits.py index ebd1ea3b..99f74bd5 100755 --- a/pyftdi/tests/bits.py +++ b/pyftdi/tests/bits.py @@ -14,11 +14,12 @@ # pylint: disable=missing-class-docstring # pylint: disable=missing-function-docstring -import unittest +from sys import modules +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from pyftdi.bits import BitSequence, BitZSequence, BitSequenceError -class BitSequenceTestCase(unittest.TestCase): +class BitSequenceTestCase(TestCase): def setUp(self): self.bs1 = BitSequence(0x01, msb=True, length=8) @@ -80,19 +81,19 @@ def test_cmp(self): self.assertTrue(bzs.matches(self.bs7)) def test_representation(self): - self.assertEqual(f'{self.bs1} / {self.bs1!r}' % (self.bs1, self.bs1), + self.assertEqual(f'{self.bs1} / {self.bs1!r}', '8: 10000000 / 10000000') - self.assertEqual(f'{self.bs2} / {self.bs2!r}' % (self.bs2, self.bs2), + self.assertEqual(f'{self.bs2} / {self.bs2!r}', '8: 01000000 / 01000000') - self.assertEqual(f'{self.bs3} / {self.bs3!r}' % (self.bs3, self.bs3), + self.assertEqual(f'{self.bs3} / {self.bs3!r}', '7: 0010000 / 0010000') - self.assertEqual(f'{self.bs4} / {self.bs4!r}' % (self.bs4, self.bs4), + self.assertEqual(f'{self.bs4} / {self.bs4!r}', '11: 001 00000000 / 00100000000') - self.assertEqual(f'{self.bs5} / {self.bs5!r}' % (self.bs5, self.bs5), + self.assertEqual(f'{self.bs5} / {self.bs5!r}', '49: 1 00010000 11011001 00110001 01101110 10111111 ' '11111110 / 100010000110110010011000101101110101111' '1111111110') - self.assertEqual(f'{self.bs6} / {self.bs6!r}' % (self.bs6, self.bs6), + self.assertEqual(f'{self.bs6} / {self.bs6!r}', '49: 1 00010000 11011001 00110001 01101110 10111111 ' '11111111 / 100010000110110010011000101101110101111' '1111111111') @@ -210,8 +211,10 @@ def test_concat(self): def suite(): - return unittest.makeSuite(BitSequenceTestCase, 'test_') + suite_ = TestSuite() + suite_.addTest(TestLoader().loadTestsFromModule(modules[__name__])) + return suite_ if __name__ == '__main__': - unittest.main(defaultTest='suite') + ut_main(defaultTest='suite') diff --git a/pyftdi/tests/cbus.py b/pyftdi/tests/cbus.py index e6679d16..78787a18 100755 --- a/pyftdi/tests/cbus.py +++ b/pyftdi/tests/cbus.py @@ -11,7 +11,8 @@ import sys from doctest import testmod from os import environ -from unittest import TestCase, TestSuite, makeSuite, main as ut_main +from sys import modules +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from pyftdi.ftdi import Ftdi, FtdiError from pyftdi.eeprom import FtdiEeprom @@ -19,7 +20,7 @@ # pylint: disable=missing-docstring -class CbusGpioTestCase(TestCase): +class CbusOutputGpioTestCase(TestCase): """FTDI CBUS GPIO feature test case""" @classmethod @@ -27,7 +28,7 @@ def setUpClass(cls): """Default values""" cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') - def test_output_gpio(self): + def test_gpio(self): """Simple test to demonstrate ouput bit-banging on CBUS. You need a CBUS-capable FTDI (FT232R/FT232H/FT230X/FT231X), whose @@ -60,7 +61,16 @@ def test_output_gpio(self): sig = int(not ftdi.get_cts()) | (int(not ftdi.get_dsr()) << 1) self.assertEqual(value, sig) - def test_input_gpio(self): + +class CbusInputGpioTestCase(TestCase): + """FTDI CBUS GPIO feature test case""" + + @classmethod + def setUpClass(cls): + """Default values""" + cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') + + def test_gpio(self): """Simple test to demonstrate input bit-banging on CBUS. You need a CBUS-capable FTDI (FT232R/FT232H/FT230X/FT231X), whose @@ -96,9 +106,14 @@ def test_input_gpio(self): def suite(): suite_ = TestSuite() + loader = TestLoader() + mod = modules[__name__] # peak the test that matches your HW setup, see test doc for details - # suite_.addTest(makeSuite(CbusGpioTestCase, 'test_output')) - suite_.addTest(makeSuite(CbusGpioTestCase, 'test_input')) + tests = ( # 'CbusOutputGpio', + 'CbusInputGpio') + for testname in tests: + testcase = getattr(mod, f'{testname}TestCase') + suite_.addTest(loader.loadTestsFromTestCase(testcase)) return suite_ diff --git a/pyftdi/tests/eeprom.py b/pyftdi/tests/eeprom.py index 70dd0302..9a9c3036 100755 --- a/pyftdi/tests/eeprom.py +++ b/pyftdi/tests/eeprom.py @@ -10,8 +10,8 @@ # SPDX-License-Identifier: BSD-3-Clause import logging -import unittest from doctest import testmod +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from os import environ from sys import modules, stdout from pyftdi import FtdiLogger @@ -20,7 +20,8 @@ # pylint: disable=missing-docstring -class EepromTestCase(unittest.TestCase): + +class EepromTestCase(TestCase): """FTDI EEPROM access method test case""" @classmethod @@ -95,8 +96,8 @@ def test_eeprom_write(self): def suite(): - suite_ = unittest.TestSuite() - suite_.addTest(unittest.makeSuite(EepromTestCase, 'test')) + suite_ = TestSuite() + suite_.addTest(TestLoader().loadTestsFromModule(modules[__name__])) return suite_ @@ -110,7 +111,7 @@ def main(): raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.set_level(loglevel) testmod(modules[__name__]) - unittest.main(defaultTest='suite') + ut_main(defaultTest='suite') if __name__ == '__main__': diff --git a/pyftdi/tests/eeprom_mock.py b/pyftdi/tests/eeprom_mock.py old mode 100644 new mode 100755 index a3a528d0..05e63d52 --- a/pyftdi/tests/eeprom_mock.py +++ b/pyftdi/tests/eeprom_mock.py @@ -8,7 +8,7 @@ import logging from os import environ from sys import modules, stdout -from unittest import TestCase, TestSuite, makeSuite, main as ut_main +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.eeprom import FtdiEeprom @@ -50,10 +50,10 @@ def setUp(self): class EepromMirrorTestCase(FtdiTestCase): """Test FTDI EEPROM mirror feature (duplicate eeprom data over 2 eeprom - sectors). Generally this is tested with a virtual eeprom (by setting - environment variable FTDI_VIRTUAL=on), however you may also test with an - actual device at your own risk. Note that none of the tests should - commit any of their eeprom changes + sectors). Generally this is tested with a virtual eeprom (by setting + environment variable FTDI_VIRTUAL=on), however you may also test with an + actual device at your own risk. Note that none of the tests should + commit any of their eeprom changes """ @classmethod @@ -200,7 +200,7 @@ def _check_for_mirrored_eeprom_contents(self, eeprom: FtdiEeprom): class NonMirroredEepromTestCase(FtdiTestCase): """Test FTDI EEPROM mirror features do not break FTDI devices that do - not use mirroring + not use mirroring """ TEST_MANU_NAME = "MNAME" TEST_PROD_NAME = "PNAME" @@ -386,17 +386,23 @@ class EepromNonMirroredFt4232hTestCase(NonMirroredEepromTestCase, TestCase): def suite(): suite_ = TestSuite() + loader = TestLoader() + mod = modules[__name__] + tests = [] # Test devices that support the mirroring capability - suite_.addTest(makeSuite(EepromMirrorFt232hTestCase, 'test')) - suite_.addTest(makeSuite(EepromMirrorFt2232hTestCase, 'test')) - suite_.addTest(makeSuite(EepromMirrorFt4232hTestCase, 'test')) + tests.extend(('EepromMirrorFt232h', + 'EepromMirrorFt2232h', + 'EepromMirrorFt4232h')) # Test devices that do not support the mirror capability - suite_.addTest(makeSuite(EepromMirrorFt232rTestCase, 'test')) - suite_.addTest(makeSuite(EepromMirrorFt230xTestCase, 'test')) + tests.extend(('EepromMirrorFt232r', + 'EepromMirrorFt230x')) # test devices that support the mirroring capability, but have it disabled - suite_.addTest(makeSuite(EepromNonMirroredFt232hTestCase, 'test')) - suite_.addTest(makeSuite(EepromNonMirroredFt2232hTestCase, 'test')) - suite_.addTest(makeSuite(EepromNonMirroredFt4232hTestCase, 'test')) + tests.extend(('EepromNonMirroredFt232h', + 'EepromNonMirroredFt2232h', + 'EepromNonMirroredFt4232h')) + for testname in tests: + testcase = getattr(mod, f'{testname}TestCase') + suite_.addTest(loader.loadTestsFromTestCase(testcase)) return suite_ diff --git a/pyftdi/tests/ftdi.py b/pyftdi/tests/ftdi.py index 56600bde..46c284e2 100755 --- a/pyftdi/tests/ftdi.py +++ b/pyftdi/tests/ftdi.py @@ -15,7 +15,7 @@ from os import environ from sys import modules, stdout from time import sleep, time as now -from unittest import TestCase, TestSuite, SkipTest, makeSuite, main as ut_main +from unittest import TestCase, TestLoader, TestSuite, SkipTest, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi, FtdiError from pyftdi.usbtools import UsbTools, UsbToolsError @@ -152,10 +152,13 @@ def test_close_on_disconnect(self): def suite(): suite_ = TestSuite() - #suite_.addTest(makeSuite(FtdiTestCase, 'test')) - #suite_.addTest(makeSuite(HotplugTestCase, 'test')) - suite_.addTest(makeSuite(ResetTestCase, 'test')) - suite_.addTest(makeSuite(DisconnectTestCase, 'test')) + loader = TestLoader() + mod = modules[__name__] + # tests = 'Ftdi Hotplug Reset Disconnect' + tests = 'Reset Disconnect' + for testname in tests.split(): + testcase = getattr(mod, f'{testname}TestCase') + suite_.addTest(loader.loadTestsFromTestCase(testcase)) return suite_ diff --git a/pyftdi/tests/gpio.py b/pyftdi/tests/gpio.py index 0a0a49eb..2fcf1e7a 100755 --- a/pyftdi/tests/gpio.py +++ b/pyftdi/tests/gpio.py @@ -16,7 +16,7 @@ from os import environ from sys import modules, stdout from time import sleep -from unittest import TestCase, TestSuite, SkipTest, makeSuite, main as ut_main +from unittest import TestCase, TestLoader, TestSuite, SkipTest, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.gpio import (GpioAsyncController, @@ -607,10 +607,7 @@ def test_stream_gpio(self): def suite(): suite_ = TestSuite() - suite_.addTest(makeSuite(GpioAsyncTestCase, 'test')) - suite_.addTest(makeSuite(GpioSyncTestCase, 'test')) - suite_.addTest(makeSuite(GpioMpsseTestCase, 'test')) - suite_.addTest(makeSuite(GpioMultiportTestCase, 'test')) + suite_.addTest(TestLoader().loadTestsFromModule(modules[__name__])) return suite_ diff --git a/pyftdi/tests/i2c.py b/pyftdi/tests/i2c.py index f74f9c19..2bc01c9a 100755 --- a/pyftdi/tests/i2c.py +++ b/pyftdi/tests/i2c.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: BSD-3-Clause import logging -from unittest import TestCase, TestSuite, main as ut_main, makeSuite +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from binascii import hexlify from doctest import testmod from os import environ @@ -54,7 +54,7 @@ def _close(self): self._i2c.terminate() -class I2cAccelTest(TestCase): +class I2cAccelTestCase(TestCase): """Basic test for an ADXL345 device on I2C bus @ address 0x53 """ @@ -81,7 +81,7 @@ def _close(self): self._i2c.terminate() -class I2cReadTest(TestCase): +class I2cReadTestCase(TestCase): """Simple test to read a sequence of bytes I2C bus @ address 0x36 """ @@ -108,7 +108,7 @@ def _close(self): self._i2c.terminate() -class I2cEepromTest(TestCase): +class I2cEepromTestCase(TestCase): """Simple test to read a sequence of bytes I2C bus @ address 0x50, from an I2C data flash """ @@ -151,7 +151,7 @@ def test_long(self): self.assertEqual(text[8:12], 'Worl') -class I2cReadGpioTest(TestCase): +class I2cReadGpioTestCase(TestCase): """Simple test to exercise I2C + GPIO mode. A slave device (such as EEPROM) should be connected to the I2C bus @@ -225,7 +225,7 @@ def _close(self): self._i2c.terminate() -class I2cClockStrechingGpioCheck(TestCase): +class I2cClockStrechingGpioTestCase(TestCase): """Simple test to check clock stretching cannot be overwritten with GPIOs. """ @@ -238,7 +238,7 @@ def test(self): self.assertRaises(I2cIOError, gpio.set_direction, 1 << 7, 0) -class I2cDualMaster(TestCase): +class I2cDualMasterTestCase(TestCase): """Check the behaviour of 2 I2C masters. Requires a multi port FTDI device, i.e. FT2232H, FT4232H or FT4232HA. See issue #159. """ @@ -255,7 +255,7 @@ def test(self): print(port.read_from(0x00, 2)) -class I2cIssue143(TestCase): +class I2cIssue143TestCase(TestCase): """#143. """ @@ -289,16 +289,17 @@ def suite(): Do NOT run this test if you use FTDI port A as an UART or SPI bridge -or any unsupported setup!! You've been warned. """ - ste = TestSuite() - # ste.addTest(I2cTca9555TestCase('test')) - # ste.addTest(I2cAccelTest('test')) - # ste.addTest(I2cReadTest('test')) - ste.addTest(makeSuite(I2cEepromTest, 'test')) - # ste.addTest(I2cReadGpioTest('test')) - ste.addTest(I2cClockStrechingGpioCheck('test')) - # ste.addTest(I2cDualMaster('test')) - ste.addTest(I2cIssue143('test')) - return ste + suite_ = TestSuite() + loader = TestLoader() + mod = modules[__name__] + tests = ( # 'I2cTca9555', 'I2cAccel', 'I2cRead', + 'I2cEeprom', # 'I2cReadGpio', + 'I2cClockStrechingGpio', # 'I2cDualMaster', + 'I2cIssue143') + for testname in tests: + testcase = getattr(mod, f'{testname}TestCase') + suite_.addTest(loader.loadTestsFromTestCase(testcase)) + return suite_ def main(): diff --git a/pyftdi/tests/jtag.py b/pyftdi/tests/jtag.py index d0e03265..1bdd1e9d 100755 --- a/pyftdi/tests/jtag.py +++ b/pyftdi/tests/jtag.py @@ -9,7 +9,8 @@ # SPDX-License-Identifier: BSD-3-Clause from os import environ -from unittest import TestCase, main as ut_main, makeSuite +from sys import modules +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from pyftdi.jtag import JtagEngine, JtagTool from pyftdi.bits import BitSequence @@ -85,7 +86,9 @@ def _test_detect_ir_length(self): def suite(): - return makeSuite(JtagTestCase, 'test') + suite_ = TestSuite() + suite_.addTest(TestLoader().loadTestsFromModule(modules[__name__])) + return suite_ if __name__ == '__main__': diff --git a/pyftdi/tests/mockusb.py b/pyftdi/tests/mockusb.py index 842eb196..bc7ca581 100755 --- a/pyftdi/tests/mockusb.py +++ b/pyftdi/tests/mockusb.py @@ -18,7 +18,7 @@ from os import environ from string import ascii_letters from sys import modules, stdout -from unittest import TestCase, TestSuite, makeSuite, main as ut_main +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from urllib.parse import urlsplit from pyftdi import FtdiLogger from pyftdi.eeprom import FtdiEeprom @@ -817,21 +817,7 @@ def test_lc231x(self): def suite(): suite_ = TestSuite() - suite_.addTest(makeSuite(MockUsbToolsTestCase, 'test')) - suite_.addTest(makeSuite(MockFtdiDiscoveryTestCase, 'test')) - suite_.addTest(makeSuite(MockSimpleDeviceTestCase, 'test')) - suite_.addTest(makeSuite(MockDualDeviceTestCase, 'test')) - suite_.addTest(makeSuite(MockTwoPortDeviceTestCase, 'test')) - suite_.addTest(makeSuite(MockFourPortDeviceTestCase, 'test')) - suite_.addTest(makeSuite(MockManyDevicesTestCase, 'test')) - suite_.addTest(makeSuite(MockSimpleDirectTestCase, 'test')) - suite_.addTest(makeSuite(MockSimpleMpsseTestCase, 'test')) - suite_.addTest(makeSuite(MockSimpleGpioTestCase, 'test')) - suite_.addTest(makeSuite(MockSimpleUartTestCase, 'test')) - suite_.addTest(makeSuite(MockRawExtEepromTestCase, 'test')) - suite_.addTest(makeSuite(MockRawIntEepromTestCase, 'test')) - suite_.addTest(makeSuite(MockCBusEepromTestCase, 'test')) - suite_.addTest(makeSuite(MockCbusGpioTestCase, 'test')) + suite_.addTest(TestLoader().loadTestsFromModule(modules[__name__])) return suite_ diff --git a/pyftdi/tests/spi.py b/pyftdi/tests/spi.py index cfb1289b..dad6e3fd 100755 --- a/pyftdi/tests/spi.py +++ b/pyftdi/tests/spi.py @@ -10,7 +10,7 @@ # pylint: disable=missing-docstring import logging -import unittest +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from binascii import hexlify from doctest import testmod from os import environ @@ -101,7 +101,7 @@ def close(self): self._spi.terminate() -class SpiTestCase(unittest.TestCase): +class SpiTestCase(TestCase): """FTDI SPI driver test case Simple test to demonstrate SPI feature. @@ -143,7 +143,7 @@ def _test_spi3(self): spi.close() -class SpiGpioTestCase(unittest.TestCase): +class SpiGpioTestCase(TestCase): """Basic test for GPIO access w/ SPI mode It expects the following I/O setup: @@ -217,7 +217,7 @@ def ad_to_ac(ad_output): self.assertRaises(SpiIOError, self._io.write, ac_pins) -class SpiUnalignedTestCase(unittest.TestCase): +class SpiUnalignedTestCase(TestCase): """Basic test for SPI with non 8-bit multiple transfer It expects the following I/O setup: @@ -311,7 +311,7 @@ def test_bytebit_duplex(self): self.assertEqual(data[-1], exp) -class SpiCsForceTestCase(unittest.TestCase): +class SpiCsForceTestCase(TestCase): """Basic test for exercing direct /CS control. It requires a scope or a digital analyzer to validate the signal @@ -362,11 +362,14 @@ def test_cs_default_pulse_rev_clock(self): def suite(): - suite_ = unittest.TestSuite() - # suite_.addTest(unittest.makeSuite(SpiTestCase, 'test')) - # suite_.addTest(unittest.makeSuite(SpiGpioTestCase, 'test')) - suite_.addTest(unittest.makeSuite(SpiUnalignedTestCase, 'test')) - suite_.addTest(unittest.makeSuite(SpiCsForceTestCase, 'test')) + suite_ = TestSuite() + loader = TestLoader() + mod = modules[__name__] + tests = ( # 'Spi', + 'SpiGpio', 'SpiUnaligned', 'SpiCsForce') + for testname in tests: + testcase = getattr(mod, f'{testname}TestCase') + suite_.addTest(loader.loadTestsFromTestCase(testcase)) return suite_ @@ -379,7 +382,7 @@ def main(): except AttributeError as exc: raise ValueError(f'Invalid log level: {level}') from exc FtdiLogger.set_level(loglevel) - unittest.main(defaultTest='suite') + ut_main(defaultTest='suite') if __name__ == '__main__': From 5f0f7c3042a29c396309af79c4c3d83fdc1869aa Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 6 Apr 2024 18:55:24 +0200 Subject: [PATCH 39/62] .gitlab: replace actions stuff with other actions stuff whatever Signed-off-by: Emmanuel Blot --- .github/workflows/pythonchecksyntax.yml | 4 ++-- .github/workflows/pythonmocktests.yml | 4 ++-- .github/workflows/pythonpackage.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index f405f341..46621e22 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -15,9 +15,9 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/pythonmocktests.yml b/.github/workflows/pythonmocktests.yml index 6b82b114..bcd764d4 100644 --- a/.github/workflows/pythonmocktests.yml +++ b/.github/workflows/pythonmocktests.yml @@ -14,10 +14,10 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 747368aa..3fc8e812 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -14,9 +14,9 @@ jobs: python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 58d60a8cdd88328a223962cac3c263b0fb79e6b5 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 6 Apr 2024 21:11:29 +0200 Subject: [PATCH 40/62] update doc date --- pyftdi/doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/doc/conf.py b/pyftdi/doc/conf.py index 1b2ae32c..4fd5655a 100644 --- a/pyftdi/doc/conf.py +++ b/pyftdi/doc/conf.py @@ -52,7 +52,7 @@ def find_meta(meta): master_doc = 'index' project = find_meta('title') contact = '%s <%s>' % (find_meta('author'), find_meta('email')) -copyright = '2010-2020, %s' % contact +copyright = '2010-2024, %s' % contact show_authors = True html_theme = 'sphinx_rtd_theme' From c0fdffdd3690eaefd42864b8f6fe81c4096c2bde Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Wed, 10 Apr 2024 09:47:12 +0200 Subject: [PATCH 41/62] fix a regression introduced in 6b2c74945 Signed-off-by: Emmanuel Blot --- pyftdi/bin/pyterm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/bin/pyterm.py b/pyftdi/bin/pyterm.py index b8065faf..f7bc77f0 100755 --- a/pyftdi/bin/pyterm.py +++ b/pyftdi/bin/pyterm.py @@ -287,7 +287,7 @@ def main(): help=f'serial port device name ' f'(default: {default_device}') argparser.add_argument('-b', '--baudrate', - efault=str(MiniTerm.DEFAULT_BAUDRATE), + default=str(MiniTerm.DEFAULT_BAUDRATE), help=f'serial port baudrate ' f'(default: {MiniTerm.DEFAULT_BAUDRATE})') argparser.add_argument('-w', '--hwflow', From 327a1a75ad6e8cbebfabac1b8ad2bc9b4a92f29a Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Wed, 10 Apr 2024 09:53:49 +0200 Subject: [PATCH 42/62] bump version to v0.55.4 --- pyftdi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index fa945b71..ee5209b5 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -6,7 +6,7 @@ # pylint: disable=missing-docstring -__version__ = '0.55.3' +__version__ = '0.55.4' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' From 426e1abd88e25e4aa1c0520b171cae1713b9180e Mon Sep 17 00:00:00 2001 From: Mark Mentovai Date: Mon, 4 Nov 2024 14:38:36 -0500 Subject: [PATCH 43/62] tests: Fix for Python 3.13 (unittest.makeSuite was removed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` ImportError while importing test module '…/pyftdi/pyftdi/tests/toolsimport.py'. Hint: make sure your test modules/packages have valid Python names. Traceback: …/lib/python3.13/importlib/__init__.py:88: in import_module return _bootstrap._gcd_import(name[level:], package, level) pyftdi/tests/toolsimport.py:16: in from unittest import TestCase, TestSuite, makeSuite, main as ut_main E ImportError: cannot import name 'makeSuite' from 'unittest' (…/lib/python3.13/unittest/__init__.py) ``` https://github.com/python/cpython/issues/104835 https://github.com/python/cpython/pull/104836 https://github.com/python/cpython/commit/b1cb30ec8639e4e65f62e8f6cd44e979640431c8 https://docs.python.org/3/whatsnew/3.13.html#unittest --- pyftdi/tests/toolsimport.py | 4 ++-- pyftdi/tests/uart.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyftdi/tests/toolsimport.py b/pyftdi/tests/toolsimport.py index a12241ae..96bdcd3b 100755 --- a/pyftdi/tests/toolsimport.py +++ b/pyftdi/tests/toolsimport.py @@ -13,7 +13,7 @@ from importlib import import_module from os.path import dirname, join as joinpath from sys import modules, path as syspath -from unittest import TestCase, TestSuite, makeSuite, main as ut_main +from unittest import TestCase, TestLoader, TestSuite, main as ut_main class ToolsTestCase(TestCase): @@ -57,7 +57,7 @@ def test_ftdi_urls(self): def suite(): suite_ = TestSuite() - suite_.addTest(makeSuite(ToolsTestCase, 'test')) + suite_.addTest(TestLoader().loadTestsFromModule(modules[__name__])) return suite_ diff --git a/pyftdi/tests/uart.py b/pyftdi/tests/uart.py index 333e3f82..d54f9590 100755 --- a/pyftdi/tests/uart.py +++ b/pyftdi/tests/uart.py @@ -18,7 +18,7 @@ from sys import modules, platform, stdout from time import sleep, time as now from threading import Thread -from unittest import TestCase, TestSuite, skipIf, makeSuite, main as ut_main +from unittest import TestCase, TestLoader, TestSuite, skipIf, main as ut_main from pyftdi import FtdiLogger from pyftdi.ftdi import Ftdi from pyftdi.misc import to_bool @@ -343,8 +343,7 @@ def test(self): def suite(): suite_ = TestSuite() - suite_.addTest(makeSuite(BaudrateTestCase, 'test')) - suite_.addTest(makeSuite(UartTestCase, 'test')) + suite_.addTest(TestLoader().loadTestsFromModule(modules[__name__])) return suite_ From acad347aa9af879a4a79e1d10b60f9a15ce3bfde Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 19 Nov 2024 18:51:17 +0100 Subject: [PATCH 44/62] pylint: address several issues that were not previously reported. Signed-off-by: Emmanuel Blot --- .pylintrc | 1 + pyftdi/ftdi.py | 5 +++-- pyftdi/i2c.py | 23 +++++++++++++---------- pyftdi/usbtools.py | 1 + 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.pylintrc b/.pylintrc index 05d31e49..f37dae20 100644 --- a/.pylintrc +++ b/.pylintrc @@ -12,6 +12,7 @@ disable= too-many-lines, too-many-locals, too-many-nested-blocks, + too-many-positional-arguments, too-many-public-methods, too-many-return-statements, too-many-statements, diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index 26e1da6a..891546e2 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.py @@ -1781,8 +1781,9 @@ def write_data(self, data: Union[bytes, bytearray]) -> int: raise FtdiError(f'UsbError: {exc}') from exc def read_data_bytes(self, size: int, attempt: int = 1, - request_gen: Optional[Callable[[int], bytes]] = None) \ - -> bytes: + request_gen: Optional[Callable[[int], + Union[bytes, bytearray]]] = None) \ + -> bytearray: """Read data from the FTDI interface In UART mode, data contains the serial stream read from the UART diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index 7231dea5..b758b53b 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -653,7 +653,7 @@ def width(self) -> int: """ return 16 if self._wide_port else 8 - def read(self, address: int, readlen: int = 1, + def read(self, address: Optional[int], readlen: int = 1, relax: bool = True) -> bytes: """Read one or more bytes from a remote slave @@ -695,7 +695,8 @@ def read(self, address: int, readlen: int = 1, if do_epilog: self._do_epilog() - def write(self, address: int, out: Union[bytes, bytearray, Iterable[int]], + def write(self, address: Optional[int], + out: Union[bytes, bytearray, Iterable[int]], relax: bool = True) -> None: """Write one or more bytes to a remote slave @@ -735,9 +736,9 @@ def write(self, address: int, out: Union[bytes, bytearray, Iterable[int]], if do_epilog: self._do_epilog() - def exchange(self, address: int, + def exchange(self, address: Optional[int], out: Union[bytes, bytearray, Iterable[int]], - readlen: int = 0, relax: bool = True) -> bytes: + readlen: int = 0, relax: bool = True) -> bytearray: """Send a byte sequence to a remote slave followed with a read request of one or more bytes. @@ -767,12 +768,14 @@ def exchange(self, address: int, i2caddress = (address << 1) & self.HIGH retries = self._retry_count do_epilog = True + data = bytearray() with self._lock: while True: try: self._do_prolog(i2caddress) self._do_write(out) - self._do_prolog(i2caddress | self.BIT0) + if i2caddress is not None: + self._do_prolog(i2caddress | self.BIT0) if readlen: data = self._do_read(readlen) do_epilog = relax @@ -1017,7 +1020,7 @@ def _write_raw(self, data: int, write_high: bool): cmd = bytes([Ftdi.SET_BITS_LOW, low_data, low_dir]) self._ftdi.write_data(cmd) - def _do_prolog(self, i2caddress: int) -> None: + def _do_prolog(self, i2caddress: Optional[int]) -> None: if i2caddress is None: return self.log.debug(' prolog 0x%x', i2caddress >> 1) @@ -1061,7 +1064,7 @@ def _send_check_ack(self, cmd: bytearray): if ack[0] & self.BIT0: raise I2cNackError('NACK from slave') - def _do_read(self, readlen: int) -> bytes: + def _do_read(self, readlen: int) -> bytearray: self.log.debug('- read %d byte(s)', readlen) if not readlen: # force a real read request on device, but discard any result @@ -1101,10 +1104,10 @@ def _do_read(self, readlen: int) -> bytes: cmd_chunk.extend(read_not_last * chunk_size) cmd_chunk.extend(self._immediate) - def write_command_gen(length: int): + def write_command_gen(length: int) -> bytearray: if length <= 0: # no more data - return b'' + return bytearray() if length <= chunk_size: cmd = bytearray() cmd.extend(read_not_last * (length-1)) @@ -1120,6 +1123,7 @@ def write_command_gen(length: int): rem -= len(buf) else: while rem: + size = rem if rem > chunk_size: if not cmd: # build the command sequence only once, as it may be @@ -1132,7 +1136,6 @@ def write_command_gen(length: int): cmd.extend(read_not_last * (rem-1)) cmd.extend(read_last) cmd.extend(self._immediate) - size = rem self._ftdi.write_data(cmd) buf = self._ftdi.read_data_bytes(size, 4) self.log.debug('- read %d byte(s): %s', diff --git a/pyftdi/usbtools.py b/pyftdi/usbtools.py index b8a593fd..15b8bd61 100644 --- a/pyftdi/usbtools.py +++ b/pyftdi/usbtools.py @@ -291,6 +291,7 @@ def parse_url(cls, urlstr: str, scheme: str, path = urlparts.path.strip('/') if path == '?' or (not path and urlstr.endswith('?')): report_devices = True + interface = -1 else: interface = to_int(path) report_devices = False From 3c78976d9cd0723d559f0687f4b8a9845984ea43 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 19 Nov 2024 18:50:43 +0100 Subject: [PATCH 45/62] python: deprecated EOL'ed v3.8, add v3.13 Signed-off-by: Emmanuel Blot --- .github/workflows/pythonchecksyntax.yml | 2 +- .github/workflows/pythonmocktests.yml | 2 +- .github/workflows/pythonpackage.yml | 2 +- README.md | 2 +- pyftdi/__init__.py | 2 +- pyftdi/doc/requirements.rst | 5 +++-- pyftdi/tests/toolsimport.py | 2 +- setup.py | 4 ++-- 8 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 46621e22..7dff3c30 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pythonmocktests.yml b/.github/workflows/pythonmocktests.yml index bcd764d4..b213d447 100644 --- a/.github/workflows/pythonmocktests.yml +++ b/.github/workflows/pythonmocktests.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 3fc8e812..6309f6cf 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index abc38fab..86c1ebc4 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,6 @@ PyFtdi currently supports the following features: ### Python support -PyFtdi requires Python 3.8+. +PyFtdi requires Python 3.9+. See `pyftdi/doc/requirements.rst` for more details. diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index ee5209b5..3c2620d3 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -6,7 +6,7 @@ # pylint: disable=missing-docstring -__version__ = '0.55.4' +__version__ = '0.56.0' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' diff --git a/pyftdi/doc/requirements.rst b/pyftdi/doc/requirements.rst index 0b0f3a51..211acf58 100644 --- a/pyftdi/doc/requirements.rst +++ b/pyftdi/doc/requirements.rst @@ -3,9 +3,10 @@ Requirements ------------ -Python_ 3.8 or above is required. +Python_ 3.9 or above is required. + +* PyFtdi *v0.55* is the last PyFtdi version to support Python 3.8. -* PyFtdi *v0.54* is the last PyFtdi version to support Python 3.7. * Python 3.7 has reached end-of-life on June 27rd, 2023. diff --git a/pyftdi/tests/toolsimport.py b/pyftdi/tests/toolsimport.py index 96bdcd3b..0c5fa4b5 100755 --- a/pyftdi/tests/toolsimport.py +++ b/pyftdi/tests/toolsimport.py @@ -22,7 +22,7 @@ class ToolsTestCase(TestCase): This is especially useful to find Python syntax version mismatch and other not-yet-supported modules/features. - PyFtdi and tools should support Python 3.8 onwards. + PyFtdi and tools should support Python 3.9 onwards. """ @classmethod diff --git a/setup.py b/setup.py index c83d2086..8da347a2 100755 --- a/setup.py +++ b/setup.py @@ -35,11 +35,11 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Hardware :: Hardware Drivers', ] @@ -182,7 +182,7 @@ def main(): 'pyftdi.serialext': ['*.rst', 'doc/api/uart.rst']}, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, - python_requires='>=3.8', + python_requires='>=3.9', ) From bab0f5fffcb1a35d56aae810ce3235f4090ff836 Mon Sep 17 00:00:00 2001 From: Sjoerd Simons Date: Sat, 2 Nov 2024 16:01:52 +0100 Subject: [PATCH 46/62] eeprom: Fall back to device version of type not set in eeprom When the type isn't set in the eeprom determine it from the device version instead. --- pyftdi/eeprom.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyftdi/eeprom.py b/pyftdi/eeprom.py index 7e692504..a52d347a 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -942,7 +942,10 @@ def _decode_eeprom(self): return name = None try: - name = Ftdi.DEVICE_NAMES[cfg['type']].replace('-', '') + type_ = cfg['type'] + if type_ == 0: + type_ = self.device_version + name = Ftdi.DEVICE_NAMES[type_].replace('-', '') if name.startswith('ft'): name = name[2:] func = getattr(self, f'_decode_{name}') From 56e6afb8277f16d90b5e4b88ac8a462b91949682 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Tue, 19 Nov 2024 19:27:58 +0100 Subject: [PATCH 47/62] update contributors Signed-off-by: Emmanuel Blot --- pyftdi/doc/authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/pyftdi/doc/authors.rst b/pyftdi/doc/authors.rst index 00f3da7c..287909c4 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -52,3 +52,4 @@ Contributors * Roman Dobrodii * Mark Mentovai * Alessandro Zini + * Sjoerd Simons From 0cc79096d1eba8ed0b3b257755e416b7c489cc7d Mon Sep 17 00:00:00 2001 From: Shareef Jalloq Date: Wed, 20 Nov 2024 21:13:41 +0000 Subject: [PATCH 48/62] bugfix(eeprom): make sure to close eeprom #394 This commit fixes a bug in ftconf.py where the eeprom device was not closed before exiting. This meant the kernel driver was not re-attached causing device disconnection. --- pyftdi/bin/ftconf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyftdi/bin/ftconf.py b/pyftdi/bin/ftconf.py index 1fb36a7d..65a41092 100755 --- a/pyftdi/bin/ftconf.py +++ b/pyftdi/bin/ftconf.py @@ -186,6 +186,8 @@ def main(): sys_exit(1) except KeyboardInterrupt: sys_exit(2) + finally: + eeprom.close() if __name__ == '__main__': From 075a73450b60791e63666fdc5e9652db0d240e00 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 23 Nov 2024 10:29:18 +0100 Subject: [PATCH 49/62] .github: improve readability Signed-off-by: Emmanuel Blot --- .github/workflows/pythonchecksyntax.yml | 9 +++++++-- .github/workflows/pythonpackage.yml | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 7dff3c30..853415d8 100644 --- a/.github/workflows/pythonchecksyntax.yml +++ b/.github/workflows/pythonchecksyntax.yml @@ -16,26 +16,31 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install -r test-requirements.txt + - name: Check style run: | python setup.py check_style + - name: Linter run: | pylint --disable=fixme --disable=duplicate-code \ - pyftdi pyftdi/bin pyftdi/serialext \ - pyftdi/tests pyftdi/serialext/tests pyftdi/tests/backend + $(git ls-files '*.py') + - name: Install package run: | python setup.py install + - name: Run tests run: | python pyftdi/tests/toolsimport.py diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 6309f6cf..874cd7da 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -15,19 +15,23 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt pip install setuptools wheel sphinx sphinx_rtd_theme sphinx_autodoc_typehints + - name: Build package run: | python setup.py bdist python setup.py sdist bdist_wheel + - name: Build documentation run: | mkdir doc From 03c33be1802e8a95619cadb11e2b46583cf24a99 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sat, 23 Nov 2024 22:54:36 +0100 Subject: [PATCH 50/62] .github: create PyPI workflow Signed-off-by: Emmanuel Blot --- .github/workflows/pypi.yml | 47 ++++++++++++++++++++++++++++++++++++++ setup.py | 1 + 2 files changed, 48 insertions(+) create mode 100644 .github/workflows/pypi.yml diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 00000000..79e0714a --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,47 @@ +name: Publish to PyPI + +on: + release: + types: + - published + workflow_dispatch: + inputs: + twine_verbose: + description: 'Enable Twine verbose mode' + required: true + type: boolean + +jobs: + pypi-publish: + name: upload release to PyPI + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/pyftdi + permissions: + id-token: write + strategy: + matrix: + python-version: ['3.13'] + steps: + + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel + + - name: Build package + run: | + python setup.py bdist_wheel + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + verbose: ${{ inputs.twine_verbose }} diff --git a/setup.py b/setup.py index 8da347a2..c9be82f9 100755 --- a/setup.py +++ b/setup.py @@ -171,6 +171,7 @@ def main(): maintainer_email=find_meta('email'), keywords=KEYWORDS, long_description=read_desc('pyftdi/doc/index.rst'), + long_description_content_type='text/x-rst', packages=PACKAGES, scripts=['pyftdi/bin/i2cscan.py', 'pyftdi/bin/ftdi_urls.py', From 353c723caa00de45ef5f7265bd6a30e665ec4d51 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Wed, 1 May 2024 17:41:32 +0200 Subject: [PATCH 51/62] pylint: configure Signed-off-by: Emmanuel Blot --- pyftdi/doc/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyftdi/doc/conf.py b/pyftdi/doc/conf.py index 4fd5655a..191cbd9f 100644 --- a/pyftdi/doc/conf.py +++ b/pyftdi/doc/conf.py @@ -1,8 +1,10 @@ -# Copyright (c) 2010-2021 Emmanuel Blot +# Copyright (c) 2010-2024 Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause +# pylint: skip-file + import os import re import sys From 556ce51ba4961aaec9424f55ae8f27439ce86188 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Sun, 24 Nov 2024 00:42:02 +0100 Subject: [PATCH 52/62] doc: update requirements and dependencies Signed-off-by: Emmanuel Blot --- pyftdi/doc/defs.rst | 3 ++- pyftdi/doc/features.rst | 3 +++ pyftdi/doc/requirements.rst | 40 ++++++++++++++++++++++--------------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/pyftdi/doc/defs.rst b/pyftdi/doc/defs.rst index 920a52da..aea1d2df 100644 --- a/pyftdi/doc/defs.rst +++ b/pyftdi/doc/defs.rst @@ -10,6 +10,7 @@ .. _FTDI_Recovery: https://www.ftdichip.com/Support/Documents/AppNotes/AN_136%20Hi%20Speed%20Mini%20Module%20EEPROM%20Disaster%20Recovery.pdf .. _PyFtdi: https://www.github.com/eblot/pyftdi .. _PyFtdiTools: https://github.com/eblot/pyftdi/tree/master/pyftdi/bin +.. _PyJtagTools: https://www.github.com/eblot/pyjtagtools .. _FTDI: https://www.ftdichip.com/ .. _PyUSB: https://pyusb.github.io/pyusb/ .. _Python: https://www.python.org/ @@ -29,7 +30,7 @@ .. _PEP_498: https://www.python.org/dev/peps/pep-0498 .. _PEP_526: https://www.python.org/dev/peps/pep-0526 .. _ruamel.yaml: https://pypi.org/project/ruamel.yaml - +.. _PyFtdiWin: https://github.com/mariusgreuel/pyftdiwin .. Restructured Text levels diff --git a/pyftdi/doc/features.rst b/pyftdi/doc/features.rst index 8080823b..05ded63f 100644 --- a/pyftdi/doc/features.rst +++ b/pyftdi/doc/features.rst @@ -84,6 +84,9 @@ JTAG API is limited to low-level access. It is not intented to be used for any flashing or debugging purpose, but may be used as a base to perform SoC tests and boundary scans. +It requires the PyJtagTools_ Python module which integrates a JTAG engine, while +PyFtdi_ implements the FTDI JTAG backend. + EEPROM `````` diff --git a/pyftdi/doc/requirements.rst b/pyftdi/doc/requirements.rst index 211acf58..8a4c3117 100644 --- a/pyftdi/doc/requirements.rst +++ b/pyftdi/doc/requirements.rst @@ -5,28 +5,15 @@ Requirements Python_ 3.9 or above is required. -* PyFtdi *v0.55* is the last PyFtdi version to support Python 3.8. - - - * Python 3.7 has reached end-of-life on June 27rd, 2023. - -* PyFtdi *v0.53* is the last PyFtdi version to support Python 3.6. - - * Python 3.6 has reached end-of-life on December 23rd, 2021. - -* PyFtdi *v0.52* is the last PyFtdi version to support Python 3.5. - - * Python 3.5 has reached end-of-life on September 5th, 2020. - PyFtdi_ relies on PyUSB_, which itself depends on one of the following native libraries: * libusb_, currently tested with 1.0.23 -PyFtdi_ does not depend on any other native library, and only uses standard -Python modules along with PyUSB_ and pyserial_. +PyFtdi_ does not depend on any other native library. It only uses standard +Python modules, and PyUSB_, pyserial_ and PyJtagTools_. -PyFtdi_ is beeing tested with PyUSB_ 1.1.0. +PyFtdi_ is being tested with PyUSB_ 1.2.1. Development ~~~~~~~~~~~ @@ -39,6 +26,8 @@ supported platforms. However, M$ Windows is a seamless source of issues and is not officially supported, although users have reported successful installation with Windows 7 for example. Your mileage may vary. +A fork of PyFtdi which relies on the official FTDI D2XX Windows library might be +a better solution for Windows users, please check out PyFtdiWin_. API breaks ~~~~~~~~~~ @@ -52,3 +41,22 @@ new PyFtdi releases. PyFtdi versions up to *v0.39.9* keep a stable API with *v0.22+* series. See the *Major Changes* section for details about potential API breaks. + +Legacy Python support +~~~~~~~~~~~~~~~~~~~~~ + +* PyFtdi *v0.55* is the last PyFtdi version to support Python 3.8. + + * Python 3.8 has reached end-of-life on October 7th, 2024. + +* PyFtdi *v0.54* is the last PyFtdi version to support Python 3.7. + + * Python 3.7 has reached end-of-life on June 27rd, 2023. + +* PyFtdi *v0.53* is the last PyFtdi version to support Python 3.6. + + * Python 3.6 has reached end-of-life on December 23rd, 2021. + +* PyFtdi *v0.52* is the last PyFtdi version to support Python 3.5. + + * Python 3.5 has reached end-of-life on September 5th, 2020. From 8d9a85c907bc89f2a2c5bb3b9e28bdd443d075a7 Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Mon, 24 Mar 2025 11:01:23 +0000 Subject: [PATCH 53/62] Correct mistake in SPI API documentation example Correct a small mistake in the SPI API documentation. Make the set_direction call clearer to readers by adding the parameter names to the function call, and using binary so it is clear the bits correspond to the pin position. --- pyftdi/doc/api/spi.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyftdi/doc/api/spi.rst b/pyftdi/doc/api/spi.rst index 3e40306c..9e2f377c 100644 --- a/pyftdi/doc/api/spi.rst +++ b/pyftdi/doc/api/spi.rst @@ -58,9 +58,9 @@ Example: communication with a SPI device and an extra GPIO # Get a SPI port to a SPI slave w/ /CS on A*BUS3 and SPI mode 0 @ 12MHz slave = spi.get_port(cs=0, freq=12E6, mode=0) - # Get GPIO port to manage extra pins, use A*BUS4 as GPO, A*BUS4 as GPI + # Get GPIO port to manage extra pins, use A*BUS4 as GPO, A*BUS5 as GPI gpio = spi.get_gpio() - gpio.set_direction(0x30, 0x10) + gpio.set_direction(pins=0b0011_0000, direction=0b0001_0000) # Assert GPO pin gpio.write(0x10) From fa849131fa91df93ef2e95134fea151a6c29cc13 Mon Sep 17 00:00:00 2001 From: David Schneider Date: Fri, 2 May 2025 14:37:14 -0700 Subject: [PATCH 54/62] i2c: fix reading >116 bytes with the non-optimized read path --- pyftdi/i2c.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index b758b53b..0ac66a0f 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -1130,7 +1130,7 @@ def write_command_gen(length: int) -> bytearray: # repeated till the end of the transfer cmd = bytearray() cmd.extend(read_not_last * chunk_size) - size = chunk_size + size = chunk_size else: cmd = bytearray() cmd.extend(read_not_last * (rem-1)) From 1c5176bcb3b8e42c51b5a3e188e6d4904a72fd0f Mon Sep 17 00:00:00 2001 From: David Schneider Date: Wed, 7 May 2025 10:35:16 -0700 Subject: [PATCH 55/62] i2c: glitch high rather than low when tristating SCL+SDA Avoids accidential start conditions, fixes #405 As a side note, _clk_lo_data_input does generate a negative glitch on SDA (if it was high) before going high-impedance. This behavior is preferable over the alternative (glitching high before going high-impedance), since the device might already be driving low on the bus to provide an ACK and we don't want brief crowbar currents. --- pyftdi/i2c.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index b758b53b..589a339a 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -962,7 +962,7 @@ def _clk_lo_data_lo(self) -> Tuple[int]: @property def _clk_input_data_input(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, - self._gpio_low, + self.I2C_DIR | self._gpio_low, (self._gpio_dir & 0xFF)) @property From c692a47773c4cb46772a5a9814beb6ce7f395797 Mon Sep 17 00:00:00 2001 From: David Schneider Date: Wed, 7 May 2025 16:23:21 -0700 Subject: [PATCH 56/62] i2c: avoid getting stuck due to adaptive clocking When adaptive clocking is enabled, the MPSSE engine gets blocked waiting for the clock feedback signal to become high. This has a lot of side effects, breaking GPIO accesses, preventing further configuration commands from executing (including both 3-phase clocking as well as disabling adaptive clocking), and confusing the pyftdi read buffer. The only way to recover seems to be purge the RX buffer, at which point the FTDI will process a "disable adaptive clocking" command. To avoid getting caught in this condition, only enable adaptive clocking at the start of an I2C transaction and disable it at the end. This helps avoid any additional USB round-trips. If an I2C-related read fails to get any data (mainly when trying to read the ACK bit), assume the bus is wedged, and apply the workaround to ensure adaptive clocking is off until the next attempted I2C transaction. Tested on an FT4232H. If the DUT is unpowered, GPIO get/set will work normally, and I2C transactions raise I2cIOError/"No answer from FTDI" (unchanged). Once the DUT becomes powered, I2C transactions immediately work correctly. --- pyftdi/i2c.py | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index 0ac66a0f..15a1f60d 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -390,6 +390,7 @@ def __init__(self): self._ck_idle = 0 self._read_optim = True self._disable_3phase_clock = False + self._clkstrch = False def set_retry_count(self, count: int) -> None: """Change the default retry count when a communication error occurs, @@ -438,10 +439,10 @@ def configure(self, url: Union[str, UsbDevice], else: timings = self.I2C_1M if 'clockstretching' in kwargs: - clkstrch = bool(kwargs['clockstretching']) + self._clkstrch = bool(kwargs['clockstretching']) del kwargs['clockstretching'] else: - clkstrch = False + self._clkstrch = False if 'direction' in kwargs: io_dir = int(kwargs['direction']) del kwargs['direction'] @@ -469,7 +470,7 @@ def configure(self, url: Union[str, UsbDevice], ck_buf = self._compute_delay_cycles(timings.t_buf) self._ck_idle = max(ck_su_sta, ck_buf) self._ck_delay = ck_buf - if clkstrch: + if self._clkstrch: self._i2c_mask = self.I2C_MASK_CS else: self._i2c_mask = self.I2C_MASK @@ -489,7 +490,6 @@ def configure(self, url: Union[str, UsbDevice], frequency = self._ftdi.open_mpsse_from_url(url, **kwargs) self._frequency = (2.0*frequency)/3.0 self._tx_size, self._rx_size = self._ftdi.fifo_sizes - self._ftdi.enable_adaptive_clock(clkstrch) if not self._disable_3phase_clock: self._ftdi.enable_3phase_clock(True) try: @@ -880,7 +880,7 @@ def flush(self) -> None: if not self.configured: raise I2cIOError("FTDI controller not initialized") with self._lock: - self._ftdi.write_data(self._immediate) + self._ftdi.write_data(bytearray(self._immediate)) self._ftdi.purge_buffers() def read_gpio(self, with_output: bool = False) -> int: @@ -1057,21 +1057,37 @@ def _send_check_ack(self, cmd: bytearray): # read SDA (ack from slave) cmd.extend(self._read_bit) cmd.extend(self._immediate) - self._ftdi.write_data(cmd) - ack = self._ftdi.read_data_bytes(1, 4) + self._i2c_write_data(cmd) + ack = self._i2c_read_data_bytes(1, 4) if not ack: raise I2cIOError('No answer from FTDI') if ack[0] & self.BIT0: raise I2cNackError('NACK from slave') + def _i2c_write_data(self, cmd: bytearray): + if self._clkstrch: + cmd.insert(0, self._ftdi.ENABLE_CLK_ADAPTIVE) + cmd.append(self._ftdi.DISABLE_CLK_ADAPTIVE) + self._ftdi.write_data(cmd) + + def _i2c_read_data_bytes(self, readlen: int, attempt: int = 1, + request_gen=None) -> bytearray: + data = self._ftdi.read_data_bytes(readlen, attempt, request_gen) + if not data and self._clkstrch: + self.log.warning('bus seems wedged') + self._ftdi.purge_rx_buffer() + self._ftdi.write_data( + bytearray((self._ftdi.DISABLE_CLK_ADAPTIVE,))) + return data + def _do_read(self, readlen: int) -> bytearray: self.log.debug('- read %d byte(s)', readlen) if not readlen: # force a real read request on device, but discard any result cmd = bytearray() cmd.extend(self._immediate) - self._ftdi.write_data(cmd) - self._ftdi.read_data_bytes(0, 4) + self._i2c_write_data(cmd) + self._i2c_read_data_bytes(0, 4) return bytearray() if self._fake_tristate: read_byte = (self._clk_lo_data_input + @@ -1117,7 +1133,7 @@ def write_command_gen(length: int) -> bytearray: return cmd_chunk while rem: - buf = self._ftdi.read_data_bytes(rem, 4, write_command_gen) + buf = self._i2c_read_data_bytes(rem, 4, write_command_gen) self.log.debug('- read %d bytes, rem: %d', len(buf), rem) chunks.append(buf) rem -= len(buf) @@ -1136,8 +1152,8 @@ def write_command_gen(length: int) -> bytearray: cmd.extend(read_not_last * (rem-1)) cmd.extend(read_last) cmd.extend(self._immediate) - self._ftdi.write_data(cmd) - buf = self._ftdi.read_data_bytes(size, 4) + self._i2c_write_data(cmd) + buf = self._i2c_read_data_bytes(size, 4) self.log.debug('- read %d byte(s): %s', len(buf), hexlify(buf).decode()) chunks.append(buf) From 6ff4b5cfc9e1189a1f7c3334dc959cf99d61835f Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 14 Aug 2025 17:01:43 +0200 Subject: [PATCH 57/62] doc: update authors Signed-off-by: Emmanuel Blot --- pyftdi/doc/authors.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/pyftdi/doc/authors.rst b/pyftdi/doc/authors.rst index 287909c4..fcf9e259 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -53,3 +53,4 @@ Contributors * Mark Mentovai * Alessandro Zini * Sjoerd Simons + * David Schneider From 129c5cd02b1f339558a74e21cdf05b135d789431 Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 14 Aug 2025 17:01:59 +0200 Subject: [PATCH 58/62] version: update to 0.57.0 --- LICENSE | 2 +- pyftdi/__init__.py | 8 ++++---- pyftdi/doc/conf.py | 2 +- pyftdi/doc/license.rst | 2 +- pyftdi/i2c.py | 2 +- setup.cfg | 3 +++ setup.py | 3 +-- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/LICENSE b/LICENSE index b262ab34..43c41ad7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2008-2024 Emmanuel Blot +Copyright (c) 2008-2025 Emmanuel Blot All Rights Reserved. SPDX-License-Identifier: BSD-3-Clause diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 3c2620d3..1e7682ba 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2024 Emmanuel Blot +# Copyright (c) 2010-2025 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # @@ -6,7 +6,7 @@ # pylint: disable=missing-docstring -__version__ = '0.56.0' +__version__ = '0.57.0' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' @@ -14,8 +14,8 @@ __author__ = 'Emmanuel Blot' # For all support requests, please open a new issue on GitHub __email__ = 'emmanuel.blot@free.fr' -__license__ = 'Modified BSD' -__copyright__ = 'Copyright (c) 2011-2024 Emmanuel Blot' +__license__ = 'BSD-3-Clause' +__copyright__ = 'Copyright (c) 2011-2025 Emmanuel Blot' from logging import WARNING, NullHandler, getLogger diff --git a/pyftdi/doc/conf.py b/pyftdi/doc/conf.py index 191cbd9f..1f42769d 100644 --- a/pyftdi/doc/conf.py +++ b/pyftdi/doc/conf.py @@ -54,7 +54,7 @@ def find_meta(meta): master_doc = 'index' project = find_meta('title') contact = '%s <%s>' % (find_meta('author'), find_meta('email')) -copyright = '2010-2024, %s' % contact +copyright = '2010-2025, %s' % contact show_authors = True html_theme = 'sphinx_rtd_theme' diff --git a/pyftdi/doc/license.rst b/pyftdi/doc/license.rst index 937235f7..fbfd4104 100644 --- a/pyftdi/doc/license.rst +++ b/pyftdi/doc/license.rst @@ -18,7 +18,7 @@ BSD 3-clause :: - Copyright (c) 2008-2021 Emmanuel Blot + Copyright (c) 2008-2025 Emmanuel Blot All Rights Reserved. Redistribution and use in source and binary forms, with or without diff --git a/pyftdi/i2c.py b/pyftdi/i2c.py index 15a1f60d..53086494 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017-2024, Emmanuel Blot +# Copyright (c) 2017-2025, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause diff --git a/setup.cfg b/setup.cfg index 28e40d98..da31f73b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,9 @@ [metadata] license_files = pyftdi/doc/license.rst +[project] +license = "BSD-3-Clause" + [build_sphinx] source-dir = pyftdi/doc build-dir = sphinx diff --git a/setup.py b/setup.py index c9be82f9..e8de6e2a 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# Copyright (c) 2010-2024 Emmanuel Blot +# Copyright (c) 2010-2025 Emmanuel Blot # Copyright (c) 2010-2016 Neotion # All rights reserved. # @@ -32,7 +32,6 @@ 'Environment :: Other Environment', 'Natural Language :: English', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', 'Programming Language :: Python :: 3.9', From 998583bdde59ec98d4dec67f141ec9992076188e Mon Sep 17 00:00:00 2001 From: Emmanuel Blot Date: Thu, 14 Aug 2025 17:57:18 +0200 Subject: [PATCH 59/62] version: update to 0.57.1 Signed-off-by: Emmanuel Blot --- pyftdi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 1e7682ba..657b40d7 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -6,7 +6,7 @@ # pylint: disable=missing-docstring -__version__ = '0.57.0' +__version__ = '0.57.1' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' From d1286fac81f0fd6fdec247cd961513a77078fef1 Mon Sep 17 00:00:00 2001 From: Jeff Plotzke Date: Mon, 30 Mar 2026 20:40:04 -0400 Subject: [PATCH 60/62] Fixing merge issues from original pyftdiwin --- pyftdi/doc/defs.rst | 2 -- pyftdi/usbtools.py | 4 ++-- setup.py | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyftdi/doc/defs.rst b/pyftdi/doc/defs.rst index 189be53b..3b200c83 100644 --- a/pyftdi/doc/defs.rst +++ b/pyftdi/doc/defs.rst @@ -19,8 +19,6 @@ .. _pyspiflash: https://github.com/eblot/pyspiflash/ .. _pyi2cflash: https://github.com/eblot/pyi2cflash/ .. _libusb: https://www.libusb.info/ -.. _Libusb on Windows: https://github.com/libusb/libusb/wiki/Windows -.. _Libusb win32: https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/ .. _FTDI macOS guide: https://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf .. _Libusb issue on macOs: https://github.com/libusb/libusb/commit/5e45e0741daee4fa295c6cc977edfb986c872152 .. _FT_PROG: https://www.ftdichip.com/Support/Utilities.htm#FT_PROG diff --git a/pyftdi/usbtools.py b/pyftdi/usbtools.py index 09cbead4..07307d95 100644 --- a/pyftdi/usbtools.py +++ b/pyftdi/usbtools.py @@ -405,8 +405,8 @@ def enumerate_candidates(cls, urlparts: SplitResult, vps.add((vid, products[pid])) devices = cls.find_all(vps) if sernum: - if sernum not in [dev.sn for dev, _ in devices]: - raise UsbToolsError(f'No USB device with S/N {sernum}') + if not [dev for dev, _ in devices if dev.sn and fnmatchcase(dev.sn, sernum)]: + raise UsbToolsError("No USB device with S/N '%s'" % sernum) for desc, ifcount in devices: if vendor and vendor != desc.vid: continue diff --git a/setup.py b/setup.py index 97be9da7..6f3b4392 100755 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ 'Intended Audience :: Developers', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', From 060123cef608586e0e3c1da136509ad380a0f619 Mon Sep 17 00:00:00 2001 From: Jeff Plotzke Date: Mon, 30 Mar 2026 20:46:44 -0400 Subject: [PATCH 61/62] Replace fstring function from upstream library --- pyftdi/usbtools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyftdi/usbtools.py b/pyftdi/usbtools.py index 07307d95..b4dde7c2 100644 --- a/pyftdi/usbtools.py +++ b/pyftdi/usbtools.py @@ -406,7 +406,7 @@ def enumerate_candidates(cls, urlparts: SplitResult, devices = cls.find_all(vps) if sernum: if not [dev for dev, _ in devices if dev.sn and fnmatchcase(dev.sn, sernum)]: - raise UsbToolsError("No USB device with S/N '%s'" % sernum) + raise UsbToolsError(f'No USB device with S/N {sernum}') for desc, ifcount in devices: if vendor and vendor != desc.vid: continue From a8fe6cb1563c1d7abf9b8b2e26e85b01e5e13626 Mon Sep 17 00:00:00 2001 From: Jeff Plotzke Date: Mon, 30 Mar 2026 20:50:41 -0400 Subject: [PATCH 62/62] Reducing function for lint test --- pyftdi/d2xx.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyftdi/d2xx.py b/pyftdi/d2xx.py index 23eb3f67..150f7b57 100644 --- a/pyftdi/d2xx.py +++ b/pyftdi/d2xx.py @@ -810,8 +810,7 @@ def bulk_read(self, dev_handle, ep, intf, buff, timeout): buff[0] = 0 buff[1] = 0 - if rx_bytes > len(buff) - 2: - rx_bytes = len(buff) - 2 + rx_bytes = min(rx_bytes, len(buff) - 2) c_buff = (c_ubyte * len(buff)).from_buffer(buff) bytes_returned = FT_Read(