├── docs ├── requirements.txt ├── pylibftdi.rst ├── pylibftdi.examples.rst ├── index.rst ├── quickstart.rst ├── advanced_usage.rst ├── basic_usage.rst ├── bitbang.rst ├── serial.rst ├── introduction.rst ├── conf.py ├── developing.rst ├── make.bat ├── Makefile ├── installation.rst ├── troubleshooting.rst └── how_to.rst ├── src └── pylibftdi │ ├── examples │ ├── __init__.py │ ├── led_flash.py │ ├── magic_candle.py │ ├── midi_output.py │ ├── list_devices.py │ ├── info.py │ ├── serial_loopback.py │ ├── morse.py │ ├── lcd.py │ ├── bit_server.py │ ├── pin_read.py │ └── serial_transfer.py │ ├── _base.py │ ├── __main__.py │ ├── util.py │ ├── __init__.py │ ├── serial_device.py │ ├── bitbang.py │ ├── driver.py │ └── device.py ├── tests ├── __init__.py ├── call_log.py ├── test_bus.py ├── test_serial.py ├── test_common.py ├── test_driver.py ├── test_device.py └── test_bitbang.py ├── .gitignore ├── .readthedocs.yaml ├── Dockerfile ├── Makefile ├── LICENSE.txt ├── .github └── workflows │ └── python-lint-and-test.yml ├── pyproject.toml ├── README.rst ├── poetry.lock └── CHANGES.txt /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/__init__.py: -------------------------------------------------------------------------------- 1 | """pylibftdi examples""" 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """tests for pylibftdi""" 2 | 3 | # this file must exist for python3 -m unittest discover to work 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | 4 | .idea 5 | .vscode 6 | 7 | .DS_Store 8 | 9 | build 10 | dist 11 | docs/_build 12 | 13 | .venv 14 | -------------------------------------------------------------------------------- /tests/call_log.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __author__ = "ben" 4 | 5 | 6 | class CallLog: 7 | fn_log: list[str] = [] 8 | 9 | @classmethod 10 | def reset(cls): 11 | cls.fn_log.clear() 12 | 13 | @classmethod 14 | def append(cls, value): 15 | cls.fn_log.append(value) 16 | 17 | @classmethod 18 | def get(cls): 19 | return cls.fn_log.copy() 20 | -------------------------------------------------------------------------------- /src/pylibftdi/_base.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2020 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | """ 10 | 11 | # This module contains things needed by at least one other 12 | # module so as to prevent circular imports. 13 | 14 | __ALL__ = ["FtdiError", "LibraryMissingError"] 15 | 16 | 17 | class FtdiError(Exception): 18 | pass 19 | 20 | 21 | class LibraryMissingError(FtdiError): 22 | pass 23 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build all formats (HTML, epub, PDF) 9 | formats: all 10 | 11 | # Set the version of Python and other tools you might need 12 | build: 13 | os: ubuntu-24.04 14 | tools: 15 | python: "3.12" 16 | 17 | # Build documentation in the docs/ directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | -------------------------------------------------------------------------------- /src/pylibftdi/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi.__main__ 3 | 4 | This module exists to support `python -im pylibftdi` as a REPL with appropriate 5 | modules and constants already loaded. 6 | 7 | e.g. 8 | 9 | $ python3 -im pylibftdi 10 | >>> d = Device() 11 | >>> d.write('Hello World') 12 | """ 13 | 14 | # This should be aligned with `__all__` in pylibftdi/__init__.py 15 | from pylibftdi import ( # noqa 16 | ALL_INPUTS, 17 | ALL_OUTPUTS, 18 | BB_INPUT, 19 | BB_OUTPUT, 20 | USB_PID_LIST, 21 | USB_VID_LIST, 22 | BitBangDevice, 23 | Bus, 24 | Device, 25 | Driver, 26 | FtdiError, 27 | ) 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Defines an ubuntu-based dev container for development and testing of pylibftdi 2 | 3 | # As well as providing poetry etc, this pre-installs the required libftdi1-2 4 | # package, providing libftdi 1.5 (as of Ubuntu 24.04) and all its dependencies. 5 | 6 | FROM ubuntu:24.04 7 | WORKDIR /app 8 | RUN \ 9 | apt-get update; \ 10 | apt-get install -y libftdi1-2 python3-minimal python3-pip python3-poetry vim; 11 | 12 | # Use in-project venvs so subsequent `poetry install` operations are quick 13 | ENV POETRY_VIRTUALENVS_IN_PROJECT=true 14 | # Required for `poetry shell` to detect the shell 15 | ENV SHELL=/bin/bash 16 | 17 | CMD ["bash"] 18 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/led_flash.py: -------------------------------------------------------------------------------- 1 | """ 2 | Flash an LED connected via a FTDI UM232R/245R module using pylibftdi 3 | 4 | Optionally supply a flash rate (in Hz, default 1) as an argument 5 | 6 | Copyright (c) 2010-2014 Ben Bass 7 | All rights reserved. 8 | """ 9 | 10 | import sys 11 | import time 12 | 13 | from pylibftdi import BitBangDevice 14 | 15 | 16 | def flash_forever(rate): 17 | """toggle bit zero at rate Hz""" 18 | # put an LED with 1Kohm or similar series resistor 19 | # on D0 pin 20 | with BitBangDevice() as bb: 21 | while True: 22 | time.sleep(1.0 / (2 * rate)) 23 | bb.port ^= 1 24 | 25 | 26 | def main(): 27 | if len(sys.argv) > 1: 28 | rate = float(sys.argv[1]) 29 | flash_forever(rate) 30 | else: 31 | flash_forever(1) 32 | 33 | 34 | if __name__ == "__main__": 35 | try: 36 | main() 37 | except KeyboardInterrupt: 38 | pass 39 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/magic_candle.py: -------------------------------------------------------------------------------- 1 | """ 2 | Magic Candle - light falling on the LDR turns on the LED, which due 3 | to arrangement keeps the LED on until LDR/LED path 4 | is blocked 5 | 6 | LDR (via a transistor switch - dark = '1') - D0 7 | LED (via series resistor) - D1 8 | 9 | pylibftdi - codedstructure 2013-2014 10 | """ 11 | 12 | import time 13 | 14 | from pylibftdi import BitBangDevice 15 | from pylibftdi.util import Bus 16 | 17 | 18 | class Candle: 19 | is_dark = Bus(0) # D0 20 | be_light = Bus(1) # D1 21 | 22 | def __init__(self): 23 | # make the device connection, this is used 24 | # in the Bus descriptors. Also set direction 25 | # appropriately. 26 | self.device = BitBangDevice(direction=0xFE) 27 | 28 | def run(self): 29 | while True: 30 | time.sleep(0.05) 31 | self.be_light = not self.is_dark 32 | 33 | 34 | if __name__ == "__main__": 35 | c = Candle() 36 | c.run() 37 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/midi_output.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from pylibftdi import Device 4 | 5 | 6 | class MidiDevice(Device): 7 | def __init__(self, *o, **k): 8 | Device.__init__(self, *o, **k) 9 | self.baudrate = 31250 10 | 11 | 12 | MAJOR_INTERVAL = [2, 2, 1, 2, 2, 2, 1, 2] 13 | MINOR_INTERVAL = [2, 1, 2, 2, 1, 2, 2, 2] 14 | START_NOTE = 48 15 | 16 | 17 | def volume(beat): 18 | return 100 if beat % 2 else 127 19 | 20 | 21 | def scale(): 22 | midi = MidiDevice() 23 | 24 | note = START_NOTE 25 | for i in range(8): 26 | midi.write("\x90%c%c" % (chr(note), chr(volume(i)))) 27 | time.sleep(0.25) 28 | midi.write("\x90%c\x00" % chr(note)) 29 | note += MAJOR_INTERVAL[i] 30 | time.sleep(0.125) 31 | 32 | time.sleep(0.5) 33 | 34 | for i in range(8): 35 | note -= MINOR_INTERVAL[7 - i] 36 | midi.write("\x90%c%c" % (chr(note), chr(volume(i)))) 37 | time.sleep(0.35) 38 | midi.write("\x90%c\x00" % chr(note)) 39 | time.sleep(0.125) 40 | 41 | 42 | if __name__ == "__main__": 43 | scale() 44 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Basic development functions for pylibftdi. 2 | 3 | # Example: 4 | # make container && make lint && make test && make build 5 | 6 | .PHONY: all 7 | all: container test lint build 8 | 9 | .PHONY: container 10 | container: 11 | docker build -t pylibftdi-dev:latest . 12 | 13 | .PHONY: build 14 | build: 15 | docker run --rm -t -v $$PWD:/app -w /app pylibftdi-dev:latest poetry build 16 | 17 | .PHONY: test 18 | test: 19 | docker run --rm -t -v $$PWD:/app -w /app pylibftdi-dev:latest bash -c 'poetry install && poetry run pytest' 20 | 21 | .PHONY: lint 22 | lint: 23 | docker run --rm -t -v $$PWD:/app -w /app pylibftdi-dev:latest bash -c 'poetry install && (poetry run black --check .; poetry run ruff check src tests; poetry run mypy src)' 24 | 25 | .PHONY: shell 26 | shell: 27 | # Drop into a poetry shell where e.g. `python3 -m pylibftdi.examples.list_devices` etc can be run 28 | docker run --rm -it -v $$PWD:/app -w /app pylibftdi-dev:latest bash -ic 'poetry install && poetry shell' 29 | 30 | .PHONY: clean 31 | clean: 32 | # Ideally this would remove all relevant dev containers too... 33 | rm -rf dist/ .venv/ 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2024 Ben Bass 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 22 | 23 | http://www.opensource.org/licenses/mit-license.php 24 | -------------------------------------------------------------------------------- /docs/pylibftdi.rst: -------------------------------------------------------------------------------- 1 | pylibftdi Package 2 | ================= 3 | 4 | :mod:`pylibftdi` Package 5 | ------------------------ 6 | 7 | .. automodule:: pylibftdi.__init__ 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`_base` Module 13 | ------------------- 14 | 15 | .. automodule:: pylibftdi._base 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`device` Module 21 | -------------------- 22 | 23 | .. automodule:: pylibftdi.device 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`driver` Module 29 | -------------------- 30 | 31 | .. automodule:: pylibftdi.driver 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | 37 | :mod:`bitbang` Module 38 | --------------------- 39 | 40 | .. automodule:: pylibftdi.bitbang 41 | :members: 42 | :undoc-members: 43 | :show-inheritance: 44 | 45 | :mod:`serial_device` Module 46 | --------------------------- 47 | 48 | .. automodule:: pylibftdi.serial_device 49 | :members: 50 | :undoc-members: 51 | :show-inheritance: 52 | 53 | :mod:`util` Module 54 | ------------------ 55 | 56 | .. automodule:: pylibftdi.util 57 | :members: 58 | :undoc-members: 59 | :show-inheritance: 60 | 61 | Subpackages 62 | ----------- 63 | 64 | .. toctree:: 65 | 66 | pylibftdi.examples 67 | 68 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/list_devices.py: -------------------------------------------------------------------------------- 1 | """ 2 | Report connected FTDI devices. This may be useful in obtaining 3 | serial numbers to use as the device_id parameter of the Device() 4 | constructor to communicate with a specific device when more than 5 | one is present. 6 | 7 | example usage: 8 | 9 | $ python pylibftdi/examples/list_devices.py 10 | FTDI:UB232R:FTAS1UN5 11 | FTDI:UM232R USB <-> Serial:FTE4FFVQ 12 | 13 | To open a device specifically to communicate with the second of 14 | these devices, the following would be used: 15 | 16 | >>> from pylibftdi import Device 17 | >>> dev = Device(device_id="FTE4FFVQ") 18 | >>> 19 | 20 | Copyright (c) 2011-2014 Ben Bass 21 | All rights reserved. 22 | """ 23 | 24 | from pylibftdi import Driver 25 | 26 | 27 | def get_ftdi_device_list(): 28 | """ 29 | return a list of lines, each a colon-separated 30 | vendor:product:serial summary of detected devices 31 | """ 32 | dev_list = [] 33 | for device in Driver().list_devices(): 34 | # device must always be this triple 35 | vendor, product, serial = device 36 | dev_list.append(f"{vendor}:{product}:{serial}") 37 | return dev_list 38 | 39 | 40 | def main(): 41 | for device in get_ftdi_device_list(): 42 | print(device) 43 | 44 | 45 | if __name__ == "__main__": 46 | main() 47 | -------------------------------------------------------------------------------- /.github/workflows/python-lint-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | # Note we don't test versions < 3.9 as some dev deps require >= 3.9 12 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install ruff pytest mypy black 24 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 25 | - name: Style with black 26 | run: | 27 | black --check . 28 | - name: Lint with ruff 29 | run: | 30 | # stop the build if there are Python syntax errors or undefined names 31 | ruff check --select=E9,F63,F7,F82 --target-version=py37 . 32 | # default set of ruff rules with GitHub Annotations 33 | ruff check --target-version=py37 . 34 | - name: Typecheck with mypy 35 | run: | 36 | mypy src/ tests/ 37 | - name: Test with pytest 38 | run: | 39 | pytest . 40 | -------------------------------------------------------------------------------- /src/pylibftdi/util.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | """ 10 | 11 | # The Bus descriptor class is probably useful outside of 12 | # pylibftdi. It tries to be to Python what bitfields are 13 | # to C. Its only requirement (which is fairly pylibftdi-ish) 14 | # is a 'device' attribute on the object this is attached 15 | # to, which has a 'port' property which is readable and 16 | # writable. 17 | 18 | 19 | class Bus: 20 | """ 21 | This class is a descriptor for a bus of a given width starting 22 | at a given offset (0 = LSB). The device which does the actual 23 | reading and writing is assumed to be a BitBangDevice instance 24 | in the 'device' attribute of the object to which this is attached. 25 | """ 26 | 27 | def __init__(self, offset, width=1): 28 | self.offset = offset 29 | self.width = width 30 | self._mask = (1 << width) - 1 31 | 32 | def __get__(self, obj, type_): 33 | val = obj.device.port 34 | return (val >> self.offset) & self._mask 35 | 36 | def __set__(self, obj, value): 37 | value = value & self._mask 38 | # in a multi-threaded environment, would 39 | # want to ensure following was locked, eg 40 | # by acquiring a device lock 41 | val = obj.device.port 42 | val &= ~(self._mask << self.offset) 43 | val |= value << self.offset 44 | obj.device.port = val 45 | -------------------------------------------------------------------------------- /tests/test_bus.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | This module contains some basic tests for the higher-level 10 | functionality without requiring an actual hardware device 11 | to be attached. 12 | """ 13 | 14 | import unittest 15 | 16 | from pylibftdi.util import Bus 17 | 18 | 19 | class TestBus(unittest.TestCase): 20 | class MockDevice: 21 | port = 0 22 | 23 | class Bus1: 24 | a = Bus(0, 2) 25 | b = Bus(2, 1) 26 | c = Bus(3, 5) 27 | 28 | def __init__(self): 29 | self.device = TestBus.MockDevice() 30 | 31 | def test_bus_write(self): 32 | test_bus = TestBus.Bus1() 33 | # test writing to the bus 34 | self.assertEqual(test_bus.device.port, 0) 35 | test_bus.a = 3 36 | test_bus.b = 1 37 | test_bus.c = 31 38 | self.assertEqual(test_bus.device.port, 255) 39 | test_bus.b = 0 40 | self.assertEqual(test_bus.device.port, 251) 41 | test_bus.c = 16 42 | self.assertEqual(test_bus.device.port, 131) 43 | 44 | def test_bus_read(self): 45 | test_bus = TestBus.Bus1() 46 | # test reading from the bus 47 | test_bus.device.port = 0x55 48 | assert test_bus.a == 1 49 | assert test_bus.b == 1 50 | assert test_bus.c == 10 51 | test_bus.device.port = 0xAA 52 | assert test_bus.a == 2 53 | assert test_bus.b == 0 54 | assert test_bus.c == 21 55 | 56 | 57 | if __name__ == "__main__": 58 | unittest.main() 59 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/info.py: -------------------------------------------------------------------------------- 1 | """ 2 | Report environment info relevant to pylibftdi 3 | 4 | example usage:: 5 | 6 | $ python3 -m pylibftdi.examples.info 7 | pylibftdi version : 0.15.0 8 | libftdi version : libftdi_version(major=1, minor=1, micro=0, version_str='1.1', snapshot_str='unknown') 9 | libftdi library path : /usr/local/lib/libftdi1.dylib 10 | libusb version : libusb_version(major=1, minor=0, micro=19, nano=10903, rc='', describe='http://libusb.info') 11 | libusb library path : /usr/local/lib/libusb-1.0.dylib 12 | Python version : 3.4.0 13 | OS platform : Darwin-14.1.0-x86_64-i386-64bit 14 | 15 | Copyright (c) 2015-2020 Ben Bass 16 | """ # noqa: E501 17 | 18 | import platform 19 | from collections import OrderedDict 20 | 21 | import pylibftdi 22 | 23 | 24 | def ftdi_info(): 25 | """ 26 | Return (ordered) dictionary contianing pylibftdi environment info 27 | 28 | Designed for display purposes only; keys and value types may vary. 29 | """ 30 | info = OrderedDict() 31 | d = pylibftdi.Driver() 32 | info["pylibftdi version"] = pylibftdi.__VERSION__ 33 | try: 34 | info["libftdi version"] = d.libftdi_version() 35 | info["libftdi library name"] = d._load_library("libftdi")._name 36 | except pylibftdi.LibraryMissingError: 37 | info["libftdi library"] = "Missing" 38 | try: 39 | info["libusb version"] = d.libusb_version() 40 | info["libusb library name"] = d._load_library("libusb")._name 41 | except pylibftdi.LibraryMissingError: 42 | info["libusb library"] = "Missing" 43 | info["Python version"] = platform.python_version() 44 | info["OS platform"] = platform.platform() 45 | return info 46 | 47 | 48 | if __name__ == "__main__": 49 | for key, value in ftdi_info().items(): 50 | print(f"{key:22}: {value}") 51 | -------------------------------------------------------------------------------- /docs/pylibftdi.examples.rst: -------------------------------------------------------------------------------- 1 | examples Package 2 | ================ 3 | 4 | :mod:`examples` Package 5 | ----------------------- 6 | 7 | .. automodule:: pylibftdi.examples 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`bit_server` Module 13 | ------------------------ 14 | 15 | .. automodule:: pylibftdi.examples.bit_server 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`lcd` Module 21 | ----------------- 22 | 23 | .. automodule:: pylibftdi.examples.lcd 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`led_flash` Module 29 | ----------------------- 30 | 31 | .. automodule:: pylibftdi.examples.led_flash 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`list_devices` Module 37 | -------------------------- 38 | 39 | .. automodule:: pylibftdi.examples.list_devices 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`magic_candle` Module 45 | -------------------------- 46 | 47 | .. automodule:: pylibftdi.examples.magic_candle 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | :mod:`midi_output` Module 53 | ------------------------- 54 | 55 | .. automodule:: pylibftdi.examples.midi_output 56 | :members: 57 | :undoc-members: 58 | :show-inheritance: 59 | 60 | :mod:`pin_read` Module 61 | ---------------------- 62 | 63 | .. automodule:: pylibftdi.examples.pin_read 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | :mod:`serial_loopback` Module 69 | ----------------------------- 70 | 71 | .. automodule:: pylibftdi.examples.serial_loopback 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | 76 | :mod:`info` Module 77 | ----------------------------- 78 | 79 | .. automodule:: pylibftdi.examples.info 80 | :members: 81 | :undoc-members: 82 | :show-inheritance: 83 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to pylibftdi's documentation! 2 | ===================================== 3 | 4 | pylibftdi is a simple library interacting with FTDI devices to provide 5 | serial and parallel IO from Python. 6 | 7 | Examples:: 8 | 9 | >>> from pylibftdi import BitBangDevice 10 | >>> with BitBangDevice('FT0123') as dev: 11 | ... dev.port |= 1 12 | 13 | :: 14 | 15 | >>> # Send a MIDI 'note on' message 16 | >>> from pylibftdi import Device 17 | >>> with Device() as dev: 18 | ... dev.baudrate = 31250 19 | ... dev.write('\x90\x64\x64') 20 | 21 | The two main use cases it serves are: 22 | 23 | * the need to control or monitor external equipment, for which a FTDI 24 | module may be a cheap and reliable starting point. 25 | 26 | * the need to interact with existing devices which are known to contain 27 | FTDI chipsets for their USB interface. 28 | 29 | FTDI (http://www.ftdichip.com) create devices (chipsets, modules, 30 | cables etc) to interface devices to the USB port of your computer. 31 | 32 | libftdi (http://www.intra2net.com/en/developer/libftdi/) is an open source 33 | driver to communicate with these devices, and runs on top of libusb. 34 | It works on Windows, Linux, and Mac OS X, and likely other systems too. 35 | 36 | pylibftdi is a pure Python module which interfaces (via ctypes) to libftdi, 37 | exposing a simple file-like API to connected devices. It supports serial and 38 | parallel IO in a straight-forward way, and aims to be one of the simplest 39 | ways of interacting with the world outside your PC. 40 | 41 | Contents 42 | ======== 43 | 44 | .. toctree:: 45 | :maxdepth: 2 46 | 47 | introduction 48 | quickstart 49 | installation 50 | basic_usage 51 | bitbang 52 | serial 53 | advanced_usage 54 | how_to 55 | troubleshooting 56 | developing 57 | pylibftdi 58 | 59 | Indices and tables 60 | ================== 61 | 62 | * :ref:`genindex` 63 | * :ref:`modindex` 64 | * :ref:`search` 65 | 66 | -------------------------------------------------------------------------------- /src/pylibftdi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2024 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | libftdi can be found at: http://www.intra2net.com/en/developer/libftdi/ 10 | 11 | Neither libftdi nor Intra2net are associated with this project; 12 | if something goes wrong here, it's almost definitely my fault 13 | rather than a problem with the libftdi library. 14 | """ 15 | 16 | __VERSION__ = "0.23.0" 17 | __AUTHOR__ = "Ben Bass" 18 | 19 | 20 | __all__ = [ 21 | "Driver", 22 | "Device", 23 | "BitBangDevice", 24 | "Bus", 25 | "FtdiError", 26 | "ALL_OUTPUTS", 27 | "ALL_INPUTS", 28 | "BB_OUTPUT", 29 | "BB_INPUT", 30 | "USB_VID_LIST", 31 | "USB_PID_LIST", 32 | ] 33 | 34 | import sys 35 | 36 | from pylibftdi import _base, bitbang, device, driver, serial_device, util 37 | 38 | if sys.version_info < (3, 7, 0): # noqa 39 | import warnings 40 | 41 | warnings.warn("Python version < 3.7.0: untested; expect issues.", stacklevel=0) 42 | 43 | 44 | # Bring them in to package scope so we can treat pylibftdi 45 | # as a module if we want. 46 | FtdiError = _base.FtdiError 47 | LibraryMissingError = _base.LibraryMissingError 48 | Bus = util.Bus 49 | Driver = driver.Driver 50 | Device = device.Device 51 | SerialDevice = serial_device.SerialDevice 52 | BitBangDevice = bitbang.BitBangDevice 53 | USB_VID_LIST = driver.USB_VID_LIST 54 | USB_PID_LIST = driver.USB_PID_LIST 55 | 56 | ALL_OUTPUTS = bitbang.ALL_OUTPUTS 57 | ALL_INPUTS = bitbang.ALL_INPUTS 58 | BB_OUTPUT = bitbang.BB_OUTPUT 59 | BB_INPUT = bitbang.BB_INPUT 60 | FLUSH_BOTH = driver.FLUSH_BOTH 61 | FLUSH_INPUT = driver.FLUSH_INPUT 62 | FLUSH_OUTPUT = driver.FLUSH_OUTPUT 63 | 64 | # Use these for interface_select on multiple-interface devices 65 | INTERFACE_ANY = 0 66 | INTERFACE_A = 1 67 | INTERFACE_B = 2 68 | INTERFACE_C = 3 69 | INTERFACE_D = 4 70 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/serial_loopback.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 -u 2 | """ 3 | test serial loopback; assumes Rx and Tx are connected 4 | 5 | Copyright (c) 2010-2020 Ben Bass 6 | All rights reserved. 7 | """ 8 | 9 | import os 10 | import sys 11 | import time 12 | 13 | from pylibftdi import SerialDevice 14 | 15 | 16 | def test_string(length): 17 | return os.urandom(length) 18 | 19 | 20 | class LoopbackTester: 21 | def __init__(self): 22 | self.device = SerialDevice(chunk_size=16) 23 | 24 | def test_loopback(self, length): 25 | test_str = test_string(length) 26 | if self.device.write(test_str) != len(test_str): 27 | sys.stdout.write("*") 28 | time.sleep(0.1) 29 | result = "" 30 | for _ in range(5): 31 | result = self.device.read(length) 32 | time.sleep(0.1) 33 | if result: 34 | break 35 | if result != test_str: 36 | self.device.flush() 37 | time.sleep(0.25) 38 | return result == test_str 39 | 40 | def test_iter(self, lengths): 41 | self.device.flush() 42 | time.sleep(0.1) 43 | for length in lengths: 44 | yield self.test_loopback(length) 45 | 46 | def bisect(self): 47 | xmin, xmax = 1, 5000 48 | last_test = None 49 | while True: 50 | test = (xmin + xmax) // 2 51 | if test == last_test: 52 | break 53 | if self.test_loopback(test): 54 | xmin = test 55 | else: 56 | xmax = test 57 | last_test = test 58 | return test 59 | 60 | def main(self): 61 | print("Determining largest non-streamed buffer size") 62 | for bd in [9600, 31250, 115200, 1152000]: 63 | print(f"Baudrate: {bd}") 64 | self.device.baudrate = bd 65 | result = self.bisect() 66 | print(f"Buffer size: {result}") 67 | 68 | 69 | if __name__ == "__main__": 70 | tester = LoopbackTester() 71 | tester.main() 72 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pylibftdi" 3 | version="0.23.0" 4 | description = "Pythonic interface to FTDI devices using libftdi." 5 | license = "MIT" 6 | authors = [ 7 | "Ben Bass ", 8 | ] 9 | readme = "README.rst" 10 | homepage = "https://github.com/codedstructure/pylibftdi" 11 | repository = "https://github.com/codedstructure/pylibftdi" 12 | documentation = "https://pylibftdi.readthedocs.io/en/latest/" 13 | keywords = [ 14 | "ftdi", 15 | "libftdi", 16 | "usb" 17 | ] 18 | classifiers = [ 19 | "Development Status :: 4 - Beta", 20 | "Environment :: Console", 21 | "Intended Audience :: Developers", 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: Microsoft :: Windows", 24 | "Operating System :: POSIX", 25 | "Programming Language :: Python", 26 | "Programming Language :: Python :: 3", 27 | "Programming Language :: Python :: 3.7", 28 | "Programming Language :: Python :: 3.8", 29 | "Programming Language :: Python :: 3.9", 30 | "Programming Language :: Python :: 3.10", 31 | "Programming Language :: Python :: 3.11", 32 | "Programming Language :: Python :: 3.12", 33 | "Programming Language :: Python :: 3.13", 34 | "Topic :: Scientific/Engineering", 35 | "Topic :: Software Development :: Embedded Systems", 36 | "Topic :: System :: Hardware", 37 | ] 38 | 39 | [build-system] 40 | requires = ["poetry-core>=1.0.0"] 41 | build-backend = "poetry.core.masonry.api" 42 | 43 | [tool.poetry.dependencies] 44 | python = ">=3.7.0" 45 | 46 | [tool.poetry.dev-dependencies] 47 | ruff = "^0.7.3" 48 | pytest = { version="^8.3.3", python=">=3.8" } 49 | mypy = { version="^1.13.0", python=">=3.8" } 50 | black = { version="^24.10.0", python=">=3.9" } 51 | 52 | [tool.ruff.lint] 53 | select = ["E", "W", "F", "B", "I", "UP", "PL"] 54 | ignore = [ 55 | "PLR2004", # Magic value used in comparison 56 | "PLR0913", # Too many arguments to function call 57 | ] 58 | 59 | [tool.pytest.ini_options] 60 | pythonpath = [ 61 | "src", ".", 62 | ] 63 | addopts = [ 64 | "--import-mode=importlib", 65 | ] 66 | -------------------------------------------------------------------------------- /tests/test_serial.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | This module contains some basic tests for the higher-level 10 | functionality without requiring an actual hardware device 11 | to be attached. 12 | """ 13 | 14 | import unittest 15 | 16 | from pylibftdi.serial_device import SerialDevice 17 | from tests.test_common import CallCheckMixin, LoopDevice 18 | 19 | 20 | class LoopSerialDevice(SerialDevice, LoopDevice): 21 | pass 22 | 23 | 24 | SerialDevice = LoopSerialDevice # type: ignore 25 | 26 | 27 | # and now some test cases... 28 | class SerialFunctions(CallCheckMixin, unittest.TestCase): 29 | def setUp(self): 30 | self.sd = SerialDevice() 31 | 32 | def _read_test(self, item): 33 | # check we ask for the modem status to get this 34 | self.assertCalls(lambda: getattr(self.sd, item), "ftdi_poll_modem_status") 35 | # check it isn't settable 36 | self.assertRaises(AttributeError, lambda: setattr(self.sd, "dsr", 1)) 37 | 38 | def _write_test(self, item): 39 | # check writes call appropriate libftdi function 40 | self.assertCalls(lambda: setattr(self.sd, item, 1), "ftdi_set" + item) 41 | # check reads don't call anything 42 | self.assertCallsExact(lambda: getattr(self.sd, item), []) 43 | 44 | def test_cts(self): 45 | """check setting and getting cts""" 46 | self._read_test("cts") 47 | 48 | def test_dsr(self): 49 | """check setting and getting dsr""" 50 | self._read_test("dsr") 51 | 52 | def test_ri(self): 53 | """check setting and getting ri""" 54 | self._read_test("ri") 55 | 56 | def test_dtr(self): 57 | """check setting and getting dtr""" 58 | self._write_test("dtr") 59 | 60 | def test_rts(self): 61 | """check setting and getting rts""" 62 | self._write_test("rts") 63 | 64 | 65 | if __name__ == "__main__": 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quick Start 2 | =========== 3 | 4 | Install pylibftdi 5 | ----------------- 6 | 7 | See the installation_ instructions for more detailed requirements, but 8 | hopefully things will work by just running the following:: 9 | 10 | $ python3 -m pip install pylibftdi 11 | 12 | .. _installation: installation.html 13 | 14 | Connect and enumerate FTDI devices 15 | ---------------------------------- 16 | 17 | Connect the FTDI device to a free USB port. Run the ``list_devices`` example 18 | to enumerate connected FTDI devices:: 19 | 20 | $ python3 -m pylibftdi.examples.list_devices 21 | 22 | For each connected device, this will show manufacturer, model identifier, 23 | and serial number. With a single device connected, the output maybe 24 | something like the following: 25 | 26 | ``FTDI:UM232H:FTUBIOWF`` 27 | 28 | Though hopefully with a different serial number, or else you've either 29 | stolen mine, or you are me... 30 | 31 | Test some actual IO 32 | ------------------- 33 | 34 | Output example 35 | ~~~~~~~~~~~~~~ 36 | 37 | Connect an LED between D0 of your bit-bang capable device and ground, via a 38 | 330 - 1K ohm resistor as appropriate. 39 | 40 | Test the installation and functioning of pylibftdi with the following:: 41 | 42 | $ python3 -m pylibftdi.examples.led_flash 43 | 44 | The LED should now flash at approximately 1Hz. 45 | 46 | Input example 47 | ~~~~~~~~~~~~~ 48 | 49 | To test some input, remove any connections from the port lines initially, 50 | then run the following, which reads and prints the status of the input lines 51 | regularly:: 52 | 53 | $ python3 -m pylibftdi.examples.pin_read 54 | 55 | The ``pin_read`` example is a complete command line application which can 56 | be used to monitor for particular values on the attached device pins, and 57 | output an appropriate error code on match. Repeat the above with a trailing 58 | ``--help`` for info. 59 | 60 | Using ``pylibftdi`` from the REPL 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | Since ``pylibftdi`` v0.18.0, a ``__main__.py`` module is included which 64 | imports all the exported constants, classes and functions from ``pylibftdi``. 65 | 66 | This allows quick interaction with FTDI devices from the Python REPL:: 67 | 68 | $ python3 -im pylibftdi 69 | >>> d = Device() 70 | >>> d.write('Hello World') 71 | >>> 72 | 73 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/morse.py: -------------------------------------------------------------------------------- 1 | """ 2 | `morse` - pylibftdi example to generate morse code signals 3 | 4 | Requires a bitbang-capable device with appropriate actuator (LED, buzzer etc) 5 | connected to bit 0. 6 | """ 7 | 8 | from os import isatty 9 | from time import sleep 10 | 11 | from pylibftdi import BitBangDevice 12 | 13 | # See https://en.wikipedia.org/wiki/Morse_code 14 | morse_code = """ 15 | a.- b-... c-.-. d-.. e. f..-. g--. h.... i.. j.--- k-.- l.-.. m-- 16 | n-. o--- p.--. q--.- r.-. s... t- u..- v...- w.-- x-..- y-.-- z--.. 17 | 1.---- 2..--- 3...-- 4....- 5..... 6-.... 7--... 8---.. 9----. 0----- 18 | """ 19 | 20 | morse_map = {m[0]: m[1:] for m in morse_code.split()} 21 | 22 | 23 | def output(s, device, wpm=12): 24 | """ 25 | output given string `s` as a Morse code pattern to given BitBangDevice 26 | 27 | :param s: string to render in morse code 28 | :param device: open bitbangdevice 29 | :param wpm: words-per-minute rate. (default 12) 30 | """ 31 | # Assume 5 letters per word, 13 units (e.g. 5 dots plus space) per letter 32 | delay = 60 / (5 * 13 * wpm) 33 | for word in s.split(): 34 | for letter in word: 35 | morse = morse_map.get(letter) 36 | if not morse: 37 | # completely ignore unknown characters 38 | continue 39 | for symbol in morse: 40 | if symbol == ".": 41 | device.port = 1 42 | sleep(delay) 43 | device.port = 0 44 | else: 45 | device.port = 1 46 | sleep(3 * delay) 47 | device.port = 0 48 | sleep(delay) # inter-symbol delay; 1 unit (same as dot) 49 | # inter-letter delay; 3 units 50 | sleep(3 * delay) 51 | # inter-word delay; 7 units 52 | sleep(7 * delay) 53 | 54 | 55 | def main(wpm=12): 56 | """ 57 | :param wpm: words per minute 58 | """ 59 | while True: 60 | try: 61 | s = input("Morse:> " if isatty(0) else "").lower().strip() 62 | except EOFError: 63 | break 64 | 65 | with BitBangDevice() as device: 66 | output(s, device, wpm=wpm) 67 | 68 | if isatty(0): 69 | print("Bye!") 70 | 71 | 72 | if __name__ == "__main__": 73 | main() 74 | -------------------------------------------------------------------------------- /docs/advanced_usage.rst: -------------------------------------------------------------------------------- 1 | Advanced Usage 2 | ============== 3 | 4 | ``libftdi`` function access 5 | --------------------------- 6 | 7 | Three attributes of ``Device`` instances are documented which allow direct 8 | access to the underlying ``libftdi`` functionality. 9 | 10 | #. ``fdll`` - this is a reference to the loaded ``libftdi`` library, loaded 11 | via ctypes. This should be used with the normal ctypes protocols. 12 | #. ``ctx`` - this is a reference to the context of the current device 13 | context. It is managed as a raw ctypes byte-string, so can be modified 14 | if required at the byte-level using appropriate ``ctypes`` methods. 15 | #. ``ftdi_fn`` - a convenience function wrapper, this is the preferred 16 | method for accessing library functions for a specific device instance. 17 | This is a function forwarder to the local ``fdll`` attribute, but also 18 | wraps the device context and passes it as the first argument. In this 19 | way, using ``device.ftdi_fn.ft_xyz`` is more like the D2XX driver 20 | provided by FTDI, in which the device context is passed in at 21 | initialisation time and then the client no longer needs to care about it. 22 | A call to:: 23 | 24 | >>> device.ftdi_fn.ft_xyz(1, 2, 3) 25 | 26 | is equivalent to the following:: 27 | 28 | >>> device.fdll.ft_xyz(ctypes.byref(device.ctx), 1, 2, 3) 29 | 30 | but has the advantages of being shorter and not requiring ctypes to be 31 | in scope. 32 | 33 | incorrect operations using any of these attributes of devices 34 | are liable to crash the Python interpreter 35 | 36 | Examples 37 | ~~~~~~~~ 38 | 39 | The following example shows opening a device in serial mode, switching 40 | temporarily to bit-bang mode, then back to serial and writing a string. 41 | Why this would be wanted is anyone's guess ;-) 42 | 43 | :: 44 | 45 | >>> from pylibftdi import Device 46 | >>> 47 | >>> with Device() as dev: 48 | >>> dev.ftdi_fn.ftdi_set_bitmode(1, 0x01) 49 | >>> dev.write('\x00\x01\x00') 50 | >>> dev.ftdi_fn.ftdi_set_bitmode(0, 0x00) 51 | >>> dev.write('Hello World!!!') 52 | 53 | 54 | The libftdi_ documentation should be consulted in conjunction with the 55 | ctypes_ reference for guidance on using these features. 56 | 57 | .. _libftdi: http://www.intra2net.com/en/developer/libftdi/documentation/ 58 | .. _ctypes: http://docs.python.org/library/ctypes.html 59 | 60 | -------------------------------------------------------------------------------- /docs/basic_usage.rst: -------------------------------------------------------------------------------- 1 | Basic Usage 2 | =========== 3 | 4 | `pylibftdi` is a minimal Pythonic interface to FTDI devices using libftdi_. 5 | Rather than simply expose all the methods of the underlying library directly, 6 | it aims to provide a simpler API for the main use-cases of serial and parallel 7 | IO, while still allowing the use of the more advanced functions of the library. 8 | 9 | .. _libftdi: http://www.intra2net.com/en/developer/libftdi/ 10 | 11 | General 12 | ------- 13 | 14 | The primary interface is the ``Device`` class in the ``pylibftdi`` package; this 15 | gives serial access on relevant FTDI devices (e.g. the UM232R), providing a 16 | file-like interface (read, write). Baudrate is controlled with the ``baudrate`` 17 | property. 18 | 19 | If a Device instance is created with ``mode='t'`` (text mode) then read() and 20 | write() can use the given ``encoding`` (defaulting to latin-1). This allows 21 | easier integration with passing unicode strings between devices. 22 | 23 | Multiple devices are supported by passing the desired device serial number (as 24 | a string) in the ``device_id`` parameter - this is the first parameter in both 25 | Device() and BitBangDevice() constructors. Alternatively the device 'description' 26 | can be given, and an attempt will be made to match this if matching by serial 27 | number fails. 28 | 29 | In the event that multiple devices (perhaps of identical type) have the same 30 | description and serial number, the ``device_index`` parameter may be given to 31 | open matching devices by numerical index; this defaults to zero, meaning the 32 | first matching device. 33 | 34 | Examples 35 | ~~~~~~~~ 36 | 37 | :: 38 | 39 | >>> from pylibftdi import Device 40 | >>> 41 | >>> with Device(mode='t') as dev: 42 | ... dev.baudrate = 115200 43 | ... dev.write('Hello World') 44 | 45 | The pylibftdi.BitBangDevice wrapper provides access to the parallel IO mode of 46 | operation through the ``port`` and ``direction`` properties. These provide an 47 | 8 bit IO port including all the relevant bit operations to make things simple. 48 | 49 | :: 50 | 51 | >>> from pylibftdi import BitBangDevice 52 | >>> 53 | >>> with BitBangDevice('FTE00P4L') as bb: 54 | ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) 55 | ... bb.port |= 2 # set bit 1 56 | ... bb.port &= 0xFE # clear bit 0 57 | 58 | There is support for a number of external devices and protocols, specifically 59 | for interfacing with HD44780 LCDs using the 4-bit interface. 60 | 61 | -------------------------------------------------------------------------------- /docs/bitbang.rst: -------------------------------------------------------------------------------- 1 | Bit-bang mode 2 | ============= 3 | 4 | Bit-bang mode allows the programmer direct access (both read and write) to the state of the IO lines from a compatible FTDI device. 5 | 6 | The interface provided by FTDI is intended to mirror the type of usage on a microcontroller, and is similar to the 'user port' on many old 8-bit computers such as the BBC Micro and Commodore 64. 7 | 8 | The basic model is to have two 8 bit ports - one for data, and one for 'direction'. The data port maps each of the 8 bits to 8 independent IO signals, each of which can be configured separately as an 'input' or an 'output'. 9 | 10 | In pylibftdi, the data port is given by the ``port`` attribute of a BitBangDevice instance, and the direction control is provided by the ``direction`` attribute. Both these attributes are implemented as Python properties, so no method calls are needed on them - simple read and write in Python-land converts to read and write in the physical world seen by the FTDI device. 11 | 12 | The direction register maps to 13 | 14 | where each bit maps to a separate digital signal, 15 | 16 | Read-Modify-Write 17 | ----------------- 18 | 19 | Port vs Latch 20 | 21 | Via the augmented assignment operations, pylibftdi ``BitBangDevice`` instances support read-modify-write operations, such as arithmetic (``+=`` etc), bitwise (``&=``), and other logical operations such as shift (``<<=``) 22 | 23 | Examples 24 | ~~~~~~~~ 25 | 26 | :: 27 | 28 | >>> from pylibftdi import BitBangDevice 29 | >>> 30 | >>> with BitBangDevice('FTE00P4L') as bb: 31 | ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) 32 | ... bb.port |= 2 # set bit 1 33 | ... bb.port &= 0xFE # clear bit 0 34 | 35 | 36 | >>> with BitBangDevice() as bb: 37 | ... bb.port = 1 38 | ... while True: 39 | ... # Rotate the value in bb.port 40 | ... bb.port = ((bb.port << 1) | ((bb.port >> 8) & 1)) & 0xFF 41 | ... time.sleep(1) 42 | 43 | 44 | The `Bus` class 45 | --------------- 46 | 47 | Dealing with bit masks and shifts gets messy quickly. Some languages such as C and C++ provide direct support for accessing bits - or series of consecutive bits - with bitfields. The ``Bus`` class provides the facility to provide a similar level of support to pylibftdi ``BitBangDevice`` classes. 48 | 49 | As an example, consider an HD44780 LCD display. These have a data channel of either 4 or 8 bits, and a number of additional status lines - ``rs`` which acts as a register select pin - indicating whether a data byte is a command (0) or data (1), and ``e`` - clock enable.:: 50 | 51 | class LCD(object): 52 | """ 53 | The UM232R/245R is wired to the LCD as follows: 54 | DB0..3 to LCD D4..D7 (pin 11..pin 14) 55 | DB6 to LCD 'RS' (pin 4) 56 | DB7 to LCD 'E' (pin 6) 57 | """ 58 | data = Bus(0, 4) 59 | rs = Bus(6) 60 | e = Bus(7) 61 | 62 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/lcd.py: -------------------------------------------------------------------------------- 1 | """ 2 | Write a string (argv[1] if run from command line) to a HD44780 3 | LCD module connected via a FTDI UM232R/245R module using pylibftdi 4 | 5 | example usage: 6 | 7 | # while true; 8 | > do python lcd.py $( awk '{print $1}' /proc/loadavg); 9 | > sleep 5; 10 | > done 11 | 12 | Copyright (c) 2010-2014 Ben Bass 13 | All rights reserved. 14 | """ 15 | 16 | from pylibftdi import BitBangDevice, Bus 17 | 18 | 19 | class LCD: 20 | """ 21 | The UM232R/245R is wired to the LCD as follows: 22 | DB0..3 to LCD D4..D7 (pin 11..pin 14) 23 | DB6 to LCD 'RS' (pin 4) 24 | DB7 to LCD 'E' (pin 6) 25 | """ 26 | 27 | data = Bus(0, 4) 28 | rs = Bus(6) 29 | e = Bus(7) 30 | 31 | def __init__(self, device): 32 | # The Bus descriptor assumes we have a 'device' 33 | # attribute which provides a port 34 | self.device = device 35 | 36 | def _trigger(self): 37 | """generate a falling edge""" 38 | self.e = 1 39 | self.e = 0 40 | 41 | def init_four_bit(self): 42 | """ 43 | set the LCD's 4 bit mode, since we only have 44 | 8 data lines and need at least 2 to strobe 45 | data into the module and select between data 46 | and commands. 47 | """ 48 | self.rs = 0 49 | self.data = 3 50 | for _ in range(3): 51 | self._trigger() 52 | self.data = 2 53 | self._trigger() 54 | 55 | def _write_raw(self, rs, x): 56 | # rs determines whether this is a command 57 | # or a data byte. Write the data as two 58 | # nibbles. Ahhh... nibbles. QBasic anyone? 59 | self.rs = rs 60 | self.data = x >> 4 61 | self._trigger() 62 | self.data = x & 0x0F 63 | self._trigger() 64 | 65 | def write_cmd(self, x): 66 | self._write_raw(0, x) 67 | 68 | def write_data(self, x): 69 | self._write_raw(1, x) 70 | 71 | 72 | def display(string, device_id=None): 73 | """ 74 | Display the given string on an attached LCD 75 | an optional `device_id` can be given. 76 | """ 77 | with BitBangDevice(device_id) as bb: 78 | # These LCDs are quite slow - and the actual baudrate 79 | # is 16x this in bitbang mode... 80 | bb.baudrate = 60 81 | 82 | lcd = LCD(bb) 83 | lcd.init_four_bit() 84 | 85 | # 001xxxxx - function set 86 | lcd.write_cmd(0x20) 87 | # 00000001 - clear display 88 | lcd.write_cmd(0x01) 89 | # 000001xx - entry mode set 90 | # bit 1: inc(1)/dec(0) 91 | # bit 0: shift display 92 | lcd.write_cmd(0x06) 93 | # 00001xxx - display config 94 | # bit 2: display on 95 | # bit 1: display cursor 96 | # bit 0: blinking cursor 97 | lcd.write_cmd(0x0C) 98 | 99 | for i in string: 100 | lcd.write_data(ord(i)) 101 | 102 | 103 | if __name__ == "__main__": 104 | import sys 105 | 106 | if len(sys.argv) == 2: 107 | display(sys.argv[1]) 108 | else: 109 | print("Usage: %s 'display string'") 110 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | This module contains some basic tests for the higher-level 10 | functionality without requiring an actual hardware device 11 | to be attached. 12 | """ 13 | 14 | import gc 15 | import logging 16 | import sys 17 | 18 | from tests.call_log import CallLog 19 | 20 | 21 | class SimpleMock: 22 | """ 23 | This is a simple mock plugin for fdll which logs any calls 24 | made through it to fn_log, which is currently rather ugly 25 | global state. 26 | """ 27 | 28 | def __init__(self, name=""): 29 | self.__name = name 30 | 31 | def __getattr__(self, key): 32 | return SimpleMock(key) 33 | 34 | def __call__(self, *o, **k): 35 | CallLog.append(self.__name) 36 | logging.debug(f"{self.__name}(*{o}, **{k})") 37 | return 0 38 | 39 | 40 | class CallCheckMixin: 41 | """ 42 | this should be used as a mixin for unittest.TestCase classes, 43 | where it allows the calls through the MockDriver to be checked 44 | this does not support multi-threading. 45 | """ 46 | 47 | def setUp(self): 48 | super().setUp() 49 | 50 | def assertCalls(self, fn, methodname): 51 | CallLog.reset() 52 | fn() 53 | self.assertIn(methodname, CallLog.get()) 54 | 55 | def assertNotCalls(self, fn, methodname): 56 | CallLog.reset() 57 | fn() 58 | self.assertNotIn(methodname, CallLog.get()) 59 | 60 | def assertCallsExact(self, fn, call_list): 61 | # ensure any pending Device.__del__ calls get triggered before running fn 62 | gc.collect() 63 | CallLog.reset() 64 | fn() 65 | self.assertEqual(call_list, CallLog.get()) 66 | 67 | 68 | import pylibftdi.driver # noqa 69 | 70 | 71 | # monkey patch the Driver class to be the mock thing above. 72 | class MockDriver: 73 | def __init__(self, *o, **k): 74 | self.fdll = SimpleMock() 75 | 76 | def libftdi_version(self): 77 | return pylibftdi.driver.libftdi_version(1, 2, 3, 0, 0) 78 | 79 | 80 | # importing this _does_ things... 81 | pylibftdi.device.Driver = MockDriver # type: ignore 82 | 83 | from pylibftdi.device import Device # noqa 84 | 85 | 86 | class LoopDevice(Device): 87 | """ 88 | a mock device object which overrides read and write 89 | to operate as an unbounded loopback pair 90 | """ 91 | 92 | def __init__(self, *o, **k): 93 | super().__init__(*o, **k) 94 | self.__buffer = [] 95 | 96 | def _read(self, size): 97 | super()._read(size) # discard result 98 | result = bytes(bytearray(self.__buffer[:size])) 99 | self.__buffer = self.__buffer[size:] 100 | return result 101 | 102 | def _write(self, data): 103 | super()._write(data) # discard result 104 | self.__buffer.extend(bytearray(data)) 105 | return len(data) 106 | 107 | 108 | verbose = {"-v", "--verbose"} & set(sys.argv) 109 | logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO) 110 | -------------------------------------------------------------------------------- /src/pylibftdi/serial_device.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | """ 10 | 11 | from ctypes import byref, c_uint16 12 | 13 | from pylibftdi.device import Device 14 | 15 | CTS_MASK = 1 << 4 16 | DSR_MASK = 1 << 5 17 | RI_MASK = 1 << 6 18 | 19 | 20 | class SerialDevice(Device): 21 | """ 22 | simple subclass to support serial(rs232) lines 23 | 24 | cts, dsr, ri - input 25 | dtr, rts - output 26 | modem_status - return a two byte bitfield of various values 27 | 28 | Note: These lines are all active-low by default, though this can be 29 | changed in the EEPROM settings. pylibftdi does not attempt to hide 30 | these settings, and simply writes out the given values (i.e. '1' 31 | will typically make an output line 'active' - and therefore low) 32 | """ 33 | 34 | _dtr = None 35 | _rts = None 36 | 37 | @property 38 | def dtr(self): 39 | """ 40 | set (or get the previous set) state of the DTR line 41 | 42 | :return: the state of the DTR line; None if not previously set 43 | """ 44 | return self._dtr 45 | 46 | @dtr.setter 47 | def dtr(self, value): 48 | value &= 1 49 | if value != self._dtr: 50 | self.ftdi_fn.ftdi_setdtr(value) 51 | 52 | self._dtr = value 53 | 54 | @property 55 | def rts(self): 56 | """ 57 | set (or get the previous set) state of the RTS line 58 | 59 | :return: the state of the RTS line; None if not previously set 60 | """ 61 | return self._rts 62 | 63 | @rts.setter 64 | def rts(self, value): 65 | value &= 1 66 | if value != self._rts: 67 | self.ftdi_fn.ftdi_setrts(value) 68 | 69 | self._rts = value 70 | 71 | @property 72 | def modem_status(self): 73 | """ 74 | Layout of the first byte: 75 | B0..B3 - must be 0 76 | B4 Clear to send (CTS) 0 = inactive 1 = active 77 | B5 Data set ready (DTS) 0 = inactive 1 = active 78 | B6 Ring indicator (RI) 0 = inactive 1 = active 79 | B7 Receive line signal detect (RLSD) 0 = inactive 1 = active 80 | 81 | Layout of the second byte: 82 | B0 Data ready (DR) 83 | B1 Overrun error (OE) 84 | B2 Parity error (PE) 85 | B3 Framing error (FE) 86 | B4 Break interrupt (BI) 87 | B5 Transmitter holding register (THRE) 88 | B6 Transmitter empty (TEMT) 89 | B7 Error in RCVR FIFO 90 | 91 | '{:016b}'.format(d.modem_status) 92 | '0110000000000001' 93 | - b5,b6 set in MSB ('2nd byte'), b0 set in first byte 94 | (despite the libftdi docs saying this shouldn't be set) 95 | """ 96 | status = c_uint16() 97 | self.ftdi_fn.ftdi_poll_modem_status(byref(status)) 98 | return status.value 99 | 100 | @property 101 | def cts(self): 102 | """ 103 | get the state of CTS (1 = 'active') 104 | """ 105 | return int(bool(self.modem_status & CTS_MASK)) 106 | 107 | @property 108 | def dsr(self): 109 | """ 110 | get the state of DSR (1 = 'active') 111 | """ 112 | return int(bool(self.modem_status & DSR_MASK)) 113 | 114 | @property 115 | def ri(self): 116 | """ 117 | get the state of RI (1 = 'active') 118 | """ 119 | return int(bool(self.modem_status & RI_MASK)) 120 | -------------------------------------------------------------------------------- /docs/serial.rst: -------------------------------------------------------------------------------- 1 | Serial mode 2 | =========== 3 | 4 | The default mode of pylibftdi devices is to behave as a serial UART device, similar to the 'COM1' device found on older PCs. Nowadays most PCs operate with serial devices over USB-serial adapters, which may often include their own FTDI chips. To remain compatible with the RS232 standard however, these adapters will often include level-shifting circuitry which is of no benefit in communicating with other circuits operating at the 3.3 or 5 volt levels the FTDI hardware uses. 5 | 6 | The default serial configuration is 9600 baud, 8 data bits, 1 stop bit and no parity (sometimes referred to as 8-N-1_). This is the default configuration of the old 'COM' devices back to the days of the original IBM PC and MS-DOS. 7 | 8 | .. _8-N-1: http://en.wikipedia.org/wiki/8-N-1 9 | 10 | 11 | Setting line parameters 12 | ----------------------- 13 | 14 | Changing line parameters other than the baudrate is supported via use of the underlying FTDI function calls. 15 | 16 | The SerialDevice class 17 | ---------------------- 18 | 19 | While the standard ``Device`` class supports standard ``read`` and ``write`` methods, as well as a ``baudrate`` property, further functionality is provided by the ``SerialDevice`` class, available either as a top-level import from ``pylibftdi`` or through the ``serial_device`` module. This subclasses ``Device`` and adds additional properties to access various control and handshake lines. 20 | 21 | The following properties are available: 22 | 23 | ======== ==================== ========= 24 | property meaning direction 25 | -------- -------------------- --------- 26 | ``cts`` Clear To Send Input 27 | ``rts`` Ready To Send Output 28 | ``dsr`` Data Set Ready Input 29 | ``dtr`` Data Transmit Ready Output 30 | ``ri`` Ring Indicator Input 31 | ======== ==================== ========= 32 | 33 | Note that these lines are normally active-low, and ``pylibftdi`` makes no attempt to hide this from the user. It is impractical to try to 'undo' this inversion in any case, since it can be disabled in the EEPROM settings of the device. Just be aware if using these lines as GPIO that the electrical sense will be the opposite of the value read. The lines are intended to support handshaking rather than GPIO, so this is not normally an issue; if CTS is connected to RTS, then values written to RTS will be reflected in the value read from CTS. 34 | 35 | Subclassing `Device` - A MIDI device 36 | ------------------------------------ 37 | 38 | To abstract application code from the details of any particular interface, it may be helpful to subclass the ``Device`` class, providing the required configuration in the ``__init__`` method to act in a certain way. For example, the MIDI_ protocol used by electronic music devices is an asynchronous serial protocol operating at 31250 baud, and with the same 8-N-1 parameters which pylibftdi defaults to. 39 | 40 | .. _MIDI: http://www.midi.org 41 | 42 | Creating a ``MidiDevice`` subclass of ``Device`` is straightforward:: 43 | 44 | class MidiDevice(Device): 45 | "subclass of pylibftdi.Device configured for MIDI" 46 | 47 | def __init__(self, *o, **k): 48 | Device.__init__(self, *o, **k) 49 | self.baudrate = 31250 50 | 51 | Note it is important that the superclass ``__init__`` is called first; calling it on an uninitialised ``Device`` would fail, and even if it succeeded, the superclass ``__init__`` method resets ``baudrate`` to 9600 anyway to ensure a consistent setup for devices which may have been previously used with different parameters. 52 | 53 | Use of the ``MidiDevice`` class is simple - as a pylibftdi Device instance, it provides a file-based API. Simply ``read()`` and ``write()`` the data to an instance of the class:: 54 | 55 | >>> m = MidiDevice() 56 | >>> m.write('\x90\x80\x80') 57 | >>> time.sleep(1) 58 | >>> m.write('\x80\x00') 59 | 60 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/bit_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | bit_server.py - remote HTTP interface to bit-bangged FTDI port 3 | This runs as a web server, connect to port 8008 4 | 5 | Change HTTP_PORT for different port number or supply alternate as args[1] 6 | 7 | Requires: 8 | - pylibftdi 9 | """ 10 | 11 | import sys 12 | import threading 13 | import time 14 | import webbrowser 15 | from http.server import BaseHTTPRequestHandler, HTTPServer 16 | from io import BytesIO 17 | from socketserver import ThreadingMixIn 18 | 19 | from pylibftdi import BitBangDevice 20 | 21 | HTTP_PORT = 8008 22 | 23 | 24 | class ThreadingServer(ThreadingMixIn, HTTPServer): 25 | pass 26 | 27 | 28 | def get_page(): 29 | port = switch.port 30 | page = f""" 31 | 32 | 33 | 34 | {port} - pylibftdi 35 | 36 | 37 |
38 | """ 39 | for idx in range(8): 40 | bit = 7 - idx 41 | is_on = port & (1 << bit) 42 | color = "#00FF00" if is_on else "#FF0000" 43 | page += f""" 44 |
46 |
47 | 50 | 51 |
52 |
""" 53 | page += f""" 54 |
55 | DATA={port} 56 | 57 | 58 | """ 59 | return page 60 | 61 | 62 | class ReqHandler(BaseHTTPRequestHandler): 63 | def do_GET(self): 64 | f = self.send_head() 65 | if f: 66 | self.wfile.write(f.read()) 67 | f.close() 68 | 69 | def do_POST(self): 70 | length = self.headers["content-length"] 71 | nbytes = int(length) 72 | query = self.rfile.read(nbytes).decode() 73 | # this is lazy and fragile - assumes only a single 74 | # query parameter XXX 75 | if query.startswith("bit"): 76 | bit = int(query[3]) 77 | value = 1 if query.rsplit("=", 1)[-1] == "true" else 0 78 | if value: 79 | switch.port |= 1 << bit 80 | else: 81 | switch.port &= 255 ^ (1 << bit) 82 | 83 | f = self.send_head() 84 | if f: 85 | self.wfile.write(f.read()) 86 | f.close() 87 | 88 | def send_head(self): 89 | f = BytesIO() 90 | f.write(get_page().encode()) 91 | length = f.tell() 92 | f.seek(0) 93 | self.send_response(200) 94 | self.send_header("Content-type", "text/html") 95 | self.send_header("Content-Length", str(length)) 96 | self.end_headers() 97 | return f 98 | 99 | 100 | def runserver(port=HTTP_PORT): 101 | serveraddr = ("", port) 102 | srvr = ThreadingServer(serveraddr, ReqHandler) 103 | srvr.serve_forever() 104 | 105 | 106 | if __name__ == "__main__": 107 | switch = BitBangDevice() 108 | 109 | try: 110 | HTTP_PORT = int(sys.argv[1]) 111 | except (ValueError, TypeError): 112 | print("Usage: FtdiWebServer [portnumber]") 113 | except IndexError: 114 | pass 115 | 116 | t = threading.Thread(target=runserver, args=(HTTP_PORT,)) 117 | t.setDaemon(True) 118 | t.start() 119 | print("Webserver running on localhost port %d" % HTTP_PORT) 120 | time.sleep(0.5) 121 | retry = 10 122 | while retry: 123 | try: 124 | webbrowser.open("http://localhost:%d" % HTTP_PORT) 125 | except OSError: 126 | time.sleep(1) 127 | retry -= 1 128 | else: 129 | break 130 | 131 | # wait for Ctrl-C 132 | try: 133 | while 1: 134 | time.sleep(100) 135 | except KeyboardInterrupt: 136 | pass 137 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pylibftdi 2 | ========= 3 | 4 | pylibftdi is a minimal Pythonic interface to FTDI devices using libftdi_. 5 | 6 | .. _libftdi: http://www.intra2net.com/en/developer/libftdi/ 7 | 8 | :Features: 9 | 10 | - No dependencies beyond standard library and a `libftdi` install. 11 | - Supports parallel and serial devices 12 | - Support for multiple devices 13 | - File-like interface wherever appropriate 14 | - Cross-platform 15 | 16 | :Limitations: 17 | 18 | - The API might change prior to reaching a 1.0 release. 19 | 20 | Usage 21 | ----- 22 | 23 | The primary interface is the ``Device`` class in the pylibftdi package; this 24 | gives serial access on relevant FTDI devices (e.g. the UM232R), providing a 25 | file-like interface (read, write). Baudrate is controlled with the ``baudrate`` 26 | property. 27 | 28 | If a Device instance is created with ``mode='t'`` (text mode) then read() and 29 | write() can use the given ``encoding`` (defaulting to latin-1). This allows 30 | easier integration with passing unicode strings between devices. 31 | 32 | Multiple devices are supported by passing the desired device serial number (as 33 | a string) in the ``device_id`` parameter - this is the first parameter in both 34 | Device() and BitBangDevice() constructors. Alternatively the device 'description' 35 | can be given, and an attempt will be made to match this if matching by serial 36 | number fails. 37 | 38 | Examples 39 | ~~~~~~~~ 40 | 41 | :: 42 | 43 | >>> from pylibftdi import Device 44 | >>> 45 | >>> with Device(mode='t') as dev: 46 | ... dev.baudrate = 115200 47 | ... dev.write('Hello World') 48 | 49 | The pylibftdi.BitBangDevice wrapper provides access to the parallel IO mode of 50 | operation through the ``port`` and ``direction`` properties. These provide an 51 | 8 bit IO port including all the relevant bit operations to make things simple. 52 | 53 | :: 54 | 55 | >>> from pylibftdi import BitBangDevice 56 | >>> 57 | >>> with BitBangDevice('FTE00P4L') as bb: 58 | ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) 59 | ... bb.port |= 2 # set bit 1 60 | ... bb.port &= 0xFE # clear bit 0 61 | 62 | There is support for a number of external devices and protocols, including 63 | interfacing with HD44780 LCDs using the 4-bit interface. 64 | 65 | History & Motivation 66 | -------------------- 67 | This package is the result of various bits of work using FTDI's 68 | devices, primarily for controlling external devices. Some of this 69 | is documented on the codedstructure blog, codedstructure.blogspot.com 70 | 71 | Several other open-source Python FTDI wrappers exist, and each may be 72 | best for some projects. Some aim at closely wrapping the libftdi interface, 73 | others use FTDI's own D2XX driver (ftd2xx_) or talk directly to USB via 74 | libusb or similar (such as pyftdi_). 75 | 76 | .. _ftd2xx: http://pypi.python.org/pypi/ftd2xx 77 | .. _pyftdi: https://github.com/eblot/pyftdi 78 | 79 | The aim for pylibftdi is to work with libftdi, but to provide 80 | a high-level Pythonic interface. Various wrappers and utility 81 | functions are also part of the distribution; following Python's 82 | batteries included approach, there are various interesting devices 83 | supported out-of-the-box - or at least there will be soon! 84 | 85 | Plans 86 | ----- 87 | * Add more examples: SPI devices, knight-rider effects, input devices, MIDI... 88 | * Perhaps add support for D2XX driver, though the name then becomes a 89 | slight liability ;) 90 | 91 | License 92 | ------- 93 | 94 | Copyright (c) 2010-2024 Ben Bass 95 | 96 | pylibftdi is released under the MIT licence; see the file "LICENSE.txt" 97 | for information. 98 | 99 | All trademarks referenced herein are property of their respective 100 | holders. 101 | libFTDI itself is developed by Intra2net AG. No association with 102 | Intra2net is claimed or implied, but I have found their library 103 | helpful and had fun with it... 104 | 105 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | pylibftdi is a minimal Pythonic interface to FTDI devices using libftdi_. 5 | 6 | .. _libftdi: http://www.intra2net.com/en/developer/libftdi/ 7 | 8 | :Features: 9 | 10 | - No dependencies beyond standard library and a `libftdi` install. 11 | - Supports parallel and serial devices 12 | - Support for multiple devices 13 | - File-like interface wherever appropriate 14 | - Cross-platform 15 | 16 | :Limitations: 17 | 18 | - The API might change prior to reaching a 1.0 release. 19 | 20 | Usage 21 | ----- 22 | 23 | The primary interface is the ``Device`` class in the pylibftdi package; this 24 | gives serial access on relevant FTDI devices (e.g. the UM232R), providing a 25 | file-like interface (read, write). Baudrate is controlled with the ``baudrate`` 26 | property. 27 | 28 | If a Device instance is created with ``mode='t'`` (text mode) then read() and 29 | write() can use the given ``encoding`` (defaulting to latin-1). This allows 30 | easier integration with passing unicode strings between devices. 31 | 32 | Multiple devices are supported by passing the desired device serial number (as 33 | a string) in the ``device_id`` parameter - this is the first parameter in both 34 | Device() and BitBangDevice() constructors. Alternatively the device 'description' 35 | can be given, and an attempt will be made to match this if matching by serial 36 | number fails. 37 | 38 | Examples 39 | ~~~~~~~~ 40 | 41 | :: 42 | 43 | >>> from pylibftdi import Device 44 | >>> 45 | >>> with Device(mode='t') as dev: 46 | ... dev.baudrate = 115200 47 | ... dev.write('Hello World') 48 | 49 | The pylibftdi.BitBangDevice wrapper provides access to the parallel IO mode of 50 | operation through the ``port`` and ``direction`` properties. These provide an 51 | 8 bit IO port including all the relevant bit operations to make things simple. 52 | 53 | :: 54 | 55 | >>> from pylibftdi import BitBangDevice 56 | >>> 57 | >>> with BitBangDevice('FTE00P4L') as bb: 58 | ... bb.direction = 0x0F # four LSB are output(1), four MSB are input(0) 59 | ... bb.port |= 2 # set bit 1 60 | ... bb.port &= 0xFE # clear bit 0 61 | 62 | There is support for a number of external devices and protocols, including 63 | interfacing with HD44780 LCDs using the 4-bit interface. 64 | 65 | History & Motivation 66 | -------------------- 67 | This package is the result of various bits of work using FTDI's 68 | devices, primarily for controlling external devices. Some of this 69 | is documented on the codedstructure blog, codedstructure.blogspot.com 70 | 71 | Several other open-source Python FTDI wrappers exist, and each may be 72 | best for some projects. Some aim at closely wrapping the libftdi interface, 73 | others use FTDI's own D2XX driver (ftd2xx_) or talk directly to USB via 74 | libusb or similar (such as pyftdi_). 75 | 76 | .. _ftd2xx: http://pypi.python.org/pypi/ftd2xx 77 | .. _pyftdi: https://github.com/eblot/pyftdi 78 | 79 | The aim for pylibftdi is to work with libftdi, but to provide 80 | a high-level Pythonic interface. Various wrappers and utility 81 | functions are also part of the distribution; following Python's 82 | batteries included approach, there are various interesting devices 83 | supported out-of-the-box - or at least there will be soon! 84 | 85 | Plans 86 | ----- 87 | * Add more examples: SPI devices, knight-rider effects, input devices, MIDI... 88 | * Perhaps add support for D2XX driver, though the name then becomes a 89 | slight liability ;) 90 | 91 | License 92 | ------- 93 | 94 | Copyright (c) 2010-2023 Ben Bass 95 | 96 | pylibftdi is released under the MIT licence; see the file "LICENSE.txt" 97 | for information. 98 | 99 | All trademarks referenced herein are property of their respective 100 | holders. 101 | libFTDI itself is developed by Intra2net AG. No association with 102 | Intra2net is claimed or implied, but I have found their library 103 | helpful and had fun with it... 104 | 105 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/pin_read.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Display values on input pins of a BitBangDevice. 4 | 5 | TODO: 6 | * ANSI colours / display differences in bold 7 | 8 | example - beep on pin 1 going high: 9 | $ pylibftdi/examples/pin_read.py -n 0.01 -m 1 -k 1 && beep 10 | 11 | Copyright (c) 2011-2014 Ben Bass 12 | All rights reserved. 13 | """ 14 | 15 | import itertools 16 | import sys 17 | import time 18 | 19 | from pylibftdi import ALL_INPUTS, BitBangDevice 20 | 21 | 22 | def get_value(): 23 | """ 24 | get the value of the pins 25 | """ 26 | if not getattr(get_value, "dev", None): 27 | get_value.dev = BitBangDevice(direction=ALL_INPUTS) 28 | dev = get_value.dev 29 | return dev.port 30 | 31 | 32 | def display_value(value): 33 | """ 34 | display the given value 35 | """ 36 | sys.stdout.write("\b" * 32) 37 | for n in range(8): 38 | sys.stdout.write("1 " if value & (1 << (7 - n)) else "0 ") 39 | sys.stdout.write(" (%d/0x%02X)" % (value, value)) 40 | sys.stdout.flush() 41 | 42 | 43 | def display_loop(interval=1, count=0, match=None, mask=0xFF): 44 | """ 45 | display and compare the value 46 | 47 | :param interval: polling interval in seconds 48 | :param count: number of polls to do, or infinite if 0 49 | :param match: value to look for to exit early 50 | :param mask: mask of read value before comparing to match 51 | :return: 'ok'. either a match was made or none was requested 52 | :rtype: bool 53 | """ 54 | if not count: 55 | count_iter = itertools.count() 56 | else: 57 | count_iter = range(count) 58 | 59 | try: 60 | for _ in count_iter: 61 | value = get_value() 62 | display_value(value) 63 | if match is not None and (value & mask == match): 64 | return True 65 | time.sleep(interval) 66 | return False 67 | except KeyboardInterrupt: 68 | pass 69 | finally: 70 | sys.stdout.write("\n") 71 | 72 | if match is not None: 73 | # we quit early while looking for a match 74 | return False 75 | else: 76 | # no match to do; no problem. 77 | return True 78 | 79 | 80 | def main(args=None): 81 | import argparse 82 | 83 | parser = argparse.ArgumentParser() 84 | parser.add_argument( 85 | "-n", 86 | "--interval", 87 | dest="interval", 88 | default=1, 89 | type=float, 90 | help="refresh interval, default 1 second", 91 | ) 92 | parser.add_argument( 93 | "-c", 94 | "--count", 95 | dest="count", 96 | default=0, 97 | type=int, 98 | help="number of cycles to run for (0 = no limit - the default)", 99 | ) 100 | parser.add_argument( 101 | "-m", "--match", dest="match", help="value to match against (e.g. 0x1F, 7, etc)" 102 | ) 103 | parser.add_argument( 104 | "-k", 105 | "--mask", 106 | dest="mask", 107 | help="mask to match with (e.g. 0x07, 2, etc) - default 0xFF", 108 | ) 109 | args = parser.parse_args(args) 110 | 111 | if args.interval < 0.001: 112 | parser.error("interval must be >= 0.001") 113 | if args.count < 0: 114 | parser.error("count must be >= 0") 115 | mask = match = None 116 | if args.mask: 117 | if not args.match: 118 | parser.error("Must specify --match with mask") 119 | try: 120 | mask = int(args.mask, 0) 121 | except ValueError: 122 | parser.error("Could not interpret given mask") 123 | else: 124 | mask = 0xFF 125 | if args.match: 126 | try: 127 | match = int(args.match, 0) 128 | except ValueError: 129 | parser.error("Could not interpret given mask") 130 | ok = display_loop(args.interval, args.count, match, mask) 131 | sys.exit(0 if ok else 1) 132 | 133 | 134 | if __name__ == "__main__": 135 | main() 136 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pylibftdi documentation build configuration file, created by 2 | # sphinx-quickstart on Mon Oct 31 22:36:38 2011. 3 | # 4 | # This file is execfile()d with the current directory set to its containing dir. 5 | # 6 | # Note that not all possible configuration values are present in this 7 | # autogenerated file. 8 | # 9 | # All configuration values have a default; values that are commented out 10 | # serve to show the default. 11 | 12 | import os 13 | import sys 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # sys.path.insert(0, os.path.abspath('.')) 19 | sys.path.insert(0, os.path.abspath("../src")) 20 | 21 | extensions = [ 22 | "sphinx.ext.autodoc", 23 | "sphinx.ext.viewcode", 24 | "sphinx_rtd_theme", 25 | ] 26 | 27 | # Sort members by type 28 | autodoc_member_order = "groupwise" 29 | 30 | 31 | # Ensure that the __init__ method gets documented. 32 | def skip(app, what, name, obj, skip, options): 33 | if name == "__init__": 34 | return False 35 | return skip 36 | 37 | 38 | def setup(app): 39 | app.connect("autodoc-skip-member", skip) 40 | 41 | 42 | # The suffix of source filenames. 43 | source_suffix = ".rst" 44 | 45 | # The master toctree document. 46 | master_doc = "index" 47 | 48 | # General information about the project. 49 | project = "pylibftdi" 50 | copyright = "2010-2024, Ben Bass" 51 | 52 | # The version info for the project you're documenting, acts as replacement for 53 | # |version| and |release|, also used in various other places throughout the 54 | # built documents. 55 | # 56 | # The short X.Y version. 57 | version = "0.23.0" 58 | # The full version, including alpha/beta/rc tags. 59 | release = "0.23.0" 60 | 61 | # List of patterns, relative to source directory, that match files and 62 | # directories to ignore when looking for source files. 63 | exclude_patterns = ["_build"] 64 | 65 | # The name of the Pygments (syntax highlighting) style to use. 66 | pygments_style = "sphinx" 67 | 68 | 69 | # -- Options for HTML output --------------------------------------------------- 70 | 71 | # The theme to use for HTML and HTML Help pages. See the documentation for 72 | # a list of builtin themes. 73 | # html_theme = "default" 74 | html_theme = "sphinx_rtd_theme" 75 | # html_theme_path = ["_themes", ] 76 | # html_static_path = ['_static'] 77 | 78 | # Use new readthedocs theme 79 | RTD_NEW_THEME = True 80 | 81 | # Output file base name for HTML help builder. 82 | htmlhelp_basename = "pylibftdidoc" 83 | 84 | 85 | # -- Options for LaTeX output -------------------------------------------------- 86 | 87 | latex_elements = { 88 | # The paper size ('letterpaper' or 'a4paper'). 89 | "papersize": "a4paper", 90 | # The font size ('10pt', '11pt' or '12pt'). 91 | "pointsize": "11pt", 92 | # Additional stuff for the LaTeX preamble. 93 | #'preamble': '', 94 | } 95 | 96 | # Support printed manuals better 97 | latex_show_urls = "footnote" 98 | latex_show_pagerefs = True 99 | 100 | # Grouping the document tree into LaTeX files. List of tuples 101 | # (source start file, target name, title, author, documentclass [howto/manual]). 102 | latex_documents = [ 103 | ("index", "pylibftdi.tex", "pylibftdi Documentation", "Ben Bass", "manual"), 104 | ] 105 | 106 | # -- Options for manual page output -------------------------------------------- 107 | 108 | # One entry per manual page. List of tuples 109 | # (source start file, name, description, authors, manual section). 110 | man_pages = [("index", "pylibftdi", "pylibftdi Documentation", ["Ben Bass"], 1)] 111 | 112 | # -- Options for Texinfo output ------------------------------------------------ 113 | 114 | # Grouping the document tree into Texinfo files. List of tuples 115 | # (source start file, target name, title, author, 116 | # dir menu entry, description, category) 117 | texinfo_documents = [ 118 | ( 119 | "index", 120 | "pylibftdi", 121 | "pylibftdi Documentation", 122 | "Ben Bass", 123 | "pylibftdi", 124 | "One line description of project.", 125 | "Miscellaneous", 126 | ), 127 | ] 128 | -------------------------------------------------------------------------------- /docs/developing.rst: -------------------------------------------------------------------------------- 1 | Developing pylibftdi 2 | -------------------- 3 | 4 | How do I checkout and use the latest development version? 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | `pylibftdi` is currently developed on GitHub, though started out as a Mercurial 8 | repository on bitbucket.org. There may still be references to old bitbucket issues 9 | in the docs. 10 | 11 | `pylibftdi` is developed using poetry_, and a Dockerfile plus Makefile make use 12 | development tasks straightforward. In any case, start with a local clone of the 13 | repository:: 14 | 15 | $ git clone https://github.com/codedstructure/pylibftdi 16 | $ cd pylibftdi 17 | 18 | .. _poetry: https://python-poetry.org/ 19 | 20 | There are then two main approaches, though pick and mix the different elements to suit: 21 | 22 | **poetry and docker** 23 | If `make` and `docker` are available in your environment, the easiest way to do development 24 | may be to simply run `make shell`. This creates an Ubuntu-based docker environment with 25 | `libftdi`, `poetry`, and other requirements pre-installed, and drops into a shell where the 26 | current `pylibftdi` code is installed. 27 | 28 | `make` on its own will run through all the unittests and linting available for `pylibftdi`, 29 | and is a useful check to make sure things haven't been broken. 30 | 31 | The downside of running in a docker container is that USB support to actual FTDI devices 32 | may be lacking... 33 | 34 | **editable install with pip** 35 | This assumes that the `venv` and `pip` packages are installed; on some (e.g. Ubuntu) 36 | Linux environments, these may need installing as OS packages. Once installed, perform 37 | an 'editable' install as follows:: 38 | 39 | .../pylibftdi$ python3 -m venv env 40 | .../pylibftdi$ source env/bin/activate 41 | (env) .../pylibftdi$ python3 -m pip install -e . 42 | 43 | Note this also creates a virtual environment within the project directory; 44 | see here_ 45 | 46 | .. _here: https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/ 47 | 48 | How do I run the tests? 49 | ~~~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | From the root directory of a cloned pylibftdi repository, run the following:: 52 | 53 | (env) .../pylibftdi$ python3 -m unittest discover 54 | ..................................... 55 | ---------------------------------------------------------------------- 56 | Ran 37 tests in 0.038s 57 | 58 | OK 59 | 60 | Note that other test runners (such as `pytest`) will also run the tests and may be 61 | easier to extend. 62 | 63 | How can I determine and select the underlying libftdi library? 64 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 65 | 66 | Since pylibftdi 0.12, the Driver exposes ``libftdi_version()`` and ``libusb_version()`` 67 | methods, which return a tuple whose first three entries correspond to major, minor, 68 | and micro versions of the libftdi driver being used. 69 | 70 | Note there are two major versions of `libftdi` - libftdi1 can coexist with 71 | the earlier 0.x versions - it is now possible to select which library to 72 | load when instantiating the Driver. Note on at least Ubuntu Linux, the `libftdi1` 73 | *OS package* actually refers to `libftdi 0.20` (or similar), whereas `libftdi1-2` 74 | refers to the more recent 1.x release (currently 1.5):: 75 | 76 | Python 3.10.6 (main, May 29 2023, 11:10:38) [GCC 11.3.0] on linux 77 | Type "help", "copyright", "credits" or "license" for more information. 78 | >>> from pylibftdi import Driver 79 | >>> Driver().libftdi_version() 80 | libftdi_version(major=1, minor=5, micro=0, version_str='1.5', snapshot_str='unknown') 81 | >>> Driver("ftdi1").libftdi_version() 82 | libftdi_version(major=1, minor=5, micro=0, version_str='1.5', snapshot_str='unknown') 83 | >>> Driver("ftdi").libftdi_version() 84 | libftdi_version(major=0, minor=0, micro=0, version_str='< 1.0 - no ftdi_get_library_version()', snapshot_str='unknown') 85 | 86 | If both are installed, ``pylibftdi`` prefers libftdi1 (e.g. libftdi 1.5) over libftdi (e.g. 0.20). 87 | Since different OSs require different parameters to be given to find a library, 88 | the default search list given to ctypes.util.find_library is defined by the 89 | `Driver._lib_search` attribute, and this may be updated as appropriate. 90 | By default it is as follows:: 91 | 92 | _lib_search = { 93 | "libftdi": ["ftdi1", "libftdi1", "ftdi", "libftdi"], 94 | "libusb": ["usb-1.0", "libusb-1.0"], 95 | } 96 | 97 | This covers Windows (which requires the 'lib' prefix), Linux (which requires 98 | its absence), and Mac OS X, which is happy with either. 99 | 100 | -------------------------------------------------------------------------------- /tests/test_driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | This module contains some basic tests for Driver class. 10 | """ 11 | 12 | import unittest 13 | 14 | from pylibftdi import LibraryMissingError 15 | from pylibftdi.driver import Driver 16 | 17 | 18 | class DriverTest(unittest.TestCase): 19 | """ 20 | Test to ensure the Driver class accepts the correct arguments. 21 | """ 22 | 23 | def setUp(self): 24 | """The default library names are stored in the Driver class as a 25 | class variable. This method will run before each unit test to reset 26 | the default library names. If other class variables are added to the 27 | Driver class, they should also be added here. 28 | """ 29 | Driver._lib_search = { 30 | "libftdi": ["ftdi1", "libftdi1", "ftdi", "libftdi"], 31 | "libusb": ["usb-1.0", "libusb-1.0"], 32 | } 33 | 34 | def testNoneLibrary(self): 35 | """ 36 | The Driver class can accept no library names passed in to 37 | the constructor. This uses the default libraries specified in the 38 | Driver class. This is the default and most typical behavior. 39 | """ 40 | driver = Driver(libftdi_search=None) 41 | self.assertListEqual( 42 | driver._lib_search["libftdi"], ["ftdi1", "libftdi1", "ftdi", "libftdi"] 43 | ) 44 | 45 | def testNoExplicitParameters(self): 46 | """ 47 | The Driver class can accept no explicit parameters. Ensures 48 | that libftdi_search is set to None by default. 49 | """ 50 | driver = Driver() 51 | self.assertListEqual( 52 | driver._lib_search["libftdi"], ["ftdi1", "libftdi1", "ftdi", "libftdi"] 53 | ) 54 | 55 | def testStringLibrary(self): 56 | """ 57 | The Driver class can accept a string library name and store the 58 | value in a list with a single element. You might use this when you 59 | know the exact name of the library (perhaps custom). 60 | """ 61 | driver = Driver(libftdi_search="libftdi") 62 | self.assertListEqual(driver._lib_search["libftdi"], ["libftdi"]) 63 | 64 | def testListLibrary(self): 65 | """ 66 | The Driver class can accept a list of library names and store the 67 | values in a list. You might use this to support a limited number of 68 | platforms with different library names. 69 | """ 70 | driver = Driver(libftdi_search=["ftdi1", "libftdi1"]) 71 | self.assertListEqual(driver._lib_search["libftdi"], ["ftdi1", "libftdi1"]) 72 | 73 | def testLoadLibrarySearchListEmpty(self): 74 | """ 75 | If a Driver object calls _load_library where search_list is an empty 76 | list, LibraryMissingError will be raised. 77 | """ 78 | # Use the default library names. 79 | driver = Driver(libftdi_search=None) 80 | # Try and find the library names for libftdi. 81 | with self.assertRaises(expected_exception=LibraryMissingError): 82 | driver._load_library(name="libftdi", search_list=[]) 83 | 84 | def testLoadLibraryMissingLibraryName(self): 85 | """ 86 | If a Driver object calls _load_library with with a name not in the 87 | default library names (Driver._lib_search), LibraryMissingError will 88 | be raised. 89 | """ 90 | driver = Driver(libftdi_search=None) 91 | with self.assertRaises(expected_exception=LibraryMissingError): 92 | driver._load_library(name="non-existent-library", search_list=None) 93 | 94 | def testLoadLibrarySearchListNone(self): 95 | """ 96 | If a Driver object calls _load_library with with a valid name (the key 97 | exists in Driver._lib_search) and search_list is None, the proper 98 | library will be returned. 99 | """ 100 | driver = Driver(libftdi_search=None) 101 | try: 102 | # Assert that Driver can find both of the defaults. 103 | libftdi = driver._load_library(name="libftdi", search_list=None) 104 | libusb = driver._load_library(name="libusb", search_list=None) 105 | self.assertIsNotNone(obj=libftdi, msg="libftdi library not found") 106 | self.assertIsNotNone(obj=libusb, msg="libusb library not found") 107 | except LibraryMissingError: 108 | self.fail("LibraryMissingError raised for default library names.") 109 | 110 | 111 | if __name__ == "__main__": 112 | unittest.main() 113 | -------------------------------------------------------------------------------- /tests/test_device.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | This module contains some basic tests for the higher-level 10 | functionality without requiring an actual hardware device 11 | to be attached. 12 | """ 13 | 14 | import unittest 15 | 16 | from pylibftdi import FtdiError 17 | from pylibftdi.device import Device 18 | from tests.test_common import CallCheckMixin, LoopDevice 19 | 20 | # and now some test cases... 21 | 22 | 23 | class DeviceFunctions(CallCheckMixin, unittest.TestCase): 24 | def testContextManager(self): 25 | def _(): 26 | with Device(): 27 | pass 28 | 29 | self.assertCallsExact( 30 | _, 31 | [ 32 | "ftdi_init", 33 | "ftdi_usb_open_desc_index", 34 | "ftdi_set_bitmode", 35 | "ftdi_setflowctrl", 36 | "ftdi_set_baudrate", 37 | "ftdi_set_latency_timer", 38 | "ftdi_usb_close", 39 | "ftdi_deinit", 40 | ], 41 | ) 42 | 43 | def testOpen(self): 44 | # a lazy_open open() shouldn't do anything 45 | self.assertCallsExact(lambda: Device(lazy_open=True), []) 46 | # a non-lazy_open open() should open the port... 47 | self.assertCalls(lambda: Device(), "ftdi_usb_open_desc_index") 48 | # should be the same with device_id... 49 | self.assertCalls(lambda: Device("bogus"), "ftdi_usb_open_desc_index") 50 | # should be the same with device_id... 51 | self.assertCalls(lambda: Device(device_index=2), "ftdi_usb_open_desc_index") 52 | 53 | def testOpenInterface(self): 54 | self.assertCalls(lambda: Device(interface_select=1), "ftdi_set_interface") 55 | # check that opening a specific interface does that 56 | self.assertNotCalls(lambda: Device(), "ftdi_set_interface") 57 | 58 | def testReadWrite(self): 59 | with Device() as dev: 60 | self.assertCalls(lambda: dev.write("xxx"), "ftdi_write_data") 61 | self.assertCalls(lambda: dev.read(10), "ftdi_read_data") 62 | 63 | def testFlush(self): 64 | with Device() as dev: 65 | self.assertCalls(dev.flush_input, "ftdi_tciflush") 66 | self.assertCalls(dev.flush_output, "ftdi_tcoflush") 67 | self.assertCalls(dev.flush, "ftdi_tcioflush") 68 | 69 | def testClose(self): 70 | d = Device() 71 | d.close() 72 | self.assertRaises(FtdiError, d.write, "hello") 73 | d = Device() 74 | d.close() 75 | self.assertRaises(FtdiError, d.read, 1) 76 | 77 | 78 | class LoopbackTest(unittest.TestCase): 79 | """ 80 | these all require mode='t' 81 | """ 82 | 83 | def testPrint(self): 84 | d = LoopDevice(mode="t") 85 | d.write("Hello") 86 | d.write(" World\n") 87 | d.write("Bye") 88 | self.assertEqual(d.readline(), "Hello World\n") 89 | self.assertEqual(d.readline(), "Bye") 90 | 91 | def testPrintBytes(self): 92 | d = LoopDevice(mode="t") 93 | d.write(b"Hello") 94 | d.write(b" World\n") 95 | d.write(b"Bye") 96 | self.assertEqual(d.readline(), "Hello World\n") 97 | self.assertEqual(d.readline(), "Bye") 98 | 99 | def testLines(self): 100 | d = LoopDevice(mode="t") 101 | lines = ["Hello\n", "World\n", "And\n", "Goodbye\n"] 102 | d.writelines(lines) 103 | self.assertEqual(d.readlines(), lines) 104 | 105 | def testLinesBytes(self): 106 | d = LoopDevice(mode="t") 107 | lines = [b"Hello\n", b"World\n", b"And\n", b"Goodbye\n"] 108 | d.writelines(lines) 109 | self.assertEqual(d.readlines(), [str(line, "ascii") for line in lines]) 110 | 111 | def testIterate(self): 112 | d = LoopDevice(mode="t") 113 | lines = ["Hello\n", "World\n", "And\n", "Goodbye\n"] 114 | d.writelines(lines) 115 | for idx, line in enumerate(d): 116 | self.assertEqual(line, lines[idx]) 117 | 118 | def testBuffer(self): 119 | d = LoopDevice(mode="t", chunk_size=3) 120 | d.write("Hello") 121 | d.write(" World\n") 122 | d.write("Bye") 123 | self.assertEqual(d.readline(), "Hello World\n") 124 | self.assertEqual(d.readline(), "Bye") 125 | 126 | def testReadLineBytes(self): 127 | """ 128 | Device.readline() when in byte mode should raise a TypeError. 129 | This method should only be used in text mode. 130 | """ 131 | d = LoopDevice(mode="b") 132 | d.write(b"Hello\n") 133 | with self.assertRaises(TypeError): 134 | d.readline() 135 | 136 | def testReadLinesBytes(self): 137 | """ 138 | Device.readlines() when in byte mode should raise a TypeError. 139 | This method should only be used in text mode. 140 | """ 141 | d = LoopDevice(mode="b") 142 | d.write(b"Hello\n") 143 | with self.assertRaises(TypeError): 144 | d.readlines() 145 | 146 | 147 | if __name__ == "__main__": 148 | unittest.main() 149 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "black" 3 | version = "24.10.0" 4 | description = "The uncompromising code formatter." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=3.9" 8 | 9 | [package.dependencies] 10 | click = ">=8.0.0" 11 | mypy-extensions = ">=0.4.3" 12 | packaging = ">=22.0" 13 | pathspec = ">=0.9.0" 14 | platformdirs = ">=2" 15 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 16 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 17 | 18 | [package.extras] 19 | colorama = ["colorama (>=0.4.3)"] 20 | d = ["aiohttp (>=3.10)"] 21 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 22 | uvloop = ["uvloop (>=0.15.2)"] 23 | 24 | [[package]] 25 | name = "click" 26 | version = "8.1.7" 27 | description = "Composable command line interface toolkit" 28 | category = "dev" 29 | optional = false 30 | python-versions = ">=3.7" 31 | 32 | [package.dependencies] 33 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 34 | 35 | [[package]] 36 | name = "colorama" 37 | version = "0.4.6" 38 | description = "Cross-platform colored terminal text." 39 | category = "dev" 40 | optional = false 41 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 42 | 43 | [[package]] 44 | name = "exceptiongroup" 45 | version = "1.2.2" 46 | description = "Backport of PEP 654 (exception groups)" 47 | category = "dev" 48 | optional = false 49 | python-versions = ">=3.7" 50 | 51 | [package.extras] 52 | test = ["pytest (>=6)"] 53 | 54 | [[package]] 55 | name = "iniconfig" 56 | version = "2.0.0" 57 | description = "brain-dead simple config-ini parsing" 58 | category = "dev" 59 | optional = false 60 | python-versions = ">=3.7" 61 | 62 | [[package]] 63 | name = "mypy" 64 | version = "1.13.0" 65 | description = "Optional static typing for Python" 66 | category = "dev" 67 | optional = false 68 | python-versions = ">=3.8" 69 | 70 | [package.dependencies] 71 | mypy-extensions = ">=1.0.0" 72 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 73 | typing-extensions = ">=4.6.0" 74 | 75 | [package.extras] 76 | dmypy = ["psutil (>=4.0)"] 77 | faster-cache = ["orjson"] 78 | install-types = ["pip"] 79 | mypyc = ["setuptools (>=50)"] 80 | reports = ["lxml"] 81 | 82 | [[package]] 83 | name = "mypy-extensions" 84 | version = "1.0.0" 85 | description = "Type system extensions for programs checked with the mypy type checker." 86 | category = "dev" 87 | optional = false 88 | python-versions = ">=3.5" 89 | 90 | [[package]] 91 | name = "packaging" 92 | version = "24.2" 93 | description = "Core utilities for Python packages" 94 | category = "dev" 95 | optional = false 96 | python-versions = ">=3.8" 97 | 98 | [[package]] 99 | name = "pathspec" 100 | version = "0.12.1" 101 | description = "Utility library for gitignore style pattern matching of file paths." 102 | category = "dev" 103 | optional = false 104 | python-versions = ">=3.8" 105 | 106 | [[package]] 107 | name = "platformdirs" 108 | version = "4.3.6" 109 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 110 | category = "dev" 111 | optional = false 112 | python-versions = ">=3.8" 113 | 114 | [package.extras] 115 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx-autodoc-typehints (>=2.4)", "sphinx (>=8.0.2)"] 116 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest (>=8.3.2)"] 117 | type = ["mypy (>=1.11.2)"] 118 | 119 | [[package]] 120 | name = "pluggy" 121 | version = "1.5.0" 122 | description = "plugin and hook calling mechanisms for python" 123 | category = "dev" 124 | optional = false 125 | python-versions = ">=3.8" 126 | 127 | [package.extras] 128 | dev = ["pre-commit", "tox"] 129 | testing = ["pytest", "pytest-benchmark"] 130 | 131 | [[package]] 132 | name = "pytest" 133 | version = "8.3.3" 134 | description = "pytest: simple powerful testing with Python" 135 | category = "dev" 136 | optional = false 137 | python-versions = ">=3.8" 138 | 139 | [package.dependencies] 140 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 141 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 142 | iniconfig = "*" 143 | packaging = "*" 144 | pluggy = ">=1.5,<2" 145 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 146 | 147 | [package.extras] 148 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 149 | 150 | [[package]] 151 | name = "ruff" 152 | version = "0.7.3" 153 | description = "An extremely fast Python linter and code formatter, written in Rust." 154 | category = "dev" 155 | optional = false 156 | python-versions = ">=3.7" 157 | 158 | [[package]] 159 | name = "tomli" 160 | version = "2.1.0" 161 | description = "A lil' TOML parser" 162 | category = "dev" 163 | optional = false 164 | python-versions = ">=3.8" 165 | 166 | [[package]] 167 | name = "typing-extensions" 168 | version = "4.12.2" 169 | description = "Backported and Experimental Type Hints for Python 3.8+" 170 | category = "dev" 171 | optional = false 172 | python-versions = ">=3.8" 173 | 174 | [metadata] 175 | lock-version = "1.1" 176 | python-versions = ">=3.7.0" 177 | content-hash = "100ccfea3feed3615b99f8234474dfb57f01dd2108b23f037063014f29d2823f" 178 | 179 | [metadata.files] 180 | black = [] 181 | click = [] 182 | colorama = [] 183 | exceptiongroup = [] 184 | iniconfig = [] 185 | mypy = [] 186 | mypy-extensions = [] 187 | packaging = [] 188 | pathspec = [] 189 | platformdirs = [] 190 | pluggy = [] 191 | pytest = [] 192 | ruff = [] 193 | tomli = [] 194 | typing-extensions = [] 195 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pylibftdi.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pylibftdi.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /tests/test_bitbang.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | This module contains some basic tests for the higher-level 10 | functionality without requiring an actual hardware device 11 | to be attached. 12 | """ 13 | 14 | import unittest 15 | 16 | from pylibftdi import FtdiError 17 | from pylibftdi.bitbang import BitBangDevice 18 | from tests.test_common import CallCheckMixin, LoopDevice 19 | 20 | 21 | class LoopBitBangDevice(BitBangDevice, LoopDevice): 22 | pass 23 | 24 | 25 | BitBangDevice = LoopBitBangDevice # type: ignore 26 | 27 | 28 | # and now some test cases... 29 | class BitBangFunctions(CallCheckMixin, unittest.TestCase): 30 | def testContextManager(self): 31 | def _(): 32 | with BitBangDevice(): 33 | pass 34 | 35 | self.assertCallsExact( 36 | _, 37 | [ 38 | "ftdi_init", 39 | "ftdi_usb_open_desc_index", 40 | "ftdi_set_bitmode", 41 | "ftdi_setflowctrl", 42 | "ftdi_set_baudrate", 43 | "ftdi_set_latency_timer", 44 | "ftdi_set_bitmode", 45 | "ftdi_usb_close", 46 | "ftdi_deinit", 47 | ], 48 | ) 49 | 50 | def testOpen(self): 51 | """ 52 | check same opening things as a normal Device still work 53 | for BitBangDevice 54 | """ 55 | # a lazy_open open() shouldn't do anything 56 | self.assertCallsExact(lambda: BitBangDevice(lazy_open=True), []) 57 | # a non-lazy_open open() should open the port... 58 | self.assertCalls(lambda: BitBangDevice(), "ftdi_usb_open_desc_index") 59 | # and set the bit mode 60 | self.assertCalls(lambda: BitBangDevice(), "ftdi_set_bitmode") 61 | # and given a device_id, it should do a open_desc 62 | self.assertCalls(lambda: BitBangDevice("bogus"), "ftdi_usb_open_desc_index") 63 | 64 | def testInitDirection(self): 65 | # check that a direction can be given on open and is honoured 66 | for dir_test in (0, 1, 4, 12, 120, 255): 67 | dev = BitBangDevice(direction=dir_test) 68 | self.assertEqual(dev.direction, dir_test) 69 | self.assertEqual(dev._direction, dir_test) 70 | self.assertEqual(dev._last_set_dir, dir_test) 71 | # check an invalid direction on open gives error 72 | self.assertRaises(FtdiError, lambda: BitBangDevice(direction=300)) 73 | 74 | def testDirection(self): 75 | dev = BitBangDevice() 76 | # check that a direction can be given on open and is honoured 77 | for dir_test in (0, 1, 4, 12, 120, 255): 78 | 79 | def assign_dir(dir_test=dir_test): 80 | dev.direction = dir_test 81 | 82 | self.assertCalls(assign_dir, "ftdi_set_bitmode") 83 | self.assertEqual(dev.direction, dir_test) 84 | self.assertEqual(dev._direction, dir_test) 85 | self.assertEqual(dev._last_set_dir, dir_test) 86 | # check an invalid direction on open gives error 87 | 88 | def _(): # noqa 89 | dev.direction = 300 90 | 91 | self.assertRaises(FtdiError, _) 92 | 93 | def testPort(self): 94 | dev = BitBangDevice() 95 | # check that a direction can be given on open and is honoured 96 | for port_test in (0, 1, 4, 12, 120, 255): 97 | 98 | def assign_port(port_test=port_test): 99 | dev.port = port_test 100 | 101 | self.assertCalls(assign_port, "ftdi_write_data") 102 | self.assertEqual(dev._latch, port_test) 103 | self.assertEqual(dev.port, port_test) 104 | 105 | def testBitAccess(self): 106 | dev = BitBangDevice(direction=0xF0) 107 | _ = dev.latch # absorb the first ftdi_read_pins 108 | self.assertCallsExact(lambda: dev.port | 2, ["ftdi_read_pins"]) 109 | self.assertCallsExact(lambda: dev.port & 2, ["ftdi_read_pins"]) 110 | 111 | def testLatchReadModifyWrite(self): 112 | dev = BitBangDevice(direction=0x55) 113 | self.assertCallsExact(lambda: dev.latch, ["ftdi_read_pins"]) 114 | self.assertCallsExact(lambda: dev.latch, []) 115 | 116 | def x(): 117 | dev.latch += 1 118 | dev.latch += 1 119 | dev.latch += 1 120 | 121 | self.assertCallsExact( 122 | x, 123 | [ 124 | "ftdi_tcoflush", 125 | "ftdi_write_data", 126 | "ftdi_tcoflush", 127 | "ftdi_write_data", 128 | "ftdi_tcoflush", 129 | "ftdi_write_data", 130 | ], 131 | ) 132 | 133 | def testAsyncLatchReadModifyWrite(self): 134 | dev = BitBangDevice(direction=0x55, sync=False) 135 | self.assertCallsExact(lambda: dev.latch, ["ftdi_read_pins"]) 136 | self.assertCallsExact(lambda: dev.latch, []) 137 | 138 | def x(): 139 | dev.latch += 1 140 | dev.latch += 1 141 | dev.latch += 1 142 | 143 | self.assertCallsExact( 144 | x, ["ftdi_write_data", "ftdi_write_data", "ftdi_write_data"] 145 | ) 146 | 147 | def testAugmentedAccess(self): 148 | dev = BitBangDevice(direction=0xAA) 149 | _ = dev.latch # absorb the first ftdi_read_pins 150 | 151 | def _1(): 152 | dev.port &= 2 153 | 154 | def _2(): 155 | dev.port |= 2 156 | 157 | self.assertCallsExact( 158 | _1, ["ftdi_read_pins", "ftdi_tcoflush", "ftdi_write_data"] 159 | ) 160 | self.assertCallsExact( 161 | _2, ["ftdi_read_pins", "ftdi_tcoflush", "ftdi_write_data"] 162 | ) 163 | 164 | 165 | if __name__ == "__main__": 166 | unittest.main() 167 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pylibftdi.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pylibftdi.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pylibftdi" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pylibftdi" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /src/pylibftdi/bitbang.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi - python wrapper for libftdi 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | """ 10 | 11 | from ctypes import byref, c_ubyte 12 | 13 | from pylibftdi._base import FtdiError 14 | from pylibftdi.device import Device 15 | from pylibftdi.driver import BITMODE_BITBANG 16 | 17 | ALL_OUTPUTS = 0xFF 18 | ALL_INPUTS = 0x00 19 | BB_OUTPUT = 1 20 | BB_INPUT = 0 21 | 22 | 23 | class BitBangDevice(Device): 24 | """ 25 | simple subclass to support bit-bang mode 26 | 27 | Internally uses async mode at the moment, but provides a 'sync' 28 | flag (defaulting to True) which controls the behaviour of port 29 | reading and writing - if set, the FIFOs are ignored (read) or 30 | cleared (write) so operations will appear synchronous 31 | 32 | Adds three read/write properties to the base class: 33 | direction: 8 bit input(0)/output(1) direction control. 34 | port: 8 bit IO port, as defined by direction. 35 | latch: 8 bit output value, allowing e.g. `bb.latch += 1` to make sense 36 | when there is a mix of input and output lines 37 | """ 38 | 39 | def __init__( 40 | self, 41 | device_id=None, 42 | direction=ALL_OUTPUTS, 43 | lazy_open=False, 44 | sync=True, 45 | bitbang_mode=BITMODE_BITBANG, 46 | interface_select=None, 47 | **kwargs, 48 | ): 49 | # initialise the super-class, but don't open yet. We really want 50 | # two-part initialisation here - set up all the instance variables 51 | # here in the super class, then open it after having set more 52 | # of our own variables. 53 | super().__init__( 54 | device_id=device_id, 55 | mode="b", 56 | lazy_open=True, 57 | interface_select=interface_select, 58 | **kwargs, 59 | ) 60 | self.direction = direction 61 | self.sync = sync 62 | self.bitbang_mode = bitbang_mode 63 | self._last_set_dir = None 64 | # latch is the latched state of output pins. 65 | # it is initialised to the read value of the pins 66 | # 'and'ed with those bits set to OUTPUT (1) 67 | self._latch = None 68 | if not lazy_open: 69 | self.open() 70 | 71 | def open(self): 72 | """open connection to a FTDI device""" 73 | # in case someone sets the direction before we are open()ed, 74 | # we intercept this call... 75 | super().open() 76 | if self.direction != self._last_set_dir: 77 | self.direction = self._direction 78 | return self 79 | 80 | def read_pins(self): 81 | """ 82 | read the current 'actual' state of the pins 83 | 84 | :return: 8-bit binary representation of pin state 85 | :rtype: int 86 | """ 87 | pin_byte = c_ubyte() 88 | res = self.ftdi_fn.ftdi_read_pins(byref(pin_byte)) 89 | if res != 0: 90 | raise FtdiError("Could not read device pins") 91 | return pin_byte.value 92 | 93 | @property 94 | def latch(self): 95 | """ 96 | latch property - the output latch (in-memory representation 97 | of output pin state) 98 | 99 | Note _latch is not masked by direction (except on initialisation), 100 | as otherwise a loop incrementing a mixed input/output port would 101 | not work, as it would 'stop' on input pins. This is the primary 102 | use case for 'latch'. It's basically a `port` which ignores input. 103 | 104 | :return: the state of the output latch 105 | """ 106 | if self._latch is None: 107 | self._latch = self.read_pins() & self.direction 108 | return self._latch 109 | 110 | @latch.setter 111 | def latch(self, value): 112 | self.port = value # this updates ._latch implicitly 113 | 114 | # direction property - 8 bit value determining whether an IO line 115 | # is output (if set to 1) or input (set to 0) 116 | @property 117 | def direction(self): 118 | """ 119 | get or set the direction of each of the IO lines. LSB=D0, MSB=D7 120 | 1 for output, 0 for input 121 | """ 122 | return self._direction 123 | 124 | @direction.setter 125 | def direction(self, new_dir): 126 | if not (0 <= new_dir <= 255): 127 | raise FtdiError("invalid direction bitmask") 128 | self._direction = new_dir 129 | if not self.closed: 130 | self.ftdi_fn.ftdi_set_bitmode(new_dir, self.bitbang_mode) 131 | self._last_set_dir = new_dir 132 | 133 | # port property - 8 bit read/write value 134 | @property 135 | def port(self): 136 | """ 137 | get or set the state of the IO lines. The value of output 138 | lines is persisted in this object for the purposes of reading, 139 | so read-modify-write operations (e.g. drv.port+=1) are valid. 140 | """ 141 | if self._direction == ALL_OUTPUTS: 142 | # a minor optimisation; no point reading from the port if 143 | # we have no input lines set 144 | result = self.latch 145 | else: 146 | if self.sync: 147 | result = self.read_pins() 148 | else: 149 | result = self.read(1)[0] 150 | 151 | # replace the 'output' bits with current value of self.latch - 152 | # the last written value. This makes read-modify-write 153 | # operations (e.g. 'drv.port |= 0x10') work as expected 154 | result = (result & ~self._direction) | ( # read input 155 | self.latch & self._direction 156 | ) # output latch 157 | return result 158 | 159 | @port.setter 160 | def port(self, value): 161 | # restrict to a single byte 162 | value &= 0xFF 163 | self._latch = value 164 | if self.sync: 165 | self.flush_output() 166 | # note to_bytes() gets these as default args in Python3.11+ 167 | return super().write(value.to_bytes(1, "big")) 168 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Unsurprisingly, ``pylibftdi`` depends on ``libftdi``, and installing this varies 5 | according to your operating system. Chances are that following one of the 6 | following instructions will install the required prerequisites. If not, be 7 | aware that libftdi in turn relies on ``libusb``. 8 | 9 | Installing ``pylibftdi`` itself is straightforward - it is a pure Python package 10 | (using ``ctypes`` for bindings), and has no dependencies outside the Python 11 | standard library for installation. Don't expect it to work happily without 12 | ``libftdi`` installed though :-) 13 | 14 | :: 15 | 16 | $ pip install pylibftdi 17 | 18 | Depending on your environment, you may want to set up a `virtual environment`_ 19 | or use either the ``--user`` flag, or prefix the command with ``sudo`` to 20 | gain root privileges. 21 | 22 | .. _virtual environment: https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/ 23 | 24 | Windows 25 | ------- 26 | 27 | I perform only limited testing of pylibftdi on Windows, but it should work 28 | correctly provided the requirements of libftdi and libusb are correctly 29 | installed. 30 | 31 | Recent libftdi binaries for Windows seem to be available from the picusb_ 32 | project on Sourceforge. Download libftdi1-1.1_devkit_x86_x64_21Feb2014.zip 33 | or later from that site, which includes the required 34 | 35 | .. _picusb: http://sourceforge.net/projects/picusb/files/ 36 | 37 | Installing libraries on Windows is easier with recent versions of Python 38 | (2.7.9, 3.4+) installing `pip` directly, so the standard approach of 39 | `pip install pylibftdi` will now easily work on Windows. 40 | 41 | Mac OS X 42 | -------- 43 | 44 | I suggest using homebrew_ to install libftdi:: 45 | 46 | $ brew install libftdi 47 | 48 | .. _homebrew: http://mxcl.github.com/homebrew/ 49 | 50 | On OS X Mavericks (and presumably future versions) Apple include a driver for 51 | FTDI devices. This needs unloading before ``libftdi`` can access FTDI devices 52 | directly. See the Troubleshooting_ section for instructions. 53 | 54 | .. _Troubleshooting: troubleshooting.html#os-x-mavericks 55 | 56 | Linux 57 | ----- 58 | 59 | There are two steps in getting a sensible installation in Linux systems: 60 | 61 | 1. Getting ``libftdi`` and its dependencies installed 62 | 2. Ensuring permissions allow access to the device without requiring root 63 | privileges. Symptoms of this not being done are programs only working 64 | properly when run with ``sudo``, giving '-4' or '-8' error codes in 65 | other cases. 66 | 67 | Each of these steps will be slightly different depending on the distribution 68 | in use. I've tested ``pylibftdi`` on Debian Wheezy (on a Raspberry Pi), 69 | Ubuntu (various versions, running on a fairly standard ThinkPad laptop), 70 | and Arch Linux (running on a PogoPlug - one of the early pink ones). 71 | 72 | Debian (Raspberry Pi) / Ubuntu etc 73 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 74 | 75 | On Debian like systems (including Ubuntu, Mint, Debian, etc), the package 76 | ``libftdi1-dev`` should give you what you need as far as the libftdi library 77 | is concerned:: 78 | 79 | $ sudo apt-get install libftdi1-dev 80 | 81 | The following works for both a Raspberry Pi (Debian Wheezy) and Ubuntu 12.04, 82 | getting ordinary users (e.g. 'pi' on the RPi) access to the FTDI device without 83 | needing root permissions: 84 | 85 | 1. Create a file ``/etc/udev/rules.d/99-libftdi.rules``. You will need sudo 86 | access to create this file. 87 | 2. Put the following in the file:: 88 | 89 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="dialout", MODE="0660" 90 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", GROUP="dialout", MODE="0660" 91 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6011", GROUP="dialout", MODE="0660" 92 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6014", GROUP="dialout", MODE="0660" 93 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6015", GROUP="dialout", MODE="0660" 94 | 95 | The list of USB product IDs above matches the default used by ``pylibftdi``, but 96 | some FTDI devices may use other USB PIDs. You could try removing the match on 97 | ``idProduct`` altogether, just matching on the FTDI vendor ID as follows:: 98 | 99 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", GROUP="dialout", MODE="0660" 100 | 101 | Or use ``lsusb`` or similar to determine the exact values to use (or try checking 102 | ``dmesg`` output on device insertion / removal). 103 | ``udevadm monitor --environment`` is also helpful, but note that the environment 104 | 'keys' it gives are different to the attributes (filenames within /sys/devices/...) 105 | which the ATTRS will match. Perhaps ENV{} matches work just as well, though I've 106 | only tried matching on ATTRS. 107 | 108 | Note that changed udev rules files will be picked up automatically by the udev 109 | daemon, but will only be acted upon on device actions, so unplug/plug in the 110 | device to check whether you're latest rules iteration actually works :-) 111 | 112 | Also note that the udev rules above assume that your user is in the 'dialout' 113 | group - if not, add it to your user with the following, though note that this 114 | will not apply immediately, not a full reboot may be needed on some systems:: 115 | 116 | sudo usermod -aG dialout $USER 117 | 118 | See http://wiki.debian.org/udev for more on writing udev rules. 119 | 120 | Arch Linux 121 | ~~~~~~~~~~ 122 | 123 | The ``libftdi`` package (sensibly enough) provides the ``libftdi`` library:: 124 | 125 | $ sudo pacman -S libftdi 126 | 127 | Similar udev rules to those above for Debian should be included (again in 128 | ``/etc/udev/rules.d/99-libftdi.rules`` or similar), though the GROUP directive 129 | should be changed to set the group to 'users':: 130 | 131 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", GROUP="users", MODE="0660" 132 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", GROUP="users", MODE="0660" 133 | (etc...) 134 | 135 | Testing installation 136 | -------------------- 137 | 138 | Connect your device, and run the following (as a regular user):: 139 | 140 | $ python3 -m pylibftdi.examples.list_devices 141 | 142 | If all goes well, the program should report information about each connected 143 | device. If no information is printed, but it is when run with ``sudo``, a 144 | possibility is permissions problems - see the section under Linux above 145 | regarding ``udev`` rules. 146 | 147 | If the above works correctly, then try the following:: 148 | 149 | $ python3 -m pylibftdi.examples.led_flash 150 | 151 | Even without any LED connected, this should 'work' without any error - quit 152 | with Ctrl-C. Likely errors at this point are either permissions problems 153 | (e.g. udev rules not working), or not finding the device at all - although 154 | the earlier stage is likely to have failed if this were the case. 155 | 156 | Feel free to contact me (@codedstructure on Twitter) if you have any issues with 157 | installation, though be aware I don't have much in the way of Windows systems 158 | to test. 159 | -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | pylibftdi troubleshooting 2 | ========================= 3 | 4 | Once up-and-running, pylibftdi is designed to be very simple, but sometimes 5 | getting it working in the first place can be more difficult. 6 | 7 | Error messages 8 | -------------- 9 | 10 | ``FtdiError: unable to claim usb device. Make sure the default FTDI driver is not in use (-5)`` 11 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12 | 13 | This indicates a conflict with FTDI's own drivers, and is (as far as I know) 14 | mainly a problem on Mac OS X, where they can be disabled (until reboot) by 15 | unloading the appropriate kernel module. 16 | 17 | MacOS (Mavericks and later) 18 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | 20 | Starting with OS X Mavericks, OS X includes kernel drivers which will reserve 21 | the FTDI device by default. In addition, the FTDI-provided VCP driver will 22 | claim the device by default. These need unloading before `libftdi` will be able` 23 | to communicate with the device:: 24 | 25 | sudo kextunload -bundle-id com.apple.driver.AppleUSBFTDI 26 | sudo kextunload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver 27 | 28 | Similarly to reload them:: 29 | 30 | sudo kextload -bundle-id com.apple.driver.AppleUSBFTDI 31 | sudo kextload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver 32 | 33 | Earlier versions of ``pylibftdi`` (prior to 0.18.0) included scripts for 34 | MacOS which unloaded / reloaded these drivers, but these complicated cross-platform 35 | packaging so have been removed. If you are on using MacOS with programs which 36 | need these drivers on a frequent basis (such as the Arduino IDE when using 37 | older FTDI-based Arduino boards), consider implementing these yourself, along the 38 | lines of the following (which assumes ~/bin is in your path):: 39 | 40 | cat << EOF > /usr/local/bin/ftdi_osx_driver_unload 41 | sudo kextunload -bundle-id com.apple.driver.AppleUSBFTDI 42 | sudo kextunload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver 43 | EOF 44 | 45 | cat << EOF > /usr/local/bin/ftdi_osx_driver_reload 46 | sudo kextload -bundle-id com.apple.driver.AppleUSBFTDI 47 | sudo kextload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver 48 | EOF 49 | 50 | chmod +x /usr/local/bin/ftdi_osx_driver_* 51 | 52 | 53 | OS X Mountain Lion and earlier 54 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 55 | Whereas Mavericks includes an FTDI driver directly, earlier versions of OS X 56 | did not, and if this issue occurred it would typically as a result of 57 | installing some other program - for example the Arduino IDE. 58 | 59 | As a result, the kernel module may have different names, but `FTDIUSBSerialDriver.kext` 60 | is the usual culprit. Unload the kernel driver as follows:: 61 | 62 | sudo kextunload /System/Library/Extensions/FTDIUSBSerialDriver.kext 63 | 64 | To reload the kernel driver, do the following:: 65 | 66 | sudo kextload /System/Library/Extensions/FTDIUSBSerialDriver.kext 67 | 68 | If you aren't using whatever program might have installed it, the driver 69 | could be permanently removed (to prevent the need to continually unload it), 70 | but this is dangerous:: 71 | 72 | sudo rm /System/Library/Extensions/FTDIUSBSerialDriver.kext 73 | 74 | Diagnosis 75 | --------- 76 | 77 | Getting a list of USB devices 78 | 79 | Mac OS X 80 | ~~~~~~~~ 81 | 82 | Start 'System Information', then select Hardware > USB, and look for your 83 | device. On the command line, ``system_profiler SPUSBDataType`` can be used. 84 | In the following example I've piped it into ``grep -C 7 FTDI``, to print 7 85 | lines either side of a match on the string 'FTDI':: 86 | 87 | ben$ system_profiler SPUSBDataType | grep -C 7 FTDI 88 | UM232H: 89 | 90 | Product ID: 0x6014 91 | Vendor ID: 0x0403 (Future Technology Devices International Limited) 92 | Version: 9.00 93 | Serial Number: FTUBIOWF 94 | Speed: Up to 480 Mb/sec 95 | Manufacturer: FTDI 96 | Location ID: 0x24710000 / 7 97 | Current Available (mA): 500 98 | Current Required (mA): 90 99 | 100 | USB Reader: 101 | 102 | Product ID: 0x4082 103 | 104 | Linux 105 | ~~~~~ 106 | Use ``lsusb``. Example from my laptop:: 107 | 108 | ben@ben-laptop:~$ lsusb 109 | Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 110 | Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub 111 | Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 112 | Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 113 | Bus 005 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 114 | Bus 006 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 115 | Bus 007 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 116 | Bus 008 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub 117 | Bus 008 Device 011: ID 0a5c:217f Broadcom Corp. Bluetooth Controller 118 | Bus 002 Device 009: ID 17ef:481d Lenovo 119 | Bus 002 Device 016: ID 0403:6014 Future Technology Devices International, Ltd FT232H Single HS USB-UART/FIFO IC 120 | 121 | 122 | Where did my ttyUSB devices go? 123 | ------------------------------- 124 | When a `pylibftdi.Device()` is opened, any kernel device which was previously 125 | present will become unavailable. On Linux for example, a serial-capable FTDI 126 | device will (via the `ftdi_sio` driver) create a device node such as 127 | `/dev/ttyUSB0` (or ttyUSB1,2,3 etc). This device allows use of the FTDI device 128 | as a simple file in the Linux filesystem which can be read and written. 129 | Various programs such as the Arduino IDE (at least when communicating with 130 | some board variants) and libraries such as `PySerial` will use this device. 131 | Once libftdi opens a device, the corresponding entry in /dev/ will disappear. 132 | Prior to `pylibftdi` version 0.16, the simplest way to get the device node to 133 | reappear would be to unplug and replug the USB device itself. Starting from 134 | 0.16, this should no longer be necessary as the kernel driver (which exports 135 | `/dev/ttyUSB...`) is reattached when the `pylibftdi` device is closed. This 136 | behaviour can be controlled by the `auto_detach` argument (which is defaulted 137 | to `True`) to the `Device` class; setting it to `False` reverts to the old 138 | behaviour. 139 | 140 | Note that on recent OS X, libftdi doesn't 'steal' the device, but instead 141 | refuses to open it. The kernel devices can be seen as 142 | `/dev/tty.usbserial-xxxxxxxx`, where `xxxxxxxx` is the device serial number. 143 | FTDI's Application Note AN134_ details this further (see section 'Using 144 | Apple-provided VCP or D2XX with OS X 10.9 & 10.10'). See the section above 145 | under Installation for further details on resolving this. 146 | 147 | .. _AN134: http://www.ftdichip.com/Support/Documents/AppNotes/AN_134_FTDI_Drivers_Installation_Guide_for_MAC_OSX.pdf 148 | 149 | Gathering information 150 | --------------------- 151 | Starting with pylibftdi version 0.15, an example script to gather system 152 | information is included, which will help in any diagnosis required. 153 | 154 | Run the following:: 155 | 156 | python3 -m pylibftdi.examples.info 157 | 158 | this will output a range of information related to the versions of libftdi 159 | libusb in use, as well as the system platform and Python version, for example:: 160 | 161 | pylibftdi version : 0.18.0 162 | libftdi version : libftdi_version(major=1, minor=4, micro=0, version_str='1.4', snapshot_str='unknown') 163 | libftdi library name : libftdi1.so.2 164 | libusb version : libusb_version(major=1, minor=0, micro=22, nano=11312, rc='', describe='http://libusb.info') 165 | libusb library name : libusb-1.0.so.0 166 | Python version : 3.7.3 167 | OS platform : Linux-5.0.0-32-generic-x86_64-with-Ubuntu-19.04-disco 168 | 169 | -------------------------------------------------------------------------------- /src/pylibftdi/examples/serial_transfer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 -u 2 | """ 3 | test serial transfer between two devices 4 | 5 | This module assumes two devices (e.g. FT232R based) are connected to the 6 | host computer, with RX and TX of the two devices wired to each other so 7 | they can communicate. It launches threads to send and receive traffic and 8 | check that a random stream sent from one device is correctly received at 9 | the other: 10 | 11 | Testing 9600 baud 12 | Half duplex d1->d2... 13 | Bytes TX: 10000 RX: 10000 14 | Checksum TX: ad7e985fdddfbc04e398daa781a9fad0 RX: ad7e985fdddfbc04e398daa781a9fad0 15 | SUCCESS 16 | Half duplex d2->d1... 17 | Bytes TX: 10000 RX: 10000 18 | Checksum TX: 61338c11fe18642a07f196094646295f RX: 61338c11fe18642a07f196094646295f 19 | SUCCESS 20 | Full duplex d1<=>d2... 21 | Bytes TX: 10000 RX: 10000 22 | Checksum TX: 7dcc7ed3b89e46592c777ec42c330fd8 RX: 7dcc7ed3b89e46592c777ec42c330fd8 23 | SUCCESS 24 | Bytes TX: 10000 RX: 10000 25 | Checksum TX: 1a957192b8219aa02ad374dd518e37fd RX: 1a957192b8219aa02ad374dd518e37fd 26 | SUCCESS 27 | 28 | Copyright (c) 2015-2020 Ben Bass 29 | All rights reserved. 30 | """ 31 | 32 | import hashlib 33 | import random 34 | import threading 35 | import time 36 | from itertools import islice 37 | 38 | from pylibftdi import Device, FtdiError 39 | 40 | 41 | class RandomStream: 42 | """ 43 | Infinite iterator of random data which can be queried at any point 44 | for the checksum of the data already yielded 45 | """ 46 | 47 | def __init__(self, block_size=1024): 48 | self._block_size = block_size 49 | # Note: `reset()` sets the initial attributes 50 | self.reset() 51 | 52 | @staticmethod 53 | def _rand_gen(size): 54 | """Return a `bytes` instance of `size` random byte""" 55 | return bytes(bytearray(random.getrandbits(8) for _ in range(size))) 56 | 57 | def reset(self): 58 | self.stream_hash = hashlib.md5() 59 | self.rand_buf = self._rand_gen(self._block_size) 60 | self.chk_tail = self.chk_head = 0 61 | self.bytecount = 0 62 | 63 | def _update_checksum(self): 64 | self.stream_hash.update(self.rand_buf[self.chk_tail : self.chk_head]) 65 | self.chk_tail = self.chk_head 66 | 67 | def checksum(self): 68 | self._update_checksum() 69 | return self.stream_hash.hexdigest() 70 | 71 | def __iter__(self): 72 | while True: 73 | # Use slice rather than index to avoid bytes->int conversion 74 | # in Python3 75 | data = self.rand_buf[self.chk_head : self.chk_head + 1 :] 76 | self.chk_head += 1 77 | self.bytecount += 1 78 | yield data 79 | if self.chk_head == self._block_size: 80 | self._update_checksum() 81 | self.rand_buf = self._rand_gen(self._block_size) 82 | self.chk_head = self.chk_tail = 0 83 | 84 | 85 | def test_rs(): 86 | r = RandomStream() 87 | prev_checksum = 0 88 | stream_bytes = [] 89 | for _i in range(30): 90 | stream_bytes.append(b"".join(islice(r, 500))) 91 | assert r.checksum() != prev_checksum 92 | assert r.checksum() == r.checksum() 93 | assert hashlib.md5(b"".join(stream_bytes)).hexdigest() == r.checksum() 94 | 95 | 96 | class HalfDuplexTransfer: 97 | """ 98 | Test streaming bytes from one device to another 99 | """ 100 | 101 | def __init__(self, source, dest, baudrate=9600, block_size=500): 102 | """ 103 | Prepare for half-duplex transmission from source device to dest 104 | """ 105 | self.source = source 106 | self.dest = dest 107 | self.source.baudrate = baudrate 108 | self.dest.baudrate = baudrate 109 | 110 | self.target = [] 111 | self.wait_signal = threading.Event() 112 | self.running = threading.Event() 113 | 114 | self.rs = RandomStream() 115 | 116 | self.block_size = block_size 117 | self.test_duration = 10 118 | 119 | self.t1 = None 120 | self.t2 = None 121 | 122 | self.done = False 123 | 124 | def reader(self): 125 | # Tell writer we're ready for the deluge... 126 | self.wait_signal.set() 127 | 128 | # if we've just finished reading when self.done get's set by the 129 | # writer, we won't get the 'last' packet. But if we assume there's 130 | # always one more after done gets set, we'll get some ReadTimeouts.... 131 | # Probably best to try one more time but catch & ignore ReadTimeout. 132 | while not self.done: 133 | data = self.dest.read(1024) 134 | self.target.append(data) 135 | 136 | try: 137 | data = self.dest.read(1024) 138 | self.target.append(data) 139 | except FtdiError: 140 | pass 141 | 142 | def writer(self): 143 | self.running.set() 144 | self.wait_signal.wait() 145 | 146 | end_time = time.time() + self.test_duration 147 | 148 | while time.time() < end_time: 149 | x = b"".join(list(islice(self.rs, self.block_size))) 150 | self.source.write(x) 151 | 152 | # Wait for the reader to catch up 153 | time.sleep(0.01) 154 | self.done = True 155 | 156 | def go(self, test_duration=None): 157 | if test_duration is not None: 158 | self.test_duration = test_duration 159 | 160 | self.t1 = threading.Thread(target=self.writer) 161 | self.t1.daemon = True 162 | self.t1.start() 163 | 164 | # We wait for the writer to be actually running (but not yet 165 | # writing anything) before we start the reader. 166 | self.running.wait() 167 | self.t2 = threading.Thread(target=self.reader) 168 | self.t2.daemon = True 169 | self.t2.start() 170 | 171 | def join(self): 172 | # Use of a timeout allows Ctrl-C interruption 173 | self.t1.join(timeout=1e6) 174 | self.t2.join(timeout=1e6) 175 | 176 | def results(self): 177 | result = b"".join(self.target) 178 | print(f" Bytes TX: {self.rs.bytecount} RX: {len(result)}") 179 | rx_chksum = hashlib.md5(b"".join(self.target)).hexdigest() 180 | print(f" Checksum TX: {self.rs.checksum()} RX: {rx_chksum}") 181 | if len(result) == self.rs.bytecount and self.rs.checksum() == rx_chksum: 182 | print(" SUCCESS") 183 | else: 184 | print(" FAIL") 185 | 186 | 187 | def test_half_duplex_transfer(d1, d2, baudrate=9600): 188 | """ 189 | Test half-duplex stream from d1 to d2, report on status 190 | """ 191 | hd = HalfDuplexTransfer(d1, d2, baudrate) 192 | hd.go() 193 | hd.join() 194 | hd.results() 195 | 196 | 197 | def test_full_duplex_transfer(d1, d2, baudrate=9600): 198 | """ 199 | Start two half-duplex streams in opposite directions at the 200 | same time, check both are OK 201 | """ 202 | hd1 = HalfDuplexTransfer(d1, d2, baudrate) 203 | hd1.go() 204 | hd2 = HalfDuplexTransfer(d2, d1, baudrate) 205 | hd2.go() 206 | hd1.join() 207 | hd1.results() 208 | hd2.join() 209 | hd2.results() 210 | 211 | 212 | def main(): 213 | d1 = Device(device_index=0) 214 | d2 = Device(device_index=1) 215 | 216 | for b in 9600, 38400, 115200: 217 | print(f"Testing {b} baud") 218 | d1.flush() 219 | d2.flush() 220 | print("Half duplex d1->d2...") 221 | test_half_duplex_transfer(d1, d2, baudrate=b) 222 | d1.flush() 223 | d2.flush() 224 | print("Half duplex d2->d1...") 225 | test_half_duplex_transfer(d2, d1, baudrate=b) 226 | d1.flush() 227 | d2.flush() 228 | print("Full duplex d1<=>d2...") 229 | test_full_duplex_transfer(d1, d2, baudrate=b) 230 | 231 | 232 | if __name__ == "__main__": 233 | test_rs() 234 | main() 235 | -------------------------------------------------------------------------------- /docs/how_to.rst: -------------------------------------------------------------------------------- 1 | pylibftdi questions 2 | =================== 3 | 4 | None of these are yet frequently asked, and perhaps they never will be... 5 | But they are still questions, and they relate to pylibftdi. 6 | 7 | Using pylibftdi - General 8 | ------------------------- 9 | 10 | Can I use pylibftdi with device XYZ? 11 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12 | 13 | If the device XYZ is (or uses as it's ) an FTDI device, then possibly. A large 14 | number of devices *will* work, but won't be recognised due to the limited 15 | USB Vendor and Product IDs which pylibftdi checks for. 16 | 17 | To see the vendor / product IDs which are supported, run the following:: 18 | 19 | >>> from pylibftdi import USB_VID_LIST, USB_PID_LIST 20 | >>> print(', '.join(hex(pid) for pid in USB_VID_LIST)) 21 | 0x403 22 | >>> print(', '.join(hex(pid) for pid in USB_PID_LIST)) 23 | 0x6001, 0x6010, 0x6011, 0x6014, 0x6015 24 | 25 | If a FTDI device with a VID / PID not matching the above is required, then 26 | the device's vendor ID and product ID can be specified with `vid` or `pid` 27 | keyword arguments passed to Device constructors. For example, to open a device 28 | with VID 0x1234 and PID 0x5678, use:: 29 | 30 | >>> from pylibftdi import Device 31 | >>> dev = Device(vid=0x1234, pid=0x5678) 32 | 33 | Alternatively, the device's vendor or product ID can be appended to the appropriate 34 | list after import:: 35 | 36 | >>> from pylibftdi import USB_PID_LIST, USB_VID_LIST, Device 37 | >>> USB_PID_LIST.append(0x1234) 38 | >>> 39 | >>> dev = Device() # will now recognise a device with PID 0x1234. 40 | 41 | Which devices are recommended? 42 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | While I used to do a lot of soldering, I prefer the cleaner way of 45 | breadboarding nowadays. As such I can strongly recommend the FTDI DIP 46 | modules which plug into a breadboard nice and easy, can be self-powered 47 | from USB, and can be re-used for dozens of different projects. 48 | 49 | I've used (and test against) the following, all of which have 0.1" pin 50 | spacing in two rows 0.5" or 0.6" apart, so will sit across the central 51 | divide of any breadboard: 52 | 53 | UB232R 54 | a small 8 pin device with mini-USB port; serial and CBUS bit-bang. 55 | 56 | UM245R 57 | a 24-pin device with parallel FIFO modes. Full-size USB type B socket. 58 | 59 | UM232R 60 | a 24-pin device with serial and bit-bang modes. Full-size USB type B 61 | socket. 62 | 63 | UM232H 64 | this contains a more modern FT232H device, and libftdi support is 65 | fairly recent (requires 0.20 or later). Supports USB 2.0 Hi-Speed mode 66 | though, and lots of interesting modes (I2C, SPI, JTAG...) which I've not 67 | looked at yet. Mini-USB socket. 68 | 69 | Personally I'd go with the UM232R device for compatibility. It works great 70 | with both UART and bit-bang IO, which I target as the two main use-cases 71 | for pylibftdi. The UM232H is certainly feature-packed though, and I hope 72 | to support some of the more interesting modes in future. 73 | 74 | Using pylibftdi - Programming 75 | ----------------------------- 76 | 77 | How do I set the baudrate? 78 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | 80 | In both serial and parallel mode, the internal baudrate generator (BRG) is 81 | set using the ``baudrate`` property of the ``Device`` instance. Reading this 82 | will show the current baudrate (which defaults to 9600); writing to it 83 | will attempt to set the BRG to that value. 84 | 85 | On failure to set the baudrate, it will remain at its previous setting. 86 | 87 | In parallel mode, the actual bytes-per-second rate of parallel data is 88 | 16x the programmed BRG value. This is an effect of the FTDI devices 89 | themselves, and is not hidden by pylibftdi. 90 | 91 | How do I send unicode over a serial connection? 92 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 93 | 94 | If a ``Device`` instance is created with ``mode='t'``, then text-mode is 95 | activated. This is analogous to opening files; after all, the API is 96 | intentionally modelled on file objects whereever possible. 97 | 98 | When text-mode is used, an encoding can be specified. The default is 99 | ``latin-1`` for the very practical reason that it is transparent to 8-bit 100 | binary data; by default a text-mode serial connection looks just like a 101 | binary mode one. 102 | 103 | An alternative encoding can be used provided in the same constructor call 104 | used to instantiate the ``Device`` class, e.g.:: 105 | 106 | >>> dev = Device(mode='t', encoding='utf-8') 107 | 108 | Read and write operations will then return / take unicode values. 109 | 110 | Whether it is sensible to try and send unicode over a ftdi connection is 111 | a separate issue... At least consider doing codec operations at a higher 112 | level in your application. 113 | 114 | 115 | How do I use multiple-interface devices? 116 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 117 | 118 | Some FTDI devices have multiple interfaces, for example the FT2232H has 2 119 | and the FT4232H has four. In terms of accessing them, they can be 120 | considered as independent devices; once a connection is established to one 121 | of them, it is isolated from the other interfaces. 122 | 123 | To select which interface to use when opening a connection to a specific 124 | interface on a multiple-interface device, use the ``interface_select`` 125 | parameter of the Device (or BitBangDevice) class constructor. 126 | The value should be one of the following values. Symbolic constants are 127 | provided in the pylibftdi namespace. 128 | 129 | ==================== ============= 130 | ``interface_select`` Meaning 131 | -------------------- ------------- 132 | INTERFACE_ANY (0) Any interface 133 | INTERFACE_A (1) INTERFACE A 134 | INTERFACE_B (2) INTERFACE B 135 | INTERFACE_C (3) INTERFACE C 136 | INTERFACE_D (4) INTERFACE D 137 | ==================== ============= 138 | 139 | You should be able to open multiple ``Device``\s with different 140 | ``interface_select`` settings. 141 | *Thanks to Daniel Forer for testing multiple device support.* 142 | 143 | What is the difference between the ``port`` and ``latch`` BitBangDevice properties? 144 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 145 | 146 | `latch` reflects the current state of the output latch (i.e. the last value 147 | written to the port), while ``port`` reflects input states as well. Writing to 148 | either ``port`` or ``latch`` has an identical effect, so when pylibftdi is used 149 | only for output, there is no effective difference, and ``port`` is recommended 150 | for simplicity and consistency. 151 | 152 | The place where it does make a difference is during read-modify-write 153 | operations. Consider the following:: 154 | 155 | >>> dev = BitBangDevice() # 1 156 | >>> dev.direction = 0x81 # 2 # set bits 0 and 7 are output 157 | >>> dev.port = 0 # 3 158 | >>> for _ in range(255): # 4 159 | >>> dev.port += 1 # 5 # read-modify-write operation 160 | 161 | In this (admittedly contrived!) scenario, if one of the input lines D1..D6 162 | were held low, then they would cause the counter to effectively 'stop'. The 163 | ``+= 1`` operation would never actually set the bit as required (because it is 164 | an input at 0), and the highest output bit would never get set. 165 | 166 | Using ``dev.latch`` in lines 3 and 5 above would resolve this, as the 167 | read-modify-write operation on line 5 is simply working on the in-memory 168 | latch value, rather than reading the inputs, and it would simply count up from 169 | 0 to 255 in steps of one, writing the value to the device (which would be 170 | ignored in the case of input lines). 171 | 172 | Similar concepts exist in many microcontrollers, for example see 173 | http://stackoverflow.com/a/2623498 for a possibly better explanation, though 174 | in a slightly different context :) 175 | 176 | If you aren't using read-modify-write operations (e.g. augmented assignment), 177 | or you have a direction on the port of either ALL_INPUTS (0) or ALL_OUTPUTS 178 | (1), then just ignore this section and use ``port`` :) 179 | 180 | What is the purpose of the ``chunk_size`` parameter? 181 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 182 | 183 | While libftdi is performing I/O to the device, it is not really running Python 184 | code at all, but C library code via ctypes. If there is a significant amount of 185 | data, especially at low baud-rates, this can be a significant delay during which 186 | no Python bytecode is executed. The most obvious result of this is that no 187 | signals are delivered to the Python process during this time, and interrupt 188 | signals (Ctrl-C) will be ignored. 189 | 190 | Try the following:: 191 | 192 | >>> dev = Device() 193 | >>> dev.baudrate = 120 # nice and slow! 194 | >>> dev.write('helloworld' * 1000) 195 | 196 | This should take approximately 10 seconds prior to returning, and crucially, 197 | Ctrl-C interruptions will be deferred for all that time. By setting 198 | ``chunk_size`` on the device (which may be set either as a keyword parameter 199 | during ``Device`` instantiation, or at a later point as an attribute of the 200 | ``Device`` instance), the I/O operations are performed in chunks of at most 201 | the specified number of bytes. Setting it to 0, the default value, disables 202 | this chunking. 203 | 204 | Repeat the above command but prior to the write operation, set 205 | ``dev.chunk_size = 10``. A Ctrl-C interruption should now kick-in almost 206 | instantly. There is a performance trade-off however; if using ``chunk_size`` is 207 | required, set it as high as is reasonable for your application. 208 | 209 | Using pylibftdi - Interfacing 210 | ----------------------------- 211 | 212 | How do I control an LED? 213 | ~~~~~~~~~~~~~~~~~~~~~~~~ 214 | 215 | pylibftdi devices generally have sufficient output current to sink or source 216 | the 10mA or so which a low(ish) current LED will need. A series resistor is 217 | essential to protect both the LED and the FTDI device itself; a value between 218 | 220 and 470 ohms should be sufficient depending on required brightness / LED 219 | efficiency. 220 | 221 | How do I control a higher current device? 222 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 223 | 224 | FTDI devices will typically provide a few tens of milli-amps, but beyond that 225 | things either just won't work, or the device could be damaged. For medium 226 | current operation, a standard bipolar transistor switch will suffice; for 227 | larger loads a MOSFET or relay should be used. (Note a relay will require a 228 | low-power transistor switch anyway). Search online for something like 229 | 'mosfet logic switch' or 'transistor relay switch' for more details. 230 | 231 | What is the state of an unconnected input pin? 232 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 233 | 234 | This depends on the device and the EEPROM configuration values. Most devices 235 | will have weak (typ. 200Kohm) pull-ups on input pins, so there is no harm 236 | leaving them floating. Consult the datasheet for your device for definitive 237 | information, but you can always just leave an (unconnected) device and read 238 | it's pins when set as inputs; chances are they will read 255 / 0xFF:: 239 | 240 | >>> dev = BitBangDevice(direction=0) 241 | >>> dev.port 242 | 255 243 | 244 | While not recommended for anything serious, this does allow the possibility 245 | of reading a input switch state by simply connecting a switch between an input 246 | pin and ground (possibly with a low value - e.g. 100 ohm - series resistor to 247 | prevent accidents should it be set to an output and set high...). Note that 248 | with a normal push-to-make switch, the value will read '1' when the switch is 249 | not pressed; pressing it will set the input line value to '0'. 250 | 251 | -------------------------------------------------------------------------------- /src/pylibftdi/driver.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi.driver - interface to the libftdi library 3 | 4 | Copyright (c) 2010-2014 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import itertools 14 | from collections import namedtuple 15 | 16 | # be disciplined so pyflakes can check us... 17 | from ctypes import ( 18 | POINTER, 19 | Structure, 20 | byref, 21 | c_char_p, 22 | c_int, 23 | c_uint16, 24 | c_void_p, 25 | cast, 26 | cdll, 27 | create_string_buffer, 28 | ) 29 | from ctypes.util import find_library 30 | from typing import Any 31 | 32 | from pylibftdi._base import FtdiError, LibraryMissingError 33 | 34 | 35 | class libusb_version_struct(Structure): 36 | _fields_ = [ 37 | ("major", c_uint16), 38 | ("minor", c_uint16), 39 | ("micro", c_uint16), 40 | ("nano", c_uint16), 41 | ("rc", c_char_p), 42 | ("describe", c_char_p), 43 | ] 44 | 45 | 46 | libusb_version = namedtuple("libusb_version", "major minor micro nano rc describe") 47 | 48 | 49 | class ftdi_device_list(Structure): 50 | _fields_ = [("next", c_void_p), ("dev", c_void_p)] 51 | 52 | 53 | class ftdi_version_info(Structure): 54 | _fields_ = [ 55 | ("major", c_int), 56 | ("minor", c_int), 57 | ("micro", c_int), 58 | ("version_str", c_char_p), 59 | ("snapshot_str", c_char_p), 60 | ] 61 | 62 | 63 | libftdi_version = namedtuple( 64 | "libftdi_version", "major minor micro version_str snapshot_str" 65 | ) 66 | 67 | 68 | # These constants determine what type of flush operation to perform 69 | FLUSH_BOTH = 1 70 | FLUSH_INPUT = 2 71 | FLUSH_OUTPUT = 3 72 | # Device Modes 73 | BITMODE_RESET = 0x00 74 | BITMODE_BITBANG = 0x01 75 | 76 | # Opening / searching for a device uses this list of IDs to search 77 | # by default. These can be extended directly after import if required. 78 | FTDI_VENDOR_ID = 0x0403 79 | USB_VID_LIST = [FTDI_VENDOR_ID] 80 | USB_PID_LIST = [0x6001, 0x6010, 0x6011, 0x6014, 0x6015] 81 | 82 | FTDI_ERROR_DEVICE_NOT_FOUND = -3 83 | 84 | 85 | class Driver: 86 | """ 87 | This is where it all happens... 88 | We load the libftdi library, and use it. 89 | """ 90 | 91 | # The default library names to search for. This can be overridden by 92 | # passing a library name or list of library names to the constructor. 93 | # Prefer libftdi1 if available. Windows uses 'lib' prefix. 94 | _lib_search = { 95 | "libftdi": ["ftdi1", "libftdi1", "ftdi", "libftdi"], 96 | "libusb": ["usb-1.0", "libusb-1.0"], 97 | } 98 | 99 | def __init__(self, libftdi_search: str | list[str] | None = None) -> None: 100 | """ 101 | :param libftdi_search: force a particular version of libftdi to be used 102 | can specify either library name(s) or path(s) 103 | :type libftdi_search: string or a list of strings 104 | """ 105 | if isinstance(libftdi_search, str): 106 | self._lib_search["libftdi"] = [libftdi_search] 107 | elif isinstance(libftdi_search, list): 108 | self._lib_search["libftdi"] = libftdi_search 109 | elif libftdi_search is not None: 110 | raise TypeError( 111 | f"libftdi_search type should be " 112 | f"Optional[str|list[str]], not {type(libftdi_search)}" 113 | ) 114 | 115 | # Library handles. 116 | self._fdll: Any = None 117 | self._libusb_dll: Any = None 118 | 119 | def _load_library(self, name: str, search_list: list[str] | None = None) -> Any: 120 | """ 121 | find and load the requested library 122 | 123 | :param name: library name 124 | :param search_list: an optional list of strings referring to library names 125 | library names or paths can be given 126 | :return: a CDLL object referring to the requested library 127 | """ 128 | # If no search list is given, use default library names from self._lib_search 129 | if search_list is None: 130 | search_list = self._lib_search.get(name, []) 131 | elif not isinstance(search_list, list): 132 | raise TypeError( 133 | f"search_list type should be list[str], not {type(search_list)}" 134 | ) 135 | 136 | lib = None 137 | for dll in search_list: 138 | try: 139 | # Windows in particular can have find_library 140 | # not find things which work fine directly on 141 | # cdll access. 142 | lib = getattr(cdll, dll) 143 | break 144 | # On DLL load fail, Python <3.12 raises OSError, 3.12+ AttributeError. 145 | except (OSError, AttributeError): 146 | lib_path = find_library(dll) 147 | if lib_path is not None: 148 | lib = getattr(cdll, lib_path) 149 | break 150 | if lib is None: 151 | raise LibraryMissingError( 152 | f"{name} library not found (search: {str(search_list)})" 153 | ) 154 | return lib 155 | 156 | @property 157 | def _libusb(self): # type: ignore 158 | """ 159 | ctypes DLL referencing the libusb library, if it exists 160 | 161 | Note this is not normally used directly by pylibftdi, and is available 162 | primarily for diagnostic purposes. 163 | """ 164 | if self._libusb_dll is None: 165 | self._libusb_dll = self._load_library("libusb") 166 | self._libusb_dll.libusb_get_version.restype = POINTER(libusb_version_struct) 167 | 168 | return self._libusb_dll 169 | 170 | def libusb_version(self) -> libusb_version: 171 | """ 172 | :return: namedtuple containing version info on libusb 173 | """ 174 | ver = self._libusb.libusb_get_version().contents 175 | return libusb_version( 176 | ver.major, 177 | ver.minor, 178 | ver.micro, 179 | ver.nano, 180 | ver.rc.decode(), 181 | ver.describe.decode(), 182 | ) 183 | 184 | @property 185 | def fdll(self) -> Any: 186 | """ 187 | ctypes DLL referencing the libftdi library 188 | 189 | This is the main interface to FTDI functionality. 190 | """ 191 | if self._fdll is None: 192 | self._fdll = self._load_library("libftdi") 193 | # most args/return types are fine with the implicit 194 | # int/void* which ctypes uses, but some need setting here 195 | self._fdll.ftdi_get_error_string.restype = c_char_p 196 | self._fdll.ftdi_usb_get_strings.argtypes = ( 197 | c_void_p, 198 | c_void_p, 199 | c_char_p, 200 | c_int, 201 | c_char_p, 202 | c_int, 203 | c_char_p, 204 | c_int, 205 | ) 206 | # library versions <1.0 don't provide ftdi_get_library_version, so 207 | # we need to check for it before setting the restype. 208 | if hasattr(self._fdll, "ftdi_get_library_version"): 209 | self._fdll.ftdi_get_library_version.restype = ftdi_version_info 210 | return self._fdll 211 | 212 | def libftdi_version(self) -> libftdi_version: 213 | """ 214 | :return: the version of the underlying library being used 215 | :rtype: tuple (major, minor, micro, version_string, snapshot_string) 216 | """ 217 | if hasattr(self.fdll, "ftdi_get_library_version"): 218 | version = self.fdll.ftdi_get_library_version() 219 | return libftdi_version( 220 | version.major, 221 | version.minor, 222 | version.micro, 223 | version.version_str.decode(), 224 | version.snapshot_str.decode(), 225 | ) 226 | else: 227 | # library versions <1.0 don't support this function... 228 | return libftdi_version( 229 | 0, 0, 0, "< 1.0 - no ftdi_get_library_version()", "unknown" 230 | ) 231 | 232 | def list_devices(self) -> list[tuple[str, str, str]]: 233 | """ 234 | :return: (manufacturer, description, serial#) for each attached 235 | device, e.g.: 236 | 237 | [('FTDI', 'UM232R USB <-> Serial', 'FTE4FFVQ'), 238 | ('FTDI', 'UM245R', 'FTE00P4L')] 239 | 240 | :rtype: a list of string triples 241 | 242 | the serial number can be used to open specific devices 243 | """ 244 | # ftdi_usb_find_all sets dev_list_ptr to a linked list 245 | # (*next/*usb_device) of usb_devices, each of which can 246 | # be passed to ftdi_usb_get_strings() to get info about 247 | # them. 248 | # this will contain the device info to return 249 | devices = [] 250 | manuf = create_string_buffer(128) 251 | desc = create_string_buffer(128) 252 | serial = create_string_buffer(128) 253 | devlistptrtype = POINTER(ftdi_device_list) 254 | dev_list_ptr = devlistptrtype() 255 | 256 | # create context for doing the enumeration 257 | ctx = create_string_buffer(1024) 258 | if self.fdll.ftdi_init(byref(ctx)) != 0: 259 | msg = self.fdll.ftdi_get_error_string(byref(ctx)) 260 | raise FtdiError(msg) 261 | 262 | def _s(s: bytes) -> str: 263 | """c_char_p -> str helper""" 264 | return s.decode() 265 | 266 | try: 267 | for usb_vid, usb_pid in itertools.product(USB_VID_LIST, USB_PID_LIST): 268 | res = self.fdll.ftdi_usb_find_all( 269 | byref(ctx), byref(dev_list_ptr), usb_vid, usb_pid 270 | ) 271 | if res < 0: 272 | err_msg = self.fdll.ftdi_get_error_string(byref(ctx)) 273 | msg = "%s (%d)" % (err_msg, res) 274 | raise FtdiError(msg) 275 | elif res > 0: 276 | # take a copy of the dev_list for subsequent list_free 277 | dev_list_base = byref(dev_list_ptr) 278 | # traverse the linked list... 279 | try: 280 | while dev_list_ptr: 281 | res = self.fdll.ftdi_usb_get_strings( 282 | byref(ctx), 283 | dev_list_ptr.contents.dev, 284 | manuf, 285 | 127, 286 | desc, 287 | 127, 288 | serial, 289 | 127, 290 | ) 291 | # don't error on failure to get all the data 292 | # error codes: -7: manuf, -8: desc, -9: serial 293 | if res < 0 and res not in (-7, -8, -9): 294 | err_msg = self.fdll.ftdi_get_error_string(byref(ctx)) 295 | msg = "%s (%d)" % (err_msg, res) 296 | raise FtdiError(msg) 297 | devices.append( 298 | (_s(manuf.value), _s(desc.value), _s(serial.value)) 299 | ) 300 | # step to next in linked-list 301 | dev_list_ptr = cast( 302 | dev_list_ptr.contents.next, devlistptrtype 303 | ) 304 | finally: 305 | self.fdll.ftdi_list_free(dev_list_base) 306 | finally: 307 | self.fdll.ftdi_deinit(byref(ctx)) 308 | return devices 309 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | pylibftdi changes 2 | ================= 3 | 4 | 0.23.0 5 | ------ 6 | * Added: #12 - pid & vid selection when opening a new `Device` - thanks @maraxen! 7 | * Minor API change: 8 | * removed use of kwargs for Device() and Driver() arguments, replacing with keyword-only 9 | arguments. Tthis is unlikely to cause issues in most cases, and protects against typos 10 | or incorrect arguments. 11 | * one valid case requiring change is passing in a `libftdi_search` argument; if this is 12 | required, create a driver instance separately (with this parameter) and pass it to 13 | `Device()` using the new `driver` keyword-only parameter. 14 | * Various bumps and fixes to linting dependencies 15 | 16 | 0.22.0 17 | ------ 18 | * Fix: #7 - support for Python3.12 19 | * Fix: #6 - prevent BitBangDevice.port values exceeding 8 bits 20 | * Bumps to linting dependencies (ruff, mypy, black, pylint) 21 | 22 | 0.21.0 23 | ------ 24 | * Breaking changes: 25 | * Python 3.7+ is now required for pylibftdi. 26 | * Driver.libusb_version() and libftdi_version() now return strings rather than bytestrings for textual version info 27 | * Development changes: 28 | * Type hinting has been added - thanks @WhatTheFuzz! 29 | * Moved to a `src` layout. See https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/ 30 | * Switched from `setuptools` to `poetry` for development. 31 | * Linting and style changes (Ruff & black) have been applied throughout. 32 | * Additional unit tests, especially covering library loading. 33 | * Added a Dockerfile and Makefile for development and testing. 34 | * Added GitHub Actions CI for linting and unittests. 35 | 36 | 0.20.0 37 | ------ 38 | * Removed claims of Python2 support, added warnings that Python2 support 39 | will be removed in future releases. 40 | NOTE: Future releases will require Python 3.6 or later. 41 | * Fix to `ftdi_get_library_version`, broken on M1 Macs. See Github issue #2. 42 | Thanks to @wrigby for diagnosing and fixing this. 43 | 44 | 0.19.0 45 | ------ 46 | * Moved to GitHub (and git, obviously!) following deprecation of Mercurial 47 | on bitbucket.org. No other changes beyond URL updates, docs pointing to 48 | GitHub etc. Note that references to old bitbucket issues will stop being 49 | helpful very soon, but there's not much I can do about this. 50 | 51 | 0.18.1 52 | ~~~~~~ 53 | * fixed - list_devices example was broken for Python3 following 0.18.0 54 | 55 | 0.18.0 56 | ~~~~~~ 57 | * fixed - memory leak in list_devices() (Bitbucket issue #34) 58 | * changed - examples now written for (and require) Python3 (Bitbucket issue #35) 59 | * new - more helpful platform-dependent error messages 60 | * added - __main__.py module to support `python -im pylibftdi` 61 | * added - `index` parameter to Device() constructor to allow indexing 62 | into the list returned from Driver.list_devices(). Useful to access 63 | e.g. the second device where PID differs between the first two devices 64 | and device_index would reset to zero for each PID/VID combination. 65 | * Removed the ftdi_osx_driver_reload/unload scripts. These complicated cross- 66 | platform packaging and error messages / docs have been updated to include 67 | appropriate instructions. 68 | 69 | 0.17.0 70 | ~~~~~~ 71 | * new - added 0x6015 USB PID to the default PID list, supporting FT230 devices 72 | * fixed - updated ftdi_osx_driver_re/unload scripts to support Python3 73 | * fixed - auto_detach bug on some systems (Bitbucket issue #27) 74 | * *API CHANGE* - Moved `auto_detach`, `chunk_size` and `lazy_open` to class 75 | attributes which can be easily overridden in subclasses, or in kwargs. 76 | This means that any program using these as *positional* arguments needs 77 | to change to use them as keyword parameters. Note: all but `device_id` and 78 | `mode` may become keyword-only arguments in the future. 79 | 80 | 0.16.1.2 81 | ~~~~~~~~ 82 | * rebuilt to fix install issue on macOS 83 | 84 | 0.16.1.1 85 | ~~~~~~~~ 86 | * changed / fixed - still getting segfault in some situations with 87 | combination of auto_detach=True and libftdi 0.x, so auto_detach 88 | is now only supported in libftdi 1.0+ and ignored otherwise. 89 | 90 | 0.16.1 91 | ~~~~~~ 92 | * fixed - segfault opening `Device` instances when `auto_detach` used and 93 | running on early (pre 1.0) versions of `libftdi`. 94 | 95 | 0.16.0 96 | ~~~~~~ 97 | * new/changed - Automatic driver detach. By default, calls 98 | `libusb_set_auto_detach_kernel_driver()` on newly opened devices, to 99 | restore kernel drivers after close. Note this only seems to have an 100 | effect on Linux, as far as I can tell. 101 | Disable with `Device(..., auto_detach=False)` 102 | * new - serial_transfer example which checks that pseudo-random data send 103 | over a serial connection is correctly transferred. 104 | * new - `modem_status` property on SerialDevice() object, 16-bit value where 105 | each bit represents a different value such as 'Overrun Error'. See docstring 106 | or documentation for ftdi_poll_modem_status function in libftdi. 107 | * fixed - reset FTDI latency timer to default (16ms) on device instantiation 108 | * changed - Python2.6 is no longer supported. 109 | * changed - now uses incremental codecs when doing text-based transfer. 110 | 111 | 0.15 112 | ~~~~ 113 | * new - additional `pylibftdi.Driver` methods: `libusb_version` to report 114 | on the version of the libusb library 115 | * new - `info` example, which displays additional info about the enviroment 116 | pylibftdi is running in; run with `python -m pylibftdi.examples.info` or 117 | similar. 118 | * changed - `libftdi_search` argument to `Driver()` can now give a path name 119 | referencing the libftdi library. 120 | * changed - libftdi is now opened on first use, not immediately when 121 | creating a `Driver` instance. 122 | * changed - missing libraries now raise `pylibftdi.LibraryMissingError`. 123 | This subclasses `FtdiError`, so should remain compatible with existing 124 | usage, but allows this to be caught explicitly. 125 | * fixed - finding the libftdi library on Windows has been improved/fixed; 126 | find_library does not work consistently across Windows / Python 127 | versions... 128 | * More test cases covering bitbang usage 129 | 130 | 0.14.2 131 | ~~~~~~ 132 | * new - optional device_index parameter to `Device()` constructor, select 133 | devices by index rather than (just) serial number / description as per 134 | device_id 135 | 136 | 0.14.1 137 | ~~~~~~ 138 | * install - only install the ftdi_osx_driver_* scripts if we're installing 139 | to an OS X machine 140 | 141 | 0.14 142 | ~~~~ 143 | 144 | * refactor - split the `Device` class from the driver.py module into its 145 | own 'device.py' module 146 | * refactor - fix tests so they work both standalone and when run via e.g. 147 | python -m unittest discover 148 | * change - now resets the flow control and baudrate during device 149 | initialisation 150 | * new - SerialDevice class - supports properties for various serial lines 151 | (CTS, DTS, RI as inputs; DTR, DSR as outputs) 152 | * new - ftdi_osx_driver_{re,un}load scripts to disable / re-enable the 153 | built-in drivers supplied on Mac OS X 154 | * new - midi_output example - using pylibftdi to output MIDI data 155 | 156 | 0.13 157 | ~~~~ 158 | 159 | * When finding and opening a device, don't just present the last error (which 160 | may well be a 'device not found (-3)' error, but more specific ones. Avoids 161 | issues where errors opening a device were being masked as pylibftdi would 162 | continue looking for other devices to open, and report on failure to find 163 | any of them. 164 | * Don't error out on failure during string retrieval part of device 165 | enumeration (list_devices) on failure to read product manufacturer, 166 | description or serial number (bitbucket issue #10) 167 | * Fix Driver.libftdi_version() for older library versions (which don't report 168 | version information...) 169 | * USB_PID_LIST now defaults to 0x6001, 0x6010, 0x6011, 0x6014. The new 170 | entries are 0x6010 and 0x6011. Note that USB_PID has been removed, and 171 | USB_VID has been replaced with FTDI_VENDOR_ID 172 | * Updated documentation, specifically regarding use on OS X Mavericks 173 | * Added serial_loopback example test, for investigating ongoing issues with 174 | UB232H module serial loopback losses... 175 | 176 | 0.12 177 | ~~~~ 178 | * Optimisation on accessing port property when direction is set to ALL_OUTPUTS 179 | - don't bother reading the pins first. Makes ``port`` identical to ``latch`` 180 | in this case. 181 | * Support for libftdi 1.x, used by preference over ftdi 0.x. The specific 182 | driver search can be given as a string (or tuple of strings) to the 183 | ``Driver`` constructor, e.g. the default is equivalent to:: 184 | 185 | >>> d = Driver() 186 | >>> # equivalent to... 187 | >>> d = Driver(('ftdi1', 'libftdi1', 'ftdi', 'libftdi')) 188 | >>> # and if we wanted to use ftdi 0.x on Linux: 189 | >>> d = Driver('ftdi') 190 | 191 | The driver in use can be determined by ``Driver.libftdi_version()`` method:: 192 | 193 | >>> from pylibftdi import Driver 194 | >>> Driver().libftdi_version() 195 | (1, 0, 0, '1.0', 'v1.0-6-gafb9082') 196 | >>> Driver('ftdi').libftdi_version() 197 | (0, 99, 0, '0.99', 'v0.17-305-g50d77f8') 198 | 199 | * Support for MS Windows (which requires the 'lib' prefix on ftdi searching - 200 | previously absent). 201 | * Minor bug fixes 202 | 203 | 0.11 204 | ~~~~ 205 | 206 | * API changes 207 | * Device() parameter 'interface' has been renamed to 'interface_select', 208 | as it wasn't clear what this meant. Symbolic constants 'INTERFACE_ANY', 209 | 'INTERFACE_A'/B/C/D are provided in the pylibftdi package namespace. 210 | * Device() parameter 'buffer_size' has been renamed to 'chunk_size' because 211 | there isn't any buffering going on, so it was potentially misleading. 212 | * new 'latch' property on BitBangDevices reflects current state of output 213 | latch, for better defined behaviour on read-modify-write operations on 214 | mixed-direction ports. 215 | * fixed the bit_server example; run as 216 | python -m pylibftdi.examples.bit_server 217 | * docstring clean-ups 218 | * BitBangDevice now reads device pin state on startup, so read/rmw operations 219 | across process runs now work as expected; for example previously running:: 220 | 221 | >>> from pylibftdi import BitBangDevice 222 | >>> d = BitBangDevice() 223 | >>> d.port = 1 224 | >>> ^D 225 | 226 | # restart interpreter 227 | 228 | >>> from pylibftdi import BitBangDevice 229 | >>> d = BitBangDevice() 230 | >>> d.port |= 2 231 | 232 | would have cleared D0, but now keeps it high (so d.port == 3) 233 | * setup.py will now use setuptools/distribute if installed, allowing the 234 | 'develop' target. 235 | 236 | 0.10.3 237 | ~~~~~~ 238 | * lots more documentation, cleaned up, on readthedocs. Still in progress though. 239 | * Experimental support for multi-device interfaces ``Device(interface=1)`` 240 | 241 | 0.10.2 242 | ~~~~~~ 243 | * include examples subpackage 244 | * ensure examples work under Python3 245 | * 'python -m pylibftdi.examples.led_flash' should now work and pulse D0 246 | 247 | 0.10.1 248 | ~~~~~~ 249 | * maintenance build to fix installation issues 250 | * USB_VID_LIST/USB_PID_LIST exposed directly as pylibftdi attributes 251 | 252 | 0.10 253 | ~~~~ 254 | * support for FT232H via iteration over lists USB_VID_LIST/USB_PID_LIST in 255 | driver.py (FT232H uses different PID) 256 | * support for 'buffering' - i.e. chunking reads and writes to allow 257 | interruption (i.e. KeyboardInterrupt) in what could otherwise be a very long 258 | uninterruptable extension code call. 259 | * reset device to serial mode on open() (previously could be left in bitbang mode) 260 | * moved docs -> /doc 261 | * moved pylibftdi/tests -> /tests - makes python -m unittest discover etc work better 262 | * removed legacy support for Driver() to acting as a Device() 263 | * API for Bus class now requires .device rather than .driver (this still needs 264 | improving at some point) 265 | 266 | 0.9 267 | ~~~ 268 | * improved and extended tests 269 | * made a start on some Sphinx-based documentation 270 | * fixed long-standing issue with input from a BitBangDevice 271 | * allow the PID/VID to be changed 272 | * add new examples 273 | * basic web server to toggle / read IO bits 274 | * magic_candle.py - example of using input and output together 275 | * pin_read.py - read and match values on device pins 276 | * All of this needs some tidying, but it fixes some key issues and has been unreleased too long. 277 | 278 | 0.8.1 279 | ~~~~~ 280 | * fix issue with bitbang following API changes in 0.8 281 | * add tests for bitbang mode 282 | * refactor tests; fix text-based tests in Python3 283 | * slight refactor Device() to improve testability (_read and _write methods) 284 | 285 | 0.8 286 | ~~~ 287 | * added some unit tests 288 | * API changes: 289 | * when opening a device with a device_id parameter, this will now check 290 | against both serial number and (if that fails), the device description. 291 | Opening by device type (under the same proviso that an arbitrary device 292 | will be selected if multiple matching devices are attached as when no 293 | device_id is given) is frequently easier than matching by serial number. 294 | * added flush(), flush_input() and flush_output() operations. This is 295 | modelled after the pySerial API providing separate flush operations, and 296 | gets the Device API closer to that of files. 297 | * increased file-API compatibility, with line-oriented methods and iteration 298 | 299 | 0.7 300 | ~~~ 301 | * support multiple attached devices 302 | * API changes: 303 | * separation between driver and device. Generally, now use Device() / 304 | BitBangDevice() rather than Driver / BitBangDriver(), though older code 305 | _should_ still work via some shims (though with Deprecation warnings). 306 | * first parameter in Device() and BitBangDevice() is a device ID - the serial 307 | number string of the target device. 308 | * open() is generally no longer required on Devices. This is to more closely 309 | model the file() API. 310 | 311 | 0.6 312 | ~~~ 313 | * same source now works on both Python2.6+ and Python3. 314 | 315 | 0.5 316 | ~~~ 317 | * fix various bugs found by pyflakes 318 | * improve module organisation, while providing interface compatibility 319 | 320 | 0.4.1 321 | ~~~~~ 322 | * fix release issue 323 | 324 | 0.4 325 | ~~~ 326 | * fixed embarrassing bug which caused things not to work on Linux 327 | (is now find_library('ftdi') instead of find_library('libftdi')) 328 | * lots of error checking, new FtdiError exception. Before it just 329 | tended to segfault if things weren't just-so. 330 | * get_error() is now get_error_string(). It's still early enough 331 | to change the API, and if I thought it was get_error_string 332 | multiple times when I wrote the error checking code, it probably 333 | should be the more natural thing. 334 | 335 | 0.3 336 | ~~~ 337 | * added some examples 338 | * new Bus class in pylibftdi (though it probably belongs somewhere else) 339 | * first release on PyPI 340 | 341 | 0.2 342 | ~~~ 343 | * fixed various bugs 344 | * added ftdi_fn and set_baudrate functions in Driver class 345 | * changed interface in BitBangDriver to direction/port properties 346 | rather than overriding the read/write functions, which are therefore 347 | still available as in the Driver class. 348 | 349 | 0.1 350 | ~~~ 351 | * first release. Tested with libftdi 0.18 on Mac OS X 10.6 and Linux 352 | (stock EEEPC 701 Xandros Linux, Ubuntu 10.04) 353 | -------------------------------------------------------------------------------- /src/pylibftdi/device.py: -------------------------------------------------------------------------------- 1 | """ 2 | pylibftdi.device - access to individual FTDI devices 3 | 4 | Copyright (c) 2010-2020 Ben Bass 5 | See LICENSE file for details and (absence of) warranty 6 | 7 | pylibftdi: https://github.com/codedstructure/pylibftdi 8 | 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import codecs 14 | import functools 15 | import itertools 16 | import os 17 | import sys 18 | from ctypes import ( 19 | POINTER, 20 | Structure, 21 | byref, 22 | c_char_p, 23 | c_void_p, 24 | cast, 25 | create_string_buffer, 26 | ) 27 | from typing import no_type_check 28 | 29 | from pylibftdi._base import FtdiError 30 | from pylibftdi.driver import ( 31 | BITMODE_RESET, 32 | FLUSH_BOTH, 33 | FLUSH_INPUT, 34 | FLUSH_OUTPUT, 35 | FTDI_ERROR_DEVICE_NOT_FOUND, 36 | USB_PID_LIST, 37 | USB_VID_LIST, 38 | Driver, 39 | ) 40 | 41 | ERR_HELP_NOT_FOUND_FAIL = """ 42 | No device matching the given specification could be found. 43 | Is the device connected? 44 | 45 | Try running the following command to see if the device is listed: 46 | 47 | python3 -m pylibftdi.examples.list_devices 48 | """ 49 | 50 | ERR_HELP_LINUX_OPEN_FAIL = """ 51 | Could not access the FTDI device - this could be a permissions 52 | issue accessing the device. 53 | 54 | If the program works when run with root privileges (i.e. sudo) 55 | this is likely to be the issue. Running as a normal user should 56 | be possible by setting appropriate udev rules on the device. 57 | """ 58 | 59 | ERR_HELP_CLAIM_FAIL = """ 60 | Could not claim the FTDI USB device - either the device is 61 | already open, or another driver is preventing libftdi from 62 | claiming the device. 63 | """ 64 | 65 | ERR_HELP_LINUX_CLAIM_FAIL = ( 66 | ERR_HELP_CLAIM_FAIL 67 | + """ 68 | The Linux `ftdi_sio` driver is often the culprit here, and may be 69 | unloaded with `sudo rmmod ftdi_sio`. However in recent libftdi 70 | versions this should not be necessary, as a driver option to 71 | switch out the driver temporarily is applied (unless 72 | `auto_detach=False` is given in `Device` instantiation). 73 | """ 74 | ) 75 | 76 | ERR_HELP_MACOS_CLAIM_FAIL = ( 77 | ERR_HELP_CLAIM_FAIL 78 | + """ 79 | The following commands may be attempted in the terminal to unload 80 | the builtin drivers: 81 | 82 | sudo kextunload -bundle-id com.apple.driver.AppleUSBFTDI 83 | sudo kextunload -bundle-id com.FTDI.driver.FTDIUSBSerialDriver 84 | 85 | Reload these with the command 'kextload' replacing 'kextunload' above. 86 | 87 | Note the second of these will only be present if the FTDI-provided 88 | driver has been installed from their website: 89 | 90 | https://www.ftdichip.com/Drivers/VCP.htm 91 | """ 92 | ) 93 | 94 | 95 | # The only part of the ftdi context we need at this point is 96 | # libusb_device_handle, so we don't encode the entire structure. 97 | # Note the structure for 0.x is different (no libusb_context 98 | # member), but we don't support auto_detach on 0.x which is 99 | # the only case this is used. 100 | class ftdi_context_partial(Structure): 101 | # This is for libftdi 1.0+ 102 | _fields_ = [("libusb_context", c_void_p), ("libusb_device_handle", c_void_p)] 103 | 104 | 105 | class Device: 106 | """ 107 | Represents a connection to a single FTDI device 108 | """ 109 | 110 | # If false, don't open the device as part of instantiation 111 | lazy_open = False 112 | 113 | # chunk_size (if not 0) chunks the reads and writes 114 | # to allow interruption 115 | chunk_size = 0 116 | 117 | # auto_detach is a flag to call libusb_set_auto_detach_kernel_driver 118 | # when we open the device 119 | auto_detach = True 120 | 121 | # defining softspace allows us to 'print' to this device 122 | softspace = 0 123 | 124 | def __init__( 125 | self, 126 | device_id: str | None = None, 127 | mode: str = "b", 128 | encoding: str = "latin1", 129 | interface_select: int | None = None, 130 | device_index: int = 0, 131 | *, 132 | auto_detach: bool | None = None, 133 | lazy_open: bool | None = None, 134 | chunk_size: int | None = None, 135 | index: int | None = None, 136 | vid: int | None = None, 137 | pid: int | None = None, 138 | driver: Driver | None = None, 139 | ) -> None: 140 | """ 141 | Device([device_id[, mode, [OPTIONS ...]]) -> Device instance 142 | 143 | represents a single FTDI device accessible via the libftdi driver. 144 | Supports a basic file-like interface (open/close/read/write, context 145 | manager support). 146 | 147 | :param device_id: an optional serial number of the device to open. 148 | if omitted, this refers to the first device found, which is 149 | convenient if only one device is attached, but otherwise 150 | fairly useless. 151 | 152 | :param mode: either 'b' (binary) or 't' (text). This primarily affects 153 | Python 3 calls to read() and write(), which will accept/return 154 | unicode strings which will be encoded/decoded according to the given... 155 | 156 | :param encoding: the codec name to be used for text operations. 157 | 158 | :param interface_select: select interface to use on multi-interface devices 159 | 160 | :param device_index: optional index of the device to open, in the 161 | event of multiple matches for other parameters (PID, VID, 162 | device_id). Defaults to zero (the first device found). 163 | 164 | The following parameters are only available as keyword parameters 165 | and override class attributes, so may be specified in subclasses. 166 | 167 | :param lazy_open: if True, then the device will not be opened immediately - 168 | the user must perform an explicit open() call prior to other 169 | operations. 170 | 171 | :param chunk_size: if non-zero, split read and write operations into chunks 172 | of this size. With large or slow accesses, interruptions (i.e. 173 | KeyboardInterrupt) may not happen in a timely fashion. 174 | 175 | :param auto_detach: default True, whether to automatically re-attach 176 | the kernel driver on device close. 177 | 178 | :param index: optional index into list_devices() to open. 179 | Useful in the event that multiple devices of differing VID/PID 180 | are attached, where `device_index` is insufficient to select 181 | as device indexing restarts at 0 for each VID/PID combination. 182 | 183 | :param vid: optional vendor ID to open. If omitted, the default USB_VID_LIST 184 | is used to search for devices. 185 | 186 | :param pid: optional product ID to open. If omitted, the default USB_PID_LIST 187 | is used to search for devices. 188 | """ 189 | self._opened = False 190 | 191 | # These args allow overriding default class attributes, which can 192 | # also be overridden in subclasses. 193 | if auto_detach is not None: 194 | self.auto_detach = auto_detach 195 | if lazy_open is not None: 196 | self.lazy_open = lazy_open 197 | if chunk_size is not None: 198 | self.chunk_size = chunk_size 199 | 200 | self.driver = Driver() if driver is None else driver 201 | self.fdll = self.driver.fdll 202 | # device_id is an optional serial number of the requested device. 203 | self.device_id = device_id 204 | # mode can be either 'b' for binary, or 't' for text. 205 | # if set to text, the values returned from read() will 206 | # be decoded using encoding before being returned as 207 | # strings; for binary the raw bytes will be returned. 208 | self.mode = mode 209 | # when giving a str to Device.write(), it is encoded. 210 | # default is latin1, because it provides 211 | # a one-to-one correspondence for code points 0-FF 212 | self.encoding = encoding 213 | self.encoder = codecs.getincrementalencoder(self.encoding)() 214 | self.decoder = codecs.getincrementaldecoder(self.encoding)() 215 | # ftdi_usb_open_dev initialises the device baudrate 216 | # to 9600, which certainly seems to be a de-facto 217 | # standard for serial devices. 218 | self._baudrate = 9600 219 | # interface can be set for devices which have multiple interface 220 | # ports (e.g. FT4232, FT2232) 221 | self.interface_select = interface_select 222 | # device_index is an optional integer index of device to choose 223 | self.device_index = device_index 224 | # list_index (from parameter `index`) is an optional integer index 225 | # into list_devices() entries. 226 | self.list_index = index 227 | self.vid = vid 228 | self.pid = pid 229 | 230 | # lazy_open tells us not to open immediately. 231 | if not self.lazy_open: 232 | self.open() 233 | 234 | def __del__(self) -> None: 235 | """free the ftdi_context resource""" 236 | if self._opened: 237 | self.close() 238 | 239 | def open(self) -> None: 240 | """ 241 | open connection to a FTDI device 242 | """ 243 | if self._opened: 244 | return 245 | 246 | if not self.device_id and self.list_index is not None: 247 | # Use serial number from list_index 248 | dev_list = self.driver.list_devices() 249 | try: 250 | # The third (index 2) field is serial number. 251 | self.device_id = dev_list[self.list_index][2] 252 | except IndexError: 253 | raise FtdiError( 254 | "index provided not in range of list_devices() entries" 255 | ) from None 256 | 257 | # create context for this device 258 | # Note I gave up on attempts to use ftdi_new/ftdi_free (just using 259 | # ctx instead of byref(ctx) in first param of most ftdi_* functions) as 260 | # (at least for 64-bit) they only worked if argtypes was declared 261 | # (c_void_p for ctx), and that's too much like hard work to maintain. 262 | # So I've reverted to using create_string_buffer for memory management, 263 | # byref(ctx) to pass in the context instance, and ftdi_init() / 264 | # ftdi_deinit() pair to manage the driver resources. It's very nice 265 | # how layered the libftdi code is, with access to each layer. 266 | self.ctx = create_string_buffer(1024) 267 | res = self.fdll.ftdi_init(byref(self.ctx)) 268 | if res != 0: 269 | msg = "%s (%d)" % (self.get_error_string(), res) 270 | del self.ctx 271 | raise FtdiError(msg) 272 | 273 | if self.interface_select is not None: 274 | res = self.fdll.ftdi_set_interface(byref(self.ctx), self.interface_select) 275 | if res != 0: 276 | msg = "%s (%d)" % (self.get_error_string(), res) 277 | del self.ctx 278 | raise FtdiError(msg) 279 | 280 | # Try to open the device. If this fails, reset things to how 281 | # they were, but we can't use self.close as that assumes things 282 | # have already been setup. 283 | res = self._open_device() 284 | 285 | if res != 0: 286 | msg = self.handle_open_error(res) 287 | # free the context 288 | self.fdll.ftdi_deinit(byref(self.ctx)) 289 | del self.ctx 290 | raise FtdiError(msg) 291 | 292 | if self.auto_detach and self.driver.libftdi_version().major > 0: 293 | # This doesn't reliably work on libftdi 0.x, so we ignore it 294 | ctx_p = cast(byref(self.ctx), POINTER(ftdi_context_partial)).contents 295 | dev = ctx_p.libusb_device_handle 296 | if dev: 297 | self.driver._libusb.libusb_set_auto_detach_kernel_driver( 298 | c_void_p(dev), 1 299 | ) 300 | 301 | # explicitly reset the device to serial mode with standard settings 302 | # - no flow control, 9600 baud - in case it had previously been used 303 | # in bitbang mode (some later driver versions might do bits of this 304 | # automatically) 305 | self.ftdi_fn.ftdi_set_bitmode(0, BITMODE_RESET) 306 | self.ftdi_fn.ftdi_setflowctrl(0) 307 | self.baudrate = 9600 308 | # reset the latency timer to 16ms (device default, but kernel device 309 | # drivers can set a different - e.g. 1ms - value) 310 | self.ftdi_fn.ftdi_set_latency_timer(16) 311 | self._opened = True 312 | 313 | def handle_open_error(self, errcode: int) -> str: 314 | """ 315 | return a (hopefully helpful) error message on a failed open() 316 | """ 317 | err_help = "" 318 | if errcode == -3: 319 | err_help = ERR_HELP_NOT_FOUND_FAIL 320 | elif errcode == -4 and sys.platform == "linux": 321 | err_help = ERR_HELP_LINUX_OPEN_FAIL 322 | elif errcode == -5: 323 | if sys.platform == "linux": 324 | err_help = ERR_HELP_LINUX_CLAIM_FAIL 325 | elif sys.platform == "darwin": 326 | err_help = ERR_HELP_MACOS_CLAIM_FAIL 327 | else: 328 | err_help = ERR_HELP_CLAIM_FAIL 329 | msg = "%s (%d)\n%s" % (self.get_error_string(), errcode, err_help) 330 | return msg 331 | 332 | def _open_device(self) -> int: 333 | """ 334 | Actually open the target device 335 | 336 | :return: status of the open command (0 = success) 337 | :rtype: int 338 | """ 339 | # FTDI vendor/product ids required here. 340 | res: int = -1 341 | vid_list = [self.vid] if self.vid is not None else USB_VID_LIST 342 | pid_list = [self.pid] if self.pid is not None else USB_PID_LIST 343 | for usb_vid, usb_pid in itertools.product(vid_list, pid_list): 344 | open_args = [byref(self.ctx), usb_vid, usb_pid, 0, 0, self.device_index] 345 | if self.device_id is None: 346 | res = self.fdll.ftdi_usb_open_desc_index(*tuple(open_args)) 347 | else: 348 | # attempt to match device_id to serial number 349 | open_args[-2] = c_char_p(self.device_id.encode("latin1")) 350 | res = self.fdll.ftdi_usb_open_desc_index(*tuple(open_args)) 351 | if res != 0: 352 | # swap (description, serial) parameters and try again 353 | # - attempt to match device_id to description 354 | open_args[-3], open_args[-2] = open_args[-2], open_args[-3] 355 | res = self.fdll.ftdi_usb_open_desc_index(*tuple(open_args)) 356 | if res != FTDI_ERROR_DEVICE_NOT_FOUND: 357 | # if we succeed (0) or get a specific error, don't continue 358 | # otherwise (-3) - look for another device 359 | break 360 | 361 | return res 362 | 363 | def close(self) -> None: 364 | """close our connection, free resources""" 365 | if self._opened: 366 | self.fdll.ftdi_usb_close(byref(self.ctx)) 367 | self.fdll.ftdi_deinit(byref(self.ctx)) 368 | del self.ctx 369 | self._opened = False 370 | 371 | @property 372 | def baudrate(self) -> int: 373 | """ 374 | get or set the baudrate of the FTDI device. Re-read after setting 375 | to ensure baudrate was accepted by the driver. 376 | """ 377 | return self._baudrate 378 | 379 | @baudrate.setter 380 | def baudrate(self, value: int) -> None: 381 | result = self.fdll.ftdi_set_baudrate(byref(self.ctx), value) 382 | if result == 0: 383 | self._baudrate = value 384 | 385 | def _read(self, length: int) -> bytes: 386 | """ 387 | actually do the low level reading 388 | 389 | :return: bytes read from the device 390 | :rtype: bytes 391 | """ 392 | buf = create_string_buffer(length) 393 | rlen = self.fdll.ftdi_read_data(byref(self.ctx), byref(buf), length) 394 | if rlen < 0: 395 | raise FtdiError(self.get_error_string()) 396 | byte_data = buf.raw[:rlen] 397 | 398 | return byte_data 399 | 400 | def read(self, length: int) -> str | bytes: 401 | """ 402 | read(length) -> bytes/string of up to `length` bytes. 403 | 404 | read upto `length` bytes from the FTDI device 405 | :param length: maximum number of bytes to read 406 | :return: value read from device 407 | :rtype: bytes if self.mode is 'b', else decode with self.encoding 408 | """ 409 | if not self._opened: 410 | raise FtdiError("read() on closed Device") 411 | 412 | # read the data 413 | if self.chunk_size != 0: 414 | remaining = length 415 | byte_data_list = [] 416 | while remaining > 0: 417 | rx_bytes = self._read(min(remaining, self.chunk_size)) 418 | if not rx_bytes: 419 | break 420 | byte_data_list.append(rx_bytes) 421 | remaining -= len(rx_bytes) 422 | byte_data = b"".join(byte_data_list) 423 | else: 424 | byte_data = self._read(length) 425 | if self.mode == "b": 426 | return byte_data 427 | else: 428 | return self.decoder.decode(byte_data) 429 | 430 | def _write(self, byte_data: bytes) -> int: 431 | """ 432 | actually do the low level writing 433 | 434 | :param byte_data: data to be written 435 | :type byte_data: bytes 436 | :return: number of bytes written 437 | """ 438 | buf = create_string_buffer(byte_data) 439 | written: int = self.fdll.ftdi_write_data( 440 | byref(self.ctx), byref(buf), len(byte_data) 441 | ) 442 | if written < 0: 443 | raise FtdiError(self.get_error_string()) 444 | return written 445 | 446 | def write(self, data: str | bytes) -> int: 447 | """ 448 | write(data) -> count of bytes actually written 449 | 450 | write given `data` string to the FTDI device 451 | 452 | :param data: string to be written 453 | :type data: string or bytes 454 | :return: count of bytes written, which may be less than `len(data)` 455 | """ 456 | if not self._opened: 457 | raise FtdiError("write() on closed Device") 458 | 459 | if not isinstance(data, bytes): 460 | data = self.encoder.encode(data) 461 | 462 | assert isinstance(data, bytes) 463 | 464 | # actually write it 465 | if self.chunk_size != 0: 466 | remaining = len(data) 467 | written = 0 468 | while remaining > 0: 469 | start = written 470 | length = min(remaining, self.chunk_size) 471 | result = self._write(data[start : start + length]) 472 | if result == 0: 473 | # don't continue to try writing forever if nothing 474 | # is actually being written 475 | break 476 | else: 477 | written += result 478 | remaining -= result 479 | else: 480 | written = self._write(data) 481 | return written 482 | 483 | def flush(self, flush_what: int = FLUSH_BOTH) -> None: 484 | """ 485 | Instruct the FTDI device to flush its FIFO buffers 486 | 487 | By default both the input and output buffers will be 488 | flushed, but the caller can selectively chose to only 489 | flush the input or output buffers using `flush_what`: 490 | 491 | :param flush_what: select what to flush: 492 | `FLUSH_BOTH` (default); 493 | `FLUSH_INPUT` (just the rx buffer); 494 | `FLUSH_OUTPUT` (just the tx buffer) 495 | """ 496 | 497 | if flush_what == FLUSH_BOTH: 498 | fn = self.fdll.ftdi_tcioflush 499 | elif flush_what == FLUSH_INPUT: 500 | fn = self.fdll.ftdi_tciflush 501 | elif flush_what == FLUSH_OUTPUT: 502 | fn = self.fdll.ftdi_tcoflush 503 | else: 504 | raise ValueError( 505 | f"Invalid value passed to {self.__class__.__name__}.flush()" 506 | ) 507 | 508 | res = fn(byref(self.ctx)) 509 | if res != 0: 510 | msg = "%s (%d)" % (self.get_error_string(), res) 511 | raise FtdiError(msg) 512 | 513 | def flush_input(self) -> None: 514 | """ 515 | flush the device input buffer 516 | """ 517 | self.flush(FLUSH_INPUT) 518 | 519 | def flush_output(self) -> None: 520 | """ 521 | flush the device output buffer 522 | """ 523 | self.flush(FLUSH_OUTPUT) 524 | 525 | def get_error_string(self) -> str: 526 | """ 527 | :return: error string from libftdi driver 528 | """ 529 | return str(self.fdll.ftdi_get_error_string(byref(self.ctx))) 530 | 531 | @property 532 | def ftdi_fn(self): # type: ignore 533 | """ 534 | this allows the vast majority of libftdi functions 535 | which are called with a pointer to a ftdi_context 536 | struct as the first parameter to be called here 537 | preventing the need to leak self.ctx into the user 538 | code (and import byref from ctypes): 539 | 540 | >>> with Device() as dev: 541 | ... # set 8 bit data, 2 stop bits, no parity 542 | ... dev.ftdi_fn.ftdi_set_line_property(8, 2, 0) 543 | ... 544 | """ 545 | 546 | # note this class is constructed on each call, so this 547 | # won't be particularly quick. It does ensure that the 548 | # fdll and ctx objects in the closure are up-to-date, though. 549 | class FtdiForwarder: 550 | @no_type_check 551 | def __getattr__(innerself, key: str): 552 | return functools.partial(getattr(self.fdll, key), byref(self.ctx)) 553 | 554 | return FtdiForwarder() 555 | 556 | def __enter__(self) -> Device: 557 | """ 558 | support for context manager. 559 | 560 | Note the device is opened and closed automatically 561 | when used in a with statement, and the device object 562 | itself is returned: 563 | >>> with Device(mode='t') as dev: 564 | ... dev.write('Hello World!') 565 | ... 566 | """ 567 | self.open() 568 | return self 569 | 570 | @no_type_check 571 | def __exit__(self, exc_type, exc_val, tb) -> None: 572 | """support for context manager""" 573 | self.close() 574 | 575 | # 576 | # following are various properties and functions to make 577 | # this emulate a file-object more closely. 578 | # 579 | 580 | @property 581 | def closed(self) -> bool: 582 | """ 583 | The Python file API defines a read-only 'closed' attribute 584 | """ 585 | return not self._opened 586 | 587 | def readline(self, size: int = 0) -> str: 588 | """ 589 | readline() for file-like compatibility. 590 | 591 | :param size: maximum amount of data to read looking for a line 592 | :return: a line of text, or size bytes if no line-ending found 593 | 594 | This only works for mode='t' on Python3 595 | """ 596 | lsl = len(os.linesep) 597 | line_buffer: list[str] = [] 598 | while True: 599 | next_char = self.read(1) 600 | if not isinstance(next_char, str): 601 | raise TypeError(".readline() only works for mode='t'") 602 | if not next_char or (0 < size < len(line_buffer)): 603 | break 604 | line_buffer.append(next_char) 605 | if len(line_buffer) >= lsl and line_buffer[-lsl:] == list(os.linesep): 606 | break 607 | return "".join(line_buffer) 608 | 609 | def readlines(self, sizehint: int | None = None) -> list[str]: 610 | """ 611 | readlines() for file-like compatibility. 612 | """ 613 | lines: list[str] = [] 614 | if sizehint is not None: 615 | string_blob = self.read(sizehint) 616 | if not isinstance(string_blob, str): 617 | raise TypeError(".readlines() only works for mode='t'") 618 | lines.extend(string_blob.splitlines()) 619 | 620 | while True: 621 | line = self.readline() 622 | if not line: 623 | break 624 | lines.append(line) 625 | return lines 626 | 627 | def writelines(self, lines: list[str | bytes]) -> None: 628 | """ 629 | writelines for file-like compatibility. 630 | 631 | :param lines: sequence of lines to write 632 | """ 633 | for line in lines: 634 | self.write(line) 635 | 636 | def __iter__(self) -> Device: 637 | return self 638 | 639 | def __next__(self) -> str: 640 | while True: 641 | line = self.readline() 642 | if line: 643 | return line 644 | else: 645 | raise StopIteration 646 | 647 | next = __next__ 648 | --------------------------------------------------------------------------------