pax_global_header00006660000000000000000000000064152706070070014516gustar00rootroot0000000000000052 comment=e82b08df1cd0f438d8a88d7b82b58dc8afade531 vsergeev-python-periphery-e82b08d/000077500000000000000000000000001527060700700173065ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/.flake8000066400000000000000000000000501527060700700204540ustar00rootroot00000000000000[flake8] extend-ignore = E501,F401,E402 vsergeev-python-periphery-e82b08d/.github/000077500000000000000000000000001527060700700206465ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/.github/workflows/000077500000000000000000000000001527060700700227035ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/.github/workflows/tests.yml000066400000000000000000000026351527060700700245760ustar00rootroot00000000000000name: Tests on: - push - pull_request jobs: build: strategy: fail-fast: false matrix: container: ["python:2.7-slim", "python:3.5-slim", "python:3.6-slim", "python:3.7-slim", "python:3.8-slim", "python:3.9-slim", "python:3.10-slim", "python:3.11-slim", "python:3.12-slim", "python:3.13-slim", "pypy:2.7-slim", "pypy:3.11-slim"] include: - check-types: true - container: "python:2.7-slim" check-types: false - container: "python:3.5-slim" check-types: false - container: "pypy:2.7-slim" check-types: false - container: "pypy:3.11-slim" check-types: false runs-on: ubuntu-24.04 container: ${{ matrix.container }} steps: - uses: actions/checkout@v4 - name: Upgrade pip run: python -m pip install --upgrade pip - name: Install mypy if: ${{ matrix.check-types }} run: python -m pip install mypy - name: Check Types if: ${{ matrix.check-types }} run: mypy periphery - name: Run tests run: | python --version python -m tests.test_gpio python -m tests.test_gpio_sysfs python -m tests.test_spi python -m tests.test_i2c python -m tests.test_mmio python -m tests.test_serial python -m tests.test_led python -m tests.test_pwm vsergeev-python-periphery-e82b08d/.gitignore000066400000000000000000000001011527060700700212660ustar00rootroot00000000000000*.swp *.pyc docs/_build/ build/ dist/ python_periphery.egg-info/ vsergeev-python-periphery-e82b08d/.readthedocs.yaml000066400000000000000000000002501527060700700225320ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/conf.py python: install: - requirements: docs/requirements.txt vsergeev-python-periphery-e82b08d/CHANGELOG.md000066400000000000000000000124301527060700700211170ustar00rootroot00000000000000* v2.5.0 - 10/29/2026 * I2C * Add support for reading in-place to `transfer()`. * SPI * Add support for reading in-place to `transfer()`. * Add `transfer_advanced()` method for transferring multiple messages, with optional deselect between messages, deselect delay, and word delay. * MMIO * Add `read64()` and `write64()` methods. * GPIO * Add `event_clock` property. * Add `debounce_us` property. * Serial * Fix blocking `poll()` for `timeout=None`. * LED * Add `trigger` and `triggers` properties. * Contributors * Julien Muchembled (@jmuchemb) - 203c22c * v2.4.3 - 10/05/2026 * Re-release with fixed wheel package. * v2.4.2 - 09/15/2026 * I2C * Fix buffer size for `I2C_FUNCS` `ioctl()`. * Contributors * Florian Larysch (@florolf) - bb7c82e * v2.4.1 - 04/21/2023 * GPIO * Fix realtime timestamp reporting for line events in gpio-cdev v2 implementation. * v2.4.0 - 04/17/2023 * GPIO * Avoid writing `direction` and `inverted` on open to retain existing state with sysfs GPIOs * Add support for gpio-cdev v2 ABI. * Add type stubs. * Contributors * Juho Kim, @AussieSeaweed - ea1ecc2 * v2.3.0 - 02/14/2021 * GPIO * Add kernel version check for line bias support. * Fix docstring for `close()`. * SPI * Add kernel version check for 32-bit mode support. * MMIO * Fix duplicate transactions in integral read and write methods. * Fix memory offset of `pointer` property. * Contributors * Michael Murton, @CrazyIvan359 - 9c1a4f3 * @paul-demo - b318a6a * v2.2.0 - 12/16/2020 * MMIO * Add `path` keyword argument to constructor for use with alternate memory character devices (e.g. `/dev/gpiomem`). * SPI * Add support for 32-bit flags to `extra_flags` property and constructor. * v2.1.1 - 11/19/2020 * GPIO * Add direction checks for improved error reporting to `write()`, `read_event()`, and `poll()` for character device GPIOs. * Contributors * Michael Murton, @CrazyIvan359 - 69bd36e * v2.1.0 - 05/29/2020 * GPIO * Add `poll_multiple()` static method. * Add line consumer `label` property. * Add line `bias`, line `drive`, and `inverted` properties. * Add additional properties as keyword arguments to constructor for character device GPIOs. * Only unexport GPIO in `close()` if exported in open for sysfs GPIOs. * Improve wording and fix typos in docstrings. * Serial * Fix performance of blocking read in `read()`. * Raise exception on unexpected empty read in `read()`, which may be caused by a serial port disconnect. * Add `vmin` and `vtime` properties for the corresponding termios settings. * Add support for termios timeout with `read()`. * Improve wording in docstrings. * Contributors * @xrombik - 444f778 * Alexander Steffen, @webmeister - f0403da * v2.0.1 - 01/08/2020 * PWM * Add retry loop for opening PWM period file after export to accommodate delayed udev permission rule application. * Contributors * Jonas Larsson, @jonasl - 28653d4 * v2.0.0 - 10/28/2019 * GPIO * Add support for character device GPIOs. * Remove support for preserve direction from GPIO constructor. * Add retry loop to direction write after export to accommodate delayed udev permission rule application for sysfs GPIOs. * Unexport GPIO line on close for sysfs GPIOs. * Fix handling of `timeout=None` with sysfs GPIO `poll()`. * Add `devpath` property. * PWM * Fix chip and channel argument names in PWM constructor and documentation. * Add retry loop to PWM open after export to accommodate delayed creation of sysfs files by kernel driver. * Unexport PWM channel on close. * Add nanosecond `period_ns` and `duty_cycle_ns` properties. * Add `devpath` property. * LED * Raise `LookupError` instead of `ValueError` if LED name is not found during open. * Add `devpath` property. * Fix exception handling for Python 2 with `ioctl()` operations in Serial, SPI, and I2C modules. * Fix `with` statement context manager support for all modules. * Update tests with running hints for Raspberry Pi 3. * Contributors * Uwe Kleine-König, @ukleinek - 0005260 * Heath Robinson, @ubiquitousthey - ac457d6 * v1.1.2 - 06/25/2019 * Add LICENSE file to packaging. * v1.1.1 - 04/03/2018 * Fix handling of delayed pin directory export when opening a GPIO. * v1.1.0 - 10/24/2016 * Add support for preserving pin direction when opening GPIO. * Improve GPIO poll() implementation to work with more platforms. * Improve atomicity of MMIO fixed width writes. * Add PWM module. * Add LED module. * Add support for universal wheel packaging. * Contributors * Sanket Dasgupta - 8ac7b40 * Joseph Kogut - 022ef29, d2e9132 * Hector Martin - 1e3343a * Francesco Valla - 34b3877 * v1.0.0 - 06/25/2015 * Initial release. vsergeev-python-periphery-e82b08d/LICENSE000066400000000000000000000021131527060700700203100ustar00rootroot00000000000000 Copyright (c) 2015-2026 vsergeev / Ivan (Vanya) A. Sergeev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. vsergeev-python-periphery-e82b08d/README.md000066400000000000000000000130351527060700700205670ustar00rootroot00000000000000# python-periphery [![Tests Status](https://github.com/vsergeev/python-periphery/actions/workflows/tests.yml/badge.svg)](https://github.com/vsergeev/python-periphery/actions/workflows/tests.yml) [![Docs Status](https://readthedocs.org/projects/python-periphery/badge/)](https://python-periphery.readthedocs.io/en/latest/) [![GitHub release](https://img.shields.io/github/release/vsergeev/python-periphery.svg?maxAge=7200)](https://github.com/vsergeev/python-periphery) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/vsergeev/python-periphery/blob/master/LICENSE) ## Linux Peripheral I/O (GPIO, LED, PWM, SPI, I2C, MMIO, Serial) with Python 2 & 3 python-periphery is a pure Python library for GPIO, LED, PWM, SPI, I2C, MMIO, and Serial peripheral I/O interface access in userspace Linux. It is useful in embedded Linux environments (including Raspberry Pi, BeagleBone, etc. platforms) for interfacing with external peripherals. python-periphery is compatible with Python 2 and Python 3, is written in pure Python, and is MIT licensed. Using Lua or C? Check out the [lua-periphery](https://github.com/vsergeev/lua-periphery) and [c-periphery](https://github.com/vsergeev/c-periphery) projects. Contributed libraries: [java-periphery](https://github.com/sgjava/java-periphery), [dart_periphery](https://github.com/pezi/dart_periphery) ## Installation With pip: ``` text pip install python-periphery ``` With easy_install: ``` text easy_install python-periphery ``` With setup.py: ``` text git clone https://github.com/vsergeev/python-periphery.git cd python-periphery python setup.py install ``` ## Examples ### GPIO ``` python from periphery import GPIO # Open GPIO /dev/gpiochip0 line 10 with input direction gpio_in = GPIO("/dev/gpiochip0", 10, "in") # Open GPIO /dev/gpiochip0 line 12 with output direction gpio_out = GPIO("/dev/gpiochip0", 12, "out") value = gpio_in.read() gpio_out.write(not value) gpio_in.close() gpio_out.close() ``` [Go to GPIO documentation.](https://python-periphery.readthedocs.io/en/latest/gpio.html) ### LED ``` python from periphery import LED # Open LED "led0" with initial state off led0 = LED("led0", False) # Open LED "led1" with initial state on led1 = LED("led1", True) value = led0.read() led1.write(value) # Set custom brightness level led1.write(led1.max_brightness / 2) led0.close() led1.close() ``` [Go to LED documentation.](https://python-periphery.readthedocs.io/en/latest/led.html) ### PWM ``` python from periphery import PWM # Open PWM chip 0, channel 10 pwm = PWM(0, 10) # Set frequency to 1 kHz pwm.frequency = 1e3 # Set duty cycle to 75% pwm.duty_cycle = 0.75 pwm.enable() # Change duty cycle to 50% pwm.duty_cycle = 0.50 pwm.close() ``` [Go to PWM documentation.](https://python-periphery.readthedocs.io/en/latest/pwm.html) ### SPI ``` python from periphery import SPI # Open spidev1.0 with mode 0 and max speed 1MHz spi = SPI("/dev/spidev1.0", 0, 1000000) data_out = [0xaa, 0xbb, 0xcc, 0xdd] data_in = spi.transfer(data_out) print("shifted out [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_out)) print("shifted in [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_in)) spi.close() ``` [Go to SPI documentation.](https://python-periphery.readthedocs.io/en/latest/spi.html) ### I2C ``` python from periphery import I2C # Open i2c-0 controller i2c = I2C("/dev/i2c-0") # Read byte at address 0x100 of EEPROM at 0x50 msgs = [I2C.Message([0x01, 0x00]), I2C.Message([0x00], read=True)] i2c.transfer(0x50, msgs) print("0x100: 0x{:02x}".format(msgs[1].data[0])) i2c.close() ``` [Go to I2C documentation.](https://python-periphery.readthedocs.io/en/latest/i2c.html) ### MMIO ``` python from periphery import MMIO # Open am335x real-time clock subsystem page rtc_mmio = MMIO(0x44E3E000, 0x1000) # Read current time rtc_secs = rtc_mmio.read32(0x00) rtc_mins = rtc_mmio.read32(0x04) rtc_hrs = rtc_mmio.read32(0x08) print("hours: {:02x} minutes: {:02x} seconds: {:02x}".format(rtc_hrs, rtc_mins, rtc_secs)) rtc_mmio.close() # Open am335x control module page ctrl_mmio = MMIO(0x44E10000, 0x1000) # Read MAC address mac_id0_lo = ctrl_mmio.read32(0x630) mac_id0_hi = ctrl_mmio.read32(0x634) print("MAC address: {:04x}{:08x}".format(mac_id0_lo, mac_id0_hi)) ctrl_mmio.close() ``` [Go to MMIO documentation.](https://python-periphery.readthedocs.io/en/latest/mmio.html) ### Serial ``` python from periphery import Serial # Open /dev/ttyUSB0 with baudrate 115200, and defaults of 8N1, no flow control serial = Serial("/dev/ttyUSB0", 115200) serial.write(b"Hello World!") # Read up to 128 bytes with 500ms timeout buf = serial.read(128, 0.5) print("read {:d} bytes: _{:s}_".format(len(buf), buf)) serial.close() ``` [Go to Serial documentation.](https://python-periphery.readthedocs.io/en/latest/serial.html) ## Documentation Documentation is hosted at [https://python-periphery.readthedocs.io](https://python-periphery.readthedocs.io). To build documentation locally with Sphinx, run: ``` cd docs make html ``` Sphinx will produce the HTML documentation in `docs/_build/html/`. Run `make help` to see other output targets (LaTeX, man, text, etc.). ## Testing The tests located in the [tests](tests/) folder may be run under Python to test the correctness and functionality of python-periphery. Some tests require interactive probing (e.g. with an oscilloscope), the installation of a physical loopback, or the existence of a particular device on a bus. See the usage of each test for more details on the required setup. ## License python-periphery is MIT licensed. See the included [LICENSE](LICENSE) file. vsergeev-python-periphery-e82b08d/docs/000077500000000000000000000000001527060700700202365ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/docs/Makefile000066400000000000000000000011721527060700700216770ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) vsergeev-python-periphery-e82b08d/docs/conf.py000066400000000000000000000031141527060700700215340ustar00rootroot00000000000000# Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. import os import sys sys.path.insert(0, os.path.abspath('..')) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = u'python-periphery' copyright = u'2015-2026, vsergeev / Ivan (Vanya) A. Sergeev' author = u'Vanya A. Sergeev' release = '2.5.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx_rtd_theme', ] autoclass_content = 'init' autodoc_member_order = 'bysource' templates_path = ['_templates'] exclude_patterns = ['_build'] pygments_style = 'sphinx' # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = 'sphinx_rtd_theme' html_static_path = ['_static'] html_show_sourcelink = False vsergeev-python-periphery-e82b08d/docs/gpio.rst000066400000000000000000000016741527060700700217360ustar00rootroot00000000000000GPIO ==== Code Example ------------ .. code-block:: python from periphery import GPIO # Open GPIO /dev/gpiochip0 line 10 with input direction gpio_in = GPIO("/dev/gpiochip0", 10, "in") # Open GPIO /dev/gpiochip0 line 12 with output direction gpio_out = GPIO("/dev/gpiochip0", 12, "out") value = gpio_in.read() gpio_out.write(not value) gpio_in.close() gpio_out.close() API --- .. class:: periphery.GPIO(path, line, direction) :noindex: .. autoclass:: periphery.gpio_cdev2.Cdev2GPIO :noindex: .. class:: periphery.GPIO(line, direction) :noindex: .. autoclass:: periphery.SysfsGPIO :noindex: .. autoclass:: periphery.GPIO :members: :undoc-members: :show-inheritance: .. autoclass:: periphery.EdgeEvent :members: :undoc-members: :show-inheritance: .. autoclass:: periphery.GPIOError :members: :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/docs/i2c.rst000066400000000000000000000011401527060700700214410ustar00rootroot00000000000000I2C === Code Example ------------ .. code-block:: python from periphery import I2C # Open i2c-0 controller i2c = I2C("/dev/i2c-0") # Read byte at address 0x100 of EEPROM at 0x50 msgs = [I2C.Message([0x01, 0x00]), I2C.Message([0x00], read=True)] i2c.transfer(0x50, msgs) print("0x100: 0x{:02x}".format(msgs[1].data[0])) i2c.close() API --- .. autoclass:: periphery.I2C :members: transfer, close, fd, devpath, Message :undoc-members: :show-inheritance: .. autoclass:: periphery.I2CError :members: :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/docs/index.rst000066400000000000000000000014601527060700700221000ustar00rootroot00000000000000.. periphery documentation master file, created by sphinx-quickstart2 on Sat Jun 20 18:30:50 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to python-periphery's documentation! ============================================ python-periphery is a pure Python library for GPIO, LED, PWM, SPI, I2C, MMIO, and Serial peripheral I/O interface access in userspace Linux. It is useful in embedded Linux environments (including Raspberry Pi, BeagleBone, etc. platforms) for interfacing with external peripherals. python-periphery is compatible with Python 2 and Python 3, is written in pure Python, and is MIT licensed. Contents -------- .. toctree:: :maxdepth: 1 gpio led pwm spi i2c mmio serial version vsergeev-python-periphery-e82b08d/docs/led.rst000066400000000000000000000014421527060700700215350ustar00rootroot00000000000000LED ==== Code Example ------------ .. code-block:: python from periphery import LED # Open LED "led0" with initial state off led0 = LED("led0", False) # Open LED "led1" with initial state on led1 = LED("led1", True) value = led0.read() led1.write(value) # Set custom brightness level led1.write(led1.max_brightness / 2) # Set trigger led1.trigger = "heartbeat" # Query active trigger and available triggers print("active trigger:", led1.trigger) print("available triggers:", led1.triggers) led0.close() led1.close() API --- .. autoclass:: periphery.LED :members: :undoc-members: :show-inheritance: .. autoclass:: periphery.LEDError :members: :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/docs/mmio.rst000066400000000000000000000016531527060700700217360ustar00rootroot00000000000000MMIO ==== Code Example ------------ .. code-block:: python from periphery import MMIO # Open am335x real-time clock subsystem page rtc_mmio = MMIO(0x44E3E000, 0x1000) # Read current time rtc_secs = rtc_mmio.read32(0x00) rtc_mins = rtc_mmio.read32(0x04) rtc_hrs = rtc_mmio.read32(0x08) print("hours: {:02x} minutes: {:02x} seconds: {:02x}".format(rtc_hrs, rtc_mins, rtc_secs)) rtc_mmio.close() # Open am335x control module page ctrl_mmio = MMIO(0x44E10000, 0x1000) # Read MAC address mac_id0_lo = ctrl_mmio.read32(0x630) mac_id0_hi = ctrl_mmio.read32(0x634) print("MAC address: {:04x}{:08x}".format(mac_id0_lo, mac_id0_hi)) ctrl_mmio.close() API --- .. autoclass:: periphery.MMIO :members: :undoc-members: :show-inheritance: .. autoclass:: periphery.MMIOError :members: :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/docs/pwm.rst000066400000000000000000000010501527060700700215670ustar00rootroot00000000000000PWM === Code Example ------------ .. code-block:: python from periphery import PWM # Open PWM chip 0, channel 10 pwm = PWM(0, 10) # Set frequency to 1 kHz pwm.frequency = 1e3 # Set duty cycle to 75% pwm.duty_cycle = 0.75 pwm.enable() # Change duty cycle to 50% pwm.duty_cycle = 0.50 pwm.close() API --- .. autoclass:: periphery.PWM :members: :undoc-members: :show-inheritance: .. autoclass:: periphery.PWMError :members: :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/docs/requirements.txt000066400000000000000000000000421527060700700235160ustar00rootroot00000000000000sphinx>=2 sphinx-rtd-theme>=1.2.0 vsergeev-python-periphery-e82b08d/docs/serial.rst000066400000000000000000000011671527060700700222540ustar00rootroot00000000000000Serial ====== Code Example ------------ .. code-block:: python from periphery import Serial # Open /dev/ttyUSB0 with baudrate 115200, and defaults of 8N1, no flow control serial = Serial("/dev/ttyUSB0", 115200) serial.write(b"Hello World!") # Read up to 128 bytes with 500ms timeout buf = serial.read(128, 0.5) print("read {:d} bytes: _{:s}_".format(len(buf), buf)) serial.close() API --- .. autoclass:: periphery.Serial :members: :undoc-members: :show-inheritance: .. autoclass:: periphery.SerialError :members: :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/docs/spi.rst000066400000000000000000000012171527060700700215640ustar00rootroot00000000000000SPI === Code Example ------------ .. code-block:: python from periphery import SPI # Open spidev1.0 with mode 0 and max speed 1MHz spi = SPI("/dev/spidev1.0", 0, 1000000) data_out = [0xaa, 0xbb, 0xcc, 0xdd] data_in = spi.transfer(data_out) print("shifted out [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_out)) print("shifted in [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_in)) spi.close() API --- .. autoclass:: periphery.SPI :members: :undoc-members: :show-inheritance: .. autoclass:: periphery.SPIError :members: :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/docs/version.rst000066400000000000000000000002771527060700700224630ustar00rootroot00000000000000Version and Helper Functions ---------------------------- .. automodule:: periphery :members: __version__, version, sleep, sleep_ms, sleep_us :undoc-members: :show-inheritance: vsergeev-python-periphery-e82b08d/periphery/000077500000000000000000000000001527060700700213155ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/periphery/__init__.py000066400000000000000000000020061527060700700234240ustar00rootroot00000000000000import time __version__ = "2.5.0" "Module version string." version = (2, 5, 0) "Module version tuple." def sleep(seconds): """Sleep for the specified number of seconds. Args: seconds (int, long, float): duration in seconds. """ time.sleep(seconds) def sleep_ms(milliseconds): """Sleep for the specified number of milliseconds. Args: milliseconds (int, long, float): duration in milliseconds. """ time.sleep(milliseconds / 1000.0) def sleep_us(microseconds): """Sleep for the specified number of microseconds. Args: microseconds (int, long, float): duration in microseconds. """ time.sleep(microseconds / 1000000.0) from periphery.gpio import GPIO, SysfsGPIO, CdevGPIO, EdgeEvent, GPIOError from periphery.led import LED, LEDError from periphery.pwm import PWM, PWMError from periphery.spi import SPI, SPIError from periphery.i2c import I2C, I2CError from periphery.mmio import MMIO, MMIOError from periphery.serial import Serial, SerialError vsergeev-python-periphery-e82b08d/periphery/__init__.pyi000066400000000000000000000013131527060700700235750ustar00rootroot00000000000000from periphery.gpio import ( GPIO as GPIO, CdevGPIO as CdevGPIO, EdgeEvent as EdgeEvent, GPIOError as GPIOError, SysfsGPIO as SysfsGPIO, ) from periphery.i2c import I2C as I2C, I2CError as I2CError from periphery.led import LED as LED, LEDError as LEDError from periphery.mmio import MMIO as MMIO, MMIOError as MMIOError from periphery.pwm import PWM as PWM, PWMError as PWMError from periphery.serial import Serial as Serial, SerialError as SerialError from periphery.spi import SPI as SPI, SPIError as SPIError __version__: str version: tuple[int, int, int] def sleep(seconds: float) -> None: ... def sleep_ms(milliseconds: float) -> None: ... def sleep_us(microseconds: float) -> None: ... vsergeev-python-periphery-e82b08d/periphery/gpio.py000066400000000000000000000252271527060700700226350ustar00rootroot00000000000000import collections import os import select class GPIOError(IOError): """Base class for GPIO errors.""" pass class EdgeEvent(collections.namedtuple('EdgeEvent', ['edge', 'timestamp'])): """EdgeEvent containing the event edge and event time reported by Linux. Attributes: edge (str): event edge, either "rising" or "falling". timestamp (int): event time in nanoseconds. """ class GPIO(object): def __new__(cls, *args, **kwargs): if len(args) > 2: return CdevGPIO.__new__(cls, *args, **kwargs) else: return SysfsGPIO.__new__(cls, *args, **kwargs) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, value, traceback): self.close() # Methods def read(self): """Read the state of the GPIO. Returns: bool: ``True`` for high state, ``False`` for low state. Raises: GPIOError: if an I/O or OS error occurs. """ raise NotImplementedError() def write(self, value): """Set the state of the GPIO to `value`. Args: value (bool): ``True`` for high state, ``False`` for low state. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `value` type is not bool. """ raise NotImplementedError() def poll(self, timeout=None): """Poll a GPIO for the edge event configured with the .edge property with an optional timeout. For character device GPIOs, the edge event should be consumed with `read_event()`. For sysfs GPIOs, the edge event should be consumed with `read()`. `timeout` can be a positive number for a timeout in seconds, zero for a non-blocking poll, or negative or None for a blocking poll. Default is a blocking poll. Args: timeout (int, float, None): timeout duration in seconds. Returns: bool: ``True`` if an edge event occurred, ``False`` on timeout. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `timeout` type is not None or int. """ raise NotImplementedError() def read_event(self): """Read the edge event that occurred with the GPIO. This method is intended for use with character device GPIOs and is unsupported by sysfs GPIOs. Returns: EdgeEvent: a namedtuple containing the string edge event that occurred (either ``"rising"`` or ``"falling"``), and the event time reported by Linux in nanoseconds. Raises: GPIOError: if an I/O or OS error occurs. NotImplementedError: if called on a sysfs GPIO. """ raise NotImplementedError() @staticmethod def poll_multiple(gpios, timeout=None): """Poll multiple GPIOs for the edge event configured with the .edge property with an optional timeout. For character device GPIOs, the edge event should be consumed with `read_event()`. For sysfs GPIOs, the edge event should be consumed with `read()`. `timeout` can be a positive number for a timeout in seconds, zero for a non-blocking poll, or negative or None for a blocking poll. Default is a blocking poll. Args: gpios (list): list of GPIO objects to poll. timeout (int, float, None): timeout duration in seconds. Returns: list: list of GPIO objects for which an edge event occurred. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `timeout` type is not None or int. """ if not isinstance(timeout, (int, float, type(None))): raise TypeError("Invalid timeout type, should be integer, float, or None.") # Setup poll p = select.poll() # Register GPIO file descriptors and build map of fd to object fd_gpio_map = {} for gpio in gpios: if isinstance(gpio, SysfsGPIO): p.register(gpio.fd, select.POLLPRI | select.POLLERR) else: p.register(gpio.fd, select.POLLIN | select.POLLRDNORM) fd_gpio_map[gpio.fd] = gpio # Scale timeout to milliseconds if isinstance(timeout, (int, float)) and timeout > 0: timeout *= 1000 # Poll events = p.poll(timeout) # Gather GPIOs that had edge events occur results = [] for (fd, _) in events: gpio = fd_gpio_map[fd] results.append(gpio) if isinstance(gpio, SysfsGPIO): # Rewind for read try: os.lseek(fd, 0, os.SEEK_SET) except OSError as e: raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) return results def close(self): """Close the GPIO. Raises: GPIOError: if an I/O or OS error occurs. """ raise NotImplementedError() # Immutable properties @property def devpath(self): """Get the device path of the underlying GPIO device. :type: str """ raise NotImplementedError() @property def fd(self): """Get the line file descriptor of the GPIO object. :type: int """ raise NotImplementedError() @property def line(self): """Get the GPIO object's line number. :type: int """ raise NotImplementedError() @property def name(self): """Get the line name of the GPIO. This method is intended for use with character device GPIOs and always returns the empty string for sysfs GPIOs. :type: str """ raise NotImplementedError() @property def label(self): """Get the line consumer label of the GPIO. This method is intended for use with character device GPIOs and always returns the empty string for sysfs GPIOs. :type: str """ raise NotImplementedError() @property def chip_fd(self): """Get the GPIO chip file descriptor of the GPIO object. This method is intended for use with character device GPIOs and is unsupported by sysfs GPIOs. Raises: NotImplementedError: if accessed on a sysfs GPIO. :type: int """ raise NotImplementedError() @property def chip_name(self): """Get the name of the GPIO chip associated with the GPIO. :type: str """ raise NotImplementedError() @property def chip_label(self): """Get the label of the GPIO chip associated with the GPIO. :type: str """ raise NotImplementedError() # Mutable properties def _get_direction(self): raise NotImplementedError() def _set_direction(self, direction): raise NotImplementedError() direction = property(_get_direction, _set_direction) """Get or set the GPIO's direction. Can be "in", "out", "high", "low". Direction "in" is input; "out" is output, initialized to low; "high" is output, initialized to high; and "low" is output, initialized to low. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `direction` type is not str. ValueError: if `direction` value is invalid. :type: str """ def _get_edge(self): raise NotImplementedError() def _set_edge(self, edge): raise NotImplementedError() edge = property(_get_edge, _set_edge) """Get or set the GPIO's interrupt edge. Can be "none", "rising", "falling", "both". Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `edge` type is not str. ValueError: if `edge` value is invalid. :type: str """ def _get_event_clock(self): raise NotImplementedError() def _set_event_clock(self, event_clock): raise NotImplementedError() event_clock = property(_get_event_clock, _set_event_clock) """Get or set the GPIO's event clock. Can be "realtime", "monotonic", "hte". This property is not supported by sysfs GPIOs. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `event_clock` type is not str. ValueError: if `event_clock` value is invalid. :type: str """ def _get_debounce_us(self): raise NotImplementedError() def _set_debounce_us(self, debounce_us): raise NotImplementedError() debounce_us = property(_get_debounce_us, _set_debounce_us) """Get or set the GPIO's debounce period in microseconds. This property is not supported by sysfs GPIOs, nor by ABI version 1 character device GPIOs. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `debounce_us` type is not int. :type: int """ def _get_bias(self): raise NotImplementedError() def _set_bias(self, bias): raise NotImplementedError() bias = property(_get_bias, _set_bias) """Get or set the GPIO's line bias. Can be "default", "pull_up", "pull_down", "disable". This property is not supported by sysfs GPIOs. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `bias` type is not str. ValueError: if `bias` value is invalid. :type: str """ def _get_drive(self): raise NotImplementedError() def _set_drive(self, drive): raise NotImplementedError() drive = property(_get_drive, _set_drive) """Get or set the GPIO's line drive. Can be "default" (for push-pull), "open_drain", "open_source". This property is not supported by sysfs GPIOs. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `drive` type is not str. ValueError: if `drive` value is invalid. :type: str """ def _get_inverted(self): raise NotImplementedError() def _set_inverted(self, inverted): raise NotImplementedError() inverted = property(_get_inverted, _set_inverted) """Get or set the GPIO's inverted (active low) property. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `inverted` type is not bool. :type: bool """ # String representation def __str__(self): """Get the string representation of the GPIO. :type: str """ raise NotImplementedError() # Assign GPIO classes from . import gpio_cdev1 from . import gpio_cdev2 from . import gpio_sysfs CdevGPIO = gpio_cdev2.Cdev2GPIO if gpio_cdev2.Cdev2GPIO.SUPPORTED else gpio_cdev1.Cdev1GPIO SysfsGPIO = gpio_sysfs.SysfsGPIO vsergeev-python-periphery-e82b08d/periphery/gpio.pyi000066400000000000000000000037421527060700700230040ustar00rootroot00000000000000from types import TracebackType from typing import Any class GPIOError(IOError): ... class EdgeEvent: def __new__(cls, edge: str, timestamp: int) -> EdgeEvent: ... # noqa: Y034 class GPIO: def __new__(cls, *args: Any, **kwargs: Any) -> GPIO: ... # noqa: Y034 def __del__(self) -> None: ... def __enter__(self) -> GPIO: ... # noqa: Y034 def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... def read(self) -> bool: ... def write(self, value: bool) -> None: ... def poll(self, timeout: float | None = ...) -> bool: ... def read_event(self) -> EdgeEvent: ... @staticmethod def poll_multiple(gpios: list[GPIO], timeout: float | None = ...) -> list[GPIO]: ... def close(self) -> None: ... @property def devpath(self) -> str: ... @property def fd(self) -> int: ... @property def line(self) -> int: ... @property def name(self) -> str: ... @property def label(self) -> str: ... @property def chip_fd(self) -> int: ... @property def chip_name(self) -> str: ... @property def chip_label(self) -> str: ... direction: property edge: property event_clock: property debounce_us: property bias: property drive: property inverted: property class CdevGPIO(GPIO): def __init__( # pyright: ignore [reportInconsistentConstructor] self, path: str, line: int | str, direction: str, edge: str = ..., bias: str = ..., drive: str = ..., inverted: bool = ..., label: str | None = ..., event_clock: str = ..., debounce_us: int = ..., ) -> None: ... def __new__(self, path: str, line: int | str, direction: str, **kwargs: Any) -> CdevGPIO: ... # noqa: Y034 class SysfsGPIO(GPIO): def __init__(self, line: int, direction: str) -> None: ... def __new__(self, line: int, direction: str) -> SysfsGPIO: ... # noqa: Y034 vsergeev-python-periphery-e82b08d/periphery/gpio_cdev1.py000066400000000000000000000515441527060700700237200ustar00rootroot00000000000000import platform import ctypes import fcntl import os import select from .gpio import GPIO, GPIOError, EdgeEvent try: KERNEL_VERSION = tuple([int(s) for s in platform.release().split(".")[:2]]) except ValueError: KERNEL_VERSION = (0, 0) _GPIO_NAME_MAX_SIZE = 32 _GPIOHANDLES_MAX = 64 class _CGpiochipInfo(ctypes.Structure): _fields_ = [ ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('lines', ctypes.c_uint32), ] class _CGpiolineInfo(ctypes.Structure): _fields_ = [ ('line_offset', ctypes.c_uint32), ('flags', ctypes.c_uint32), ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('consumer', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ] class _CGpiohandleRequest(ctypes.Structure): _fields_ = [ ('lineoffsets', ctypes.c_uint32 * _GPIOHANDLES_MAX), ('flags', ctypes.c_uint32), ('default_values', ctypes.c_uint8 * _GPIOHANDLES_MAX), ('consumer_label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('lines', ctypes.c_uint32), ('fd', ctypes.c_int), ] class _CGpiohandleData(ctypes.Structure): _fields_ = [ ('values', ctypes.c_uint8 * _GPIOHANDLES_MAX), ] class _CGpioeventRequest(ctypes.Structure): _fields_ = [ ('lineoffset', ctypes.c_uint32), ('handleflags', ctypes.c_uint32), ('eventflags', ctypes.c_uint32), ('consumer_label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('fd', ctypes.c_int), ] class _CGpioeventData(ctypes.Structure): _fields_ = [ ('timestamp', ctypes.c_uint64), ('id', ctypes.c_uint32), ] class Cdev1GPIO(GPIO): # Constants scraped from _GPIOHANDLE_GET_LINE_VALUES_IOCTL = 0xc040b408 _GPIOHANDLE_SET_LINE_VALUES_IOCTL = 0xc040b409 _GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 _GPIO_GET_LINEINFO_IOCTL = 0xc048b402 _GPIO_GET_LINEHANDLE_IOCTL = 0xc16cb403 _GPIO_GET_LINEEVENT_IOCTL = 0xc030b404 _GPIOHANDLE_REQUEST_INPUT = 0x1 _GPIOHANDLE_REQUEST_OUTPUT = 0x2 _GPIOHANDLE_REQUEST_ACTIVE_LOW = 0x4 _GPIOHANDLE_REQUEST_OPEN_DRAIN = 0x8 _GPIOHANDLE_REQUEST_OPEN_SOURCE = 0x10 _GPIOHANDLE_REQUEST_BIAS_PULL_UP = 0x20 _GPIOHANDLE_REQUEST_BIAS_PULL_DOWN = 0x40 _GPIOHANDLE_REQUEST_BIAS_DISABLE = 0x80 _GPIOEVENT_REQUEST_RISING_EDGE = 0x1 _GPIOEVENT_REQUEST_FALLING_EDGE = 0x2 _GPIOEVENT_REQUEST_BOTH_EDGES = 0x3 _GPIOEVENT_EVENT_RISING_EDGE = 0x1 _GPIOEVENT_EVENT_FALLING_EDGE = 0x2 _SUPPORTS_LINE_BIAS = KERNEL_VERSION >= (5, 5) def __init__(self, path, line, direction, edge="none", bias="default", drive="default", inverted=False, label=None): """**Character device GPIO (ABI version 1)** Instantiate a GPIO object and open the character device GPIO with the specified line and direction at the specified GPIO chip path (e.g. "/dev/gpiochip0"). Default properties can be overridden with keyword arguments. Args: path (str): GPIO chip character device path. line (int, str): GPIO line number or name. direction (str): GPIO direction, can be "in", "out", "high", or "low". edge (str): GPIO interrupt edge, can be "none", "rising", "falling", or "both". bias (str): GPIO line bias, can be "default", "pull_up", "pull_down", or "disable". drive (str): GPIO line drive, can be "default", "open_drain", or "open_source". inverted (bool): GPIO is inverted (active low). label (str, None): GPIO line consumer label. Returns: Cdev1GPIO: GPIO object. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `path`, `line`, `direction`, `edge`, `bias`, `drive`, `inverted`, or `label` types are invalid. ValueError: if `direction`, `edge`, `bias`, or `drive` value is invalid. LookupError: if the GPIO line was not found by the provided name. """ self._devpath = None self._line = None self._line_fd = None self._chip_fd = None self._direction = None self._edge = None self._bias = None self._drive = None self._inverted = None self._label = None self._open(path, line, direction, edge, bias, drive, inverted, label) def __new__(self, path, line, direction, **kwargs): return object.__new__(Cdev1GPIO) def _open(self, path, line, direction, edge, bias, drive, inverted, label): if not isinstance(path, str): raise TypeError("Invalid path type, should be string.") if not isinstance(line, (int, str)): raise TypeError("Invalid line type, should be integer or string.") if not isinstance(direction, str): raise TypeError("Invalid direction type, should be string.") elif direction not in ["in", "out", "high", "low"]: raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") if not isinstance(edge, str): raise TypeError("Invalid edge type, should be string.") elif edge not in ["none", "rising", "falling", "both"]: raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") if not isinstance(bias, str): raise TypeError("Invalid bias type, should be string.") elif bias not in ["default", "pull_up", "pull_down", "disable"]: raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") if not isinstance(drive, str): raise TypeError("Invalid drive type, should be string.") elif drive not in ["default", "open_drain", "open_source"]: raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") if not isinstance(inverted, bool): raise TypeError("Invalid inverted type, should be bool.") if not isinstance(label, (type(None), str)): raise TypeError("Invalid label type, should be None or str.") if isinstance(line, str): line = self._find_line_by_name(path, line) # Open GPIO chip try: self._chip_fd = os.open(path, 0) except OSError as e: raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) self._devpath = path self._line = line self._label = label.encode() if label is not None else b"periphery" self._reopen(direction, edge, bias, drive, inverted) def _reopen(self, direction, edge, bias, drive, inverted): flags = 0 if bias != "default" and not Cdev1GPIO._SUPPORTS_LINE_BIAS: raise GPIOError(None, "Line bias configuration not supported by kernel version {}.{}.".format(*KERNEL_VERSION)) elif bias == "pull_up": flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_BIAS_PULL_UP elif bias == "pull_down": flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_BIAS_PULL_DOWN elif bias == "disable": flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_BIAS_DISABLE if drive == "open_drain": flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_OPEN_DRAIN elif drive == "open_source": flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_OPEN_SOURCE if inverted: flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_ACTIVE_LOW # FIXME this should really use GPIOHANDLE_SET_CONFIG_IOCTL instead of # closing and reopening, especially to preserve output value on # configuration changes # Close existing line if self._line_fd is not None: try: os.close(self._line_fd) except OSError as e: raise GPIOError(e.errno, "Closing existing GPIO line: " + e.strerror) self._line_fd = None if direction == "in": if edge == "none": request = _CGpiohandleRequest() request.lineoffsets[0] = self._line request.flags = flags | Cdev1GPIO._GPIOHANDLE_REQUEST_INPUT request.consumer_label = self._label request.lines = 1 try: fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEHANDLE_IOCTL, request) except (OSError, IOError) as e: raise GPIOError(e.errno, "Opening input line handle: " + e.strerror) self._line_fd = request.fd else: request = _CGpioeventRequest() request.lineoffset = self._line request.handleflags = flags | Cdev1GPIO._GPIOHANDLE_REQUEST_INPUT request.eventflags = Cdev1GPIO._GPIOEVENT_REQUEST_RISING_EDGE if edge == "rising" else Cdev1GPIO._GPIOEVENT_REQUEST_FALLING_EDGE if edge == "falling" else Cdev1GPIO._GPIOEVENT_REQUEST_BOTH_EDGES request.consumer_label = self._label try: fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEEVENT_IOCTL, request) except (OSError, IOError) as e: raise GPIOError(e.errno, "Opening input line event handle: " + e.strerror) self._line_fd = request.fd else: request = _CGpiohandleRequest() initial_value = True if direction == "high" else False initial_value ^= inverted request.lineoffsets[0] = self._line request.flags = flags | Cdev1GPIO._GPIOHANDLE_REQUEST_OUTPUT request.default_values[0] = initial_value request.consumer_label = self._label request.lines = 1 try: fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEHANDLE_IOCTL, request) except (OSError, IOError) as e: raise GPIOError(e.errno, "Opening output line handle: " + e.strerror) self._line_fd = request.fd self._direction = "in" if direction == "in" else "out" self._edge = edge self._bias = bias self._drive = drive self._inverted = inverted def _find_line_by_name(self, path, line): # Open GPIO chip try: fd = os.open(path, 0) except OSError as e: raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) # Get chip info for number of lines chip_info = _CGpiochipInfo() try: fcntl.ioctl(fd, Cdev1GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) # Get each line info line_info = _CGpiolineInfo() found = False for i in range(chip_info.lines): line_info.line_offset = i try: fcntl.ioctl(fd, Cdev1GPIO._GPIO_GET_LINEINFO_IOCTL, line_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) if line_info.name.decode() == line: found = True break try: os.close(fd) except OSError as e: raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) if found: return i raise LookupError("Opening GPIO line: GPIO line \"{:s}\" not found by name.".format(line)) # Methods def read(self): data = _CGpiohandleData() try: fcntl.ioctl(self._line_fd, Cdev1GPIO._GPIOHANDLE_GET_LINE_VALUES_IOCTL, data) except (OSError, IOError) as e: raise GPIOError(e.errno, "Getting line value: " + e.strerror) return bool(data.values[0]) def write(self, value): if not isinstance(value, bool): raise TypeError("Invalid value type, should be bool.") elif self._direction != "out": raise GPIOError(None, "Invalid operation: cannot write to input GPIO") data = _CGpiohandleData() data.values[0] = value try: fcntl.ioctl(self._line_fd, Cdev1GPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data) except (OSError, IOError) as e: raise GPIOError(e.errno, "Setting line value: " + e.strerror) def poll(self, timeout=None): if not isinstance(timeout, (int, float, type(None))): raise TypeError("Invalid timeout type, should be integer, float, or None.") elif self._direction != "in": raise GPIOError(None, "Invalid operation: cannot poll output GPIO") # Setup poll p = select.poll() p.register(self._line_fd, select.POLLIN | select.POLLPRI | select.POLLERR) # Scale timeout to milliseconds if isinstance(timeout, (int, float)) and timeout > 0: timeout *= 1000 # Poll events = p.poll(timeout) return len(events) > 0 def read_event(self): if self._direction != "in": raise GPIOError(None, "Invalid operation: cannot read event of output GPIO") elif self._edge == "none": raise GPIOError(None, "Invalid operation: GPIO edge not set") try: buf = os.read(self._line_fd, ctypes.sizeof(_CGpioeventData)) except OSError as e: raise GPIOError(e.errno, "Reading GPIO event: " + e.strerror) event_data = _CGpioeventData.from_buffer_copy(buf) if event_data.id == Cdev1GPIO._GPIOEVENT_EVENT_RISING_EDGE: edge = "rising" elif event_data.id == Cdev1GPIO._GPIOEVENT_EVENT_FALLING_EDGE: edge = "falling" else: edge = "none" timestamp = event_data.timestamp return EdgeEvent(edge, timestamp) def close(self): try: if self._line_fd is not None: os.close(self._line_fd) except OSError as e: raise GPIOError(e.errno, "Closing GPIO line: " + e.strerror) try: if self._chip_fd is not None: os.close(self._chip_fd) except OSError as e: raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) self._line_fd = None self._chip_fd = None self._edge = "none" self._direction = "in" self._line = None # Immutable properties @property def devpath(self): return self._devpath @property def fd(self): return self._line_fd @property def line(self): return self._line @property def name(self): line_info = _CGpiolineInfo() line_info.line_offset = self._line try: fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEINFO_IOCTL, line_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) return line_info.name.decode() @property def label(self): line_info = _CGpiolineInfo() line_info.line_offset = self._line try: fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEINFO_IOCTL, line_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) return line_info.consumer.decode() @property def chip_fd(self): return self._chip_fd @property def chip_name(self): chip_info = _CGpiochipInfo() try: fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) return chip_info.name.decode() @property def chip_label(self): chip_info = _CGpiochipInfo() try: fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) return chip_info.label.decode() # Mutable properties def _get_direction(self): return self._direction def _set_direction(self, direction): if not isinstance(direction, str): raise TypeError("Invalid direction type, should be string.") if direction not in ["in", "out", "high", "low"]: raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") if self._direction == direction: return self._reopen(direction, "none", self._bias, self._drive, self._inverted) direction = property(_get_direction, _set_direction) def _get_edge(self): return self._edge def _set_edge(self, edge): if not isinstance(edge, str): raise TypeError("Invalid edge type, should be string.") if edge not in ["none", "rising", "falling", "both"]: raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") if self._direction != "in": raise GPIOError(None, "Invalid operation: cannot set edge on output GPIO") if self._edge == edge: return self._reopen(self._direction, edge, self._bias, self._drive, self._inverted) edge = property(_get_edge, _set_edge) def _get_event_clock(self): return "realtime" def _set_event_clock(self, event_clock): if not isinstance(event_clock, str): raise TypeError("Invalid event_clock type, should be string.") if event_clock not in ["realtime", "monotonic", "hte"]: raise ValueError("Invalid event_clock, can be: \"realtime\", \"monotonic\", \"hte\".") if event_clock != "realtime": raise GPIOError(None, "Kernel version does not support configuring event clock") event_clock = property(_get_event_clock, _set_event_clock) def _get_debounce_us(self): return 0 def _set_debounce_us(self, debounce_us): if not isinstance(debounce_us, int): raise TypeError("Invalid debounce_us type, should be integer.") if debounce_us < 0: raise ValueError("Invalid debounce_us value, should be zero or positive.") raise GPIOError(None, "Kernel version does not support configuring debounce") debounce_us = property(_get_debounce_us, _set_debounce_us) def _get_bias(self): return self._bias def _set_bias(self, bias): if not isinstance(bias, str): raise TypeError("Invalid bias type, should be string.") if bias not in ["default", "pull_up", "pull_down", "disable"]: raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") if self._bias == bias: return self._reopen(self._direction, self._edge, bias, self._drive, self._inverted) bias = property(_get_bias, _set_bias) def _get_drive(self): return self._drive def _set_drive(self, drive): if not isinstance(drive, str): raise TypeError("Invalid drive type, should be string.") if drive not in ["default", "open_drain", "open_source"]: raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") if self._direction != "out" and drive != "default": raise GPIOError(None, "Invalid operation: cannot set line drive on input GPIO") if self._drive == drive: return self._reopen(self._direction, self._edge, self._bias, drive, self._inverted) drive = property(_get_drive, _set_drive) def _get_inverted(self): return self._inverted def _set_inverted(self, inverted): if not isinstance(inverted, bool): raise TypeError("Invalid inverted type, should be bool.") if self._inverted == inverted: return self._reopen(self._direction, self._edge, self._bias, self._drive, inverted) inverted = property(_get_inverted, _set_inverted) # String representation def __str__(self): try: str_name = self.name except GPIOError: str_name = "" try: str_label = self.label except GPIOError: str_label = "" str_direction = self._direction str_edge = self._edge str_bias = self._bias str_drive = self._drive str_inverted = str(self._inverted) try: str_chip_name = self.chip_name except GPIOError: str_chip_name = "" try: str_chip_label = self.chip_label except GPIOError: str_chip_label = "" return "GPIO {:d} (name=\"{:s}\", label=\"{:s}\", device={:s}, line_fd={:d}, chip_fd={:d}, direction={:s}, edge={:s}, bias={:s}, drive={:s}, inverted={:s}, chip_name=\"{:s}\", chip_label=\"{:s}\", type=cdev)" \ .format(self._line, str_name, str_label, self._devpath, self._line_fd, self._chip_fd, str_direction, str_edge, str_bias, str_drive, str_inverted, str_chip_name, str_chip_label) vsergeev-python-periphery-e82b08d/periphery/gpio_cdev2.py000066400000000000000000000576771527060700700237360ustar00rootroot00000000000000import platform import ctypes import fcntl import os import select from .gpio import GPIO, GPIOError, EdgeEvent try: KERNEL_VERSION = tuple([int(s) for s in platform.release().split(".")[:2]]) except ValueError: KERNEL_VERSION = (0, 0) _GPIO_NAME_MAX_SIZE = 32 _GPIO_V2_LINES_MAX = 64 _GPIO_V2_LINE_NUM_ATTRS_MAX = 10 class _CGpiochipInfo(ctypes.Structure): _fields_ = [ ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('lines', ctypes.c_uint32), ] class _CGpioV2LineValues(ctypes.Structure): _fields_ = [ ('bits', ctypes.c_uint64), ('mask', ctypes.c_uint64), ] class _CGpioV2LineAttributeData(ctypes.Union): _fields_ = [ ('flags', ctypes.c_uint64), ('values', ctypes.c_uint64), ('debounce_period_us', ctypes.c_uint32), ] class _CGpioV2LineAttribute(ctypes.Structure): _fields_ = [ ('id', ctypes.c_uint32), ('padding', ctypes.c_uint32), ('data', _CGpioV2LineAttributeData), ] class _CGpioV2LineConfigAttribute(ctypes.Structure): _fields_ = [ ('attr', _CGpioV2LineAttribute), ('mask', ctypes.c_uint64), ] class _CGpioV2LineConfig(ctypes.Structure): _fields_ = [ ('flags', ctypes.c_uint64), ('num_attrs', ctypes.c_uint32), ('padding', ctypes.c_uint32 * 5), ('attrs', _CGpioV2LineConfigAttribute * _GPIO_V2_LINE_NUM_ATTRS_MAX), ] class _CGpioV2LineRequest(ctypes.Structure): _fields_ = [ ('offsets', ctypes.c_uint32 * _GPIO_V2_LINES_MAX), ('consumer', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('config', _CGpioV2LineConfig), ('num_lines', ctypes.c_uint32), ('event_buffer_size', ctypes.c_uint32), ('padding', ctypes.c_uint32 * 5), ('fd', ctypes.c_int32), ] class _CGpioV2LineInfo(ctypes.Structure): _fields_ = [ ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('consumer', ctypes.c_char * _GPIO_NAME_MAX_SIZE), ('offset', ctypes.c_uint32), ('num_attrs', ctypes.c_uint32), ('flags', ctypes.c_uint64), ('attrs', _CGpioV2LineAttribute * _GPIO_V2_LINE_NUM_ATTRS_MAX), ('padding', ctypes.c_uint32 * 4), ] class _CGpioV2LineEvent(ctypes.Structure): _fields_ = [ ('timestamp_ns', ctypes.c_uint64), ('id', ctypes.c_uint32), ('offset', ctypes.c_uint32), ('seqno', ctypes.c_uint32), ('line_seqno', ctypes.c_uint32), ('padding', ctypes.c_uint32 * 6), ] class Cdev2GPIO(GPIO): # Constants scraped from _GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 _GPIO_V2_GET_LINEINFO_IOCTL = 0xc100b405 _GPIO_V2_GET_LINE_IOCTL = 0xc250b407 _GPIO_V2_LINE_GET_VALUES_IOCTL = 0xc010b40e _GPIO_V2_LINE_SET_CONFIG_IOCTL = 0xc110b40d _GPIO_V2_LINE_SET_VALUES_IOCTL = 0xc010b40f _GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES = 0x2 _GPIO_V2_LINE_ATTR_ID_DEBOUNCE = 0x3 _GPIO_V2_LINE_EVENT_RISING_EDGE = 0x1 _GPIO_V2_LINE_EVENT_FALLING_EDGE = 0x2 _GPIO_V2_LINE_FLAG_ACTIVE_LOW = 0x2 _GPIO_V2_LINE_FLAG_INPUT = 0x4 _GPIO_V2_LINE_FLAG_OUTPUT = 0x8 _GPIO_V2_LINE_FLAG_EDGE_RISING = 0x10 _GPIO_V2_LINE_FLAG_EDGE_FALLING = 0x20 _GPIO_V2_LINE_FLAG_OPEN_DRAIN = 0x40 _GPIO_V2_LINE_FLAG_OPEN_SOURCE = 0x80 _GPIO_V2_LINE_FLAG_BIAS_PULL_UP = 0x100 _GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN = 0x200 _GPIO_V2_LINE_FLAG_BIAS_DISABLED = 0x400 _GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME = 0x800 _GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE = 0x1000 SUPPORTED = KERNEL_VERSION >= (5, 10) _SUPPORTS_EVENT_CLOCK_HTE = KERNEL_VERSION >= (5, 19) def __init__(self, path, line, direction, edge="none", bias="default", drive="default", inverted=False, label=None, event_clock="realtime", debounce_us=0): """**Character device GPIO (ABI version 2)** Instantiate a GPIO object and open the character device GPIO with the specified line and direction at the specified GPIO chip path (e.g. "/dev/gpiochip0"). Default properties can be overridden with keyword arguments. Args: path (str): GPIO chip character device path. line (int, str): GPIO line number or name. direction (str): GPIO direction, can be "in", "out", "high", or "low". edge (str): GPIO interrupt edge, can be "none", "rising", "falling", or "both". bias (str): GPIO line bias, can be "default", "pull_up", "pull_down", or "disable". drive (str): GPIO line drive, can be "default", "open_drain", or "open_source". inverted (bool): GPIO is inverted (active low). label (str, None): GPIO line consumer label. event_clock (str): GPIO event clock, can be "realtime", "monotonic", or "hte". debounce_us (int): GPIO debounce period in microseconds. Returns: Cdev2GPIO: GPIO object. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `path`, `line`, `direction`, `edge`, `bias`, `drive`, `inverted`, `label`, `event_clock`, or `debounce_us` types are invalid. ValueError: if `direction`, `edge`, `bias`, `drive`, `event_clock`, or `debounce_us` value is invalid. LookupError: if the GPIO line was not found by the provided name. """ self._devpath = None self._line = None self._line_fd = None self._chip_fd = None self._direction = None self._edge = None self._event_clock = None self._debounce_us = None self._bias = None self._drive = None self._inverted = None self._label = None self._open(path, line, direction, edge, event_clock, debounce_us, bias, drive, inverted, label) def __new__(self, path, line, direction, **kwargs): return object.__new__(Cdev2GPIO) def _open(self, path, line, direction, edge, event_clock, debounce_us, bias, drive, inverted, label): if not isinstance(path, str): raise TypeError("Invalid path type, should be string.") if not isinstance(line, (int, str)): raise TypeError("Invalid line type, should be integer or string.") if not isinstance(direction, str): raise TypeError("Invalid direction type, should be string.") elif direction not in ["in", "out", "high", "low"]: raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") if not isinstance(edge, str): raise TypeError("Invalid edge type, should be string.") elif edge not in ["none", "rising", "falling", "both"]: raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") if not isinstance(event_clock, str): raise TypeError("Invalid event_clock type, should be string.") elif event_clock not in ["realtime", "monotonic", "hte"]: raise ValueError("Invalid event_clock, can be: \"realtime\", \"monotonic\", \"hte\".") if not isinstance(debounce_us, int): raise TypeError("Invalid debounce_us type, should be integer.") elif debounce_us < 0: raise ValueError("Invalid debounce_us value, should be zero or positive.") if not isinstance(bias, str): raise TypeError("Invalid bias type, should be string.") elif bias not in ["default", "pull_up", "pull_down", "disable"]: raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") if not isinstance(drive, str): raise TypeError("Invalid drive type, should be string.") elif drive not in ["default", "open_drain", "open_source"]: raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") if not isinstance(inverted, bool): raise TypeError("Invalid inverted type, should be bool.") if not isinstance(label, (type(None), str)): raise TypeError("Invalid label type, should be None or str.") if isinstance(line, str): line = self._find_line_by_name(path, line) # Open GPIO chip try: self._chip_fd = os.open(path, 0) except OSError as e: raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) self._devpath = path self._line = line self._label = label.encode() if label is not None else b"periphery" self._reopen(direction, edge, event_clock, debounce_us, bias, drive, inverted) def _reopen(self, direction, edge, event_clock, debounce_us, bias, drive, inverted): flags = 0 if event_clock == "hte" and not Cdev2GPIO._SUPPORTS_EVENT_CLOCK_HTE: raise GPIOError(None, "Kernel version does not support GPIO event clock HTE") if bias == "pull_up": flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_BIAS_PULL_UP elif bias == "pull_down": flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN elif bias == "disable": flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_BIAS_DISABLED if drive == "open_drain": flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_OPEN_DRAIN elif drive == "open_source": flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_OPEN_SOURCE if inverted: flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_ACTIVE_LOW # FIXME this should really use GPIOHANDLE_SET_CONFIG_IOCTL instead of # closing and reopening, especially to preserve output value on # configuration changes # Close existing line if self._line_fd is not None: try: os.close(self._line_fd) except OSError as e: raise GPIOError(e.errno, "Closing existing GPIO line: " + e.strerror) self._line_fd = None line_request = _CGpioV2LineRequest() if direction == "in": flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_EDGE_RISING if edge == "rising" else Cdev2GPIO._GPIO_V2_LINE_FLAG_EDGE_FALLING if edge == "falling" else (Cdev2GPIO._GPIO_V2_LINE_FLAG_EDGE_RISING | Cdev2GPIO._GPIO_V2_LINE_FLAG_EDGE_FALLING) if edge == "both" else 0 flags |= (Cdev2GPIO._GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME if event_clock == "realtime" else Cdev2GPIO._GPIO_V2_LINE_FLAG_EVENT_CLOCK_HTE if event_clock == "hte" else 0) if edge != "none" else 0 line_request.offsets[0] = self._line line_request.consumer = self._label line_request.config.flags = flags | Cdev2GPIO._GPIO_V2_LINE_FLAG_INPUT line_request.num_lines = 1 if debounce_us: line_request.config.num_attrs = 1 line_request.config.attrs[0].attr.id = Cdev2GPIO._GPIO_V2_LINE_ATTR_ID_DEBOUNCE line_request.config.attrs[0].attr.data.debounce_period_us = debounce_us line_request.config.attrs[0].mask = 0x1 try: fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINE_IOCTL, line_request) except (OSError, IOError) as e: raise GPIOError(e.errno, "Opening input line handle: " + e.strerror) else: initial_value = True if direction == "high" else False initial_value ^= inverted line_request.offsets[0] = self._line line_request.consumer = self._label line_request.config.flags = flags | Cdev2GPIO._GPIO_V2_LINE_FLAG_OUTPUT line_request.config.num_attrs = 1 line_request.config.attrs[0].attr.id = Cdev2GPIO._GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES line_request.config.attrs[0].attr.data.values = int(initial_value) line_request.config.attrs[0].mask = 0x1 line_request.num_lines = 1 try: fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINE_IOCTL, line_request) except (OSError, IOError) as e: raise GPIOError(e.errno, "Opening output line handle: " + e.strerror) self._line_fd = line_request.fd self._direction = "in" if direction == "in" else "out" self._edge = edge self._event_clock = event_clock self._debounce_us = debounce_us self._bias = bias self._drive = drive self._inverted = inverted def _find_line_by_name(self, path, line): # Open GPIO chip try: fd = os.open(path, 0) except OSError as e: raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) # Get chip info for number of lines chip_info = _CGpiochipInfo() try: fcntl.ioctl(fd, Cdev2GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) # Get each line info line_info = _CGpioV2LineInfo() found = False for i in range(chip_info.lines): line_info.offset = i try: fcntl.ioctl(fd, Cdev2GPIO._GPIO_V2_GET_LINEINFO_IOCTL, line_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) if line_info.name.decode() == line: found = True break try: os.close(fd) except OSError as e: raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) if found: return i raise LookupError("Opening GPIO line: GPIO line \"{:s}\" not found by name.".format(line)) # Methods def read(self): data = _CGpioV2LineValues() data.mask = 0x1 try: fcntl.ioctl(self._line_fd, Cdev2GPIO._GPIO_V2_LINE_GET_VALUES_IOCTL, data) except (OSError, IOError) as e: raise GPIOError(e.errno, "Getting line value: " + e.strerror) return bool(data.bits & 0x1) def write(self, value): if not isinstance(value, bool): raise TypeError("Invalid value type, should be bool.") elif self._direction != "out": raise GPIOError(None, "Invalid operation: cannot write to input GPIO") data = _CGpioV2LineValues() data.mask = 0x1 data.bits = int(value) try: fcntl.ioctl(self._line_fd, Cdev2GPIO._GPIO_V2_LINE_SET_VALUES_IOCTL, data) except (OSError, IOError) as e: raise GPIOError(e.errno, "Setting line value: " + e.strerror) def poll(self, timeout=None): if not isinstance(timeout, (int, float, type(None))): raise TypeError("Invalid timeout type, should be integer, float, or None.") elif self._direction != "in": raise GPIOError(None, "Invalid operation: cannot poll output GPIO") # Setup poll p = select.poll() p.register(self._line_fd, select.POLLIN | select.POLLPRI | select.POLLERR) # Scale timeout to milliseconds if isinstance(timeout, (int, float)) and timeout > 0: timeout *= 1000 # Poll events = p.poll(timeout) return len(events) > 0 def read_event(self): if self._direction != "in": raise GPIOError(None, "Invalid operation: cannot read event of output GPIO") elif self._edge == "none": raise GPIOError(None, "Invalid operation: GPIO edge not set") try: buf = os.read(self._line_fd, ctypes.sizeof(_CGpioV2LineEvent)) except OSError as e: raise GPIOError(e.errno, "Reading GPIO event: " + e.strerror) line_event = _CGpioV2LineEvent.from_buffer_copy(buf) if line_event.id == Cdev2GPIO._GPIO_V2_LINE_EVENT_RISING_EDGE: edge = "rising" elif line_event.id == Cdev2GPIO._GPIO_V2_LINE_EVENT_FALLING_EDGE: edge = "falling" else: edge = "none" timestamp = line_event.timestamp_ns return EdgeEvent(edge, timestamp) def close(self): try: if self._line_fd is not None: os.close(self._line_fd) except OSError as e: raise GPIOError(e.errno, "Closing GPIO line: " + e.strerror) try: if self._chip_fd is not None: os.close(self._chip_fd) except OSError as e: raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) self._line_fd = None self._chip_fd = None self._edge = "none" self._direction = "in" self._line = None # Immutable properties @property def devpath(self): return self._devpath @property def fd(self): return self._line_fd @property def line(self): return self._line @property def name(self): line_info = _CGpioV2LineInfo() line_info.offset = self._line try: fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINEINFO_IOCTL, line_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) return line_info.name.decode() @property def label(self): line_info = _CGpioV2LineInfo() line_info.offset = self._line try: fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINEINFO_IOCTL, line_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) return line_info.consumer.decode() @property def chip_fd(self): return self._chip_fd @property def chip_name(self): chip_info = _CGpiochipInfo() try: fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) return chip_info.name.decode() @property def chip_label(self): chip_info = _CGpiochipInfo() try: fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) except (OSError, IOError) as e: raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) return chip_info.label.decode() # Mutable properties def _get_direction(self): return self._direction def _set_direction(self, direction): if not isinstance(direction, str): raise TypeError("Invalid direction type, should be string.") if direction not in ["in", "out", "high", "low"]: raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") if self._direction == direction: return self._reopen(direction, "none", self._event_clock, self._debounce_us, self._bias, self._drive, self._inverted) direction = property(_get_direction, _set_direction) def _get_edge(self): return self._edge def _set_edge(self, edge): if not isinstance(edge, str): raise TypeError("Invalid edge type, should be string.") if edge not in ["none", "rising", "falling", "both"]: raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") if self._direction != "in": raise GPIOError(None, "Invalid operation: cannot set edge on output GPIO") if self._edge == edge: return self._reopen(self._direction, edge, self._event_clock, self._debounce_us, self._bias, self._drive, self._inverted) edge = property(_get_edge, _set_edge) def _get_event_clock(self): return self._event_clock def _set_event_clock(self, event_clock): if not isinstance(event_clock, str): raise TypeError("Invalid event_clock type, should be string.") if event_clock not in ["realtime", "monotonic", "hte"]: raise ValueError("Invalid event_clock, can be: \"realtime\", \"monotonic\", \"hte\".") if self._direction != "in": raise GPIOError(None, "Invalid operation: cannot set event clock on output GPIO") if self._event_clock == event_clock: return self._reopen(self._direction, self._edge, event_clock, self._debounce_us, self._bias, self._drive, self._inverted) event_clock = property(_get_event_clock, _set_event_clock) def _get_debounce_us(self): return self._debounce_us def _set_debounce_us(self, debounce_us): if not isinstance(debounce_us, int): raise TypeError("Invalid debounce_us type, should be integer.") if debounce_us < 0: raise ValueError("Invalid debounce_us value, should be zero or positive.") if self._direction != "in": raise GPIOError(None, "Invalid operation: cannot set debounce on output GPIO") if self._debounce_us == debounce_us: return self._reopen(self._direction, self._edge, self._event_clock, debounce_us, self._bias, self._drive, self._inverted) debounce_us = property(_get_debounce_us, _set_debounce_us) def _get_bias(self): return self._bias def _set_bias(self, bias): if not isinstance(bias, str): raise TypeError("Invalid bias type, should be string.") if bias not in ["default", "pull_up", "pull_down", "disable"]: raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") if self._bias == bias: return self._reopen(self._direction, self._edge, self._event_clock, self._debounce_us, bias, self._drive, self._inverted) bias = property(_get_bias, _set_bias) def _get_drive(self): return self._drive def _set_drive(self, drive): if not isinstance(drive, str): raise TypeError("Invalid drive type, should be string.") if drive not in ["default", "open_drain", "open_source"]: raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") if self._direction != "out" and drive != "default": raise GPIOError(None, "Invalid operation: cannot set line drive on input GPIO") if self._drive == drive: return self._reopen(self._direction, self._edge, self._event_clock, self._debounce_us, self._bias, drive, self._inverted) drive = property(_get_drive, _set_drive) def _get_inverted(self): return self._inverted def _set_inverted(self, inverted): if not isinstance(inverted, bool): raise TypeError("Invalid inverted type, should be bool.") if self._inverted == inverted: return self._reopen(self._direction, self._edge, self._event_clock, self._debounce_us, self._bias, self._drive, inverted) inverted = property(_get_inverted, _set_inverted) # String representation def __str__(self): try: str_name = self.name except GPIOError: str_name = "" try: str_label = self.label except GPIOError: str_label = "" str_direction = self._direction str_edge = self._edge str_event_clock = self._event_clock str_debounce_us = str(self._debounce_us) str_bias = self._bias str_drive = self._drive str_inverted = str(self._inverted) try: str_chip_name = self.chip_name except GPIOError: str_chip_name = "" try: str_chip_label = self.chip_label except GPIOError: str_chip_label = "" return "GPIO {:d} (name=\"{:s}\", label=\"{:s}\", device={:s}, line_fd={:d}, chip_fd={:d}, direction={:s}, edge={:s}, event_clock={:s}, debounce_us={:s}, bias={:s}, drive={:s}, inverted={:s}, chip_name=\"{:s}\", chip_label=\"{:s}\", type=cdev)" \ .format(self._line, str_name, str_label, self._devpath, self._line_fd, self._chip_fd, str_direction, str_edge, str_event_clock, str_debounce_us, str_bias, str_drive, str_inverted, str_chip_name, str_chip_label) vsergeev-python-periphery-e82b08d/periphery/gpio_sysfs.py000066400000000000000000000303301527060700700240530ustar00rootroot00000000000000import errno import os import os.path import select import time from .gpio import GPIO, GPIOError class SysfsGPIO(GPIO): # Number of retries to check for GPIO export or direction on open _GPIO_STAT_RETRIES = 10 # Delay between check for GPIO export or direction write on open (100ms) _GPIO_STAT_DELAY = 0.1 def __init__(self, line, direction): """**Sysfs GPIO** Instantiate a GPIO object and open the sysfs GPIO with the specified line and direction. `direction` can be "in" for input; "out" for output, initialized to low; "high" for output, initialized to high; or "low" for output, initialized to low. Args: line (int): GPIO line number. direction (str): GPIO direction, can be "in", "out", "high", or "low", Returns: SysfsGPIO: GPIO object. Raises: GPIOError: if an I/O or OS error occurs. TypeError: if `line` or `direction` types are invalid. ValueError: if `direction` value is invalid. TimeoutError: if waiting for GPIO export times out. """ self._fd = None self._line = None self._exported = False self._open(line, direction) def __new__(self, line, direction): return object.__new__(SysfsGPIO) def _open(self, line, direction): if not isinstance(line, int): raise TypeError("Invalid line type, should be integer.") if not isinstance(direction, str): raise TypeError("Invalid direction type, should be string.") if direction.lower() not in ["in", "out", "high", "low"]: raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") gpio_path = "/sys/class/gpio/gpio{:d}".format(line) if not os.path.isdir(gpio_path): # Export the line try: with open("/sys/class/gpio/export", "w") as f_export: f_export.write("{:d}\n".format(line)) except IOError as e: raise GPIOError(e.errno, "Exporting GPIO: " + e.strerror) # Loop until GPIO is exported for i in range(SysfsGPIO._GPIO_STAT_RETRIES): if os.path.isdir(gpio_path): self._exported = True break time.sleep(SysfsGPIO._GPIO_STAT_DELAY) if not self._exported: raise TimeoutError("Exporting GPIO: waiting for \"{:s}\" timed out".format(gpio_path)) # Loop until direction is writable. This could take some time after # export as application of udev rules after export is asynchronous. for i in range(SysfsGPIO._GPIO_STAT_RETRIES): try: with open(os.path.join(gpio_path, "direction"), 'w'): break except IOError as e: if e.errno != errno.EACCES or i == SysfsGPIO._GPIO_STAT_RETRIES - 1: raise GPIOError(e.errno, "Opening GPIO direction: " + e.strerror) time.sleep(SysfsGPIO._GPIO_STAT_DELAY) # Open value try: self._fd = os.open(os.path.join(gpio_path, "value"), os.O_RDWR) except OSError as e: raise GPIOError(e.errno, "Opening GPIO: " + e.strerror) self._line = line self._path = gpio_path # Initialize direction if self.direction != direction.lower(): self.direction = direction # Methods def read(self): # Read value try: buf = os.read(self._fd, 2) except OSError as e: raise GPIOError(e.errno, "Reading GPIO: " + e.strerror) # Rewind try: os.lseek(self._fd, 0, os.SEEK_SET) except OSError as e: raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) if buf[0] == b"0"[0]: return False elif buf[0] == b"1"[0]: return True raise GPIOError(None, "Unknown GPIO value: {}".format(buf)) def write(self, value): if not isinstance(value, bool): raise TypeError("Invalid value type, should be bool.") # Write value try: if value: os.write(self._fd, b"1\n") else: os.write(self._fd, b"0\n") except OSError as e: raise GPIOError(e.errno, "Writing GPIO: " + e.strerror) # Rewind try: os.lseek(self._fd, 0, os.SEEK_SET) except OSError as e: raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) def poll(self, timeout=None): if not isinstance(timeout, (int, float, type(None))): raise TypeError("Invalid timeout type, should be integer, float, or None.") # Setup poll p = select.poll() p.register(self._fd, select.POLLPRI | select.POLLERR) # Scale timeout to milliseconds if isinstance(timeout, (int, float)) and timeout > 0: timeout *= 1000 # Poll events = p.poll(timeout) # If GPIO edge interrupt occurred if events: # Rewind try: os.lseek(self._fd, 0, os.SEEK_SET) except OSError as e: raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) return True return False def read_event(self): raise NotImplementedError() def close(self): if self._fd is None: return try: os.close(self._fd) except OSError as e: raise GPIOError(e.errno, "Closing GPIO: " + e.strerror) self._fd = None if self._exported: # Unexport the line try: unexport_fd = os.open("/sys/class/gpio/unexport", os.O_WRONLY) os.write(unexport_fd, "{:d}\n".format(self._line).encode()) os.close(unexport_fd) except OSError as e: raise GPIOError(e.errno, "Unexporting GPIO: " + e.strerror) # Immutable properties @property def devpath(self): return self._path @property def fd(self): return self._fd @property def line(self): return self._line @property def name(self): return "" @property def label(self): return "" @property def chip_fd(self): raise NotImplementedError("Sysfs GPIO does not have a gpiochip file descriptor.") @property def chip_name(self): gpio_path = os.path.join(self._path, "device") gpiochip_path = os.readlink(gpio_path) if '/' not in gpiochip_path: raise GPIOError(None, "Reading gpiochip name: invalid device symlink \"{:s}\"".format(gpiochip_path)) return gpiochip_path.split('/')[-1] @property def chip_label(self): gpio_path = "/sys/class/gpio/{:s}/label".format(self.chip_name) try: with open(gpio_path, "r") as f_label: label = f_label.read() except (GPIOError, IOError) as e: if isinstance(e, IOError): raise GPIOError(e.errno, "Reading gpiochip label: " + e.strerror) raise GPIOError(None, "Reading gpiochip label: " + e.strerror) return label.strip() # Mutable properties def _get_direction(self): # Read direction try: with open(os.path.join(self._path, "direction"), "r") as f_direction: direction = f_direction.read() except IOError as e: raise GPIOError(e.errno, "Getting GPIO direction: " + e.strerror) return direction.strip() def _set_direction(self, direction): if not isinstance(direction, str): raise TypeError("Invalid direction type, should be string.") if direction.lower() not in ["in", "out", "high", "low"]: raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") # Write direction try: with open(os.path.join(self._path, "direction"), "w") as f_direction: f_direction.write(direction.lower() + "\n") except IOError as e: raise GPIOError(e.errno, "Setting GPIO direction: " + e.strerror) direction = property(_get_direction, _set_direction) def _get_edge(self): # Read edge try: with open(os.path.join(self._path, "edge"), "r") as f_edge: edge = f_edge.read() except IOError as e: raise GPIOError(e.errno, "Getting GPIO edge: " + e.strerror) return edge.strip() def _set_edge(self, edge): if not isinstance(edge, str): raise TypeError("Invalid edge type, should be string.") if edge.lower() not in ["none", "rising", "falling", "both"]: raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") # Write edge try: with open(os.path.join(self._path, "edge"), "w") as f_edge: f_edge.write(edge.lower() + "\n") except IOError as e: raise GPIOError(e.errno, "Setting GPIO edge: " + e.strerror) edge = property(_get_edge, _set_edge) def _get_event_clock(self): raise NotImplementedError("Sysfs GPIO does not support event clock property.") def _set_event_clock(self, event_clock): raise NotImplementedError("Sysfs GPIO does not support event clock property.") event_clock = property(_get_event_clock, _set_event_clock) def _get_debounce_us(self): raise NotImplementedError("Sysfs GPIO does not support debounce property.") def _set_debounce_us(self, debounce_us): raise NotImplementedError("Sysfs GPIO does not support debounce property.") debounce_us = property(_get_debounce_us, _set_debounce_us) def _get_bias(self): raise NotImplementedError("Sysfs GPIO does not support line bias property.") def _set_bias(self, bias): raise NotImplementedError("Sysfs GPIO does not support line bias property.") bias = property(_get_bias, _set_bias) def _get_drive(self): raise NotImplementedError("Sysfs GPIO does not support line drive property.") def _set_drive(self, drive): raise NotImplementedError("Sysfs GPIO does not support line drive property.") drive = property(_get_drive, _set_drive) def _get_inverted(self): # Read active_low try: with open(os.path.join(self._path, "active_low"), "r") as f_inverted: inverted = f_inverted.read().strip() except IOError as e: raise GPIOError(e.errno, "Getting GPIO active_low: " + e.strerror) if inverted == "0": return False elif inverted == "1": return True raise GPIOError(None, "Unknown GPIO active_low value: {}".format(inverted)) def _set_inverted(self, inverted): if not isinstance(inverted, bool): raise TypeError("Invalid inverted type, should be bool.") # Write active_low try: with open(os.path.join(self._path, "active_low"), "w") as f_active_low: f_active_low.write("1\n" if inverted else "0\n") except IOError as e: raise GPIOError(e.errno, "Setting GPIO active_low: " + e.strerror) inverted = property(_get_inverted, _set_inverted) # String representation def __str__(self): try: str_direction = self.direction except GPIOError: str_direction = "" try: str_edge = self.edge except GPIOError: str_edge = "" try: str_chip_name = self.chip_name except GPIOError: str_chip_name = "" try: str_chip_label = self.chip_label except GPIOError: str_chip_label = "" try: str_inverted = str(self.inverted) except GPIOError: str_inverted = "" return "GPIO {:d} (device={:s}, fd={:d}, direction={:s}, edge={:s}, inverted={:s}, chip_name=\"{:s}\", chip_label=\"{:s}\", type=sysfs)" \ .format(self._line, self._path, self._fd, str_direction, str_edge, str_inverted, str_chip_name, str_chip_label) vsergeev-python-periphery-e82b08d/periphery/i2c.py000066400000000000000000000153121527060700700223460ustar00rootroot00000000000000import os import ctypes import array import fcntl class I2CError(IOError): """Base class for I2C errors.""" pass class _CI2CMessage(ctypes.Structure): _fields_ = [ ("addr", ctypes.c_ushort), ("flags", ctypes.c_ushort), ("len", ctypes.c_ushort), ("buf", ctypes.POINTER(ctypes.c_ubyte)), ] class _CI2CIocTransfer(ctypes.Structure): _fields_ = [ ("msgs", ctypes.POINTER(_CI2CMessage)), ("nmsgs", ctypes.c_uint), ] class I2C(object): # Constants scraped from and _I2C_IOC_FUNCS = 0x705 _I2C_IOC_RDWR = 0x707 _I2C_FUNC_I2C = 0x1 _I2C_M_TEN = 0x0010 _I2C_M_RD = 0x0001 _I2C_M_STOP = 0x8000 _I2C_M_NOSTART = 0x4000 _I2C_M_REV_DIR_ADDR = 0x2000 _I2C_M_IGNORE_NAK = 0x1000 _I2C_M_NO_RD_ACK = 0x0800 _I2C_M_RECV_LEN = 0x0400 def __init__(self, devpath): """Instantiate an I2C object and open the i2c-dev device at the specified path. Args: devpath (str): i2c-dev device path. Returns: I2C: I2C object. Raises: I2CError: if an I/O or OS error occurs. """ self._fd = None self._devpath = None self._open(devpath) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, value, traceback): self.close() def _open(self, devpath): # Open i2c device try: self._fd = os.open(devpath, os.O_RDWR) except OSError as e: raise I2CError(e.errno, "Opening I2C device: " + e.strerror) self._devpath = devpath # Query supported functions buf = array.array('L', [0]) try: fcntl.ioctl(self._fd, I2C._I2C_IOC_FUNCS, buf, True) except (OSError, IOError) as e: self.close() raise I2CError(e.errno, "Querying supported functions: " + e.strerror) # Check that I2C_RDWR ioctl() is supported on this device if (buf[0] & I2C._I2C_FUNC_I2C) == 0: self.close() raise I2CError(None, "I2C not supported on device \"{:s}\"".format(devpath)) # Methods def transfer(self, address, messages, inplace=False): """Transfer `messages` to the specified I2C `address`. Modifies the `messages` array with the results of any read transactions. Args: address (int): I2C address. messages (list, tuple): list of I2C.Message messages. inplace (bool): read into `messages` in-place. data must be of type `bytearray`. Raises: I2CError: if an I/O or OS error occurs. TypeError: if `messages` or message types are invalid. ValueError: if `messages` length is zero. """ if not isinstance(messages, (list, tuple)): raise TypeError("Invalid messages type, should be list or tuple of I2C.Message.") elif len(messages) == 0: raise ValueError("Invalid messages, should contain at least one message.") for message in messages: if not isinstance(message, I2C.Message): raise TypeError("Invalid message type, should be I2C.Message.") # Convert I2C.Message messages to _CI2CMessage messages cmessages = (_CI2CMessage * len(messages))() bufs = [] for i in range(len(messages)): # Convert data to bytearray (or re-use it if inplace) buf = messages[i].data if inplace and isinstance(messages[i].data, bytearray) else bytearray(messages[i].data) cmessages[i].addr = address cmessages[i].flags = messages[i].flags | (I2C._I2C_M_RD if messages[i].read else 0) cmessages[i].len = len(buf) cmessages[i].buf = ctypes.cast((ctypes.c_ubyte * len(buf)).from_buffer(buf), ctypes.POINTER(ctypes.c_ubyte)) bufs.append(buf) # Prepare transfer structure i2c_xfer = _CI2CIocTransfer() i2c_xfer.nmsgs = len(cmessages) i2c_xfer.msgs = cmessages # Transfer try: fcntl.ioctl(self._fd, I2C._I2C_IOC_RDWR, i2c_xfer, False) except (OSError, IOError) as e: raise I2CError(e.errno, "I2C transfer: " + e.strerror) # Update any read I2C.Message messages for i in range(len(messages)): if messages[i].read: # Convert read data to type used in I2C.Message messages if isinstance(messages[i].data, list): messages[i].data = list(bufs[i]) elif isinstance(messages[i].data, bytearray): messages[i].data = bufs[i] elif isinstance(messages[i].data, bytes): messages[i].data = bytes(bufs[i]) def close(self): """Close the i2c-dev I2C device. Raises: I2CError: if an I/O or OS error occurs. """ if self._fd is None: return try: os.close(self._fd) except OSError as e: raise I2CError(e.errno, "Closing I2C device: " + e.strerror) self._fd = None # Immutable properties @property def fd(self): """Get the file descriptor of the underlying i2c-dev device. :type: int """ return self._fd @property def devpath(self): """Get the device path of the underlying i2c-dev device. :type: str """ return self._devpath # String representation def __str__(self): return "I2C (device={:s}, fd={:d})".format(self.devpath, self.fd) class Message: def __init__(self, data, read=False, flags=0): """Instantiate an I2C Message object. Args: data (bytes, bytearray, list): a byte array or list of 8-bit integers to write. read (bool): specify this as a read message, where `data` serves as placeholder bytes for the read. flags (int): additional i2c-dev flags for this message. Returns: Message: Message object. Raises: TypeError: if `data`, `read`, or `flags` types are invalid. """ if not isinstance(data, (bytes, bytearray, list)): raise TypeError("Invalid data type, should be bytes, bytearray, or list.") if not isinstance(read, bool): raise TypeError("Invalid read type, should be boolean.") if not isinstance(flags, int): raise TypeError("Invalid flags type, should be integer.") self.data = data self.read = read self.flags = flags vsergeev-python-periphery-e82b08d/periphery/i2c.pyi000066400000000000000000000014051527060700700225150ustar00rootroot00000000000000from types import TracebackType class I2CError(IOError): ... class I2C: class Message: data: bytes | bytearray | list[int] read: bool flags: int def __init__(self, data: bytes | bytearray | list[int], read: bool = ..., flags: int = ...) -> None: ... def __init__(self, devpath: str) -> None: ... def __del__(self) -> None: ... def __enter__(self) -> I2C: ... # noqa: Y034 def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... def transfer(self, address: int, messages: list[Message], inplace: bool = ...) -> None: ... def close(self) -> None: ... @property def fd(self) -> int: ... @property def devpath(self) -> str: ... vsergeev-python-periphery-e82b08d/periphery/led.py000066400000000000000000000200521527060700700224320ustar00rootroot00000000000000import os import os.path class LEDError(IOError): """Base class for LED errors.""" pass class LED(object): def __init__(self, name, brightness=None): """Instantiate an LED object and open the sysfs LED corresponding to the specified name. `brightness` can be a boolean for on/off, integer value for a specific brightness, or None to preserve existing brightness. Default is preserve existing brightness. Args: name (str): Linux led name. brightness (bool, int, None): Initial brightness. Returns: LED: LED object. Raises: LEDError: if an I/O or OS error occurs. TypeError: if `name` or `brightness` types are invalid. LookupError: if LED name does not exist. ValueError: if `brightness` value is invalid. """ self._path = None self._fd = None self._name = None self._max_brightness = None self._open(name, brightness) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, value, traceback): self.close() def _open(self, name, brightness): if not isinstance(name, str): raise TypeError("Invalid name type, should be string.") if not isinstance(brightness, (bool, int, type(None))): raise TypeError("Invalid brightness type, should be bool, int, or None.") led_path = "/sys/class/leds/{:s}".format(name) if not os.path.isdir(led_path): raise LookupError("Opening LED: LED \"{:s}\" not found.".format(name)) # Read max brightness try: with open(os.path.join(led_path, "max_brightness"), "r") as f_max_brightness: self._max_brightness = int(f_max_brightness.read()) except IOError as e: raise LEDError(e.errno, "Reading LED max brightness: " + e.strerror) # Open brightness try: self._fd = os.open(os.path.join(led_path, "brightness"), os.O_RDWR) except OSError as e: raise LEDError(e.errno, "Opening LED brightness: " + e.strerror) self._name = name self._path = led_path # Set initial brightness if brightness: self.write(brightness) # Methods def read(self): """Read the brightness of the LED. Returns: int: Current brightness. Raises: LEDError: if an I/O or OS error occurs. """ # Read value try: buf = os.read(self._fd, 8) except OSError as e: raise LEDError(e.errno, "Reading LED brightness: " + e.strerror) # Rewind try: os.lseek(self._fd, 0, os.SEEK_SET) except OSError as e: raise LEDError(e.errno, "Rewinding LED brightness: " + e.strerror) return int(buf) def write(self, brightness): """Set the brightness of the LED to `brightness`. `brightness` can be a boolean for on/off, or integer value for a specific brightness. Args: brightness (bool, int): Brightness value to set. Raises: LEDError: if an I/O or OS error occurs. TypeError: if `brightness` type is not bool or int. """ if not isinstance(brightness, (bool, int)): raise TypeError("Invalid brightness type, should be bool or int.") if isinstance(brightness, bool): brightness = self._max_brightness if brightness else 0 else: if not 0 <= brightness <= self._max_brightness: raise ValueError("Invalid brightness value: should be between 0 and {:d}".format(self._max_brightness)) # Write value try: os.write(self._fd, "{:d}\n".format(brightness).encode()) except OSError as e: raise LEDError(e.errno, "Writing LED brightness: " + e.strerror) # Rewind try: os.lseek(self._fd, 0, os.SEEK_SET) except OSError as e: raise LEDError(e.errno, "Rewinding LED brightness: " + e.strerror) def close(self): """Close the sysfs LED. Raises: LEDError: if an I/O or OS error occurs. """ if self._fd is None: return try: os.close(self._fd) except OSError as e: raise LEDError(e.errno, "Closing LED: " + e.strerror) self._fd = None def _read_trigger(self): """Read and parse the LED "trigger" file. Returns: (list, str or None): a list of the available triggers, and the active trigger or None if no active trigger was found. Raises: LEDError: if an I/O or OS error occurs. """ try: with open(os.path.join(self._path, "trigger"), "r") as f_trigger: content = f_trigger.read() except IOError as e: raise LEDError(e.errno, "Reading LED trigger: " + e.strerror) triggers = [] active_trigger = None for token in content.split(): if token.startswith("[") and token.endswith("]"): active_trigger = token[1:-1] triggers.append(active_trigger) else: triggers.append(token) return triggers, active_trigger # Immutable properties @property def devpath(self): """Get the device path of the underlying sysfs LED device. :type: str """ return self._path @property def fd(self): """Get the file descriptor for the underlying sysfs LED "brightness" file of the LED object. :type: int """ return self._fd @property def name(self): """Get the sysfs LED name. :type: str """ return self._name @property def max_brightness(self): """Get the LED's max brightness. :type: int """ return self._max_brightness @property def triggers(self): """Get the LED's available triggers. Returns: list: list of available triggers. Raises: LEDError: if an I/O or OS error occurs. :type: list """ triggers, _ = self._read_trigger() return triggers # Mutable properties def _get_brightness(self): # Read brightness return self.read() def _set_brightness(self, brightness): return self.write(brightness) brightness = property(_get_brightness, _set_brightness) """Get or set the LED's brightness. Value can be a boolean for on/off, or integer value a for specific brightness. Raises: LEDError: if an I/O or OS error occurs. TypeError: if `brightness` type is not bool or int. ValueError: if `brightness` value is invalid. :type: int """ def _get_trigger(self): _, active_trigger = self._read_trigger() if active_trigger is None: raise LEDError(None, "Active LED trigger not found.") return active_trigger def _set_trigger(self, trigger): if not isinstance(trigger, str): raise TypeError("Invalid trigger type, should be string.") try: with open(os.path.join(self._path, "trigger"), "w") as f_trigger: f_trigger.write(trigger + "\n") except IOError as e: raise LEDError(e.errno, "Setting LED trigger: " + e.strerror) trigger = property(_get_trigger, _set_trigger) """Get or set the LED's active trigger. Raises: LEDError: if an I/O or OS error occurs, or if no active trigger is found. TypeError: if `trigger` type is not str. :type: str """ # String representation def __str__(self): try: str_trigger = self.trigger except LEDError: str_trigger = "" return "LED {:s} (device={:s}, fd={:d}, max_brightness={:d}, trigger={:s})".format(self._name, self._path, self._fd, self._max_brightness, str_trigger) vsergeev-python-periphery-e82b08d/periphery/led.pyi000066400000000000000000000014101527060700700226000ustar00rootroot00000000000000from types import TracebackType class LEDError(IOError): ... class LED: def __init__(self, name: str, brightness: bool | int | None = ...) -> None: ... def __del__(self) -> None: ... def __enter__(self) -> LED: ... # noqa: Y034 def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... def read(self) -> int: ... def write(self, brightness: bool | int) -> None: ... def close(self) -> None: ... @property def devpath(self) -> str: ... @property def fd(self) -> int: ... @property def name(self) -> str: ... @property def max_brightness(self) -> int: ... @property def triggers(self) -> list[str]: ... trigger: str brightness: int vsergeev-python-periphery-e82b08d/periphery/mmio.py000066400000000000000000000275461527060700700226460ustar00rootroot00000000000000import sys import os import mmap import ctypes # Alias long to int on Python 3 if sys.version_info[0] >= 3: long = int class MMIOError(IOError): """Base class for MMIO errors.""" pass class MMIO(object): def __init__(self, physaddr, size, path="/dev/mem"): """Instantiate an MMIO object and map the region of physical memory specified by the `physaddr` base physical address and `size` size in bytes. The default memory character device "/dev/mem" can be overridden with the keyword argument `path`, for use with sandboxed memory character devices, e.g. "/dev/gpiomem". Args: physaddr (int, long): base physical address of memory region. size (int, long): size of memory region. path (str): memory character device path. Returns: MMIO: MMIO object. Raises: MMIOError: if an I/O or OS error occurs. TypeError: if `physaddr` or `size` types are invalid. """ self.mapping = None self._open(physaddr, size, path) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, value, traceback): self.close() def _open(self, physaddr, size, path): if not isinstance(physaddr, (int, long)): raise TypeError("Invalid physaddr type, should be integer.") if not isinstance(size, (int, long)): raise TypeError("Invalid size type, should be integer.") pagesize = os.sysconf(os.sysconf_names['SC_PAGESIZE']) self._physaddr = physaddr self._size = size self._aligned_physaddr = physaddr - (physaddr % pagesize) self._aligned_size = size + (physaddr - self._aligned_physaddr) try: fd = os.open(path, os.O_RDWR | os.O_SYNC) except OSError as e: raise MMIOError(e.errno, "Opening {:s}: {:s}".format(path, e.strerror)) try: self.mapping = mmap.mmap(fd, self._aligned_size, flags=mmap.MAP_SHARED, prot=(mmap.PROT_READ | mmap.PROT_WRITE), offset=self._aligned_physaddr) except OSError as e: raise MMIOError(e.errno, "Mapping {:s}: {:s}".format(path, e.strerror)) try: os.close(fd) except OSError as e: raise MMIOError(e.errno, "Closing {:s}: {:s}".format(path, e.strerror)) # Methods def _adjust_offset(self, offset): return offset + (self._physaddr - self._aligned_physaddr) def _validate_offset(self, offset, length): if (offset + length) > self._aligned_size: raise ValueError("Offset out of bounds.") def read64(self, offset): """Read 64-bits from the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. Returns: int: 64-bit value read. Raises: TypeError: if `offset` type is invalid. ValueError: if `offset` is out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") offset = self._adjust_offset(offset) self._validate_offset(offset, 8) return ctypes.c_uint64.from_buffer(self.mapping, offset).value def read32(self, offset): """Read 32-bits from the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. Returns: int: 32-bit value read. Raises: TypeError: if `offset` type is invalid. ValueError: if `offset` is out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") offset = self._adjust_offset(offset) self._validate_offset(offset, 4) return ctypes.c_uint32.from_buffer(self.mapping, offset).value def read16(self, offset): """Read 16-bits from the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. Returns: int: 16-bit value read. Raises: TypeError: if `offset` type is invalid. ValueError: if `offset` is out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") offset = self._adjust_offset(offset) self._validate_offset(offset, 2) return ctypes.c_uint16.from_buffer(self.mapping, offset).value def read8(self, offset): """Read 8-bits from the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. Returns: int: 8-bit value read. Raises: TypeError: if `offset` type is invalid. ValueError: if `offset` is out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") offset = self._adjust_offset(offset) self._validate_offset(offset, 1) return ctypes.c_uint8.from_buffer(self.mapping, offset).value def read(self, offset, length): """Read a string of bytes from the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. length (int): number of bytes to read. Returns: bytes: bytes read. Raises: TypeError: if `offset` type is invalid. ValueError: if `offset` is out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") offset = self._adjust_offset(offset) self._validate_offset(offset, length) return bytes(self.mapping[offset:offset + length]) def write64(self, offset, value): """Write 64-bits to the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. value (int, long): 64-bit value to write. Raises: TypeError: if `offset` or `value` type are invalid. ValueError: if `offset` or `value` are out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") if not isinstance(value, (int, long)): raise TypeError("Invalid value type, should be integer.") if value < 0 or value > 0xffffffffffffffff: raise ValueError("Value out of bounds.") offset = self._adjust_offset(offset) self._validate_offset(offset, 8) ctypes.c_uint64.from_buffer(self.mapping, offset).value = value def write32(self, offset, value): """Write 32-bits to the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. value (int, long): 32-bit value to write. Raises: TypeError: if `offset` or `value` type are invalid. ValueError: if `offset` or `value` are out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") if not isinstance(value, (int, long)): raise TypeError("Invalid value type, should be integer.") if value < 0 or value > 0xffffffff: raise ValueError("Value out of bounds.") offset = self._adjust_offset(offset) self._validate_offset(offset, 4) ctypes.c_uint32.from_buffer(self.mapping, offset).value = value def write16(self, offset, value): """Write 16-bits to the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. value (int, long): 16-bit value to write. Raises: TypeError: if `offset` or `value` type are invalid. ValueError: if `offset` or `value` are out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") if not isinstance(value, (int, long)): raise TypeError("Invalid value type, should be integer.") if value < 0 or value > 0xffff: raise ValueError("Value out of bounds.") offset = self._adjust_offset(offset) self._validate_offset(offset, 2) ctypes.c_uint16.from_buffer(self.mapping, offset).value = value def write8(self, offset, value): """Write 8-bits to the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. value (int, long): 8-bit value to write. Raises: TypeError: if `offset` or `value` type are invalid. ValueError: if `offset` or `value` are out of bounds. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") if not isinstance(value, (int, long)): raise TypeError("Invalid value type, should be integer.") if value < 0 or value > 0xff: raise ValueError("Value out of bounds.") offset = self._adjust_offset(offset) self._validate_offset(offset, 1) ctypes.c_uint8.from_buffer(self.mapping, offset).value = value def write(self, offset, data): """Write a string of bytes to the specified `offset` in bytes, relative to the base physical address of the MMIO region. Args: offset (int, long): offset from base physical address, in bytes. data (bytes, bytearray, list): a byte array or list of 8-bit integers to write. Raises: TypeError: if `offset` or `data` type are invalid. ValueError: if `offset` is out of bounds, or if data is not valid bytes. """ if not isinstance(offset, (int, long)): raise TypeError("Invalid offset type, should be integer.") if not isinstance(data, (bytes, bytearray, list)): raise TypeError("Invalid data type, expected bytes, bytearray, or list.") offset = self._adjust_offset(offset) self._validate_offset(offset, len(data)) data = bytes(bytearray(data)) self.mapping[offset:offset + len(data)] = data def close(self): """Unmap the MMIO object's mapped physical memory.""" if self.mapping is None: return self.mapping.close() self.mapping = None # Immutable properties @property def base(self): """Get the base physical address of the MMIO region. :type: int """ return self._physaddr @property def size(self): """Get the mapping size of the MMIO region. :type: int """ return self._size @property def pointer(self): """Get a ctypes void pointer to the memory mapped region. :type: ctypes.c_void_p """ return ctypes.cast(ctypes.pointer(ctypes.c_uint8.from_buffer(self.mapping, self._adjust_offset(0))), ctypes.c_void_p) # String representation def __str__(self): return "MMIO 0x{:08x} (size={:d})".format(self.base, self.size) vsergeev-python-periphery-e82b08d/periphery/mmio.pyi000066400000000000000000000021621527060700700230020ustar00rootroot00000000000000from ctypes import c_void_p from types import TracebackType class MMIOError(IOError): ... class MMIO: def __init__(self, physaddr: int, size: int, path: str = ...) -> None: ... def __del__(self) -> None: ... def __enter__(self) -> MMIO: ... # noqa: Y034 def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... def read64(self, offset: int) -> int: ... def read32(self, offset: int) -> int: ... def read16(self, offset: int) -> int: ... def read8(self, offset: int) -> int: ... def read(self, offset: int, length: int) -> bytes: ... def write64(self, offset: int, value: int) -> None: ... def write32(self, offset: int, value: int) -> None: ... def write16(self, offset: int, value: int) -> None: ... def write8(self, offset: int, value: int) -> None: ... def write(self, offset: int, data: bytes | bytearray | list[int]) -> None: ... def close(self) -> None: ... @property def base(self) -> int: ... @property def size(self) -> int: ... @property def pointer(self) -> c_void_p: ... vsergeev-python-periphery-e82b08d/periphery/pwm.py000066400000000000000000000237161527060700700225030ustar00rootroot00000000000000import errno import os import time class PWMError(IOError): """Base class for PWM errors.""" pass class PWM(object): # Number of retries to check for successful PWM export on open _PWM_STAT_RETRIES = 10 # Delay between check for successful PWM export on open (100ms) _PWM_STAT_DELAY = 0.1 def __init__(self, chip, channel): """Instantiate a PWM object and open the sysfs PWM corresponding to the specified chip and channel. Args: chip (int): PWM chip number. channel (int): PWM channel number. Returns: PWM: PWM object. Raises: PWMError: if an I/O or OS error occurs. TypeError: if `chip` or `channel` types are invalid. LookupError: if PWM chip does not exist. TimeoutError: if waiting for PWM export times out. """ self._chip = None self._channel = None self._path = None self._period_ns = None self._open(chip, channel) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, value, traceback): self.close() def _open(self, chip, channel): if not isinstance(chip, int): raise TypeError("Invalid chip type, should be integer.") if not isinstance(channel, int): raise TypeError("Invalid channel type, should be integer.") chip_path = "/sys/class/pwm/pwmchip{}".format(chip) channel_path = "/sys/class/pwm/pwmchip{}/pwm{}".format(chip, channel) if not os.path.isdir(chip_path): raise LookupError("Opening PWM: PWM chip {} not found.".format(chip)) if not os.path.isdir(channel_path): # Export the PWM try: with open(os.path.join(chip_path, "export"), "w") as f_export: f_export.write("{:d}\n".format(channel)) except IOError as e: raise PWMError(e.errno, "Exporting PWM channel: " + e.strerror) # Loop until PWM is exported exported = False for i in range(PWM._PWM_STAT_RETRIES): if os.path.isdir(channel_path): exported = True break time.sleep(PWM._PWM_STAT_DELAY) if not exported: raise TimeoutError("Exporting PWM: waiting for \"{:s}\" timed out".format(channel_path)) # Loop until period is writable. This could take some time after # export as application of udev rules after export is asynchronous. for i in range(PWM._PWM_STAT_RETRIES): try: with open(os.path.join(channel_path, "period"), 'w'): break except IOError as e: if e.errno != errno.EACCES or i == PWM._PWM_STAT_RETRIES - 1: raise PWMError(e.errno, "Opening PWM period: " + e.strerror) time.sleep(PWM._PWM_STAT_DELAY) self._chip = chip self._channel = channel self._path = channel_path # Cache the period for fast duty cycle updates self._period_ns = self._get_period_ns() def close(self): """Close the PWM.""" if self._channel is not None: # Unexport the PWM channel try: unexport_fd = os.open("/sys/class/pwm/pwmchip{}/unexport".format(self._chip), os.O_WRONLY) os.write(unexport_fd, "{:d}\n".format(self._channel).encode()) os.close(unexport_fd) except OSError as e: raise PWMError(e.errno, "Unexporting PWM: " + e.strerror) self._chip = None self._channel = None def _write_channel_attr(self, attr, value): with open(os.path.join(self._path, attr), 'w') as f_attr: f_attr.write(value + "\n") def _read_channel_attr(self, attr): with open(os.path.join(self._path, attr), 'r') as f_attr: return f_attr.read().strip() # Methods def enable(self): """Enable the PWM output.""" self.enabled = True def disable(self): """Disable the PWM output.""" self.enabled = False # Immutable properties @property def devpath(self): """Get the device path of the underlying sysfs PWM device. :type: str """ return self._path @property def chip(self): """Get the PWM chip number. :type: int """ return self._chip @property def channel(self): """Get the PWM channel number. :type: int """ return self._channel # Mutable properties def _get_period_ns(self): period_ns_str = self._read_channel_attr("period") try: period_ns = int(period_ns_str) except ValueError: raise PWMError(None, "Unknown period value: \"{:s}\"".format(period_ns_str)) # Update our cached period self._period_ns = period_ns return period_ns def _set_period_ns(self, period_ns): if not isinstance(period_ns, int): raise TypeError("Invalid period type, should be int.") self._write_channel_attr("period", str(period_ns)) # Update our cached period self._period_ns = period_ns period_ns = property(_get_period_ns, _set_period_ns) """Get or set the PWM's output period in nanoseconds. Raises: PWMError: if an I/O or OS error occurs. TypeError: if value type is not int. :type: int """ def _get_duty_cycle_ns(self): duty_cycle_ns_str = self._read_channel_attr("duty_cycle") try: duty_cycle_ns = int(duty_cycle_ns_str) except ValueError: raise PWMError(None, "Unknown duty cycle value: \"{:s}\"".format(duty_cycle_ns_str)) return duty_cycle_ns def _set_duty_cycle_ns(self, duty_cycle_ns): if not isinstance(duty_cycle_ns, int): raise TypeError("Invalid duty cycle type, should be int.") self._write_channel_attr("duty_cycle", str(duty_cycle_ns)) duty_cycle_ns = property(_get_duty_cycle_ns, _set_duty_cycle_ns) """Get or set the PWM's output duty cycle in nanoseconds. Raises: PWMError: if an I/O or OS error occurs. TypeError: if value type is not int. :type: int """ def _get_period(self): return float(self.period_ns) / 1e9 def _set_period(self, period): if not isinstance(period, (int, float)): raise TypeError("Invalid period type, should be int or float.") # Convert period from seconds to integer nanoseconds self.period_ns = int(period * 1e9) period = property(_get_period, _set_period) """Get or set the PWM's output period in seconds. Raises: PWMError: if an I/O or OS error occurs. TypeError: if value type is not int or float. :type: int, float """ def _get_duty_cycle(self): return float(self.duty_cycle_ns) / self._period_ns def _set_duty_cycle(self, duty_cycle): if not isinstance(duty_cycle, (int, float)): raise TypeError("Invalid duty cycle type, should be int or float.") elif not 0.0 <= duty_cycle <= 1.0: raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.") # Convert duty cycle from ratio to nanoseconds self.duty_cycle_ns = int(duty_cycle * self._period_ns) duty_cycle = property(_get_duty_cycle, _set_duty_cycle) """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0. Raises: PWMError: if an I/O or OS error occurs. TypeError: if value type is not int or float. ValueError: if value is out of bounds of 0.0 to 1.0. :type: int, float """ def _get_frequency(self): return 1.0 / self.period def _set_frequency(self, frequency): if not isinstance(frequency, (int, float)): raise TypeError("Invalid frequency type, should be int or float.") self.period = 1.0 / frequency frequency = property(_get_frequency, _set_frequency) """Get or set the PWM's output frequency in Hertz. Raises: PWMError: if an I/O or OS error occurs. TypeError: if value type is not int or float. :type: int, float """ def _get_polarity(self): return self._read_channel_attr("polarity") def _set_polarity(self, polarity): if not isinstance(polarity, str): raise TypeError("Invalid polarity type, should be str.") elif polarity.lower() not in ["normal", "inversed"]: raise ValueError("Invalid polarity, can be: \"normal\" or \"inversed\".") self._write_channel_attr("polarity", polarity.lower()) polarity = property(_get_polarity, _set_polarity) """Get or set the PWM's output polarity. Can be "normal" or "inversed". Raises: PWMError: if an I/O or OS error occurs. TypeError: if value type is not str. ValueError: if value is invalid. :type: str """ def _get_enabled(self): enabled = self._read_channel_attr("enable") if enabled == "1": return True elif enabled == "0": return False raise PWMError(None, "Unknown enabled value: \"{:s}\"".format(enabled)) def _set_enabled(self, value): if not isinstance(value, bool): raise TypeError("Invalid enabled type, should be bool.") self._write_channel_attr("enable", "1" if value else "0") enabled = property(_get_enabled, _set_enabled) """Get or set the PWM's output enabled state. Raises: PWMError: if an I/O or OS error occurs. TypeError: if value type is not bool. :type: bool """ # String representation def __str__(self): return "PWM {:d}, chip {:d} (period={:f} sec, duty_cycle={:f}%, polarity={:s}, enabled={:s})" \ .format(self._channel, self._chip, self.period, self.duty_cycle * 100, self.polarity, str(self.enabled)) vsergeev-python-periphery-e82b08d/periphery/pwm.pyi000066400000000000000000000013551527060700700226470ustar00rootroot00000000000000from types import TracebackType class PWMError(IOError): ... class PWM: def __init__(self, chip: int, channel: int) -> None: ... def __del__(self) -> None: ... def __enter__(self) -> PWM: ... # noqa: Y034 def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... def close(self) -> None: ... enabled: bool def enable(self) -> None: ... def disable(self) -> None: ... @property def devpath(self) -> str: ... @property def chip(self) -> int: ... @property def channel(self) -> int: ... period_ns: int duty_cycle_ns: int period: int | float duty_cycle: int | float frequency: int | float polarity: str vsergeev-python-periphery-e82b08d/periphery/py.typed000066400000000000000000000000001527060700700230020ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/periphery/serial.py000066400000000000000000000612621527060700700231550ustar00rootroot00000000000000import os import fcntl import array import termios import select class SerialError(IOError): """Base class for Serial errors.""" pass class Serial(object): _DATABITS_TO_CFLAG = { 5: termios.CS5, 6: termios.CS6, 7: termios.CS7, 8: termios.CS8 } _CFLAG_TO_DATABITS = {v: k for k, v in _DATABITS_TO_CFLAG.items()} _BAUDRATE_TO_OSPEED = { 50: termios.B50, 75: termios.B75, 110: termios.B110, 134: termios.B134, 150: termios.B150, 200: termios.B200, 300: termios.B300, 600: termios.B600, 1200: termios.B1200, 1800: termios.B1800, 2400: termios.B2400, 4800: termios.B4800, 9600: termios.B9600, 19200: termios.B19200, 38400: termios.B38400, 57600: termios.B57600, 115200: termios.B115200, 230400: termios.B230400, # Linux baudrates bits missing in termios module included below 460800: 0x1004, 500000: 0x1005, 576000: 0x1006, 921600: 0x1007, 1000000: 0x1008, 1152000: 0x1009, 1500000: 0x100A, 2000000: 0x100B, 2500000: 0x100C, 3000000: 0x100D, 3500000: 0x100E, 4000000: 0x100F, } _OSPEED_TO_BAUDRATE = {v: k for k, v in _BAUDRATE_TO_OSPEED.items()} def __init__(self, devpath, baudrate, databits=8, parity="none", stopbits=1, xonxoff=False, rtscts=False): """Instantiate a Serial object and open the tty device at the specified path with the specified baudrate, and the defaults of 8 data bits, no parity, 1 stop bit, no software flow control (xonxoff), and no hardware flow control (rtscts). Args: devpath (str): tty device path. baudrate (int): baudrate. databits (int): data bits, can be 5, 6, 7, 8. parity (str): parity, can be "none", "even", "odd". stopbits (int): stop bits, can be 1 or 2. xonxoff (bool): software flow control. rtscts (bool): hardware flow control. Returns: Serial: Serial object. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `devpath`, `baudrate`, `databits`, `parity`, `stopbits`, `xonxoff`, or `rtscts` types are invalid. ValueError: if `baudrate`, `databits`, `parity`, or `stopbits` values are invalid. """ self._fd = None self._devpath = None self._use_termios_timeout = False self._open(devpath, baudrate, databits, parity, stopbits, xonxoff, rtscts) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, value, traceback): self.close() def _open(self, devpath, baudrate, databits, parity, stopbits, xonxoff, rtscts): if not isinstance(devpath, str): raise TypeError("Invalid devpath type, should be string.") elif not isinstance(baudrate, int): raise TypeError("Invalid baud rate type, should be integer.") elif not isinstance(databits, int): raise TypeError("Invalid data bits type, should be integer.") elif not isinstance(parity, str): raise TypeError("Invalid parity type, should be string.") elif not isinstance(stopbits, int): raise TypeError("Invalid stop bits type, should be integer.") elif not isinstance(xonxoff, bool): raise TypeError("Invalid xonxoff type, should be boolean.") elif not isinstance(rtscts, bool): raise TypeError("Invalid rtscts type, should be boolean.") if baudrate not in Serial._BAUDRATE_TO_OSPEED: raise ValueError("Unknown baud rate: {:d}".format(baudrate)) elif databits not in [5, 6, 7, 8]: raise ValueError("Invalid data bits, can be 5, 6, 7, 8.") elif parity.lower() not in ["none", "even", "odd"]: raise ValueError("Invalid parity, can be: \"none\", \"even\", \"odd\".") elif stopbits not in [1, 2]: raise ValueError("Invalid stop bits, can be 1, 2.") # Open tty try: self._fd = os.open(devpath, os.O_RDWR | os.O_NOCTTY) except OSError as e: raise SerialError(e.errno, "Opening serial port: " + e.strerror) self._devpath = devpath parity = parity.lower() (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = (0, 0, 0, 0, 0, 0, [0] * 32) ### # iflag # Ignore break characters iflag = termios.IGNBRK # Setup parity if parity != "none": iflag |= (termios.INPCK | termios.ISTRIP) # Setup xonxoff if xonxoff: iflag |= (termios.IXON | termios.IXOFF) ####### # oflag oflag = 0 ####### # lflag lflag = 0 ####### # cflag # Enable receiver, ignore modem control lines cflag = (termios.CREAD | termios.CLOCAL) # Setup data bits cflag |= Serial._DATABITS_TO_CFLAG[databits] # Setup parity if parity == "even": cflag |= termios.PARENB elif parity == "odd": cflag |= (termios.PARENB | termios.PARODD) # Setup stop bits if stopbits == 2: cflag |= termios.CSTOPB # Setup rtscts if rtscts: cflag |= termios.CRTSCTS # Setup baud rate cflag |= Serial._BAUDRATE_TO_OSPEED[baudrate] ######## # ispeed ispeed = Serial._BAUDRATE_TO_OSPEED[baudrate] ######## # ospeed ospeed = Serial._BAUDRATE_TO_OSPEED[baudrate] # Set tty attributes try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) # Methods def read(self, length, timeout=None): """Read up to `length` number of bytes from the serial port with an optional timeout. `timeout` can be positive for a blocking read with a timeout in seconds, zero for a non-blocking read, or negative or None for a blocking read that will block until `length` number of bytes are read. Default is a blocking read. For a non-blocking or timeout-bound read, `read()` may return less than the requested number of bytes. For a blocking read with the VMIN setting configured, `read()` will block until at least VMIN bytes are read. For a blocking read with both VMIN and VTIME settings configured, `read()` will block until at least VMIN bytes are read or the VTIME interbyte timeout expires after the last byte read. In either case, `read()` may return less than the requested number of bytes. Args: length (int): length in bytes. timeout (int, float, None): timeout duration in seconds. Returns: bytes: data read. Raises: SerialError: if an I/O or OS error occurs. """ data = b"" while len(data) < length: (rlist, _, _) = select.select([self._fd], [], [], timeout) if self._fd not in rlist: break try: chunk = os.read(self._fd, length - len(data)) except OSError as e: raise SerialError(e.errno, "Reading serial port: " + e.strerror) if self._use_termios_timeout: return chunk if not chunk: raise SerialError(0, "Reading serial port: unexpected empty read") data += chunk return data def write(self, data): """Write `data` to the serial port and return the number of bytes written. Args: data (bytes, bytearray, list): a byte array or list of 8-bit integers to write. Returns: int: number of bytes written. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `data` type is invalid. ValueError: if data is not valid bytes. """ if not isinstance(data, (bytes, bytearray, list)): raise TypeError("Invalid data type, should be bytes, bytearray, or list.") if isinstance(data, list): data = bytearray(data) try: return os.write(self._fd, data) except OSError as e: raise SerialError(e.errno, "Writing serial port: " + e.strerror) def poll(self, timeout=None): """Poll for data available for reading from the serial port with an optional timeout. `timeout` can be positive for a timeout in seconds, zero for a non-blocking poll, or negative or None for a blocking poll. Default is a blocking poll. Args: timeout (int, float, None): timeout duration in seconds. Returns: bool: ``True`` if data is available for reading from the serial port, ``False`` if not. """ p = select.poll() p.register(self._fd, select.POLLIN | select.POLLPRI) events = p.poll(int(timeout * 1000) if timeout is not None else None) if len(events) > 0: return True return False def flush(self): """Flush the write buffer of the serial port, blocking until all bytes are written. Raises: SerialError: if an I/O or OS error occurs. """ try: termios.tcdrain(self._fd) except termios.error as e: raise SerialError(e.errno, "Flushing serial port: " + e.strerror) def input_waiting(self): """Query the number of bytes waiting to be read from the serial port. Returns: int: number of bytes waiting to be read. Raises: SerialError: if an I/O or OS error occurs. """ # Get input waiting buf = array.array('I', [0]) try: fcntl.ioctl(self._fd, termios.TIOCINQ, buf, True) except (OSError, IOError) as e: raise SerialError(e.errno, "Querying input waiting: " + e.strerror) return buf[0] def output_waiting(self): """Query the number of bytes waiting to be written to the serial port. Returns: int: number of bytes waiting to be written. Raises: SerialError: if an I/O or OS error occurs. """ # Get output waiting buf = array.array('I', [0]) try: fcntl.ioctl(self._fd, termios.TIOCOUTQ, buf, True) except (OSError, IOError) as e: raise SerialError(e.errno, "Querying output waiting: " + e.strerror) return buf[0] def close(self): """Close the tty device. Raises: SerialError: if an I/O or OS error occurs. """ if self._fd is None: return try: os.close(self._fd) except OSError as e: raise SerialError(e.errno, "Closing serial port: " + e.strerror) self._fd = None # Immutable properties @property def fd(self): """Get the file descriptor of the underlying tty device. :type: int """ return self._fd @property def devpath(self): """Get the device path of the underlying tty device. :type: str """ return self._devpath # Mutable properties def _get_baudrate(self): # Get tty attributes try: (_, _, _, _, _, ospeed, _) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) if ospeed not in Serial._OSPEED_TO_BAUDRATE: raise SerialError(None, "Unknown baud rate: ospeed 0x{:x}".format(ospeed)) return Serial._OSPEED_TO_BAUDRATE[ospeed] def _set_baudrate(self, baudrate): if not isinstance(baudrate, int): raise TypeError("Invalid baud rate type, should be integer.") if baudrate not in Serial._BAUDRATE_TO_OSPEED: raise ValueError("Unknown baud rate: {:d}".format(baudrate)) # Get tty attributes try: (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) # Modify tty attributes cflag &= ~(termios.CBAUD | termios.CBAUDEX) cflag |= Serial._BAUDRATE_TO_OSPEED[baudrate] ispeed = Serial._BAUDRATE_TO_OSPEED[baudrate] ospeed = Serial._BAUDRATE_TO_OSPEED[baudrate] # Set tty attributes try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) baudrate = property(_get_baudrate, _set_baudrate) """Get or set the baudrate. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `baudrate` type is not int. ValueError: if `baudrate` value is not supported. :type: int """ def _get_databits(self): # Get tty attributes try: (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) cs = cflag & termios.CSIZE if cs not in Serial._CFLAG_TO_DATABITS: raise SerialError(None, "Unknown data bits setting: csize 0x{:x}".format(cs)) return Serial._CFLAG_TO_DATABITS[cs] def _set_databits(self, databits): if not isinstance(databits, int): raise TypeError("Invalid data bits type, should be integer.") elif databits not in [5, 6, 7, 8]: raise ValueError("Invalid data bits, can be 5, 6, 7, 8.") # Get tty attributes try: (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) # Modify tty attributes cflag &= ~termios.CSIZE cflag |= Serial._DATABITS_TO_CFLAG[databits] # Set tty attributes try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) databits = property(_get_databits, _set_databits) """Get or set the data bits. Can be 5, 6, 7, 8. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `databits` type is not int. ValueError: if `databits` value is invalid. :type: int """ def _get_parity(self): # Get tty attributes try: (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) if (cflag & termios.PARENB) == 0: return "none" elif (cflag & termios.PARODD) == 0: return "even" else: return "odd" def _set_parity(self, parity): if not isinstance(parity, str): raise TypeError("Invalid parity type, should be string.") elif parity.lower() not in ["none", "even", "odd"]: raise ValueError("Invalid parity, can be: \"none\", \"even\", \"odd\".") parity = parity.lower() # Get tty attributes try: (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) # Modify tty attributes iflag &= ~(termios.INPCK | termios.ISTRIP) cflag &= ~(termios.PARENB | termios.PARODD) if parity != "none": iflag |= (termios.INPCK | termios.ISTRIP) cflag |= termios.PARENB if parity == "odd": cflag |= termios.PARODD # Set tty attributes try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) parity = property(_get_parity, _set_parity) """Get or set the parity. Can be "none", "even", "odd". Raises: SerialError: if an I/O or OS error occurs. TypeError: if `parity` type is not str. ValueError: if `parity` value is invalid. :type: str """ def _get_stopbits(self): # Get tty attributes try: (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) if (cflag & termios.CSTOPB) != 0: return 2 else: return 1 def _set_stopbits(self, stopbits): if not isinstance(stopbits, int): raise TypeError("Invalid stop bits type, should be integer.") elif stopbits not in [1, 2]: raise ValueError("Invalid stop bits, can be 1, 2.") # Get tty attributes try: (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) # Modify tty attributes cflag &= ~termios.CSTOPB if stopbits == 2: cflag |= termios.CSTOPB # Set tty attributes try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) stopbits = property(_get_stopbits, _set_stopbits) """Get or set the stop bits. Can be 1 or 2. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `stopbits` type is not int. ValueError: if `stopbits` value is invalid. :type: int """ def _get_xonxoff(self): # Get tty attributes try: (iflag, _, _, _, _, _, _) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) if (iflag & (termios.IXON | termios.IXOFF)) != 0: return True else: return False def _set_xonxoff(self, enabled): if not isinstance(enabled, bool): raise TypeError("Invalid enabled type, should be boolean.") # Get tty attributes try: (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) # Modify tty attributes iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY) if enabled: iflag |= (termios.IXON | termios.IXOFF) # Set tty attributes try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) xonxoff = property(_get_xonxoff, _set_xonxoff) """Get or set software flow control. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `xonxoff` type is not bool. :type: bool """ def _get_rtscts(self): # Get tty attributes try: (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) if (cflag & termios.CRTSCTS) != 0: return True else: return False def _set_rtscts(self, enabled): if not isinstance(enabled, bool): raise TypeError("Invalid enabled type, should be boolean.") # Get tty attributes try: (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) # Modify tty attributes cflag = ~termios.CRTSCTS if enabled: cflag |= termios.CRTSCTS # Set tty attributes try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) rtscts = property(_get_rtscts, _set_rtscts) """Get or set hardware flow control. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `rtscts` type is not bool. :type: bool """ def _get_vmin(self): try: iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) return cc[termios.VMIN] def _set_vmin(self, vmin): if not isinstance(vmin, int): raise TypeError("Invalid vmin type, should be integer.") elif not (0 <= vmin <= 255): raise ValueError("Invalid vmin, can be 0 to 255.") try: iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) cc[termios.VMIN] = vmin try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) self._use_termios_timeout = vmin > 0 vmin = property(_get_vmin, _set_vmin) """Get or set the VMIN termios setting for minimum number of bytes returned from a blocking read. Can be between 0 and 255. When configured in conjunction with VTIME, VTIME acts as an interbyte timeout that restarts on every byte received, and a blocking read will block until at least VMIN bytes are read or the VTIME timeout expires after the last byte read. See the `termios` man page for more information. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `vmin` type is not int. ValueError: if `vmin` value is invalid. :type: int """ def _get_vtime(self): try: iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) return float(cc[termios.VTIME]) / 10.0 def _set_vtime(self, vtime): if not isinstance(vtime, (float, int)): raise TypeError("Invalid vtime type, should be float or integer.") elif not (0 <= vtime <= 25.5): raise ValueError("Invalid vtime, can be 0 to 25.5 seconds.") try: iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(self._fd) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) cc[termios.VTIME] = int(float(vtime) * 10.0) try: termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) except termios.error as e: raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) vtime = property(_get_vtime, _set_vtime) """Get or set the VTIME termios setting for timeout in seconds of a blocking read. Can be between 0 to 25.5 seconds, with a resolution of 0.1 seconds. When configured in conjunction with VMIN, VTIME acts as an interbyte timeout that restarts on every byte received, and a blocking read will block until at least VMIN bytes are read or the VTIME timeout expires after the last byte read. See the `termios` man page for more information. Raises: SerialError: if an I/O or OS error occurs. TypeError: if `vtime` type is not float or int. ValueError: if `vtime` value is invalid. :type: float """ # String representation def __str__(self): return "Serial (device={:s}, fd={:d}, baudrate={:d}, databits={:d}, parity={:s}, stopbits={:d}, xonxoff={:s}, rtscts={:s}, vmin={:d}, vtime={:.1f})" \ .format(self.devpath, self.fd, self.baudrate, self.databits, self.parity, self.stopbits, str(self.xonxoff), str(self.rtscts), self.vmin, self.vtime) vsergeev-python-periphery-e82b08d/periphery/serial.pyi000066400000000000000000000021321527060700700233150ustar00rootroot00000000000000from types import TracebackType class SerialError(IOError): ... class Serial: def __init__( self, devpath: str, baudrate: int, databits: int = ..., parity: str = ..., stopbits: int = ..., xonxoff: bool = ..., rtscts: bool = ..., ) -> None: ... def __del__(self) -> None: ... def __enter__(self) -> Serial: ... # noqa: Y034 def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... def read(self, length: int, timeout: float | None = ...) -> bytes: ... def write(self, data: bytes | bytearray | list[int]) -> int: ... def poll(self, timeout: float | None = ...) -> bool: ... def flush(self) -> None: ... def input_waiting(self) -> int: ... def output_waiting(self) -> int: ... def close(self) -> None: ... @property def fd(self) -> int: ... @property def devpath(self) -> str: ... baudrate: int databits: int parity: str stopbits: int xonxoff: bool rtscts: bool vmin: int vtime: float vsergeev-python-periphery-e82b08d/periphery/spi.py000066400000000000000000000502471527060700700224720ustar00rootroot00000000000000import os import fcntl import array import ctypes import platform try: KERNEL_VERSION = tuple([int(s) for s in platform.release().split(".")[:2]]) except ValueError: KERNEL_VERSION = (0, 0) class SPIError(IOError): """Base class for SPI errors.""" pass class _CSpiIocTransfer(ctypes.Structure): _fields_ = [ ('tx_buf', ctypes.c_ulonglong), ('rx_buf', ctypes.c_ulonglong), ('len', ctypes.c_uint), ('speed_hz', ctypes.c_uint), ('delay_usecs', ctypes.c_ushort), ('bits_per_word', ctypes.c_ubyte), ('cs_change', ctypes.c_ubyte), ('tx_nbits', ctypes.c_ubyte), ('rx_nbits', ctypes.c_ubyte), ('word_delay_usecs', ctypes.c_ubyte), ('pad', ctypes.c_ubyte), ] class SPI(object): # Constants scraped from _SPI_CPHA = 0x1 _SPI_CPOL = 0x2 _SPI_LSB_FIRST = 0x8 _SPI_IOC_WR_MODE = 0x40016b01 _SPI_IOC_RD_MODE = 0x80016b01 _SPI_IOC_WR_MODE32 = 0x40046b05 _SPI_IOC_RD_MODE32 = 0x80046b05 _SPI_IOC_WR_MAX_SPEED_HZ = 0x40046b04 _SPI_IOC_RD_MAX_SPEED_HZ = 0x80046b04 _SPI_IOC_WR_BITS_PER_WORD = 0x40016b03 _SPI_IOC_RD_BITS_PER_WORD = 0x80016b03 _SPI_IOC_MESSAGE_1 = 0x40206b00 _SPI_IOC_MAGIC = 0x6b _SUPPORTS_MODE32 = KERNEL_VERSION >= (3, 15) _SUPPORTS_WORD_DELAY = KERNEL_VERSION >= (5, 2) def __init__(self, devpath, mode, max_speed, bit_order="msb", bits_per_word=8, extra_flags=0): """Instantiate a SPI object and open the spidev device at the specified path with the specified SPI mode, max speed in hertz, and the defaults of "msb" bit order and 8 bits per word. Args: devpath (str): spidev device path. mode (int): SPI mode, can be 0, 1, 2, 3. max_speed (int, float): maximum speed in Hertz. bit_order (str): bit order, can be "msb" or "lsb". bits_per_word (int): bits per word. extra_flags (int): extra spidev flags to be bitwise-ORed with the SPI mode. Returns: SPI: SPI object. Raises: SPIError: if an I/O or OS error occurs. TypeError: if `devpath`, `mode`, `max_speed`, `bit_order`, `bits_per_word`, or `extra_flags` types are invalid. ValueError: if `mode`, `bit_order`, `bits_per_word`, or `extra_flags` values are invalid. """ self._fd = None self._devpath = None self._open(devpath, mode, max_speed, bit_order, bits_per_word, extra_flags) def __del__(self): self.close() def __enter__(self): return self def __exit__(self, t, value, traceback): self.close() def _open(self, devpath, mode, max_speed, bit_order, bits_per_word, extra_flags): if not isinstance(devpath, str): raise TypeError("Invalid devpath type, should be string.") elif not isinstance(mode, int): raise TypeError("Invalid mode type, should be integer.") elif not isinstance(max_speed, (int, float)): raise TypeError("Invalid max_speed type, should be integer or float.") elif not isinstance(bit_order, str): raise TypeError("Invalid bit_order type, should be string.") elif not isinstance(bits_per_word, int): raise TypeError("Invalid bits_per_word type, should be integer.") elif not isinstance(extra_flags, int): raise TypeError("Invalid extra_flags type, should be integer.") if mode not in [0, 1, 2, 3]: raise ValueError("Invalid mode, can be 0, 1, 2, 3.") elif bit_order.lower() not in ["msb", "lsb"]: raise ValueError("Invalid bit_order, can be \"msb\" or \"lsb\".") elif bits_per_word < 0 or bits_per_word > 255: raise ValueError("Invalid bits_per_word, must be 0-255.") # Open spidev try: self._fd = os.open(devpath, os.O_RDWR) except OSError as e: raise SPIError(e.errno, "Opening SPI device: " + e.strerror) self._devpath = devpath bit_order = bit_order.lower() # Set mode, bit order, extra flags if extra_flags > 0xff: if not SPI._SUPPORTS_MODE32: raise SPIError(None, "32-bit mode configuration not supported by kernel version {}.{}.".format(*KERNEL_VERSION)) # Use 32-bit mode if extra flags is wider than 8-bits buf = array.array("I", [mode | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) | extra_flags]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE32, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) else: # Prefer 8-bit mode for compatibility with older kernels buf = array.array("B", [mode | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) | extra_flags]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) # Set max speed buf = array.array("I", [int(max_speed)]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MAX_SPEED_HZ, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI max speed: " + e.strerror) # Set bits per word buf = array.array("B", [bits_per_word]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_BITS_PER_WORD, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI bits per word: " + e.strerror) # Methods def transfer(self, data, inplace=False): """Shift out `data` and return shifted in data. Args: data (bytes, bytearray, list): a byte array or list of 8-bit integers to shift out. inplace (bool): read into `data` in-place. data must be of type `bytearray`. Returns: bytes, bytearray, list: data shifted in. Raises: SPIError: if an I/O or OS error occurs. TypeError: if `data` type is invalid. """ if not isinstance(data, (bytes, bytearray, list)): raise TypeError("Invalid data type, should be bytes, bytearray, or list.") # Convert data to bytearray (or re-use it if inplace) buf = data if inplace and isinstance(data, bytearray) else bytearray(data) # Prepare transfer structure spi_xfer = _CSpiIocTransfer() spi_xfer.tx_buf = ctypes.addressof((ctypes.c_ubyte * len(data)).from_buffer(buf)) spi_xfer.rx_buf = spi_xfer.tx_buf spi_xfer.len = len(data) # Transfer try: fcntl.ioctl(self._fd, SPI._SPI_IOC_MESSAGE_1, spi_xfer) except (OSError, IOError) as e: raise SPIError(e.errno, "SPI transfer: " + e.strerror) # Return shifted out data with the same type as shifted in data if isinstance(data, bytes): return bytes(buf) elif isinstance(data, bytearray): return buf elif isinstance(data, list): return list(buf) def transfer_advanced(self, messages, inplace=False): """Transfer multiple messages, shifting out `data` of each message, while shifting the same length in, modifying the `messages` list with the shifted in data of each message. If `deselect` is true, deselect the device before reselecting it for the following transfer. `deselect_delay_us` specifies a delay in microseconds before deselection when `deselect` is true. `word_delay_us` specifies a delay in microseconds between words within a message. Note that `deselect_delay_us` and `word_delay_us` may not be supported by all SPI controllers and may be silently ignored. Args: messages (list, tuple): list of SPI.Message messages to transfer. inplace (bool): read into `messages` in-place. data must be of type `bytearray`. Raises: SPIError: if an I/O or OS error occurs. TypeError: if `messages` or message types are invalid. ValueError: if `messages` length is zero. """ if not isinstance(messages, (list, tuple)): raise TypeError("Invalid messages type, should be list or tuple of SPI.Message.") elif len(messages) == 0: raise ValueError("Invalid messages, should contain at least one message.") for message in messages: if not isinstance(message, SPI.Message): raise TypeError("Invalid message type, should be SPI.Message.") # Prepare transfer structures spi_xfer = (_CSpiIocTransfer * len(messages))() bufs = [] for i, message in enumerate(messages): # Validate message if not isinstance(message.data, (bytes, bytearray, list)): raise TypeError("Invalid data type in message {:d}, should be bytes, bytearray, or list.".format(i)) if not isinstance(message.deselect, bool): raise TypeError("Invalid deselect type in message {:d}, should be bool.".format(i)) if not isinstance(message.deselect_delay_us, int): raise TypeError("Invalid deselect_delay_us type in message {:d}, should be integer.".format(i)) if not isinstance(message.word_delay_us, int): raise TypeError("Invalid word_delay_us type in message {:d}, should be integer.".format(i)) # Convert data to bytearray (or re-use it if inplace) buf = message.data if inplace and isinstance(message.data, bytearray) else bytearray(message.data) spi_xfer[i].tx_buf = ctypes.addressof((ctypes.c_ubyte * len(buf)).from_buffer(buf)) spi_xfer[i].rx_buf = spi_xfer[i].tx_buf spi_xfer[i].len = len(buf) spi_xfer[i].delay_usecs = message.deselect_delay_us spi_xfer[i].cs_change = int(message.deselect) if SPI._SUPPORTS_WORD_DELAY: spi_xfer[i].word_delay_usecs = message.word_delay_us bufs.append(buf) # Prepare the SPI_IOC_MESSAGE ioctl number for message count: # _IOW(SPI_IOC_MAGIC, 0, struct spi_ioc_transfer[count]) spi_ioc_message = (1 << 30) | ((ctypes.sizeof(_CSpiIocTransfer) * len(messages)) << 16) | (SPI._SPI_IOC_MAGIC << 8) | 0x0 # Transfer try: fcntl.ioctl(self._fd, spi_ioc_message, spi_xfer) except (OSError, IOError) as e: raise SPIError(e.errno, "SPI transfer: " + e.strerror) # Update SPI.Message data with the shifted in data for i in range(len(messages)): # Convert shifted in data to type used in SPI.Message messages if isinstance(messages[i].data, list): messages[i].data = list(bufs[i]) elif isinstance(messages[i].data, bytearray): messages[i].data = bufs[i] elif isinstance(messages[i].data, bytes): messages[i].data = bytes(bufs[i]) def close(self): """Close the spidev SPI device. Raises: SPIError: if an I/O or OS error occurs. """ if self._fd is None: return try: os.close(self._fd) except OSError as e: raise SPIError(e.errno, "Closing SPI device: " + e.strerror) self._fd = None # Immutable properties @property def fd(self): """Get the file descriptor of the underlying spidev device. :type: int """ return self._fd @property def devpath(self): """Get the device path of the underlying spidev device. :type: str """ return self._devpath # Mutable properties def _get_mode(self): buf = array.array('B', [0]) # Get mode try: fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) return buf[0] & 0x3 def _set_mode(self, mode): if not isinstance(mode, int): raise TypeError("Invalid mode type, should be integer.") if mode not in [0, 1, 2, 3]: raise ValueError("Invalid mode, can be 0, 1, 2, 3.") # Read-modify-write mode, because the mode contains bits for other settings # Get mode buf = array.array('B', [0]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) buf[0] = (buf[0] & ~(SPI._SPI_CPOL | SPI._SPI_CPHA)) | mode # Set mode try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) mode = property(_get_mode, _set_mode) """Get or set the SPI mode. Can be 0, 1, 2, 3. Raises: SPIError: if an I/O or OS error occurs. TypeError: if `mode` type is not int. ValueError: if `mode` value is invalid. :type: int """ def _get_max_speed(self): # Get max speed buf = array.array('I', [0]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MAX_SPEED_HZ, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI max speed: " + e.strerror) return buf[0] def _set_max_speed(self, max_speed): if not isinstance(max_speed, (int, float)): raise TypeError("Invalid max_speed type, should be integer or float.") # Set max speed buf = array.array('I', [int(max_speed)]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MAX_SPEED_HZ, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI max speed: " + e.strerror) max_speed = property(_get_max_speed, _set_max_speed) """Get or set the maximum speed in Hertz. Raises: SPIError: if an I/O or OS error occurs. TypeError: if `max_speed` type is not int or float. :type: int, float """ def _get_bit_order(self): # Get mode buf = array.array('B', [0]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) if (buf[0] & SPI._SPI_LSB_FIRST) > 0: return "lsb" return "msb" def _set_bit_order(self, bit_order): if not isinstance(bit_order, str): raise TypeError("Invalid bit_order type, should be string.") elif bit_order.lower() not in ["msb", "lsb"]: raise ValueError("Invalid bit_order, can be \"msb\" or \"lsb\".") # Read-modify-write mode, because the mode contains bits for other settings # Get mode buf = array.array('B', [0]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) bit_order = bit_order.lower() buf[0] = (buf[0] & ~SPI._SPI_LSB_FIRST) | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) # Set mode try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) bit_order = property(_get_bit_order, _set_bit_order) """Get or set the SPI bit order. Can be "msb" or "lsb". Raises: SPIError: if an I/O or OS error occurs. TypeError: if `bit_order` type is not str. ValueError: if `bit_order` value is invalid. :type: str """ def _get_bits_per_word(self): # Get bits per word buf = array.array('B', [0]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_BITS_PER_WORD, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI bits per word: " + e.strerror) return buf[0] def _set_bits_per_word(self, bits_per_word): if not isinstance(bits_per_word, int): raise TypeError("Invalid bits_per_word type, should be integer.") if bits_per_word < 0 or bits_per_word > 255: raise ValueError("Invalid bits_per_word, must be 0-255.") # Set bits per word buf = array.array('B', [bits_per_word]) try: fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_BITS_PER_WORD, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI bits per word: " + e.strerror) bits_per_word = property(_get_bits_per_word, _set_bits_per_word) """Get or set the SPI bits per word. Raises: SPIError: if an I/O or OS error occurs. TypeError: if `bits_per_word` type is not int. ValueError: if `bits_per_word` value is invalid. :type: int """ def _get_extra_flags(self): if SPI._SUPPORTS_MODE32: buf = array.array('I', [0]) rd_cmd = SPI._SPI_IOC_RD_MODE32 else: buf = array.array('B', [0]) rd_cmd = SPI._SPI_IOC_RD_MODE try: fcntl.ioctl(self._fd, rd_cmd, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) return buf[0] & ~(SPI._SPI_LSB_FIRST | SPI._SPI_CPHA | SPI._SPI_CPOL) def _set_extra_flags(self, extra_flags): if not isinstance(extra_flags, int): raise TypeError("Invalid extra_flags type, should be integer.") # Read-modify-write mode, because the mode contains bits for other settings if extra_flags > 0xff: if not SPI._SUPPORTS_MODE32: raise SPIError(None, "32-bit mode configuration not supported by kernel version {}.{}.".format(*KERNEL_VERSION)) buf = array.array('I', [0]) rd_cmd = SPI._SPI_IOC_RD_MODE32 wr_cmd = SPI._SPI_IOC_WR_MODE32 else: buf = array.array('B', [0]) rd_cmd = SPI._SPI_IOC_RD_MODE wr_cmd = SPI._SPI_IOC_WR_MODE # Get mode try: fcntl.ioctl(self._fd, rd_cmd, buf, True) except (OSError, IOError) as e: raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) buf[0] = (buf[0] & (SPI._SPI_LSB_FIRST | SPI._SPI_CPHA | SPI._SPI_CPOL)) | extra_flags # Set mode try: fcntl.ioctl(self._fd, wr_cmd, buf, False) except (OSError, IOError) as e: raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) extra_flags = property(_get_extra_flags, _set_extra_flags) """Get or set the spidev extra flags. Extra flags are bitwise-ORed with the SPI mode. Raises: SPIError: if an I/O or OS error occurs. TypeError: if `extra_flags` type is not int. ValueError: if `extra_flags` value is invalid. :type: int """ # String representation def __str__(self): return "SPI (device={:s}, fd={:d}, mode={:d}, max_speed={:d}, bit_order={:s}, bits_per_word={:d}, extra_flags=0x{:08x})" \ .format(self.devpath, self.fd, self.mode, self.max_speed, self.bit_order, self.bits_per_word, self.extra_flags) class Message: def __init__(self, data, deselect=False, deselect_delay_us=0, word_delay_us=0): """Instantiate an SPI Message object. Args: data (bytes, bytearray, list): a byte array or list of 8-bit integers to shift out. deselect (bool): deselect the device before reselecting it for the following transfer. deselect_delay_us (int): delay in microseconds before deselection when `deselect` is true. word_delay_us (int): delay in microseconds between words within the transfer. Returns: Message: Message object. """ self.data = data self.deselect = deselect self.deselect_delay_us = deselect_delay_us self.word_delay_us = word_delay_us vsergeev-python-periphery-e82b08d/periphery/spi.pyi000066400000000000000000000023021527060700700226300ustar00rootroot00000000000000from types import TracebackType KERNEL_VERSION: tuple[int, int] class SPIError(IOError): ... class SPI: class Message: data: bytes | bytearray | list[int] deselect: bool deselect_delay_us: int word_delay_us: int def __init__(self, data: bytes | bytearray | list[int], deselect: bool = ..., deselect_delay_us: int = ..., word_delay_us: int = ...) -> None: ... def __init__( self, devpath: str, mode: int, max_speed: float, bit_order: str = ..., bits_per_word: int = ..., extra_flags: int = ... ) -> None: ... def __del__(self) -> None: ... def __enter__(self) -> SPI: ... # noqa: Y034 def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... def transfer(self, data: bytes | bytearray | list[int], inplace: bool = ...) -> bytes | bytearray | list[int]: ... def transfer_advanced(self, messages: list[Message], inplace: bool = ...) -> None: ... def close(self) -> None: ... @property def fd(self) -> int: ... @property def devpath(self) -> str: ... mode: int max_speed: float bit_order: str bits_per_word: int extra_flags: int vsergeev-python-periphery-e82b08d/setup.cfg000066400000000000000000000001341527060700700211250ustar00rootroot00000000000000[metadata] description-file = README.md license_file = LICENSE [bdist_wheel] universal = 1 vsergeev-python-periphery-e82b08d/setup.py000066400000000000000000000032301527060700700210160ustar00rootroot00000000000000try: from setuptools import setup except ImportError: from distutils.core import setup setup( name='python-periphery', version='2.5.0', description='A pure Python 2/3 library for peripheral I/O (GPIO, LED, PWM, SPI, I2C, MMIO, Serial) in Linux.', author='vsergeev', author_email='v@sergeev.io', url='https://github.com/vsergeev/python-periphery', packages=['periphery'], package_data={'periphery': ['*.pyi', 'py.typed']}, long_description="""python-periphery is a pure Python library for GPIO, LED, PWM, SPI, I2C, MMIO, and Serial peripheral I/O interface access in userspace Linux. It is useful in embedded Linux environments (including Raspberry Pi, BeagleBone, etc. platforms) for interfacing with external peripherals. python-periphery is compatible with Python 2 and Python 3, is written in pure Python, and is MIT licensed. See https://github.com/vsergeev/python-periphery for more information.""", classifiers=[ 'Development Status :: 5 - Production/Stable', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Embedded Systems', 'Topic :: System :: Hardware', 'Topic :: System :: Hardware :: Hardware Drivers', ], license='MIT', keywords='gpio spi led pwm i2c mmio serial uart embedded linux beaglebone raspberrypi rpi odroid', ) vsergeev-python-periphery-e82b08d/tests/000077500000000000000000000000001527060700700204505ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/tests/__init__.py000066400000000000000000000000001527060700700225470ustar00rootroot00000000000000vsergeev-python-periphery-e82b08d/tests/test.py000066400000000000000000000023521527060700700220030ustar00rootroot00000000000000import inspect STR_OKAY = " [\x1b[1;32m OK \x1b[0m]" STR_FAIL = " [\x1b[1;31mFAIL\x1b[0m]" def ptest(): frame = inspect.stack()[1] function, lineno = frame[3], frame[2] print("\n\nStarting test {:s}():{:d}".format(function, lineno)) def pokay(msg): print("{:s} {:s}".format(STR_OKAY, msg)) def passert(name, condition): frame = inspect.stack()[1] filename, function, lineno = frame[1].split('/')[-1], frame[3], frame[2] print("{:s} {:s} {:s}():{:d} {:s}".format(STR_OKAY if condition else STR_FAIL, filename, function, lineno, name)) assert condition class AssertRaises(object): def __init__(self, name, exception_type): self.name = name self.exception_type = exception_type def __enter__(self): return self def __exit__(self, t, value, traceback): frame = inspect.stack()[1] filename, function, lineno = frame[1].split('/')[-1], frame[3], frame[2] if not isinstance(value, self.exception_type): print("{:s} {:s} {:s}():{:d} {:s}".format(STR_FAIL, filename, function, lineno, self.name)) return False print("{:s} {:s} {:s}():{:d} {:s}".format(STR_OKAY, filename, function, lineno, self.name)) return True vsergeev-python-periphery-e82b08d/tests/test_gpio.py000066400000000000000000000333221527060700700230220ustar00rootroot00000000000000import os import sys import threading import time import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input import queue else: import Queue as queue path = None line_input = None line_output = None def test_arguments(): ptest() # Invalid open types with AssertRaises("invalid open types", TypeError): periphery.GPIO(1, 1, "in") with AssertRaises("invalid open types", TypeError): periphery.GPIO("abc", 2.3, "in") with AssertRaises("invalid open types", TypeError): periphery.GPIO("abc", 1, 1) # Invalid direction with AssertRaises("invalid direction", ValueError): periphery.GPIO("abc", 1, "blah") def test_open_close(): ptest() # Open non-existent GPIO (export should fail with EINVAL) with AssertRaises("non-existent GPIO", periphery.GPIOError): periphery.GPIO(path, 9999, "in") # Open legitimate GPIO gpio = periphery.GPIO(path, line_output, "in") passert("property line", gpio.line == line_output) passert("direction is in", gpio.direction == "in") passert("fd >= 0", gpio.fd >= 0) passert("chip_fd >= 0", gpio.chip_fd >= 0) # Check default label passert("property label", gpio.label == "periphery") # Set invalid direction with AssertRaises("set invalid direction", ValueError): gpio.direction = "blah" # Set invalid edge with AssertRaises("set invalid edge", ValueError): gpio.edge = "blah" # Set invalid event clock with AssertRaises("set invalid event clock", ValueError): gpio.event_clock = "blah" # Set invalid bias with AssertRaises("set invalid bias", ValueError): gpio.bias = "blah" # Set invalid drive with AssertRaises("set invalid drive", ValueError): gpio.drive = "blah" # Set direction out, check direction out, check value low gpio.direction = "out" passert("direction is out", gpio.direction == "out") passert("value is low", gpio.read() == False) # Set direction low, check direction out, check value low gpio.direction = "low" passert("direction is out", gpio.direction == "out") passert("value is low", gpio.read() == False) # Set direction high, check direction out, check value high gpio.direction = "high" passert("direction is out", gpio.direction == "out") passert("value is high", gpio.read() == True) # Set drive open drain, check drive open drain gpio.drive = "open_drain" passert("drive is open drain", gpio.drive == "open_drain") # Set drive open source, check drive open source gpio.drive = "open_source" passert("drive is open drain", gpio.drive == "open_source") # Set drive default, check drive default gpio.drive = "default" passert("drive is default", gpio.drive == "default") # Set inverted true, check inverted true gpio.inverted = True passert("inverted is True", gpio.inverted == True) # Set inverted false, check inverted false gpio.inverted = False passert("inverted is False", gpio.inverted == False) # Attempt to set interrupt edge on output GPIO with AssertRaises("set interrupt edge on output GPIO", periphery.GPIOError): gpio.edge = "rising" # Attempt to set event clock on output GPIO with AssertRaises("set event clock on output GPIO", periphery.GPIOError): gpio.event_clock = "hte" # Attempt to set debounce on output GPIO with AssertRaises("set debounce on output GPIO", periphery.GPIOError): gpio.debounce_us = 10 # Attempt to read event on output GPIO with AssertRaises("read event on output GPIO", periphery.GPIOError): gpio.read_event() # Set direction in, check direction in gpio.direction = "in" passert("direction is in", gpio.direction == "in") # Set edge none, check edge none gpio.edge = "none" passert("edge is none", gpio.edge == "none") # Set edge rising, check edge rising gpio.edge = "rising" passert("edge is rising", gpio.edge == "rising") # Set edge falling, check edge falling gpio.edge = "falling" passert("edge is falling", gpio.edge == "falling") # Set edge both, check edge both gpio.edge = "both" passert("edge is both", gpio.edge == "both") # Set event clock realtime, check event clock realtime gpio.event_clock = "realtime" passert("event clock is realtime", gpio.event_clock == "realtime") # Set event clock monotonic, check event clock monotonic gpio.event_clock = "monotonic" passert("event clock is monotonic", gpio.event_clock == "monotonic") # Set debounce, check debounce gpio.debounce_us = 10 passert("debounce us is 10", gpio.debounce_us == 10) # Disable debounce, check debounce gpio.debounce_us = 0 passert("debounce us is 0", gpio.debounce_us == 0) # Set bias pull up, check bias pull up gpio.bias = "pull_up" passert("bias is pull up", gpio.bias == "pull_up") # Set bias pull down, check bias pull down gpio.bias = "pull_down" passert("bias is pull down", gpio.bias == "pull_down") # Set bias disable, check bias disable gpio.bias = "disable" passert("bias is disable", gpio.bias == "disable") # Set bias default, check bias default gpio.bias = "default" passert("bias is default", gpio.bias == "default") # Attempt to set drive on input GPIO with AssertRaises("set drive on input GPIO", periphery.GPIOError): gpio.drive = "open_drain" # Attempt to set negative debounce on input GPIO with AssertRaises("set negative debounce on input GPIO", ValueError): gpio.debounce_us = -1 gpio.close() # Open with keyword arguments gpio = periphery.GPIO(path, line_input, "in", edge="rising", bias="default", drive="default", inverted=False, label="test123") passert("property line", gpio.line == line_input) passert("direction is in", gpio.direction == "in") passert("fd >= 0", gpio.fd >= 0) passert("chip_fd >= 0", gpio.chip_fd >= 0) passert("edge is rising", gpio.edge == "rising") passert("bias is default", gpio.bias == "default") passert("drive is default", gpio.drive == "default") passert("inverted is False", gpio.inverted == False) passert("label is test123", gpio.label == "test123") gpio.close() def test_loopback(): ptest() # Open in and out lines gpio_in = periphery.GPIO(path, line_input, "in") gpio_out = periphery.GPIO(path, line_output, "out") # Drive out low, check in low print("Drive out low, check in low") gpio_out.write(False) passert("value is False", gpio_in.read() == False) # Drive out high, check in high print("Drive out high, check in high") gpio_out.write(True) passert("value is True", gpio_in.read() == True) # Wrapper for running poll() in a thread def threaded_poll(gpio, timeout): ret = queue.Queue() def f(): ret.put(gpio.poll(timeout)) thread = threading.Thread(target=f) thread.start() return ret # Check poll falling 1 -> 0 interrupt print("Check poll falling 1 -> 0 interrupt") gpio_in.edge = "falling" poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(False) passert("gpio_in polled True", poll_ret.get() == True) passert("value is low", gpio_in.read() == False) event = gpio_in.read_event() passert("event edge is falling", event.edge == "falling") passert("event timestamp is non-zero", event.timestamp != 0) # Check poll rising 0 -> 1 interrupt print("Check poll rising 0 -> 1 interrupt") gpio_in.edge = "rising" poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(True) passert("gpin_in polled True", poll_ret.get() == True) passert("value is high", gpio_in.read() == True) event = gpio_in.read_event() passert("event edge is rising", event.edge == "rising") passert("event timestamp is non-zero", event.timestamp != 0) # Set edge to both gpio_in.edge = "both" # Check poll falling 1 -> 0 interrupt print("Check poll falling 1 -> 0 interrupt") poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(False) passert("gpio_in polled True", poll_ret.get() == True) passert("value is low", gpio_in.read() == False) event = gpio_in.read_event() passert("event edge is falling", event.edge == "falling") passert("event timestamp is non-zero", event.timestamp != 0) # Check poll rising 0 -> 1 interrupt print("Check poll rising 0 -> 1 interrupt") poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(True) passert("gpio_in polled True", poll_ret.get() == True) passert("value is high", gpio_in.read() == True) event = gpio_in.read_event() passert("event edge is rising", event.edge == "rising") passert("event timestamp is non-zero", event.timestamp != 0) # Check poll timeout print("Check poll timeout") passert("gpio_in polled False", gpio_in.poll(1) == False) # Check poll falling 1 -> 0 interrupt with the poll_multiple() API print("Check poll falling 1 -> 0 interrupt with poll_multiple()") gpio_out.write(False) gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) passert("value is low", gpio_in.read() == False) event = gpio_in.read_event() passert("event edge is falling", event.edge == "falling") passert("event timestamp is non-zero", event.timestamp != 0) # Check poll rising 0 -> 1 interrupt with the poll_multiple() API print("Check poll rising 0 -> 1 interrupt with poll_multiple()") gpio_out.write(True) gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) passert("value is high", gpio_in.read() == True) event = gpio_in.read_event() passert("event edge is rising", event.edge == "rising") passert("event timestamp is non-zero", event.timestamp != 0) # Check poll timeout print("Check poll timeout with poll_multiple()") gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) passert("gpios ready is empty", gpios_ready == []) gpio_in.close() gpio_out.close() # Open both GPIOs as inputs gpio_in = periphery.GPIO(path, line_input, "in") gpio_out = periphery.GPIO(path, line_output, "in") # Set bias pull-up, check value is high print("Check input GPIO reads high with pull-up bias") gpio_in.bias = "pull_up" time.sleep(0.1) passert("value is high", gpio_in.read() == True) # Set bias pull-down, check value is low print("Check input GPIO reads low with pull-down bias") gpio_in.bias = "pull_down" time.sleep(0.1) passert("value is low", gpio_in.read() == False) gpio_in.close() gpio_out.close() def test_interactive(): print("Starting interactive test...") gpio_out = periphery.GPIO(path, line_output, "out") print("Starting interactive test. Get out your multimeter, buddy!") raw_input("Press enter to continue...") # Check tostring print("GPIO description: {}".format(str(gpio_out))) passert("interactive success", raw_input("GPIO description looks ok? y/n ") == "y") # Drive GPIO out low gpio_out.write(False) passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") # Drive GPIO out high gpio_out.write(True) passert("interactive success", raw_input("GPIO out is high? y/n ") == "y") # Drive GPIO out low gpio_out.write(False) passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") # Open input pin gpio_in = periphery.GPIO(path, line_input, "in") # Set both edge and realtime event clock gpio_in.edge = "both" gpio_in.event_clock = "realtime" # Read line event with realtime event clock print("Driving GPIO out high and reading GPIO in line event...") gpio_out.write(True) event = gpio_in.read_event() passert("event edge is rising", event.edge == "rising") passert("interactive success", raw_input("Line event timestamp {:d} is realtime? y/n ".format(event.timestamp)) == "y") # Set monotonic event clock gpio_in.event_clock = "monotonic" # Read line event with monotonic event clock print("Driving GPIO out low and reading GPIO in line event...") gpio_out.write(False) event = gpio_in.read_event() passert("event edge is falling", event.edge == "falling") passert("interactive success", raw_input("Line event timestamp {:d} is monotonic? y/n ".format(event.timestamp)) == "y") gpio_in.close() gpio_out.close() if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) if len(sys.argv) < 3: print("Usage: python -m tests.test_gpio ") print("") print("[1/4] Argument test: No requirements.") print("[2/4] Open/close test: GPIO #2 should be real.") print("[3/4] Loopback test: GPIOs #1 and #2 should be connected with a wire.") print("[4/4] Interactive test: GPIO #2 should be observed with a multimeter.") print("") print("Hint: for Raspberry Pi 3,") print("Use GPIO 17 (header pin 11) and GPIO 27 (header pin 13),") print("connect a loopback between them, and run this test with:") print(" python -m tests.test_gpio /dev/gpiochip0 17 27") print("") sys.exit(1) path = sys.argv[1] line_input = int(sys.argv[2]) line_output = int(sys.argv[3]) test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!") vsergeev-python-periphery-e82b08d/tests/test_gpio_sysfs.py000066400000000000000000000204631527060700700242530ustar00rootroot00000000000000import os import sys import threading import time import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input import queue else: import Queue as queue line_input = None line_output = None def test_arguments(): ptest() # Invalid open types with AssertRaises("invalid open types", TypeError): periphery.GPIO("abc", "out") with AssertRaises("invalid open types", TypeError): periphery.GPIO(100, 100) # Invalid direction with AssertRaises("invalid direction", ValueError): periphery.GPIO(100, "blah") def test_open_close(): ptest() # Open non-existent GPIO (export should fail with EINVAL) with AssertRaises("non-existent GPIO", periphery.GPIOError): periphery.GPIO(9999, "in") # Open legitimate GPIO gpio = periphery.GPIO(line_output, "in") passert("property line", gpio.line == line_output) passert("direction is in", gpio.direction == "in") passert("fd >= 0", gpio.fd >= 0) # Set invalid direction with AssertRaises("set invalid direction", ValueError): gpio.direction = "blah" # Set invalid edge with AssertRaises("set invalid edge", ValueError): gpio.edge = "blah" # Unsupported property bias with AssertRaises("unsupported property bias", NotImplementedError): _ = gpio.bias with AssertRaises("unsupported property bias", NotImplementedError): gpio.bias = "pull_up" # Unsupported property drive with AssertRaises("unsupported property drive", NotImplementedError): _ = gpio.drive with AssertRaises("unsupported property drive", NotImplementedError): gpio.drive = "open_drain" # Unsupported proprety with AssertRaises("unsupported property chip_fd", NotImplementedError): _ = gpio.chip_fd # Unsupported method with AssertRaises("unsupported method", NotImplementedError): gpio.read_event() # Set direction out, check direction out, check value low gpio.direction = "out" passert("direction is out", gpio.direction == "out") passert("value is low", gpio.read() == False) # Set direction low, check direction out, check value low gpio.direction = "low" passert("direction is out", gpio.direction == "out") passert("value is low", gpio.read() == False) # Set direction high, check direction out, check value high gpio.direction = "high" passert("direction is out", gpio.direction == "out") passert("ivalue is high", gpio.read() == True) # Set inverted true, check inverted gpio.inverted = True passert("inverted is True", gpio.inverted == True) # Set inverted false, check inverted gpio.inverted = False passert("inverted is False", gpio.inverted == False) # Set direction in, check direction in gpio.direction = "in" passert("direction is in", gpio.direction == "in") # Set edge none, check edge none gpio.edge = "none" passert("edge is none", gpio.edge == "none") # Set edge rising, check edge rising gpio.edge = "rising" passert("edge is rising", gpio.edge == "rising") # Set edge falling, check edge falling gpio.edge = "falling" passert("edge is falling", gpio.edge == "falling") # Set edge both, check edge both gpio.edge = "both" passert("edge is both", gpio.edge == "both") # Set edge none, check edge none gpio.edge = "none" passert("edge is none", gpio.edge == "none") gpio.close() def test_loopback(): ptest() # Open in and out lines gpio_in = periphery.GPIO(line_input, "in") gpio_out = periphery.GPIO(line_output, "out") # Drive out low, check in low print("Drive out low, check in low") gpio_out.write(False) passert("value is low", gpio_in.read() == False) # Drive out high, check in high print("Drive out high, check in high") gpio_out.write(True) passert("value is high", gpio_in.read() == True) # Wrapper for running poll() in a thread def threaded_poll(gpio, timeout): ret = queue.Queue() def f(): ret.put(gpio.poll(timeout)) thread = threading.Thread(target=f) thread.start() return ret # Check poll falling 1 -> 0 interrupt print("Check poll falling 1 -> 0 interrupt") gpio_in.edge = "falling" poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(False) passert("gpio_in polled True", poll_ret.get() == True) passert("value is low", gpio_in.read() == False) # Check poll rising 0 -> 1 interrupt print("Check poll rising 0 -> 1 interrupt") gpio_in.edge = "rising" poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(True) passert("gpio_in polled True", poll_ret.get() == True) passert("value is high", gpio_in.read() == True) # Set edge to both gpio_in.edge = "both" # Check poll falling 1 -> 0 interrupt print("Check poll falling 1 -> 0 interrupt") poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(False) passert("gpio_in polled True", poll_ret.get() == True) passert("value is low", gpio_in.read() == False) # Check poll rising 0 -> 1 interrupt print("Check poll rising 0 -> 1 interrupt") poll_ret = threaded_poll(gpio_in, 5) time.sleep(0.5) gpio_out.write(True) passert("gpio_in polled True", poll_ret.get() == True) passert("value is high", gpio_in.read() == True) # Check poll timeout print("Check poll timeout") passert("gpio_in polled False", gpio_in.poll(1) == False) # Check poll falling 1 -> 0 interrupt with the poll_multiple() API print("Check poll falling 1 -> 0 interrupt with poll_multiple()") gpio_out.write(False) gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) passert("value is low", gpio_in.read() == False) # Check poll rising 0 -> 1 interrupt with the poll_multiple() API print("Check poll rising 0 -> 1 interrupt with poll_multiple()") gpio_out.write(True) gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) passert("value is high", gpio_in.read() == True) # Check poll timeout print("Check poll timeout with poll_multiple()") gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) passert("gpios ready is empty", gpios_ready == []) gpio_in.close() gpio_out.close() def test_interactive(): ptest() gpio = periphery.GPIO(line_output, "out") print("Starting interactive test. Get out your multimeter, buddy!") raw_input("Press enter to continue...") # Check tostring print("GPIO description: {}".format(str(gpio))) passert("interactive success", raw_input("GPIO description looks ok? y/n ") == "y") # Drive GPIO out low gpio.write(False) passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") # Drive GPIO out high gpio.write(True) passert("interactive success", raw_input("GPIO out is high? y/n ") == "y") # Drive GPIO out low gpio.write(False) passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") gpio.close() if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) if len(sys.argv) < 3: print("Usage: python -m tests.test_gpio ") print("") print("[1/4] Argument test: No requirements.") print("[2/4] Open/close test: GPIO #2 should be real.") print("[3/4] Loopback test: GPIOs #1 and #2 should be connected with a wire.") print("[4/4] Interactive test: GPIO #2 should be observed with a multimeter.") print("") print("Hint: for Raspberry Pi 3,") print("Use GPIO 17 (header pin 11) and GPIO 27 (header pin 13),") print("connect a loopback between them, and run this test with:") print(" python -m tests.test_gpio_sysfs 17 27") print("") sys.exit(1) line_input = int(sys.argv[1]) line_output = int(sys.argv[2]) test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!") vsergeev-python-periphery-e82b08d/tests/test_i2c.py000066400000000000000000000111051527060700700225340ustar00rootroot00000000000000import os import random import sys import time import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input i2c_devpath = None i2c_eeprom_addr = 0x51 def test_arguments(): ptest() # Open with invalid type with AssertRaises("open invalid type", TypeError): periphery.I2C(123) def test_open_close(): ptest() # Open non-existent device with AssertRaises("non-existent device", periphery.I2CError): periphery.I2C("/foo/bar") # Open legitimate device i2c = periphery.I2C(i2c_devpath) passert("fd >= 0", i2c.fd >= 0) # Close I2C i2c.close() def test_loopback(): ptest() # Generate random byte vector vector = [random.randint(0, 255) for _ in range(32)] i2c = periphery.I2C(i2c_devpath) # Write vector to 0x100 with list # S [ addr W ] [ 0x01 ] [ 0x00 ] [ Data... ] P msgs = [periphery.I2C.Message([0x01, 0x00] + vector)] i2c.transfer(i2c_eeprom_addr, msgs) # Wait for write cycle time.sleep(0.01) # Read from 0x100 with list # S [ addr W ] [ 0x01 ] [ 0x00 ] S [ addr R ] [ Data... ] P msgs = [periphery.I2C.Message([0x01, 0x00]), periphery.I2C.Message([0x00] * 32, read=True)] i2c.transfer(i2c_eeprom_addr, msgs) # Verify bytes passert("verify bytes", msgs[1].data == vector) # Write vector to 0x200 with bytes # S [ addr W ] [ 0x02 ] [ 0x00 ] [ Data... ] P msgs = [periphery.I2C.Message(b"\x02\x00" + bytes(vector))] i2c.transfer(i2c_eeprom_addr, msgs) # Wait for write cycle time.sleep(0.01) # Read from 0x200 with bytes # S [ addr W ] [ 0x02 ] [ 0x00 ] S [ addr R ] [ Data... ] P msgs = [periphery.I2C.Message(b"\x02\x00"), periphery.I2C.Message(b"\x00" * 32, read=True)] i2c.transfer(i2c_eeprom_addr, msgs) # Verify bytes passert("verify bytes", msgs[1].data == bytes(vector)) # Read from 0x110 with bytearray, in-place buf = bytearray(16) msgs = [periphery.I2C.Message(bytearray([0x01, 0x10])), periphery.I2C.Message(buf, read=True)] i2c.transfer(i2c_eeprom_addr, msgs, inplace=True) # Verify bytes passert("verify in-place readback", msgs[1].data == bytearray(vector[0x10:])) passert("verify in-place readback id", msgs[1].data is buf) i2c.close() print("Loopback test passed.") def test_interactive(): ptest() # Open device 2 i2c = periphery.I2C(i2c_devpath) print("") print("Starting interactive test. Get out your logic analyzer, buddy!") raw_input("Press enter to continue...") # Check tostring print("I2C description: {}".format(str(i2c))) passert("interactive success", raw_input("I2C description looks ok? y/n ") == "y") # There isn't much we can do without assuming a device on the other end, # because I2C needs an acknowledgement bit on each transferred byte. # # But we can send a transaction and expect it to time out. # S [ 0x7a W ] [0xaa] [0xbb] [0xcc] [0xdd] NA messages = [periphery.I2C.Message([0xaa, 0xbb, 0xcc, 0xdd])] raw_input("Press enter to start transfer...") # Transfer to non-existent device with AssertRaises("transfer to non-existent device", periphery.I2CError): i2c.transfer(0x7a, messages) i2c.close() success = raw_input("I2C transfer occurred? y/n ") passert("interactive success", success == "y") if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) if len(sys.argv) < 2: print("Usage: python -m tests.test_i2c ") print("") print("[1/4] Arguments test: No requirements.") print("[2/4] Open/close test: I2C device should be real.") print("[3/4] Loopback test: Expects 24XX32 EEPROM (or similar) at address 0x51.") print("[4/4] Interactive test: I2C bus should be observed with an oscilloscope or logic analyzer.") print("") print("Hint: for Raspberry Pi 3, enable I2C1 with:") print(" $ echo \"dtparam=i2c_arm=on\" | sudo tee -a /boot/config.txt") print(" $ sudo reboot") print("Use pins I2C1 SDA (header pin 2) and I2C1 SCL (header pin 3),") print("and run this test with:") print(" python -m tests.test_i2c /dev/i2c-1") print("") sys.exit(1) i2c_devpath = sys.argv[1] test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!") vsergeev-python-periphery-e82b08d/tests/test_led.py000066400000000000000000000107361527060700700226340ustar00rootroot00000000000000import os import sys import time import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input led_name = None def test_arguments(): ptest() # Invalid open types with AssertRaises("invalid open types", TypeError): periphery.LED("abc", "out") with AssertRaises("invalid open types", TypeError): periphery.LED(100, 100) def test_open_close(): ptest() # Open non-existent LED with AssertRaises("non-existent led", LookupError): periphery.LED("invalid_led_XXX", 0) # Open legitimate LED led = periphery.LED(led_name, 0) passert("property name", led.name == led_name) passert("fd >= 0", led.fd >= 0) passert("max_brightness > 0", led.max_brightness > 0) # Check triggers list triggers = led.triggers passert("triggers is a list", isinstance(triggers, list)) passert("triggers count > 0", len(triggers) > 0) # Check each trigger entry passert("each trigger is a non-empty string", all(isinstance(t, str) and t for t in triggers)) # Set brightness to True, check brightness led.write(True) time.sleep(0.01) passert("brightness is non-zero", led.read() > 0) # Set brightness to False, check brightness led.write(False) time.sleep(0.01) passert("brightness is zero", led.read() == 0) # Set brightness to 1, check brightness led.write(1) time.sleep(0.01) passert("brightness is non-zero", led.read() >= 1) # Set brightness to 0, check brightness led.write(0) time.sleep(0.01) passert("brightness is zero", led.read() == 0) # Set brightness to 1, check brightness led.brightness = 1 time.sleep(0.01) passert("brightness is non-zero", led.brightness >= 1) # Set brightness to 0, check brightness led.brightness = 0 time.sleep(0.01) passert("brightness is zero", led.brightness == 0) # Set trigger to default, check it isn't none led.trigger = "default" passert("trigger is not none", led.trigger != "none") # Set trigger to none, check it is none led.trigger = "none" passert("trigger is none", led.trigger == "none") # Set trigger to invalid type with AssertRaises("invalid trigger type", TypeError): led.trigger = 100 led.close() def test_loopback(): ptest() # No general way to do a loopback test for I2C without a real component, skipping... def test_interactive(): ptest() led = periphery.LED(led_name, False) raw_input("Press enter to continue...") # Check tostring print("LED description: {}".format(str(led))) passert("interactive success", raw_input("LED description looks ok? y/n ") == "y") # Check triggers print("LED active trigger: {}".format(led.trigger)) print("LED available triggers ({}): {}".format(len(led.triggers), " ".join(led.triggers))) passert("interactive success", raw_input("LED triggers look OK? y/n ") == "y") # Turn LED off led.write(False) passert("interactive success", raw_input("LED is off? y/n ") == "y") # Turn LED on led.write(True) passert("interactive success", raw_input("LED is on? y/n ") == "y") # Turn LED off led.write(False) passert("interactive success", raw_input("LED is off? y/n ") == "y") # Turn LED on led.write(True) passert("interactive success", raw_input("LED is on? y/n ") == "y") # Restore default trigger led.trigger = "default" led.close() if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) if len(sys.argv) < 2: print("Usage: python -m tests.test_led ") print("") print("[1/4] Arguments test: No requirements.") print("[2/4] Open/close test: LED should be real.") print("[3/4] Loopback test: No test.") print("[4/4] Interactive test: LED should be observed.") print("") print("Hint: for Raspberry Pi 3, disable triggers for led1:") print(" $ echo none > /sys/class/leds/led1/trigger") print("Observe led1 (red power LED), and run this test:") print(" python -m tests.test_led led1") print("") sys.exit(1) led_name = sys.argv[1] test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!") vsergeev-python-periphery-e82b08d/tests/test_mmio.py000066400000000000000000000134101527060700700230210ustar00rootroot00000000000000import os import sys import time import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input PAGE_SIZE = 4096 CONTROL_MODULE_BASE = 0x44e10000 USB_VID_PID_OFFSET = 0x7f4 USB_VID_PID = 0x04516141 RTCSS_BASE = 0x44e3e000 RTC_SCRATCH2_REG_OFFSET = 0x68 RTC_KICK0R_REG_OFFSET = 0x6C RTC_KICK1R_REG_OFFSET = 0x70 def test_arguments(): ptest() def test_open_close(): ptest() # Open aligned base mmio = periphery.MMIO(CONTROL_MODULE_BASE, PAGE_SIZE) # Check properties passert("property base", mmio.base == CONTROL_MODULE_BASE) passert("property size", mmio.size == PAGE_SIZE) # Try to write immutable properties with AssertRaises("write immutable", AttributeError): mmio.base = 1000 with AssertRaises("write immutable", AttributeError): mmio.size = 1000 mmio.close() # Open unaligned base mmio = periphery.MMIO(CONTROL_MODULE_BASE + 123, PAGE_SIZE) # Check properties passert("property base", mmio.base == CONTROL_MODULE_BASE + 123) passert("property size", mmio.size == PAGE_SIZE) # Read out of bounds with AssertRaises("read 1 byte over", ValueError): mmio.read32(PAGE_SIZE - 3) with AssertRaises("read 2 bytes over", ValueError): mmio.read32(PAGE_SIZE - 2) with AssertRaises("read 3 bytes over", ValueError): mmio.read32(PAGE_SIZE - 1) with AssertRaises("read 4 bytes over", ValueError): mmio.read32(PAGE_SIZE) mmio.close() def test_loopback(): ptest() # Open control module mmio = periphery.MMIO(CONTROL_MODULE_BASE, PAGE_SIZE) # Read and compare USB VID/PID with read32() passert("compare USB VID/PID", mmio.read32(USB_VID_PID_OFFSET) == USB_VID_PID) # Read and compare USB VID/PID with bytes read data = mmio.read(USB_VID_PID_OFFSET, 4) data = bytearray(data) passert("compare byte 1", data[0] == USB_VID_PID & 0xff) passert("compare byte 2", data[1] == (USB_VID_PID >> 8) & 0xff) passert("compare byte 3", data[2] == (USB_VID_PID >> 16) & 0xff) passert("compare byte 4", data[3] == (USB_VID_PID >> 24) & 0xff) mmio.close() # Open RTC subsystem mmio = periphery.MMIO(RTCSS_BASE, PAGE_SIZE) # Disable write protection mmio.write32(RTC_KICK0R_REG_OFFSET, 0x83E70B13) mmio.write32(RTC_KICK1R_REG_OFFSET, 0x95A4F1E0) # Write/Read RTC Scratch2 Register mmio.write32(RTC_SCRATCH2_REG_OFFSET, 0xdeadbeef) passert("compare write 32-bit uint and readback", mmio.read32(RTC_SCRATCH2_REG_OFFSET) == 0xdeadbeef) # Write/Read RTC Scratch2 Register with bytes write mmio.write(RTC_SCRATCH2_REG_OFFSET, b"\xaa\xbb\xcc\xdd") data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) passert("compare write 4-byte bytes and readback", data == b"\xaa\xbb\xcc\xdd") # Write/Read RTC Scratch2 Register with bytearray write mmio.write(RTC_SCRATCH2_REG_OFFSET, bytearray(b"\xbb\xcc\xdd\xee")) data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) passert("compare write 4-byte bytearray and readback", data == b"\xbb\xcc\xdd\xee") # Write/Read RTC Scratch2 Register with list write mmio.write(RTC_SCRATCH2_REG_OFFSET, [0xcc, 0xdd, 0xee, 0xff]) data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) passert("compare write 4-byte list and readback", data == b"\xcc\xdd\xee\xff") # Write/Read RTC Scratch2 Register with 16-bit write mmio.write16(RTC_SCRATCH2_REG_OFFSET, 0xaabb) passert("compare write 16-bit uint and readback", mmio.read16(RTC_SCRATCH2_REG_OFFSET) == 0xaabb) # Write/Read RTC Scratch2 Register with 8-bit write mmio.write8(RTC_SCRATCH2_REG_OFFSET, 0xab) passert("compare write 8-bit uint and readback", mmio.read8(RTC_SCRATCH2_REG_OFFSET) == 0xab) mmio.close() def test_interactive(): ptest() mmio = periphery.MMIO(RTCSS_BASE, PAGE_SIZE) # Check tostring print("MMIO description: {}".format(str(mmio))) passert("interactive success", raw_input("MMIO description looks ok? y/n ") == "y") print("Waiting for seconds ones digit to reset to 0...\n") # Wait until seconds low go to 0, so we don't have to deal with # overflows in comparing times tic = time.time() while mmio.read32(0x00) & 0xf != 0: periphery.sleep(1) passert("less than 12 seconds elapsed", (time.time() - tic) < 12) # Compare passage of OS time with RTC time tic = time.time() rtc_tic = mmio.read32(0x00) & 0xf bcd2dec = lambda x: 10 * ((x >> 4) & 0xf) + (x & 0xf) print("Date: {:04d}-{:02d}-{:02d}".format(2000 + bcd2dec(mmio.read32(0x14)), bcd2dec(mmio.read32(0x10)), bcd2dec(mmio.read32(0x0c)))) print("Time: {:02d}:{:02d}:{:02d}".format(bcd2dec(mmio.read32(0x08) & 0x7f), bcd2dec(mmio.read32(0x04)), bcd2dec(mmio.read32(0x00)))) periphery.sleep(3) print("Date: {:04d}-{:02d}-{:02d}".format(2000 + bcd2dec(mmio.read32(0x14)), bcd2dec(mmio.read32(0x10)), bcd2dec(mmio.read32(0x0c)))) print("Time: {:02d}:{:02d}:{:02d}".format(bcd2dec(mmio.read32(0x08) & 0x7f), bcd2dec(mmio.read32(0x04)), bcd2dec(mmio.read32(0x00)))) toc = time.time() rtc_toc = mmio.read32(0x00) & 0xf passert("real time elapsed", (toc - tic) > 2) passert("rtc time elapsed", (rtc_toc - rtc_tic) > 2) mmio.close() if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) print("WARNING: This test suite assumes a BeagleBone Black (AM335x) host!") print("Other systems may experience unintended and dire consequences!") raw_input("Press enter to continue!") test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!") vsergeev-python-periphery-e82b08d/tests/test_pwm.py000066400000000000000000000162201527060700700226650ustar00rootroot00000000000000import os import sys import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input pwm_chip = None pwm_channel = None def test_arguments(): ptest() # Invalid open types with AssertRaises("invalid open types", TypeError): periphery.PWM("foo", 0) with AssertRaises("invalid open types", TypeError): periphery.PWM(0, "foo") def test_open_close(): ptest() # Open non-existent PWM chip with AssertRaises("non-existent PWM chip", LookupError): periphery.PWM(9999, pwm_channel) # Open non-existent PWM channel with AssertRaises("non-existent PWM channel", periphery.PWMError): periphery.PWM(pwm_chip, 9999) # Open legitimate PWM chip/channel pwm = periphery.PWM(pwm_chip, pwm_channel) passert("property chip", pwm.chip == pwm_chip) passert("property channel", pwm.channel == pwm_channel) # Initialize period and duty cycle pwm.period = 5e-3 pwm.duty_cycle = 0 # Set period, check period, check period_ns, check frequency pwm.period = 1e-3 passert("period is correct", abs(pwm.period - 1e-3) < 1e-4) passert("period_ns is correct", abs(pwm.period_ns - 1000000) < 1e5) passert("frequency is correct", abs(pwm.frequency - 1000) < 100) pwm.period = 5e-4 passert("period is correct", abs(pwm.period - 5e-4) < 1e-5) passert("period_ns is correct", abs(pwm.period_ns - 500000) < 1e4) passert("frequency is correct", abs(pwm.frequency - 2000) < 100) # Set frequency, check frequency, check period, check period_ns pwm.frequency = 1000 passert("frequency is correct", abs(pwm.frequency - 1000) < 100) passert("period is correct", abs(pwm.period - 1e-3) < 1e-4) passert("period_ns is correct", abs(pwm.period_ns - 1000000) < 1e5) pwm.frequency = 2000 passert("frequency is correct", abs(pwm.frequency - 2000) < 100) passert("period is correct", abs(pwm.period - 5e-4) < 1e-5) passert("period_ns is correct", abs(pwm.period_ns - 500000) < 1e4) # Set period_ns, check period_ns, check period, check frequency pwm.period_ns = 1000000 passert("period_ns is correct", abs(pwm.period_ns - 1000000) < 1e5) passert("period is correct", abs(pwm.period - 1e-3) < 1e-4) passert("frequency is correct", abs(pwm.frequency - 1000) < 100) pwm.period_ns = 500000 passert("period_ns is correct", abs(pwm.period_ns - 500000) < 1e4) passert("period is correct", abs(pwm.period - 5e-4) < 1e-5) passert("frequency is correct", abs(pwm.frequency - 2000) < 100) pwm.period_ns = 1000000 # Set duty cycle, check duty cycle, check duty_cycle_ns pwm.duty_cycle = 0.25 passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.25) < 1e-3) passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 250000) < 1e4) pwm.duty_cycle = 0.50 passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.50) < 1e-3) passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 500000) < 1e4) pwm.duty_cycle = 0.75 passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.75) < 1e-3) passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 750000) < 1e4) # Set duty_cycle_ns, check duty_cycle_ns, check duty_cycle pwm.duty_cycle_ns = 250000 passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 250000) < 1e4) passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.25) < 1e-3) pwm.duty_cycle_ns = 500000 passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 500000) < 1e4) passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.50) < 1e-3) pwm.duty_cycle_ns = 750000 passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 750000) < 1e4) passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.75) < 1e-3) # Set polarity, check polarity pwm.polarity = "normal" passert("polarity is normal", pwm.polarity == "normal") pwm.polarity = "inversed" passert("polarity is inversed", pwm.polarity == "inversed") # Set enabled, check enabled pwm.enabled = True passert("pwm is enabled", pwm.enabled == True) pwm.enabled = False passert("pwm is disabled", pwm.enabled == False) # Use enable()/disable(), check enabled pwm.enable() passert("pwm is enabled", pwm.enabled == True) pwm.disable() passert("pwm is disabled", pwm.enabled == False) # Set invalid polarity with AssertRaises("set invalid polarity", ValueError): pwm.polarity = "foo" pwm.close() def test_loopback(): ptest() def test_interactive(): ptest() pwm = periphery.PWM(pwm_chip, pwm_channel) print("Starting interactive test. Get out your oscilloscope, buddy!") raw_input("Press enter to continue...") # Set initial parameters and enable PWM pwm.duty_cycle = 0.0 pwm.frequency = 1e3 pwm.polarity = "normal" pwm.enabled = True # Check tostring print("PWM description: {}".format(str(pwm))) passert("interactive success", raw_input("PWM description looks ok? y/n ") == "y") # Set 1 kHz frequency, 0.25 duty cycle pwm.frequency = 1e3 pwm.duty_cycle = 0.25 passert("interactive success", raw_input("Frequency is 1 kHz, duty cycle is 25%? y/n ") == "y") # Set 1 kHz frequency, 0.50 duty cycle pwm.frequency = 1e3 pwm.duty_cycle = 0.50 passert("interactive success", raw_input("Frequency is 1 kHz, duty cycle is 50%? y/n ") == "y") # Set 2 kHz frequency, 0.25 duty cycle pwm.frequency = 2e3 pwm.duty_cycle = 0.25 passert("interactive success", raw_input("Frequency is 2 kHz, duty cycle is 25%? y/n ") == "y") # Set 2 kHz frequency, 0.50 duty cycle pwm.frequency = 2e3 pwm.duty_cycle = 0.50 passert("interactive success", raw_input("Frequency is 2 kHz, duty cycle is 50%? y/n ") == "y") pwm.duty_cycle = 0.0 pwm.enabled = False pwm.close() if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) if len(sys.argv) < 3: print("Usage: python -m tests.test_pwm ") print("") print("[1/4] Arguments test: No requirements.") print("[2/4] Open/close test: PWM channel should be real.") print("[3/4] Loopback test: No test.") print("[4/4] Interactive test: PWM channel should be observed with an oscilloscope or logic analyzer.") print("") print("Hint: for Raspberry Pi 3, enable PWM0 and PWM1 with:") print(" $ echo \"dtoverlay=pwm-2chan,pin=18,func=2,pin2=13,func2=4\" | sudo tee -a /boot/config.txt") print(" $ sudo reboot") print("Monitor GPIO 18 (header pin 12), and run this test with:") print(" python -m tests.test_pwm 0 0") print("or, monitor GPIO 13 (header pin 33), and run this test with:") print(" python -m tests.test_pwm 0 1") print("") sys.exit(1) pwm_chip = int(sys.argv[1]) pwm_channel = int(sys.argv[2]) test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!") vsergeev-python-periphery-e82b08d/tests/test_serial.py000066400000000000000000000212411527060700700233400ustar00rootroot00000000000000import os import sys import time import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input serial_device = None def test_arguments(): ptest() # Invalid data bits with AssertRaises("invalid databits", ValueError): periphery.Serial("/dev/ttyS0", 115200, databits=4) with AssertRaises("invalid databits", ValueError): periphery.Serial("/dev/ttyS0", 115200, databits=9) # Invalid parity with AssertRaises("invalid parity", ValueError): periphery.Serial("/dev/ttyS0", 115200, parity="blah") # Invalid stop bits with AssertRaises("invalid stop bits", ValueError): periphery.Serial("/dev/ttyS0", 115200, stopbits=0) with AssertRaises("invalid stop bits", ValueError): periphery.Serial("/dev/ttyS0", 115200, stopbits=3) # Everything else is fair game, although termios might not like it. def test_open_close(): ptest() serial = periphery.Serial(serial_device, 115200) # Confirm default settings passert("fd > 0", serial.fd > 0) passert("baudrate is 115200", serial.baudrate == 115200) passert("databits is 8", serial.databits == 8) passert("parity is none", serial.parity == "none") passert("stopbits is 1", serial.stopbits == 1) passert("xonxoff is False", serial.xonxoff == False) passert("rtscts is False", serial.rtscts == False) passert("vmin is 0", serial.vmin == 0) passert("vtime is 0", serial.vtime == 0) # Change some stuff and check that it changed serial.baudrate = 4800 passert("baudrate is 4800", serial.baudrate == 4800) serial.baudrate = 9600 passert("baudrate is 9600", serial.baudrate == 9600) serial.databits = 7 passert("databits is 7", serial.databits == 7) serial.parity = "odd" passert("parity is odd", serial.parity == "odd") serial.stopbits = 2 passert("stopbits is 2", serial.stopbits == 2) serial.xonxoff = True passert("xonxoff is True", serial.xonxoff == True) # Test serial port may not support rtscts serial.vmin = 50 passert("vmin is 50", serial.vmin == 50) serial.vtime = 15.3 passert("vtime is 15.3", abs(serial.vtime - 15.3) < 0.1) serial.close() def test_loopback(): ptest() lorem_ipsum = b"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." serial = periphery.Serial(serial_device, 115200) # Test write/flush/read with bytes write print("Write, flush, read lorem ipsum with bytes type") passert("wrote lorem ipsum bytes", serial.write(lorem_ipsum) == len(lorem_ipsum)) serial.flush() buf = serial.read(len(lorem_ipsum), timeout=3) passert("compare readback lorem ipsum", buf == lorem_ipsum) # Test write/flush/read with bytearray write print("Write, flush, read lorem ipsum with bytearray type") passert("wrote lorem ipsum bytearray", serial.write(bytearray(lorem_ipsum)) == len(lorem_ipsum)) serial.flush() buf = serial.read(len(lorem_ipsum), timeout=3) passert("compare readback lorem ipsum", buf == lorem_ipsum) # Test write/flush/read with list write print("Write, flush, read lorem ipsum with list type") passert("write lorem ipsum list", serial.write(list(bytearray(lorem_ipsum))) == len(lorem_ipsum)) serial.flush() buf = serial.read(len(lorem_ipsum), timeout=3) passert("compare readback lorem ipsum", buf == lorem_ipsum) # Test poll/write/flush/poll/input waiting/read print("Write, flush, poll, input waiting, read lorem ipsum") passert("poll timed out", serial.poll(0.5) == False) passert("write lorem ipsum", serial.write(lorem_ipsum) == len(lorem_ipsum)) serial.flush() passert("poll succeeded", serial.poll(0.5) == True) periphery.sleep_ms(500) passert("input waiting is lorem ipsum size", serial.input_waiting() == len(lorem_ipsum)) buf = serial.read(len(lorem_ipsum)) passert("compare readback lorem ipsum", buf == lorem_ipsum) # Test non-blocking poll print("Check non-blocking poll") passert("non-blocking poll is False", serial.poll(0) == False) # Test a very large read-write (likely to exceed internal buffer size (~4096)) print("Write, flush, read large buffer") lorem_hugesum = b"\xaa" * (4096 * 3) passert("wrote lorem hugesum", serial.write(lorem_hugesum) == len(lorem_hugesum)) serial.flush() buf = serial.read(len(lorem_hugesum), timeout=3) passert("compare readback lorem hugesum", buf == lorem_hugesum) # Test read timeout print("Check read timeout") tic = time.time() passert("read timed out", serial.read(4096 * 3, timeout=2) == b"") toc = time.time() passert("time elapsed", (toc - tic) > 1) # Test non-blocking read print("Check non-blocking read") tic = time.time() passert("read non-blocking is empty", serial.read(4096 * 3, timeout=0) == b"") toc = time.time() # Assuming we weren't context switched out for a second passert("almost no time elapsed", int(toc - tic) == 0) # Test blocking read with vmin=5 termios timeout print("Check blocking read with vmin termios timeout") serial.vmin = 5 passert("write 5 bytes of lorem ipsum", serial.write(lorem_ipsum[0:5]) == 5) serial.flush() buf = serial.read(len(lorem_ipsum)) passert("compare readback partial lorem ipsum", buf == lorem_ipsum[0:5]) # Test blocking read with vmin=5, vtime=2 termios timeout print("Check blocking read with vmin + vtime termios timeout") serial.vtime = 2 passert("write 3 bytes of lorem ipsum", serial.write(lorem_ipsum[0:3]) == 3) serial.flush() tic = time.time() buf = serial.read(len(lorem_ipsum)) toc = time.time() passert("compare readback partial lorem ipsum", buf == lorem_ipsum[0:3]) passert("time elapsed", (toc - tic) > 1) serial.close() def test_interactive(): ptest() buf = b"Hello World!" serial = periphery.Serial(serial_device, 4800) print("Starting interactive test. Get out your logic analyzer, buddy!") raw_input("Press enter to continue...") # Check tostring print("Serial description: {}".format(str(serial))) passert("interactive success", raw_input("Serial description looks ok? y/n ") == "y") serial.baudrate = 4800 raw_input("Press enter to start transfer...") passert("serial write", serial.write(buf) == len(buf)) passert("interactive success", raw_input("Serial transfer baudrate 4800, 8n1 occurred? y/n ") == "y") serial.baudrate = 9600 raw_input("Press enter to start transfer...") passert("serial write", serial.write(buf) == len(buf)) passert("interactive success", raw_input("Serial transfer baudrate 9600, 8n1 occurred? y/n ") == "y") serial.baudrate = 115200 raw_input("Press enter to start transfer...") passert("serial write", serial.write(buf) == len(buf)) passert("interactive success", raw_input("Serial transfer baudrate 115200, 8n1 occurred? y/n ") == "y") serial.close() if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) if len(sys.argv) < 2: print("Usage: python -m tests.test_serial ") print("") print("[1/4] Arguments test: No requirements.") print("[2/4] Open/close test: Serial port device should be real.") print("[3/4] Loopback test: Serial TX and RX should be connected with a wire.") print("[4/4] Interactive test: Serial TX should be observed with an oscilloscope or logic analyzer.") print("") print("Hint: for Raspberry Pi 3, enable UART0 with:") print(" $ echo \"dtoverlay=pi3-disable-bt\" | sudo tee -a /boot/config.txt") print(" $ sudo systemctl disable hciuart") print(" $ sudo reboot") print(" (Note that this will disable Bluetooth)") print("Use pins UART0 TXD (header pin 8) and UART0 RXD (header pin 10),") print("connect a loopback between TXD and RXD, and run this test with:") print(" python -m tests.test_serial /dev/ttyAMA0") print("") sys.exit(1) serial_device = sys.argv[1] test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!") vsergeev-python-periphery-e82b08d/tests/test_spi.py000066400000000000000000000173041527060700700226610ustar00rootroot00000000000000import os import sys import periphery from .test import ptest, pokay, passert, AssertRaises if sys.version_info[0] == 3: raw_input = input spi_device = None def test_arguments(): ptest() # Invalid mode with AssertRaises("invalid mode", ValueError): periphery.SPI("/dev/spidev0.0", 4, int(1e6)) # Invalid bit order with AssertRaises("invalid bit order", ValueError): periphery.SPI("/dev/spidev0.0", 4, int(1e6), bit_order="blah") def test_open_close(): ptest() # Normal open (mode=1, max_speed = 100000) spi = periphery.SPI(spi_device, 1, 100000) # Confirm fd and defaults passert("fd > 0", spi.fd > 0) passert("mode is 1", spi.mode == 1) passert("max speed is 100000", spi.max_speed == 100000) passert("default bit_order is msb", spi.bit_order == "msb") passert("default bits_per_word is 8", spi.bits_per_word == 8) # Not going to try different bit order or bits per word, because not # all SPI controllers support them # Try modes 0, 1, 2, 3 spi.mode = 0 passert("spi mode is 0", spi.mode == 0) spi.mode = 1 passert("spi mode is 1", spi.mode == 1) spi.mode = 2 passert("spi mode is 2", spi.mode == 2) spi.mode = 3 passert("spi mode is 3", spi.mode == 3) # Try max speeds 100Khz, 500KHz, 1MHz, 2MHz spi.max_speed = 100000 passert("max speed is 100KHz", spi.max_speed == 100000) spi.max_speed = 500000 passert("max speed is 500KHz", spi.max_speed == 500000) spi.max_speed = 1000000 passert("max speed is 1MHz", spi.max_speed == 1000000) spi.max_speed = 2e6 passert("max speed is 2MHz", spi.max_speed == 2000000) spi.close() def test_loopback(): ptest() spi = periphery.SPI(spi_device, 0, 100000) # Try list transfer buf_in = list(range(256)) * 4 buf_out = spi.transfer(buf_in) passert("compare readback", buf_out == buf_in) # Try bytearray transfer buf_in = bytearray(list(range(256)) * 4) buf_out = spi.transfer(buf_in) passert("compare readback", buf_out == buf_in) passert("compare readback id", buf_out is not buf_in) # Try bytearray transfer, reading in-place buf_in = bytearray(list(range(256)) * 4) buf_out = spi.transfer(buf_in, inplace=True) passert("compare readback", buf_out == buf_in) passert("compare readback id", buf_out is buf_in) # Try bytes transfer buf_in = bytes(bytearray(list(range(256)) * 4)) buf_out = spi.transfer(buf_in) passert("compare readback", buf_out == buf_in) # Try multiple transfer buf1 = bytearray(range(32)) buf2 = bytearray(range(32)) msgs = [periphery.SPI.Message(buf1), periphery.SPI.Message(buf2)] spi.transfer_advanced(msgs) passert("compare readback 1", msgs[0].data == bytearray(range(32))) passert("compare readback 2", msgs[1].data == bytearray(range(32))) passert("compare readback 1 id", msgs[0].data is not buf1) passert("compare readback 2 id", msgs[1].data is not buf2) # Try multiple transfer, reading in-place buf1 = bytearray(range(32)) buf2 = bytearray(range(32)) msgs = [periphery.SPI.Message(buf1), periphery.SPI.Message(buf2)] spi.transfer_advanced(msgs, inplace=True) passert("compare readback 1", msgs[0].data == bytearray(range(32))) passert("compare readback 2", msgs[1].data == bytearray(range(32))) passert("compare readback 1 id", msgs[0].data is buf1) passert("compare readback 2 id", msgs[1].data is buf2) # Try multiple transfer of two messages with deselect, one with list data, # one with bytes data msgs = [periphery.SPI.Message(list(range(32)), deselect=True), periphery.SPI.Message(bytes(bytearray(range(32))))] spi.transfer_advanced(msgs) passert("compare readback 1", msgs[0].data == list(range(32))) passert("compare readback 2", msgs[1].data == bytes(bytearray(range(32)))) spi.close() def test_interactive(): ptest() spi = periphery.SPI(spi_device, 0, 100000) print("Starting interactive test. Get out your logic analyzer, buddy!") raw_input("Press enter to continue...") # Check tostring print("SPI description: {}".format(str(spi))) passert("interactive success", raw_input("SPI description looks ok? y/n ") == "y") # Mode 0 transfer raw_input("Press enter to start transfer...") spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) print("SPI data 0x55, 0xaa, 0x0f, 0xf0") passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 0 occurred? y/n ") == "y") # Mode 1 transfer spi.mode = 1 raw_input("Press enter to start transfer...") spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) print("SPI data 0x55, 0xaa, 0x0f, 0xf0") passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 1 occurred? y/n ") == "y") # Mode 2 transfer spi.mode = 2 raw_input("Press enter to start transfer...") spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) print("SPI data 0x55, 0xaa, 0x0f, 0xf0") passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 2 occurred? y/n ") == "y") # Mode 3 transfer spi.mode = 3 raw_input("Press enter to start transfer...") spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) print("SPI data 0x55, 0xaa, 0x0f, 0xf0") passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 3 occurred? y/n ") == "y") spi.mode = 0 # Multiple transfer msgs = [periphery.SPI.Message(bytearray([0x55, 0xaa, 0x0f, 0xf0]), deselect=True), periphery.SPI.Message(b"\x55\xaa\x0f\xf0"), periphery.SPI.Message([0x55, 0xaa, 0x0f, 0xf0])] raw_input("Press enter to start transfer...") spi.transfer_advanced(msgs) print("SPI data 0x55, 0xaa, 0x0f, 0xf0") passert("interactive success", raw_input("SPI transfer of three messages, with deselect after first message occurred? y/n ") == "y") # 500KHz transfer spi.max_speed = 500000 raw_input("Press enter to start transfer...") spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) print("SPI data 0x55, 0xaa, 0x0f, 0xf0") passert("interactive success", raw_input("SPI transfer speed <= 500KHz, mode 0 occurred? y/n ") == "y") # 1MHz transfer spi.max_speed = 1000000 raw_input("Press enter to start transfer...") spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) print("SPI data 0x55, 0xaa, 0x0f, 0xf0") passert("interactive success", raw_input("SPI transfer speed <= 1MHz, mode 0 occurred? y/n ") == "y") spi.close() if __name__ == "__main__": if os.environ.get("CI") == "true": test_arguments() sys.exit(0) if len(sys.argv) < 2: print("Usage: python -m tests.test_spi ") print("") print("[1/4] Arguments test: No requirements.") print("[2/4] Open/close test: SPI device should be real.") print("[3/4] Loopback test: SPI MISO and MOSI should be connected with a wire.") print("[4/4] Interactive test: SPI MOSI, CLK, CS should be observed with an oscilloscope or logic analyzer.") print("") print("Hint: for Raspberry Pi 3, enable SPI0 with:") print(" $ echo \"dtparam=spi=on\" | sudo tee -a /boot/config.txt") print(" $ sudo reboot") print("Use pins SPI0 MOSI (header pin 19), SPI0 MISO (header pin 21), SPI0 SCLK (header pin 23),") print("connect a loopback between MOSI and MISO, and run this test with:") print(" python -m tests.test_spi /dev/spidev0.0") print("") sys.exit(1) spi_device = sys.argv[1] test_arguments() pokay("Arguments test passed.") test_open_close() pokay("Open/close test passed.") test_loopback() pokay("Loopback test passed.") test_interactive() pokay("Interactive test passed.") pokay("All tests passed!")