"""
pylibftdi.driver - interface to the libftdi library
Copyright (c) 2010-2014 Ben Bass <benbass@codedstructure.net>
See LICENSE file for details and (absence of) warranty
pylibftdi: https://github.com/codedstructure/pylibftdi
"""
from __future__ import annotations
import itertools
from collections import namedtuple
# be disciplined so pyflakes can check us...
from ctypes import (
POINTER,
Structure,
byref,
c_char_p,
c_int,
c_uint16,
c_void_p,
cast,
cdll,
create_string_buffer,
)
from ctypes.util import find_library
from typing import Any
from pylibftdi._base import FtdiError, LibraryMissingError
[docs]
class libusb_version_struct(Structure):
_fields_ = [
("major", c_uint16),
("minor", c_uint16),
("micro", c_uint16),
("nano", c_uint16),
("rc", c_char_p),
("describe", c_char_p),
]
libusb_version = namedtuple("libusb_version", "major minor micro nano rc describe")
[docs]
class ftdi_device_list(Structure):
_fields_ = [("next", c_void_p), ("dev", c_void_p)]
[docs]
class ftdi_version_info(Structure):
_fields_ = [
("major", c_int),
("minor", c_int),
("micro", c_int),
("version_str", c_char_p),
("snapshot_str", c_char_p),
]
libftdi_version = namedtuple(
"libftdi_version", "major minor micro version_str snapshot_str"
)
# These constants determine what type of flush operation to perform
FLUSH_BOTH = 1
FLUSH_INPUT = 2
FLUSH_OUTPUT = 3
# Device Modes
BITMODE_RESET = 0x00
BITMODE_BITBANG = 0x01
# Opening / searching for a device uses this list of IDs to search
# by default. These can be extended directly after import if required.
FTDI_VENDOR_ID = 0x0403
USB_VID_LIST = [FTDI_VENDOR_ID]
USB_PID_LIST = [0x6001, 0x6010, 0x6011, 0x6014, 0x6015]
FTDI_ERROR_DEVICE_NOT_FOUND = -3
[docs]
class Driver:
"""
This is where it all happens...
We load the libftdi library, and use it.
"""
# The default library names to search for. This can be overridden by
# passing a library name or list of library names to the constructor.
# Prefer libftdi1 if available. Windows uses 'lib' prefix.
_lib_search = {
"libftdi": ["ftdi1", "libftdi1", "ftdi", "libftdi"],
"libusb": ["usb-1.0", "libusb-1.0"],
}
[docs]
def __init__(self, libftdi_search: str | list[str] | None = None) -> None:
"""
:param libftdi_search: force a particular version of libftdi to be used
can specify either library name(s) or path(s)
:type libftdi_search: string or a list of strings
"""
if isinstance(libftdi_search, str):
self._lib_search["libftdi"] = [libftdi_search]
elif isinstance(libftdi_search, list):
self._lib_search["libftdi"] = libftdi_search
elif libftdi_search is not None:
raise TypeError(
f"libftdi_search type should be "
f"Optional[str|list[str]], not {type(libftdi_search)}"
)
# Library handles.
self._fdll: Any = None
self._libusb_dll: Any = None
def _load_library(self, name: str, search_list: list[str] | None = None) -> Any:
"""
find and load the requested library
:param name: library name
:param search_list: an optional list of strings referring to library names
library names or paths can be given
:return: a CDLL object referring to the requested library
"""
# If no search list is given, use default library names from self._lib_search
if search_list is None:
search_list = self._lib_search.get(name, [])
elif not isinstance(search_list, list):
raise TypeError(
f"search_list type should be list[str], not {type(search_list)}"
)
lib = None
for dll in search_list:
try:
# Windows in particular can have find_library
# not find things which work fine directly on
# cdll access.
lib = getattr(cdll, dll)
break
# On DLL load fail, Python <3.12 raises OSError, 3.12+ AttributeError.
except (OSError, AttributeError):
lib_path = find_library(dll)
if lib_path is not None:
lib = getattr(cdll, lib_path)
break
if lib is None:
raise LibraryMissingError(
f"{name} library not found (search: {str(search_list)})"
)
return lib
@property
def _libusb(self): # type: ignore
"""
ctypes DLL referencing the libusb library, if it exists
Note this is not normally used directly by pylibftdi, and is available
primarily for diagnostic purposes.
"""
if self._libusb_dll is None:
self._libusb_dll = self._load_library("libusb")
self._libusb_dll.libusb_get_version.restype = POINTER(libusb_version_struct)
return self._libusb_dll
[docs]
def libusb_version(self) -> libusb_version:
"""
:return: namedtuple containing version info on libusb
"""
ver = self._libusb.libusb_get_version().contents
return libusb_version(
ver.major,
ver.minor,
ver.micro,
ver.nano,
ver.rc.decode(),
ver.describe.decode(),
)
@property
def fdll(self) -> Any:
"""
ctypes DLL referencing the libftdi library
This is the main interface to FTDI functionality.
"""
if self._fdll is None:
self._fdll = self._load_library("libftdi")
# most args/return types are fine with the implicit
# int/void* which ctypes uses, but some need setting here
self._fdll.ftdi_get_error_string.restype = c_char_p
self._fdll.ftdi_usb_get_strings.argtypes = (
c_void_p,
c_void_p,
c_char_p,
c_int,
c_char_p,
c_int,
c_char_p,
c_int,
)
# library versions <1.0 don't provide ftdi_get_library_version, so
# we need to check for it before setting the restype.
if hasattr(self._fdll, "ftdi_get_library_version"):
self._fdll.ftdi_get_library_version.restype = ftdi_version_info
return self._fdll
[docs]
def libftdi_version(self) -> libftdi_version:
"""
:return: the version of the underlying library being used
:rtype: tuple (major, minor, micro, version_string, snapshot_string)
"""
if hasattr(self.fdll, "ftdi_get_library_version"):
version = self.fdll.ftdi_get_library_version()
return libftdi_version(
version.major,
version.minor,
version.micro,
version.version_str.decode(),
version.snapshot_str.decode(),
)
else:
# library versions <1.0 don't support this function...
return libftdi_version(
0, 0, 0, "< 1.0 - no ftdi_get_library_version()", "unknown"
)
[docs]
def list_devices(self) -> list[tuple[str, str, str]]:
"""
:return: (manufacturer, description, serial#) for each attached
device, e.g.:
[('FTDI', 'UM232R USB <-> Serial', 'FTE4FFVQ'),
('FTDI', 'UM245R', 'FTE00P4L')]
:rtype: a list of string triples
the serial number can be used to open specific devices
"""
# ftdi_usb_find_all sets dev_list_ptr to a linked list
# (*next/*usb_device) of usb_devices, each of which can
# be passed to ftdi_usb_get_strings() to get info about
# them.
# this will contain the device info to return
devices = []
manuf = create_string_buffer(128)
desc = create_string_buffer(128)
serial = create_string_buffer(128)
devlistptrtype = POINTER(ftdi_device_list)
dev_list_ptr = devlistptrtype()
# create context for doing the enumeration
ctx = create_string_buffer(1024)
if self.fdll.ftdi_init(byref(ctx)) != 0:
msg = self.fdll.ftdi_get_error_string(byref(ctx))
raise FtdiError(msg)
def _s(s: bytes) -> str:
"""c_char_p -> str helper"""
return s.decode()
try:
for usb_vid, usb_pid in itertools.product(USB_VID_LIST, USB_PID_LIST):
res = self.fdll.ftdi_usb_find_all(
byref(ctx), byref(dev_list_ptr), usb_vid, usb_pid
)
if res < 0:
err_msg = self.fdll.ftdi_get_error_string(byref(ctx))
msg = "%s (%d)" % (err_msg, res)
raise FtdiError(msg)
elif res > 0:
# take a copy of the dev_list for subsequent list_free
dev_list_base = byref(dev_list_ptr)
# traverse the linked list...
try:
while dev_list_ptr:
res = self.fdll.ftdi_usb_get_strings(
byref(ctx),
dev_list_ptr.contents.dev,
manuf,
127,
desc,
127,
serial,
127,
)
# don't error on failure to get all the data
# error codes: -7: manuf, -8: desc, -9: serial
if res < 0 and res not in (-7, -8, -9):
err_msg = self.fdll.ftdi_get_error_string(byref(ctx))
msg = "%s (%d)" % (err_msg, res)
raise FtdiError(msg)
devices.append(
(_s(manuf.value), _s(desc.value), _s(serial.value))
)
# step to next in linked-list
dev_list_ptr = cast(
dev_list_ptr.contents.next, devlistptrtype
)
finally:
self.fdll.ftdi_list_free(dev_list_base)
finally:
self.fdll.ftdi_deinit(byref(ctx))
return devices