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/.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/.github/workflows/pythonchecksyntax.yml b/.github/workflows/pythonchecksyntax.yml index 93e81e5f..853415d8 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: @@ -9,25 +12,35 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + 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 ruamel.yaml + pip install -r test-requirements.txt + - name: Check style run: | python setup.py check_style + + - name: Linter + run: | + pylint --disable=fixme --disable=duplicate-code \ + $(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/pythonmocktests.yml b/.github/workflows/pythonmocktests.yml index 7011d78f..b213d447 100644 --- a/.github/workflows/pythonmocktests.yml +++ b/.github/workflows/pythonmocktests.yml @@ -11,13 +11,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index c46c128e..874cd7da 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: @@ -8,25 +11,27 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + 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_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 python setup.py sdist bdist_wheel + - name: Build documentation run: | mkdir doc diff --git a/.gitignore b/.gitignore index 478fdb19..9ce75b7a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ build/ sphinx/ .vs/ .vscode/ +.venv/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..f37dae20 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,19 @@ +[MASTER] + +init-hook='import sys; sys.path.append(".")' + +[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-positional-arguments, + too-many-public-methods, + too-many-return-statements, + too-many-statements, + unspecified-encoding diff --git a/LICENSE b/LICENSE index ab67d8cf..43c41ad7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2008-2021 Emmanuel Blot +Copyright (c) 2008-2025 Emmanuel Blot All Rights Reserved. SPDX-License-Identifier: BSD-3-Clause diff --git a/README.md b/README.md index 39201c8b..0f595582 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # PyFtdiWin + ![Python package](https://github.com/mariusgreuel/pyftdiwin/workflows/Python%20package/badge.svg) ![Mock tests](https://github.com/mariusgreuel/pyftdiwin/workflows/Python%20mock%20tests/badge.svg) ![Syntax tests](https://github.com/mariusgreuel/pyftdiwin/workflows/Python%20syntax%20tests/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/pyftdiwin.svg?maxAge=2592000)](https://pypi.org/project/pyftdiwin/) @@ -44,6 +46,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 @@ -90,6 +93,6 @@ pip install pyftdiwin ### Python support -PyFtdi requires Python 3.7+. +PyFtdi requires Python 3.9+. See `pyftdi/doc/requirements.rst` for more details. diff --git a/pyftdi/__init__.py b/pyftdi/__init__.py index 7433e95e..657b40d7 100644 --- a/pyftdi/__init__.py +++ b/pyftdi/__init__.py @@ -1,12 +1,12 @@ -# Copyright (c) 2010-2022 Emmanuel Blot +# Copyright (c) 2010-2025 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause -#pylint: disable-msg=missing-docstring +# pylint: disable=missing-docstring -__version__ = '0.54.0' +__version__ = '0.57.1' __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-2021 Emmanuel Blot' +__license__ = 'BSD-3-Clause' +__copyright__ = 'Copyright (c) 2011-2025 Emmanuel Blot' from logging import WARNING, NullHandler, getLogger diff --git a/pyftdi/bin/ftconf.py b/pyftdi/bin/ftconf.py index cce9635e..65a41092 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 @@ -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(): @@ -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 @@ -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', ) @@ -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,14 @@ 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) + finally: + eeprom.close() if __name__ == '__main__': diff --git a/pyftdi/bin/ftdi_urls.py b/pyftdi/bin/ftdi_urls.py index 1297e602..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 @@ -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', ) @@ -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 3ef681b5..41f3dbcd 100755 --- a/pyftdi/bin/i2cscan.py +++ b/pyftdi/bin/i2cscan.py @@ -1,19 +1,18 @@ #!/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 -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 @@ -33,7 +32,8 @@ 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) @@ -74,12 +75,12 @@ def scan(cls, url: str, smb_mode: bool = True) -> None: 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 @@ -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 @@ -120,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', ) @@ -134,19 +137,19 @@ 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) + 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 73ad2eb4..f7bc77f0 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 @@ -24,14 +18,14 @@ 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 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 @@ -63,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 @@ -115,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() @@ -138,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() @@ -188,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: @@ -228,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: @@ -249,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 @@ -278,7 +273,6 @@ def get_default_device() -> str: return device - def main(): """Main routine""" debug = False @@ -286,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) + default=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') @@ -361,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 f646f618..9dcac416 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""" @@ -72,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: @@ -273,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 @@ -372,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 @@ -495,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/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( 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..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) @@ -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..fcf9e259 100644 --- a/pyftdi/doc/authors.rst +++ b/pyftdi/doc/authors.rst @@ -44,3 +44,13 @@ Contributors * Amanita-muscaria * len0rd * Rod Whitby + * Kornel Swierzy + * Taisuke Yamada + * Michael Niewöhner + * Kalofin + * Henry Au-Yeung + * Roman Dobrodii + * Mark Mentovai + * Alessandro Zini + * Sjoerd Simons + * David Schneider diff --git a/pyftdi/doc/conf.py b/pyftdi/doc/conf.py index 1b2ae32c..1f42769d 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 @@ -52,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-2020, %s' % contact +copyright = '2010-2025, %s' % contact show_authors = True html_theme = 'sphinx_rtd_theme' diff --git a/pyftdi/doc/defs.rst b/pyftdi/doc/defs.rst index 57f5e30d..3b200c83 100644 --- a/pyftdi/doc/defs.rst +++ b/pyftdi/doc/defs.rst @@ -1,23 +1,25 @@ .. |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/ +.. _PyJtagTools: https://www.github.com/eblot/pyjtagtools +.. _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/ -.. _FTDI macOS guide: http://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf +.. _libusb: https://www.libusb.info/ +.. _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 @@ -25,7 +27,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/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..05ded63f 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. @@ -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/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 7f11b38a..ce7c97a5 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 6d68602f..76d673ca 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** @@ -166,7 +168,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 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/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/requirements.rst b/pyftdi/doc/requirements.rst index 0a9dd9bf..341f20b3 100644 --- a/pyftdi/doc/requirements.rst +++ b/pyftdi/doc/requirements.rst @@ -3,25 +3,17 @@ Requirements ------------ -Python_ 3.7 or above is required. - -* 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. +Python_ 3.9 or above is required. 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 ~~~~~~~~~~~ @@ -32,6 +24,8 @@ regular basis on Linux hosts. As it contains no native code, it should work on any PyUSB_ and libusb_ supported platforms. On Windows, PyFtdi_ supports the FTDI D2XX drivers. +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 ~~~~~~~~~~ @@ -45,3 +39,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. 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..a52d347a 100644 --- a/pyftdi/eeprom.py +++ b/pyftdi/eeprom.py @@ -1,32 +1,23 @@ -# Copyright (c) 2019-2022, Emmanuel Blot +# Copyright (c) 2019-2024, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause """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 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 @@ -39,13 +30,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: @@ -70,10 +61,10 @@ 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.""" - CBUS = IntEnum('CBus', 'TXDEN PWREN TXLED RXLED TXRXLED SLEEP CLK48 CLK24 CLK12 ' 'CLK6 GPIO BB_WR BB_RD', start=0) @@ -120,14 +111,16 @@ 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: 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]: + # pylint: disable=no-self-argument """Return a list of supported EEPROM sizes. :return: the supported EEPROM sizes @@ -251,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 @@ -265,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. @@ -295,7 +288,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 @@ -312,7 +305,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 @@ -345,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. @@ -380,7 +373,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: @@ -410,7 +403,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: @@ -421,11 +414,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') @@ -438,12 +430,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) @@ -453,7 +445,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: @@ -486,7 +478,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 @@ -500,6 +492,75 @@ 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 == 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 + 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 == 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 + 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), @@ -521,7 +582,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. @@ -602,7 +693,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) @@ -625,7 +716,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. @@ -634,7 +725,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 """ @@ -651,7 +742,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 @@ -659,16 +750,20 @@ 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 ':/': # 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 @@ -686,13 +781,14 @@ 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 - # 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') @@ -702,16 +798,16 @@ 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 - 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 @@ -734,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) @@ -778,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 @@ -787,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]: @@ -809,8 +905,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 @@ -840,12 +936,19 @@ def _decode_eeprom(self): if cfg['use_usb_version']: cfg['usb_version'] = \ Hex4Int(sunpack(' 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') @@ -946,12 +1049,13 @@ 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, out: Optional[TextIO]) -> None: - if self.device_version in (0x0700, 0x800): # 2232H/4232H + # 2232H/4232H/4232HA + if self.device_version in (0x0700, 0x800, 0x3600): offset = 0x0c + group//2 nibble = group & 1 else: # 232H @@ -970,7 +1074,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'): @@ -979,10 +1083,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 @@ -1011,7 +1114,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] @@ -1028,20 +1131,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 @@ -1059,20 +1162,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 @@ -1084,19 +1187,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 @@ -1118,10 +1221,11 @@ 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}_type'] = ( + 'RS485' if conf & (rs485 << chix) else 'UART') 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' @@ -1131,8 +1235,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) diff --git a/pyftdi/ftdi.py b/pyftdi/ftdi.py index c0e3d768..891546e2 100644 --- a/pyftdi/ftdi.py +++ b/pyftdi/ftdi.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. # @@ -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): @@ -81,6 +71,7 @@ class Ftdi: ('230x', 0x6015), ('231x', 0x6015), ('234x', 0x6015), + ('4232ha', 0x6048), ('ft232', 0x6001), ('ft232r', 0x6001), ('ft232h', 0x6014), @@ -92,7 +83,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 +102,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 @@ -188,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 @@ -256,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) @@ -311,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 @@ -417,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 @@ -437,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 @@ -497,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 @@ -508,9 +502,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 @@ -535,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() @@ -544,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 @@ -654,10 +648,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 +701,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 @@ -726,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') @@ -945,7 +939,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,8 +1002,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 def is_mpsse(self) -> bool: @@ -1029,6 +1022,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 @@ -1086,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: @@ -1223,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) @@ -1266,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: @@ -1299,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] @@ -1417,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 @@ -1667,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, @@ -1730,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) @@ -1785,11 +1778,12 @@ 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) \ - -> 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 @@ -1911,7 +1905,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") @@ -1992,7 +1986,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 @@ -2008,7 +2002,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: @@ -2038,7 +2032,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 @@ -2051,7 +2045,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)""" @@ -2065,8 +2059,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""" @@ -2074,8 +2068,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: @@ -2088,15 +2082,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()) @@ -2133,13 +2127,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], @@ -2166,13 +2160,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', @@ -2265,9 +2259,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, @@ -2275,14 +2268,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 18353dd2..f55da6c4 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 @@ -428,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 @@ -481,7 +480,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 @@ -499,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 @@ -510,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 563a59bc..8bd254cd 100644 --- a/pyftdi/i2c.py +++ b/pyftdi/i2c.py @@ -1,19 +1,10 @@ -# Copyright (c) 2017-2021, Emmanuel Blot +# Copyright (c) 2017-2025, 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 @@ -117,7 +108,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 +125,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 @@ -222,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) @@ -353,10 +344,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 @@ -399,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, @@ -447,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'] @@ -478,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 @@ -498,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: @@ -601,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: @@ -662,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 @@ -704,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 @@ -744,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. @@ -776,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 @@ -886,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: @@ -909,8 +903,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) @@ -965,6 +959,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.I2C_DIR | self._gpio_low, + (self._gpio_dir & 0xFF)) + @property def _idle(self) -> Tuple[int]: return (Ftdi.SET_BITS_LOW, @@ -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: Optional[int]) -> None: if i2caddress is None: return self.log.debug(' prolog 0x%x', i2caddress >> 1) @@ -1031,12 +1031,15 @@ def _do_prolog(self, i2caddress: int) -> None: try: self._send_check_ack(cmd) except I2cNackError: - self.log.warning('NACK @ 0x%02x', (i2caddress>>1)) + self.log.warning('NACK @ 0x%02x', (i2caddress >> 1)) raise 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) @@ -1048,29 +1051,43 @@ def _send_check_ack(self, cmd: bytearray): 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) else: # SCL low, SDA high-Z cmd.extend(self._clk_lo_data_hi) # 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 _do_read(self, readlen: int) -> bytes: + 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 + @@ -1102,10 +1119,11 @@ 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): + + 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)) @@ -1115,27 +1133,27 @@ def write_command_gen(length: int): 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) else: while rem: + size = rem if rem > chunk_size: if not cmd: # build the command sequence only once, as it may be # 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)) cmd.extend(read_last) cmd.extend(self._immediate) - size = rem - 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) @@ -1150,6 +1168,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) diff --git a/pyftdi/jtag.py b/pyftdi/jtag.py index caf6b9a2..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. # @@ -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 @@ -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 a959a34b..c7c06294 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 @@ -48,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 = [] @@ -63,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) @@ -97,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: @@ -158,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: @@ -198,8 +195,8 @@ def xor(_a_: bool, _b_: bool) -> bool: :param _b_: second argument :return: xor-ed value """ - #pylint: disable-msg=superfluous-parens - return bool((not(_a_) and _b_) or (_a_ and not(_b_))) + # pylint: disable=superfluous-parens + return bool((not (_a_) and _b_) or (_a_ and not (_b_))) def is_iterable(obj: Any) -> bool: @@ -239,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, @@ -311,7 +308,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) @@ -330,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 e48c2eaa..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-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=broad-except +# pylint: disable=invalid-name +# 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 4a59674a..f5bf91e0 100644 --- a/pyftdi/serialext/protocol_ftdi.py +++ b/pyftdi/serialext/protocol_ftdi.py @@ -1,14 +1,14 @@ -# Copyright (c) 2008-2020, Emmanuel Blot +# Copyright (c) 2008-2024, Emmanuel Blot # Copyright (c) 2008-2016, Neotion # 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=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 @@ -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 fa679952..e239573d 100644 --- a/pyftdi/serialext/protocol_unix.py +++ b/pyftdi/serialext/protocol_unix.py @@ -1,26 +1,25 @@ -# 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 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 @@ -44,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 @@ -66,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): - # pylint: disable-msg=no-member - raise SerialExceptionWithErrno(msg, e.errno) from e - raise SerialException(msg) from e + msg = f'Could not open port: {exc}' + if isinstance(exc, socket.error): + # pylint: disable=no-member + raise SerialExceptionWithErrno(msg, exc.errno) from exc + raise SerialException(msg) from exc self._set_open_state(True) self._lastdtr = None @@ -91,7 +90,6 @@ def close(self): def in_waiting(self): """Return the number of characters currently in the input buffer.""" - #pylint: disable-msg=no-self-use return 0 def read(self, size=1): @@ -99,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: @@ -110,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 @@ -119,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: @@ -166,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 @@ -194,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/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..e7c98821 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 @@ -109,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 @@ -148,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') @@ -394,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'] @@ -476,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)) @@ -679,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 e179d6e1..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. # @@ -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 @@ -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 91016bdd..c413d2fd 100644 --- a/pyftdi/tests/backend/consts.py +++ b/pyftdi/tests/backend/consts.py @@ -1,17 +1,15 @@ """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 -from sys import version_info from pyftdi.ftdi import Ftdi from pyftdi.misc import EasyDict @@ -104,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 4797e50e..b188f6ac 100644 --- a/pyftdi/tests/backend/ftdivirt.py +++ b/pyftdi/tests/backend/ftdivirt.py @@ -1,21 +1,13 @@ """PyUSB virtual FTDI device.""" -# 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=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 import os from array import array @@ -24,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: @@ -211,7 +205,6 @@ class Fifos(NamedTuple): rx: Fifo # Host-to-FTDI tx: Fifo # FTDI-to-host - @unique class BitMode(IntEnum): """Function mode selection. @@ -230,14 +223,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 +246,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) @@ -903,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) @@ -972,6 +966,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/backend/loader.py b/pyftdi/tests/backend/loader.py index 4e0bfccd..bf04cf0a 100644 --- a/pyftdi/tests/backend/loader.py +++ b/pyftdi/tests/backend/loader.py @@ -1,21 +1,20 @@ """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 -#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=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 d3becd88..319087df 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 @@ -22,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: @@ -85,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, @@ -163,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] = [] @@ -229,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: @@ -319,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): @@ -332,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 @@ -357,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 a3c4b6c7..99f74bd5 100755 --- a/pyftdi/tests/bits.py +++ b/pyftdi/tests/bits.py @@ -1,19 +1,25 @@ #!/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-msg=broad-except +# pylint: disable=broad-except +# pylint: disable=invalid-name +# 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) @@ -75,23 +81,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}', + '8: 10000000 / 10000000') + self.assertEqual(f'{self.bs2} / {self.bs2!r}', + '8: 01000000 / 01000000') + self.assertEqual(f'{self.bs3} / {self.bs3!r}', + '7: 0010000 / 0010000') + self.assertEqual(f'{self.bs4} / {self.bs4!r}', + '11: 001 00000000 / 00100000000') + 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}', + '49: 1 00010000 11011001 00110001 01101110 10111111 ' + '11111111 / 100010000110110010011000101101110101111' + '1111111111') self.assertEqual(repr(self.bzs4), '11Z1Z010ZZ0100') self.assertEqual(repr(self.bzs5), '100Z01') @@ -117,8 +122,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 +144,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 +184,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' @@ -206,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 46f2a7fd..78787a18 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 @@ -9,15 +11,16 @@ 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 -#pylint: disable-msg=empty-docstring -#pylint: disable-msg=missing-docstring +# pylint: disable=empty-docstring +# pylint: disable=missing-docstring -class CbusGpioTestCase(TestCase): +class CbusOutputGpioTestCase(TestCase): """FTDI CBUS GPIO feature test case""" @classmethod @@ -25,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 @@ -58,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 @@ -94,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 986d7c31..9a9c3036 100755 --- a/pyftdi/tests/eeprom.py +++ b/pyftdi/tests/eeprom.py @@ -1,25 +1,27 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +"""EEPROM unit tests.""" + # 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 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 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): +class EepromTestCase(TestCase): """FTDI EEPROM access method test case""" @classmethod @@ -28,7 +30,6 @@ def setUpClass(cls): cls.eeprom_size = int(environ.get('FTDI_EEPROM_SIZE', '256')) cls.url = environ.get('FTDI_DEVICE', 'ftdi://ftdi:2232h/1') - def setUp(self): """Open a connection to the FTDI, defining which pins are configured as output and input""" @@ -43,7 +44,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""" @@ -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_ @@ -107,10 +108,10 @@ 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') + 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 7acf99f3..05e63d52 --- a/pyftdi/tests/eeprom_mock.py +++ b/pyftdi/tests/eeprom_mock.py @@ -1,27 +1,38 @@ #!/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, TestLoader, TestSuite, 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 -class FtdiTestCase(TestCase): +class FtdiTestCase: """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" + TEST_CONFIG_FILENAME = '' + @classmethod def setUpClass(cls): cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), permissive=False) @@ -39,17 +50,12 @@ 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 """ - # manufacturer name string to use in tests - TEST_MANU_NAME = "MNAME" - TEST_PROD_NAME = "PNAME" - TEST_SN = "SN123" - @classmethod def setUpClass(cls): FtdiTestCase.setUpClass() @@ -60,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): @@ -70,8 +76,10 @@ def test_mirror_properties(self): # properties should work regardless of if the mirror option is set # or not eeprom = FtdiEeprom() + eeprom.set_test_mode(True) 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,8 +87,9 @@ 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.mirror_sector) mirrored_eeprom.close() def test_mirror_manufacturer(self): @@ -88,6 +97,7 @@ def test_mirror_manufacturer(self): eeprom sectors """ eeprom = FtdiEeprom() + eeprom.set_test_mode(True) eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) eeprom.erase() @@ -99,6 +109,7 @@ def test_mirror_product(self): sectors """ eeprom = FtdiEeprom() + eeprom.set_test_mode(True) eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) eeprom.erase() @@ -110,6 +121,7 @@ def test_mirror_serial(self): sectors """ eeprom = FtdiEeprom() + eeprom.set_test_mode(True) eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) eeprom.erase() @@ -121,6 +133,7 @@ def test_varstr_combinations(self): across the 2 eeprom sectors """ eeprom = FtdiEeprom() + eeprom.set_test_mode(True) eeprom.enable_mirroring(True) eeprom.open(self.url, ignore=True) @@ -153,7 +166,9 @@ 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) _, mirrored = eeprom._compute_size([]) self.assertFalse(mirrored) @@ -176,23 +191,20 @@ 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], - 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' + 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 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): @@ -204,35 +216,115 @@ 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): """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 bool(getattr(self, 'DEVICE_CAN_MIRROR', None)): + 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.set_test_mode(True) 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.mirror_sector eeprom.close() # even if mirroring is enabled, should still stay false mirrored_eeprom = FtdiEeprom() 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.set_test_mode(True) + 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.set_test_mode(True) + 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.set_test_mode(True) + 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.set_test_mode(True) + 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. """ + # pylint: disable=protected-access + if self.DEVICE_CAN_MIRROR: + 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) _, mirrored = eeprom._compute_size([]) self.assertFalse(mirrored) @@ -244,18 +336,80 @@ 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, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' + + +class EepromMirrorFt2232hTestCase(EepromMirrorTestCase, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' + + +class EepromMirrorFt4232hTestCase(EepromMirrorTestCase, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft4232h.yaml' + + +class EepromMirrorFt232rTestCase(NonMirroredEepromTestCase, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232r.yaml' + DEVICE_CAN_MIRROR = False + + +class EepromMirrorFt230xTestCase(NonMirroredEepromTestCase, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft230x.yaml' + DEVICE_CAN_MIRROR = False + + +class EepromNonMirroredFt232hTestCase(NonMirroredEepromTestCase, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft232h.yaml' + DEVICE_CAN_MIRROR = True + + +class EepromNonMirroredFt2232hTestCase(NonMirroredEepromTestCase, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft2232h.yaml' + DEVICE_CAN_MIRROR = True + + +class EepromNonMirroredFt4232hTestCase(NonMirroredEepromTestCase, TestCase): + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft4232h.yaml' + DEVICE_CAN_MIRROR = True + def suite(): suite_ = TestSuite() - suite_.addTest(makeSuite(EepromMirrorFt232hTestCase, 'test')) - suite_.addTest(makeSuite(EepromMirrorFt2232hTestCase, 'test')) - suite_.addTest(makeSuite(EepromMirrorFt230xTestCase, 'test')) + loader = TestLoader() + mod = modules[__name__] + tests = [] + # Test devices that support the mirroring capability + tests.extend(('EepromMirrorFt232h', + 'EepromMirrorFt2232h', + 'EepromMirrorFt4232h')) + # Test devices that do not support the mirror capability + tests.extend(('EepromMirrorFt232r', + 'EepromMirrorFt230x')) + # test devices that support the mirroring capability, but have it disabled + tests.extend(('EepromNonMirroredFt232h', + 'EepromNonMirroredFt2232h', + 'EepromNonMirroredFt4232h')) + for testname in tests: + testcase = getattr(mod, f'{testname}TestCase') + suite_.addTest(loader.loadTestsFromTestCase(testcase)) return 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', ) @@ -263,15 +417,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 main(): - import doctest - doctest.testmod(modules[__name__]) +def setup_module(): + # 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' @@ -282,12 +438,16 @@ def main(): 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) virtualize() + + +def main(): + setup_module() try: ut_main(defaultTest='suite') except KeyboardInterrupt: diff --git a/pyftdi/tests/ftdi.py b/pyftdi/tests/ftdi.py index 14d08d28..46c284e2 100755 --- a/pyftdi/tests/ftdi.py +++ b/pyftdi/tests/ftdi.py @@ -1,17 +1,21 @@ #!/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 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 @@ -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) @@ -148,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 3c1ca732..2fcf1e7a 100755 --- a/pyftdi/tests/gpio.py +++ b/pyftdi/tests/gpio.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2016-2020, Emmanuel Blot +# Copyright (c) 2016-2024, Emmanuel Blot # All rights reserved. # # 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=global-statement +# pylint: disable=invalid-name +# pylint: disable=missing-docstring import logging from collections import deque 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, @@ -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] @@ -354,7 +353,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 @@ -443,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() @@ -579,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() @@ -609,16 +607,14 @@ 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_ 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', ) @@ -632,9 +628,10 @@ def virtualize(): raise AssertionError('Cannot load virtual USB backend') from exc -def main(): - import doctest - doctest.testmod(modules[__name__]) +def setup_module(): + # 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' @@ -651,6 +648,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/i2c.py b/pyftdi/tests/i2c.py index 5b841957..2bc01c9a 100755 --- a/pyftdi/tests/i2c.py +++ b/pyftdi/tests/i2c.py @@ -1,24 +1,26 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2017-2020, Emmanuel Blot +"""I2C unit tests.""" + +# Copyright (c) 2017-2024, Emmanuel Blot # All rights reserved. # # 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 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-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 class I2cTca9555TestCase(TestCase): @@ -52,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 """ @@ -79,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 """ @@ -106,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 """ @@ -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() @@ -152,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 @@ -226,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. """ @@ -239,16 +238,16 @@ 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 or FT4232H. See issue #159. + i.e. FT2232H, FT4232H or FT4232HA. See issue #159. """ 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) @@ -256,7 +255,7 @@ def test(self): print(port.read_from(0x00, 2)) -class I2cIssue143(TestCase): +class I2cIssue143TestCase(TestCase): """#143. """ @@ -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) @@ -290,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 ff13936f..1bdd1e9d 100755 --- a/pyftdi/tests/jtag.py +++ b/pyftdi/tests/jtag.py @@ -1,17 +1,20 @@ #!/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 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 -#pylint: disable-msg=missing-docstring +# pylint: disable=missing-docstring # Should match the tested device @@ -38,7 +41,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 +49,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 +57,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""" @@ -83,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 13ac3cd2..bc7ca581 100755 --- a/pyftdi/tests/mockusb.py +++ b/pyftdi/tests/mockusb.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2020-2021, Emmanuel Blot +# 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=global-statement +# pylint: disable=invalid-name +# pylint: disable=missing-docstring +# pylint: disable=too-many-locals import logging from collections import defaultdict @@ -19,8 +17,8 @@ from io import StringIO from os import environ from string import ascii_letters -from sys import modules, stdout, version_info -from unittest import TestCase, TestSuite, makeSuite, main as ut_main +from sys import modules, stdout +from unittest import TestCase, TestLoader, TestSuite, main as ut_main from urllib.parse import urlsplit from pyftdi import FtdiLogger from pyftdi.eeprom import FtdiEeprom @@ -101,16 +99,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 +143,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 = [ln.strip() for ln 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) @@ -185,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() @@ -211,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() @@ -238,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() @@ -250,7 +253,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 @@ -265,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() @@ -292,11 +295,11 @@ 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() - 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 +307,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$') @@ -406,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() @@ -481,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() @@ -499,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() @@ -723,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() @@ -769,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() @@ -816,25 +817,11 @@ 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_ -def main(): +def setup_module(): testmod(modules[__name__]) debug = to_bool(environ.get('FTDI_DEBUG', 'off')) if debug: @@ -861,6 +848,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') 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/tests/spi.py b/pyftdi/tests/spi.py index 1753c872..dad6e3fd 100755 --- a/pyftdi/tests/spi.py +++ b/pyftdi/tests/spi.py @@ -6,12 +6,11 @@ # # 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 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 @@ -102,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. @@ -144,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: @@ -218,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: @@ -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,13 +305,13 @@ 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) -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 @@ -363,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_ @@ -380,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__': 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 68491935..0c5fa4b5 100755 --- a/pyftdi/tests/toolsimport.py +++ b/pyftdi/tests/toolsimport.py @@ -1,20 +1,19 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2020, Emmanuel Blot +# Copyright (c) 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=empty-docstring +# pylint: disable=global-statement +# pylint: disable=invalid-name +# pylint: disable=missing-docstring from doctest import testmod 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): @@ -23,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.7 onwards. + PyFtdi and tools should support Python 3.9 onwards. """ @classmethod @@ -58,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 3a248029..d54f9590 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 @@ -16,14 +18,14 @@ 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 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 @@ -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: @@ -174,9 +176,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 +207,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') @@ -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): @@ -340,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_ @@ -352,10 +354,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 91d96f28..50b295f5 100644 --- a/pyftdi/tracer.py +++ b/pyftdi/tracer.py @@ -1,21 +1,21 @@ -# Copyright (c) 2017-2020, 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 +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: @@ -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): @@ -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 bf73ffb1..b4dde7c2 100644 --- a/pyftdi/usbtools.py +++ b/pyftdi/usbtools.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. # @@ -19,9 +19,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), @@ -58,6 +56,7 @@ it may be degraded to (vid, pid) 2-uple. """ + class UsbToolsError(Exception): """UsbTools error.""" @@ -88,15 +87,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) @@ -105,8 +103,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, ): @@ -120,9 +116,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: @@ -137,16 +132,15 @@ 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. :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: @@ -202,8 +196,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): @@ -212,10 +204,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: @@ -226,8 +217,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: @@ -236,9 +225,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]) @@ -249,8 +238,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, @@ -298,18 +285,19 @@ 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') 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 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: @@ -319,30 +307,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 @@ -378,13 +366,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 @@ -395,8 +383,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: @@ -418,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 @@ -454,10 +442,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 @@ -483,7 +471,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) @@ -492,7 +480,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] @@ -510,14 +498,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: @@ -527,7 +515,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: @@ -544,7 +532,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 @@ -568,11 +556,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, @@ -607,6 +592,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] @@ -620,17 +606,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()) @@ -645,11 +631,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.cfg b/setup.cfg index fd242be7..da31f73b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,10 @@ [bdist_wheel] [metadata] -license_file = pyftdi/doc/license.rst +license_files = pyftdi/doc/license.rst + +[project] +license = "BSD-3-Clause" [build_sphinx] source-dir = pyftdi/doc diff --git a/setup.py b/setup.py index 44a0c146..6f3b4392 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-2025 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' @@ -35,14 +32,14 @@ 'Environment :: Other Environment', 'Natural Language :: English', 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', 'Operating System :: Microsoft :: Windows', - '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', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Hardware :: Hardware Drivers', ] @@ -50,9 +47,6 @@ 'pyusb >= 1.0.0, != 1.2.0', 'pyserial >= 3.0', ] -TEST_REQUIRES = [ - 'ruamel.yaml >= 0.16', -] HERE = abspath(dirname(__file__)) @@ -81,13 +75,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): @@ -119,7 +110,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) @@ -138,26 +129,29 @@ 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() + error_count = 0 for dpath, dnames, fnames in walk(topdir): dnames[:] = [d for d in dnames 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) > 120: - print('\n %d: %s' % (lpos, line.rstrip())) - raise RuntimeError("Invalid line width '%s'" % - relpath(filename, topdir)) + toppath = relpath(filename, topdir) + 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('No Python file found from "%s"' % - topdir) + raise RuntimeError(f'No Python file found from "{topdir}"') def main(): @@ -177,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, package_dir={'': '.'}, package_data={'pyftdi': ['*.rst', 'doc/*.rst', 'doc/api/*.rst', @@ -192,14 +187,13 @@ def main(): ], }, install_requires=INSTALL_REQUIRES, - test_requires=TEST_REQUIRES, - python_requires='>=3.7', + python_requires='>=3.9', ) 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..2edb271d --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,4 @@ +setuptools +wheel +pylint +ruamel.yaml >= 0.16