diff -Nru pyftdi-0.53.3/debian/changelog pyftdi-0.54.0/debian/changelog --- pyftdi-0.53.3/debian/changelog 2021-12-21 19:54:54.000000000 +0000 +++ pyftdi-0.54.0/debian/changelog 2022-10-24 21:05:22.000000000 +0000 @@ -1,3 +1,12 @@ +pyftdi (0.54.0-1) unstable; urgency=medium + + * [a3bbd4f] New upstream version 0.54.0 + * [832df40] Update standards version to 4.6.1, no changes needed. + * [e3230ad] Fix homepage. + * [d8764c3] Replace dep from sphinx to python3-sphinx. + + -- Anton Gladky Mon, 24 Oct 2022 23:05:22 +0200 + pyftdi (0.53.3-1) unstable; urgency=medium * [8369272] New upstream version 0.53.3. (Closes: #1002138) diff -Nru pyftdi-0.53.3/debian/control pyftdi-0.54.0/debian/control --- pyftdi-0.53.3/debian/control 2021-12-21 19:54:37.000000000 +0000 +++ pyftdi-0.54.0/debian/control 2022-10-24 21:05:08.000000000 +0000 @@ -10,14 +10,14 @@ python3-pytest, python3-setuptools, python3-ruamel.yaml, - sphinx, + python3-sphinx, python3-sphinx-autodoc-typehints, python3-sphinx-rtd-theme, python3-usb -Standards-Version: 4.5.1 +Standards-Version: 4.6.1 Vcs-Browser: https://salsa.debian.org/electronics-team/pyftdi Vcs-Git: https://salsa.debian.org/electronics-team/pyftdi.git -Homepage: https://github.com/rm-hull/ftdi +Homepage: https://github.com/eblot/pyftdi Testsuite: autopkgtest-pkg-python Package: python3-ftdi diff -Nru pyftdi-0.53.3/.github/workflows/pythonchecksyntax.yml pyftdi-0.54.0/.github/workflows/pythonchecksyntax.yml --- pyftdi-0.53.3/.github/workflows/pythonchecksyntax.yml 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/.github/workflows/pythonchecksyntax.yml 2022-03-13 21:41:38.000000000 +0000 @@ -9,7 +9,7 @@ runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff -Nru pyftdi-0.53.3/.github/workflows/pythonmocktests.yml pyftdi-0.54.0/.github/workflows/pythonmocktests.yml --- pyftdi-0.53.3/.github/workflows/pythonmocktests.yml 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/.github/workflows/pythonmocktests.yml 2022-03-13 21:41:38.000000000 +0000 @@ -11,28 +11,33 @@ runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 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 + - name: Install package run: | python setup.py install + - name: Run Mock tests env: FTDI_LOGLEVEL: WARNING FTDI_DEBUG: on run: | python pyftdi/tests/mockusb.py + - name: Run GPIO tests env: FTDI_LOGLEVEL: WARNING @@ -40,3 +45,11 @@ FTDI_VIRTUAL: on run: | python pyftdi/tests/gpio.py + + - name: Run EEPROM tests + env: + FTDI_LOGLEVEL: WARNING + FTDI_DEBUG: on + FTDI_VIRTUAL: on + run: | + python pyftdi/tests/eeprom_mock.py diff -Nru pyftdi-0.53.3/.github/workflows/pythonpackage.yml pyftdi-0.54.0/.github/workflows/pythonpackage.yml --- pyftdi-0.53.3/.github/workflows/pythonpackage.yml 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/.github/workflows/pythonpackage.yml 2022-03-13 21:41:38.000000000 +0000 @@ -8,7 +8,7 @@ runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 diff -Nru pyftdi-0.53.3/pyftdi/bin/ftconf.py pyftdi-0.54.0/pyftdi/bin/ftconf.py --- pyftdi-0.53.3/pyftdi/bin/ftconf.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/bin/ftconf.py 2022-03-13 21:41:38.000000000 +0000 @@ -3,7 +3,7 @@ """Simple FTDI EEPROM configurator. """ -# Copyright (c) 2019-2020, Emmanuel Blot +# Copyright (c) 2019-2022, 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 +from sys import modules, stderr, stdout from textwrap import fill from traceback import format_exc from pyftdi import FtdiLogger @@ -31,38 +31,63 @@ argparser = ArgumentParser(description=modules[__name__].__doc__) argparser.add_argument('device', nargs='?', default='ftdi:///?', help='serial port device name') - argparser.add_argument('-x', '--hexdump', action='store_true', - help='dump EEPROM content as ASCII') - argparser.add_argument('-X', '--hexblock', type=int, - help='dump EEPROM as indented hexa blocks') - argparser.add_argument('-i', '--input', type=FileType('rt'), - help='input ini file to load EEPROM content') - argparser.add_argument('-l', '--load', default='all', - choices=('all', 'raw', 'values'), - help='section(s) to load from input file') - argparser.add_argument('-o', '--output', type=FileType('wt'), - help='output ini file to save EEPROM content') - argparser.add_argument('-s', '--serial-number', - help='set serial number') - argparser.add_argument('-m', '--manufacturer', - help='set manufacturer name') - argparser.add_argument('-p', '--product', - help='set product name') - argparser.add_argument('-c', '--config', action='append', - help='change/configure a property ' - 'as key=value pair') - argparser.add_argument('-e', '--erase', action='store_true', - help='erase the whole EEPROM content') - argparser.add_argument('-u', '--update', action='store_true', - help='perform actual update, use w/ care') - argparser.add_argument('-P', '--vidpid', action='append', - help='specify a custom VID:PID device ID, ' - 'may be repeated') - argparser.add_argument('-V', '--virtual', type=FileType('r'), - help='use a virtual device, specified as YaML') - argparser.add_argument('-v', '--verbose', action='count', default=0, + + files = argparser.add_argument_group(title='Files') + files.add_argument('-i', '--input', type=FileType('rt'), + help='input ini file to load EEPROM content') + files.add_argument('-l', '--load', default='all', + choices=('all', 'raw', 'values'), + help='section(s) to load from input file') + files.add_argument('-o', '--output', + help='output ini file to save EEPROM content') + files.add_argument('-V', '--virtual', type=FileType('r'), + help='use a virtual device, specified as YaML') + + device = argparser.add_argument_group(title='Device') + device.add_argument('-P', '--vidpid', action='append', + help='specify a custom VID:PID device ID ' + '(search for FTDI devices)') + device.add_argument('-M', '--eeprom', + help='force an EEPROM model') + device.add_argument('-S', '--size', type=int, + choices=FtdiEeprom.eeprom_sizes, + help='force an EEPROM size') + + fmt = argparser.add_argument_group(title='Format') + fmt.add_argument('-x', '--hexdump', action='store_true', + help='dump EEPROM content as ASCII') + fmt.add_argument('-X', '--hexblock', type=int, + help='dump EEPROM as indented hexa blocks') + + config = argparser.add_argument_group(title='Configuration') + config.add_argument('-s', '--serial-number', + help='set serial number') + config.add_argument('-m', '--manufacturer', + help='set manufacturer name') + config.add_argument('-p', '--product', + help='set product name') + config.add_argument('-c', '--config', action='append', + help='change/configure a property as key=value ' + 'pair') + config.add_argument('--vid', type=lambda x: int(x, 16), + help='shortcut to configure the USB vendor ID') + config.add_argument('--pid', type=lambda x: int(x, 16), + help='shortcut to configure the USB product ID') + + action = argparser.add_argument_group(title='Action') + action.add_argument('-e', '--erase', action='store_true', + help='erase the whole EEPROM content') + action.add_argument('-E', '--full-erase', action='store_true', + default=False, + help='erase the whole EEPROM content, including ' + 'the CRC') + action.add_argument('-u', '--update', action='store_true', + help='perform actual update, use w/ care') + + extra = argparser.add_argument_group(title='Extras') + extra.add_argument('-v', '--verbose', action='count', default=0, help='increase verbosity') - argparser.add_argument('-d', '--debug', action='store_true', + extra.add_argument('-d', '--debug', action='store_true', help='enable debug mode') args = argparser.parse_args() debug = args.debug @@ -97,8 +122,8 @@ argparser.error(str(exc)) eeprom = FtdiEeprom() - eeprom.open(args.device) - if args.erase: + eeprom.open(args.device, size=args.size, model=args.eeprom) + if args.erase or args.full_erase: eeprom.erase() if args.input: eeprom.load_config(args.input, args.load) @@ -109,7 +134,7 @@ if args.product: eeprom.set_product_name(args.product) for conf in args.config or []: - if conf == '?': + if conf in ('?', 'help'): helpstr = ', '.join(sorted(eeprom.properties)) print(fill(helpstr, initial_indent=' ', subsequent_indent=' ')) @@ -120,6 +145,8 @@ if not value: argparser.error('Configuration %s without value' % conf) + if value == 'help': + value = '?' helpio = StringIO() eeprom.set_property(name, value, helpio) helpstr = helpio.getvalue() @@ -130,6 +157,10 @@ break else: argparser.error('Missing name:value separator in %s' % conf) + if args.vid: + eeprom.set_property('vendor_id', args.vid) + if args.pid: + eeprom.set_property('product_id', args.pid) if args.hexdump: print(hexdump(eeprom.data)) if args.hexblock is not None: @@ -138,12 +169,16 @@ hexa = ' '.join(['%02x' % x for x in eeprom.data[pos:pos+16]]) print(indent, hexa, sep='') if args.update: - if eeprom.commit(False): + if eeprom.commit(False, no_crc=args.full_erase): eeprom.reset_device() if args.verbose > 0: eeprom.dump_config() if args.output: - eeprom.save_config(args.output) + if args.output == '-': + eeprom.save_config(stdout) + else: + with open(args.output, 'wt') as ofp: + eeprom.save_config(ofp) except (ImportError, IOError, NotImplementedError, ValueError) as exc: print('\nError: %s' % exc, file=stderr) diff -Nru pyftdi-0.53.3/pyftdi/bin/ftdi_urls.py pyftdi-0.54.0/pyftdi/bin/ftdi_urls.py --- pyftdi-0.53.3/pyftdi/bin/ftdi_urls.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/bin/ftdi_urls.py 2022-03-13 21:41:38.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -# Copyright (c) 2019-2020, Emmanuel Blot +# Copyright (c) 2019-2022, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -55,7 +55,7 @@ loader.load(args.virtual) try: - add_custom_devices(Ftdi, args.vidpid) + add_custom_devices(Ftdi, args.vidpid, force_hex=True) except ValueError as exc: argparser.error(str(exc)) diff -Nru pyftdi-0.53.3/pyftdi/bin/i2cscan.py pyftdi-0.54.0/pyftdi/bin/i2cscan.py --- pyftdi-0.53.3/pyftdi/bin/i2cscan.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/bin/i2cscan.py 2022-03-13 21:41:38.000000000 +0000 @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -# Copyright (c) 2018-2020, Emmanuel Blot +# Copyright (c) 2018-2022, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -130,7 +130,7 @@ loader.load(args.virtual) try: - add_custom_devices(Ftdi, args.vidpid) + add_custom_devices(Ftdi, args.vidpid, force_hex=True) except ValueError as exc: argparser.error(str(exc)) diff -Nru pyftdi-0.53.3/pyftdi/bin/pyterm.py pyftdi-0.54.0/pyftdi/bin/pyterm.py --- pyftdi-0.53.3/pyftdi/bin/pyterm.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/bin/pyterm.py 2022-03-13 21:41:38.000000000 +0000 @@ -3,7 +3,7 @@ """Simple Python serial terminal """ -# Copyright (c) 2010-2020, Emmanuel Blot +# Copyright (c) 2010-2022, Emmanuel Blot # Copyright (c) 2016, Emmanuel Bouaziz # All rights reserved. # @@ -76,14 +76,12 @@ # out from the HW as soon as it is made available, and use a deque # to serve the actual reader thread args.append(self._get_from_source) - sourcer = Thread(target=self._sourcer) - sourcer.setDaemon(1) + sourcer = Thread(target=self._sourcer, daemon=True) sourcer.start() else: # regular kernel buffered device args.append(self._get_from_port) - reader = Thread(target=self._reader, args=tuple(args)) - reader.setDaemon(1) + reader = Thread(target=self._reader, args=tuple(args), daemon=True) reader.start() # start the writer (host to target direction) self._writer(fullmode, silent, localecho, autocr) @@ -350,7 +348,7 @@ loader.load(args.virtual) try: - add_custom_devices(Ftdi, args.vidpid) + add_custom_devices(Ftdi, args.vidpid, force_hex=True) except ValueError as exc: argparser.error(str(exc)) diff -Nru pyftdi-0.53.3/pyftdi/doc/api/uart.rst pyftdi-0.54.0/pyftdi/doc/api/uart.rst --- pyftdi-0.53.3/pyftdi/doc/api/uart.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/api/uart.rst 2022-03-13 21:41:38.000000000 +0000 @@ -145,7 +145,8 @@ PYTHONPATH=$PWD pyftdi/bin/pyterm.py ftdi:///? -The above command lists all the available FTDI device ports. +The above command lists all the available FTDI device ports. To avoid conflicts +with some shells such as `zsh`, escape the `?` char as ``ftdi:///\?``. To start up a serial terminal session, specify the FTDI port to use, for example: diff -Nru pyftdi-0.53.3/pyftdi/doc/authors.rst pyftdi-0.54.0/pyftdi/doc/authors.rst --- pyftdi-0.53.3/pyftdi/doc/authors.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/authors.rst 2022-03-13 21:41:38.000000000 +0000 @@ -42,3 +42,4 @@ * alexforencich * TedKus * Amanita-muscaria + * len0rd diff -Nru pyftdi-0.53.3/pyftdi/doc/defs.rst pyftdi-0.54.0/pyftdi/doc/defs.rst --- pyftdi-0.53.3/pyftdi/doc/defs.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/defs.rst 2022-03-13 21:41:38.000000000 +0000 @@ -27,6 +27,7 @@ .. _DCE: https://en.wikipedia.org/wiki/Data_circuit-terminating_equipment .. _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 .. Restructured Text levels diff -Nru pyftdi-0.53.3/pyftdi/doc/eeprom.rst pyftdi-0.54.0/pyftdi/doc/eeprom.rst --- pyftdi-0.53.3/pyftdi/doc/eeprom.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/eeprom.rst 2022-03-13 21:41:38.000000000 +0000 @@ -39,27 +39,44 @@ :: - ftconf.py [-h] [-x] [-X HEXBLOCK] [-i INPUT] [-l {all,raw,values}] - [-o OUTPUT] [-s SERIAL_NUMBER] [-m MANUFACTURER] [-p PRODUCT] - [-c CONFIG] [-e] [-u] [-P VIDPID] [-V VIRTUAL] [-v] [-d] + usage: ftconf.py [-h] [-i INPUT] [-l {all,raw,values}] [-o OUTPUT] [-V VIRTUAL] + [-P VIDPID] [-M EEPROM] [-S {128,256,1024}] [-x] [-X HEXBLOCK] + [-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 - -x, --hexdump dump EEPROM content as ASCII - -X HEXBLOCK, --hexblock HEXBLOCK - dump EEPROM as indented hexa blocks + + Files: -i INPUT, --input INPUT input ini file to load EEPROM content -l {all,raw,values}, --load {all,raw,values} section(s) to load from input file -o OUTPUT, --output OUTPUT 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) + -M EEPROM, --eeprom EEPROM + 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 -m MANUFACTURER, --manufacturer MANUFACTURER @@ -68,16 +85,18 @@ set product name -c CONFIG, --config CONFIG 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 - -P VIDPID, --vidpid VIDPID - specify a custom VID:PID device ID, may be repeated - -V VIRTUAL, --virtual VIRTUAL - use a virtual device, specified as YaML + + Extras: -v, --verbose increase verbosity -d, --debug enable debug mode - **Again, please read the** :doc:`license` **terms before using the EEPROM API or this script. You may brick your device if something goes wrong, and there may be no way to recover your device.** @@ -116,20 +135,44 @@ The name should be separated from the value with an equal ``=`` sign or alternatively a full column ``:`` character. - * To obtain the list of supported name, use the `?` wildcard: ``-c ?``. - * To obtain the list of supported values for a namw, use the `?` wildcard: - ``-c name=?``, where *name* is a supported name. + * To obtain the list of supported name, use the `?` wildcard: ``-c ?``, or + `-c help` to avoid conflicts with some shells + * To obtain the list of supported values for a name, use the `?` or the `help` + wildcard: + ``-c name=help``, where *name* is a supported name. See :ref:`cbus_func` table for the alternate function associated with each name. +.. _option_E_: + +``-E`` + Erase the full EEPROM content including the CRC. As the CRC no longer + validates the EEPROM content, the EEPROM configuration is ignored on the next + power cycle of the device, so the default FTDI configuration is used. + + This may be useful to recover from a corrupted EEPROM, as when no EEPROM or a + blank EEPROM is detected, the FTDI falls back to a default configuration. + + Note that without option :ref:`-u `, the EEPROM content is not + actually modified, the script runs in dry-run mode. + .. _option_e: ``-e`` - Erase the whole EEPROM. This may be useful to recover from a corrupted - EEPROM, as when no EEPROM or a blank EEPROM is detected, the FTDI falls back - to a default configuration. Note that without option :ref:`-u `, - the EEPROM content is not actually modified, the script runs in dry-run mode. + Erase the whole EEPROM and regenerates a valid CRC. + + Beware that as `-e` option generates a valid CRC for the erased EEPROM + content, the FTDI device may identified itself as VID:PID FFFF:FFFF on next + reboot. You should likely use the `--vid` and `--pid` option to define a + valid FDTI device USB identifier with this option to ensure the device + identifies itself as a FTDI device on next power cycle. + + Note that without option :ref:`-u `, the EEPROM content is not + actually modified, the script runs in dry-run mode. + + Alternatively, use `-E` option that erase the full EEPROM content including + the CRC. .. _option_i: @@ -151,6 +194,13 @@ through the :ref:`-c ` option switch. Unsupported feature are ignored, and a warning is emitted for each unsupported feature. +.. _option_M_: + +``-M `` + Specify the EEPROM model (93c46, 93c56, 93c66) that is connected to the FTDI + device. There is no reason to use this option except for recovery purposes, + see option `-E`. It is mutually exclusive with the `-S` option. + .. _option_m: ``-m `` @@ -159,7 +209,6 @@ runs in dry-run mode. Manufacturer names with ``/`` or ``:`` characters are rejected, to avoid parsing issues with FTDI :ref:`URLs `. - .. _option_o: ``-o `` @@ -181,6 +230,19 @@ dry-run mode. Product names with ``/`` or ``:`` characters are rejected, to avoid parsing issues with FTDI :ref:`URLs `. +.. _option_pid: + +``--pid`` + Define the USB product identifier - as an hexadecimal number. This is a + shortcut for `-c product_id` + +.. _option_S_: + +``-S `` + Specify the EEPROM size -in bytes- that is connected to the FTDI device. + There is no reason to use this option except for recovery purposes, + see option `-E`. It is mutually exclusive with the `-M` option. + .. _option_s: ``-s `` @@ -199,6 +261,12 @@ warned. PyFtdi_ offers neither guarantee whatsoever than altering the EEPROM content is safe, nor that it is possible to recover from a bricked device. +.. _option_vid: + +``--vid`` + Define the USB vendor identifier - as an hexadecimal number. This is a + shortcut for `-c vendor_id`. + .. _option_x: ``-x`` @@ -303,6 +371,26 @@ DRIVE0, DRIVE1, I2C_RXF, I2C_TXE, GPIO, PWREN, RXLED, SLEEP, 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. + + :: + + pyftdi/bin/ftconf.py -P ffff:ffff ftdi://ffff:ffff/1 -E -u + +* Recover from an erased EEPROM with a valid CRC + + :: + + # for a FT4232 device + # note that ffff matches an erased EEPROM, other corrupted values may + # exist, such device can be identified with system tools such as lsusb + + pyftdi/bin/ftconf.py -P ffff:ffff ftdi://ffff:ffff/1 -e -u \ + --vid 0403 --pid 6011 + .. _eeprom_cbus: * Configure CBUS: 0 and 3 as GPIOs, then show the device configuration diff -Nru pyftdi-0.53.3/pyftdi/doc/gpio.rst pyftdi-0.54.0/pyftdi/doc/gpio.rst --- pyftdi-0.53.3/pyftdi/doc/gpio.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/gpio.rst 2022-03-13 21:41:38.000000000 +0000 @@ -198,7 +198,7 @@ * b\ :sub:`0`\ (``0x01``) represents the first pin of a port, *i.e.* AD0/BD0 * b\ :sub:`1`\ (``0x02``) represents the second pin of a port, *i.e.* AD1/BD1 * ... -* b\ :sub:`7`\ (``0x80``) represents the seventh pin of a port, *i.e.* AD7/BD7 +* b\ :sub:`7`\ (``0x80``) represents the eighth pin of a port, *i.e.* AD7/BD7 * b\ :sub:`N`\ represents the highest pin of a port, *i.e.* AD7/BD7 for an 8-bit port, AD15/BD15 for a 16-bit port, etc. diff -Nru pyftdi-0.53.3/pyftdi/doc/requirements.rst pyftdi-0.54.0/pyftdi/doc/requirements.rst --- pyftdi-0.53.3/pyftdi/doc/requirements.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/requirements.rst 2022-03-13 21:41:38.000000000 +0000 @@ -3,9 +3,11 @@ Requirements ------------ -Python_ 3.6 or above is required. +Python_ 3.7 or above is required. -Python 3.5 has reached end-of-life on September 5th, 2020. +Python 3.6 has reached end-of-life on December 23rd, 2021. + +PyFtdi *v0.53* is the last PyFtdi version to support Python 3.6. PyFtdi *v0.52* is the last PyFtdi version to support Python 3.5. PyFtdi_ relies on PyUSB_, which itself depends on one of the following native diff -Nru pyftdi-0.53.3/pyftdi/doc/testing.rst pyftdi-0.54.0/pyftdi/doc/testing.rst --- pyftdi-0.53.3/pyftdi/doc/testing.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/testing.rst 2022-03-13 21:41:38.000000000 +0000 @@ -82,10 +82,12 @@ No hardware is required to run these tests, to even a single FTDI device. -This new test framework require Python 3.6+, as it uses the fstring_ syntax. +The test configuration files are described in YaML file format, therefore the +ruamel.yaml_ package is required. .. code-block:: python + pip3 install ruamel.yaml PYTHONPATH=. FTDI_LOGLEVEL=info pyftdi/tests/mockusb.py Configuration diff -Nru pyftdi-0.53.3/pyftdi/doc/tools.rst pyftdi-0.54.0/pyftdi/doc/tools.rst --- pyftdi-0.53.3/pyftdi/doc/tools.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/tools.rst 2022-03-13 21:41:38.000000000 +0000 @@ -102,8 +102,9 @@ omitted as they only serve as human-readable aliases for the vendor and product names. See example below. * ``vendor_id`` and ``product_id`` are mandatory strings that should resolve - into 16-bit integers (USB VID and PID values). They may either be expressed - as decimal or hexadecimal syntax. + into 16-bit integers (USB VID and PID values). Integer values are always + interpreted as hexadecimal values, *e.g.* `-P 1234:6789` is parsed as + `-P 0x1234:0x6789`. This option may be repeated as many times as required to add support for several custom devices. diff -Nru pyftdi-0.53.3/pyftdi/doc/urlscheme.rst pyftdi-0.54.0/pyftdi/doc/urlscheme.rst --- pyftdi-0.53.3/pyftdi/doc/urlscheme.rst 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/doc/urlscheme.rst 2022-03-13 21:41:38.000000000 +0000 @@ -65,7 +65,8 @@ You can also ask PyFtdi to enumerate all the compatible devices with the special ``ftdi:///?`` syntax. This syntax is useful to retrieve the available -FTDI URLs with serial number and/or bus:address selectors. +FTDI URLs with serial number and/or bus:address selectors. To avoid conflicts +with some shells such as `zsh`, escape the `?` char as ``ftdi:///\?``. There are several APIs available to enumerate/filter available FTDI device. See :doc:`api/ftdi`. diff -Nru pyftdi-0.53.3/pyftdi/eeprom.py pyftdi-0.54.0/pyftdi/eeprom.py --- pyftdi-0.53.3/pyftdi/eeprom.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/eeprom.py 2022-03-13 21:41:38.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2019-2021, Emmanuel Blot +# Copyright (c) 2019-2022, Emmanuel Blot # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause @@ -24,12 +24,12 @@ 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 +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 to_bool, to_int +from .misc import classproperty, to_bool, to_int class FtdiEepromError(FtdiError): @@ -112,24 +112,37 @@ self.log = getLogger('pyftdi.eeprom') self._ftdi = Ftdi() self._eeprom = bytearray() + self._size = 0 self._dev_ver = 0 self._valid = False self._config = OrderedDict() self._dirty = set() self._modified = False self._chip: Optional[int] = None + self._mirror = False def __getattr__(self, name): if name in self._config: return self._config[name] raise AttributeError('No such attribute: %s' % name) + @classproperty + def eeprom_sizes(cls) -> List[int]: + """Return a list of supported EEPROM sizes. + + :return: the supported EEPROM sizes + """ + return sorted({p.size for p in cls._PROPERTIES.values() if p.size}) + def open(self, device: Union[str, UsbDevice], - ignore: bool = False) -> None: + ignore: bool = False, size: Optional[int] = None, + model: Optional[str] = None) -> None: """Open a new connection to the FTDI USB device. :param device: the device URL or a USB device instance. :param ignore: whether to ignore existing content + :param size: a custom EEPROM size + :param model: the EEPROM model used to specify a custom size """ if self._ftdi.is_connected: raise FtdiError('Already open') @@ -137,6 +150,17 @@ self._ftdi.open_from_url(device) else: self._ftdi.open_from_device(device) + if model and not size: + # 93xxx46/56/66 + mmo = match(r'(?i)^93[a-z]*([456])6.*$', model) + if not mmo: + raise ValueError(f'Unknown EEPROM device: {model}') + mmul = int(mmo.group(1)) + size = 128 << (mmul - 4) + if size: + if size not in self.eeprom_sizes: + raise ValueError(f'Unsupported EEPROM size: {size}') + self._size = min(size, 256) if not ignore: self._eeprom = self._read_eeprom() if self._valid: @@ -185,7 +209,21 @@ def size(self) -> int: """Report the EEPROM size. - The physical EEPROM size may be greater. + Use the most common (default) EEPROM size of the size is not yet + known. + + :return: the size in bytes + """ + if not self._size: + self._size = self.default_size + return self._size + + @property + def default_size(self) -> int: + """Report the default EEPROM size based on the FTDI type. + + The physical EEPROM size may be greater or lower, depending on the + actual connected EEPROM device. :return: the size in bytes """ @@ -202,6 +240,22 @@ return eeprom_size @property + def storage_size(self) -> int: + """Report the number of EEPROM bytes that can be used for configuration + storage. The physical EEPROM size may be greater + + :return: the number of bytes in the eeprom that will be used for + configuration storage + """ + try: + eeprom_storage_size = self.size + if self.is_mirroring_enabled: + eeprom_storage_size = self.mirror_sector + except FtdiError as fe: + raise fe + return eeprom_storage_size + + @property def data(self) -> bytes: """Returns the content of the EEPROM. @@ -262,6 +316,52 @@ mask |= 1 << bix return mask + @property + def has_mirroring(self) -> bool: + """Report whether the device supports EEPROM content duplication + across its two sectors. + + :return: True if the device support mirorring + """ + return (self._PROPERTIES[self.device_version].user and + self._ftdi.device_version != 0x1000) + + @property + def mirror_sector(self) -> int: + """Report start address of the mirror sector in the EEPROM. + This is only valid if the FTDI is capable of mirroring EEPROM data. + + :return: the start address + """ + if self.has_mirroring: + return self.size // 2 + raise FtdiError('EEPROM does not support mirroring') + + @property + def is_mirroring_enabled(self) -> bool: + """Check if EEPROM mirroring is currently enabled for this EEPROM. + See enable_mirroring for more details on EEPROM mirroring + functionality + """ + return self.has_mirroring and self._mirror + + 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. + + For example on a 256 byte EEPROM, two 128 byte 'sectors' will be + used to store identical data. Configuration properties/strings will + be writen to both of these sectors. For some devices (like the + 4232H), this makes the PyFtdi EEPROM functionally similar to + FT_PROG. + + Note: Data will only be mirrored if the has_mirroring property + returns true (after establishing a connection to the ftdi) + + :param enable: enable or disable EEPROM mirroring + """ + self._mirror = enable + def save_config(self, file: TextIO) -> None: """Save the EEPROM content as an INI stream. @@ -304,8 +404,11 @@ cfg = ConfigParser() cfg.read_file(file) loaded = False + sections = cfg.sections() + if section not in ('all', None) and section not in sections: + raise FtdiEepromError(f'No such configuration section {section}') sect = 'raw' - if section in (None, 'all', sect, ''): + 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) options = cfg.options(sect) @@ -333,7 +436,7 @@ 'product': 'product_name', 'serial': 'serial_number' } - if section in (None, 'all', sect, ''): + 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) options = cfg.options(sect) @@ -395,6 +498,7 @@ mobj = match(r'group_(\d)_(drive|schmitt|slow_slew)', name) if mobj: self._set_group(int(mobj.group(1)), mobj.group(2), value, out) + self._dirty.add(name) return confs = { 'remote_wakeup': (0, 5), @@ -420,19 +524,41 @@ raise ValueError('Invalid value for %s' % name) offset = hwords[name] self._eeprom[offset:offset+2] = spack('> 1 - self._eeprom[0x09] = val + idx = 0x09 + self._eeprom[idx] = val + if self.is_mirroring_enabled: + # duplicate in 'sector 2' + idx2 = self.mirror_sector + idx + self._eeprom[idx2] = val + self._dirty.add(name) return if name.startswith('invert_'): if not self.device_version in (0x600, 0x1000): @@ -457,9 +583,12 @@ return raise ValueError(f"unknown property: {name}") - def erase(self) -> None: - """Erase the whole EEPROM.""" - self._eeprom = bytearray([0xFF] * self.size) + def erase(self, erase_byte: Optional[int] = 0xFF) -> None: + """Erase the whole EEPROM. + + :param erase_byte: Optional erase byte to use. Default to 0xFF + """ + self._eeprom = bytearray([erase_byte] * self.size) self._config.clear() self._dirty.add('eeprom') @@ -498,15 +627,18 @@ for name, value in self._config.items(): print('%s: %s' % (name, value), file=file or sys.stdout) - def commit(self, dry_run: bool = True) -> bool: + def commit(self, dry_run: bool = True, no_crc: bool = False) -> bool: """Commit any changes to the EEPROM. - :param dry_run: log what should be written, do not actually - change the EEPROM content + :param dry_run: log what should be written, do not actually change + 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. :return: True if some changes have been committed to the EEPROM """ - self._sync_eeprom() + self._sync_eeprom(no_crc) if not self._modified: self.log.warning('No change to commit, EEPROM not modified') return False @@ -547,10 +679,20 @@ self._dirty.add(name) def _generate_var_strings(self, fill=True) -> None: + """ + :param fill: fill the remainder of the space after the var strings + with 0s + """ stream = bytearray() dynpos = self._PROPERTIES[self.device_version].dynoff + if dynpos > self._size: + # 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 for name in self.VAR_STRINGS: try: ustr = self._config[name].encode('utf-16le') @@ -561,27 +703,43 @@ stream.append(0x03) # string descriptor stream.extend(ustr) self._eeprom[tbl_pos] = data_pos + if self.is_mirroring_enabled: + self._eeprom[tbl_sector2_pos] = data_pos tbl_pos += 1 + tbl_sector2_pos += 1 self._eeprom[tbl_pos] = length + if self.is_mirroring_enabled: + self._eeprom[tbl_sector2_pos] = length tbl_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 self._eeprom[dynpos:dynpos+len(stream)] = stream + mtp = self._ftdi.device_version == 0x1000 + crc_pos = 0x100 if mtp else self._size + rem = crc_pos - (dynpos + len(stream)) + if rem < 0: + oversize = (-rem + 2) // 2 + raise FtdiEepromError(f'Cannot fit strings into EEPROM, ' + f'{oversize} oversize characters') if fill: - mtp = self._ftdi.device_version == 0x1000 - crc_pos = 0x100 if mtp else len(self._eeprom) - rem = crc_pos - (dynpos + len(stream)) self._eeprom[dynpos+len(stream):crc_pos] = bytes(rem) + if self.is_mirroring_enabled: + crc_s1_pos = self.mirror_sector + self._eeprom[s1_vstr_start+len(stream):crc_s1_pos] = bytes(rem) - def _sync_eeprom(self): + def _sync_eeprom(self, no_crc: bool = False): if not self._dirty: self.log.debug('No change detected for EEPROM content') return - 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) - self._update_crc() - self._decode_eeprom() + if not no_crc: + 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) + self._update_crc() + self._decode_eeprom() self._dirty.clear() self._modified = True self.log.debug('EEPROM content regenerated (not yet committed)') @@ -593,7 +751,16 @@ if not check: # check mode: add CRC itself, so that result should be zero crc_pos -= crc_size - crc = self._ftdi.calc_eeprom_checksum(eeprom[:crc_pos]) + if self.is_mirroring_enabled: + mirror_s1_crc_pos = self.mirror_sector + if not check: + mirror_s1_crc_pos -= crc_size + # if mirroring, only calculate the crc for the first sector/half + # of the eeprom. Data (including this crc) are duplicated in + # the second sector/half + crc = self._ftdi.calc_eeprom_checksum(eeprom[:mirror_s1_crc_pos]) + else: + crc = self._ftdi.calc_eeprom_checksum(eeprom[:crc_pos]) if check: self._valid = not bool(crc) if not self._valid: @@ -603,25 +770,36 @@ return crc, crc_pos, crc_size def _update_crc(self): - crc, crc_pos, crc_size = self._compute_crc(self._eeprom, False) + crc, crc_pos, crc_size = self._compute_crc( + self._eeprom, False) self._eeprom[crc_pos:crc_pos+crc_size] = spack(' int: + if self.is_mirroring_enabled: + # if mirroring calculate where the CRC will start in first sector + crc_s1_start = self.mirror_sector - crc_size + self._eeprom[crc_s1_start:crc_s1_start+crc_size] = spack(' Tuple[int, bool]: + """ + :return: Tuple of: + - int of usable size of the eeprom + - bool of whether eeprom mirroring was detected or not + """ if self._ftdi.is_eeprom_internal: - return self._ftdi.max_eeprom_size + return self._ftdi.max_eeprom_size, False if all([x == 0xFF for x in eeprom]): # erased EEPROM, size is unknown - return self._ftdi.max_eeprom_size + return self._ftdi.max_eeprom_size, False if eeprom[0:0x80] == eeprom[0x80:0x100]: - return 0x80 + return 0x80, True if eeprom[0:0x40] == eeprom[0x40:0x80]: - return 0x40 - return 0x100 + return 0x40, True + return 0x100, False def _read_eeprom(self) -> bytes: - buf = self._ftdi.read_eeprom(0) + buf = self._ftdi.read_eeprom(0, eeprom_size=self.size) eeprom = bytearray(buf) - size = self._compute_size(eeprom) + size, mirror_detected = self._compute_size(eeprom) if size < len(eeprom): eeprom = eeprom[:size] crc = self._compute_crc(eeprom, True)[0] @@ -630,6 +808,10 @@ self.log.info('No EEPROM or EEPROM erased') 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._mirror = True return eeprom def _decode_eeprom(self): @@ -752,10 +934,11 @@ # for now, only support FT-X devices raise ValueError('Bus control not implemented for this device') - def _set_group(self, group: int, value: Union[str, int, bool], - out: Optional[TextIO]) -> None: + def _set_group(self, group: int, control: str, + value: Union[str, int, bool], out: Optional[TextIO]) \ + -> None: if self.device_version in (0x0700, 0x0800, 0x0900): - self._set_group_x232h(group, value, out) + self._set_group_x232h(group, control, value, out) return raise ValueError('Group not implemented for this device') diff -Nru pyftdi-0.53.3/pyftdi/ftdi.py pyftdi-0.54.0/pyftdi/ftdi.py --- pyftdi-0.53.3/pyftdi/ftdi.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/ftdi.py 2022-03-13 21:41:38.000000000 +0000 @@ -164,16 +164,12 @@ READ_BITS_NVE_LSB = 0x2e RW_BYTES_PVE_NVE_MSB = 0x31 RW_BYTES_NVE_PVE_MSB = 0x34 - RW_BITS_PVE_PVE_MSB = 0x32 RW_BITS_PVE_NVE_MSB = 0x33 RW_BITS_NVE_PVE_MSB = 0x36 - RW_BITS_NVE_NVE_MSB = 0x37 RW_BYTES_PVE_NVE_LSB = 0x39 RW_BYTES_NVE_PVE_LSB = 0x3c - RW_BITS_PVE_PVE_LSB = 0x3a RW_BITS_PVE_NVE_LSB = 0x3b RW_BITS_NVE_PVE_LSB = 0x3e - RW_BITS_NVE_NVE_LSB = 0x3f WRITE_BITS_TMS_PVE = 0x4a WRITE_BITS_TMS_NVE = 0x4b RW_BITS_TMS_PVE_PVE = 0x6a @@ -1289,7 +1285,6 @@ try: self.set_bitmode(outv, Ftdi.BitMode.CBUS) inv = self.read_pins() - #print(f'BM {outv:04b} {inv:04b}') finally: if oldmode != self._bitmode: self.set_bitmode(0, oldmode) @@ -1785,7 +1780,6 @@ if offset + write_size > size: write_size = size - offset length = self._write(data[offset:offset+write_size]) - # print('WRITE', offset, size, length) if length <= 0: raise FtdiError("Usb bulk write error") offset += length @@ -2254,7 +2248,7 @@ div |= 0x00020000 value = div & 0xFFFF index = (div >> 16) & 0xFFFF - if self.has_mpsse: + if self.device_version >= 0x0700 or self.device_version == 0x0500: index <<= 8 index |= self._index estimate = int(((8 * clock) + (div8//2))//div8) diff -Nru pyftdi-0.53.3/pyftdi/__init__.py pyftdi-0.54.0/pyftdi/__init__.py --- pyftdi-0.53.3/pyftdi/__init__.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/__init__.py 2022-03-13 21:41:38.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2021 Emmanuel Blot +# Copyright (c) 2010-2022 Emmanuel Blot # Copyright (c) 2010-2016, Neotion # All rights reserved. # @@ -6,7 +6,7 @@ #pylint: disable-msg=missing-docstring -__version__ = '0.53.3' +__version__ = '0.54.0' __title__ = 'PyFtdi' __description__ = 'FTDI device driver (pure Python)' __uri__ = 'http://github.com/eblot/pyftdi' diff -Nru pyftdi-0.53.3/pyftdi/misc.py pyftdi-0.54.0/pyftdi/misc.py --- pyftdi-0.53.3/pyftdi/misc.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/misc.py 2022-03-13 21:41:38.000000000 +0000 @@ -309,6 +309,13 @@ print_stack(_current_frames()[current_thread().ident]) +class classproperty(property): + """Getter property decorator for a class""" + #pylint: disable=invalid-name + def __get__(self, obj: Any, objtype=None) -> Any: + return super().__get__(objtype) + + class EasyDict(dict): """Dictionary whose members can be accessed as instance members """ diff -Nru pyftdi-0.53.3/pyftdi/tests/backend/ftdivirt.py pyftdi-0.54.0/pyftdi/tests/backend/ftdivirt.py --- pyftdi-0.53.3/pyftdi/tests/backend/ftdivirt.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/tests/backend/ftdivirt.py 2022-03-13 21:41:38.000000000 +0000 @@ -583,7 +583,7 @@ FRAC_INV_DIV = (0, 4, 2, 1, 3, 5, 6, 7) BAUDRATE_REF_BASE = 3.0E6 # 3 MHz BAUDRATE_REF_HIGH = 12.0E6 # 12 MHz - if self._parent.is_hispeed_device: + if self._parent.is_hispeed_device or self._parent.is_x_series: wIndex >>= 8 divisor = wValue | (wIndex << 16) div = divisor & 0x3FFF @@ -1028,6 +1028,14 @@ """ return self._version in (0x0700, 0x0800, 0x0900) + @property + def is_x_series(self) -> bool: + """Tell whether the device is a FT-X device + + :return: True for FT-X device + """ + return self._version == 0x1000 + def apply_eeprom_config(self, devdesc: dict, cfgdescs: Sequence[dict]) -> None: self._load_eeprom(devdesc, cfgdescs) diff -Nru pyftdi-0.53.3/pyftdi/tests/eeprom_mock.py pyftdi-0.54.0/pyftdi/tests/eeprom_mock.py --- pyftdi-0.53.3/pyftdi/tests/eeprom_mock.py 1970-01-01 00:00:00.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/tests/eeprom_mock.py 2022-03-13 21:41:38.000000000 +0000 @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019-2021, Emmanuel Blot +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# +# 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 pyftdi import FtdiLogger +from pyftdi.ftdi import Ftdi +from pyftdi.eeprom import FtdiEeprom +from pyftdi.misc import to_bool, hexdump +from pyftdi.ftdi import FtdiError + +VirtLoader = None + + +class FtdiTestCase(TestCase): + """Common features for all tests. + """ + + @classmethod + def setUpClass(cls): + cls.debug = to_bool(environ.get('FTDI_DEBUG', 'off'), permissive=False) + cls.url = environ.get('FTDI_DEVICE', 'ftdi:///1') + cls.loader = None + + @classmethod + def tearDownClass(cls): + if cls.loader: + cls.loader.unload() + + def setUp(self): + pass + + +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 + """ + + # manufacturer name string to use in tests + TEST_MANU_NAME = "MNAME" + TEST_PROD_NAME = "PNAME" + TEST_SN = "SN123" + + @classmethod + def setUpClass(cls): + FtdiTestCase.setUpClass() + if VirtLoader: + cls.loader = VirtLoader() + with open(cls.TEST_CONFIG_FILENAME, 'rb') as yfp: + cls.loader.load(yfp) + if cls.url == 'ftdi:///1': + ftdi = Ftdi() + ftdi.open_from_url(cls.url) + count = ftdi.device_port_count + ftdi.close() + + def test_mirror_properties(self): + """Check FtdiEeprom properties are accurate for a device that can + mirror + """ + # properties should work regardless of if the mirror option is set + # or not + eeprom = FtdiEeprom() + eeprom.open(self.url, ignore=True) + self.assertTrue(eeprom.has_mirroring) + self.assertEqual(eeprom.size // 2, eeprom.mirror_sector) + eeprom.close() + + mirrored_eeprom = FtdiEeprom() + mirrored_eeprom.enable_mirroring(True) + mirrored_eeprom.open(self.url, ignore=True) + self.assertTrue(mirrored_eeprom.has_mirroring) + self.assertEqual(mirrored_eeprom.size // 2, + mirrored_eeprom.mirror_sector) + mirrored_eeprom.close() + + def test_mirror_manufacturer(self): + """Verify manufacturer string is properly duplicated across the 2 + eeprom sectors + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(True) + eeprom.open(self.url, ignore=True) + eeprom.erase() + eeprom.set_manufacturer_name(self.TEST_MANU_NAME) + self._check_for_mirrored_eeprom_contents(eeprom) + + def test_mirror_product(self): + """Verify product string is properly duplicated across the 2 eeprom + sectors + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(True) + eeprom.open(self.url, ignore=True) + eeprom.erase() + eeprom.set_product_name(self.TEST_PROD_NAME) + self._check_for_mirrored_eeprom_contents(eeprom) + + def test_mirror_serial(self): + """Verify serial string is properly duplicated across the 2 eeprom + sectors + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(True) + eeprom.open(self.url, ignore=True) + eeprom.erase() + eeprom.set_serial_number(self.TEST_SN) + self._check_for_mirrored_eeprom_contents(eeprom) + + def test_varstr_combinations(self): + """Verify various combinations of var strings are properly duplicated + across the 2 eeprom sectors + """ + eeprom = FtdiEeprom() + eeprom.enable_mirroring(True) + 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_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_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_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_mirrored_eeprom_contents(eeprom) + + 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 + """ + eeprom = FtdiEeprom() + eeprom.open(self.url, ignore=True) + _, mirrored = eeprom._compute_size([]) + self.assertFalse(mirrored) + test_buf = bytearray(eeprom.size) + sector_mid = eeprom.size // 2 + for ii in range(sector_mid): + test_buf[ii] = ii % 255 + test_buf[sector_mid+ii] = test_buf[ii] + _, mirrored = eeprom._compute_size(bytes(test_buf)) + self.assertTrue(mirrored) + + # change one byte and confirm failure + test_buf[eeprom.size - 2] = test_buf[eeprom.size - 2] - 1 + _, mirrored = eeprom._compute_size(bytes(test_buf)) + self.assertFalse(mirrored) + + def _check_for_mirrored_eeprom_contents(self, eeprom: FtdiEeprom): + """Check that contents of the eeprom is identical over the two + sectors + """ + 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' + + +class EepromMirrorFt230xTestCase(FtdiTestCase): + """Test FTDI eeprom with non-mirroring capabilities to ensure it works as + expected. + """ + TEST_CONFIG_FILENAME = 'pyftdi/tests/resources/ft230x.yaml' + + @classmethod + def setUpClass(cls): + FtdiTestCase.setUpClass() + if VirtLoader: + cls.loader = VirtLoader() + with open(cls.TEST_CONFIG_FILENAME, 'rb') as yfp: + cls.loader.load(yfp) + if cls.url == 'ftdi:///1': + ftdi = Ftdi() + ftdi.open_from_url(cls.url) + count = ftdi.device_port_count + ftdi.close() + + def test_mirror_properties(self): + """Check FtdiEeprom properties are accurate for a device that can not + mirror. + """ + # properties should work regardless of if the mirror option is set + # or not + eeprom = FtdiEeprom() + eeprom.open(self.url, ignore=True) + self.assertFalse(eeprom.has_mirroring) + with self.assertRaises(FtdiError): + 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) + with self.assertRaises(FtdiError): + eeprom.mirror_sector + mirrored_eeprom.close() + + 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. + """ + eeprom = FtdiEeprom() + eeprom.open(self.url, ignore=True) + _, mirrored = eeprom._compute_size([]) + self.assertFalse(mirrored) + eeprom.close() + + eeprom = FtdiEeprom() + eeprom.open(self.url, ignore=False) + _, mirrored = eeprom._compute_size([]) + self.assertFalse(mirrored) + eeprom.close() + + +def suite(): + suite_ = TestSuite() + suite_.addTest(makeSuite(EepromMirrorFt232hTestCase, 'test')) + suite_.addTest(makeSuite(EepromMirrorFt2232hTestCase, 'test')) + suite_.addTest(makeSuite(EepromMirrorFt230xTestCase, 'test')) + return suite_ + + +def virtualize(): + if not to_bool(environ.get('FTDI_VIRTUAL', 'off')): + return + from pyftdi.usbtools import UsbTools + # Force PyUSB to use PyFtdi test framework for USB backends + UsbTools.BACKENDS = ('backend.usbvirt', ) + # Ensure the virtual backend can be found and is loaded + backend = UsbTools.find_backend() + try: + # obtain the loader class associated with the virtual backend + global VirtLoader + VirtLoader = backend.create_loader() + except AttributeError: + raise AssertionError('Cannot load virtual USB backend') + + +def main(): + import doctest + doctest.testmod(modules[__name__]) + debug = to_bool(environ.get('FTDI_DEBUG', 'off')) + if debug: + formatter = logging.Formatter('%(asctime)s.%(msecs)03d %(levelname)-7s' + ' %(name)-20s [%(lineno)4d] %(message)s', + '%H:%M:%S') + else: + formatter = logging.Formatter('%(message)s') + level = environ.get('FTDI_LOGLEVEL', 'warning').upper() + try: + loglevel = getattr(logging, level) + except AttributeError: + raise ValueError(f'Invalid log level: {level}') + FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) + FtdiLogger.set_level(loglevel) + FtdiLogger.set_formatter(formatter) + virtualize() + try: + ut_main(defaultTest='suite') + except KeyboardInterrupt: + pass + + +if __name__ == '__main__': + # Useful environment variables: + # FTDI_DEVICE: a specific FTDI URL, default to ftdi:///1 + # FTDI_LOGLEVEL: a Logger debug level, to define log verbosity + # FTDI_DEBUG: to enable/disable debug mode + # FTDI_VIRTUAL: to use a virtual device rather than a physical device + main() diff -Nru pyftdi-0.53.3/pyftdi/tests/toolsimport.py pyftdi-0.54.0/pyftdi/tests/toolsimport.py --- pyftdi-0.53.3/pyftdi/tests/toolsimport.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/pyftdi/tests/toolsimport.py 2022-03-13 21:41:38.000000000 +0000 @@ -23,7 +23,7 @@ This is especially useful to find Python syntax version mismatch and other not-yet-supported modules/features. - PyFtdi and tools should support Python 3.6 onwards. + PyFtdi and tools should support Python 3.7 onwards. """ @classmethod diff -Nru pyftdi-0.53.3/README.md pyftdi-0.54.0/README.md --- pyftdi-0.53.3/README.md 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/README.md 2022-03-13 21:41:38.000000000 +0000 @@ -61,7 +61,7 @@ ### Python support -PyFtdi requires Python 3.6+. +PyFtdi requires Python 3.7+. ### API break diff -Nru pyftdi-0.53.3/setup.py pyftdi-0.54.0/setup.py --- pyftdi-0.53.3/setup.py 2021-08-23 07:52:29.000000000 +0000 +++ pyftdi-0.54.0/setup.py 2022-03-13 21:41:38.000000000 +0000 @@ -38,10 +38,10 @@ 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: POSIX', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: System :: Hardware :: Hardware Drivers', ] @@ -188,7 +188,7 @@ classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, test_requires=TEST_REQUIRES, - python_requires='>=3.6', + python_requires='>=3.7', )