MikrofonSensor und TemperaturSenor die zwei Python programme funktionieren. mit den jeweiligen 2 json Datein. Beim TemperaturSensor wird im Terminal keine Wertre ausgegeben aber in der json Datei kann man die Temp und Hum sehen.
This commit is contained in:
parent
4c654ec969
commit
1751076592
2614 changed files with 349009 additions and 0 deletions
1
lib/python3.11/site-packages/pyftdi/INSTALL
Normal file
1
lib/python3.11/site-packages/pyftdi/INSTALL
Normal file
|
@ -0,0 +1 @@
|
|||
Please read pyftdi/README.rst for installation instructions.
|
42
lib/python3.11/site-packages/pyftdi/__init__.py
Normal file
42
lib/python3.11/site-packages/pyftdi/__init__.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2010-2016, Neotion
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
__version__ = '0.56.0'
|
||||
__title__ = 'PyFtdi'
|
||||
__description__ = 'FTDI device driver (pure Python)'
|
||||
__uri__ = 'http://github.com/eblot/pyftdi'
|
||||
__doc__ = __description__ + ' <' + __uri__ + '>'
|
||||
__author__ = 'Emmanuel Blot'
|
||||
# For all support requests, please open a new issue on GitHub
|
||||
__email__ = 'emmanuel.blot@free.fr'
|
||||
__license__ = 'Modified BSD'
|
||||
__copyright__ = 'Copyright (c) 2011-2024 Emmanuel Blot'
|
||||
|
||||
|
||||
from logging import WARNING, NullHandler, getLogger
|
||||
|
||||
|
||||
class FtdiLogger:
|
||||
|
||||
log = getLogger('pyftdi')
|
||||
log.addHandler(NullHandler())
|
||||
log.setLevel(level=WARNING)
|
||||
|
||||
@classmethod
|
||||
def set_formatter(cls, formatter):
|
||||
handlers = list(cls.log.handlers)
|
||||
for handler in handlers:
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
@classmethod
|
||||
def get_level(cls):
|
||||
return cls.log.getEffectiveLevel()
|
||||
|
||||
@classmethod
|
||||
def set_level(cls, level):
|
||||
cls.log.setLevel(level=level)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
534
lib/python3.11/site-packages/pyftdi/bits.py
Normal file
534
lib/python3.11/site-packages/pyftdi/bits.py
Normal file
|
@ -0,0 +1,534 @@
|
|||
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2008-2016, Neotion
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Bit field and sequence management."""
|
||||
|
||||
from typing import Iterable, List, Optional, Tuple, Union
|
||||
from .misc import is_iterable, xor
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
# pylint: disable=unneeded-not
|
||||
# pylint: disable=duplicate-key
|
||||
|
||||
|
||||
class BitSequenceError(Exception):
|
||||
"""Bit sequence error"""
|
||||
|
||||
|
||||
class BitSequence:
|
||||
"""Bit sequence.
|
||||
|
||||
Support most of the common bit operations: or, and, shift, comparison,
|
||||
and conversion from and to integral values.
|
||||
|
||||
Bit sequence objects are iterable.
|
||||
|
||||
Can be initialized with another bit sequence, a integral value,
|
||||
a sequence of bytes or an iterable of common boolean values.
|
||||
|
||||
:param value: initial value
|
||||
:param msb: most significant bit first or not
|
||||
:param length: count of signficant bits in the bit sequence
|
||||
:param bytes_: initial value specified as a sequence of bytes
|
||||
:param msby: most significant byte first or not
|
||||
"""
|
||||
|
||||
def __init__(self, value: Union['BitSequence', str, int] = None,
|
||||
msb: bool = False, length: int = 0,
|
||||
bytes_: Optional[bytes] = None, msby: bool = True):
|
||||
"""Instantiate a new bit sequence.
|
||||
"""
|
||||
self._seq = bytearray()
|
||||
seq = self._seq
|
||||
if value and bytes_:
|
||||
raise BitSequenceError("Cannot inialize with both a value and "
|
||||
"bytes")
|
||||
if bytes_:
|
||||
provider = list(bytes_).__iter__() if msby else reversed(bytes_)
|
||||
for byte in provider:
|
||||
if isinstance(byte, str):
|
||||
byte = ord(byte)
|
||||
elif byte > 0xff:
|
||||
raise BitSequenceError("Invalid byte value")
|
||||
b = []
|
||||
for _ in range(8):
|
||||
b.append(bool(byte & 0x1))
|
||||
byte >>= 1
|
||||
if msb:
|
||||
b.reverse()
|
||||
seq.extend(b)
|
||||
else:
|
||||
value = self._tomutable(value)
|
||||
if isinstance(value, int):
|
||||
self._init_from_integer(value, msb, length)
|
||||
elif isinstance(value, BitSequence):
|
||||
self._init_from_sibling(value, msb)
|
||||
elif is_iterable(value):
|
||||
self._init_from_iterable(value, msb)
|
||||
elif value is None:
|
||||
pass
|
||||
else:
|
||||
raise BitSequenceError(f"Cannot initialize from '{type(value)}'")
|
||||
self._update_length(length, msb)
|
||||
|
||||
def sequence(self) -> bytearray:
|
||||
"""Return the internal representation as a new mutable sequence"""
|
||||
return bytearray(self._seq)
|
||||
|
||||
def reverse(self) -> 'BitSequence':
|
||||
"""In-place reverse"""
|
||||
self._seq.reverse()
|
||||
return self
|
||||
|
||||
def invert(self) -> 'BitSequence':
|
||||
"""In-place invert sequence values"""
|
||||
self._seq = bytearray([x ^ 1 for x in self._seq])
|
||||
return self
|
||||
|
||||
def append(self, seq) -> 'BitSequence':
|
||||
"""Concatenate a new BitSequence"""
|
||||
if not isinstance(seq, BitSequence):
|
||||
seq = BitSequence(seq)
|
||||
self._seq.extend(seq.sequence())
|
||||
return self
|
||||
|
||||
def lsr(self, count: int) -> None:
|
||||
"""Left shift rotate"""
|
||||
count %= len(self)
|
||||
self._seq[:] = self._seq[count:] + self._seq[:count]
|
||||
|
||||
def rsr(self, count: int) -> None:
|
||||
"""Right shift rotate"""
|
||||
count %= len(self)
|
||||
self._seq[:] = self._seq[-count:] + self._seq[:-count]
|
||||
|
||||
def tobit(self) -> bool:
|
||||
"""Degenerate the sequence into a single bit, if possible"""
|
||||
if len(self) != 1:
|
||||
raise BitSequenceError("BitSequence should be a scalar")
|
||||
return bool(self._seq[0])
|
||||
|
||||
def tobyte(self, msb: bool = False) -> int:
|
||||
"""Convert the sequence into a single byte value, if possible"""
|
||||
if len(self) > 8:
|
||||
raise BitSequenceError("Cannot fit into a single byte")
|
||||
byte = 0
|
||||
pos = -1 if not msb else 0
|
||||
# copy the sequence
|
||||
seq = self._seq[:]
|
||||
while seq:
|
||||
byte <<= 1
|
||||
byte |= seq.pop(pos)
|
||||
return byte
|
||||
|
||||
def tobytes(self, msb: bool = False, msby: bool = False) -> bytearray:
|
||||
"""Convert the sequence into a sequence of byte values"""
|
||||
blength = (len(self)+7) & (~0x7)
|
||||
sequence = list(self._seq)
|
||||
if not msb:
|
||||
sequence.reverse()
|
||||
bytes_ = bytearray()
|
||||
for pos in range(0, blength, 8):
|
||||
seq = sequence[pos:pos+8]
|
||||
byte = 0
|
||||
while seq:
|
||||
byte <<= 1
|
||||
byte |= seq.pop(0)
|
||||
bytes_.append(byte)
|
||||
if msby:
|
||||
bytes_.reverse()
|
||||
return bytes_
|
||||
|
||||
@staticmethod
|
||||
def _tomutable(value: Union[str, Tuple]) -> List:
|
||||
"""Convert a immutable sequence into a mutable one"""
|
||||
if isinstance(value, tuple):
|
||||
# convert immutable sequence into a list so it can be popped out
|
||||
value = list(value)
|
||||
elif isinstance(value, str):
|
||||
# convert immutable sequence into a list so it can be popped out
|
||||
if value.startswith('0b'):
|
||||
value = list(value[2:])
|
||||
else:
|
||||
value = list(value)
|
||||
return value
|
||||
|
||||
def _init_from_integer(self, value: int, msb: bool, length: int) -> None:
|
||||
"""Initialize from any integer value"""
|
||||
bl = length or -1
|
||||
seq = self._seq
|
||||
while bl:
|
||||
seq.append(bool(value & 1))
|
||||
value >>= 1
|
||||
if not value:
|
||||
break
|
||||
bl -= 1
|
||||
if msb:
|
||||
seq.reverse()
|
||||
|
||||
def _init_from_iterable(self, iterable: Iterable, msb: bool) -> None:
|
||||
"""Initialize from an iterable"""
|
||||
smap = {'0': 0, '1': 1, False: 0, True: 1, 0: 0, 1: 1}
|
||||
seq = self._seq
|
||||
try:
|
||||
if msb:
|
||||
seq.extend([smap[bit] for bit in reversed(iterable)])
|
||||
else:
|
||||
seq.extend([smap[bit] for bit in iterable])
|
||||
except KeyError as exc:
|
||||
raise BitSequenceError('Invalid binary character in initializer') \
|
||||
from exc
|
||||
|
||||
def _init_from_sibling(self, value: 'BitSequence', msb: bool) -> None:
|
||||
"""Initialize from a fellow object"""
|
||||
self._seq = value.sequence()
|
||||
if msb:
|
||||
self._seq.reverse()
|
||||
|
||||
def _update_length(self, length, msb):
|
||||
"""If a specific length is specified, extend the sequence as
|
||||
expected"""
|
||||
if length and (len(self) < length):
|
||||
extra = bytearray([False] * (length-len(self)))
|
||||
if msb:
|
||||
extra.extend(self._seq)
|
||||
self._seq = extra
|
||||
else:
|
||||
self._seq.extend(extra)
|
||||
|
||||
def __iter__(self):
|
||||
return self._seq.__iter__()
|
||||
|
||||
def __reversed__(self):
|
||||
return self._seq.__reversed__()
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, slice):
|
||||
return self.__class__(value=self._seq[index])
|
||||
return self._seq[index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if isinstance(value, BitSequence):
|
||||
if issubclass(value.__class__, self.__class__) and \
|
||||
value.__class__ != self.__class__:
|
||||
raise BitSequenceError("Cannot set item with instance of a "
|
||||
"subclass")
|
||||
if isinstance(index, slice):
|
||||
value = self.__class__(value, length=len(self._seq[index]))
|
||||
self._seq[index] = value.sequence()
|
||||
else:
|
||||
if not isinstance(value, BitSequence):
|
||||
value = self.__class__(value)
|
||||
val = value.tobit()
|
||||
if index > len(self._seq):
|
||||
raise BitSequenceError("Cannot change the sequence size")
|
||||
self._seq[index] = val
|
||||
|
||||
def __len__(self):
|
||||
return len(self._seq)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._cmp(other) == 0
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __le__(self, other):
|
||||
return not self._cmp(other) <= 0
|
||||
|
||||
def __lt__(self, other):
|
||||
return not self._cmp(other) < 0
|
||||
|
||||
def __ge__(self, other):
|
||||
return not self._cmp(other) >= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
return not self._cmp(other) > 0
|
||||
|
||||
def _cmp(self, other):
|
||||
# the bit sequence should be of the same length
|
||||
ld = len(self) - len(other)
|
||||
if ld:
|
||||
return ld
|
||||
for n, (x, y) in enumerate(zip(self._seq, other.sequence()), start=1):
|
||||
if xor(x, y):
|
||||
return n
|
||||
return 0
|
||||
|
||||
def __repr__(self):
|
||||
# cannot use bin() as it truncates the MSB zero bits
|
||||
return ''.join([b and '1' or '0' for b in reversed(self._seq)])
|
||||
|
||||
def __str__(self):
|
||||
chunks = []
|
||||
srepr = repr(self)
|
||||
length = len(self)
|
||||
for i in range(0, length, 8):
|
||||
if i:
|
||||
j = -i
|
||||
else:
|
||||
j = None
|
||||
chunks.append(srepr[-i-8:j])
|
||||
return f'{len(self)}: {" ".join(reversed(chunks))}'
|
||||
|
||||
def __int__(self):
|
||||
value = 0
|
||||
for b in reversed(self._seq):
|
||||
value <<= 1
|
||||
value |= b and 1
|
||||
return value
|
||||
|
||||
def __and__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
raise BitSequenceError('Need a BitSequence to combine')
|
||||
if len(self) != len(other):
|
||||
raise BitSequenceError('Sequences must be the same size')
|
||||
return self.__class__(value=list(map(lambda x, y: x and y,
|
||||
self._seq, other.sequence())))
|
||||
|
||||
def __or__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
raise BitSequenceError('Need a BitSequence to combine')
|
||||
if len(self) != len(other):
|
||||
raise BitSequenceError('Sequences must be the same size')
|
||||
return self.__class__(value=list(map(lambda x, y: x or y,
|
||||
self._seq, other.sequence())))
|
||||
|
||||
def __add__(self, other):
|
||||
return self.__class__(value=self._seq + other.sequence())
|
||||
|
||||
def __ilshift__(self, count):
|
||||
count %= len(self)
|
||||
seq = bytearray([0]*count)
|
||||
seq.extend(self._seq[:-count])
|
||||
self._seq = seq
|
||||
return self
|
||||
|
||||
def __irshift__(self, count):
|
||||
count %= len(self)
|
||||
seq = self._seq[count:]
|
||||
seq.extend([0]*count)
|
||||
self._seq = seq
|
||||
return self
|
||||
|
||||
def inc(self) -> None:
|
||||
"""Increment the sequence"""
|
||||
for p, b in enumerate(self._seq):
|
||||
b ^= True
|
||||
self._seq[p] = b
|
||||
if b:
|
||||
break
|
||||
|
||||
def dec(self) -> None:
|
||||
"""Decrement the sequence"""
|
||||
for p, b in enumerate(self._seq):
|
||||
b ^= True
|
||||
self._seq[p] = b
|
||||
if not b:
|
||||
break
|
||||
|
||||
def invariant(self) -> bool:
|
||||
"""Tells whether all bits of the sequence are of the same value.
|
||||
|
||||
Return the value, or ValueError if the bits are not of the same
|
||||
value
|
||||
"""
|
||||
try:
|
||||
ref = self._seq[0]
|
||||
except IndexError as exc:
|
||||
raise ValueError('Empty sequence') from exc
|
||||
if len(self._seq) == 1:
|
||||
return ref
|
||||
for b in self._seq[1:]:
|
||||
if b != ref:
|
||||
raise ValueError('Bits do no match')
|
||||
return ref
|
||||
|
||||
|
||||
class BitZSequence(BitSequence):
|
||||
"""Tri-state bit sequence manipulation.
|
||||
|
||||
Support most of the BitSequence operations, with an extra high-Z state
|
||||
|
||||
:param value: initial value
|
||||
:param msb: most significant bit first or not
|
||||
:param length: count of signficant bits in the bit sequence
|
||||
"""
|
||||
|
||||
__slots__ = ['_seq']
|
||||
|
||||
Z = 0xff # maximum byte value
|
||||
|
||||
def __init__(self, value=None, msb=False, length=0):
|
||||
BitSequence.__init__(self, value=value, msb=msb, length=length)
|
||||
|
||||
def invert(self):
|
||||
self._seq = [x in (None, BitZSequence.Z) and BitZSequence.Z or x ^ 1
|
||||
for x in self._seq]
|
||||
return self
|
||||
|
||||
def tobyte(self, msb=False):
|
||||
raise BitSequenceError(f'Type {type(self)} cannot be converted to '
|
||||
f'byte')
|
||||
|
||||
def tobytes(self, msb=False, msby=False):
|
||||
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
|
||||
ld = len(self) - len(other)
|
||||
if ld:
|
||||
return ld
|
||||
for (x, y) in zip(self._seq, other.sequence()):
|
||||
if BitZSequence.Z in (x, y):
|
||||
continue
|
||||
if x is not y:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _init_from_iterable(self, iterable, msb):
|
||||
"""Initialize from an iterable"""
|
||||
smap = {'0': 0, '1': 1, 'Z': BitZSequence.Z,
|
||||
False: 0, True: 1, None: BitZSequence.Z,
|
||||
0: 0, 1: 1, BitZSequence.Z: BitZSequence.Z}
|
||||
seq = self._seq
|
||||
try:
|
||||
if msb:
|
||||
seq.extend([smap[bit] for bit in reversed(iterable)])
|
||||
else:
|
||||
seq.extend([smap[bit] for bit in iterable])
|
||||
except KeyError as exc:
|
||||
raise BitSequenceError("Invalid binary character in initializer") \
|
||||
from exc
|
||||
|
||||
def __repr__(self):
|
||||
smap = {False: '0', True: '1', BitZSequence.Z: 'Z'}
|
||||
return ''.join([smap[b] for b in reversed(self._seq)])
|
||||
|
||||
def __int__(self):
|
||||
if BitZSequence.Z in self._seq:
|
||||
raise BitSequenceError("High-Z BitSequence cannot be converted to "
|
||||
"an integral type")
|
||||
return BitSequence.__int__(self)
|
||||
|
||||
def __cmp__(self, other):
|
||||
# the bit sequence should be of the same length
|
||||
ld = len(self) - len(other)
|
||||
if ld:
|
||||
return ld
|
||||
for n, (x, y) in enumerate(zip(self._seq, other.sequence()), start=1):
|
||||
if x is not y:
|
||||
return n
|
||||
return 0
|
||||
|
||||
def __and__(self, other):
|
||||
if not isinstance(self, BitSequence):
|
||||
raise BitSequenceError('Need a BitSequence-compliant object to '
|
||||
'combine')
|
||||
if len(self) != len(other):
|
||||
raise BitSequenceError('Sequences must be the same size')
|
||||
|
||||
def andz(x, y):
|
||||
"""Compute the boolean AND operation for a tri-state boolean"""
|
||||
if BitZSequence.Z in (x, y):
|
||||
return BitZSequence.Z
|
||||
return x and y
|
||||
return self.__class__(
|
||||
value=list(map(andz, self._seq, other.sequence())))
|
||||
|
||||
def __or__(self, other):
|
||||
if not isinstance(self, BitSequence):
|
||||
raise BitSequenceError('Need a BitSequence-compliant object to '
|
||||
'combine')
|
||||
if len(self) != len(other):
|
||||
raise BitSequenceError('Sequences must be the same size')
|
||||
|
||||
def orz(x, y):
|
||||
"""Compute the boolean OR operation for a tri-state boolean"""
|
||||
if BitZSequence.Z in (x, y):
|
||||
return BitZSequence.Z
|
||||
return x or y
|
||||
return self.__class__(value=list(map(orz, self._seq,
|
||||
other.sequence())))
|
||||
|
||||
def __rand__(self, other):
|
||||
return self.__and__(other)
|
||||
|
||||
def __ror__(self, other):
|
||||
return self.__or__(other)
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__class__(value=other) + self
|
||||
|
||||
|
||||
class BitField:
|
||||
"""Bit field class to access and modify an integral value
|
||||
|
||||
Beware the slices does not behave as regular Python slices:
|
||||
bitfield[3:5] means b3..b5, NOT b3..b4 as with regular slices
|
||||
"""
|
||||
|
||||
__slots__ = ['_val']
|
||||
|
||||
def __init__(self, value=0):
|
||||
self._val = value
|
||||
|
||||
def to_seq(self, msb=0, lsb=0):
|
||||
"""Return the BitFiled as a sequence of boolean value"""
|
||||
seq = bytearray()
|
||||
count = 0
|
||||
value = self._val
|
||||
while value:
|
||||
count += 1
|
||||
value >>= 1
|
||||
for x in range(lsb, max(msb, count)):
|
||||
seq.append(bool((self._val >> x) & 1))
|
||||
return tuple(reversed(seq))
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, slice):
|
||||
if index.stop == index.start:
|
||||
return None
|
||||
if index.stop < index.start:
|
||||
offset = index.stop
|
||||
count = index.start-index.stop+1
|
||||
else:
|
||||
offset = index.start
|
||||
count = index.stop-index.start+1
|
||||
mask = (1 << count)-1
|
||||
return (self._val >> offset) & mask
|
||||
return (self._val >> index) & 1
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
if isinstance(index, slice):
|
||||
if index.stop == index.start:
|
||||
return
|
||||
if index.stop < index.start:
|
||||
offset = index.stop
|
||||
count = index.start-index.stop+1
|
||||
else:
|
||||
offset = index.start
|
||||
count = index.stop-index.start+1
|
||||
mask = (1 << count)-1
|
||||
value = (value & mask) << offset
|
||||
mask <<= offset
|
||||
self._val = (self._val & ~mask) | value
|
||||
else:
|
||||
if isinstance(value, bool):
|
||||
value = int(value)
|
||||
value = (value & int(1)) << index
|
||||
mask = int(1) << index
|
||||
self._val = (self._val & ~mask) | value
|
||||
|
||||
def __int__(self):
|
||||
return self._val
|
||||
|
||||
def __str__(self):
|
||||
return bin(self._val)
|
69
lib/python3.11/site-packages/pyftdi/doc/api/eeprom.rst
Normal file
69
lib/python3.11/site-packages/pyftdi/doc/api/eeprom.rst
Normal file
|
@ -0,0 +1,69 @@
|
|||
|
||||
.. include:: ../defs.rst
|
||||
|
||||
:mod:`eeprom` - EEPROM API
|
||||
--------------------------
|
||||
|
||||
.. module :: pyftdi.eeprom
|
||||
|
||||
|
||||
Quickstart
|
||||
~~~~~~~~~~
|
||||
|
||||
Example: dump the EEPROM content
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Instantiate an EEPROM manager
|
||||
eeprom = FtdiEeprom()
|
||||
|
||||
# Select the FTDI device to access (the interface is mandatory but any
|
||||
# valid interface for the device fits)
|
||||
eeprom.open('ftdi://ftdi:2232h/1')
|
||||
|
||||
# Show the EEPROM content
|
||||
eeprom.dump_config()
|
||||
|
||||
# Show the raw EEPROM content
|
||||
from pyftdi.misc import hexdump
|
||||
print(hexdump(eeprom.data))
|
||||
|
||||
|
||||
Example: update the serial number
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Instantiate an EEPROM manager
|
||||
eeprom = FtdiEeprom()
|
||||
|
||||
# Select the FTDI device to access
|
||||
eeprom.open('ftdi://ftdi:2232h/1')
|
||||
|
||||
# Change the serial number
|
||||
eeprom.set_serial_number('123456')
|
||||
|
||||
# Commit the change to the EEPROM
|
||||
eeprom.commit(dry_run=False)
|
||||
|
||||
|
||||
Classes
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass :: FtdiEeprom
|
||||
:members:
|
||||
|
||||
|
||||
Exceptions
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoexception :: FtdiEepromError
|
||||
|
||||
|
||||
Tests
|
||||
~~~~~
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# optional: specify an alternative FTDI device
|
||||
export FTDI_DEVICE=ftdi://ftdi:2232h/1
|
||||
PYTHONPATH=. python3 pyftdi/tests/eeprom.py
|
26
lib/python3.11/site-packages/pyftdi/doc/api/ftdi.rst
Normal file
26
lib/python3.11/site-packages/pyftdi/doc/api/ftdi.rst
Normal file
|
@ -0,0 +1,26 @@
|
|||
.. -*- coding: utf-8 -*-
|
||||
|
||||
.. include:: ../defs.rst
|
||||
|
||||
:mod:`ftdi` - FTDI low-level driver
|
||||
-----------------------------------
|
||||
|
||||
.. module :: pyftdi.ftdi
|
||||
|
||||
This module implements access to the low level FTDI hardware. There are very
|
||||
few reasons to use this module directly. Most of PyFtdi_ features are available
|
||||
through the dedicated :doc:`APIs <index>`.
|
||||
|
||||
Classes
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass :: Ftdi
|
||||
:members:
|
||||
|
||||
|
||||
Exceptions
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoexception :: FtdiError
|
||||
.. autoexception :: FtdiMpsseError
|
||||
.. autoexception :: FtdiFeatureError
|
56
lib/python3.11/site-packages/pyftdi/doc/api/gpio.rst
Normal file
56
lib/python3.11/site-packages/pyftdi/doc/api/gpio.rst
Normal file
|
@ -0,0 +1,56 @@
|
|||
.. -*- coding: utf-8 -*-
|
||||
|
||||
.. include:: ../defs.rst
|
||||
|
||||
:mod:`gpio` - GPIO API
|
||||
----------------------
|
||||
|
||||
.. module :: pyftdi.gpio
|
||||
|
||||
Direct drive GPIO pins of FTDI device.
|
||||
|
||||
.. note::
|
||||
|
||||
This mode is mutually exclusive with advanced serial MPSSE features, such as
|
||||
|I2C|, SPI, JTAG, ...
|
||||
|
||||
If you need to use GPIO pins and MPSSE interface on the same port, you need
|
||||
to use the dedicated API. This shared mode is supported with the
|
||||
:doc:`SPI API <spi>` and the :doc:`I2C API <i2c>`.
|
||||
|
||||
.. warning::
|
||||
|
||||
This API does not provide access to the special CBUS port of FT232R, FT232H,
|
||||
FT230X and FT231X devices. See :ref:`cbus_gpio` for details.
|
||||
|
||||
Quickstart
|
||||
~~~~~~~~~~
|
||||
|
||||
See ``tests/gpio.py`` example
|
||||
|
||||
|
||||
Classes
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass :: GpioPort
|
||||
|
||||
.. autoclass :: GpioAsyncController
|
||||
:members:
|
||||
|
||||
.. autoclass :: GpioSyncController
|
||||
:members:
|
||||
|
||||
.. autoclass :: GpioMpsseController
|
||||
:members:
|
||||
|
||||
|
||||
Exceptions
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoexception :: GpioException
|
||||
|
||||
|
||||
Info about GPIO API
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
See :doc:`../gpio` for details
|
191
lib/python3.11/site-packages/pyftdi/doc/api/i2c.rst
Normal file
191
lib/python3.11/site-packages/pyftdi/doc/api/i2c.rst
Normal file
|
@ -0,0 +1,191 @@
|
|||
|
||||
.. include:: ../defs.rst
|
||||
|
||||
:mod:`i2c` - |I2C| API
|
||||
----------------------
|
||||
|
||||
.. module :: pyftdi.i2c
|
||||
|
||||
|
||||
Quickstart
|
||||
~~~~~~~~~~
|
||||
|
||||
Example: communication with an |I2C| GPIO expander
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Instantiate an I2C controller
|
||||
i2c = I2cController()
|
||||
|
||||
# Configure the first interface (IF/1) of the FTDI device as an I2C master
|
||||
i2c.configure('ftdi://ftdi:2232h/1')
|
||||
|
||||
# Get a port to an I2C slave device
|
||||
slave = i2c.get_port(0x21)
|
||||
|
||||
# Send one byte, then receive one byte
|
||||
slave.exchange([0x04], 1)
|
||||
|
||||
# Write a register to the I2C slave
|
||||
slave.write_to(0x06, b'\x00')
|
||||
|
||||
# Read a register from the I2C slave
|
||||
slave.read_from(0x00, 1)
|
||||
|
||||
Example: mastering the |I2C| bus with a complex transaction
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from time import sleep
|
||||
|
||||
port = I2cController().get_port(0x56)
|
||||
|
||||
# emit a START sequence is read address, but read no data and keep the bus
|
||||
# busy
|
||||
port.read(0, relax=False)
|
||||
|
||||
# wait for ~1ms
|
||||
sleep(0.001)
|
||||
|
||||
# write 4 bytes, without neither emitting the start or stop sequence
|
||||
port.write(b'\x00\x01', relax=False, start=False)
|
||||
|
||||
# read 4 bytes, without emitting the start sequence, and release the bus
|
||||
port.read(4, start=False)
|
||||
|
||||
See also pyi2cflash_ module and ``tests/i2c.py``, which provide more detailed
|
||||
examples on how to use the |I2C| API.
|
||||
|
||||
|
||||
Classes
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass :: I2cPort
|
||||
:members:
|
||||
|
||||
.. autoclass :: I2cGpioPort
|
||||
:members:
|
||||
|
||||
.. autoclass :: I2cController
|
||||
:members:
|
||||
|
||||
|
||||
Exceptions
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoexception :: I2cIOError
|
||||
.. autoexception :: I2cNackError
|
||||
.. autoexception:: I2cTimeoutError
|
||||
|
||||
|
||||
GPIOs
|
||||
~~~~~
|
||||
|
||||
See :doc:`../gpio` for details
|
||||
|
||||
Tests
|
||||
~~~~~
|
||||
|
||||
|I2C| sample tests expect:
|
||||
* TCA9555 device on slave address 0x21
|
||||
* ADXL345 device on slave address 0x53
|
||||
|
||||
Checkout a fresh copy from PyFtdi_ github repository.
|
||||
|
||||
See :doc:`../pinout` for FTDI wiring.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# optional: specify an alternative FTDI device
|
||||
export FTDI_DEVICE=ftdi://ftdi:2232h/1
|
||||
# optional: increase log level
|
||||
export FTDI_LOGLEVEL=DEBUG
|
||||
# be sure to connect the appropriate I2C slaves to the FTDI I2C bus and run
|
||||
PYTHONPATH=. python3 pyftdi/tests/i2c.py
|
||||
|
||||
|
||||
.. _i2c_limitations:
|
||||
|
||||
Caveats
|
||||
~~~~~~~
|
||||
|
||||
Open-collector bus
|
||||
``````````````````
|
||||
|
||||
|I2C| uses only two bidirectional open collector (or open drain) lines, pulled
|
||||
up with resistors. These resistors are also required on an |I2C| bus when an
|
||||
FTDI master is used.
|
||||
|
||||
However, most FTDI devices do not use open collector outputs. Some software
|
||||
tricks are used to fake open collector mode when possible, for example to
|
||||
sample for slave ACK/NACK, but most communication (R/W, addressing, data)
|
||||
cannot use open collector mode. This means that most FTDI devices source
|
||||
current to the SCL and SDA lines. FTDI HW is able to cope with conflicting
|
||||
signalling, where FTDI HW forces a line the high logical level while a slave
|
||||
forces it to the low logical level, and limits the sourced current. You may
|
||||
want to check your schematics if the slave is not able to handle 4 .. 16 mA
|
||||
input current in SCL and SDA, for example. The maximal source current depends
|
||||
on the FTDI device and the attached EEPROM configuration which may be used to
|
||||
limit further down the sourced current.
|
||||
|
||||
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, FT4232H and FT4232HA do not support open
|
||||
collector mode, and source current to SCL and SDA lines.
|
||||
|
||||
Clock streching
|
||||
```````````````
|
||||
|
||||
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, 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
|
||||
`````
|
||||
|
||||
Due to the FTDI MPSSE engine limitations, the actual bitrate for write
|
||||
operations over I2C is very slow. As the I2C protocol enforces that each I2C
|
||||
exchanged byte needs to be acknowledged by the peer, a I2C byte cannot be
|
||||
written to the slave before the previous byte has been acknowledged by the
|
||||
slave and read back by the I2C master, that is the host. This requires several
|
||||
USB transfer for each byte, on top of each latency of the USB stack may add up.
|
||||
With the introduction of PyFtdi_ v0.51, read operations have been optimized so
|
||||
that long read operations are now much faster thanwith previous PyFtdi_
|
||||
versions, and exhibits far shorter latencies.
|
||||
|
||||
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.
|
||||
|
||||
.. _i2c_wiring:
|
||||
|
||||
Wiring
|
||||
~~~~~~
|
||||
|
||||
.. figure:: ../images/i2c_wiring.png
|
||||
:scale: 50 %
|
||||
:alt: I2C wiring
|
||||
:align: right
|
||||
|
||||
Fig.1: FT2232H with clock stretching
|
||||
|
||||
* ``AD0`` should be connected to the SCL bus
|
||||
* ``AD1`` and ``AD2`` should be both connected to the SDA bus
|
||||
* ``AD7`` should be connected to the SCL bus, if clock streching is required
|
||||
* remaining pins can be freely used as regular GPIOs.
|
||||
|
||||
*Fig.1*:
|
||||
|
||||
* ``D1`` is only required when clock streching is used along with
|
||||
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.
|
20
lib/python3.11/site-packages/pyftdi/doc/api/index.rst
Normal file
20
lib/python3.11/site-packages/pyftdi/doc/api/index.rst
Normal file
|
@ -0,0 +1,20 @@
|
|||
API documentation
|
||||
=================
|
||||
|
||||
.. include:: ../defs.rst
|
||||
|
||||
|release|
|
||||
---------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
ftdi
|
||||
gpio
|
||||
i2c
|
||||
spi
|
||||
uart
|
||||
usbtools
|
||||
misc
|
||||
eeprom
|
11
lib/python3.11/site-packages/pyftdi/doc/api/misc.rst
Normal file
11
lib/python3.11/site-packages/pyftdi/doc/api/misc.rst
Normal file
|
@ -0,0 +1,11 @@
|
|||
.. -*- coding: utf-8 -*-
|
||||
|
||||
:mod:`misc` - Miscellaneous helpers
|
||||
-----------------------------------
|
||||
|
||||
Functions
|
||||
~~~~~~~~~
|
||||
|
||||
.. automodule:: pyftdi.misc
|
||||
:members:
|
||||
|
203
lib/python3.11/site-packages/pyftdi/doc/api/spi.rst
Normal file
203
lib/python3.11/site-packages/pyftdi/doc/api/spi.rst
Normal file
|
@ -0,0 +1,203 @@
|
|||
.. include:: ../defs.rst
|
||||
|
||||
:mod:`spi` - SPI API
|
||||
--------------------
|
||||
|
||||
.. module :: pyftdi.spi
|
||||
|
||||
Quickstart
|
||||
~~~~~~~~~~
|
||||
|
||||
Example: communication with a SPI data flash (half-duplex example)
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Instantiate a SPI controller
|
||||
spi = SpiController()
|
||||
|
||||
# Configure the first interface (IF/1) of the FTDI device as a SPI master
|
||||
spi.configure('ftdi://ftdi:2232h/1')
|
||||
|
||||
# Get a 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)
|
||||
|
||||
# Request the JEDEC ID from the SPI slave
|
||||
jedec_id = slave.exchange([0x9f], 3)
|
||||
|
||||
|
||||
Example: communication with a remote SPI device using full-duplex mode
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Instantiate a SPI controller
|
||||
# We need want to use A*BUS4 for /CS, so at least 2 /CS lines should be
|
||||
# reserved for SPI, the remaining IO are available as GPIOs.
|
||||
spi = SpiController(cs_count=2)
|
||||
|
||||
# Configure the first interface (IF/1) of the FTDI device as a SPI master
|
||||
spi.configure('ftdi://ftdi:2232h/1')
|
||||
|
||||
# Get a port to a SPI slave w/ /CS on A*BUS4 and SPI mode 2 @ 10MHz
|
||||
slave = spi.get_port(cs=1, freq=10E6, mode=2)
|
||||
|
||||
# Synchronous exchange with the remote SPI slave
|
||||
write_buf = b'\x01\x02\x03'
|
||||
read_buf = slave.exchange(write_buf, duplex=True)
|
||||
|
||||
Example: communication with a SPI device and an extra GPIO
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Instantiate a SPI controller
|
||||
spi = SpiController()
|
||||
|
||||
# Configure the first interface (IF/1) of the first FTDI device as a
|
||||
# SPI master
|
||||
spi.configure('ftdi://::/1')
|
||||
|
||||
# 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
|
||||
gpio = spi.get_gpio()
|
||||
gpio.set_direction(0x30, 0x10)
|
||||
|
||||
# Assert GPO pin
|
||||
gpio.write(0x10)
|
||||
# Write to SPI slace
|
||||
slave.write(b'hello world!')
|
||||
# Release GPO pin
|
||||
gpio.write(0x00)
|
||||
# Test GPI pin
|
||||
pin = bool(gpio.read() & 0x20)
|
||||
|
||||
|
||||
Example: managing non-byte aligned transfers
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Instantiate a SPI controller
|
||||
spi = SpiController()
|
||||
|
||||
# Configure the first interface (IF/1) of the first FTDI device as a
|
||||
# SPI master
|
||||
spi.configure('ftdi://::/1')
|
||||
|
||||
# Get a SPI port to a SPI slave w/ /CS on A*BUS3
|
||||
slave = spi.get_port(cs=0)
|
||||
|
||||
# write 6 first bits of a byte buffer
|
||||
slave.write(b'\xff', droptail=2)
|
||||
|
||||
# read only 13 bits from a slave (13 clock cycles)
|
||||
# only the 5 MSBs of the last byte are valid, 3 LSBs are force to zero
|
||||
slave.read(2, droptail=3)
|
||||
|
||||
See also pyspiflash_ module and ``tests/spi.py``, which provide more detailed
|
||||
examples on how to use the SPI API.
|
||||
|
||||
|
||||
Classes
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass :: SpiPort
|
||||
:members:
|
||||
|
||||
.. autoclass :: SpiGpioPort
|
||||
:members:
|
||||
|
||||
.. autoclass :: SpiController
|
||||
:members:
|
||||
|
||||
Exceptions
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoexception :: SpiIOError
|
||||
|
||||
|
||||
GPIOs
|
||||
~~~~~
|
||||
|
||||
See :doc:`../gpio` for details
|
||||
|
||||
Tests
|
||||
~~~~~
|
||||
|
||||
SPI sample tests expect:
|
||||
* MX25L1606E device on /CS 0, SPI mode 0
|
||||
* ADXL345 device on /CS 1, SPI mode 2
|
||||
* RFDA2125 device on /CS 2, SPI mode 0
|
||||
|
||||
Checkout a fresh copy from PyFtdi_ github repository.
|
||||
|
||||
See :doc:`../pinout` for FTDI wiring.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# optional: specify an alternative FTDI device
|
||||
export FTDI_DEVICE=ftdi://ftdi:2232h/1
|
||||
# optional: increase log level
|
||||
export FTDI_LOGLEVEL=DEBUG
|
||||
# be sure to connect the appropriate SPI slaves to the FTDI SPI bus and run
|
||||
PYTHONPATH=. python3 pyftdi/tests/spi.py
|
||||
|
||||
.. _spi_limitations:
|
||||
|
||||
Limitations
|
||||
~~~~~~~~~~~
|
||||
|
||||
SPI Modes 1 & 3
|
||||
```````````````
|
||||
|
||||
FTDI hardware does not support cpha=1 (mode 1 and mode 3). As stated in
|
||||
Application Node 114:
|
||||
|
||||
"*It is recommended that designers review the SPI Slave
|
||||
data sheet to determine the SPI mode implementation. FTDI device can only
|
||||
support mode 0 and mode 2 due to the limitation of MPSSE engine.*".
|
||||
|
||||
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, 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
|
||||
consequence of this workaround is that SCLK duty cycle is not longer 50% but
|
||||
25% (mode 1) or 75% (mode 3). Again, support for mode 1 and mode 3 should be
|
||||
considered as a kludge, you've been warned.
|
||||
|
||||
Time-sensitive usage
|
||||
````````````````````
|
||||
|
||||
Due to the MPSSE engine limitation, it is not possible to achieve
|
||||
time-controlled request sequence. In other words, if the SPI slave needs to
|
||||
receive command sequences at precise instants - for example ADC or DAC
|
||||
devices - PyFtdi_ use is not recommended. This limitation is likely to apply
|
||||
to any library that relies on FTDI device. The USB bus latency and the lack
|
||||
of timestamped commands always add jitter and delays, with no easy known
|
||||
workaround.
|
||||
|
||||
.. _spi_wiring:
|
||||
|
||||
Wiring
|
||||
~~~~~~
|
||||
|
||||
.. figure:: ../images/spi_wiring.png
|
||||
:scale: 50 %
|
||||
:alt: SPI wiring
|
||||
:align: right
|
||||
|
||||
Fig.1: FT2232H with two SPI slaves
|
||||
|
||||
* ``AD0`` should be connected to SCLK
|
||||
* ``AD1`` should be connected to MOSI
|
||||
* ``AD2`` should be connected to MISO
|
||||
* ``AD3`` should be connected to the first slave /CS.
|
||||
* ``AD4`` should be connected to the second slave /CS, if any
|
||||
* remaining pins can be freely used as regular GPIOs.
|
||||
|
||||
*Fig.1*:
|
||||
|
||||
* ``AD4`` may be used as a regular GPIO if a single SPI slave is used
|
||||
* ``AD5`` may be used as another /CS signal for a third slave, in this case
|
||||
the first available GPIO is ``AD6``, etc.
|
228
lib/python3.11/site-packages/pyftdi/doc/api/uart.rst
Normal file
228
lib/python3.11/site-packages/pyftdi/doc/api/uart.rst
Normal file
|
@ -0,0 +1,228 @@
|
|||
.. include:: ../defs.rst
|
||||
|
||||
:mod:`serialext` - UART API
|
||||
---------------------------
|
||||
|
||||
There is no dedicated module for the UART API, as PyFtdi_ acts as a backend of
|
||||
the well-known pyserial_ module.
|
||||
|
||||
The pyserial_ backend module is implemented as the `serialext.protocol_ftdi`
|
||||
module. It is not documented here as no direct call to this module is required,
|
||||
as the UART client should use the regular pyserial_ API.
|
||||
|
||||
Usage
|
||||
~~~~~
|
||||
|
||||
To enable PyFtdi_ as a pyserial_ backend, use the following import:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pyftdi.serialext
|
||||
|
||||
Then use
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pyftdi.serialext.serial_for_url(url, **options)
|
||||
|
||||
to open a pyserial_ serial port instance.
|
||||
|
||||
|
||||
Quickstart
|
||||
~~~~~~~~~~
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Enable pyserial extensions
|
||||
import pyftdi.serialext
|
||||
|
||||
# Open a serial port on the second FTDI device interface (IF/2) @ 3Mbaud
|
||||
port = pyftdi.serialext.serial_for_url('ftdi://ftdi:2232h/2', baudrate=3000000)
|
||||
|
||||
# Send bytes
|
||||
port.write(b'Hello World')
|
||||
|
||||
# Receive bytes
|
||||
data = port.read(1024)
|
||||
|
||||
.. _uart_gpio:
|
||||
|
||||
GPIO access
|
||||
~~~~~~~~~~~
|
||||
|
||||
UART mode, the primary function of FTDI \*232\* devices, is somewhat limited
|
||||
when it comes to GPIO management, as opposed to alternative mode such as |I2C|,
|
||||
SPI and JTAG. It is not possible to assign the unused pins of an UART mode to
|
||||
arbitrary GPIO functions.
|
||||
|
||||
All the 8 lower pins of an UART port are dedicated to the UART function,
|
||||
although most of them are seldomely used, as dedicated to manage a modem or a
|
||||
legacy DCE_ device. Upper pins (b\ :sub:`7`\ ..b\ :sub:`15`\ ), on devices that
|
||||
have ones, cannot be driven while UART port is enabled.
|
||||
|
||||
It is nevertheless possible to have limited access to the lower pins as GPIO,
|
||||
with many limitations:
|
||||
|
||||
- the GPIO direction of each pin is hardcoded and cannot be changed
|
||||
- GPIO pins cannot be addressed atomically: it is possible to read the state
|
||||
of an input GPIO, or to change the state of an output GPIO, one after
|
||||
another. This means than obtaining the state of several input GPIOs or
|
||||
changing the state of several output GPIO at once is not possible.
|
||||
- some pins cannot be used as GPIO is hardware flow control is enabled.
|
||||
Keep in mind However that HW flow control with FTDI is not reliable, see the
|
||||
:ref:`hardware_flow_control` section.
|
||||
|
||||
Accessing those GPIO pins is done through the UART extended pins, using their
|
||||
UART assigned name, as PySerial port attributes. See the table below:
|
||||
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| Bit | UART | Direction | API |
|
||||
+===============+======+===========+===============================+
|
||||
| b\ :sub:`0`\ | TX | Out | ``port.write(buffer)`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| b\ :sub:`1`\ | RX | In | ``buffer = port.read(count)`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| b\ :sub:`2`\ | RTS | Out | ``port.rts = state`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| b\ :sub:`3`\ | CTS | In | ``state = port.cts`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| b\ :sub:`4`\ | DTR | Out | ``port.dtr = state`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| b\ :sub:`5`\ | DSR | In | ``state = port.dsr`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| b\ :sub:`6`\ | DCD | In | ``state = port.dcd`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
| b\ :sub:`7`\ | RI | In | ``state = port.ri`` |
|
||||
+---------------+------+-----------+-------------------------------+
|
||||
|
||||
CBUS support
|
||||
````````````
|
||||
|
||||
Some FTDI devices (FT232R, FT232H, FT230X, FT231X) support additional CBUS
|
||||
pins, which can be used as regular GPIOs pins. See :ref:`CBUS GPIO<cbus_gpio>`
|
||||
for details.
|
||||
|
||||
|
||||
.. _pyterm:
|
||||
|
||||
Mini serial terminal
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``pyterm.py`` is a simple serial terminal that can be used to test the serial
|
||||
port feature. See the :ref:`tools` chapter to locate this tool.
|
||||
|
||||
::
|
||||
|
||||
Usage: pyterm.py [-h] [-f] [-b BAUDRATE] [-w] [-e] [-r] [-l] [-s] [-P VIDPID]
|
||||
[-V VIRTUAL] [-v] [-d]
|
||||
[device]
|
||||
|
||||
Simple Python serial terminal
|
||||
|
||||
positional arguments:
|
||||
device serial port device name (default: ftdi:///1)
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-f, --fullmode use full terminal mode, exit with [Ctrl]+B
|
||||
-b BAUDRATE, --baudrate BAUDRATE
|
||||
serial port baudrate (default: 115200)
|
||||
-w, --hwflow hardware flow control
|
||||
-e, --localecho local echo mode (print all typed chars)
|
||||
-r, --crlf prefix LF with CR char, use twice to replace all LF
|
||||
with CR chars
|
||||
-l, --loopback loopback mode (send back all received chars)
|
||||
-s, --silent silent mode
|
||||
-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
|
||||
-v, --verbose increase verbosity
|
||||
-d, --debug enable debug mode
|
||||
|
||||
If the PyFtdi module is not yet installed and ``pyterm.py`` is run from the
|
||||
archive directory, ``PYTHONPATH`` should be defined to the current directory::
|
||||
|
||||
PYTHONPATH=$PWD pyftdi/bin/pyterm.py ftdi:///?
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
# detect all FTDI connected devices
|
||||
PYTHONPATH=. python3 pyftdi/bin/ftdi_urls.py
|
||||
# use the first interface of the first FT2232H as a serial port
|
||||
PYTHONPATH=$PWD pyftdi/bin/pyterm.py ftdi://ftdi:2232/1
|
||||
|
||||
|
||||
.. _uart-limitations:
|
||||
|
||||
Limitations
|
||||
~~~~~~~~~~~
|
||||
|
||||
Although the FTDI H series are in theory capable of 12 MBps baudrate, baudrates
|
||||
above 6 Mbps are barely usable.
|
||||
|
||||
See the following table for details.
|
||||
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| Requ. bps |HW capability| 9-bit time | Real bps | Duty cycle | Stable |
|
||||
+============+=============+============+=============+============+========+
|
||||
| 115.2 Kbps | 115.2 Kbps | 78.08 µs | 115.26 Kbps | 49.9% | Yes |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 460.8 Kbps | 461.54 Kbps | 19.49 µs | 461.77 Kbps | 49.9% | Yes |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 1 Mbps | 1 Mbps | 8.98 µs | 1.002 Mbps | 49.5% | Yes |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 4 Mbps | 4 Mbps | 2.24 µs | 4.018 Mbps | 48% | Yes |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 5 Mbps | 5.052 Mbps | 1.78 µs | 5.056 Mbps | 50% | Yes |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 6 Mbps | 6 Mbps | 1.49 µs | 6.040 Mbps | 48.5% | Yes |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 7 Mbps | 6.857 Mbps | 1.11 µs | 8.108 Mbps | 44% | No |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 8 Mbps | 8 Mbps | 1.11 µs | 8.108 Mbps | 44%-48% | No |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 8.8 Mbps | 8.727 Mbps | 1.13 µs | 7.964 Mbps | 44% | No |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 9.6 Mbps | 9.6 Mbps | 1.12 µs | 8.036 Mbps | 48% | No |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 10.5 Mbps | 10.667 Mbps | 1.11 µs | 8.108 Mbps | 44% | No |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
| 12 Mbps | 12 Mbps | 0.75 µs | 12 Mbps | 43% | Yes |
|
||||
+------------+-------------+------------+-------------+------------+--------+
|
||||
|
||||
* 9-bit time is the measured time @ FTDI output pins for a 8-bit character
|
||||
(start bit + 8 bit data)
|
||||
* Duty cycle is the ratio between a low-bit duration and a high-bit duration,
|
||||
a good UART should exhibit the same duration for low bits and high bits,
|
||||
*i.e.* a duty cycle close to 50%.
|
||||
* Stability reports whether subsequent runs, with the very same HW settings,
|
||||
produce the same timings.
|
||||
|
||||
Achieving a reliable connection over 6 Mbps has proven difficult, if not
|
||||
impossible: Any baudrate greater than 6 Mbps (except the upper 12 Mbps limit)
|
||||
results into an actual baudrate of about 8 Mbps, and suffer from clock
|
||||
fluterring [7.95 .. 8.1Mbps].
|
||||
|
||||
.. _hardware_flow_control:
|
||||
|
||||
Hardware flow control
|
||||
`````````````````````
|
||||
|
||||
Moreover, as the hardware flow control of the FTDI device is not a true HW
|
||||
flow control. Quoting FTDI application note:
|
||||
|
||||
*If CTS# is logic 1 it is indicating the external device cannot accept more
|
||||
data. the FTxxx will stop transmitting within 0~3 characters, depending on
|
||||
what is in the buffer.*
|
||||
**This potential 3 character overrun does occasionally present problems.**
|
||||
*Customers shoud be made aware the FTxxx is a USB device and not a "normal"
|
||||
RS232 device as seen on a PC. As such the device operates on a packet
|
||||
basis as opposed to a byte basis.*
|
||||
|
19
lib/python3.11/site-packages/pyftdi/doc/api/usbtools.rst
Normal file
19
lib/python3.11/site-packages/pyftdi/doc/api/usbtools.rst
Normal file
|
@ -0,0 +1,19 @@
|
|||
.. -*- coding: utf-8 -*-
|
||||
|
||||
:mod:`usbtools` - USB tools
|
||||
---------------------------
|
||||
|
||||
.. module :: pyftdi.usbtools
|
||||
|
||||
|
||||
Classes
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass :: UsbTools
|
||||
:members:
|
||||
|
||||
|
||||
Exceptions
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoexception :: UsbToolsError
|
55
lib/python3.11/site-packages/pyftdi/doc/authors.rst
Normal file
55
lib/python3.11/site-packages/pyftdi/doc/authors.rst
Normal file
|
@ -0,0 +1,55 @@
|
|||
Authors
|
||||
-------
|
||||
|
||||
Main developers
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
* Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
* Emmanuel Bouaziz <ebouaziz@free.fr>
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Nikus-V
|
||||
* Dave McCoy
|
||||
* Adam Feuer
|
||||
* endlesscoil
|
||||
* humm (Fabien Benureau)
|
||||
* dlharmon
|
||||
* DavidWC
|
||||
* Sebastian
|
||||
* Anders (anders-code)
|
||||
* Andrea Concil
|
||||
* Darren Garnier
|
||||
* Michael Leonard
|
||||
* nopeppermint (Stefan)
|
||||
* hannesweisbach
|
||||
* Vianney le Clément de Saint-Marcq
|
||||
* Pete Schwamb
|
||||
* Will Richey
|
||||
* sgoadhouse
|
||||
* tavip (Octavian Purdila)
|
||||
* Tim Legrand
|
||||
* vestom
|
||||
* meierphil
|
||||
* etherfi
|
||||
* sgoadhouse
|
||||
* jnmacd
|
||||
* naushir
|
||||
* markmelvin (Mark Melvin)
|
||||
* stiebrs
|
||||
* mpratt14
|
||||
* alexforencich
|
||||
* TedKus
|
||||
* Amanita-muscaria
|
||||
* len0rd
|
||||
* Rod Whitby
|
||||
* Kornel Swierzy
|
||||
* Taisuke Yamada
|
||||
* Michael Niewöhner
|
||||
* Kalofin
|
||||
* Henry Au-Yeung
|
||||
* Roman Dobrodii
|
||||
* Mark Mentovai
|
||||
* Alessandro Zini
|
||||
* Sjoerd Simons
|
49
lib/python3.11/site-packages/pyftdi/doc/defs.rst
Normal file
49
lib/python3.11/site-packages/pyftdi/doc/defs.rst
Normal file
|
@ -0,0 +1,49 @@
|
|||
.. |I2C| replace:: I\ :sup:`2`\ C
|
||||
|
||||
.. _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: 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: https://www.libusb.info/
|
||||
.. _Libusb on Windows: https://github.com/libusb/libusb/wiki/Windows
|
||||
.. _Libusb win32: https://sourceforge.net/projects/libusb-win32/files/libusb-win32-releases/
|
||||
.. _Zadig: https://zadig.akeo.ie/
|
||||
.. _FTDI macOS guide: https://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf
|
||||
.. _Libusb issue on macOs: https://github.com/libusb/libusb/commit/5e45e0741daee4fa295c6cc977edfb986c872152
|
||||
.. _FT_PROG: https://www.ftdichip.com/Support/Utilities.htm#FT_PROG
|
||||
.. _fstring: https://www.python.org/dev/peps/pep-0498
|
||||
.. _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
|
||||
|
||||
.. Level 1
|
||||
.. -------
|
||||
|
||||
.. Level 2
|
||||
.. ~~~~~~~
|
||||
|
||||
.. Level 3
|
||||
.. ```````
|
||||
|
||||
.. Level 4
|
||||
.. .......
|
||||
|
||||
.. Level 5
|
||||
.. +++++++
|
401
lib/python3.11/site-packages/pyftdi/doc/eeprom.rst
Normal file
401
lib/python3.11/site-packages/pyftdi/doc/eeprom.rst
Normal file
|
@ -0,0 +1,401 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
EEPROM management
|
||||
-----------------
|
||||
|
||||
.. warning::
|
||||
Writing to the EEPROM can cause very **undesired** effects if the wrong
|
||||
value is written in the wrong place. You can even essentially **brick** your
|
||||
FTDI device. Use this function only with **extreme** caution.
|
||||
|
||||
It is not recommended to use this application with devices that use an
|
||||
internal EEPROM such as FT232R or FT-X series, as if something goes wrong,
|
||||
recovery options are indeed limited. FT232R internal EEPROM seems to be
|
||||
unstable, even the official FT_PROG_ tool from FTDI may fail to fix it on
|
||||
some conditions.
|
||||
|
||||
If using a Hi-Speed Mini Module and you brick for FTDI device, see
|
||||
FTDI_Recovery_
|
||||
|
||||
|
||||
Supported features
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
EEPROM support is under active development.
|
||||
|
||||
Some features may be wrongly decoded, as each FTDI model implements a different
|
||||
feature map, and more test/validation are required.
|
||||
|
||||
The :doc:`EEPROM API <api/eeprom>` implements the upper API to access the
|
||||
EEPROM content.
|
||||
|
||||
.. _ftconf:
|
||||
|
||||
EEPROM configuration tool
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
``ftconf.py`` is a companion script to help managing the content of the FTDI
|
||||
EEPROM from the command line. See the :ref:`tools` chapter to locate this tool.
|
||||
|
||||
::
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
set manufacturer name
|
||||
-p PRODUCT, --product PRODUCT
|
||||
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
|
||||
|
||||
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.**
|
||||
|
||||
Note that to protect the EEPROM content of unexpected modification, it is
|
||||
mandatory to specify the :ref:`-u <option_u>` flag along any alteration/change
|
||||
of the EEPROM content. Without this flag, the script performs a dry-run
|
||||
execution of the changes, *i.e.* all actions but the write request to the
|
||||
EEPROM are executed.
|
||||
|
||||
Once updated, you need to unplug/plug back the device to use the new EEPROM
|
||||
configuration.
|
||||
|
||||
It is recommended to first save the current content of the EEPROM, using the
|
||||
:ref:`-o <option_o>` flag, to have a working copy of the EEPROM data before any
|
||||
attempt to modify it. It can help restoring the EEPROM if something gets wrong
|
||||
during a subsequence update, thanks to the :ref:`-i <option_i>` option switch.
|
||||
|
||||
Most FTDI device can run without an EEPROM. If something goes wrong, try to
|
||||
erase the EEPROM content, then restore the original content.
|
||||
|
||||
|
||||
Option switches
|
||||
```````````````
|
||||
In addition to the :ref:`common_option_switches` for PyFtdi_ tools,
|
||||
``ftconf.py`` support the following arguments:
|
||||
|
||||
.. _option_c:
|
||||
|
||||
``-c name=value``
|
||||
Change a configuration in the EEPROM. This flag can be repeated as many times
|
||||
as required to change several configuration parameter at once. Note that
|
||||
without option ``-u``, the EEPROM content is not actually modified, the
|
||||
script runs in dry-run mode.
|
||||
|
||||
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 ?``, 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 <option_u>`, the EEPROM content is not
|
||||
actually modified, the script runs in dry-run mode.
|
||||
|
||||
.. _option_e:
|
||||
|
||||
``-e``
|
||||
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 <option_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:
|
||||
|
||||
``-i``
|
||||
Load a INI file (as generated with the :ref:`-o <option_o>` option switch. It
|
||||
is possible to select which section(s) from the INI file are loaded, using
|
||||
:ref:`-l <option_l>` option switch. The ``values`` section may be modified,
|
||||
as it takes precedence over the ``raw`` section. Note that without option
|
||||
:ref:`-u <option_u>`, the EEPROM content is not actually modified, the script
|
||||
runs in dry-run mode.
|
||||
|
||||
.. _option_l:
|
||||
|
||||
``-l <all|raw|values>``
|
||||
Define which section(s) of the INI file are used to update the EEPROM content
|
||||
along with the :ref:`-i <option_i>` option switch. Defaults to ``all``.
|
||||
|
||||
The supported feature set of the ``values`` is the same as the one exposed
|
||||
through the :ref:`-c <option_c>` option switch. Unsupported feature are
|
||||
ignored, and a warning is emitted for each unsupported feature.
|
||||
|
||||
.. _option_M_:
|
||||
|
||||
``-M <model>``
|
||||
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 <manufacturer>``
|
||||
Assign a new manufacturer name to the device. Note that without option
|
||||
:ref:`-u <option_u>`, the EEPROM content is not actually modified, the script
|
||||
runs in dry-run mode. Manufacturer names with ``/`` or ``:`` characters are
|
||||
rejected, to avoid parsing issues with FTDI :ref:`URLs <url_scheme>`.
|
||||
|
||||
.. _option_o:
|
||||
|
||||
``-o <output>``
|
||||
Generate and write to the specified file the EEPROM content as decoded
|
||||
values and a hexa dump. The special ``-`` file can be used as the output file
|
||||
to print to the standard output. The output file contains two sections:
|
||||
|
||||
* ``[values]`` that contain the decoded EEPROM configuration as key, value
|
||||
pair. Note that the keys and values can be used as configuration input, see
|
||||
option :ref:`-c <option_c>`.
|
||||
* ``[raw]`` that contains a compact representation of the EEPROM raw content,
|
||||
encoded as hexadecimal strings.
|
||||
|
||||
.. _option_p:
|
||||
|
||||
``-p <product>``
|
||||
Assign a new product name to the device. Note that without option :ref:`-u
|
||||
<option_u>`, the EEPROM content is not actually modified, the script runs in
|
||||
dry-run mode. Product names with ``/`` or ``:`` characters are rejected, to
|
||||
avoid parsing issues with FTDI :ref:`URLs <url_scheme>`.
|
||||
|
||||
.. _option_pid:
|
||||
|
||||
``--pid``
|
||||
Define the USB product identifier - as an hexadecimal number. This is a
|
||||
shortcut for `-c product_id`
|
||||
|
||||
.. _option_S_:
|
||||
|
||||
``-S <size>``
|
||||
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 <serial>``
|
||||
Assign a new serial number to the device. Note that without option :ref:`-u
|
||||
<option_u>`, the EEPROM content is not actually modified, the script runs in
|
||||
dry-run mode. Serial number with ``/`` or ``:`` characters are rejected, to
|
||||
avoid parsing issues with FTDI :ref:`URLs <url_scheme>`.
|
||||
|
||||
.. _option_u:
|
||||
|
||||
``-u``
|
||||
Update the EEPROM with the new settings. Without this flag, the script runs
|
||||
in dry-run mode, so no change is made to the EEPROM. Whenever this flag is
|
||||
used, the EEPROM is actually updated and its checksum regenerated. If
|
||||
something goes wrong at this point, you may brick you board, you've been
|
||||
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``
|
||||
Generate and print a hexadecimal raw dump of the EEPROM content, similar to
|
||||
the output of the `hexdump -Cv` tool.
|
||||
|
||||
|
||||
.. _cbus_func:
|
||||
|
||||
CBUS function
|
||||
`````````````
|
||||
|
||||
The following table describes the CBUS pin alternate functions. Note that
|
||||
depending on the actual device, some alternate function may not be available.
|
||||
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| Name | Active | Description |
|
||||
+=================+========+================================================================================+
|
||||
| ``TRISTATE`` | Hi-Z | IO Pad is tri-stated |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``TXLED`` | Low | TX activity, can be used as status for LED |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``RXLED`` | Low | RX activity, can be used as status for LED |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``TXRXLED`` | Low | TX & RX activity, can be used as status for LED |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``PWREN`` | Low | USB configured, USB suspend: high |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``SLEEP`` | Low | USB suspend, typically used to power down external devices. |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``DRIVE0`` | Low | Drive a constant (FT232H and FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``DRIVE1`` | High | Drive a constant (FT232H and FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``GPIO`` | | IO port for CBUS bit bang mode |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``TXDEN`` | High | Enable transmit for RS485 mode |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``CLK48`` | | Output 48 MHz clock (FT232R only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``CLK30`` | | Output 30 MHz clock (FT232H only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``CLK24`` | | Output 24 MHz clock (FT232R and FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``CLK15`` | | Output 12 MHz clock (FT232H only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``CLK12`` | | Output 12 MHz clock (FT232R and FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``CLK7_5`` | | Output 7.5 MHz clock (FT232H only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``CLK6`` | | Output 6 MHz clock (FT232R and FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``BAT_DETECT`` | High | Battery Charger Detect (FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``BAT_NDETECT`` | Low | Inverse signal of BAT_DETECT (FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``I2C_TXE`` | Low | Transmit buffer empty (FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``I2C_RXF`` | Low | Receive buffer full (FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``VBUS_SENSE`` | High | Detect when VBUS is present via the appropriate AC IO pad (FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``BB_WR`` | Low | Synchronous Bit Bang Write strobe (FT232R and FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``BB_RD`` | Low | Synchronous Bit Bang Read strobe (FT232R and FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``TIMESTAMP`` | | Toggle signal each time a USB SOF is received (FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
| ``AWAKE`` | Low | Do not suspend when unplugged/disconnect/suspsend (FT-X only) |
|
||||
+-----------------+--------+--------------------------------------------------------------------------------+
|
||||
|
||||
|
||||
Examples
|
||||
````````
|
||||
|
||||
* Change product name and serial number
|
||||
|
||||
::
|
||||
|
||||
pyftdi/bin/ftconf.py ftdi:///1 -p UartBridge -s abcd1234 -u
|
||||
|
||||
* List supported configuration parameters
|
||||
|
||||
::
|
||||
|
||||
pyftdi/bin/ftconf.py ftdi:///1 -c ?
|
||||
cbus_func_0, cbus_func_1, cbus_func_2, cbus_func_3, cbus_func_4,
|
||||
cbus_func_5, cbus_func_6, cbus_func_7, cbus_func_8, cbus_func_9,
|
||||
channel_a_driver, channel_a_type, chip, clock_polarity,
|
||||
flow_control, group_0_drive, group_0_schmitt, group_0_slew,
|
||||
group_1_drive, group_1_schmitt, group_1_slew, has_serial,
|
||||
has_usb_version, in_isochronous, lsb_data, out_isochronous,
|
||||
power_max, powersave, product_id, remote_wakeup, self_powered,
|
||||
suspend_pull_down, type, usb_version, vendor_id
|
||||
|
||||
* List supported configuration values for CBUS0
|
||||
|
||||
::
|
||||
|
||||
pyftdi/bin/ftconf.py ftdi:///1 -c cbus_func_0:?
|
||||
AWAKE, BAT_DETECT, BAT_NDETECT, BB_RD, BB_WR, CLK12, CLK24, CLK6,
|
||||
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
|
||||
|
||||
::
|
||||
|
||||
pyftdi/bin/ftconf.py ftdi:///1 -v
|
||||
-c cbus_func_0:GPIO -c cbus_func_3:GPIO
|
98
lib/python3.11/site-packages/pyftdi/doc/features.rst
Normal file
98
lib/python3.11/site-packages/pyftdi/doc/features.rst
Normal file
|
@ -0,0 +1,98 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
Devices
|
||||
~~~~~~~
|
||||
|
||||
* All FTDI device ports (UART, MPSSE) can be used simultaneously.
|
||||
|
||||
* SPI and |I2C| SPI support simultaneous GPIO R/W access for all pins that
|
||||
are not used for SPI/|I2C| feature.
|
||||
|
||||
* Several FTDI adapters can be accessed simultaneously from the same Python
|
||||
runtime instance.
|
||||
|
||||
Supported features
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
UART
|
||||
````
|
||||
|
||||
Serial port, up to 6 Mbps. PyFtdi_ includes a pyserial_ emulation layer that
|
||||
offers transparent access to the FTDI serial ports through a pyserial_-
|
||||
compliant API. The ``serialext`` directory contains a minimal serial terminal
|
||||
demonstrating the use of this extension, and a dispatcher automatically
|
||||
selecting the serial backend (pyserial_, PyFtdi_), based on the serial port
|
||||
name.
|
||||
|
||||
See also :ref:`uart-limitations`.
|
||||
|
||||
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_/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
|
||||
several common devices.
|
||||
|
||||
Both Half-duplex (write or read) and full-duplex (synchronous write and read)
|
||||
communication modes are supported.
|
||||
|
||||
Experimental support for non-byte aligned access, where up to 7 trailing bits
|
||||
can be discarded: no clock pulse is generated for those bits, so that SPI
|
||||
transfer of non byte-sized can be performed.
|
||||
|
||||
See :ref:`spi_wiring` and :ref:`spi_limitations`.
|
||||
|
||||
Note: FTDI*232* devices cannot be used as an SPI slave.
|
||||
|
||||
|I2C| master
|
||||
````````````
|
||||
|
||||
Supported devices: FT232H_, FT2232H_, FT4232H_, FT4232HA_
|
||||
|
||||
For now, only 7-bit addresses are supported.
|
||||
|
||||
GPIOs can be used while |I2C| mode is enabled.
|
||||
|
||||
The ``i2cscan.py`` script helps to discover which I2C devices are connected to
|
||||
the FTDI I2C bus. See the :ref:`tools` chapter to locate this tool.
|
||||
|
||||
The pyi2cflash_ module demonstrates how to use the FTDI |I2C| master to access
|
||||
serial EEPROMS.
|
||||
|
||||
See :ref:`i2c_wiring` and :ref:`i2c_limitations`.
|
||||
|
||||
Note: FTDI*232* devices cannot be used as an |I2C| slave.
|
||||
|
||||
JTAG
|
||||
````
|
||||
|
||||
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.
|
||||
|
||||
EEPROM
|
||||
``````
|
||||
|
||||
The ``pyftdi/bin/ftconf.py`` script helps to manage the content of the FTDI
|
||||
companion EEPROM.
|
||||
|
||||
|
||||
Status
|
||||
~~~~~~
|
||||
|
||||
This project is still in beta development stage. PyFtdi_ is developed as an
|
||||
open-source solution.
|
451
lib/python3.11/site-packages/pyftdi/doc/gpio.rst
Normal file
451
lib/python3.11/site-packages/pyftdi/doc/gpio.rst
Normal file
|
@ -0,0 +1,451 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
GPIOs
|
||||
-----
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
Many PyFtdi APIs give direct access to the IO pins of the FTDI devices:
|
||||
|
||||
* *GpioController*, implemented as ``GpioAsyncController``,
|
||||
``GpioSyncController`` and ``GpioMpsseController`` (see :doc:`api/gpio`)
|
||||
gives full access to the FTDI pins as raw I/O pins,
|
||||
* ``SpiGpioPort`` (see :doc:`api/spi`) gives access to all free pins of an
|
||||
FTDI interface, which are not reserved for the SPI feature,
|
||||
* ``I2cGpioPort`` (see :doc:`api/i2c`) gives access to all free pins of an
|
||||
FTDI interface, which are not reserved for the I2C feature
|
||||
|
||||
Other modes
|
||||
```````````
|
||||
|
||||
* Gpio raw access is not yet supported with JTAG feature.
|
||||
* It is not possible to use GPIO along with UART mode on the same interface.
|
||||
However, UART mode still provides (very) limited access to GPIO pins, see
|
||||
UART :ref:`uart_gpio` for details.
|
||||
|
||||
This document presents the common definitions for these APIs and explain how to
|
||||
drive those pins.
|
||||
|
||||
|
||||
Definitions
|
||||
~~~~~~~~~~~
|
||||
|
||||
Interfaces
|
||||
``````````
|
||||
|
||||
An FTDI *interface* follows the definition of a *USB interface*: it is an
|
||||
independent hardware communication port with an FTDI device. Each interface can
|
||||
be configured independently from the other interfaces on the same device, e.g.
|
||||
one interface may be configured as an UART, the other one as |I2C| + GPIO.
|
||||
|
||||
It is possible to access two distinct interfaces of the same FTDI device
|
||||
from a multithreaded application, and even from different applications, or
|
||||
Python interpreters. However two applications cannot access the same interface
|
||||
at the same time.
|
||||
|
||||
.. warning::
|
||||
|
||||
Performing a USB device reset affects all the interfaces of an FTDI device,
|
||||
this is the rationale for not automatically performing a device reset when
|
||||
an interface is initialiazed and configured from PyFtdi_.
|
||||
|
||||
.. _ftdi_ports:
|
||||
|
||||
Ports
|
||||
`````
|
||||
|
||||
An FTDI port is ofter used in PyFtdi as a synonym for an interface. This may
|
||||
differ from the FTDI datasheets that sometimes show an interface with several
|
||||
ports (A\*BUS, B\*BUS). From a software standpoint, ports and interfaces are
|
||||
equivalent: APIs access all the HW port from the same interface at once. From a
|
||||
pure hardware standpoint, a single interface may be depicted as one or two
|
||||
*ports*.
|
||||
|
||||
With PyFtdi_, *ports* and *interfaces* should be considered as synomyms.
|
||||
|
||||
Each port can be accessed as raw input/output pins. At a given time, a pin is
|
||||
either configured as an input or an output function.
|
||||
|
||||
The width of a port, that is the number of pins of the interface, depending on
|
||||
the actual hardware, *i.e.* the FTDI model:
|
||||
|
||||
* FT232R features a single port, which is 8-bit wide: `DBUS`,
|
||||
* FT232H features a single port, which is 16-bit wide: `ADBUS/ACBUS`,
|
||||
* FT2232D features two ports, which are 12-bit wide each: `ADBUS/ACBUS` and
|
||||
`BDBUS/BCBUS`,
|
||||
* FT2232H features two ports, which are 16-bit wide each: `ADBUS/ACBUS` and
|
||||
`BDBUS/BCBUS`,
|
||||
* 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
|
||||
|
||||
For historical reasons, 16-bit ports used to be named *wide* ports and 8-bit
|
||||
ports used to be called *narrow* with PyFtdi_. This terminology and APIs are
|
||||
no longer used, but are kept to prevent API break. Please only use the port
|
||||
``width`` rather than these legacy port types.
|
||||
|
||||
|
||||
GPIO value
|
||||
``````````
|
||||
|
||||
* A logical ``0`` bit represents a low level value on a pin, that is *GND*
|
||||
* A logical ``1`` bit represents a high level value on a pin, that is *Vdd*
|
||||
which is typically 3.3 volts on most FTDIs
|
||||
|
||||
Please refers to the FTDI datasheet of your device for the tolerance and
|
||||
supported analog levels for more details
|
||||
|
||||
.. hint::
|
||||
|
||||
FT232H supports a specific feature, which is dedicated to better supporting
|
||||
the |I2C| feature. This specific devices enables an open-collector mode:
|
||||
|
||||
* Setting a pin to a low level drains it to *GND*
|
||||
* Setting a pin to a high level sets the pin as High-Z
|
||||
|
||||
This feature is automatically activated when |I2C| feature is enabled on a
|
||||
port, for the two first pins, i.e. `SCL` and `SDA out`.
|
||||
|
||||
However, PyFTDI does not yet provide an API to enable this mode to the
|
||||
other pins of a port, *i.e.* for the pins used as GPIOs.
|
||||
|
||||
|
||||
Direction
|
||||
`````````
|
||||
|
||||
An FTDI pin should either be configured as an input or an ouput. It is
|
||||
mandatory to (re)configure the direction of a pin before changing the way it is
|
||||
used.
|
||||
|
||||
* A logical ``0`` bit represents an input pin, *i.e.* a pin whose value can be
|
||||
sampled and read via the PyFTDI APIs
|
||||
* A logical ``1`` bit represents an output pin, *i.e.* a pin whose value can be
|
||||
set/written with the PyFTDI APIs
|
||||
|
||||
|
||||
.. _cbus_gpio:
|
||||
|
||||
CBUS GPIOs
|
||||
~~~~~~~~~~
|
||||
|
||||
FT232R, FT232H and FT230X/FT231X support an additional port denoted CBUS:
|
||||
|
||||
* FT232R provides an additional 5-bit wide port, where only 4 LSBs can be
|
||||
used as programmable GPIOs: ``CBUS0`` to ``CBUS3``,
|
||||
* FT232H provices an additional 10-bit wide port, where only 4 pins can be
|
||||
used as programmable GPIOs: ``CBUS5``, ``CBUS6``, ``CBUS8``, ``CBUS9``
|
||||
* FT230X/FT231X provides an additional 4-bit wide port: ``CBUS0`` to ``CBUS3``
|
||||
|
||||
Note that CBUS access is slower than regular asynchronous bitbang mode.
|
||||
|
||||
CBUS EEPROM configuration
|
||||
`````````````````````````
|
||||
|
||||
Accessing this extra port requires a specific EEPROM configuration.
|
||||
|
||||
The EEPROM needs to be configured so that the CBUS pins that need to be used
|
||||
as GPIOs are defined as ``GPIO``. Without this special configuration, CBUS
|
||||
pins are used for other functions, such as driving leds when data is exchanged
|
||||
over the UART port. Remember to power-cycle the FTDI device after changing its
|
||||
EEPROM configuration to force load the new configuration.
|
||||
|
||||
The :ref:`ftconf` tool can be used to query and change the EEPROM
|
||||
configuration. See the EEPROM configuration :ref:`example <eeprom_cbus>`.
|
||||
|
||||
CBUS GPIO API
|
||||
`````````````
|
||||
|
||||
PyFtdi_ starting from v0.47 supports CBUS pins as special GPIO port. This port
|
||||
is *not* mapped as regular GPIO, a dedicated API is reserved to drive those
|
||||
pins:
|
||||
|
||||
* :py:meth:`pyftdi.ftdi.Ftdi.has_cbus` to report whether the device supports
|
||||
CBUS gpios,
|
||||
* :py:meth:`pyftdi.ftdi.Ftdi.set_cbus_direction` to configure the port,
|
||||
* :py:meth:`pyftdi.ftdi.Ftdi.get_cbus_gpio` to get the logical values from the
|
||||
port,
|
||||
* :py:meth:`pyftdi.ftdi.Ftdi.set_cbus_gpio` to set new logical values to the
|
||||
port
|
||||
|
||||
Additionally, the EEPROM configuration can be queried to retrieve which CBUS
|
||||
pins have been assigned to GPIO functions:
|
||||
|
||||
* :py:meth:`pyftdi.eeprom.FtdiEeprom.cbus_pins` to report CBUS GPIO pins
|
||||
|
||||
The CBUS port is **not** available through the
|
||||
:py:class:`pyftdi.gpio.GpioController` API, as it cannot be considered as a
|
||||
regular GPIO port.
|
||||
|
||||
.. warning::
|
||||
|
||||
CBUS GPIO feature has only be tested with the virtual test framework and a
|
||||
real FT231X HW device. It should be considered as an experimental feature
|
||||
for now.
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
GPIO bitmap
|
||||
```````````
|
||||
|
||||
The GPIO pins of a port are always accessed as an integer, whose supported
|
||||
width depends on the width of the port. These integers should be considered as
|
||||
a bitmap of pins, and are always assigned the same mapping, whatever feature is
|
||||
enabled:
|
||||
|
||||
* 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 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.
|
||||
|
||||
Pins reserved for a specific feature (|I2C|, SPI, ...) cannot be accessed as
|
||||
a regular GPIO. They cannot be arbitrarily written and should be masked out
|
||||
when the GPIO output value is set. See :ref:`reserved_pins` for details.
|
||||
|
||||
FT232H CBUS exception
|
||||
.....................
|
||||
|
||||
Note that there is an exception to this rule for FT232H CBUS port: FTDI has
|
||||
decided to map non-contiguous CBUS pins as GPIO-capable CBUS pins, that is
|
||||
``CBUS5``, ``CBUS6``, ``CBUS8``, ``CBUS9``, where other CBUS-enabled devices
|
||||
use ``CBUS0``, ``CBUS1``, ``CBUS2``, ``CBUS3``.
|
||||
|
||||
If the CBUS GPIO feature is used with an FT232H device, the pin positions for
|
||||
the GPIO port are not b\ :sub:`5`\ .. b\ :sub:`9`\ but b\ :sub:`0`\ to
|
||||
b\ :sub:`3`\ . This may sounds weird, but CBUS feature is somewhat hack-ish
|
||||
even with FTDI commands, so it did not deserve a special treatment for the sake
|
||||
of handling the weird implementation of FT232H.
|
||||
|
||||
Direction bitmap
|
||||
````````````````
|
||||
|
||||
Before using a port as GPIO, the port must be configured as GPIO. This is
|
||||
achieved by either instanciating one of the *GpioController* or by requesting
|
||||
the GPIO port from a specific serial bus controller:
|
||||
``I2cController.get_gpio()`` and ``SpiController.get_gpio()``. All instances
|
||||
provide a similar API (duck typing API) to configure, read and write to GPIO
|
||||
pins.
|
||||
|
||||
Once a GPIO port is instanciated, the direction of each pin should be defined.
|
||||
The direction can be changed at any time. It is not possible to write to /
|
||||
read from a pin before the proper direction has been defined.
|
||||
|
||||
To configure the direction, use the `set_direction` API with a bitmap integer
|
||||
value that defines the direction to use of each pin.
|
||||
|
||||
Direction example
|
||||
.................
|
||||
|
||||
A 8-bit port, dedicated to GPIO, is configured as follows:
|
||||
|
||||
* BD0, BD3, BD7: input, `I` for short
|
||||
* BD1-BD2, BD4-BD6: output, `O` for short
|
||||
|
||||
That is, MSB to LSB: *I O O O I O O I*.
|
||||
|
||||
This translates to 0b ``0111 0110`` as output is ``1`` and input is ``0``,
|
||||
that is ``0x76`` as an hexa value. This is the direction value to use to
|
||||
``configure()`` the port.
|
||||
|
||||
See also the ``set_direction()`` API to reconfigure the direction of GPIO pins
|
||||
at any time. This method accepts two arguments. This first arguments,
|
||||
``pins``, defines which pins - the ones with the maching bit set - to consider
|
||||
in the second ``direction`` argument, so there is no need to
|
||||
preserve/read-modify-copy the configuration of other pins. Pins with their
|
||||
matching bit reset are not reconfigured, whatever their direction bit.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
gpio = GpioAsyncController()
|
||||
gpio.configure('ftdi:///1', direction=0x76)
|
||||
# later, reconfigure BD2 as input and BD7 as output
|
||||
gpio.set_direction(0x84, 0x80)
|
||||
|
||||
|
||||
Using GPIO APIs
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
There are 3 variant of *GpioController*, depending on which features are needed
|
||||
and how the GPIO port usage is intended. :doc:`api/gpio` gives in depth details
|
||||
about those controllers. Those controllers are mapped onto FTDI HW features.
|
||||
|
||||
* ``GpioAsyncController`` is likely the most useful API to drive GPIOs.
|
||||
|
||||
It enables reading current GPIO input pin levels and to change GPIO output
|
||||
pin levels. When vector values (byte buffers) are used instead of scalar
|
||||
value (single byte), GPIO pins are samples/updated at a regular pace, whose
|
||||
frequency can be configured. It is however impossible to control the exact
|
||||
time when input pins start to be sampled, which can be tricky to use with
|
||||
most applications. See :doc:`api/gpio` for details.
|
||||
|
||||
* ``GpioSyncController`` is a variant of the previous API.
|
||||
|
||||
It is aimed at precise time control of sampling/updating the GPIO: a new
|
||||
GPIO input sample is captured once every time GPIO output pins are updated.
|
||||
With byte buffers, GPIO pins are samples/updated at a regular pace, whose
|
||||
frequency can be configured as well. The API of ``GpioSyncController``
|
||||
slightly differ from the other GPIO APIs, as the usual ``read``/``write``
|
||||
method are replaced with a single ``exchange`` method.
|
||||
|
||||
Both ``GpioAsyncController`` and ``GpioSyncController`` are restricted to only
|
||||
access the 8 LSB pins of a port, which means that FTDI device with wider port
|
||||
(12- and 16- pins) cannot be fully addressed, as only b\ :sub:`0`\ to b\
|
||||
:sub:`7`\ can be addressed.
|
||||
|
||||
* ``GpioMpsseController`` enables access to the MSB pins of wide ports.
|
||||
|
||||
However LSB and MSB pins cannot be addressed in a true atomic manner, which
|
||||
means that there is a short delay between sampling/updating the LSB and MSB
|
||||
part of the same wide port. Byte buffer can also be sampled/updated at a
|
||||
regular pace, but the achievable frequency range may differ from the other
|
||||
controllers.
|
||||
|
||||
It is recommened to read the ``tests/gpio.py`` files - available from GitHub -
|
||||
to get some examples on how to use these API variants.
|
||||
|
||||
Setting GPIO pin state
|
||||
``````````````````````
|
||||
|
||||
To write to a GPIO, use the `write()` method. The caller needs to mask out
|
||||
the bits configured as input, or an exception is triggered:
|
||||
|
||||
* writing ``0`` to an input pin is ignored
|
||||
* writing ``1`` to an input pin raises an exception
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
gpio = GpioAsyncController()
|
||||
gpio.configure('ftdi:///1', direction=0x76)
|
||||
# all output set low
|
||||
gpio.write(0x00)
|
||||
# all output set high
|
||||
gpio.write(0x76)
|
||||
# all output set high, apply direction mask
|
||||
gpio.write(0xFF & gpio.direction)
|
||||
# all output forced to high, writing to input pins is illegal
|
||||
gpio.write(0xFF) # raises an IOError
|
||||
gpio.close()
|
||||
|
||||
|
||||
Retrieving GPIO pin state
|
||||
`````````````````````````
|
||||
|
||||
To read a GPIO, use the `read()` method.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
gpio = GpioAsyncController()
|
||||
gpio.configure('ftdi:///1', direction=0x76)
|
||||
# read whole port
|
||||
pins = gpio.read()
|
||||
# ignore output values (optional)
|
||||
pins &= ~gpio.direction
|
||||
gpio.close()
|
||||
|
||||
|
||||
Modifying GPIO pin state
|
||||
````````````````````````
|
||||
|
||||
A read-modify-write sequence is required.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
gpio = GpioAsyncController()
|
||||
gpio.configure('ftdi:///1', direction=0x76)
|
||||
# read whole port
|
||||
pins = gpio.read()
|
||||
# clearing out AD1 and AD2
|
||||
pins &= ~((1 << 1) | (1 << 2)) # or 0x06
|
||||
# want AD2=0, AD1=1
|
||||
pins |= 1 << 1
|
||||
# update GPIO output
|
||||
gpio.write(pins)
|
||||
gpio.close()
|
||||
|
||||
|
||||
Synchronous GPIO access
|
||||
```````````````````````
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
gpio = GpioSyncController()
|
||||
gpio.configure('ftdi:///1', direction=0x0F, frequency=1e6)
|
||||
outs = bytes(range(16))
|
||||
ins = gpio.exchange(outs)
|
||||
# ins contains as many bytes as outs
|
||||
gpio.close()
|
||||
|
||||
|
||||
CBUS GPIO access
|
||||
````````````````
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ftdi = Ftdi()
|
||||
ftdi.open_from_url('ftdi:///1')
|
||||
# validate CBUS feature with the current device
|
||||
assert ftdi.has_cbus
|
||||
# validate CBUS EEPROM configuration with the current device
|
||||
eeprom = FtdiEeprom()
|
||||
eeprom.connect(ftdi)
|
||||
# here we use CBUS0 and CBUS3 (or CBUS5 and CBUS9 on FT232H)
|
||||
assert eeprom.cbus_mask & 0b1001 == 0b1001
|
||||
# configure CBUS0 as output and CBUS3 as input
|
||||
ftdi.set_cbus_direction(0b1001, 0b0001)
|
||||
# set CBUS0
|
||||
ftdi.set_cbus_gpio(0x1)
|
||||
# get CBUS3
|
||||
cbus3 = ftdi.get_cbus_gpio() >> 3
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# it is possible to open the ftdi object from an existing serial connection:
|
||||
port = serial_for_url('ftdi:///1')
|
||||
ftdi = port.ftdi
|
||||
ftdi.has_cbus
|
||||
# etc...
|
||||
|
||||
.. _reserved_pins:
|
||||
|
||||
Reserved pins
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
GPIO pins vs. feature pins
|
||||
``````````````````````````
|
||||
|
||||
It is important to note that the reserved pins do not change the pin
|
||||
assignment, *i.e.* the lowest pins of a port may become unavailable as regular
|
||||
GPIO when the feature is enabled:
|
||||
|
||||
Example
|
||||
.......
|
||||
|
||||
|I2C| feature reserves
|
||||
the three first pins, as *SCL*, *SDA output*, *SDA input* (w/o clock stretching
|
||||
feature which also reserves another pin). This means that AD0, AD1 and AD2,
|
||||
that is b\ :sub:`0`\ , b\ :sub:`1`\ , b\ :sub:`2`\ cannot be directly
|
||||
accessed.
|
||||
|
||||
The first accessible GPIO pin in this case is no longer AD0 but AD3, which
|
||||
means that b\ :sub:`3`\ becomes the lowest bit which can be read/written.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# use I2C feature
|
||||
i2c = I2cController()
|
||||
# configure the I2C feature, and predefines the direction of the GPIO pins
|
||||
i2c.configure('ftdi:///1', direction=0x78)
|
||||
gpio = i2c.get_gpio()
|
||||
# read whole port
|
||||
pins = gpio.read()
|
||||
# clearing out I2C bits (SCL, SDAo, SDAi)
|
||||
pins &= 0x07
|
||||
# set AD4
|
||||
pins |= 1 << 4
|
||||
# update GPIO output
|
||||
gpio.write(pins)
|
125
lib/python3.11/site-packages/pyftdi/doc/index.rst
Normal file
125
lib/python3.11/site-packages/pyftdi/doc/index.rst
Normal file
|
@ -0,0 +1,125 @@
|
|||
PyFtdi
|
||||
======
|
||||
|
||||
.. cannot use defs.rst here, as PyPi wants a standalone file.
|
||||
.. |I2C| replace:: I\ :sup:`2`\ C
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
The latest PyFtdi online documentation is always available from
|
||||
`here <https://eblot.github.io/pyftdi>`_.
|
||||
|
||||
Beware the online version may be more recent than the PyPI hosted version, as
|
||||
intermediate development versions are not published to
|
||||
`PyPi <https://pypi.org/project/pyftdi>`_.
|
||||
|
||||
PyFtdi documentation can be locally build with Sphinx, see the installation
|
||||
instructions.
|
||||
|
||||
Source code
|
||||
-----------
|
||||
|
||||
PyFtdi releases are available from the Python Package Index from
|
||||
`PyPi <https://pypi.org/project/pyftdi>`_.
|
||||
|
||||
PyFtdi development code is available from
|
||||
`GitHub <https://github.com/eblot/pyftdi>`_.
|
||||
|
||||
Overview
|
||||
--------
|
||||
|
||||
PyFtdi aims at providing a user-space driver for popular FTDI devices,
|
||||
implemented in pure Python language.
|
||||
|
||||
Supported FTDI devices include:
|
||||
|
||||
* UART and GPIO bridges
|
||||
|
||||
* FT232R (single port, 3Mbps)
|
||||
* FT230X/FT231X/FT234X (single port, 3Mbps)
|
||||
|
||||
* UART and multi-serial protocols (SPI, |I2C|, JTAG) bridges
|
||||
|
||||
* FT2232C/D (dual port, clock up to 6 MHz)
|
||||
* 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
|
||||
--------
|
||||
|
||||
PyFtdi currently supports the following features:
|
||||
|
||||
* UART/Serial USB converter, up to 12Mbps (depending on the FTDI device
|
||||
capability)
|
||||
* GPIO/Bitbang support, with 8-bit asynchronous, 8-bit synchronous and
|
||||
8-/16-bit MPSSE variants
|
||||
* SPI master, with simultanous GPIO support, up to 12 pins per port,
|
||||
with support for non-byte sized transfer
|
||||
* |I2C| master, with simultanous GPIO support, up to 14 pins per port
|
||||
* Basic JTAG master capabilities
|
||||
* EEPROM support (some parameters cannot yet be modified, only retrieved)
|
||||
* Experimental CBUS support on selected devices, 4 pins per port
|
||||
|
||||
Supported host OSes
|
||||
-------------------
|
||||
|
||||
* macOS
|
||||
* Linux
|
||||
* FreeBSD
|
||||
* Windows, although not officially supported
|
||||
|
||||
.. EOT
|
||||
|
||||
Warning
|
||||
-------
|
||||
|
||||
Starting with version *v0.40.0*, several API changes are being introduced.
|
||||
While PyFtdi tries to maintain backward compatibility with previous versions,
|
||||
some of these changes may require existing clients to update calls to PyFtdi.
|
||||
|
||||
Do not upgrade to *v0.40.0* or above without testing your client against the
|
||||
new PyFtdi releases. PyFtdi versions up to *v0.39.9* keep a stable API
|
||||
with *v0.22+* series.
|
||||
|
||||
See the *Major Changes* section on the online documentation for details about
|
||||
potential API breaks.
|
||||
|
||||
|
||||
Major changes
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
* *read* methods now return ``bytearray`` instead of `Array('B')` so that
|
||||
pyserial ``readline()`` may be used. It also brings some performance
|
||||
improvements.
|
||||
* PyFtdi URLs now supports ``bus:address`` alternative specifiers, which
|
||||
required to augment the ``open_*()`` methods with new, optional parameters.
|
||||
* ``SpiController`` reserves only one slave line (*/CS*) where it used to
|
||||
reserve 4 slave lines in previous releases. This frees more GPIOs when
|
||||
default value is used - it is nevertheless still possible to reserve up to 5
|
||||
slave lines.
|
||||
* type hinting is used for most, if not all, public methods.
|
||||
* simplified baudrate divider calculation.
|
||||
|
||||
PyFTDI in details
|
||||
-----------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:glob:
|
||||
|
||||
features
|
||||
requirements
|
||||
installation
|
||||
urlscheme
|
||||
tools
|
||||
api/index
|
||||
pinout
|
||||
gpio
|
||||
eeprom
|
||||
testing
|
||||
troubleshooting
|
||||
authors
|
||||
license
|
274
lib/python3.11/site-packages/pyftdi/doc/installation.rst
Normal file
274
lib/python3.11/site-packages/pyftdi/doc/installation.rst
Normal file
|
@ -0,0 +1,274 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Prerequisites
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
PyFTDI_ relies on PyUSB_, which requires a native dependency: libusb 1.x.
|
||||
|
||||
The actual command to install depends on your OS and/or your distribution,
|
||||
see below
|
||||
|
||||
.. _install_linux:
|
||||
|
||||
Debian/Ubuntu Linux
|
||||
```````````````````
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
apt-get install libusb-1.0
|
||||
|
||||
On Linux, you also need to create a `udev` configuration file to allow
|
||||
user-space processes to access to the FTDI devices. There are many ways to
|
||||
configure `udev`, here is a typical setup:
|
||||
|
||||
::
|
||||
|
||||
# /etc/udev/rules.d/11-ftdi.rules
|
||||
|
||||
# FT232AM/FT232BM/FT232R
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6001", GROUP="plugdev", MODE="0664"
|
||||
# FT2232C/FT2232D/FT2232H
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6010", GROUP="plugdev", MODE="0664"
|
||||
# FT4232/FT4232H
|
||||
SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6011", GROUP="plugdev", MODE="0664"
|
||||
# FT232H
|
||||
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**
|
||||
|
||||
You need to add a line for each device with a custom VID / PID pair you
|
||||
declare, see :ref:`custom_vid_pid` for details.
|
||||
|
||||
You need to unplug / plug back the FTDI device once this file has been
|
||||
created so that `udev` loads the rules for the matching device, or
|
||||
alternatively, inform the ``udev`` daemon about the changes:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger
|
||||
|
||||
With this setup, be sure to add users that want to run PyFtdi_ to the
|
||||
`plugdev` group, *e.g.*
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo adduser $USER plugdev
|
||||
|
||||
Remember that you need to log out / log in to get the above command
|
||||
effective, or start a subshell to try testing PyFtdi_:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
newgrp plugdev
|
||||
|
||||
|
||||
.. _install_macos:
|
||||
|
||||
Homebrew macOS
|
||||
``````````````
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
brew install libusb
|
||||
|
||||
|
||||
.. _install_windows:
|
||||
|
||||
Windows
|
||||
```````
|
||||
|
||||
Windows is not officially supported (*i.e.* not tested) but some users have
|
||||
reported successful installations. Windows requires a specific libusb backend
|
||||
installation.
|
||||
|
||||
Zadig
|
||||
.....
|
||||
|
||||
The probably easiest way to deal with libusb on Windows is to use Zadig_
|
||||
|
||||
1. Start up the Zadig utility
|
||||
|
||||
2. Select ``Options/List All Devices``, then select the FTDI devices you want
|
||||
to communicate with. Its names depends on your hardware, *i.e.* the name
|
||||
stored in the FTDI EEPROM.
|
||||
|
||||
* With FTDI devices with multiple channels, such as FT2232 (2 channels) and
|
||||
FT4232 (4 channels), you **must** install the driver for the composite
|
||||
parent, **not** for the individual interfaces. If you install the driver
|
||||
for each interface, each interface will be presented as a unique FTDI
|
||||
device and you may have difficulties to select a specific FTDI device port
|
||||
once the installation is completed. To make the composite parents to appear
|
||||
in the device list, uncheck the ``Options/Ignore Hubs or Composite Parents``
|
||||
menu item.
|
||||
|
||||
* Be sure to select the parent device, *i.e.* the device name should not end
|
||||
with *(Interface N)*, where *N* is the channel number.
|
||||
|
||||
* for example *Dual RS232-HS* represents the composite parent, while
|
||||
*Dual RS232-HS (Interface 0)* represents a single channel of the FTDI
|
||||
device. Always select the former.
|
||||
|
||||
3. Select ``libusb-win32`` (not ``WinUSB``) in the driver list.
|
||||
|
||||
4. Click on ``Replace Driver``
|
||||
|
||||
See also `Libusb on Windows`_
|
||||
|
||||
|
||||
.. _install_python:
|
||||
|
||||
Python
|
||||
~~~~~~
|
||||
|
||||
Python dependencies
|
||||
```````````````````
|
||||
|
||||
Dependencies should be automatically installed with PIP.
|
||||
|
||||
* pyusb >= 1.0.0, != 1.2.0
|
||||
* pyserial >= 3.0
|
||||
|
||||
Do *not* install PyUSB_ from GitHub development branch (``master``, ...).
|
||||
Always prefer a stable, tagged release.
|
||||
|
||||
PyUSB 1.2.0 also broke the backward compatibility of the Device API, so it will
|
||||
not work with PyFtdi.
|
||||
|
||||
Installing with PIP
|
||||
```````````````````
|
||||
|
||||
PIP should automatically install the missing dependencies.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
pip3 install pyftdi
|
||||
|
||||
|
||||
.. _install_from_source:
|
||||
|
||||
Installing from source
|
||||
``````````````````````
|
||||
|
||||
If you prefer to install from source, check out a fresh copy from PyFtdi_
|
||||
github repository.
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git clone https://github.com/eblot/pyftdi.git
|
||||
cd pyftdi
|
||||
# note: 'pip3' may simply be 'pip' on some hosts
|
||||
pip3 install -r requirements.txt
|
||||
python3 setup.py install
|
||||
|
||||
|
||||
.. _generate_doc:
|
||||
|
||||
Generating the documentation
|
||||
````````````````````````````
|
||||
|
||||
Follow :ref:`install_from_source` then:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
pip3 install setuptools wheel sphinx sphinx_autodoc_typehints
|
||||
# Shpinx Read the Doc theme seems to never get a release w/ fixed issues
|
||||
pip3 install -U -e git+https://github.com/readthedocs/sphinx_rtd_theme.git@2b8717a3647cc650625c566259e00305f7fb60aa#egg=sphinx_rtd_theme
|
||||
sphinx-build -b html pyftdi/doc .
|
||||
|
||||
The documentation may be accessed from the generated ``index.html`` entry file.
|
||||
|
||||
|
||||
Post-installation sanity check
|
||||
``````````````````````````````
|
||||
|
||||
Open a *shell*, or a *CMD* on Windows
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
python3 # or 'python' on Windows
|
||||
from pyftdi.ftdi import Ftdi
|
||||
Ftdi.show_devices()
|
||||
|
||||
should list all the FTDI devices available on your host.
|
||||
|
||||
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
|
||||
with no serial number, connected to the host:
|
||||
|
||||
.. code-block::
|
||||
|
||||
Available interfaces:
|
||||
ftdi://ftdi:232h:FT1PWZ0Q/1 (C232HD-DDHSP-0)
|
||||
ftdi://ftdi:2232/1 (Dual RS232-HS)
|
||||
ftdi://ftdi:2232/2 (Dual RS232-HS)
|
||||
|
||||
|
||||
Note that FTDI devices with custom VID/PID are not listed with this simple
|
||||
command, please refer to the PyFtdi_ API to add custom identifiers, *i.e.* see
|
||||
:py:meth:`pyftdi.ftdi.Ftdi.add_custom_vendor` and
|
||||
:py:meth:`pyftdi.ftdi.Ftdi.add_custom_product` APIs.
|
||||
|
||||
|
||||
.. _custom_vid_pid:
|
||||
|
||||
Custom USB vendor and product IDs
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PyFtdi only recognizes FTDI official vendor and product IDs.
|
||||
|
||||
If you have an FTDI device with an EEPROM with customized IDs, you need to tell
|
||||
PyFtdi to support those custom USB identifiers.
|
||||
|
||||
Custom PID
|
||||
``````````
|
||||
|
||||
To support a custom product ID (16-bit integer) with the official FTDI ID, add
|
||||
the following code **before** any call to an FTDI ``open()`` method.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pyftdi.ftdi import Ftdi
|
||||
|
||||
Ftdi.add_custom_product(Ftdi.DEFAULT_VENDOR, product_id)
|
||||
|
||||
Custom VID
|
||||
``````````
|
||||
|
||||
To support a custom vendor ID and product ID (16-bit integers), add the
|
||||
following code **before** any call to an FTDI ``open()`` method.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pyftdi.ftdi import Ftdi
|
||||
|
||||
Ftdi.add_custom_vendor(vendor_id)
|
||||
Ftdi.add_custom_product(vendor_id, product_id)
|
||||
|
||||
You may also specify an arbitrary string to each method if you want to specify
|
||||
a URL by custom vendor and product names instead of their numerical values:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from pyftdi.ftdi import Ftdi
|
||||
|
||||
Ftdi.add_custom_vendor(0x1234, 'myvendor')
|
||||
Ftdi.add_custom_product(0x1234, 0x5678, 'myproduct')
|
||||
|
||||
f1 = Ftdi.create_from_url('ftdi://0x1234:0x5678/1')
|
||||
f2 = Ftdi.create_from_url('ftdi://myvendor:myproduct/2')
|
||||
|
||||
.. note::
|
||||
|
||||
Remember that on OSes that require per-device access permissions such as
|
||||
Linux, you also need to add the custom VID/PID entry to the configuration
|
||||
file, see :ref:`Linux installation <install_linux>` ``udev`` rule file.
|
44
lib/python3.11/site-packages/pyftdi/doc/license.rst
Normal file
44
lib/python3.11/site-packages/pyftdi/doc/license.rst
Normal file
|
@ -0,0 +1,44 @@
|
|||
License
|
||||
-------
|
||||
|
||||
.. include:: defs.rst
|
||||
|
||||
For historical reasons (PyFtdi has been initially developed as a compatibility
|
||||
layer with libftdi_), the main ``ftdi.py`` file had originally been licensed
|
||||
under the same license as the libftdi_ project, the GNU Lesser General Public
|
||||
License LGPL v2 license. It does not share code from this project anymore, but
|
||||
implements a similar API.
|
||||
|
||||
From my perspective, you may use it freely in open source or close source, free
|
||||
or commercial projects as long as you comply with the BSD 3-clause license.
|
||||
|
||||
|
||||
BSD 3-clause
|
||||
~~~~~~~~~~~~
|
||||
|
||||
::
|
||||
|
||||
Copyright (c) 2008-2021 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
All Rights Reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the author nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL NEOTION BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
39
lib/python3.11/site-packages/pyftdi/doc/pinout.rst
Normal file
39
lib/python3.11/site-packages/pyftdi/doc/pinout.rst
Normal file
|
@ -0,0 +1,39 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
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
|
||||
``ADBUS3`` ``BDBUS3`` GPIO3 CTS GPIO3 CS0 TMS Brown
|
||||
``ADBUS4`` ``BDBUS4`` GPIO4 DTR GPIO4 CS1/GPIO4 Grey
|
||||
``ADBUS5`` ``BDBUS5`` GPIO5 DSR GPIO5 CS2/GPIO5 Purple
|
||||
``ADBUS6`` ``BDBUS6`` GPIO6 DCD GPIO6 CS3/GPIO6 White
|
||||
``ADBUS7`` ``BDBUS7`` GPIO7 RI RSCK [#rck]_ CS4/GPIO7 RCLK Blue
|
||||
``ACBUS0`` ``BCBUS0`` GPIO8 GPIO8
|
||||
``ACBUS1`` ``BCBUS1`` GPIO9 GPIO9
|
||||
``ACBUS2`` ``BCBUS2`` GPIO10 GPIO10
|
||||
``ACBUS3`` ``BCBUS3`` GPIO11 GPIO11
|
||||
``ACBUS4`` ``BCBUS4`` GPIO12 GPIO12
|
||||
``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.
|
||||
.. [#i2c] FTDI pins are either configured as input or output. As |I2C| SDA line
|
||||
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_,
|
||||
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.
|
54
lib/python3.11/site-packages/pyftdi/doc/requirements.rst
Normal file
54
lib/python3.11/site-packages/pyftdi/doc/requirements.rst
Normal file
|
@ -0,0 +1,54 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Python_ 3.9 or above is required.
|
||||
|
||||
* PyFtdi *v0.55* is the last PyFtdi version to support Python 3.8.
|
||||
|
||||
|
||||
* Python 3.7 has reached end-of-life on June 27rd, 2023.
|
||||
|
||||
* PyFtdi *v0.53* is the last PyFtdi version to support Python 3.6.
|
||||
|
||||
* Python 3.6 has reached end-of-life on December 23rd, 2021.
|
||||
|
||||
* PyFtdi *v0.52* is the last PyFtdi version to support Python 3.5.
|
||||
|
||||
* Python 3.5 has reached end-of-life on September 5th, 2020.
|
||||
|
||||
PyFtdi_ relies on PyUSB_, which itself depends on one of the following native
|
||||
libraries:
|
||||
|
||||
* libusb_, currently tested with 1.0.23
|
||||
|
||||
PyFtdi_ does not depend on any other native library, and only uses standard
|
||||
Python modules along with PyUSB_ and pyserial_.
|
||||
|
||||
PyFtdi_ is beeing tested with PyUSB_ 1.1.0.
|
||||
|
||||
Development
|
||||
~~~~~~~~~~~
|
||||
|
||||
PyFtdi_ is developed on macOS platforms (64-bit kernel), and is validated on a
|
||||
regular basis on Linux hosts.
|
||||
|
||||
As it contains no native code, it should work on any PyUSB_ and libusb_
|
||||
supported platforms. However, M$ Windows is a seamless source of issues and is
|
||||
not officially supported, although users have reported successful installation
|
||||
with Windows 7 for example. Your mileage may vary.
|
||||
|
||||
|
||||
API breaks
|
||||
~~~~~~~~~~
|
||||
|
||||
Starting with version *v0.40.0*, several API changes are being introduced.
|
||||
While PyFtdi tries to maintain backward compatibility with previous versions,
|
||||
some of these changes may require existing clients to update calls to PyFtdi.
|
||||
|
||||
Do not upgrade to *v0.40.0* or above without testing your client against the
|
||||
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.
|
126
lib/python3.11/site-packages/pyftdi/doc/testing.rst
Normal file
126
lib/python3.11/site-packages/pyftdi/doc/testing.rst
Normal file
|
@ -0,0 +1,126 @@
|
|||
Testing
|
||||
-------
|
||||
|
||||
.. include:: defs.rst
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
Testing PyFTDI is challenging because it relies on several pieces of hardware:
|
||||
|
||||
* one or more FTDI device
|
||||
* |I2C|, SPI, JTAG bus slaves or communication equipment for UART
|
||||
|
||||
The ``tests`` directory contain several tests files, which are primarily aimed
|
||||
at demonstrating usage of PyFTDI in common use cases.
|
||||
|
||||
Most unit tests are disabled, as they require specific slaves, with a dedicated
|
||||
HW wiring. Reproducing such test environments can be challenging, as it
|
||||
requires dedicated test benchs.
|
||||
|
||||
This is a growing concern as PyFTDI keeps evolving, and up to now, regression
|
||||
tests were hard to run.
|
||||
|
||||
Hardware tests
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Please refer to the ``pyftdi/tests`` directory. There is one file dedicated to
|
||||
each feature to test. Note that you need to read and edit these tests files to
|
||||
fit your actual test environment, and enable the proper unit test cases, as
|
||||
most are actually disabled by default.
|
||||
|
||||
You need specific bus slaves to perform most of these tests.
|
||||
|
||||
.. _virtual_framework:
|
||||
|
||||
Virtual test framework
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
With PyFTDI v0.45, a new test module enables PyFTDI API partial testing using a
|
||||
pure software environment with no hardware. This also eases automatic testing
|
||||
within a continuous integration environment.
|
||||
|
||||
This new module implements a virtual USB backend for PyUSB, which creates some
|
||||
kind of virtual, limited USB stack. The PyUSB can be told to substitute the
|
||||
native platform's libusb with this module.
|
||||
|
||||
This module, ``usbvirt`` can be dynamically confifured with the help of YaML
|
||||
definition files to create one or more virtual FTDI devices on a virtual USB
|
||||
bus topology. This enables to test ``usbtools`` module to enumerate, detect,
|
||||
report and access FTDI devices using the regular :doc:`urlscheme` syntax.
|
||||
|
||||
``usbvirt`` also routes all vendor-specific USB API calls to a secondary
|
||||
``ftdivirt`` module, which is in charge of handling all FTDI USB requests.
|
||||
|
||||
This module enables testing PyFtdi_ APIs. It also re-uses the MPSSE tracker
|
||||
engine to decode and verify MPSSE requests used to support |I2C|, SPI and UART
|
||||
features.
|
||||
|
||||
For now, it is able to emulate most of GPIO requests (async, sync and MPSSE)
|
||||
and UART input/output. It also manages the frequency and baudrate settings.
|
||||
|
||||
It is not able to emulate the MPSSE commands (with the exception of set and get
|
||||
GPIO values), as it represents a massive workload...
|
||||
|
||||
Beware: WIP
|
||||
```````````
|
||||
|
||||
This is an experimental work in progress, which is its early inception stage.
|
||||
|
||||
It has nevertheless already revealed a couple of bugs that had been hiding
|
||||
within PyFtdi_ for years.
|
||||
|
||||
There is a large work effort ahead to be able to support more use cases and
|
||||
tests more APIs, and many unit tests to write.
|
||||
|
||||
It cannot replace hardware tests with actual boards and slaves, but should
|
||||
simplify test setup and help avoiding regression issues.
|
||||
|
||||
|
||||
Usage
|
||||
`````
|
||||
|
||||
No hardware is required to run these tests, to even a single FTDI device.
|
||||
|
||||
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
|
||||
`````````````
|
||||
|
||||
The ``pyftdi/tests/resources`` directory contains definition files which are
|
||||
loaded by the mock unit tests.
|
||||
|
||||
Although it is possible to create fine grained USB device definitions, the
|
||||
configuration loader tries to automatically define missing parts to match the
|
||||
USB device topology of FTDI devices.
|
||||
|
||||
This enables to create simple definition files without having to mess with low
|
||||
level USB definitions whenever possible.
|
||||
|
||||
EEPROM content
|
||||
..............
|
||||
|
||||
The :ref:`ftconf` tool can be used to load, modify and generate the content of
|
||||
a virtual EEPROM, see :doc:`eeprom`.
|
||||
|
||||
Examples
|
||||
........
|
||||
|
||||
* An example of a nearly comprehensive syntax can be found in ``ft232h.yaml``.
|
||||
* Another, much more simple example with only mandatory settings can be found
|
||||
in ``ft230x.yaml``.
|
||||
* An example of multiple FTDI device definitions can be found in
|
||||
``ftmany.yaml``
|
||||
|
||||
|
||||
Availability
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Note that unit tests and the virtual infrastructure are not included in the
|
||||
distributed Python packages, they are only available from the git repository.
|
137
lib/python3.11/site-packages/pyftdi/doc/tools.rst
Normal file
137
lib/python3.11/site-packages/pyftdi/doc/tools.rst
Normal file
|
@ -0,0 +1,137 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
.. _tools:
|
||||
|
||||
Tools
|
||||
-----
|
||||
|
||||
Overview
|
||||
~~~~~~~~
|
||||
|
||||
PyFtdi_ comes with a couple of scripts designed to help using PyFtdi_ APIs,
|
||||
and can be useful to quick start working with PyFtdi_.
|
||||
|
||||
Scripts
|
||||
~~~~~~~
|
||||
|
||||
.. _ftdi_urls:
|
||||
|
||||
``ftdi_urls``
|
||||
`````````````
|
||||
|
||||
This tiny script ``ftdi_urls.py`` to list the available, *i.e.* detected,
|
||||
FTDI devices connected to the host, and the URLs than can be used to open a
|
||||
:py:class:`pyftdi.ftdi.Ftdi` instance with the
|
||||
:py:class:`pyftdi.ftdi.Ftdi.open_from_url` family and ``configure`` methods.
|
||||
|
||||
|
||||
``ftconf``
|
||||
``````````
|
||||
|
||||
``ftconf.py`` is a companion script to help managing the content of
|
||||
the FTDI EEPROM from the command line. See the :ref:`ftconf` documentation.
|
||||
|
||||
|
||||
.. _i2cscan:
|
||||
|
||||
``i2cscan``
|
||||
```````````
|
||||
|
||||
The ``i2cscan.py`` script helps to discover which I2C devices
|
||||
are connected to the FTDI I2C bus.
|
||||
|
||||
|
||||
.. _pyterm.py:
|
||||
|
||||
``pyterm``
|
||||
``````````
|
||||
|
||||
``pyterm.py`` is a simple serial terminal that can be used to test the serial
|
||||
port feature, see the :ref:`pyterm` documentation.
|
||||
|
||||
|
||||
Where to find these tools?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These scripts can be downloaded from PyFtdiTools_, and are also installed along
|
||||
with the PyFtdi_ module on the local host.
|
||||
|
||||
The location of the scripts depends on how PyFtdi_ has been installed and the
|
||||
type of hosts:
|
||||
|
||||
* on linux and macOS, there are located in the ``bin/`` directory, that is the
|
||||
directory where the Python interpreter is installed.
|
||||
|
||||
* on Windows, there are located in the ``Scripts/`` directory, which is a
|
||||
subdirectory of the directory where the Python interpreter is installed.
|
||||
|
||||
|
||||
.. _common_option_switches:
|
||||
|
||||
Common options switches
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PyFtdi_ tools share many common option switches:
|
||||
|
||||
.. _option_d:
|
||||
|
||||
``-d``
|
||||
Enable debug mode, which emits Python traceback on exceptions
|
||||
|
||||
.. _option_h:
|
||||
|
||||
``-h``
|
||||
Show quick help and exit
|
||||
|
||||
.. _option_P_:
|
||||
|
||||
``-P <vidpid>``
|
||||
Add custom vendor and product identifiers.
|
||||
|
||||
PyFtdi_ only recognizes FTDI official USB vendor identifier (*0x403*) and
|
||||
the USB identifiers of their products.
|
||||
|
||||
In order to use alternative VID/PID values, the PyFtdi_ tools accept the
|
||||
``-P`` option to describe those products
|
||||
|
||||
The ``vidpid`` argument should match the following format:
|
||||
|
||||
``[vendor_name=]<vendor_id>:[product_name=]<product_id>``
|
||||
|
||||
* ``vendor_name`` and ``product_name`` are optional strings, they may be
|
||||
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). 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.
|
||||
|
||||
examples:
|
||||
|
||||
* ``0x403:0x9999``, *vid:pid* short syntax, with no alias names;
|
||||
a matching FTDI :ref:`URL <url_scheme>` would be ``ftdi://ftdi:0x9999/1``
|
||||
* ``mycompany=0x666:myproduct=0xcafe``, *vid:pid* complete syntax with
|
||||
aliases; matching FTDI :ref:`URLs <url_scheme>` could be:
|
||||
|
||||
* ``ftdi://0x666:0x9999/1``
|
||||
* ``ftdi://mycompany:myproduct/1``
|
||||
* ``ftdi://mycompany:0x9999/1``
|
||||
* ...
|
||||
|
||||
.. _option_v:
|
||||
|
||||
``-v``
|
||||
Increase verbosity, useful for debugging the tool. It can be repeated to
|
||||
increase more the verbosity.
|
||||
|
||||
.. _option_V_:
|
||||
|
||||
``-V <virtual>``
|
||||
Load a virtual USB device configuration, to use a virtualized FTDI/EEPROM
|
||||
environment. This is useful for PyFtdi_ development, and to test EEPROM
|
||||
configuration with a virtual setup. This option is not useful for regular
|
||||
usage. See :ref:`virtual_framework`.
|
||||
|
122
lib/python3.11/site-packages/pyftdi/doc/troubleshooting.rst
Normal file
122
lib/python3.11/site-packages/pyftdi/doc/troubleshooting.rst
Normal file
|
@ -0,0 +1,122 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
Reporting a bug
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Please do not contact the author by email. The preferered method to report bugs
|
||||
and/or enhancement requests is through
|
||||
`GitHub <https://github.com/eblot/pyftdi/issues>`_.
|
||||
|
||||
Please be sure to read the next sections before reporting a new issue.
|
||||
|
||||
Logging
|
||||
~~~~~~~
|
||||
|
||||
FTDI uses the `pyftdi` logger.
|
||||
|
||||
It emits log messages with raw payload bytes at DEBUG level, and data loss
|
||||
at ERROR level.
|
||||
|
||||
Common error messages
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
"Error: No backend available"
|
||||
`````````````````````````````
|
||||
|
||||
libusb native library cannot be loaded. Try helping the dynamic loader:
|
||||
|
||||
* On Linux: ``export LD_LIBRARY_PATH=<path>``
|
||||
|
||||
where ``<path>`` is the directory containing the ``libusb-1.*.so``
|
||||
library file
|
||||
|
||||
* On macOS: ``export DYLD_LIBRARY_PATH=.../lib``
|
||||
|
||||
where ``<path>`` is the directory containing the ``libusb-1.*.dylib``
|
||||
library file
|
||||
|
||||
* On Windows:
|
||||
|
||||
Try to copy the USB dll where the Python executable is installed, along
|
||||
with the other Python DLLs.
|
||||
|
||||
If this happens while using an exe created by pyinstaller:
|
||||
``copy C:\Windows\System32\libusb0.dll <path>``
|
||||
|
||||
where ``<path>`` is the directory containing the executable created
|
||||
by pyinstaller. This assumes you have installed libusb (using a tool
|
||||
like Zadig) as referenced in the installation guide for Windows.
|
||||
|
||||
|
||||
"Error: Access denied (insufficient permissions)"
|
||||
`````````````````````````````````````````````````
|
||||
|
||||
The system may already be using the device.
|
||||
|
||||
* On macOS: starting with 10.9 "*Mavericks*", macOS ships with a native FTDI
|
||||
kernel extension that preempts access to the FTDI device.
|
||||
|
||||
Up to 10.13 "*High Sierra*", this driver can be unloaded this way:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo kextunload [-v] -bundle com.apple.driver.AppleUSBFTDI
|
||||
|
||||
You may want to use an alias or a tiny script such as
|
||||
``pyftdi/bin/uphy.sh``
|
||||
|
||||
Please note that the system automatically reloads the driver, so it may be
|
||||
useful to move the kernel extension so that the system never loads it.
|
||||
|
||||
.. warning::
|
||||
|
||||
From macOS 10.14 "*Mojave*", the Apple kernel extension peacefully
|
||||
co-exists with libusb_ and PyFtdi_, so you no longer need - and **should
|
||||
not attempt** - to unload the kernel extension. If you still experience
|
||||
this error, please verify you have not installed another driver from FTDI,
|
||||
such as FTDI's D2XX.
|
||||
|
||||
* On Linux: it may indicate a missing or invalid udev configuration. See
|
||||
the :doc:`installation` section.
|
||||
|
||||
* This error message may also be triggered whenever the communication port is
|
||||
already in use.
|
||||
|
||||
|
||||
"Error: The device has no langid"
|
||||
`````````````````````````````````
|
||||
|
||||
* On Linux, it usually comes from the same installation issue as the
|
||||
``Access denied`` error: the current user is not granted the permissions to
|
||||
access the FTDI device, therefore pyusb cannot read the FTDI registers. Check
|
||||
out the :doc:`installation` section.
|
||||
|
||||
|
||||
"Bus error / Access violation"
|
||||
``````````````````````````````
|
||||
|
||||
PyFtdi does not use any native library, but relies on PyUSB_ and libusb_. The
|
||||
latter uses native code that may trigger OS error. Some early development
|
||||
versions of libusb_, for example 1.0.22-b…, have been reported to trigger
|
||||
such issues. Please ensure you use a stable/final versions of libusb_ if you
|
||||
experience this kind of fatal error.
|
||||
|
||||
|
||||
"serial.serialutil.SerialException: Unable to open USB port"
|
||||
````````````````````````````````````````````````````````````
|
||||
|
||||
May be caused by a conflict with the FTDI virtual COM port (VCOM). Try
|
||||
uninstalling the driver. On macOS, refer to this `FTDI macOS guide`_.
|
||||
|
||||
|
||||
Slow initialisation on OS X El Capitan
|
||||
``````````````````````````````````````
|
||||
|
||||
It may take several seconds to open or enumerate FTDI devices.
|
||||
|
||||
If you run libusb <= v1.0.20, be sure to read the `Libusb issue on macOS`_
|
||||
with OS X 10.11+.
|
||||
|
125
lib/python3.11/site-packages/pyftdi/doc/urlscheme.rst
Normal file
125
lib/python3.11/site-packages/pyftdi/doc/urlscheme.rst
Normal file
|
@ -0,0 +1,125 @@
|
|||
.. include:: defs.rst
|
||||
|
||||
.. _url_scheme:
|
||||
|
||||
URL Scheme
|
||||
----------
|
||||
|
||||
There are two ways to open a connection to an `Ftdi` object.
|
||||
|
||||
The recommended way to open a connection is to specify connection details
|
||||
using a URL. The URL scheme is defined as:
|
||||
|
||||
::
|
||||
|
||||
ftdi://[vendor][:[product][:serial|:bus:address|:index]]/interface
|
||||
|
||||
where:
|
||||
|
||||
* vendor: the USB vendor ID of the manufacturer
|
||||
|
||||
* ex: ``ftdi`` or ``0x403``
|
||||
|
||||
* product: the USB product ID of the device
|
||||
|
||||
* ex: ``232h`` or ``0x6014``
|
||||
* Supported product IDs: ``0x6001``, ``0x6010``, ``0x6011``, ``0x6014``,
|
||||
``0x6015``
|
||||
* Supported product aliases:
|
||||
|
||||
* ``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
|
||||
uniquely identify a specific FTDI device. However, some FTDI device are not
|
||||
fitted with an EEPROM, or the EEPROM is either corrupted or erased. In this
|
||||
case, FTDI devices report no serial number
|
||||
|
||||
Examples:
|
||||
* ``ftdi://ftdi:232h:FT0FMF6V/1``
|
||||
* ``ftdi://:232h:FT0FMF6V/1``
|
||||
* ``ftdi://::FT0FMF6V/1``
|
||||
|
||||
* ``bus:addess``: it is possible to select a FTDI device through a bus:address
|
||||
pair, specified as *hexadecimal* integer values.
|
||||
|
||||
Examples:
|
||||
* ``ftdi://ftdi:232h:10:22/1``
|
||||
* ``ftdi://ftdi:232h:10:22/1``
|
||||
* ``ftdi://::10:22/1``
|
||||
|
||||
Here, bus ``(0x)10`` = 16 (decimal) and address ``(0x)22`` = 34 (decimal)
|
||||
|
||||
* ``index``: an integer - not particularly useful, as it depends on the
|
||||
enumeration order on the USB buses, and may vary from on session to another.
|
||||
|
||||
* ``interface``: the interface of FTDI device, starting from 1
|
||||
|
||||
* ``1`` for 230x and 232\* devices,
|
||||
* ``1`` or ``2`` for 2232\* devices,
|
||||
* ``1``, ``2``, ``3`` or ``4`` for 4232\* devices
|
||||
|
||||
All parameters but the interface are optional, PyFtdi tries to find the best
|
||||
match. Therefore, if you have a single FTDI device connected to your system,
|
||||
``ftdi:///1`` should be enough.
|
||||
|
||||
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. 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`.
|
||||
|
||||
Note that opening an FTDI connection with a URL ending with `?` is interpreted
|
||||
as a query for matching FTDI devices and immediately stop. With this special
|
||||
URL syntax, the avaialble devices are printed out to the standard output, and
|
||||
the Python interpreter is forced to exit (`SystemExit` is raised).
|
||||
|
||||
When simple enumeration of the available FTDI devices is needed - so that
|
||||
execution is not interrupted, two helper methods are available as
|
||||
:py:meth:`pyftdi.ftdi.Ftdi.list_devices` and
|
||||
:py:meth:`pyftdi.ftdi.Ftdi.show_devices` and accept the same URL syntax.
|
||||
|
||||
Opening a connection
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
URL-based methods to open a connection
|
||||
``````````````````````````````````````
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
open_from_url()
|
||||
open_mpsse_from_url()
|
||||
open_bitbang_from_url()
|
||||
|
||||
|
||||
Device-based methods to open a connection
|
||||
`````````````````````````````````````````
|
||||
|
||||
You may also open an Ftdi device from an existing PyUSB_ device, with the help
|
||||
of the ``open_from_device()`` helper method.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
open_from_device()
|
||||
open_mpsse_from_device()
|
||||
open_bitbang_from_device()
|
||||
|
||||
|
||||
Legacy methods to open a connection
|
||||
```````````````````````````````````
|
||||
|
||||
The old, deprecated method to open a connection is to use the ``open()``
|
||||
methods without the ``_from_url`` suffix, which accept VID, PID, and serial
|
||||
parameters (among others).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
open()
|
||||
open_mpsse()
|
||||
open_bitbang()
|
||||
|
||||
See the :ref:`ftdi_urls` tool to obtain the URLs for the connected FTDI
|
||||
devices.
|
1242
lib/python3.11/site-packages/pyftdi/eeprom.py
Normal file
1242
lib/python3.11/site-packages/pyftdi/eeprom.py
Normal file
File diff suppressed because it is too large
Load diff
2328
lib/python3.11/site-packages/pyftdi/ftdi.py
Normal file
2328
lib/python3.11/site-packages/pyftdi/ftdi.py
Normal file
File diff suppressed because it is too large
Load diff
532
lib/python3.11/site-packages/pyftdi/gpio.py
Normal file
532
lib/python3.11/site-packages/pyftdi/gpio.py
Normal file
|
@ -0,0 +1,532 @@
|
|||
# Copyright (c) 2014-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""GPIO/BitBang support for PyFdti"""
|
||||
|
||||
|
||||
from struct import calcsize as scalc, unpack as sunpack
|
||||
from typing import Iterable, Optional, Tuple, Union
|
||||
from .ftdi import Ftdi, FtdiError
|
||||
from .misc import is_iterable
|
||||
|
||||
|
||||
class GpioException(FtdiError):
|
||||
"""Base class for GPIO errors.
|
||||
"""
|
||||
|
||||
|
||||
class GpioPort:
|
||||
"""Duck-type GPIO port for GPIO all controllers.
|
||||
"""
|
||||
|
||||
|
||||
class GpioBaseController(GpioPort):
|
||||
"""GPIO controller for an FTDI port, in bit-bang legacy mode.
|
||||
|
||||
GPIO bit-bang mode is limited to the 8 lower pins of each GPIO port.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._ftdi = Ftdi()
|
||||
self._direction = 0
|
||||
self._width = 0
|
||||
self._mask = 0
|
||||
self._frequency = 0
|
||||
|
||||
@property
|
||||
def ftdi(self) -> Ftdi:
|
||||
"""Return the Ftdi instance.
|
||||
|
||||
:return: the Ftdi instance
|
||||
"""
|
||||
return self._ftdi
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""Reports whether a connection exists with the FTDI interface.
|
||||
|
||||
:return: the FTDI slave connection status
|
||||
"""
|
||||
return self._ftdi.is_connected
|
||||
|
||||
def configure(self, url: str, direction: int = 0,
|
||||
**kwargs) -> int:
|
||||
"""Open a new interface to the specified FTDI device in bitbang mode.
|
||||
|
||||
:param str url: a FTDI URL selector
|
||||
:param int direction: a bitfield specifying the FTDI GPIO direction,
|
||||
where high level defines an output, and low level defines an
|
||||
input
|
||||
:param initial: optional initial GPIO output value
|
||||
:param pace: optional pace in GPIO sample per second
|
||||
:return: actual bitbang pace in sample per second
|
||||
"""
|
||||
if self.is_connected:
|
||||
raise FtdiError('Already connected')
|
||||
kwargs = dict(kwargs)
|
||||
frequency = kwargs.get('frequency', None)
|
||||
if frequency is None:
|
||||
frequency = kwargs.get('baudrate', None)
|
||||
for k in ('direction', 'sync', 'frequency', 'baudrate'):
|
||||
if k in kwargs:
|
||||
del kwargs[k]
|
||||
self._frequency = self._configure(url, direction, frequency, **kwargs)
|
||||
|
||||
def close(self, freeze: bool = False) -> None:
|
||||
"""Close the GPIO port.
|
||||
|
||||
:param freeze: if set, FTDI port is not reset to its default
|
||||
state on close. This means the port is left with
|
||||
its current configuration and output signals.
|
||||
This feature should not be used except for very
|
||||
specific needs.
|
||||
"""
|
||||
if self._ftdi.is_connected:
|
||||
self._ftdi.close(freeze)
|
||||
|
||||
def get_gpio(self) -> GpioPort:
|
||||
"""Retrieve the GPIO port.
|
||||
|
||||
This method is mostly useless, it is a wrapper to duck type other
|
||||
GPIO APIs (I2C, SPI, ...)
|
||||
|
||||
:return: GPIO port
|
||||
"""
|
||||
return self
|
||||
|
||||
@property
|
||||
def direction(self) -> int:
|
||||
"""Reports the GPIO direction.
|
||||
|
||||
:return: a bitfield specifying the FTDI GPIO direction, where high
|
||||
level reports an output pin, and low level reports an input pin
|
||||
"""
|
||||
return self._direction
|
||||
|
||||
@property
|
||||
def pins(self) -> int:
|
||||
"""Report the configured GPIOs as a bitfield.
|
||||
|
||||
A true bit represents a GPIO, a false bit a reserved or not
|
||||
configured pin.
|
||||
|
||||
:return: always 0xFF for GpioController instance.
|
||||
"""
|
||||
return self._mask
|
||||
|
||||
@property
|
||||
def all_pins(self) -> int:
|
||||
"""Report the addressable GPIOs as a bitfield.
|
||||
|
||||
A true bit represents a pin which may be used as a GPIO, a false bit
|
||||
a reserved pin
|
||||
|
||||
:return: always 0xFF for GpioController instance.
|
||||
"""
|
||||
return self._mask
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
"""Report the FTDI count of addressable pins.
|
||||
|
||||
:return: the width of the GPIO port.
|
||||
"""
|
||||
return self._width
|
||||
|
||||
@property
|
||||
def frequency(self) -> float:
|
||||
"""Return the pace at which sequence of GPIO samples are read
|
||||
and written.
|
||||
"""
|
||||
return self._frequency
|
||||
|
||||
def set_frequency(self, frequency: Union[int, float]) -> None:
|
||||
"""Set the frequency at which sequence of GPIO samples are read
|
||||
and written.
|
||||
|
||||
:param frequency: the new frequency, in GPIO samples per second
|
||||
"""
|
||||
raise NotImplementedError('GpioBaseController cannot be instanciated')
|
||||
|
||||
def set_direction(self, pins: int, direction: int) -> None:
|
||||
"""Update the GPIO pin direction.
|
||||
|
||||
:param pins: which GPIO pins should be reconfigured
|
||||
:param direction: a bitfield of GPIO pins. Each bit represent a
|
||||
GPIO pin, where a high level sets the pin as output and a low
|
||||
level sets the pin as input/high-Z.
|
||||
"""
|
||||
if direction > self._mask:
|
||||
raise GpioException("Invalid direction mask")
|
||||
self._direction &= ~pins
|
||||
self._direction |= (pins & direction)
|
||||
self._update_direction()
|
||||
|
||||
def _configure(self, url: str, direction: int,
|
||||
frequency: Union[int, float, None] = None, **kwargs) -> int:
|
||||
raise NotImplementedError('GpioBaseController cannot be instanciated')
|
||||
|
||||
def _update_direction(self) -> None:
|
||||
raise NotImplementedError('Missing implementation')
|
||||
|
||||
|
||||
class GpioAsyncController(GpioBaseController):
|
||||
"""GPIO controller for an FTDI port, in bit-bang asynchronous mode.
|
||||
|
||||
GPIO accessible pins are limited to the 8 lower pins of each GPIO port.
|
||||
|
||||
Asynchronous bitbang output are updated on write request using the
|
||||
:py:meth:`write` method, clocked at the selected frequency.
|
||||
|
||||
Asynchronous bitbang input are sampled at the same rate, as soon as the
|
||||
controller is initialized. The GPIO input samples fill in the FTDI HW
|
||||
buffer until it is filled up, in which case sampling stops until the
|
||||
GPIO samples are read out with the :py:meth:`read` method. It may be
|
||||
therefore hard to use, except if peek mode is selected,
|
||||
see :py:meth:`read` for details.
|
||||
|
||||
Note that FTDI internal clock divider cannot generate any arbitrary
|
||||
frequency, so the closest frequency to the request one that can be
|
||||
generated is selected. The actual :py:attr:`frequency` may be tested to
|
||||
check if it matches the board requirements.
|
||||
"""
|
||||
|
||||
def read(self, readlen: int = 1, peek: Optional[bool] = None,
|
||||
noflush: bool = False) -> Union[int, bytes]:
|
||||
"""Read the GPIO input pin electrical level.
|
||||
|
||||
:param readlen: how many GPIO samples to retrieve. Each sample is
|
||||
8-bit wide.
|
||||
:param peek: whether to peek/sample the instantaneous GPIO pin
|
||||
values from port, or to use the HW FIFO. The HW FIFO is
|
||||
continously filled up with GPIO sample at the current
|
||||
frequency, until it is full - samples are no longer
|
||||
collected until the FIFO is read. This means than
|
||||
non-peek mode read "old" values, with no way to know at
|
||||
which time they have been sampled. PyFtdi ensures that
|
||||
old sampled values before the completion of a previous
|
||||
GPIO write are discarded. When peek mode is selected,
|
||||
readlen should be 1.
|
||||
:param noflush: whether to disable the RX buffer flush before
|
||||
reading out data
|
||||
:return: a 8-bit wide integer if peek mode is used, or
|
||||
a bytes buffer otherwise.
|
||||
"""
|
||||
if not self.is_connected:
|
||||
raise GpioException('Not connected')
|
||||
if peek is None and readlen == 1:
|
||||
# compatibility with legacy API
|
||||
peek = True
|
||||
if peek:
|
||||
if readlen != 1:
|
||||
raise ValueError('Invalid read length with peek mode')
|
||||
return self._ftdi.read_pins()
|
||||
# in asynchronous bitbang mode, the FTDI-to-host FIFO is filled in
|
||||
# continuously once this mode is activated. This means there is no
|
||||
# way to trigger the exact moment where the buffer is filled in, nor
|
||||
# to define the write pointer in the buffer. Reading out this buffer
|
||||
# at any time is likely to contain a mix of old and new values.
|
||||
# Anyway, flushing the FTDI-to-host buffer seems to be a proper
|
||||
# to get in sync with the buffer.
|
||||
if noflush:
|
||||
return self._ftdi.read_data(readlen)
|
||||
loop = 10000
|
||||
while loop:
|
||||
loop -= 1
|
||||
# do not attempt to do anything till the FTDI HW buffer has been
|
||||
# emptied, i.e. previous write calls have been handled.
|
||||
status = self._ftdi.poll_modem_status()
|
||||
if status & Ftdi.MODEM_TEMT:
|
||||
# TX buffer is now empty, any "write" GPIO rquest has completed
|
||||
# so start reading GPIO samples from this very moment.
|
||||
break
|
||||
else:
|
||||
# sanity check to avoid endless loop on errors
|
||||
raise FtdiError('FTDI TX buffer error')
|
||||
# now flush the FTDI-to-host buffer as it keeps being filled with data
|
||||
self._ftdi.purge_tx_buffer()
|
||||
# finally perform the actual read out
|
||||
return self._ftdi.read_data(readlen)
|
||||
|
||||
def write(self, out: Union[bytes, bytearray, int]) -> None:
|
||||
"""Set the GPIO output pin electrical level, or output a sequence of
|
||||
bytes @ constant frequency to GPIO output pins.
|
||||
|
||||
:param out: a bitfield of GPIO pins, or a sequence of them
|
||||
"""
|
||||
if not self.is_connected:
|
||||
raise GpioException('Not connected')
|
||||
if isinstance(out, (bytes, bytearray)):
|
||||
pass
|
||||
else:
|
||||
if isinstance(out, int):
|
||||
out = bytes([out])
|
||||
else:
|
||||
if not is_iterable(out):
|
||||
raise TypeError('Invalid output value')
|
||||
for val in out:
|
||||
if val > self._mask:
|
||||
raise ValueError('Invalid output value')
|
||||
out = bytes(out)
|
||||
self._ftdi.write_data(out)
|
||||
|
||||
def set_frequency(self, frequency: Union[int, float]) -> None:
|
||||
"""Set the frequency at which sequence of GPIO samples are read
|
||||
and written.
|
||||
|
||||
note: FTDI may update its clock register before it has emptied its
|
||||
internal buffer. If the current frequency is "low", some
|
||||
yet-to-output bytes may end up being clocked at the new frequency.
|
||||
|
||||
Unfortunately, it seems there is no way to wait for the internal
|
||||
buffer to be emptied out. They can be flushed (i.e. discarded), but
|
||||
not synchronized :-(
|
||||
|
||||
PyFtdi client should add "some" short delay to ensure a previous,
|
||||
long write request has been fully output @ low freq before changing
|
||||
the frequency.
|
||||
|
||||
Beware that only some exact frequencies can be generated. Contrary
|
||||
to the UART mode, an approximate frequency is always accepted for
|
||||
GPIO/bitbang mode. To get the actual frequency, and optionally abort
|
||||
if it is out-of-spec, use :py:meth:`frequency` property.
|
||||
|
||||
:param frequency: the new frequency, in GPIO samples per second
|
||||
"""
|
||||
self._frequency = float(self._ftdi.set_baudrate(int(frequency), False))
|
||||
|
||||
def _configure(self, url: str, direction: int,
|
||||
frequency: Union[int, float, None] = None, **kwargs) -> int:
|
||||
if 'initial' in kwargs:
|
||||
initial = kwargs['initial']
|
||||
del kwargs['initial']
|
||||
else:
|
||||
initial = None
|
||||
if 'debug' in kwargs:
|
||||
# debug is not implemented
|
||||
del kwargs['debug']
|
||||
baudrate = int(frequency) if frequency is not None else None
|
||||
baudrate = self._ftdi.open_bitbang_from_url(url,
|
||||
direction=direction,
|
||||
sync=False,
|
||||
baudrate=baudrate,
|
||||
**kwargs)
|
||||
self._width = 8
|
||||
self._mask = (1 << self._width) - 1
|
||||
self._direction = direction & self._mask
|
||||
if initial is not None:
|
||||
initial &= self._mask
|
||||
self.write(initial)
|
||||
return float(baudrate)
|
||||
|
||||
def _update_direction(self) -> None:
|
||||
self._ftdi.set_bitmode(self._direction, Ftdi.BitMode.BITBANG)
|
||||
|
||||
# old API names
|
||||
open_from_url = GpioBaseController.configure
|
||||
read_port = read
|
||||
write_port = write
|
||||
|
||||
|
||||
# old API compatibility
|
||||
GpioController = GpioAsyncController
|
||||
|
||||
|
||||
class GpioSyncController(GpioBaseController):
|
||||
"""GPIO controller for an FTDI port, in bit-bang synchronous mode.
|
||||
|
||||
GPIO accessible pins are limited to the 8 lower pins of each GPIO port.
|
||||
|
||||
Synchronous bitbang input and output are synchronized. Eveery time GPIO
|
||||
output is updated, the GPIO input is sampled and buffered.
|
||||
|
||||
Update and sampling are clocked at the selected frequency. The GPIO
|
||||
samples are transfer in both direction with the :py:meth:`exchange`
|
||||
method, which therefore always returns as many input samples as output
|
||||
bytes.
|
||||
|
||||
Note that FTDI internal clock divider cannot generate any arbitrary
|
||||
frequency, so the closest frequency to the request one that can be
|
||||
generated is selected. The actual :py:attr:`frequency` may be tested to
|
||||
check if it matches the board requirements.
|
||||
"""
|
||||
|
||||
def exchange(self, out: Union[bytes, bytearray]) -> bytes:
|
||||
"""Set the GPIO output pin electrical level, or output a sequence of
|
||||
bytes @ constant frequency to GPIO output pins.
|
||||
|
||||
:param out: the byte buffer to output as GPIO
|
||||
:return: a byte buffer of the same length as out buffer.
|
||||
"""
|
||||
if not self.is_connected:
|
||||
raise GpioException('Not connected')
|
||||
if isinstance(out, (bytes, bytearray)):
|
||||
pass
|
||||
else:
|
||||
if isinstance(out, int):
|
||||
out = bytes([out])
|
||||
elif not is_iterable(out):
|
||||
raise TypeError('Invalid output value')
|
||||
for val in out:
|
||||
if val > self._mask:
|
||||
raise GpioException("Invalid value")
|
||||
self._ftdi.write_data(out)
|
||||
data = self._ftdi.read_data_bytes(len(out), 4)
|
||||
return data
|
||||
|
||||
def set_frequency(self, frequency: Union[int, float]) -> None:
|
||||
"""Set the frequency at which sequence of GPIO samples are read
|
||||
and written.
|
||||
|
||||
:param frequency: the new frequency, in GPIO samples per second
|
||||
"""
|
||||
self._frequency = float(self._ftdi.set_baudrate(int(frequency), False))
|
||||
|
||||
def _configure(self, url: str, direction: int,
|
||||
frequency: Union[int, float, None] = None, **kwargs):
|
||||
if 'initial' in kwargs:
|
||||
initial = kwargs['initial']
|
||||
del kwargs['initial']
|
||||
else:
|
||||
initial = None
|
||||
if 'debug' in kwargs:
|
||||
# debug is not implemented
|
||||
del kwargs['debug']
|
||||
baudrate = int(frequency) if frequency is not None else None
|
||||
baudrate = self._ftdi.open_bitbang_from_url(url,
|
||||
direction=direction,
|
||||
sync=True,
|
||||
baudrate=baudrate,
|
||||
**kwargs)
|
||||
self._width = 8
|
||||
self._mask = (1 << self._width) - 1
|
||||
self._direction = direction & self._mask
|
||||
if initial is not None:
|
||||
initial &= self._mask
|
||||
self.exchange(initial)
|
||||
return float(baudrate)
|
||||
|
||||
def _update_direction(self) -> None:
|
||||
self._ftdi.set_bitmode(self._direction, Ftdi.BitMode.SYNCBB)
|
||||
|
||||
|
||||
class GpioMpsseController(GpioBaseController):
|
||||
"""GPIO controller for an FTDI port, in MPSSE mode.
|
||||
|
||||
All GPIO pins are reachable, but MPSSE mode is slower than other modes.
|
||||
|
||||
Beware that LSBs (b0..b7) and MSBs (b8..b15) are accessed with two
|
||||
subsequence commands, so a slight delay may occur when sampling or
|
||||
changing both groups at once. In other word, it is not possible to
|
||||
atomically read to / write from LSBs and MSBs. This might be worth
|
||||
checking the board design if atomic access to several lines is required.
|
||||
"""
|
||||
|
||||
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]]:
|
||||
"""Read the GPIO input pin electrical level.
|
||||
|
||||
:param readlen: how many GPIO samples to retrieve. Each sample if
|
||||
:py:meth:`width` bit wide.
|
||||
:param peek: whether to peak current value from port, or to use
|
||||
MPSSE stream and HW FIFO. When peek mode is selected,
|
||||
readlen should be 1. It is not available with wide
|
||||
ports if some of the MSB pins are configured as input
|
||||
:return: a :py:meth:`width` bit wide integer if direct mode is used,
|
||||
a bytes buffer if :py:meth:`width` is a byte,
|
||||
a list of integer otherwise (MPSSE mode only).
|
||||
"""
|
||||
if not self.is_connected:
|
||||
raise GpioException('Not connected')
|
||||
if peek:
|
||||
if readlen != 1:
|
||||
raise ValueError('Invalid read length with direct mode')
|
||||
if self._width > 8:
|
||||
if (0xFFFF & ~self._direction) >> 8:
|
||||
raise ValueError('Peek mode not available with selected '
|
||||
'input config')
|
||||
if peek:
|
||||
return self._ftdi.read_pins()
|
||||
return self._read_mpsse(readlen)
|
||||
|
||||
def write(self, out: Union[bytes, bytearray, Iterable[int], int]) -> None:
|
||||
"""Set the GPIO output pin electrical level, or output a sequence of
|
||||
bytes @ constant frequency to GPIO output pins.
|
||||
|
||||
:param out: a bitfield of GPIO pins, or a sequence of them
|
||||
"""
|
||||
if not self.is_connected:
|
||||
raise GpioException('Not connected')
|
||||
if isinstance(out, (bytes, bytearray)):
|
||||
pass
|
||||
else:
|
||||
if isinstance(out, int):
|
||||
out = [out]
|
||||
elif not is_iterable(out):
|
||||
raise TypeError('Invalid output value')
|
||||
for val in out:
|
||||
if val > self._mask:
|
||||
raise GpioException("Invalid value")
|
||||
self._write_mpsse(out)
|
||||
|
||||
def set_frequency(self, frequency: Union[int, float]) -> None:
|
||||
if not self.is_connected:
|
||||
raise GpioException('Not connected')
|
||||
self._frequency = self._ftdi.set_frequency(float(frequency))
|
||||
|
||||
def _update_direction(self) -> None:
|
||||
# nothing to do in MPSSE mode, as direction is updated with each
|
||||
# GPIO command
|
||||
pass
|
||||
|
||||
def _configure(self, url: str, direction: int,
|
||||
frequency: Union[int, float, None] = None, **kwargs):
|
||||
frequency = self._ftdi.open_mpsse_from_url(url,
|
||||
direction=direction,
|
||||
frequency=frequency,
|
||||
**kwargs)
|
||||
self._width = self._ftdi.port_width
|
||||
self._mask = (1 << self._width) - 1
|
||||
self._direction = direction & self._mask
|
||||
return frequency
|
||||
|
||||
def _read_mpsse(self, count: int) -> Tuple[int]:
|
||||
if self._width > 8:
|
||||
cmd = bytearray([Ftdi.GET_BITS_LOW, Ftdi.GET_BITS_HIGH] * count)
|
||||
fmt = f'<{count}H'
|
||||
else:
|
||||
cmd = bytearray([Ftdi.GET_BITS_LOW] * count)
|
||||
fmt = None
|
||||
cmd.append(Ftdi.SEND_IMMEDIATE)
|
||||
if len(cmd) > self.MPSSE_PAYLOAD_MAX_LENGTH:
|
||||
raise ValueError('Too many samples')
|
||||
self._ftdi.write_data(cmd)
|
||||
size = scalc(fmt) if fmt else count
|
||||
data = self._ftdi.read_data_bytes(size, 4)
|
||||
if 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
|
||||
|
||||
def _write_mpsse(self,
|
||||
out: Union[bytes, bytearray, Iterable[int], int]) -> None:
|
||||
cmd = []
|
||||
low_dir = self._direction & 0xFF
|
||||
if self._width > 8:
|
||||
high_dir = (self._direction >> 8) & 0xFF
|
||||
for data in out:
|
||||
low_data = data & 0xFF
|
||||
high_data = (data >> 8) & 0xFF
|
||||
cmd.extend([Ftdi.SET_BITS_LOW, low_data, low_dir,
|
||||
Ftdi.SET_BITS_HIGH, high_data, high_dir])
|
||||
else:
|
||||
for data in out:
|
||||
cmd.extend([Ftdi.SET_BITS_LOW, data, low_dir])
|
||||
self._ftdi.write_data(bytes(cmd))
|
1162
lib/python3.11/site-packages/pyftdi/i2c.py
Normal file
1162
lib/python3.11/site-packages/pyftdi/i2c.py
Normal file
File diff suppressed because it is too large
Load diff
654
lib/python3.11/site-packages/pyftdi/jtag.py
Normal file
654
lib/python3.11/site-packages/pyftdi/jtag.py
Normal file
|
@ -0,0 +1,654 @@
|
|||
# Copyright (c) 2010-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""JTAG support for PyFdti"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
||||
from time import sleep
|
||||
from typing import List, Tuple, Union
|
||||
from .ftdi import Ftdi
|
||||
from .bits import BitSequence
|
||||
|
||||
|
||||
class JtagError(Exception):
|
||||
"""Generic JTAG error."""
|
||||
|
||||
|
||||
class JtagState:
|
||||
"""Test Access Port controller state"""
|
||||
|
||||
def __init__(self, name: str, modes: Tuple[str, str]):
|
||||
self.name = name
|
||||
self.modes = modes
|
||||
self.exits = [self, self] # dummy value before initial configuration
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
def setx(self, fstate: 'JtagState', tstate: 'JtagState'):
|
||||
self.exits = [fstate, tstate]
|
||||
|
||||
def getx(self, event):
|
||||
x = int(bool(event))
|
||||
return self.exits[x]
|
||||
|
||||
def is_of(self, mode: str) -> bool:
|
||||
return mode in self.modes
|
||||
|
||||
|
||||
class JtagStateMachine:
|
||||
"""Test Access Port controller state machine."""
|
||||
|
||||
def __init__(self):
|
||||
self.states = {}
|
||||
for s, modes in [('test_logic_reset', ('reset', ' idle')),
|
||||
('run_test_idle', ('idle',)),
|
||||
('select_dr_scan', ('dr',)),
|
||||
('capture_dr', ('dr', 'shift', 'capture')),
|
||||
('shift_dr', ('dr', 'shift')),
|
||||
('exit_1_dr', ('dr', 'update', 'pause')),
|
||||
('pause_dr', ('dr', 'pause')),
|
||||
('exit_2_dr', ('dr', 'shift', 'udpate')),
|
||||
('update_dr', ('dr', 'idle')),
|
||||
('select_ir_scan', ('ir',)),
|
||||
('capture_ir', ('ir', 'shift', 'capture')),
|
||||
('shift_ir', ('ir', 'shift')),
|
||||
('exit_1_ir', ('ir', 'udpate', 'pause')),
|
||||
('pause_ir', ('ir', 'pause')),
|
||||
('exit_2_ir', ('ir', 'shift', 'update')),
|
||||
('update_ir', ('ir', 'idle'))]:
|
||||
self.states[s] = JtagState(s, modes)
|
||||
self['test_logic_reset'].setx(self['run_test_idle'],
|
||||
self['test_logic_reset'])
|
||||
self['run_test_idle'].setx(self['run_test_idle'],
|
||||
self['select_dr_scan'])
|
||||
self['select_dr_scan'].setx(self['capture_dr'],
|
||||
self['select_ir_scan'])
|
||||
self['capture_dr'].setx(self['shift_dr'], self['exit_1_dr'])
|
||||
self['shift_dr'].setx(self['shift_dr'], self['exit_1_dr'])
|
||||
self['exit_1_dr'].setx(self['pause_dr'], self['update_dr'])
|
||||
self['pause_dr'].setx(self['pause_dr'], self['exit_2_dr'])
|
||||
self['exit_2_dr'].setx(self['shift_dr'], self['update_dr'])
|
||||
self['update_dr'].setx(self['run_test_idle'],
|
||||
self['select_dr_scan'])
|
||||
self['select_ir_scan'].setx(self['capture_ir'],
|
||||
self['test_logic_reset'])
|
||||
self['capture_ir'].setx(self['shift_ir'], self['exit_1_ir'])
|
||||
self['shift_ir'].setx(self['shift_ir'], self['exit_1_ir'])
|
||||
self['exit_1_ir'].setx(self['pause_ir'], self['update_ir'])
|
||||
self['pause_ir'].setx(self['pause_ir'], self['exit_2_ir'])
|
||||
self['exit_2_ir'].setx(self['shift_ir'], self['update_ir'])
|
||||
self['update_ir'].setx(self['run_test_idle'], self['select_dr_scan'])
|
||||
self._current = self['test_logic_reset']
|
||||
|
||||
def __getitem__(self, name: str) -> JtagState:
|
||||
return self.states[name]
|
||||
|
||||
def state(self) -> JtagState:
|
||||
return self._current
|
||||
|
||||
def state_of(self, mode: str) -> bool:
|
||||
return self._current.is_of(mode)
|
||||
|
||||
def reset(self):
|
||||
self._current = self['test_logic_reset']
|
||||
|
||||
def find_path(self, target: Union[JtagState, str],
|
||||
source: Union[JtagState, str, None] = None) \
|
||||
-> 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.
|
||||
|
||||
:return: the list of states, including source and target states.
|
||||
"""
|
||||
if source is None:
|
||||
source = self.state()
|
||||
if isinstance(source, str):
|
||||
source = self[source]
|
||||
if isinstance(target, str):
|
||||
target = self[target]
|
||||
|
||||
def next_path(state, target, path):
|
||||
# this test match the target, path is valid
|
||||
if state == target:
|
||||
return path+[state]
|
||||
# candidate paths
|
||||
paths = []
|
||||
for x in state.exits:
|
||||
# next state is self (loop around), kill the path
|
||||
if x == state:
|
||||
continue
|
||||
# next state already in upstream (loop back), kill the path
|
||||
if x in path:
|
||||
continue
|
||||
# try the current path
|
||||
npath = next_path(x, target, path + [state])
|
||||
# downstream is a valid path, store it
|
||||
if npath:
|
||||
paths.append(npath)
|
||||
# keep the shortest path
|
||||
return min(((len(p), p) for p in paths), key=lambda x: x[0])[1] \
|
||||
if paths else []
|
||||
return next_path(source, target, [])
|
||||
|
||||
@classmethod
|
||||
def get_events(cls, path):
|
||||
"""Build up an event sequence from a state sequence, so that the
|
||||
resulting event sequence allows the JTAG state machine to advance
|
||||
from the first state to the last one of the input sequence"""
|
||||
events = []
|
||||
for s, d in zip(path[:-1], path[1:]):
|
||||
for e, x in enumerate(s.exits):
|
||||
if x == d:
|
||||
events.append(e)
|
||||
if len(events) != len(path) - 1:
|
||||
raise JtagError("Invalid path")
|
||||
return BitSequence(events)
|
||||
|
||||
def handle_events(self, events):
|
||||
for event in events:
|
||||
self._current = self._current.getx(event)
|
||||
|
||||
|
||||
class JtagController:
|
||||
"""JTAG master of an FTDI device"""
|
||||
|
||||
TCK_BIT = 0x01 # FTDI output
|
||||
TDI_BIT = 0x02 # FTDI output
|
||||
TDO_BIT = 0x04 # FTDI input
|
||||
TMS_BIT = 0x08 # FTDI output
|
||||
TRST_BIT = 0x10 # FTDI output, not available on 2232 JTAG debugger
|
||||
JTAG_MASK = 0x1f
|
||||
FTDI_PIPE_LEN = 512
|
||||
|
||||
# Private API
|
||||
def __init__(self, trst: bool = False, frequency: float = 3.0E6):
|
||||
"""
|
||||
trst uses the nTRST optional JTAG line to hard-reset the TAP
|
||||
controller
|
||||
"""
|
||||
self._ftdi = Ftdi()
|
||||
self._trst = trst
|
||||
self._frequency = frequency
|
||||
self.direction = (JtagController.TCK_BIT |
|
||||
JtagController.TDI_BIT |
|
||||
JtagController.TMS_BIT |
|
||||
(self._trst and JtagController.TRST_BIT or 0))
|
||||
self._last = None # Last deferred TDO bit
|
||||
self._write_buff = bytearray()
|
||||
|
||||
# Public API
|
||||
def configure(self, url: str) -> None:
|
||||
"""Configure the FTDI interface as a JTAG controller"""
|
||||
self._ftdi.open_mpsse_from_url(
|
||||
url, direction=self.direction, frequency=self._frequency)
|
||||
# FTDI requires to initialize all GPIOs before MPSSE kicks in
|
||||
cmd = bytearray((Ftdi.SET_BITS_LOW, 0x0, self.direction))
|
||||
self._ftdi.write_data(cmd)
|
||||
|
||||
def close(self, freeze: bool = False) -> None:
|
||||
"""Close the JTAG interface/port.
|
||||
|
||||
:param freeze: if set, FTDI port is not reset to its default
|
||||
state on close. This means the port is left with
|
||||
its current configuration and output signals.
|
||||
This feature should not be used except for very
|
||||
specific needs.
|
||||
"""
|
||||
if self._ftdi.is_connected:
|
||||
self._ftdi.close(freeze)
|
||||
|
||||
def purge(self) -> None:
|
||||
self._ftdi.purge_buffers()
|
||||
|
||||
def reset(self, sync: bool = False) -> None:
|
||||
"""Reset the attached TAP controller.
|
||||
sync sends the command immediately (no caching)
|
||||
"""
|
||||
# we can either send a TRST HW signal or perform 5 cycles with TMS=1
|
||||
# to move the remote TAP controller back to 'test_logic_reset' state
|
||||
# do both for now
|
||||
if not self._ftdi.is_connected:
|
||||
raise JtagError("FTDI controller terminated")
|
||||
if self._trst:
|
||||
# nTRST
|
||||
value = 0
|
||||
cmd = bytearray((Ftdi.SET_BITS_LOW, value, self.direction))
|
||||
self._ftdi.write_data(cmd)
|
||||
sleep(0.1)
|
||||
# nTRST should be left to the high state
|
||||
value = JtagController.TRST_BIT
|
||||
cmd = bytearray((Ftdi.SET_BITS_LOW, value, self.direction))
|
||||
self._ftdi.write_data(cmd)
|
||||
sleep(0.1)
|
||||
# TAP reset (even with HW reset, could be removed though)
|
||||
self.write_tms(BitSequence('11111'))
|
||||
if sync:
|
||||
self.sync()
|
||||
|
||||
def sync(self) -> None:
|
||||
if not self._ftdi.is_connected:
|
||||
raise JtagError("FTDI controller terminated")
|
||||
if self._write_buff:
|
||||
self._ftdi.write_data(self._write_buff)
|
||||
self._write_buff = bytearray()
|
||||
|
||||
def write_tms(self, tms: BitSequence,
|
||||
should_read: bool = False) -> None:
|
||||
"""Change the TAP controller state"""
|
||||
if not isinstance(tms, BitSequence):
|
||||
raise JtagError('Expect a BitSequence')
|
||||
length = len(tms)
|
||||
if not 0 < length < 8:
|
||||
raise JtagError('Invalid TMS length')
|
||||
out = BitSequence(tms, length=8)
|
||||
# apply the last TDO bit
|
||||
if self._last is not None:
|
||||
out[7] = self._last
|
||||
# print("TMS", tms, (self._last is not None) and 'w/ Last' or '')
|
||||
# reset last bit
|
||||
self._last = None
|
||||
if should_read:
|
||||
cmd = bytearray((Ftdi.RW_BITS_TMS_PVE_NVE, length-1, out.tobyte()))
|
||||
else:
|
||||
cmd = bytearray((Ftdi.WRITE_BITS_TMS_NVE, length-1, out.tobyte()))
|
||||
self._stack_cmd(cmd)
|
||||
if should_read:
|
||||
self.sync()
|
||||
|
||||
def read(self, length: int) -> BitSequence:
|
||||
"""Read out a sequence of bits from TDO."""
|
||||
byte_count = length//8
|
||||
bit_count = length-8*byte_count
|
||||
bs = BitSequence()
|
||||
if byte_count:
|
||||
bytes_ = self._read_bytes(byte_count)
|
||||
bs.append(bytes_)
|
||||
if bit_count:
|
||||
bits = self._read_bits(bit_count)
|
||||
bs.append(bits)
|
||||
return bs
|
||||
|
||||
def write(self, out: Union[BitSequence, str], use_last: bool = True):
|
||||
"""Write a sequence of bits to TDI"""
|
||||
if isinstance(out, str):
|
||||
if len(out) > 1:
|
||||
self._write_bytes_raw(out[:-1])
|
||||
out = out[-1]
|
||||
out = BitSequence(bytes_=out)
|
||||
elif not isinstance(out, BitSequence):
|
||||
out = BitSequence(out)
|
||||
if use_last:
|
||||
(out, self._last) = (out[:-1], bool(out[-1]))
|
||||
byte_count = len(out)//8
|
||||
pos = 8*byte_count
|
||||
bit_count = len(out)-pos
|
||||
if byte_count:
|
||||
self._write_bytes(out[:pos])
|
||||
if bit_count:
|
||||
self._write_bits(out[pos:])
|
||||
|
||||
def write_with_read(self, out: BitSequence,
|
||||
use_last: bool = False) -> int:
|
||||
"""Write the given BitSequence while reading the same
|
||||
number of bits into the FTDI read buffer.
|
||||
Returns the number of bits written."""
|
||||
if not isinstance(out, BitSequence):
|
||||
return JtagError('Expect a BitSequence')
|
||||
if use_last:
|
||||
(out, self._last) = (out[:-1], int(out[-1]))
|
||||
byte_count = len(out)//8
|
||||
pos = 8*byte_count
|
||||
bit_count = len(out)-pos
|
||||
if not byte_count and not bit_count:
|
||||
raise JtagError("Nothing to shift")
|
||||
if byte_count:
|
||||
blen = byte_count-1
|
||||
# print("RW OUT %s" % out[:pos])
|
||||
cmd = bytearray((Ftdi.RW_BYTES_PVE_NVE_LSB,
|
||||
blen, (blen >> 8) & 0xff))
|
||||
cmd.extend(out[:pos].tobytes(msby=True))
|
||||
self._stack_cmd(cmd)
|
||||
# print("push %d bytes" % byte_count)
|
||||
if bit_count:
|
||||
# print("RW OUT %s" % out[pos:])
|
||||
cmd = bytearray((Ftdi.RW_BITS_PVE_NVE_LSB, bit_count-1))
|
||||
cmd.append(out[pos:].tobyte())
|
||||
self._stack_cmd(cmd)
|
||||
# print("push %d bits" % bit_count)
|
||||
return len(out)
|
||||
|
||||
def read_from_buffer(self, length) -> BitSequence:
|
||||
"""Read the specified number of bits from the FTDI read buffer."""
|
||||
self.sync()
|
||||
bs = BitSequence()
|
||||
byte_count = length//8
|
||||
pos = 8*byte_count
|
||||
bit_count = length-pos
|
||||
if byte_count:
|
||||
data = self._ftdi.read_data_bytes(byte_count, 4)
|
||||
if not data:
|
||||
raise JtagError('Unable to read data from FTDI')
|
||||
byteseq = BitSequence(bytes_=data, length=8*byte_count)
|
||||
# print("RW IN %s" % byteseq)
|
||||
bs.append(byteseq)
|
||||
# print("pop %d bytes" % byte_count)
|
||||
if bit_count:
|
||||
data = self._ftdi.read_data_bytes(1, 4)
|
||||
if not data:
|
||||
raise JtagError('Unable to read data from FTDI')
|
||||
byte = data[0]
|
||||
# need to shift bits as they are shifted in from the MSB in FTDI
|
||||
byte >>= 8-bit_count
|
||||
bitseq = BitSequence(byte, length=bit_count)
|
||||
bs.append(bitseq)
|
||||
# print("pop %d bits" % bit_count)
|
||||
if len(bs) != length:
|
||||
raise ValueError("Internal error")
|
||||
return bs
|
||||
|
||||
@property
|
||||
def ftdi(self) -> Ftdi:
|
||||
"""Return the Ftdi instance.
|
||||
|
||||
:return: the Ftdi instance
|
||||
"""
|
||||
return self._ftdi
|
||||
|
||||
def _stack_cmd(self, cmd: Union[bytes, bytearray]):
|
||||
if not isinstance(cmd, (bytes, bytearray)):
|
||||
raise TypeError('Expect bytes or bytearray')
|
||||
if not self._ftdi:
|
||||
raise JtagError("FTDI controller terminated")
|
||||
# Currrent buffer + new command + send_immediate
|
||||
if (len(self._write_buff)+len(cmd)+1) >= JtagController.FTDI_PIPE_LEN:
|
||||
self.sync()
|
||||
self._write_buff.extend(cmd)
|
||||
|
||||
def _read_bits(self, length: int):
|
||||
"""Read out bits from TDO"""
|
||||
if length > 8:
|
||||
raise JtagError("Cannot fit into FTDI fifo")
|
||||
cmd = bytearray((Ftdi.READ_BITS_NVE_LSB, length-1))
|
||||
self._stack_cmd(cmd)
|
||||
self.sync()
|
||||
data = self._ftdi.read_data_bytes(1, 4)
|
||||
# need to shift bits as they are shifted in from the MSB in FTDI
|
||||
byte = data[0] >> 8-length
|
||||
bs = BitSequence(byte, length=length)
|
||||
# print("READ BITS %s" % bs)
|
||||
return bs
|
||||
|
||||
def _write_bits(self, out: BitSequence) -> None:
|
||||
"""Output bits on TDI"""
|
||||
length = len(out)
|
||||
byte = out.tobyte()
|
||||
# print("WRITE BITS %s" % out)
|
||||
cmd = bytearray((Ftdi.WRITE_BITS_NVE_LSB, length-1, byte))
|
||||
self._stack_cmd(cmd)
|
||||
|
||||
def _read_bytes(self, length: int) -> BitSequence:
|
||||
"""Read out bytes from TDO"""
|
||||
if length > JtagController.FTDI_PIPE_LEN:
|
||||
raise JtagError("Cannot fit into FTDI fifo")
|
||||
alen = length-1
|
||||
cmd = bytearray((Ftdi.READ_BYTES_NVE_LSB, alen & 0xff,
|
||||
(alen >> 8) & 0xff))
|
||||
self._stack_cmd(cmd)
|
||||
self.sync()
|
||||
data = self._ftdi.read_data_bytes(length, 4)
|
||||
bs = BitSequence(bytes_=data, length=8*length)
|
||||
# print("READ BYTES %s" % bs)
|
||||
return bs
|
||||
|
||||
def _write_bytes(self, out: BitSequence):
|
||||
"""Output bytes on TDI"""
|
||||
bytes_ = out.tobytes(msby=True) # don't ask...
|
||||
olen = len(bytes_)-1
|
||||
# print("WRITE BYTES %s" % out)
|
||||
cmd = bytearray((Ftdi.WRITE_BYTES_NVE_LSB, olen & 0xff,
|
||||
(olen >> 8) & 0xff))
|
||||
cmd.extend(bytes_)
|
||||
self._stack_cmd(cmd)
|
||||
|
||||
def _write_bytes_raw(self, out: BitSequence):
|
||||
"""Output bytes on TDI"""
|
||||
olen = len(out)-1
|
||||
cmd = bytearray((Ftdi.WRITE_BYTES_NVE_LSB, olen & 0xff,
|
||||
(olen >> 8) & 0xff))
|
||||
cmd.extend(out)
|
||||
self._stack_cmd(cmd)
|
||||
|
||||
|
||||
class JtagEngine:
|
||||
"""High-level JTAG engine controller"""
|
||||
|
||||
def __init__(self, trst: bool = False, frequency: float = 3E06):
|
||||
self._ctrl = JtagController(trst, frequency)
|
||||
self._sm = JtagStateMachine()
|
||||
self._seq = bytearray()
|
||||
|
||||
@property
|
||||
def state_machine(self):
|
||||
return self._sm
|
||||
|
||||
@property
|
||||
def controller(self):
|
||||
return self._ctrl
|
||||
|
||||
def configure(self, url: str) -> None:
|
||||
"""Configure the FTDI interface as a JTAG controller"""
|
||||
self._ctrl.configure(url)
|
||||
|
||||
def close(self, freeze: bool = False) -> None:
|
||||
"""Terminate a JTAG session/connection.
|
||||
|
||||
:param freeze: if set, FTDI port is not reset to its default
|
||||
state on close.
|
||||
"""
|
||||
self._ctrl.close(freeze)
|
||||
|
||||
def purge(self) -> None:
|
||||
"""Purge low level HW buffers"""
|
||||
self._ctrl.purge()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the attached TAP controller"""
|
||||
self._ctrl.reset()
|
||||
self._sm.reset()
|
||||
|
||||
def write_tms(self, out, should_read=False) -> None:
|
||||
"""Change the TAP controller state"""
|
||||
self._ctrl.write_tms(out, should_read=should_read)
|
||||
|
||||
def read(self, length):
|
||||
"""Read out a sequence of bits from TDO"""
|
||||
return self._ctrl.read(length)
|
||||
|
||||
def write(self, out, use_last=False) -> None:
|
||||
"""Write a sequence of bits to TDI"""
|
||||
self._ctrl.write(out, use_last)
|
||||
|
||||
def get_available_statenames(self):
|
||||
"""Return a list of supported state name"""
|
||||
return [str(s) for s in self._sm.states]
|
||||
|
||||
def change_state(self, statename) -> None:
|
||||
"""Advance the TAP controller to the defined state"""
|
||||
# find the state machine path to move to the new instruction
|
||||
path = self._sm.find_path(statename)
|
||||
# convert the path into an event sequence
|
||||
events = self._sm.get_events(path)
|
||||
# update the remote device tap controller
|
||||
self._ctrl.write_tms(events)
|
||||
# update the current state machine's state
|
||||
self._sm.handle_events(events)
|
||||
|
||||
def go_idle(self) -> None:
|
||||
"""Change the current TAP controller to the IDLE state"""
|
||||
self.change_state('run_test_idle')
|
||||
|
||||
def write_ir(self, instruction) -> None:
|
||||
"""Change the current instruction of the TAP controller"""
|
||||
self.change_state('shift_ir')
|
||||
self._ctrl.write(instruction)
|
||||
self.change_state('update_ir')
|
||||
|
||||
def capture_ir(self) -> None:
|
||||
"""Capture the current instruction from the TAP controller"""
|
||||
self.change_state('capture_ir')
|
||||
|
||||
def write_dr(self, data) -> None:
|
||||
"""Change the data register of the TAP controller"""
|
||||
self.change_state('shift_dr')
|
||||
self._ctrl.write(data)
|
||||
self.change_state('update_dr')
|
||||
|
||||
def read_dr(self, length: int) -> BitSequence:
|
||||
"""Read the data register from the TAP controller"""
|
||||
self.change_state('shift_dr')
|
||||
data = self._ctrl.read(length)
|
||||
self.change_state('update_dr')
|
||||
return data
|
||||
|
||||
def capture_dr(self) -> None:
|
||||
"""Capture the current data register from the TAP controller"""
|
||||
self.change_state('capture_dr')
|
||||
|
||||
def sync(self) -> None:
|
||||
self._ctrl.sync()
|
||||
|
||||
def shift_register(self, out: BitSequence) -> BitSequence:
|
||||
if not self._sm.state_of('shift'):
|
||||
raise JtagError(f'Invalid state: {self._sm.state()}')
|
||||
if self._sm.state_of('capture'):
|
||||
bs = BitSequence(False)
|
||||
self._ctrl.write_tms(bs)
|
||||
self._sm.handle_events(bs)
|
||||
|
||||
bits_out = self._ctrl.write_with_read(out)
|
||||
bs = self._ctrl.read_from_buffer(bits_out)
|
||||
if len(bs) != len(out):
|
||||
raise ValueError("Internal error")
|
||||
|
||||
return bs
|
||||
|
||||
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(f'Invalid state: {self._sm.state()}')
|
||||
if self._sm.state_of('capture'):
|
||||
bs = BitSequence(False)
|
||||
self._ctrl.write_tms(bs)
|
||||
self._sm.handle_events(bs)
|
||||
|
||||
# Write with read using the last bit for next TMS transition
|
||||
bits_out = self._ctrl.write_with_read(out, use_last=True)
|
||||
|
||||
# Advance the state from shift to update
|
||||
events = BitSequence('11')
|
||||
self.write_tms(events, should_read=True)
|
||||
# (write_tms calls sync())
|
||||
# update the current state machine's state
|
||||
self._sm.handle_events(events)
|
||||
|
||||
# Read the bits clocked out as part of the initial write
|
||||
bs = self._ctrl.read_from_buffer(bits_out)
|
||||
|
||||
# Read the last two bits clocked out with TMS above
|
||||
# (but only use the lowest bit in the return data)
|
||||
last_bits = self._ctrl.read_from_buffer(2)
|
||||
bs.append(BitSequence((last_bits.tobyte() & 0x1), length=1))
|
||||
if len(bs) != len(out):
|
||||
raise ValueError("Internal error")
|
||||
|
||||
return bs
|
||||
|
||||
|
||||
class JtagTool:
|
||||
"""A helper class with facility functions"""
|
||||
|
||||
def __init__(self, engine):
|
||||
self._engine = engine
|
||||
|
||||
def idcode(self) -> None:
|
||||
idcode = self._engine.read_dr(32)
|
||||
self._engine.go_idle()
|
||||
return int(idcode)
|
||||
|
||||
def preload(self, bsdl, data) -> None:
|
||||
instruction = bsdl.get_jtag_ir('preload')
|
||||
self._engine.write_ir(instruction)
|
||||
self._engine.write_dr(data)
|
||||
self._engine.go_idle()
|
||||
|
||||
def sample(self, bsdl):
|
||||
instruction = bsdl.get_jtag_ir('sample')
|
||||
self._engine.write_ir(instruction)
|
||||
data = self._engine.read_dr(bsdl.get_boundary_length())
|
||||
self._engine.go_idle()
|
||||
return data
|
||||
|
||||
def extest(self, bsdl) -> None:
|
||||
instruction = bsdl.get_jtag_ir('extest')
|
||||
self._engine.write_ir(instruction)
|
||||
|
||||
def readback(self, bsdl):
|
||||
data = self._engine.read_dr(bsdl.get_boundary_length())
|
||||
self._engine.go_idle()
|
||||
return data
|
||||
|
||||
def detect_register_size(self) -> int:
|
||||
# Freely inpired from UrJTAG
|
||||
stm = self._engine.state_machine
|
||||
if not stm.state_of('shift'):
|
||||
raise JtagError(f'Invalid state: {stm.state()}')
|
||||
if stm.state_of('capture'):
|
||||
bs = BitSequence(False)
|
||||
self._engine.controller.write_tms(bs)
|
||||
stm.handle_events(bs)
|
||||
MAX_REG_LEN = 1024
|
||||
PATTERN_LEN = 8
|
||||
stuck = None
|
||||
for length in range(1, MAX_REG_LEN):
|
||||
print(f'Testing for length {length}')
|
||||
if length > 5:
|
||||
raise ValueError(f'Abort detection over reg length {length}')
|
||||
zero = BitSequence(length=length)
|
||||
inj = BitSequence(length=length+PATTERN_LEN)
|
||||
inj.inc()
|
||||
ok = False
|
||||
for _ in range(1, 1 << PATTERN_LEN):
|
||||
ok = False
|
||||
self._engine.write(zero, False)
|
||||
rcv = self._engine.shift_register(inj)
|
||||
try:
|
||||
tdo = rcv.invariant()
|
||||
except ValueError:
|
||||
tdo = None
|
||||
if stuck is None:
|
||||
stuck = tdo
|
||||
if stuck != tdo:
|
||||
stuck = None
|
||||
rcv >>= length
|
||||
if rcv == inj:
|
||||
ok = True
|
||||
else:
|
||||
break
|
||||
inj.inc()
|
||||
if ok:
|
||||
print(f'Register detected length: {length}')
|
||||
return length
|
||||
if stuck is not None:
|
||||
raise JtagError('TDO seems to be stuck')
|
||||
raise JtagError('Unable to detect register length')
|
349
lib/python3.11/site-packages/pyftdi/misc.py
Normal file
349
lib/python3.11/site-packages/pyftdi/misc.py
Normal file
|
@ -0,0 +1,349 @@
|
|||
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2008-2016, Neotion
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Miscellaneous helpers"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
# pylint: disable=import-outside-toplevel
|
||||
|
||||
from array import array
|
||||
from copy import deepcopy
|
||||
from re import match
|
||||
from typing import Any, Iterable, Optional, Sequence, Union
|
||||
|
||||
|
||||
# String values evaluated as true boolean values
|
||||
TRUE_BOOLEANS = ['on', 'true', 'enable', 'enabled', 'yes', 'high', '1']
|
||||
# String values evaluated as false boolean values
|
||||
FALSE_BOOLEANS = ['off', 'false', 'disable', 'disabled', 'no', 'low', '0']
|
||||
# ASCII or '.' filter
|
||||
ASCIIFILTER = ''.join([((len(repr(chr(_x))) == 3) or (_x == 0x5c)) and chr(_x)
|
||||
or '.' for _x in range(128)]) + '.' * 128
|
||||
ASCIIFILTER = bytearray(ASCIIFILTER.encode('ascii'))
|
||||
|
||||
|
||||
def hexdump(data: Union[bytes, bytearray, Iterable[int]],
|
||||
full: bool = False, abbreviate: bool = False) -> str:
|
||||
"""Convert a binary buffer into a hexadecimal representation.
|
||||
|
||||
Return a multi-line strings with hexadecimal values and ASCII
|
||||
representation of the buffer data.
|
||||
|
||||
:param data: binary buffer to dump
|
||||
:param full: use `hexdump -Cv` format
|
||||
:param abbreviate: replace identical lines with '*'
|
||||
:return: the generated string
|
||||
"""
|
||||
try:
|
||||
if isinstance(data, (bytes, array)):
|
||||
src = bytearray(data)
|
||||
elif not isinstance(data, bytearray):
|
||||
# data may be a list/tuple
|
||||
src = bytearray(b''.join(data))
|
||||
else:
|
||||
src = data
|
||||
except Exception as exc:
|
||||
raise TypeError(f"Unsupported data type '{type(data)}'") from exc
|
||||
|
||||
length = 16
|
||||
result = []
|
||||
last = b''
|
||||
abv = False
|
||||
for i in range(0, len(src), length):
|
||||
s = src[i:i+length]
|
||||
if abbreviate:
|
||||
if s == last:
|
||||
if not abv:
|
||||
result.append('*\n')
|
||||
abv = True
|
||||
continue
|
||||
abv = False
|
||||
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(f'{i:08x} {hx1:<{hl*3}} {hx2:<{hl*3}} '
|
||||
f'|{printable}|\n')
|
||||
else:
|
||||
result.append(f'{i:06x} {hexa:<{length*3}} {printable}\n')
|
||||
last = s
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
def hexline(data: Union[bytes, bytearray, Iterable[int]],
|
||||
sep: str = ' ') -> str:
|
||||
"""Convert a binary buffer into a hexadecimal representation.
|
||||
|
||||
Return a string with hexadecimal values and ASCII representation
|
||||
of the buffer data.
|
||||
|
||||
:param data: binary buffer to dump
|
||||
:param sep: the separator string/char
|
||||
:return: the formatted string
|
||||
"""
|
||||
try:
|
||||
if isinstance(data, (bytes, array)):
|
||||
src = bytearray(data)
|
||||
elif not isinstance(data, bytearray):
|
||||
# data may be a list/tuple
|
||||
src = bytearray(b''.join(data))
|
||||
else:
|
||||
src = data
|
||||
except Exception as exc:
|
||||
raise TypeError(f"Unsupported data type '{type(data)}'") from exc
|
||||
|
||||
hexa = sep.join((f'{x:02x}' for x in src))
|
||||
printable = src.translate(ASCIIFILTER).decode('ascii')
|
||||
return f'({len(data)}) {hexa} : {printable}'
|
||||
|
||||
|
||||
def to_int(value: Union[int, str]) -> int:
|
||||
"""Parse a value and convert it into an integer value if possible.
|
||||
|
||||
Input value may be:
|
||||
- a string with an integer coded as a decimal value
|
||||
- a string with an integer coded as a hexadecimal value
|
||||
- a integral value
|
||||
- a integral value with a unit specifier (kilo or mega)
|
||||
|
||||
:param value: input value to convert to an integer
|
||||
:return: the value as an integer
|
||||
:rtype: int
|
||||
:raise ValueError: if the input value cannot be converted into an int
|
||||
"""
|
||||
if not value:
|
||||
return 0
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
mo = match(r'^\s*(\d+)\s*(?:([KMkm]i?)?B?)?\s*$', value)
|
||||
if mo:
|
||||
mult = {'K': (1000),
|
||||
'KI': (1 << 10),
|
||||
'M': (1000 * 1000),
|
||||
'MI': (1 << 20)}
|
||||
value = int(mo.group(1))
|
||||
if mo.group(2):
|
||||
value *= mult[mo.group(2).upper()]
|
||||
return value
|
||||
return int(value.strip(), value.startswith('0x') and 16 or 10)
|
||||
|
||||
|
||||
def to_bool(value: Union[int, bool, str], permissive: bool = True,
|
||||
allow_int: bool = False) -> bool:
|
||||
"""Parse a string and convert it into a boolean value if possible.
|
||||
|
||||
Input value may be:
|
||||
- a string with an integer value, if `allow_int` is enabled
|
||||
- a boolean value
|
||||
- a string with a common boolean definition
|
||||
|
||||
:param value: the value to parse and convert
|
||||
:param permissive: default to the False value if parsing fails
|
||||
:param allow_int: allow an integral type as the input value
|
||||
:raise ValueError: if the input value cannot be converted into an bool
|
||||
"""
|
||||
if value is None:
|
||||
return False
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, int):
|
||||
if allow_int:
|
||||
return bool(value)
|
||||
if permissive:
|
||||
return False
|
||||
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(f"Invalid boolean value: '{value}'")
|
||||
|
||||
|
||||
def to_bps(value: str) -> int:
|
||||
"""Parse a string and convert it into a baudrate value.
|
||||
|
||||
The function accepts common multipliers as K, M and G
|
||||
|
||||
:param value: the value to parse and convert
|
||||
:type value: str or int or float
|
||||
:rtype: float
|
||||
:raise ValueError: if the input value cannot be converted into a float
|
||||
"""
|
||||
if isinstance(value, float):
|
||||
return int(value)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
mo = match(r'^(?P<value>[-+]?[0-9]*\.?[0-9]+(?:[Ee][-+]?[0-9]+)?)'
|
||||
r'(?P<unit>[KkMmGg])?$', value)
|
||||
if not mo:
|
||||
raise ValueError('Invalid frequency')
|
||||
frequency = float(mo.group(1))
|
||||
if mo.group(2):
|
||||
mult = {'K': 1E3, 'M': 1E6, 'G': 1E9}
|
||||
frequency *= mult[mo.group(2).upper()]
|
||||
return int(frequency)
|
||||
|
||||
|
||||
def xor(_a_: bool, _b_: bool) -> bool:
|
||||
"""XOR logical operation.
|
||||
|
||||
:param _a_: first argument
|
||||
:param _b_: second argument
|
||||
:return: xor-ed value
|
||||
"""
|
||||
# pylint: disable=superfluous-parens
|
||||
return bool((not (_a_) and _b_) or (_a_ and not (_b_)))
|
||||
|
||||
|
||||
def is_iterable(obj: Any) -> bool:
|
||||
"""Tells whether an instance is iterable or not.
|
||||
|
||||
:param obj: the instance to test
|
||||
:type obj: object
|
||||
:return: True if the object is iterable
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
iter(obj)
|
||||
return True
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
|
||||
def pretty_size(size, sep: str = ' ',
|
||||
lim_k: int = 1 << 10, lim_m: int = 10 << 20,
|
||||
plural: bool = True, floor: bool = True) -> str:
|
||||
"""Convert a size into a more readable unit-indexed size (KiB, MiB)
|
||||
|
||||
:param size: integral value to convert
|
||||
:param sep: the separator character between the integral value and
|
||||
the unit specifier
|
||||
:param lim_k: any value above this limit is a candidate for KiB
|
||||
conversion.
|
||||
:param lim_m: any value above this limit is a candidate for MiB
|
||||
conversion.
|
||||
:param plural: whether to append a final 's' to byte(s)
|
||||
:param floor: how to behave when exact conversion cannot be
|
||||
achieved: take the closest, smaller value or fallback to the next
|
||||
unit that allows the exact representation of the input value
|
||||
:return: the prettyfied size
|
||||
"""
|
||||
size = int(size)
|
||||
if size > lim_m:
|
||||
ssize = size >> 20
|
||||
if floor or (ssize << 20) == size:
|
||||
return f'{ssize}{sep}MiB'
|
||||
if size > lim_k:
|
||||
ssize = size >> 10
|
||||
if floor or (ssize << 10) == size:
|
||||
return f'{ssize}{sep}KiB'
|
||||
return f'{size}{sep}byte{plural and "s" or ""}'
|
||||
|
||||
|
||||
def add_custom_devices(ftdicls=None,
|
||||
vpstr: Optional[Sequence[str]] = None,
|
||||
force_hex: bool = False) -> None:
|
||||
"""Helper function to add custom VID/PID to FTDI device identifer map.
|
||||
|
||||
The string to parse should match the following format:
|
||||
|
||||
[vendor_name=]<vendor_id>:[product_name=]<product_id>
|
||||
|
||||
* vendor_name and product_name are optional strings, they may be omitted
|
||||
as they only serve as human-readable aliases for vendor and product
|
||||
names.
|
||||
* vendor_id and product_id are mandatory strings that should resolve
|
||||
as 16-bit integer (USB VID and PID values). They may be expressed as
|
||||
decimal or hexadecimal syntax.
|
||||
|
||||
ex:
|
||||
* ``0x403:0x9999``, vid:pid short syntax, with no alias names
|
||||
* ``mycompany=0x666:myproduct=0xcafe``, vid:pid complete syntax with
|
||||
aliases
|
||||
|
||||
:param vpstr: typically, a option switch string describing the device
|
||||
to add
|
||||
:param ftdicls: the Ftdi class that should support the new device.
|
||||
:param force_hex: if set, consider that the pid/vid string are
|
||||
hexadecimal encoded values.
|
||||
"""
|
||||
from inspect import isclass
|
||||
if not isclass(ftdicls):
|
||||
raise ValueError('Expect Ftdi class, not instance')
|
||||
for vidpid in vpstr or []:
|
||||
vidpids = {vid: set() for vid in ftdicls.PRODUCT_IDS}
|
||||
vname = ''
|
||||
pname = ''
|
||||
try:
|
||||
vid, pid = vidpid.split(':')
|
||||
if '=' in vid:
|
||||
vname, vid = vid.split('=', 1)
|
||||
if '=' in pid:
|
||||
pname, pid = pid.split('=', 1)
|
||||
if force_hex:
|
||||
vid, pid = [int(v, 16) for v in (vid, pid)]
|
||||
else:
|
||||
vid, pid = [to_int(v) for v in (vid, pid)]
|
||||
except ValueError as exc:
|
||||
raise ValueError('Invalid VID:PID value') from exc
|
||||
if vid not in vidpids:
|
||||
ftdicls.add_custom_vendor(vid, vname)
|
||||
vidpids[vid] = set()
|
||||
if pid not in vidpids[vid]:
|
||||
ftdicls.add_custom_product(vid, pid, pname)
|
||||
vidpids[vid].add(pid)
|
||||
|
||||
|
||||
def show_call_stack():
|
||||
"""Print the current call stack, only useful for debugging purpose."""
|
||||
from sys import _current_frames
|
||||
from threading import current_thread
|
||||
from traceback import print_stack
|
||||
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
|
||||
"""
|
||||
|
||||
def __init__(self, dictionary=None, **kwargs):
|
||||
super().__init__(self)
|
||||
if dictionary is not None:
|
||||
self.update(dictionary)
|
||||
self.update(kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.__getitem__(name)
|
||||
except KeyError as exc:
|
||||
raise AttributeError(f"'{self.__class__.__name__}' object has no "
|
||||
f"attribute '{name}'") from exc
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
self.__setitem__(name, value)
|
||||
|
||||
@classmethod
|
||||
def copy(cls, dictionary):
|
||||
|
||||
def _deep_copy(obj):
|
||||
if isinstance(obj, list):
|
||||
return [_deep_copy(v) for v in obj]
|
||||
if isinstance(obj, dict):
|
||||
return EasyDict({k: _deep_copy(obj[k]) for k in obj})
|
||||
return deepcopy(obj)
|
||||
return cls(_deep_copy(dictionary))
|
||||
|
||||
def mirror(self) -> 'EasyDict':
|
||||
"""Instanciate a mirror EasyDict."""
|
||||
return EasyDict({v: k for k, v in self.items()})
|
33
lib/python3.11/site-packages/pyftdi/serialext/__init__.py
Normal file
33
lib/python3.11/site-packages/pyftdi/serialext/__init__.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2008-2015, Neotion
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""Serial modules compliant with pyserial APIs
|
||||
"""
|
||||
|
||||
try:
|
||||
from serial.serialutil import SerialException
|
||||
except ImportError as exc:
|
||||
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('.'))
|
||||
if version < (3, 0):
|
||||
raise ValueError
|
||||
except (ValueError, IndexError, ImportError) as exc:
|
||||
raise ImportError("pyserial 3.0+ is required") from exc
|
||||
try:
|
||||
from serial import protocol_handler_packages
|
||||
protocol_handler_packages.append('pyftdi.serialext')
|
||||
except ImportError as exc:
|
||||
raise SerialException('Cannot register pyftdi extensions') from exc
|
||||
|
||||
serial_for_url = serial4url
|
||||
|
||||
|
||||
def touch():
|
||||
"""Do nothing, only for static checkers than do not like module import
|
||||
with no module references
|
||||
"""
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
178
lib/python3.11/site-packages/pyftdi/serialext/logger.py
Normal file
178
lib/python3.11/site-packages/pyftdi/serialext/logger.py
Normal file
|
@ -0,0 +1,178 @@
|
|||
# Copyright (c) 2010-2024 Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2008-2016, Neotion
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# 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
|
||||
from ..misc import hexdump
|
||||
|
||||
|
||||
__all__ = ['SerialLogger']
|
||||
|
||||
|
||||
class SerialLogger:
|
||||
"""Serial port wrapper to log input/output data to a log file.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
logpath = kwargs.pop('logfile', None)
|
||||
if not logpath:
|
||||
raise ValueError('Missing logfile')
|
||||
try:
|
||||
# pylint: disable=consider-using-with
|
||||
self._logger = open(logpath, "wt")
|
||||
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)
|
||||
|
||||
def open(self,):
|
||||
self._log_open()
|
||||
super(SerialLogger, self).open()
|
||||
|
||||
def close(self):
|
||||
self._log_close()
|
||||
self._logger.close()
|
||||
super(SerialLogger, self).close()
|
||||
|
||||
def read(self, size=1):
|
||||
data = super(SerialLogger, self).read(size)
|
||||
self._log_read(data)
|
||||
return data
|
||||
|
||||
def write(self, data):
|
||||
if data:
|
||||
self._log_write(data)
|
||||
super(SerialLogger, self).write(data)
|
||||
|
||||
def flush(self):
|
||||
self._log_flush()
|
||||
super(SerialLogger, self).flush()
|
||||
|
||||
def reset_input_buffer(self):
|
||||
self._log_reset('I')
|
||||
super(SerialLogger, self).reset_input_buffer()
|
||||
|
||||
def reset_output_buffer(self):
|
||||
self._log_reset('O')
|
||||
super(SerialLogger, self).reset_output_buffer()
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
self._log_signal('BREAK', f'for {duration:.3f}')
|
||||
super(SerialLogger, self).send_break()
|
||||
|
||||
def _update_break_state(self):
|
||||
self._log_signal('BREAK', self._break_state)
|
||||
super(SerialLogger, self)._update_break_state()
|
||||
|
||||
def _update_rts_state(self):
|
||||
self._log_signal('RTS', self._rts_state)
|
||||
super(SerialLogger, self)._update_rts_state()
|
||||
|
||||
def _update_dtr_state(self):
|
||||
self._log_signal('DTR', self._dtr_state)
|
||||
super(SerialLogger, self)._update_dtr_state()
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
level = super(SerialLogger, self).cts
|
||||
self._log_signal('CTS', level)
|
||||
return level
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
level = super(SerialLogger, self).dsr
|
||||
self._log_signal('DSR', level)
|
||||
return level
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
level = super(SerialLogger, self).ri
|
||||
self._log_signal('RI', level)
|
||||
return level
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
level = super(SerialLogger, self).cd
|
||||
self._log_signal('CD', level)
|
||||
return level
|
||||
|
||||
def in_waiting(self):
|
||||
count = super(SerialLogger, self).in_waiting()
|
||||
self._log_waiting(count)
|
||||
return count
|
||||
|
||||
def _print(self, header, string=None):
|
||||
if self._logger:
|
||||
now = time()
|
||||
delta = (now-self._last)*1000
|
||||
self._last = now
|
||||
print(f'{header} ({delta:3.3f} ms):\n{string or ""}',
|
||||
file=self._logger)
|
||||
self._logger.flush()
|
||||
|
||||
def _log_init(self, *args, **kwargs):
|
||||
try:
|
||||
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 exc:
|
||||
print(f'Cannot log open ({exc})', file=stderr)
|
||||
|
||||
def _log_close(self):
|
||||
try:
|
||||
self._print('CLOSE')
|
||||
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 exc:
|
||||
print(f'Cannot log input data ({exc})', file=stderr)
|
||||
|
||||
def _log_write(self, data):
|
||||
try:
|
||||
self._print('WRITE', hexdump(data))
|
||||
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 exc:
|
||||
print(f'Cannot log flush action ({exc})', file=stderr)
|
||||
|
||||
def _log_reset(self, type_):
|
||||
try:
|
||||
self._print('RESET BUFFER', type_)
|
||||
except Exception as exc:
|
||||
print(f'Cannot log reset buffer ({exc})', file=stderr)
|
||||
|
||||
def _log_waiting(self, count):
|
||||
try:
|
||||
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(), str(value))
|
||||
except Exception as exc:
|
||||
print(f'Cannot log {name} ({exc})', file=stderr)
|
196
lib/python3.11/site-packages/pyftdi/serialext/protocol_ftdi.py
Normal file
196
lib/python3.11/site-packages/pyftdi/serialext/protocol_ftdi.py
Normal file
|
@ -0,0 +1,196 @@
|
|||
# Copyright (c) 2008-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# 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=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
|
||||
from serial import SerialBase, SerialException, VERSION as pyserialver
|
||||
from pyftdi.ftdi import Ftdi
|
||||
from pyftdi.usbtools import UsbToolsError
|
||||
|
||||
|
||||
class FtdiSerial(SerialBase):
|
||||
"""Base class for Serial port implementation compatible with pyserial API
|
||||
using a USB device.
|
||||
"""
|
||||
|
||||
BAUDRATES = sorted([9600 * (x+1) for x in range(6)] +
|
||||
list(range(115200, 1000000, 115200)) +
|
||||
list(range(1000000, 13000000, 100000)))
|
||||
|
||||
PYSERIAL_VERSION = tuple(int(x) for x in pyserialver.split('.'))
|
||||
|
||||
def open(self):
|
||||
"""Open the initialized serial port"""
|
||||
if self.port is None:
|
||||
raise SerialException("Port must be configured before use.")
|
||||
try:
|
||||
device = Ftdi.create_from_url(self.port)
|
||||
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()
|
||||
|
||||
def close(self):
|
||||
"""Close the open port"""
|
||||
self._set_open_state(False)
|
||||
if self.udev:
|
||||
self.udev.close()
|
||||
self.udev = None
|
||||
|
||||
def read(self, size=1):
|
||||
"""Read size bytes from the serial port. If a timeout is set it may
|
||||
return less characters as requested. With no timeout it will block
|
||||
until the requested number of bytes is read."""
|
||||
data = bytearray()
|
||||
start = now()
|
||||
while True:
|
||||
buf = self.udev.read_data(size)
|
||||
data.extend(buf)
|
||||
size -= len(buf)
|
||||
if size <= 0:
|
||||
break
|
||||
if self._timeout is not None:
|
||||
if buf:
|
||||
break
|
||||
ms = now()-start
|
||||
if ms > self._timeout:
|
||||
break
|
||||
sleep(0.01)
|
||||
return bytes(data)
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given string over the serial port."""
|
||||
return self.udev.write_data(data)
|
||||
|
||||
def flush(self):
|
||||
"""Flush of file like objects. In this case, wait until all data
|
||||
is written."""
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
self.udev.purge_rx_buffer()
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer."""
|
||||
self.udev.purge_tx_buffer()
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""Send break condition."""
|
||||
self.udev.set_break(True)
|
||||
sleep(duration)
|
||||
self.udev.set_break(False)
|
||||
|
||||
def _update_break_state(self):
|
||||
"""Send break condition. Not supported"""
|
||||
self.udev.set_break(self._break_state)
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
self.udev.set_rts(self._rts_state)
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
self.udev.set_dtr(self._dtr_state)
|
||||
|
||||
@property
|
||||
def ftdi(self) -> Ftdi:
|
||||
"""Return the Ftdi instance.
|
||||
|
||||
:return: the Ftdi instance
|
||||
"""
|
||||
return self.udev
|
||||
|
||||
@property
|
||||
def usb_path(self):
|
||||
"""Return the physical location as a triplet.
|
||||
* bus is the USB bus
|
||||
* address is the address on the USB bus
|
||||
* interface is the interface number on the FTDI debice
|
||||
|
||||
:return: (bus, address, interface)
|
||||
:rtype: tuple(int)
|
||||
"""
|
||||
return self.udev.usb_path
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
return self.udev.get_cts()
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
return self.udev.get_dsr()
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
return self.udev.get_ri()
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
return self.udev.get_cd()
|
||||
|
||||
@property
|
||||
def in_waiting(self):
|
||||
"""Return the number of characters currently in the input buffer."""
|
||||
# not implemented
|
||||
return 0
|
||||
|
||||
@property
|
||||
def out_waiting(self):
|
||||
"""Return the number of bytes currently in the output buffer."""
|
||||
return 0
|
||||
|
||||
@property
|
||||
def fifoSizes(self):
|
||||
"""Return the (TX, RX) tupple of hardware FIFO sizes"""
|
||||
return self.udev.fifo_sizes
|
||||
|
||||
def _reconfigure_port(self):
|
||||
try:
|
||||
self._baudrate = self.udev.set_baudrate(self._baudrate, True)
|
||||
self.udev.set_line_property(self._bytesize,
|
||||
self._stopbits,
|
||||
self._parity)
|
||||
if self._rtscts:
|
||||
self.udev.set_flowctrl('hw')
|
||||
elif self._xonxoff:
|
||||
self.udev.set_flowctrl('sw')
|
||||
else:
|
||||
self.udev.set_flowctrl('')
|
||||
try:
|
||||
self.udev.set_dynamic_latency(12, 200, 50)
|
||||
except AttributeError:
|
||||
# backend does not support this feature
|
||||
pass
|
||||
except IOError as exc:
|
||||
err = self.udev.get_error_string()
|
||||
raise SerialException(f'{exc} ({err})') from exc
|
||||
|
||||
def _set_open_state(self, open_):
|
||||
self.is_open = bool(open_)
|
||||
|
||||
|
||||
# assemble Serial class with the platform specific implementation and the base
|
||||
# for file-like behavior.
|
||||
class Serial(FtdiSerial, RawIOBase):
|
||||
|
||||
BACKEND = 'pyftdi'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
RawIOBase.__init__(self)
|
||||
FtdiSerial.__init__(self, *args, **kwargs)
|
213
lib/python3.11/site-packages/pyftdi/serialext/protocol_unix.py
Normal file
213
lib/python3.11/site-packages/pyftdi/serialext/protocol_unix.py
Normal file
|
@ -0,0 +1,213 @@
|
|||
# Copyright (c) 2008-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
|
||||
# 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=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,
|
||||
VERSION as pyserialver)
|
||||
from ..misc import hexdump
|
||||
|
||||
|
||||
__all__ = ['Serial']
|
||||
|
||||
|
||||
class SerialExceptionWithErrno(SerialException):
|
||||
"""Serial exception with errno extension"""
|
||||
|
||||
def __init__(self, message, errno=None):
|
||||
SerialException.__init__(self, message)
|
||||
self.errno = errno
|
||||
|
||||
|
||||
class SocketSerial(SerialBase):
|
||||
"""Fake serial port redirected to a Unix socket.
|
||||
|
||||
This is basically a copy of the serialposix serial port implementation
|
||||
with redefined IO for a Unix socket"""
|
||||
|
||||
BACKEND = 'socket'
|
||||
VIRTUAL_DEVICE = True
|
||||
|
||||
PYSERIAL_VERSION = tuple(int(x) for x in pyserialver.split('.'))
|
||||
|
||||
def _reconfigure_port(self):
|
||||
pass
|
||||
|
||||
def open(self):
|
||||
"""Open the initialized serial port"""
|
||||
if self._port is None:
|
||||
raise SerialException("Port must be configured before use.")
|
||||
if self.isOpen():
|
||||
raise SerialException("Port is already open.")
|
||||
self._dump = False
|
||||
self.sock = None
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
filename = self.portstr[self.portstr.index('://')+3:]
|
||||
if filename.startswith('~/'):
|
||||
home = os.getenv('HOME')
|
||||
if home:
|
||||
filename = os.path.join(home, filename[2:])
|
||||
self._filename = filename
|
||||
self.sock.connect(self._filename)
|
||||
except Exception as exc:
|
||||
self.close()
|
||||
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
|
||||
|
||||
def close(self):
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.sock = None
|
||||
self._set_open_state(False)
|
||||
|
||||
def in_waiting(self):
|
||||
"""Return the number of characters currently in the input buffer."""
|
||||
return 0
|
||||
|
||||
def read(self, size=1):
|
||||
"""Read size bytes from the serial port. If a timeout is set it may
|
||||
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
|
||||
read = bytearray()
|
||||
if size > 0:
|
||||
while len(read) < size:
|
||||
ready, _, _ = select.select([self.sock], [], [], self._timeout)
|
||||
if not ready:
|
||||
break # timeout
|
||||
buf = self.sock.recv(size-len(read))
|
||||
if not buf:
|
||||
# Some character is ready, but none can be read
|
||||
# it is a marker for a disconnected peer
|
||||
raise PortNotOpenError
|
||||
read += buf
|
||||
if self._timeout >= 0 and not buf:
|
||||
break # early abort on timeout
|
||||
return read
|
||||
|
||||
def write(self, data):
|
||||
"""Output the given string over the serial port."""
|
||||
if self.sock is None:
|
||||
raise PortNotOpenError
|
||||
t = len(data)
|
||||
d = data
|
||||
while t > 0:
|
||||
try:
|
||||
if self.writeTimeout is not None and self.writeTimeout > 0:
|
||||
_, ready, _ = select.select([], [self.sock], [],
|
||||
self.writeTimeout)
|
||||
if not ready:
|
||||
raise TimeoutError()
|
||||
n = self.sock.send(d)
|
||||
if self._dump:
|
||||
print(hexdump(d[:n]))
|
||||
if self.writeTimeout is not None and self.writeTimeout > 0:
|
||||
_, ready, _ = select.select([], [self.sock], [],
|
||||
self.writeTimeout)
|
||||
if not ready:
|
||||
raise TimeoutError()
|
||||
d = d[n:]
|
||||
t = t - n
|
||||
except OSError as e:
|
||||
if e.errno != errno.EAGAIN:
|
||||
raise
|
||||
|
||||
def flush(self):
|
||||
"""Flush of file like objects. In this case, wait until all data
|
||||
is written."""
|
||||
|
||||
def reset_input_buffer(self):
|
||||
"""Clear input buffer, discarding all that is in the buffer."""
|
||||
|
||||
def reset_output_buffer(self):
|
||||
"""Clear output buffer, aborting the current output and
|
||||
discarding all that is in the buffer."""
|
||||
|
||||
def send_break(self, duration=0.25):
|
||||
"""Send break condition. Not supported"""
|
||||
|
||||
def _update_break_state(self):
|
||||
"""Send break condition. Not supported"""
|
||||
|
||||
def _update_rts_state(self):
|
||||
"""Set terminal status line: Request To Send"""
|
||||
|
||||
def _update_dtr_state(self):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
|
||||
def setDTR(self, value=1):
|
||||
"""Set terminal status line: Data Terminal Ready"""
|
||||
|
||||
@property
|
||||
def cts(self):
|
||||
"""Read terminal status line: Clear To Send"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def dsr(self):
|
||||
"""Read terminal status line: Data Set Ready"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def ri(self):
|
||||
"""Read terminal status line: Ring Indicator"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def cd(self):
|
||||
"""Read terminal status line: Carrier Detect"""
|
||||
return False
|
||||
|
||||
# - - platform specific - - - -
|
||||
|
||||
def nonblocking(self):
|
||||
"""internal - not portable!"""
|
||||
if self.sock is None:
|
||||
raise PortNotOpenError
|
||||
self.sock.setblocking(0)
|
||||
|
||||
def dump(self, enable):
|
||||
self._dump = enable
|
||||
|
||||
# - - Helpers - -
|
||||
|
||||
def _set_open_state(self, open_):
|
||||
if self.PYSERIAL_VERSION < (3, 0):
|
||||
self._isOpen = bool(open_)
|
||||
else:
|
||||
self.is_open = bool(open_)
|
||||
|
||||
|
||||
# assemble Serial class with the platform specifc implementation and the base
|
||||
# for file-like behavior.
|
||||
class Serial(SocketSerial, RawIOBase):
|
||||
pass
|
949
lib/python3.11/site-packages/pyftdi/spi.py
Normal file
949
lib/python3.11/site-packages/pyftdi/spi.py
Normal file
|
@ -0,0 +1,949 @@
|
|||
# Copyright (c) 2010-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""SPI support for PyFdti"""
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
from logging import getLogger
|
||||
from struct import calcsize as scalc, pack as spack, unpack as sunpack
|
||||
from threading import Lock
|
||||
from typing import Any, Iterable, Mapping, Optional, Set, Union
|
||||
from usb.core import Device as UsbDevice
|
||||
from .ftdi import Ftdi, FtdiError
|
||||
|
||||
|
||||
class SpiIOError(FtdiError):
|
||||
"""SPI I/O error"""
|
||||
|
||||
|
||||
class SpiPort:
|
||||
"""SPI port
|
||||
|
||||
An SPI port is never instanciated directly: use
|
||||
:py:meth:`SpiController.get_port()` method to obtain an SPI port.
|
||||
|
||||
Example:
|
||||
|
||||
>>> ctrl = SpiController()
|
||||
>>> ctrl.configure('ftdi://ftdi:232h/1')
|
||||
>>> spi = ctrl.get_port(1)
|
||||
>>> spi.set_frequency(1000000)
|
||||
>>> # send 2 bytes
|
||||
>>> spi.exchange([0x12, 0x34])
|
||||
>>> # send 2 bytes, then receive 2 bytes
|
||||
>>> out = spi.exchange([0x12, 0x34], 2)
|
||||
>>> # send 2 bytes, then receive 4 bytes, manage the transaction
|
||||
>>> out = spi.exchange([0x12, 0x34], 2, True, False)
|
||||
>>> out.extend(spi.exchange([], 2, False, True))
|
||||
"""
|
||||
|
||||
def __init__(self, controller: 'SpiController', cs: int, cs_hold: int = 3,
|
||||
spi_mode: int = 0):
|
||||
self.log = getLogger('pyftdi.spi.port')
|
||||
self._controller = controller
|
||||
self._frequency = self._controller.frequency
|
||||
self._cs = cs
|
||||
self._cs_hold = cs_hold
|
||||
self.set_mode(spi_mode)
|
||||
|
||||
def exchange(self, out: Union[bytes, bytearray, Iterable[int]] = b'',
|
||||
readlen: int = 0, start: bool = True, stop: bool = True,
|
||||
duplex: bool = False, droptail: int = 0) -> bytes:
|
||||
"""Perform an exchange or a transaction with the SPI slave
|
||||
|
||||
:param out: data to send to the SPI slave, may be empty to read out
|
||||
data from the slave with no write.
|
||||
:param readlen: count of bytes to read out from the slave,
|
||||
may be zero to only write to the slave
|
||||
:param start: whether to start an SPI transaction, i.e.
|
||||
activate the /CS line for the slave. Use False to
|
||||
resume a previously started transaction
|
||||
:param stop: whether to desactivete the /CS line for the slave.
|
||||
Use False if the transaction should complete with a
|
||||
further call to exchange()
|
||||
:param duplex: perform a full-duplex exchange (vs. half-duplex),
|
||||
i.e. bits are clocked in and out at once.
|
||||
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
|
||||
accesses)
|
||||
:return: an array of bytes containing the data read out from the
|
||||
slave
|
||||
"""
|
||||
return self._controller.exchange(self._frequency, out, readlen,
|
||||
start and self._cs_prolog,
|
||||
stop and self._cs_epilog,
|
||||
self._cpol, self._cpha,
|
||||
duplex, droptail)
|
||||
|
||||
def read(self, readlen: int = 0, start: bool = True, stop: bool = True,
|
||||
droptail: int = 0) -> bytes:
|
||||
"""Read out bytes from the slave
|
||||
|
||||
:param readlen: count of bytes to read out from the slave,
|
||||
may be zero to only write to the slave
|
||||
:param start: whether to start an SPI transaction, i.e.
|
||||
activate the /CS line for the slave. Use False to
|
||||
resume a previously started transaction
|
||||
:param stop: whether to desactivete the /CS line for the slave.
|
||||
Use False if the transaction should complete with a
|
||||
further call to exchange()
|
||||
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
|
||||
accesses)
|
||||
:return: an array of bytes containing the data read out from the
|
||||
slave
|
||||
"""
|
||||
return self._controller.exchange(self._frequency, [], readlen,
|
||||
start and self._cs_prolog,
|
||||
stop and self._cs_epilog,
|
||||
self._cpol, self._cpha, False,
|
||||
droptail)
|
||||
|
||||
def write(self, out: Union[bytes, bytearray, Iterable[int]],
|
||||
start: bool = True, stop: bool = True, droptail: int = 0) \
|
||||
-> None:
|
||||
"""Write bytes to the slave
|
||||
|
||||
:param out: data to send to the SPI slave, may be empty to read out
|
||||
data from the slave with no write.
|
||||
:param start: whether to start an SPI transaction, i.e.
|
||||
activate the /CS line for the slave. Use False to
|
||||
resume a previously started transaction
|
||||
:param stop: whether to desactivete the /CS line for the slave.
|
||||
Use False if the transaction should complete with a
|
||||
further call to exchange()
|
||||
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
|
||||
accesses)
|
||||
"""
|
||||
return self._controller.exchange(self._frequency, out, 0,
|
||||
start and self._cs_prolog,
|
||||
stop and self._cs_epilog,
|
||||
self._cpol, self._cpha, False,
|
||||
droptail)
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Force the flush of the HW FIFOs"""
|
||||
self._controller.flush()
|
||||
|
||||
def set_frequency(self, frequency: float):
|
||||
"""Change SPI bus frequency
|
||||
|
||||
:param float frequency: the new frequency in Hz
|
||||
"""
|
||||
self._frequency = min(frequency, self._controller.frequency_max)
|
||||
|
||||
def set_mode(self, mode: int, cs_hold: Optional[int] = None) -> None:
|
||||
"""Set or change the SPI mode to communicate with the SPI slave.
|
||||
|
||||
:param mode: new SPI mode
|
||||
:param cs_hold: change the /CS hold duration (or keep using previous
|
||||
value)
|
||||
"""
|
||||
if not 0 <= mode <= 3:
|
||||
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')
|
||||
if cs_hold is None:
|
||||
cs_hold = self._cs_hold
|
||||
else:
|
||||
self._cs_hold = cs_hold
|
||||
self._cpol = bool(mode & 0x2)
|
||||
self._cpha = bool(mode & 0x1)
|
||||
cs_clock = 0xFF & ~((int(not self._cpol) and SpiController.SCK_BIT) |
|
||||
SpiController.DO_BIT)
|
||||
cs_select = 0xFF & ~((SpiController.CS_BIT << self._cs) |
|
||||
(int(not self._cpol) and SpiController.SCK_BIT) |
|
||||
SpiController.DO_BIT)
|
||||
self._cs_prolog = bytes([cs_clock, cs_select])
|
||||
self._cs_epilog = bytes([cs_select] + [cs_clock] * int(cs_hold))
|
||||
|
||||
def force_select(self, level: Optional[bool] = None,
|
||||
cs_hold: float = 0) -> None:
|
||||
"""Force-drive /CS signal.
|
||||
|
||||
This API is not designed for a regular usage, but is reserved to
|
||||
very specific slave devices that require non-standard SPI
|
||||
signalling. There are very few use cases where this API is required.
|
||||
|
||||
:param level: level to force on /CS output. This is a tri-state
|
||||
value. A boolean value forces the selected signal
|
||||
level; note that SpiPort no longer enforces that
|
||||
following API calls generates valid SPI signalling:
|
||||
use with extreme care. `None` triggers a pulse on /CS
|
||||
output, i.e. /CS is not asserted once the method
|
||||
returns, whatever the actual /CS level when this API
|
||||
is called.
|
||||
:param cs_hold: /CS hold duration, as a unitless value. It is not
|
||||
possible to control the exact duration of the pulse,
|
||||
as it depends on the USB bus and the FTDI frequency.
|
||||
"""
|
||||
clk, sel = self._cs_prolog
|
||||
if cs_hold:
|
||||
hold = max(1, cs_hold)
|
||||
if hold > SpiController.PAYLOAD_MAX_LENGTH:
|
||||
raise ValueError('cs_hold is too long')
|
||||
else:
|
||||
hold = self._cs_hold
|
||||
if level is None:
|
||||
seq = bytearray([clk])
|
||||
seq.extend([sel]*(1+hold))
|
||||
seq.extend([clk]*self._cs_hold)
|
||||
elif level:
|
||||
seq = bytearray([clk] * hold)
|
||||
else:
|
||||
seq = bytearray([clk] * hold)
|
||||
seq.extend([sel]*(1+hold))
|
||||
self._controller.force_control(self._frequency, bytes(seq))
|
||||
|
||||
@property
|
||||
def frequency(self) -> float:
|
||||
"""Return the current SPI bus block"""
|
||||
return self._frequency
|
||||
|
||||
@property
|
||||
def cs(self) -> int:
|
||||
"""Return the /CS index.
|
||||
|
||||
:return: the /CS index (starting from 0)
|
||||
"""
|
||||
return self._cs
|
||||
|
||||
@property
|
||||
def mode(self) -> int:
|
||||
"""Return the current SPI mode.
|
||||
|
||||
:return: the SPI mode
|
||||
"""
|
||||
return (int(self._cpol) << 2) | int(self._cpha)
|
||||
|
||||
|
||||
class SpiGpioPort:
|
||||
"""GPIO port
|
||||
|
||||
A SpiGpioPort instance enables to drive GPIOs wich are not reserved for
|
||||
SPI feature as regular GPIOs.
|
||||
|
||||
GPIO are managed as a bitfield. The LSBs are reserved for the SPI
|
||||
feature, which means that the lowest pin that can be used as a GPIO is
|
||||
*b4*:
|
||||
|
||||
* *b0*: SPI SCLK
|
||||
* *b1*: SPI MOSI
|
||||
* *b2*: SPI MISO
|
||||
* *b3*: SPI CS0
|
||||
* *b4*: SPI CS1 or first GPIO
|
||||
|
||||
If more than one SPI device is used, less GPIO pins are available, see
|
||||
the cs_count argument of the SpiController constructor.
|
||||
|
||||
There is no offset bias in GPIO bit position, *i.e.* the first available
|
||||
GPIO can be reached from as ``0x10``.
|
||||
|
||||
Bitfield size depends on the FTDI device: 4432H series use 8-bit GPIO
|
||||
ports, while 232H and 2232H series use wide 16-bit ports.
|
||||
|
||||
An SpiGpio port is never instanciated directly: use
|
||||
:py:meth:`SpiController.get_gpio()` method to obtain the GPIO port.
|
||||
"""
|
||||
def __init__(self, controller: 'SpiController'):
|
||||
self.log = getLogger('pyftdi.spi.gpio')
|
||||
self._controller = controller
|
||||
|
||||
@property
|
||||
def pins(self) -> int:
|
||||
"""Report the configured GPIOs as a bitfield.
|
||||
|
||||
A true bit represents a GPIO, a false bit a reserved or not
|
||||
configured pin.
|
||||
|
||||
:return: the bitfield of configured GPIO pins.
|
||||
"""
|
||||
return self._controller.gpio_pins
|
||||
|
||||
@property
|
||||
def all_pins(self) -> int:
|
||||
"""Report the addressable GPIOs as a bitfield.
|
||||
|
||||
A true bit represents a pin which may be used as a GPIO, a false bit
|
||||
a reserved pin (for SPI support)
|
||||
|
||||
:return: the bitfield of configurable GPIO pins.
|
||||
"""
|
||||
return self._controller.gpio_all_pins
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
"""Report the FTDI count of addressable pins.
|
||||
|
||||
Note that all pins, including reserved SPI ones, are reported.
|
||||
|
||||
:return: the count of IO pins (including SPI ones).
|
||||
"""
|
||||
return self._controller.width
|
||||
|
||||
@property
|
||||
def direction(self) -> int:
|
||||
"""Provide the FTDI GPIO direction.self
|
||||
|
||||
A true bit represents an output GPIO, a false bit an input GPIO.
|
||||
|
||||
:return: the bitfield of direction.
|
||||
"""
|
||||
return self._controller.direction
|
||||
|
||||
def read(self, with_output: bool = False) -> int:
|
||||
"""Read GPIO port.
|
||||
|
||||
:param with_output: set to unmask output pins
|
||||
:return: the GPIO port pins as a bitfield
|
||||
"""
|
||||
return self._controller.read_gpio(with_output)
|
||||
|
||||
def write(self, value: int) -> None:
|
||||
"""Write GPIO port.
|
||||
|
||||
:param value: the GPIO port pins as a bitfield
|
||||
"""
|
||||
return self._controller.write_gpio(value)
|
||||
|
||||
def set_direction(self, pins: int, direction: int) -> None:
|
||||
"""Change the direction of the GPIO pins.
|
||||
|
||||
:param pins: which GPIO pins should be reconfigured
|
||||
:param direction: direction bitfield (high level for output)
|
||||
"""
|
||||
self._controller.set_gpio_direction(pins, direction)
|
||||
|
||||
|
||||
class SpiController:
|
||||
"""SPI master.
|
||||
|
||||
:param int cs_count: is the number of /CS lines (one per device to
|
||||
drive on the SPI bus)
|
||||
:param turbo: increase throughput over USB bus, but may not be
|
||||
supported with some specific slaves
|
||||
"""
|
||||
|
||||
SCK_BIT = 0x01
|
||||
DO_BIT = 0x02
|
||||
DI_BIT = 0x04
|
||||
CS_BIT = 0x08
|
||||
SPI_BITS = DI_BIT | DO_BIT | SCK_BIT
|
||||
PAYLOAD_MAX_LENGTH = 0xFF00 # 16 bits max (- spare for control)
|
||||
|
||||
def __init__(self, cs_count: int = 1, turbo: bool = True):
|
||||
self.log = getLogger('pyftdi.spi.ctrl')
|
||||
self._ftdi = Ftdi()
|
||||
self._lock = Lock()
|
||||
self._gpio_port = None
|
||||
self._gpio_dir = 0
|
||||
self._gpio_mask = 0
|
||||
self._gpio_low = 0
|
||||
self._wide_port = False
|
||||
self._cs_count = cs_count
|
||||
self._turbo = turbo
|
||||
self._immediate = bytes((Ftdi.SEND_IMMEDIATE,))
|
||||
self._frequency = 0.0
|
||||
self._clock_phase = False
|
||||
self._cs_bits = 0
|
||||
self._spi_ports = []
|
||||
self._spi_dir = 0
|
||||
self._spi_mask = self.SPI_BITS
|
||||
|
||||
def configure(self, url: Union[str, UsbDevice],
|
||||
**kwargs: Mapping[str, Any]) -> None:
|
||||
"""Configure the FTDI interface as a SPI master
|
||||
|
||||
:param url: FTDI URL string, such as ``ftdi://ftdi:232h/1``
|
||||
:param kwargs: options to configure the SPI bus
|
||||
|
||||
Accepted options:
|
||||
|
||||
* ``interface``: when URL is specifed as a USB device, the interface
|
||||
named argument can be used to select a specific port of the FTDI
|
||||
device, as an integer starting from 1.
|
||||
* ``direction`` a bitfield specifying the FTDI GPIO direction,
|
||||
where high level defines an output, and low level defines an
|
||||
input. Only useful to setup default IOs at start up, use
|
||||
:py:class:`SpiGpioPort` to drive GPIOs. Note that pins reserved
|
||||
for SPI feature take precedence over any this setting.
|
||||
* ``initial`` a bitfield specifying the initial output value. Only
|
||||
useful to setup default IOs at start up, use
|
||||
:py:class:`SpiGpioPort` to drive GPIOs.
|
||||
* ``frequency`` the SPI bus frequency in Hz. Note that each slave
|
||||
may reconfigure the SPI bus with a specialized
|
||||
frequency.
|
||||
* ``cs_count`` count of chip select signals dedicated to select
|
||||
SPI slave devices, starting from A*BUS3 pin
|
||||
* ``turbo`` whether to enable or disable turbo mode
|
||||
* ``debug`` to increase log verbosity, using MPSSE tracer
|
||||
"""
|
||||
# it is better to specify CS and turbo in configure, but the older
|
||||
# API where these parameters are specified at instanciation has been
|
||||
# preserved
|
||||
if 'cs_count' in kwargs:
|
||||
self._cs_count = int(kwargs['cs_count'])
|
||||
del kwargs['cs_count']
|
||||
if not 1 <= self._cs_count <= 5:
|
||||
raise ValueError(f'Unsupported CS line count: {self._cs_count}')
|
||||
if 'turbo' in kwargs:
|
||||
self._turbo = bool(kwargs['turbo'])
|
||||
del kwargs['turbo']
|
||||
if 'direction' in kwargs:
|
||||
io_dir = int(kwargs['direction'])
|
||||
del kwargs['direction']
|
||||
else:
|
||||
io_dir = 0
|
||||
if 'initial' in kwargs:
|
||||
io_out = int(kwargs['initial'])
|
||||
del kwargs['initial']
|
||||
else:
|
||||
io_out = 0
|
||||
if 'interface' in kwargs:
|
||||
if isinstance(url, str):
|
||||
raise SpiIOError('url and interface are mutually exclusive')
|
||||
interface = int(kwargs['interface'])
|
||||
del kwargs['interface']
|
||||
else:
|
||||
interface = 1
|
||||
with self._lock:
|
||||
if self._frequency > 0.0:
|
||||
raise SpiIOError('Already configured')
|
||||
self._cs_bits = (((SpiController.CS_BIT << self._cs_count) - 1) &
|
||||
~(SpiController.CS_BIT - 1))
|
||||
self._spi_ports = [None] * self._cs_count
|
||||
self._spi_dir = (self._cs_bits |
|
||||
SpiController.DO_BIT |
|
||||
SpiController.SCK_BIT)
|
||||
self._spi_mask = self._cs_bits | self.SPI_BITS
|
||||
# until the device is open, there is no way to tell if it has a
|
||||
# wide (16) or narrow port (8). Lower API can deal with any, so
|
||||
# delay any truncation till the device is actually open
|
||||
self._set_gpio_direction(16, (~self._spi_mask) & 0xFFFF, io_dir)
|
||||
kwargs['direction'] = self._spi_dir | self._gpio_dir
|
||||
kwargs['initial'] = self._cs_bits | (io_out & self._gpio_mask)
|
||||
if not isinstance(url, str):
|
||||
self._frequency = self._ftdi.open_mpsse_from_device(
|
||||
url, interface=interface, **kwargs)
|
||||
else:
|
||||
self._frequency = self._ftdi.open_mpsse_from_url(url, **kwargs)
|
||||
self._ftdi.enable_adaptive_clock(False)
|
||||
self._wide_port = self._ftdi.has_wide_port
|
||||
if not self._wide_port:
|
||||
self._set_gpio_direction(8, io_out & 0xFF, io_dir & 0xFF)
|
||||
|
||||
def close(self, freeze: bool = False) -> None:
|
||||
"""Close the FTDI interface.
|
||||
|
||||
:param freeze: if set, FTDI port is not reset to its default
|
||||
state on close.
|
||||
"""
|
||||
with self._lock:
|
||||
if self._ftdi.is_connected:
|
||||
self._ftdi.close(freeze)
|
||||
self._frequency = 0.0
|
||||
|
||||
def terminate(self) -> None:
|
||||
"""Close the FTDI interface.
|
||||
|
||||
:note: deprecated API, use close()
|
||||
"""
|
||||
self.close()
|
||||
|
||||
def get_port(self, cs: int, freq: Optional[float] = None,
|
||||
mode: int = 0) -> SpiPort:
|
||||
"""Obtain a SPI port to drive a SPI device selected by Chip Select.
|
||||
|
||||
:note: SPI mode 1 and 3 are not officially supported.
|
||||
|
||||
:param cs: chip select slot, starting from 0
|
||||
:param freq: SPI bus frequency for this slave in Hz
|
||||
:param mode: SPI mode [0, 1, 2, 3]
|
||||
"""
|
||||
with self._lock:
|
||||
if not self._ftdi.is_connected:
|
||||
raise SpiIOError("FTDI controller not initialized")
|
||||
if cs >= len(self._spi_ports):
|
||||
if cs < 5:
|
||||
# increase cs_count (up to 4) to reserve more /CS channels
|
||||
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))
|
||||
self._spi_ports[cs] = SpiPort(self, cs, cs_hold=hold,
|
||||
spi_mode=mode)
|
||||
self._spi_ports[cs].set_frequency(freq)
|
||||
self._flush()
|
||||
return self._spi_ports[cs]
|
||||
|
||||
def get_gpio(self) -> SpiGpioPort:
|
||||
"""Retrieve the GPIO port.
|
||||
|
||||
:return: GPIO port
|
||||
"""
|
||||
with self._lock:
|
||||
if not self._ftdi.is_connected:
|
||||
raise SpiIOError("FTDI controller not initialized")
|
||||
if not self._gpio_port:
|
||||
self._gpio_port = SpiGpioPort(self)
|
||||
return self._gpio_port
|
||||
|
||||
@property
|
||||
def ftdi(self) -> Ftdi:
|
||||
"""Return the Ftdi instance.
|
||||
|
||||
:return: the Ftdi instance
|
||||
"""
|
||||
return self._ftdi
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
"""Test whether the device has been properly configured.
|
||||
|
||||
:return: True if configured
|
||||
"""
|
||||
return self._ftdi.is_connected
|
||||
|
||||
@property
|
||||
def frequency_max(self) -> float:
|
||||
"""Provides the maximum SPI clock frequency in Hz.
|
||||
|
||||
:return: SPI bus clock frequency
|
||||
"""
|
||||
return self._ftdi.frequency_max
|
||||
|
||||
@property
|
||||
def frequency(self) -> float:
|
||||
"""Provides the current SPI clock frequency in Hz.
|
||||
|
||||
:return: the SPI bus clock frequency
|
||||
"""
|
||||
return self._frequency
|
||||
|
||||
@property
|
||||
def direction(self):
|
||||
"""Provide the FTDI pin direction
|
||||
|
||||
A true bit represents an output pin, a false bit an input pin.
|
||||
|
||||
:return: the bitfield of direction.
|
||||
"""
|
||||
return self._spi_dir | self._gpio_dir
|
||||
|
||||
@property
|
||||
def channels(self) -> int:
|
||||
"""Provide the maximum count of slaves.
|
||||
|
||||
|
||||
:return: the count of pins reserved to drive the /CS signal
|
||||
"""
|
||||
return self._cs_count
|
||||
|
||||
@property
|
||||
def active_channels(self) -> Set[int]:
|
||||
"""Provide the set of configured slaves /CS.
|
||||
|
||||
:return: Set of /CS, one for each configured slaves
|
||||
"""
|
||||
return {port[0] for port in enumerate(self._spi_ports) if port[1]}
|
||||
|
||||
@property
|
||||
def gpio_pins(self):
|
||||
"""Report the configured GPIOs as a bitfield.
|
||||
|
||||
A true bit represents a GPIO, a false bit a reserved or not
|
||||
configured pin.
|
||||
|
||||
:return: the bitfield of configured GPIO pins.
|
||||
"""
|
||||
with self._lock:
|
||||
return self._gpio_mask
|
||||
|
||||
@property
|
||||
def gpio_all_pins(self):
|
||||
"""Report the addressable GPIOs as a bitfield.
|
||||
|
||||
A true bit represents a pin which may be used as a GPIO, a false bit
|
||||
a reserved pin (for SPI support)
|
||||
|
||||
:return: the bitfield of configurable GPIO pins.
|
||||
"""
|
||||
mask = (1 << self.width) - 1
|
||||
with self._lock:
|
||||
return mask & ~self._spi_mask
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""Report the FTDI count of addressable pins.
|
||||
|
||||
:return: the count of IO pins (including SPI ones).
|
||||
"""
|
||||
return 16 if self._wide_port else 8
|
||||
|
||||
@property
|
||||
def is_inverted_cpha_supported(self) -> bool:
|
||||
"""Report whether it is possible to supported CPHA=1.
|
||||
|
||||
:return: inverted CPHA supported (with a kludge)
|
||||
"""
|
||||
return self._ftdi.is_H_series
|
||||
|
||||
def exchange(self, frequency: float,
|
||||
out: Union[bytes, bytearray, Iterable[int]], readlen: int,
|
||||
cs_prolog: Optional[bytes] = None,
|
||||
cs_epilog: Optional[bytes] = None,
|
||||
cpol: bool = False, cpha: bool = False,
|
||||
duplex: bool = False, droptail: int = 0) -> bytes:
|
||||
"""Perform an exchange or a transaction with the SPI slave
|
||||
|
||||
:param out: data to send to the SPI slave, may be empty to read out
|
||||
data from the slave with no write.
|
||||
:param readlen: count of bytes to read out from the slave,
|
||||
may be zero to only write to the slave,
|
||||
:param cs_prolog: the prolog MPSSE command sequence to execute
|
||||
before the actual exchange.
|
||||
:param cs_epilog: the epilog MPSSE command sequence to execute
|
||||
after the actual exchange.
|
||||
:param cpol: SPI clock polarity, derived from the SPI mode
|
||||
:param cpol: SPI clock phase, derived from the SPI mode
|
||||
:param duplex: perform a full-duplex exchange (vs. half-duplex),
|
||||
i.e. bits are clocked in and out at once or
|
||||
in a write-then-read manner.
|
||||
:param droptail: ignore up to 7 last bits (for non-byte sized SPI
|
||||
accesses)
|
||||
:return: bytes containing the data read out from the slave, if any
|
||||
"""
|
||||
if not 0 <= droptail <= 7:
|
||||
raise ValueError('Invalid skip bit count')
|
||||
if duplex:
|
||||
if readlen > len(out):
|
||||
tmp = bytearray(out)
|
||||
tmp.extend([0] * (readlen - len(out)))
|
||||
out = tmp
|
||||
elif not readlen:
|
||||
readlen = len(out)
|
||||
with self._lock:
|
||||
if duplex:
|
||||
data = self._exchange_full_duplex(frequency, out,
|
||||
cs_prolog, cs_epilog,
|
||||
cpol, cpha, droptail)
|
||||
return data[:readlen]
|
||||
return self._exchange_half_duplex(frequency, out, readlen,
|
||||
cs_prolog, cs_epilog,
|
||||
cpol, cpha, droptail)
|
||||
|
||||
def force_control(self, frequency: float, sequence: bytes) -> None:
|
||||
"""Execution an arbitrary SPI control bit sequence.
|
||||
Use with extreme care, as it may lead to unexpected results. Regular
|
||||
usage of SPI does not require to invoke this API.
|
||||
|
||||
:param sequence: the bit sequence to execute.
|
||||
"""
|
||||
with self._lock:
|
||||
self._force(frequency, sequence)
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush the HW FIFOs.
|
||||
"""
|
||||
with self._lock:
|
||||
self._flush()
|
||||
|
||||
def read_gpio(self, with_output: bool = False) -> int:
|
||||
"""Read GPIO port
|
||||
|
||||
:param with_output: set to unmask output pins
|
||||
:return: the GPIO port pins as a bitfield
|
||||
"""
|
||||
with self._lock:
|
||||
data = self._read_raw(self._wide_port)
|
||||
value = data & self._gpio_mask
|
||||
if not with_output:
|
||||
value &= ~self._gpio_dir
|
||||
return value
|
||||
|
||||
def write_gpio(self, value: int) -> None:
|
||||
"""Write GPIO port
|
||||
|
||||
:param value: the GPIO port pins as a bitfield
|
||||
"""
|
||||
with self._lock:
|
||||
if (value & 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)
|
||||
data &= ~self._gpio_mask
|
||||
data |= value
|
||||
self._write_raw(data, use_high)
|
||||
self._gpio_low = data & 0xFF & ~self._spi_mask
|
||||
|
||||
def set_gpio_direction(self, pins: int, direction: int) -> None:
|
||||
"""Change the direction of the GPIO pins
|
||||
|
||||
:param pins: which GPIO pins should be reconfigured
|
||||
:param direction: direction bitfield (on for output)
|
||||
"""
|
||||
with self._lock:
|
||||
self._set_gpio_direction(16 if self._wide_port else 8,
|
||||
pins, direction)
|
||||
|
||||
def _set_gpio_direction(self, width: int, pins: int,
|
||||
direction: int) -> None:
|
||||
if pins & self._spi_mask:
|
||||
raise SpiIOError('Cannot access SPI pins as GPIO')
|
||||
gpio_mask = (1 << width) - 1
|
||||
gpio_mask &= ~self._spi_mask
|
||||
if (pins & gpio_mask) != pins:
|
||||
raise SpiIOError('No such GPIO pin(s)')
|
||||
self._gpio_dir &= ~pins
|
||||
self._gpio_dir |= (pins & direction)
|
||||
self._gpio_mask = gpio_mask & pins
|
||||
|
||||
def _read_raw(self, read_high: bool) -> int:
|
||||
if not self._ftdi.is_connected:
|
||||
raise SpiIOError("FTDI controller not initialized")
|
||||
if read_high:
|
||||
cmd = bytes([Ftdi.GET_BITS_LOW,
|
||||
Ftdi.GET_BITS_HIGH,
|
||||
Ftdi.SEND_IMMEDIATE])
|
||||
fmt = '<H'
|
||||
else:
|
||||
cmd = bytes([Ftdi.GET_BITS_LOW,
|
||||
Ftdi.SEND_IMMEDIATE])
|
||||
fmt = 'B'
|
||||
self._ftdi.write_data(cmd)
|
||||
size = scalc(fmt)
|
||||
data = self._ftdi.read_data_bytes(size, 4)
|
||||
if len(data) != size:
|
||||
raise SpiIOError('Cannot read GPIO')
|
||||
value, = sunpack(fmt, data)
|
||||
return value
|
||||
|
||||
def _write_raw(self, data: int, write_high: bool) -> None:
|
||||
if not self._ftdi.is_connected:
|
||||
raise SpiIOError("FTDI controller not initialized")
|
||||
direction = self.direction
|
||||
low_data = data & 0xFF
|
||||
low_dir = direction & 0xFF
|
||||
if write_high:
|
||||
high_data = (data >> 8) & 0xFF
|
||||
high_dir = (direction >> 8) & 0xFF
|
||||
cmd = bytes([Ftdi.SET_BITS_LOW, low_data, low_dir,
|
||||
Ftdi.SET_BITS_HIGH, high_data, high_dir])
|
||||
else:
|
||||
cmd = bytes([Ftdi.SET_BITS_LOW, low_data, low_dir])
|
||||
self._ftdi.write_data(cmd)
|
||||
|
||||
def _force(self, frequency: float, sequence: bytes):
|
||||
if not self._ftdi.is_connected:
|
||||
raise SpiIOError("FTDI controller not initialized")
|
||||
if len(sequence) > SpiController.PAYLOAD_MAX_LENGTH:
|
||||
raise SpiIOError("Output payload is too large")
|
||||
if self._frequency != frequency:
|
||||
self._ftdi.set_frequency(frequency)
|
||||
# store the requested value, not the actual one (best effort),
|
||||
# to avoid setting unavailable values on each call.
|
||||
self._frequency = frequency
|
||||
cmd = bytearray()
|
||||
direction = self.direction & 0xFF
|
||||
for ctrl in sequence:
|
||||
ctrl &= self._spi_mask
|
||||
ctrl |= self._gpio_low
|
||||
cmd.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
|
||||
self._ftdi.write_data(cmd)
|
||||
|
||||
def _exchange_half_duplex(self, frequency: float,
|
||||
out: Union[bytes, bytearray, Iterable[int]],
|
||||
readlen: int, cs_prolog: bytes, cs_epilog: bytes,
|
||||
cpol: bool, cpha: bool,
|
||||
droptail: int) -> bytes:
|
||||
if not self._ftdi.is_connected:
|
||||
raise SpiIOError("FTDI controller not initialized")
|
||||
if len(out) > SpiController.PAYLOAD_MAX_LENGTH:
|
||||
raise SpiIOError("Output payload is too large")
|
||||
if readlen > SpiController.PAYLOAD_MAX_LENGTH:
|
||||
raise SpiIOError("Input payload is too large")
|
||||
if cpha:
|
||||
# to enable CPHA, we need to use a workaround with FTDI device,
|
||||
# that is enable 3-phase clocking (which is usually dedicated to
|
||||
# I2C support). This mode use use 3 clock period instead of 2,
|
||||
# which implies the FTDI frequency should be fixed to match the
|
||||
# requested one.
|
||||
frequency = (3*frequency)//2
|
||||
if self._frequency != frequency:
|
||||
self._ftdi.set_frequency(frequency)
|
||||
# store the requested value, not the actual one (best effort),
|
||||
# to avoid setting unavailable values on each call.
|
||||
self._frequency = frequency
|
||||
direction = self.direction & 0xFF # low bits only
|
||||
cmd = bytearray()
|
||||
for ctrl in cs_prolog or []:
|
||||
ctrl &= self._spi_mask
|
||||
ctrl |= self._gpio_low
|
||||
cmd.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
|
||||
epilog = bytearray()
|
||||
if cs_epilog:
|
||||
for ctrl in cs_epilog:
|
||||
ctrl &= self._spi_mask
|
||||
ctrl |= self._gpio_low
|
||||
epilog.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
|
||||
# Restore idle state
|
||||
cs_high = [Ftdi.SET_BITS_LOW, self._cs_bits | self._gpio_low,
|
||||
direction]
|
||||
if not self._turbo:
|
||||
cs_high.append(Ftdi.SEND_IMMEDIATE)
|
||||
epilog.extend(cs_high)
|
||||
writelen = len(out)
|
||||
if self._clock_phase != cpha:
|
||||
self._ftdi.enable_3phase_clock(cpha)
|
||||
self._clock_phase = cpha
|
||||
if writelen:
|
||||
if not droptail:
|
||||
wcmd = (Ftdi.WRITE_BYTES_NVE_MSB if not cpol else
|
||||
Ftdi.WRITE_BYTES_PVE_MSB)
|
||||
write_cmd = spack('<BH', wcmd, writelen-1)
|
||||
cmd.extend(write_cmd)
|
||||
cmd.extend(out)
|
||||
else:
|
||||
bytelen = writelen-1
|
||||
if bytelen:
|
||||
wcmd = (Ftdi.WRITE_BYTES_NVE_MSB if not cpol else
|
||||
Ftdi.WRITE_BYTES_PVE_MSB)
|
||||
write_cmd = spack('<BH', wcmd, bytelen-1)
|
||||
cmd.extend(write_cmd)
|
||||
cmd.extend(out[:-1])
|
||||
wcmd = (Ftdi.WRITE_BITS_NVE_MSB if not cpol else
|
||||
Ftdi.WRITE_BITS_PVE_MSB)
|
||||
write_cmd = spack('<BBB', wcmd, 7-droptail, out[-1])
|
||||
cmd.extend(write_cmd)
|
||||
if readlen:
|
||||
if not droptail:
|
||||
rcmd = (Ftdi.READ_BYTES_NVE_MSB if not cpol else
|
||||
Ftdi.READ_BYTES_PVE_MSB)
|
||||
read_cmd = spack('<BH', rcmd, readlen-1)
|
||||
cmd.extend(read_cmd)
|
||||
else:
|
||||
bytelen = readlen-1
|
||||
if bytelen:
|
||||
rcmd = (Ftdi.READ_BYTES_NVE_MSB if not cpol else
|
||||
Ftdi.READ_BYTES_PVE_MSB)
|
||||
read_cmd = spack('<BH', rcmd, bytelen-1)
|
||||
cmd.extend(read_cmd)
|
||||
rcmd = (Ftdi.READ_BITS_NVE_MSB if not cpol else
|
||||
Ftdi.READ_BITS_PVE_MSB)
|
||||
read_cmd = spack('<BB', rcmd, 7-droptail)
|
||||
cmd.extend(read_cmd)
|
||||
cmd.extend(self._immediate)
|
||||
if self._turbo:
|
||||
if epilog:
|
||||
cmd.extend(epilog)
|
||||
self._ftdi.write_data(cmd)
|
||||
else:
|
||||
self._ftdi.write_data(cmd)
|
||||
if epilog:
|
||||
self._ftdi.write_data(epilog)
|
||||
# USB read cycle may occur before the FTDI device has actually
|
||||
# sent the data, so try to read more than once if no data is
|
||||
# actually received
|
||||
data = self._ftdi.read_data_bytes(readlen, 4)
|
||||
if droptail:
|
||||
data[-1] = 0xff & (data[-1] << droptail)
|
||||
else:
|
||||
if writelen:
|
||||
if self._turbo:
|
||||
if epilog:
|
||||
cmd.extend(epilog)
|
||||
self._ftdi.write_data(cmd)
|
||||
else:
|
||||
self._ftdi.write_data(cmd)
|
||||
if epilog:
|
||||
self._ftdi.write_data(epilog)
|
||||
data = bytearray()
|
||||
return data
|
||||
|
||||
def _exchange_full_duplex(self, frequency: float,
|
||||
out: Union[bytes, bytearray, Iterable[int]],
|
||||
cs_prolog: bytes, cs_epilog: bytes,
|
||||
cpol: bool, cpha: bool,
|
||||
droptail: int) -> bytes:
|
||||
if not self._ftdi.is_connected:
|
||||
raise SpiIOError("FTDI controller not initialized")
|
||||
if len(out) > SpiController.PAYLOAD_MAX_LENGTH:
|
||||
raise SpiIOError("Output payload is too large")
|
||||
if cpha:
|
||||
# to enable CPHA, we need to use a workaround with FTDI device,
|
||||
# that is enable 3-phase clocking (which is usually dedicated to
|
||||
# I2C support). This mode use use 3 clock period instead of 2,
|
||||
# which implies the FTDI frequency should be fixed to match the
|
||||
# requested one.
|
||||
frequency = (3*frequency)//2
|
||||
if self._frequency != frequency:
|
||||
self._ftdi.set_frequency(frequency)
|
||||
# store the requested value, not the actual one (best effort),
|
||||
# to avoid setting unavailable values on each call.
|
||||
self._frequency = frequency
|
||||
direction = self.direction & 0xFF # low bits only
|
||||
cmd = bytearray()
|
||||
for ctrl in cs_prolog or []:
|
||||
ctrl &= self._spi_mask
|
||||
ctrl |= self._gpio_low
|
||||
cmd.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
|
||||
epilog = bytearray()
|
||||
if cs_epilog:
|
||||
for ctrl in cs_epilog:
|
||||
ctrl &= self._spi_mask
|
||||
ctrl |= self._gpio_low
|
||||
epilog.extend((Ftdi.SET_BITS_LOW, ctrl, direction))
|
||||
# Restore idle state
|
||||
cs_high = [Ftdi.SET_BITS_LOW, self._cs_bits | self._gpio_low,
|
||||
direction]
|
||||
if not self._turbo:
|
||||
cs_high.append(Ftdi.SEND_IMMEDIATE)
|
||||
epilog.extend(cs_high)
|
||||
exlen = len(out)
|
||||
if self._clock_phase != cpha:
|
||||
self._ftdi.enable_3phase_clock(cpha)
|
||||
self._clock_phase = cpha
|
||||
if not droptail:
|
||||
wcmd = (Ftdi.RW_BYTES_PVE_NVE_MSB if not cpol else
|
||||
Ftdi.RW_BYTES_NVE_PVE_MSB)
|
||||
write_cmd = spack('<BH', wcmd, exlen-1)
|
||||
cmd.extend(write_cmd)
|
||||
cmd.extend(out)
|
||||
else:
|
||||
bytelen = exlen-1
|
||||
if bytelen:
|
||||
wcmd = (Ftdi.RW_BYTES_PVE_NVE_MSB if not cpol else
|
||||
Ftdi.RW_BYTES_NVE_PVE_MSB)
|
||||
write_cmd = spack('<BH', wcmd, bytelen-1)
|
||||
cmd.extend(write_cmd)
|
||||
cmd.extend(out[:-1])
|
||||
wcmd = (Ftdi.RW_BITS_PVE_NVE_MSB if not cpol else
|
||||
Ftdi.RW_BITS_NVE_PVE_MSB)
|
||||
write_cmd = spack('<BBB', wcmd, 7-droptail, out[-1])
|
||||
cmd.extend(write_cmd)
|
||||
cmd.extend(self._immediate)
|
||||
if self._turbo:
|
||||
if epilog:
|
||||
cmd.extend(epilog)
|
||||
self._ftdi.write_data(cmd)
|
||||
else:
|
||||
self._ftdi.write_data(cmd)
|
||||
if epilog:
|
||||
self._ftdi.write_data(epilog)
|
||||
# USB read cycle may occur before the FTDI device has actually
|
||||
# sent the data, so try to read more than once if no data is
|
||||
# actually received
|
||||
data = self._ftdi.read_data_bytes(exlen, 4)
|
||||
if droptail:
|
||||
data[-1] = 0xff & (data[-1] << droptail)
|
||||
return data
|
||||
|
||||
def _flush(self) -> None:
|
||||
self._ftdi.write_data(self._immediate)
|
||||
self._ftdi.purge_buffers()
|
210
lib/python3.11/site-packages/pyftdi/term.py
Normal file
210
lib/python3.11/site-packages/pyftdi/term.py
Normal file
|
@ -0,0 +1,210 @@
|
|||
"""Terminal management helpers"""
|
||||
|
||||
# Copyright (c) 2020-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2020, Michael Pratt <mpratt51@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from os import environ, read as os_read
|
||||
from sys import platform, stderr, stdin, stdout
|
||||
|
||||
# pylint: disable=import-error
|
||||
if platform == 'win32':
|
||||
import msvcrt
|
||||
from subprocess import call # ugly workaround for an ugly OS
|
||||
else:
|
||||
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
|
||||
14: b'\x1b^H',
|
||||
# Ctrl + Alt + Enter
|
||||
28: b'\x1b\r',
|
||||
# Pause/Break
|
||||
29: b'\x1c',
|
||||
# Arrows
|
||||
72: b'\x1b[A',
|
||||
80: b'\x1b[B',
|
||||
77: b'\x1b[C',
|
||||
75: b'\x1b[D',
|
||||
# Arrows (Alt)
|
||||
152: b'\x1b[1;3A',
|
||||
160: b'\x1b[1;3B',
|
||||
157: b'\x1b[1;3C',
|
||||
155: b'\x1b[1;3D',
|
||||
# Arrows (Ctrl)
|
||||
141: b'\x1b[1;5A',
|
||||
145: b'\x1b[1;5B',
|
||||
116: b'\x1b[1;5C',
|
||||
115: b'\x1b[1;5D',
|
||||
# Ctrl + Tab
|
||||
148: b'\x1b[2J',
|
||||
# Cursor (Home, Ins, Del...)
|
||||
71: b'\x1b[1~',
|
||||
82: b'\x1b[2~',
|
||||
83: b'\x1b[3~',
|
||||
79: b'\x1b[4~',
|
||||
73: b'\x1b[5~',
|
||||
81: b'\x1b[6~',
|
||||
# Cursor + Alt
|
||||
151: b'\x1b[1;3~',
|
||||
162: b'\x1b[2;3~',
|
||||
163: b'\x1b[3;3~',
|
||||
159: b'\x1b[4;3~',
|
||||
153: b'\x1b[5;3~',
|
||||
161: b'\x1b[6;3~',
|
||||
# Cursor + Ctrl (xterm)
|
||||
119: b'\x1b[1;5H',
|
||||
146: b'\x1b[2;5~',
|
||||
147: b'\x1b[3;5~',
|
||||
117: b'\x1b[1;5F',
|
||||
114: b'\x1b[5;5~',
|
||||
118: b'\x1b[6;5~',
|
||||
# Function Keys (F1 - F12)
|
||||
59: b'\x1b[11~',
|
||||
60: b'\x1b[12~',
|
||||
61: b'\x1b[13~',
|
||||
62: b'\x1b[14~',
|
||||
63: b'\x1b[15~',
|
||||
64: b'\x1b[17~',
|
||||
65: b'\x1b[18~',
|
||||
66: b'\x1b[19~',
|
||||
67: b'\x1b[20~',
|
||||
68: b'\x1b[21~',
|
||||
133: b'\x1b[23~',
|
||||
134: b'\x1b[24~',
|
||||
# Function Keys + Shift (F11 - F22)
|
||||
84: b'\x1b[23;2~',
|
||||
85: b'\x1b[24;2~',
|
||||
86: b'\x1b[25~',
|
||||
87: b'\x1b[26~',
|
||||
88: b'\x1b[28~',
|
||||
89: b'\x1b[29~',
|
||||
90: b'\x1b[31~',
|
||||
91: b'\x1b[32~',
|
||||
92: b'\x1b[33~',
|
||||
93: b'\x1b[34~',
|
||||
135: b'\x1b[20;2~',
|
||||
136: b'\x1b[21;2~',
|
||||
# Function Keys + Ctrl (xterm)
|
||||
94: b'\x1bOP',
|
||||
95: b'\x1bOQ',
|
||||
96: b'\x1bOR',
|
||||
97: b'\x1bOS',
|
||||
98: b'\x1b[15;2~',
|
||||
99: b'\x1b[17;2~',
|
||||
100: b'\x1b[18;2~',
|
||||
101: b'\x1b[19;2~',
|
||||
102: b'\x1b[20;3~',
|
||||
103: b'\x1b[21;3~',
|
||||
137: b'\x1b[23;3~',
|
||||
138: b'\x1b[24;3~',
|
||||
# Function Keys + Alt (xterm)
|
||||
104: b'\x1b[11;5~',
|
||||
105: b'\x1b[12;5~',
|
||||
106: b'\x1b[13;5~',
|
||||
107: b'\x1b[14;5~',
|
||||
108: b'\x1b[15;5~',
|
||||
109: b'\x1b[17;5~',
|
||||
110: b'\x1b[18;5~',
|
||||
111: b'\x1b[19;5~',
|
||||
112: b'\x1b[20;5~',
|
||||
113: b'\x1b[21;5~',
|
||||
139: b'\x1b[23;5~',
|
||||
140: b'\x1b[24;5~',
|
||||
}
|
||||
"""
|
||||
Pause/Break, Ctrl+Alt+Del, Ctrl+Alt+arrows not mapable
|
||||
key: ordinal of char from msvcrt.getch()
|
||||
value: bytes string of ANSI escape sequence for linux/xterm
|
||||
numerical used over linux specifics for Home and End
|
||||
VT or CSI escape sequences used when linux has no sequence
|
||||
something unique for keys without an escape function
|
||||
0x1b == Escape key
|
||||
"""
|
||||
|
||||
IS_MSWIN = platform == 'win32'
|
||||
"""Whether we run on crap OS."""
|
||||
|
||||
def __init__(self):
|
||||
self._termstates = []
|
||||
|
||||
def init(self, fullterm: bool) -> None:
|
||||
"""Internal terminal initialization function"""
|
||||
if not self.IS_MSWIN:
|
||||
self._termstates = [(t.fileno(),
|
||||
tcgetattr(t.fileno()) if t.isatty() else None)
|
||||
for t in (stdin, stdout, stderr)]
|
||||
tfd, istty = self._termstates[0]
|
||||
if istty:
|
||||
new = tcgetattr(tfd)
|
||||
new[3] = new[3] & ~ICANON & ~ECHO
|
||||
new[6][VMIN] = 1
|
||||
new[6][VTIME] = 0
|
||||
if fullterm:
|
||||
new[6][VINTR] = 0
|
||||
new[6][VSUSP] = 0
|
||||
tcsetattr(tfd, TCSANOW, new)
|
||||
else:
|
||||
# Windows black magic
|
||||
# https://stackoverflow.com/questions/12492810
|
||||
call('', shell=True)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the terminal to its original state."""
|
||||
for tfd, att in self._termstates:
|
||||
# terminal modes have to be restored on exit...
|
||||
if att is not None:
|
||||
tcsetattr(tfd, TCSANOW, att)
|
||||
tcsetattr(tfd, TCSAFLUSH, att)
|
||||
|
||||
@staticmethod
|
||||
def is_term() -> bool:
|
||||
"""Tells whether the current stdout/stderr stream are connected to a
|
||||
terminal (vs. a regular file or pipe)"""
|
||||
return stdout.isatty()
|
||||
|
||||
@staticmethod
|
||||
def is_colorterm() -> bool:
|
||||
"""Tells whether the current terminal (if any) support colors escape
|
||||
sequences"""
|
||||
terms = ['xterm-color', 'ansi']
|
||||
return stdout.isatty() and environ.get('TERM') in terms
|
||||
|
||||
@classmethod
|
||||
def getkey(cls) -> bytes:
|
||||
"""Return a key from the current console, in a platform independent
|
||||
way.
|
||||
"""
|
||||
# there's probably a better way to initialize the module without
|
||||
# relying onto a singleton pattern. To be fixed
|
||||
if cls.IS_MSWIN:
|
||||
# w/ py2exe, it seems the importation fails to define the global
|
||||
# symbol 'msvcrt', to be fixed
|
||||
while True:
|
||||
char = msvcrt.getch()
|
||||
if char == b'\r':
|
||||
return b'\n'
|
||||
return char
|
||||
else:
|
||||
char = os_read(stdin.fileno(), 1)
|
||||
return char
|
||||
|
||||
@classmethod
|
||||
def getch_to_escape(cls, char: bytes) -> bytes:
|
||||
"""Get Windows escape sequence."""
|
||||
if cls.IS_MSWIN:
|
||||
return cls.FNKEYS.get(ord(char), char)
|
||||
return char
|
489
lib/python3.11/site-packages/pyftdi/tracer.py
Normal file
489
lib/python3.11/site-packages/pyftdi/tracer.py
Normal file
|
@ -0,0 +1,489 @@
|
|||
# Copyright (c) 2017-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""MPSSE command debug tracer."""
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class FtdiMpsseTracer:
|
||||
"""FTDI MPSSE protocol decoder."""
|
||||
|
||||
MPSSE_ENGINES = {
|
||||
0x0200: 0,
|
||||
0x0400: 0,
|
||||
0x0500: 0,
|
||||
0x0600: 0,
|
||||
0x0700: 2,
|
||||
0x0800: 2,
|
||||
0x0900: 1,
|
||||
0x1000: 0,
|
||||
0x3600: 2}
|
||||
"""Count of MPSSE engines."""
|
||||
|
||||
def __init__(self, version):
|
||||
count = self.MPSSE_ENGINES[version]
|
||||
self._engines = [None] * count
|
||||
|
||||
def send(self, iface: int, buf: Union[bytes, bytearray]) -> None:
|
||||
self._get_engine(iface).send(buf)
|
||||
|
||||
def receive(self, iface: int, buf: Union[bytes, bytearray]) -> None:
|
||||
self._get_engine(iface).receive(buf)
|
||||
|
||||
def _get_engine(self, iface: int):
|
||||
iface -= 1
|
||||
try:
|
||||
self._engines[iface]
|
||||
except IndexError as 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]
|
||||
|
||||
|
||||
class FtdiMpsseEngine:
|
||||
"""FTDI MPSSE virtual engine
|
||||
|
||||
Far from being complete for now
|
||||
"""
|
||||
|
||||
COMMAND_PREFIX = \
|
||||
'GET SET READ WRITE RW ENABLE DISABLE CLK LOOPBACK SEND DRIVE'
|
||||
|
||||
ST_IDLE = range(1)
|
||||
|
||||
def __init__(self, iface: int):
|
||||
self.log = getLogger('pyftdi.mpsse.tracer')
|
||||
self._if = iface
|
||||
self._trace_tx = bytearray()
|
||||
self._trace_rx = bytearray()
|
||||
self._state = self.ST_IDLE
|
||||
self._clkdiv5 = False
|
||||
self._cmd_decoded = True
|
||||
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]
|
||||
if self._cmd_decoded:
|
||||
self.log.debug('[%d]:[Command: %02X: %s]',
|
||||
self._if, code, cmd)
|
||||
cmd_decoder = getattr(self, f'_cmd_{cmd.lower()}')
|
||||
rdepth = len(self._expect_resp)
|
||||
try:
|
||||
self._cmd_decoded = cmd_decoder()
|
||||
except AttributeError as exc:
|
||||
raise ValueError(str(exc)) from exc
|
||||
if len(self._expect_resp) > rdepth:
|
||||
self._last_codes.append(code)
|
||||
if self._cmd_decoded:
|
||||
continue
|
||||
# not enough data in buffer to decode a whole command
|
||||
return
|
||||
except IndexError:
|
||||
self.log.warning('[%d]:Empty buffer on %02X: %s',
|
||||
self._if, code, cmd)
|
||||
except KeyError:
|
||||
self.log.warning('[%d]:Unknown command code: %02X',
|
||||
self._if, code)
|
||||
except AttributeError:
|
||||
self.log.warning('[%d]:Decoder for command %s [%02X] is not '
|
||||
'implemented', self._if, cmd, code)
|
||||
except ValueError as exc:
|
||||
self.log.warning('[%d]:Decoder for command %s [%02X] failed: '
|
||||
'%s', self._if, cmd, code, exc)
|
||||
# on error, flush all buffers
|
||||
self.log.warning('Flush TX/RX buffers')
|
||||
self._trace_tx = bytearray()
|
||||
self._trace_rx = bytearray()
|
||||
self._last_codes.clear()
|
||||
|
||||
def receive(self, buf: Union[bytes, bytearray]) -> None:
|
||||
self.log.info(' .. %s', hexlify(buf).decode())
|
||||
self._trace_rx.extend(buf)
|
||||
while self._trace_rx:
|
||||
code = None
|
||||
try:
|
||||
code = self._last_codes.popleft()
|
||||
cmd = self._commands[code]
|
||||
resp_decoder = getattr(self, f'_resp_{cmd.lower()}')
|
||||
self._resp_decoded = resp_decoder()
|
||||
if self._resp_decoded:
|
||||
continue
|
||||
# not enough data in buffer to decode a whole response
|
||||
return
|
||||
except IndexError:
|
||||
self.log.warning('[%d]:Empty buffer', self._if)
|
||||
except KeyError:
|
||||
self.log.warning('[%d]:Unknown command code: %02X',
|
||||
self._if, code)
|
||||
except AttributeError:
|
||||
self.log.warning('[%d]:Decoder for response %s [%02X] is not '
|
||||
'implemented', self._if, cmd, code)
|
||||
# on error, flush RX buffer
|
||||
self.log.warning('[%d]:Flush RX buffer', self._if)
|
||||
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
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_disable_clk_div5(self):
|
||||
self.log.info(' [%d]:Disable clock divisor /5', self._if)
|
||||
self._clkdiv5 = False
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_set_tck_divisor(self):
|
||||
if len(self._trace_tx) < 3:
|
||||
return False
|
||||
value, = sunpack('<H', self._trace_tx[1:3])
|
||||
base = 12E6 if self._clkdiv5 else 60E6
|
||||
freq = base / ((1 + value) * 2)
|
||||
self.log.info(' [%d]:Set frequency %.3fMHZ', self._if, freq/1E6)
|
||||
self._trace_tx[:] = self._trace_tx[3:]
|
||||
return True
|
||||
|
||||
def _cmd_loopback_end(self):
|
||||
self.log.info(' [%d]:Disable loopback', self._if)
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_enable_clk_adaptive(self):
|
||||
self.log.info(' [%d]:Enable adaptive clock', self._if)
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_disable_clk_adaptive(self):
|
||||
self.log.info(' [%d]:Disable adaptive clock', self._if)
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_enable_clk_3phase(self):
|
||||
self.log.info(' [%d]:Enable 3-phase clock', self._if)
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_disable_clk_3phase(self):
|
||||
self.log.info(' [%d]:Disable 3-phase clock', self._if)
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_drive_zero(self):
|
||||
if len(self._trace_tx) < 3:
|
||||
return False
|
||||
value, = sunpack('H', self._trace_tx[1:3])
|
||||
self.log.info(' [%d]:Open collector [15:0] %04x %s',
|
||||
self._if, value, self.bitfmt(value, 16))
|
||||
self._trace_tx[:] = self._trace_tx[3:]
|
||||
return True
|
||||
|
||||
def _cmd_send_immediate(self):
|
||||
self.log.debug(' [%d]:Send immediate', self._if)
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
return True
|
||||
|
||||
def _cmd_get_bits_low(self):
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
self._expect_resp.append(1)
|
||||
return True
|
||||
|
||||
def _cmd_get_bits_high(self):
|
||||
self._trace_tx[:] = self._trace_tx[1:]
|
||||
self._expect_resp.append(1)
|
||||
return True
|
||||
|
||||
def _cmd_set_bits_low(self):
|
||||
if len(self._trace_tx) < 3:
|
||||
return False
|
||||
value, direction = sunpack('BB', self._trace_tx[1:3])
|
||||
self.log.info(' [%d]:Set gpio[7:0] %02x %s',
|
||||
self._if, value, self.bm2str(value, direction))
|
||||
self._trace_tx[:] = self._trace_tx[3:]
|
||||
return True
|
||||
|
||||
def _cmd_set_bits_high(self):
|
||||
if len(self._trace_tx) < 3:
|
||||
return False
|
||||
value, direction = sunpack('BB', self._trace_tx[1:3])
|
||||
self.log.info(' [%d]:Set gpio[15:8] %02x %s',
|
||||
self._if, value, self.bm2str(value, direction))
|
||||
self._trace_tx[:] = self._trace_tx[3:]
|
||||
return True
|
||||
|
||||
def _cmd_write_bytes_pve_msb(self):
|
||||
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_write_bytes_nve_msb(self):
|
||||
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_write_bytes_pve_lsb(self):
|
||||
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_write_bytes_nve_lsb(self):
|
||||
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bytes_pve_msb(self):
|
||||
return self._decode_input_mpsse_byte_request()
|
||||
|
||||
def _resp_read_bytes_pve_msb(self):
|
||||
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bytes_nve_msb(self):
|
||||
return self._decode_input_mpsse_byte_request()
|
||||
|
||||
def _resp_read_bytes_nve_msb(self):
|
||||
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bytes_pve_lsb(self):
|
||||
return self._decode_input_mpsse_byte_request()
|
||||
|
||||
def _resp_read_bytes_pve_lsb(self):
|
||||
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bytes_nve_lsb(self):
|
||||
return self._decode_input_mpsse_byte_request()
|
||||
|
||||
def _resp_read_bytes_nve_lsb(self):
|
||||
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_rw_bytes_nve_pve_msb(self):
|
||||
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name,
|
||||
True)
|
||||
|
||||
def _resp_rw_bytes_nve_pve_msb(self):
|
||||
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_rw_bytes_pve_nve_msb(self):
|
||||
return self._decode_output_mpsse_bytes(currentframe().f_code.co_name,
|
||||
True)
|
||||
|
||||
def _resp_rw_bytes_pve_nve_msb(self):
|
||||
return self._decode_input_mpsse_bytes(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_write_bits_pve_msb(self):
|
||||
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_write_bits_nve_msb(self):
|
||||
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_write_bits_pve_lsb(self):
|
||||
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_write_bits_nve_lsb(self):
|
||||
return self._decode_output_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bits_pve_msb(self):
|
||||
return self._decode_input_mpsse_bit_request()
|
||||
|
||||
def _resp_read_bits_pve_msb(self):
|
||||
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bits_nve_msb(self):
|
||||
return self._decode_input_mpsse_bit_request()
|
||||
|
||||
def _resp_read_bits_nve_msb(self):
|
||||
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bits_pve_lsb(self):
|
||||
return self._decode_input_mpsse_bit_request()
|
||||
|
||||
def _resp_read_bits_pve_lsb(self):
|
||||
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_read_bits_nve_lsb(self):
|
||||
return self._decode_input_mpsse_bit_request()
|
||||
|
||||
def _resp_read_bits_nve_lsb(self):
|
||||
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_rw_bits_nve_pve_msb(self):
|
||||
return self._decode_output_mpsse_bits(currentframe().f_code.co_name,
|
||||
True)
|
||||
|
||||
def _resp_rw_bits_nve_pve_msb(self):
|
||||
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _cmd_rw_bits_pve_nve_msb(self):
|
||||
return self._decode_output_mpsse_bits(currentframe().f_code.co_name,
|
||||
True)
|
||||
|
||||
def _resp_rw_bits_pve_nve_msb(self):
|
||||
return self._decode_input_mpsse_bits(currentframe().f_code.co_name)
|
||||
|
||||
def _resp_get_bits_low(self):
|
||||
if self._trace_rx:
|
||||
return False
|
||||
value = self._trace_rx[0]
|
||||
self.log.info(' [%d]:Get gpio[7:0] %02x %s',
|
||||
self._if, value, self.bm2str(value, 0xFF))
|
||||
self._trace_rx[:] = self._trace_rx[1:]
|
||||
return True
|
||||
|
||||
def _resp_get_bits_high(self):
|
||||
if self._trace_rx:
|
||||
return False
|
||||
value = self._trace_rx[0]
|
||||
self.log.info(' [%d]:Get gpio[15:8] %02x %s',
|
||||
self._if, value, self.bm2str(value, 0xFF))
|
||||
self._trace_rx[:] = self._trace_rx[1:]
|
||||
return True
|
||||
|
||||
def _decode_output_mpsse_bytes(self, caller, expect_rx=False):
|
||||
if len(self._trace_tx) < 4:
|
||||
return False
|
||||
length = sunpack('<H', self._trace_tx[1:3])[0] + 1
|
||||
if len(self._trace_tx) < 4 + length:
|
||||
return False
|
||||
if expect_rx:
|
||||
self._expect_resp.append(length)
|
||||
payload = self._trace_tx[3:3+length]
|
||||
funcname = caller[5:].title().replace('_', '')
|
||||
self.log.info(' [%d]:%s> (%d) %s',
|
||||
self._if, funcname, length,
|
||||
hexlify(payload).decode('utf8'))
|
||||
self._trace_tx[:] = self._trace_tx[3+length:]
|
||||
return True
|
||||
|
||||
def _decode_output_mpsse_bits(self, caller, expect_rx=False):
|
||||
if len(self._trace_tx) < 3:
|
||||
return False
|
||||
bitlen = self._trace_tx[1] + 1
|
||||
if expect_rx:
|
||||
self._expect_resp.append(-bitlen)
|
||||
payload = self._trace_tx[2]
|
||||
funcname = caller[5:].title().replace('_', '')
|
||||
msb = caller[5:][-3].lower() == 'm'
|
||||
self.log.info(' %s> (%d) %s',
|
||||
funcname, bitlen, self.bit2str(payload, bitlen, msb))
|
||||
self._trace_tx[:] = self._trace_tx[3:]
|
||||
return True
|
||||
|
||||
def _decode_input_mpsse_byte_request(self):
|
||||
if len(self._trace_tx) < 3:
|
||||
return False
|
||||
length = sunpack('<H', self._trace_tx[1:3])[0] + 1
|
||||
self._expect_resp.append(length)
|
||||
self._trace_tx[:] = self._trace_tx[3:]
|
||||
return True
|
||||
|
||||
def _decode_input_mpsse_bit_request(self):
|
||||
if len(self._trace_tx) < 2:
|
||||
return False
|
||||
bitlen = self._trace_tx[1] + 1
|
||||
self._expect_resp.append(-bitlen)
|
||||
self._trace_tx[:] = self._trace_tx[2:]
|
||||
return True
|
||||
|
||||
def _decode_input_mpsse_bytes(self, caller):
|
||||
if not self._expect_resp:
|
||||
self.log.warning('[%d]:Response w/o request?', self._if)
|
||||
return False
|
||||
if self._expect_resp[0] < 0:
|
||||
self.log.warning('[%d]:Handling byte request w/ bit length',
|
||||
self._if)
|
||||
return False
|
||||
if len(self._trace_rx) < self._expect_resp[0]: # peek
|
||||
return False
|
||||
length = self._expect_resp.popleft()
|
||||
payload = self._trace_rx[:length]
|
||||
self._trace_rx[:] = self._trace_rx[length:]
|
||||
funcname = caller[5:].title().replace('_', '')
|
||||
self.log.info(' %s< (%d) %s',
|
||||
funcname, length, hexlify(payload).decode('utf8'))
|
||||
return True
|
||||
|
||||
def _decode_input_mpsse_bits(self, caller):
|
||||
if not self._expect_resp:
|
||||
self.log.warning('[%d]:Response w/o request?', self._if)
|
||||
return False
|
||||
if not self._trace_rx: # peek
|
||||
return False
|
||||
if self._expect_resp[0] > 0:
|
||||
self.log.warning('[%d]:Handling bit request w/ byte length',
|
||||
self._if)
|
||||
bitlen = -self._expect_resp.popleft()
|
||||
payload = self._trace_rx[0]
|
||||
self._trace_rx[:] = self._trace_rx[1:]
|
||||
funcname = caller[5:].title().replace('_', '')
|
||||
msb = caller[5:][-3].lower() == 'm'
|
||||
self.log.info(' %s< (%d) %s',
|
||||
funcname, bitlen, self.bit2str(payload, bitlen, msb))
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def bit2str(cls, value: int, count: int, msb: bool, hiz: str = '_') -> str:
|
||||
mask = (1 << count) - 1
|
||||
if msb:
|
||||
mask <<= 8 - count
|
||||
return cls.bm2str(value, mask, hiz)
|
||||
|
||||
@classmethod
|
||||
def bm2str(cls, value: int, mask: int, hiz: str = '_') -> str:
|
||||
vstr = cls.bitfmt(value, 8)
|
||||
mstr = cls.bitfmt(mask, 8)
|
||||
return ''.join([m == '1' and v or hiz for v, m in zip(vstr, mstr)])
|
||||
|
||||
@classmethod
|
||||
def bitfmt(cls, value, width):
|
||||
return format(value, f'0{width}b')
|
||||
|
||||
# rw_bytes_pve_pve_lsb
|
||||
# rw_bytes_pve_nve_lsb
|
||||
# rw_bytes_nve_pve_lsb
|
||||
# rw_bytes_nve_nve_lsb
|
||||
# rw_bits_pve_pve_lsb
|
||||
# rw_bits_pve_nve_lsb
|
||||
# rw_bits_nve_pve_lsb
|
||||
# rw_bits_nve_nve_lsb
|
||||
# write_bits_tms_pve
|
||||
# write_bits_tms_nve
|
||||
# rw_bits_tms_pve_pve
|
||||
# rw_bits_tms_nve_pve
|
||||
# rw_bits_tms_pve_nve
|
||||
# rw_bits_tms_nve_nve
|
649
lib/python3.11/site-packages/pyftdi/usbtools.py
Normal file
649
lib/python3.11/site-packages/pyftdi/usbtools.py
Normal file
|
@ -0,0 +1,649 @@
|
|||
# Copyright (c) 2014-2024, Emmanuel Blot <emmanuel.blot@free.fr>
|
||||
# Copyright (c) 2016, Emmanuel Bouaziz <ebouaziz@free.fr>
|
||||
# All rights reserved.
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
"""USB Helpers"""
|
||||
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from string import printable as printablechars
|
||||
from threading import RLock
|
||||
from typing import (Any, Dict, List, NamedTuple, Optional, Sequence, Set,
|
||||
TextIO, Type, Tuple, Union)
|
||||
from urllib.parse import SplitResult, urlsplit, urlunsplit
|
||||
from usb.backend import IBackend
|
||||
from usb.core import Device as UsbDevice, USBError
|
||||
from usb.util import dispose_resources, get_string as usb_get_string
|
||||
from .misc import to_int
|
||||
|
||||
# pylint: disable=broad-except
|
||||
|
||||
UsbDeviceDescriptor = NamedTuple('UsbDeviceDescriptor',
|
||||
(('vid', int),
|
||||
('pid', int),
|
||||
('bus', Optional[int]),
|
||||
('address', Optional[int]),
|
||||
('sn', Optional[str]),
|
||||
('index', Optional[int]),
|
||||
('description', Optional[str])))
|
||||
"""USB Device descriptor are used to report known information about a FTDI
|
||||
compatible device, and as a device selection filter
|
||||
|
||||
* vid: vendor identifier, 16-bit integer
|
||||
* pid: product identifier, 16-bit integer
|
||||
* bus: USB bus identifier, host dependent integer
|
||||
* address: USB address identifier on a USB bus, host dependent integer
|
||||
* sn: serial number, string
|
||||
* index: integer, can be used to descriminate similar devices
|
||||
* description: device description, as a string
|
||||
|
||||
To select a device, use None for unknown fields
|
||||
|
||||
.. note::
|
||||
|
||||
* Always prefer serial number to other identification methods if available
|
||||
* Prefer bus/address selector over index
|
||||
"""
|
||||
|
||||
UsbDeviceKey = Union[Tuple[int, int, int, int], Tuple[int, int]]
|
||||
"""USB device indentifier on the system.
|
||||
|
||||
This is used as USB device identifiers on the host. On proper hosts,
|
||||
this is a (bus, address, vid, pid) 4-uple. On stupid hosts (such as M$Win),
|
||||
it may be degraded to (vid, pid) 2-uple.
|
||||
"""
|
||||
|
||||
|
||||
class UsbToolsError(Exception):
|
||||
"""UsbTools error."""
|
||||
|
||||
|
||||
class UsbTools:
|
||||
"""Helpers to obtain information about connected USB devices."""
|
||||
|
||||
# Supported back ends, in preference order
|
||||
BACKENDS = ('usb.backend.libusb1', 'usb.backend.libusb0')
|
||||
|
||||
# Need to maintain a list of reference USB devices, to circumvent a
|
||||
# limitation in pyusb that prevents from opening several times the same
|
||||
# USB device. The following dictionary used bus/address/vendor/product keys
|
||||
# to track (device, refcount) pairs
|
||||
Lock = RLock()
|
||||
Devices = {} # (bus, address, vid, pid): (usb.core.Device, refcount)
|
||||
UsbDevices = {} # (vid, pid): {usb.core.Device}
|
||||
UsbApi = None
|
||||
|
||||
@classmethod
|
||||
def find_all(cls, vps: Sequence[Tuple[int, int]],
|
||||
nocache: bool = False) -> \
|
||||
List[Tuple[UsbDeviceDescriptor, int]]:
|
||||
"""Find all devices that match the specified vendor/product pairs.
|
||||
|
||||
:param vps: a sequence of 2-tuple (vid, pid) pairs
|
||||
:param bool nocache: bypass cache to re-enumerate USB devices on
|
||||
the host
|
||||
:return: a list of 2-tuple (UsbDeviceDescriptor, interface count)
|
||||
"""
|
||||
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)
|
||||
# TODO: handle / is serial number strings
|
||||
sernum = UsbTools.get_string(dev, dev.iSerialNumber)
|
||||
description = UsbTools.get_string(dev, dev.iProduct)
|
||||
descriptor = UsbDeviceDescriptor(dev.idVendor, dev.idProduct,
|
||||
dev.bus, dev.address,
|
||||
sernum, None, description)
|
||||
devices.add((descriptor, ifcount))
|
||||
return list(devices)
|
||||
|
||||
@classmethod
|
||||
def flush_cache(cls, ):
|
||||
"""Flush the FTDI device cache.
|
||||
|
||||
It is highly recommanded to call this method a FTDI device is
|
||||
unplugged/plugged back since the last enumeration, as the device
|
||||
may appear on a different USB location each time it is plugged
|
||||
in.
|
||||
|
||||
Failing to clear out the cache may lead to USB Error 19:
|
||||
``Device may have been disconnected``.
|
||||
"""
|
||||
with cls.Lock:
|
||||
cls.UsbDevices.clear()
|
||||
|
||||
@classmethod
|
||||
def get_device(cls, devdesc: UsbDeviceDescriptor) -> UsbDevice:
|
||||
"""Find a previously open device with the same vendor/product
|
||||
or initialize a new one, and return it.
|
||||
|
||||
If several FTDI devices of the same kind (vid, pid) are connected
|
||||
to the host, either index or serial argument should be used to
|
||||
discriminate the FTDI device.
|
||||
|
||||
index argument is not a reliable solution as the host may enumerate
|
||||
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,
|
||||
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
|
||||
"""
|
||||
with cls.Lock:
|
||||
if devdesc.index or devdesc.sn or devdesc.description:
|
||||
dev = None
|
||||
if not devdesc.vid:
|
||||
raise ValueError('Vendor identifier is required')
|
||||
devs = cls._find_devices(devdesc.vid, devdesc.pid)
|
||||
if devdesc.description:
|
||||
devs = [dev for dev in devs if
|
||||
UsbTools.get_string(dev, dev.iProduct) ==
|
||||
devdesc.description]
|
||||
if devdesc.sn:
|
||||
devs = [dev for dev in devs if
|
||||
UsbTools.get_string(dev, dev.iSerialNumber) ==
|
||||
devdesc.sn]
|
||||
if devdesc.bus is not None and devdesc.address is not None:
|
||||
devs = [dev for dev in devs if
|
||||
(devdesc.bus == dev.bus and
|
||||
devdesc.address == dev.address)]
|
||||
if isinstance(devs, set):
|
||||
# there is no guarantee the same index with lead to the
|
||||
# same device. Indexing should be reworked
|
||||
devs = list(devs)
|
||||
try:
|
||||
dev = devs[devdesc.index or 0]
|
||||
except IndexError as exc:
|
||||
raise IOError("No such device") from exc
|
||||
else:
|
||||
devs = cls._find_devices(devdesc.vid, devdesc.pid)
|
||||
dev = list(devs)[0] if devs else None
|
||||
if not dev:
|
||||
raise IOError('Device not found')
|
||||
try:
|
||||
devkey = (dev.bus, dev.address, devdesc.vid, devdesc.pid)
|
||||
if None in devkey[0:2]:
|
||||
raise AttributeError('USB backend does not support bus '
|
||||
'enumeration')
|
||||
except AttributeError:
|
||||
devkey = (devdesc.vid, devdesc.pid)
|
||||
if devkey not in cls.Devices:
|
||||
# only change the active configuration if the active one is
|
||||
# not the first. This allows other libusb sessions running
|
||||
# with the same device to run seamlessly.
|
||||
try:
|
||||
config = dev.get_active_configuration()
|
||||
setconf = config.bConfigurationValue != 1
|
||||
except USBError:
|
||||
setconf = True
|
||||
if setconf:
|
||||
try:
|
||||
dev.set_configuration()
|
||||
except USBError:
|
||||
pass
|
||||
cls.Devices[devkey] = [dev, 1]
|
||||
else:
|
||||
cls.Devices[devkey][1] += 1
|
||||
return cls.Devices[devkey][0]
|
||||
|
||||
@classmethod
|
||||
def release_device(cls, usb_dev: UsbDevice):
|
||||
"""Release a previously open device, if it not used anymore.
|
||||
|
||||
:param usb_dev: a previously instanciated USB device instance
|
||||
"""
|
||||
# Lookup for ourselves in the class dictionary
|
||||
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:
|
||||
# another interface is open, decrement
|
||||
cls.Devices[devkey][1] -= 1
|
||||
else:
|
||||
# last interface in use, release
|
||||
dispose_resources(cls.Devices[devkey][0])
|
||||
del cls.Devices[devkey]
|
||||
break
|
||||
|
||||
@classmethod
|
||||
def release_all_devices(cls, devclass: Optional[Type] = None) -> int:
|
||||
"""Release all open devices.
|
||||
|
||||
:param devclass: optional class to only release devices of one type
|
||||
:return: the count of device that have been released.
|
||||
"""
|
||||
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])
|
||||
if dev is None or not isinstance(dev, devclass):
|
||||
continue
|
||||
dispose_resources(cls.Devices[devkey][0])
|
||||
remove_devs.add(devkey)
|
||||
for devkey in remove_devs:
|
||||
del cls.Devices[devkey]
|
||||
return len(remove_devs)
|
||||
|
||||
@classmethod
|
||||
def list_devices(cls, urlstr: str,
|
||||
vdict: Dict[str, int],
|
||||
pdict: Dict[int, Dict[str, int]],
|
||||
default_vendor: int) -> \
|
||||
List[Tuple[UsbDeviceDescriptor, int]]:
|
||||
"""List candidates that match the device URL pattern.
|
||||
|
||||
:see: :py:meth:`show_devices` to generate the URLs from the
|
||||
candidates list
|
||||
|
||||
:param url: the URL to parse
|
||||
:param vdict: vendor name map of USB vendor ids
|
||||
:param pdict: vendor id map of product name map of product ids
|
||||
:param default_vendor: default vendor id
|
||||
:return: list of (UsbDeviceDescriptor, interface)
|
||||
"""
|
||||
urlparts = urlsplit(urlstr)
|
||||
if not urlparts.path:
|
||||
raise UsbToolsError('URL string is missing device port')
|
||||
candidates, _ = cls.enumerate_candidates(urlparts, vdict, pdict,
|
||||
default_vendor)
|
||||
return candidates
|
||||
|
||||
@classmethod
|
||||
def parse_url(cls, urlstr: str, scheme: str,
|
||||
vdict: Dict[str, int],
|
||||
pdict: Dict[int, Dict[str, int]],
|
||||
default_vendor: int) -> Tuple[UsbDeviceDescriptor, int]:
|
||||
"""Parse a device specifier URL.
|
||||
|
||||
:param url: the URL to parse
|
||||
:param scheme: scheme to match in the URL string (scheme://...)
|
||||
:param vdict: vendor name map of USB vendor ids
|
||||
:param pdict: vendor id map of product name map of product ids
|
||||
:param default_vendor: default vendor id
|
||||
:return: UsbDeviceDescriptor, interface
|
||||
|
||||
..note:
|
||||
|
||||
URL syntax:
|
||||
|
||||
protocol://vendor:product[:serial|:index|:bus:addr]/interface
|
||||
"""
|
||||
urlparts = urlsplit(urlstr)
|
||||
if scheme != urlparts.scheme:
|
||||
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(f'Invalid device URL: {urlstr}') from exc
|
||||
candidates, idx = cls.enumerate_candidates(urlparts, vdict, pdict,
|
||||
default_vendor)
|
||||
if report_devices:
|
||||
UsbTools.show_devices(scheme, vdict, pdict, candidates)
|
||||
raise SystemExit(candidates and
|
||||
'Please specify the USB device' or
|
||||
'No USB-Serial device has been detected')
|
||||
if idx is None:
|
||||
if len(candidates) > 1:
|
||||
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(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:
|
||||
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():
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def enumerate_candidates(cls, urlparts: SplitResult,
|
||||
vdict: Dict[str, int],
|
||||
pdict: Dict[int, Dict[str, int]],
|
||||
default_vendor: int) -> \
|
||||
Tuple[List[Tuple[UsbDeviceDescriptor, int]], Optional[int]]:
|
||||
"""Enumerate USB device URLs that match partial URL and VID/PID
|
||||
criteria.
|
||||
|
||||
:param urlpart: splitted device specifier URL
|
||||
:param vdict: vendor name map of USB vendor ids
|
||||
:param pdict: vendor id map of product name map of product ids
|
||||
:param default_vendor: default vendor id
|
||||
:return: list of (usbdev, iface), parsed index if any
|
||||
"""
|
||||
specifiers = urlparts.netloc.split(':')
|
||||
plcomps = specifiers + [''] * 2
|
||||
try:
|
||||
plcomps[0] = vdict.get(plcomps[0], plcomps[0])
|
||||
if plcomps[0]:
|
||||
vendor = to_int(plcomps[0])
|
||||
else:
|
||||
vendor = None
|
||||
product_ids = pdict.get(vendor, None)
|
||||
if not product_ids:
|
||||
product_ids = pdict[default_vendor]
|
||||
plcomps[1] = product_ids.get(plcomps[1], plcomps[1])
|
||||
if plcomps[1]:
|
||||
try:
|
||||
product = to_int(plcomps[1])
|
||||
except ValueError as exc:
|
||||
raise UsbToolsError(f'Product {plcomps[1]} is not '
|
||||
f'referenced') from exc
|
||||
else:
|
||||
product = None
|
||||
except (IndexError, ValueError) as exc:
|
||||
raise UsbToolsError(f'Invalid device URL: '
|
||||
f'{urlunsplit(urlparts)}') from exc
|
||||
sernum = None
|
||||
idx = None
|
||||
bus = None
|
||||
address = None
|
||||
locators = specifiers[2:]
|
||||
if len(locators) > 1:
|
||||
try:
|
||||
bus = int(locators[0], 16)
|
||||
address = int(locators[1], 16)
|
||||
except ValueError as exc:
|
||||
raise UsbToolsError(f'Invalid bus/address: '
|
||||
f'{":".join(locators)}') from exc
|
||||
else:
|
||||
if locators and locators[0]:
|
||||
try:
|
||||
devidx = to_int(locators[0])
|
||||
if devidx > 255:
|
||||
raise ValueError()
|
||||
idx = devidx
|
||||
if idx:
|
||||
idx = devidx-1
|
||||
except ValueError:
|
||||
sernum = locators[0]
|
||||
candidates = []
|
||||
vendors = [vendor] if vendor else set(vdict.values())
|
||||
vps = set()
|
||||
for vid in vendors:
|
||||
products = pdict.get(vid, [])
|
||||
for pid in products:
|
||||
vps.add((vid, products[pid]))
|
||||
devices = cls.find_all(vps)
|
||||
if sernum:
|
||||
if sernum not in [dev.sn for dev, _ in devices]:
|
||||
raise UsbToolsError(f'No USB device with S/N {sernum}')
|
||||
for desc, ifcount in devices:
|
||||
if vendor and vendor != desc.vid:
|
||||
continue
|
||||
if product and product != desc.pid:
|
||||
continue
|
||||
if sernum and sernum != desc.sn:
|
||||
continue
|
||||
if bus is not None:
|
||||
if bus != desc.bus or address != desc.address:
|
||||
continue
|
||||
candidates.append((desc, ifcount))
|
||||
return candidates, idx
|
||||
|
||||
@classmethod
|
||||
def show_devices(cls, scheme: str,
|
||||
vdict: Dict[str, int],
|
||||
pdict: Dict[int, Dict[str, int]],
|
||||
devdescs: Sequence[Tuple[UsbDeviceDescriptor, int]],
|
||||
out: Optional[TextIO] = None):
|
||||
"""Show supported devices. When the joker url ``scheme://*/?`` is
|
||||
specified as an URL, it generates a list of connected USB devices
|
||||
that match the supported USB devices. It can be used to provide the
|
||||
end-user with a list of valid URL schemes.
|
||||
|
||||
:param scheme: scheme to match in the URL string (scheme://...)
|
||||
:param vdict: vendor name map of USB vendor ids
|
||||
:param pdict: vendor id map of product name map of product ids
|
||||
:param devdescs: candidate devices
|
||||
:param out: output stream, none for stdout
|
||||
"""
|
||||
if not devdescs:
|
||||
return
|
||||
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 url, desc in devstrs:
|
||||
print(f' {url:{max_url_len}s} {desc}', file=out)
|
||||
print('', file=out)
|
||||
|
||||
@classmethod
|
||||
def build_dev_strings(cls, scheme: str,
|
||||
vdict: Dict[str, int],
|
||||
pdict: Dict[int, Dict[str, int]],
|
||||
devdescs: Sequence[Tuple[UsbDeviceDescriptor,
|
||||
int]]) -> \
|
||||
List[Tuple[str, str]]:
|
||||
"""Build URL and device descriptors from UsbDeviceDescriptors.
|
||||
|
||||
:param scheme: protocol part of the URLs to generate
|
||||
:param vdict: vendor name map of USB vendor ids
|
||||
:param pdict: vendor id map of product name map of product ids
|
||||
:param devdescs: USB devices and interfaces
|
||||
:return: list of (url, descriptors)
|
||||
"""
|
||||
indices = {} # Dict[Tuple[int, int], int]
|
||||
descs = []
|
||||
for desc, ifcount in sorted(devdescs):
|
||||
ikey = (desc.vid, desc.pid)
|
||||
indices[ikey] = indices.get(ikey, 0) + 1
|
||||
# try to find a matching string for the current vendor
|
||||
vendors = []
|
||||
# fallback if no matching string for the current vendor is found
|
||||
vendor = f'{desc.vid:04x}'
|
||||
for vidc in vdict:
|
||||
if vdict[vidc] == desc.vid:
|
||||
vendors.append(vidc)
|
||||
if vendors:
|
||||
vendors.sort(key=len)
|
||||
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 = f'{desc.pid:04x}'
|
||||
try:
|
||||
products = []
|
||||
productids = pdict[desc.vid]
|
||||
for prdc in productids:
|
||||
if productids[prdc] == desc.pid:
|
||||
products.append(prdc)
|
||||
if products:
|
||||
product = products[0]
|
||||
except KeyError:
|
||||
pass
|
||||
for port in range(1, ifcount+1):
|
||||
fmt = '%s://%s/%d'
|
||||
parts = [vendor, product]
|
||||
sernum = desc.sn
|
||||
if not sernum:
|
||||
sernum = ''
|
||||
if [c for c in sernum if c not in printablechars or c == '?']:
|
||||
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(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:
|
||||
url = fmt % (scheme, ':'.join(parts), port)
|
||||
except Exception:
|
||||
url = fmt % (scheme, ':'.join([vendor, product, '???']),
|
||||
port)
|
||||
try:
|
||||
if desc.description:
|
||||
description = f'({desc.description})'
|
||||
else:
|
||||
description = ''
|
||||
except Exception:
|
||||
description = ''
|
||||
descs.append((url, description))
|
||||
return descs
|
||||
|
||||
@classmethod
|
||||
def get_string(cls, device: UsbDevice, stridx: int) -> str:
|
||||
"""Retrieve a string from the USB device, dealing with PyUSB API breaks
|
||||
|
||||
:param device: USB device instance
|
||||
:param stridx: the string identifier
|
||||
:return: the string read from the USB device
|
||||
"""
|
||||
if cls.UsbApi is None:
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import inspect
|
||||
args, _, _, _ = \
|
||||
inspect.signature(UsbDevice.read).parameters
|
||||
if (len(args) >= 3) and args[1] == 'length':
|
||||
cls.UsbApi = 1
|
||||
else:
|
||||
cls.UsbApi = 2
|
||||
try:
|
||||
if cls.UsbApi == 2:
|
||||
return usb_get_string(device, stridx)
|
||||
return usb_get_string(device, 64, stridx)
|
||||
except UnicodeDecodeError:
|
||||
# do not abort if EEPROM data is somewhat incoherent
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def find_backend(cls) -> IBackend:
|
||||
"""Try to find and load an PyUSB backend.
|
||||
|
||||
..note:: There is no need to call this method for regular usage.
|
||||
|
||||
:return: PyUSB backend
|
||||
"""
|
||||
with cls.Lock:
|
||||
return cls._load_backend()
|
||||
|
||||
@classmethod
|
||||
def _find_devices(cls, vendor: int, product: int,
|
||||
nocache: bool = False) -> Set[UsbDevice]:
|
||||
"""Find a USB device and return it.
|
||||
|
||||
This code re-implements the usb.core.find() method using a local
|
||||
cache to avoid calling several times the underlying LibUSB and the
|
||||
system USB calls to enumerate the available USB devices. As these
|
||||
calls are time-hungry (about 1 second/call), the enumerated devices
|
||||
are cached. It consumes a bit more memory but dramatically improves
|
||||
start-up time.
|
||||
Hopefully, this kludge is temporary and replaced with a better
|
||||
implementation from PyUSB at some point.
|
||||
|
||||
:param vendor: USB vendor id
|
||||
:param product: USB product id
|
||||
:param bool nocache: bypass cache to re-enumerate USB devices on
|
||||
the host
|
||||
:return: a set of USB device matching the vendor/product identifier
|
||||
pair
|
||||
"""
|
||||
backend = cls._load_backend()
|
||||
vidpid = (vendor, product)
|
||||
if nocache or (vidpid not in cls.UsbDevices):
|
||||
# not freed until Python runtime completion
|
||||
# enumerate_devices returns a generator, so back up the
|
||||
# generated device into a list. To save memory, we only
|
||||
# back up the supported devices
|
||||
devs = set()
|
||||
vpdict = {} # Dict[int, List[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]
|
||||
if products and (device.idProduct not in products):
|
||||
continue
|
||||
devs.add(device)
|
||||
if sys.platform == 'win32':
|
||||
# ugly kludge for a boring OS:
|
||||
# on Windows, the USB stack may enumerate the very same
|
||||
# devices several times: a real device with N interface
|
||||
# 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 = {}
|
||||
for dev in devs:
|
||||
vid = dev.idVendor
|
||||
pid = dev.idProduct
|
||||
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)
|
||||
if fifc < ifc:
|
||||
filtered_devs[k] = dev
|
||||
devs = set(filtered_devs.values())
|
||||
cls.UsbDevices[vidpid] = devs
|
||||
return cls.UsbDevices[vidpid]
|
||||
|
||||
@classmethod
|
||||
def _get_backend_device(cls, device: UsbDevice) -> Any:
|
||||
"""Return the backend implementation of a device.
|
||||
|
||||
:param device: the UsbDevice (usb.core.Device)
|
||||
:return: the implementation of any
|
||||
"""
|
||||
try:
|
||||
# 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=protected-access
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _load_backend(cls) -> IBackend:
|
||||
backend = None # Optional[IBackend]
|
||||
for candidate in cls.BACKENDS:
|
||||
mod = import_module(candidate)
|
||||
backend = mod.get_backend()
|
||||
if backend is not None:
|
||||
return backend
|
||||
raise ValueError('No backend available')
|
Loading…
Add table
Add a link
Reference in a new issue