├── .flake8 ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── gpio.rst ├── i2c.rst ├── index.rst ├── led.rst ├── mmio.rst ├── pwm.rst ├── requirements.txt ├── serial.rst ├── spi.rst └── version.rst ├── periphery ├── __init__.py ├── __init__.pyi ├── gpio.py ├── gpio.pyi ├── gpio_cdev1.py ├── gpio_cdev2.py ├── gpio_sysfs.py ├── i2c.py ├── i2c.pyi ├── led.py ├── led.pyi ├── mmio.py ├── mmio.pyi ├── pwm.py ├── pwm.pyi ├── py.typed ├── serial.py ├── serial.pyi ├── spi.py └── spi.pyi ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── test.py ├── test_gpio.py ├── test_gpio_sysfs.py ├── test_i2c.py ├── test_led.py ├── test_mmio.py ├── test_pwm.py ├── test_serial.py └── test_spi.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E501,F401,E402 3 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | container: ["python:2.7-slim", "python:3.5-slim", "python:3.6-slim", "python:3.7-slim", 13 | "python:3.8-slim", "python:3.9-slim", "python:3.10-slim", "python:3.11-slim", 14 | "python:3.12-slim", "python:3.13-slim", "pypy:2.7-slim", "pypy:3.11-slim"] 15 | include: 16 | - check-types: true 17 | - container: "python:2.7-slim" 18 | check-types: false 19 | - container: "python:3.5-slim" 20 | check-types: false 21 | - container: "pypy:2.7-slim" 22 | check-types: false 23 | 24 | runs-on: ubuntu-24.04 25 | container: ${{ matrix.container }} 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | 30 | - name: Upgrade pip 31 | run: python -m pip install --upgrade pip 32 | 33 | - name: Install mypy 34 | if: ${{ matrix.check-types }} 35 | run: python -m pip install mypy 36 | 37 | - name: Check Types 38 | if: ${{ matrix.check-types }} 39 | run: mypy periphery 40 | 41 | - name: Run tests 42 | run: | 43 | python --version 44 | python -m tests.test_gpio 45 | python -m tests.test_gpio_sysfs 46 | python -m tests.test_spi 47 | python -m tests.test_i2c 48 | python -m tests.test_mmio 49 | python -m tests.test_serial 50 | python -m tests.test_led 51 | python -m tests.test_pwm 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | docs/_build/ 4 | build/ 5 | dist/ 6 | python_periphery.egg-info/ 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - requirements: docs/requirements.txt 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | * v2.4.1 - 04/21/2023 2 | * GPIO 3 | * Fix realtime timestamp reporting for line events in gpio-cdev v2 4 | implementation. 5 | 6 | * v2.4.0 - 04/17/2023 7 | * GPIO 8 | * Avoid writing `direction` and `inverted` on open to retain existing 9 | state with sysfs GPIOs 10 | * Add support for gpio-cdev v2 ABI. 11 | * Add type stubs. 12 | * Contributors 13 | * Juho Kim, @AussieSeaweed - ea1ecc2 14 | 15 | * v2.3.0 - 02/14/2021 16 | * GPIO 17 | * Add kernel version check for line bias support. 18 | * Fix docstring for `close()`. 19 | * SPI 20 | * Add kernel version check for 32-bit mode support. 21 | * MMIO 22 | * Fix duplicate transactions in integral read and write methods. 23 | * Fix memory offset of `pointer` property. 24 | * Contributors 25 | * Michael Murton, @CrazyIvan359 - 9c1a4f3 26 | * @paul-demo - b318a6a 27 | 28 | * v2.2.0 - 12/16/2020 29 | * MMIO 30 | * Add `path` keyword argument to constructor for use with alternate 31 | memory character devices (e.g. `/dev/gpiomem`). 32 | * SPI 33 | * Add support for 32-bit flags to `extra_flags` property and 34 | constructor. 35 | 36 | * v2.1.1 - 11/19/2020 37 | * GPIO 38 | * Add direction checks for improved error reporting to `write()`, 39 | `read_event()`, and `poll()` for character device GPIOs. 40 | * Contributors 41 | * Michael Murton, @CrazyIvan359 - 69bd36e 42 | 43 | * v2.1.0 - 05/29/2020 44 | * GPIO 45 | * Add `poll_multiple()` static method. 46 | * Add line consumer `label` property. 47 | * Add line `bias`, line `drive`, and `inverted` properties. 48 | * Add additional properties as keyword arguments to constructor for 49 | character device GPIOs. 50 | * Only unexport GPIO in `close()` if exported in open for sysfs GPIOs. 51 | * Improve wording and fix typos in docstrings. 52 | * Serial 53 | * Fix performance of blocking read in `read()`. 54 | * Raise exception on unexpected empty read in `read()`, which may be 55 | caused by a serial port disconnect. 56 | * Add `vmin` and `vtime` properties for the corresponding termios 57 | settings. 58 | * Add support for termios timeout with `read()`. 59 | * Improve wording in docstrings. 60 | * Contributors 61 | * @xrombik - 444f778 62 | * Alexander Steffen, @webmeister - f0403da 63 | 64 | * v2.0.1 - 01/08/2020 65 | * PWM 66 | * Add retry loop for opening PWM period file after export to 67 | accommodate delayed udev permission rule application. 68 | * Contributors 69 | * Jonas Larsson, @jonasl - 28653d4 70 | 71 | * v2.0.0 - 10/28/2019 72 | * GPIO 73 | * Add support for character device GPIOs. 74 | * Remove support for preserve direction from GPIO constructor. 75 | * Add retry loop to direction write after export to accommodate delayed 76 | udev permission rule application for sysfs GPIOs. 77 | * Unexport GPIO line on close for sysfs GPIOs. 78 | * Fix handling of `timeout=None` with sysfs GPIO `poll()`. 79 | * Add `devpath` property. 80 | * PWM 81 | * Fix chip and channel argument names in PWM constructor and 82 | documentation. 83 | * Add retry loop to PWM open after export to accommodate delayed 84 | creation of sysfs files by kernel driver. 85 | * Unexport PWM channel on close. 86 | * Add nanosecond `period_ns` and `duty_cycle_ns` properties. 87 | * Add `devpath` property. 88 | * LED 89 | * Raise `LookupError` instead of `ValueError` if LED name is not found 90 | during open. 91 | * Add `devpath` property. 92 | * Fix exception handling for Python 2 with `ioctl()` operations in Serial, 93 | SPI, and I2C modules. 94 | * Fix `with` statement context manager support for all modules. 95 | * Update tests with running hints for Raspberry Pi 3. 96 | * Contributors 97 | * Uwe Kleine-König, @ukleinek - 0005260 98 | * Heath Robinson, @ubiquitousthey - ac457d6 99 | 100 | * v1.1.2 - 06/25/2019 101 | * Add LICENSE file to packaging. 102 | 103 | * v1.1.1 - 04/03/2018 104 | * Fix handling of delayed pin directory export when opening a GPIO. 105 | 106 | * v1.1.0 - 10/24/2016 107 | * Add support for preserving pin direction when opening GPIO. 108 | * Improve GPIO poll() implementation to work with more platforms. 109 | * Improve atomicity of MMIO fixed width writes. 110 | * Add PWM module. 111 | * Add LED module. 112 | * Add support for universal wheel packaging. 113 | * Contributors 114 | * Sanket Dasgupta - 8ac7b40 115 | * Joseph Kogut - 022ef29, d2e9132 116 | * Hector Martin - 1e3343a 117 | * Francesco Valla - 34b3877 118 | 119 | * v1.0.0 - 06/25/2015 120 | * Initial release. 121 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2023 vsergeev / Ivan (Vanya) A. Sergeev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 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) 2 | 3 | ## Linux Peripheral I/O (GPIO, LED, PWM, SPI, I2C, MMIO, Serial) with Python 2 & 3 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | Contributed libraries: [java-periphery](https://github.com/sgjava/java-periphery), [dart_periphery](https://github.com/pezi/dart_periphery) 10 | 11 | ## Installation 12 | 13 | With pip: 14 | ``` text 15 | pip install python-periphery 16 | ``` 17 | 18 | With easy_install: 19 | ``` text 20 | easy_install python-periphery 21 | ``` 22 | 23 | With setup.py: 24 | ``` text 25 | git clone https://github.com/vsergeev/python-periphery.git 26 | cd python-periphery 27 | python setup.py install 28 | ``` 29 | 30 | ## Examples 31 | 32 | ### GPIO 33 | 34 | ``` python 35 | from periphery import GPIO 36 | 37 | # Open GPIO /dev/gpiochip0 line 10 with input direction 38 | gpio_in = GPIO("/dev/gpiochip0", 10, "in") 39 | # Open GPIO /dev/gpiochip0 line 12 with output direction 40 | gpio_out = GPIO("/dev/gpiochip0", 12, "out") 41 | 42 | value = gpio_in.read() 43 | gpio_out.write(not value) 44 | 45 | gpio_in.close() 46 | gpio_out.close() 47 | ``` 48 | 49 | [Go to GPIO documentation.](https://python-periphery.readthedocs.io/en/latest/gpio.html) 50 | 51 | ### LED 52 | 53 | ``` python 54 | from periphery import LED 55 | 56 | # Open LED "led0" with initial state off 57 | led0 = LED("led0", False) 58 | # Open LED "led1" with initial state on 59 | led1 = LED("led1", True) 60 | 61 | value = led0.read() 62 | led1.write(value) 63 | 64 | # Set custom brightness level 65 | led1.write(led1.max_brightness / 2) 66 | 67 | led0.close() 68 | led1.close() 69 | ``` 70 | 71 | [Go to LED documentation.](https://python-periphery.readthedocs.io/en/latest/led.html) 72 | 73 | ### PWM 74 | 75 | ``` python 76 | from periphery import PWM 77 | 78 | # Open PWM chip 0, channel 10 79 | pwm = PWM(0, 10) 80 | 81 | # Set frequency to 1 kHz 82 | pwm.frequency = 1e3 83 | # Set duty cycle to 75% 84 | pwm.duty_cycle = 0.75 85 | 86 | pwm.enable() 87 | 88 | # Change duty cycle to 50% 89 | pwm.duty_cycle = 0.50 90 | 91 | pwm.close() 92 | ``` 93 | 94 | [Go to PWM documentation.](https://python-periphery.readthedocs.io/en/latest/pwm.html) 95 | 96 | ### SPI 97 | 98 | ``` python 99 | from periphery import SPI 100 | 101 | # Open spidev1.0 with mode 0 and max speed 1MHz 102 | spi = SPI("/dev/spidev1.0", 0, 1000000) 103 | 104 | data_out = [0xaa, 0xbb, 0xcc, 0xdd] 105 | data_in = spi.transfer(data_out) 106 | 107 | print("shifted out [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_out)) 108 | print("shifted in [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_in)) 109 | 110 | spi.close() 111 | ``` 112 | 113 | [Go to SPI documentation.](https://python-periphery.readthedocs.io/en/latest/spi.html) 114 | 115 | ### I2C 116 | 117 | ``` python 118 | from periphery import I2C 119 | 120 | # Open i2c-0 controller 121 | i2c = I2C("/dev/i2c-0") 122 | 123 | # Read byte at address 0x100 of EEPROM at 0x50 124 | msgs = [I2C.Message([0x01, 0x00]), I2C.Message([0x00], read=True)] 125 | i2c.transfer(0x50, msgs) 126 | print("0x100: 0x{:02x}".format(msgs[1].data[0])) 127 | 128 | i2c.close() 129 | ``` 130 | 131 | [Go to I2C documentation.](https://python-periphery.readthedocs.io/en/latest/i2c.html) 132 | 133 | ### MMIO 134 | 135 | ``` python 136 | from periphery import MMIO 137 | 138 | # Open am335x real-time clock subsystem page 139 | rtc_mmio = MMIO(0x44E3E000, 0x1000) 140 | 141 | # Read current time 142 | rtc_secs = rtc_mmio.read32(0x00) 143 | rtc_mins = rtc_mmio.read32(0x04) 144 | rtc_hrs = rtc_mmio.read32(0x08) 145 | 146 | print("hours: {:02x} minutes: {:02x} seconds: {:02x}".format(rtc_hrs, rtc_mins, rtc_secs)) 147 | 148 | rtc_mmio.close() 149 | 150 | # Open am335x control module page 151 | ctrl_mmio = MMIO(0x44E10000, 0x1000) 152 | 153 | # Read MAC address 154 | mac_id0_lo = ctrl_mmio.read32(0x630) 155 | mac_id0_hi = ctrl_mmio.read32(0x634) 156 | 157 | print("MAC address: {:04x}{:08x}".format(mac_id0_lo, mac_id0_hi)) 158 | 159 | ctrl_mmio.close() 160 | ``` 161 | 162 | [Go to MMIO documentation.](https://python-periphery.readthedocs.io/en/latest/mmio.html) 163 | 164 | ### Serial 165 | 166 | ``` python 167 | from periphery import Serial 168 | 169 | # Open /dev/ttyUSB0 with baudrate 115200, and defaults of 8N1, no flow control 170 | serial = Serial("/dev/ttyUSB0", 115200) 171 | 172 | serial.write(b"Hello World!") 173 | 174 | # Read up to 128 bytes with 500ms timeout 175 | buf = serial.read(128, 0.5) 176 | print("read {:d} bytes: _{:s}_".format(len(buf), buf)) 177 | 178 | serial.close() 179 | ``` 180 | 181 | [Go to Serial documentation.](https://python-periphery.readthedocs.io/en/latest/serial.html) 182 | 183 | ## Documentation 184 | 185 | Documentation is hosted at [https://python-periphery.readthedocs.io](https://python-periphery.readthedocs.io). 186 | 187 | To build documentation locally with Sphinx, run: 188 | 189 | ``` 190 | cd docs 191 | make html 192 | ``` 193 | 194 | Sphinx will produce the HTML documentation in `docs/_build/html/`. 195 | 196 | Run `make help` to see other output targets (LaTeX, man, text, etc.). 197 | 198 | ## Testing 199 | 200 | 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. 201 | 202 | ## License 203 | 204 | python-periphery is MIT licensed. See the included [LICENSE](LICENSE) file. 205 | 206 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Path setup -------------------------------------------------------------- 7 | 8 | # If extensions (or modules to document with autodoc) are in another directory, 9 | # add these directories to sys.path here. If the directory is relative to the 10 | # documentation root, use os.path.abspath to make it absolute, like shown here. 11 | 12 | import os 13 | import sys 14 | sys.path.insert(0, os.path.abspath('..')) 15 | 16 | # -- Project information ----------------------------------------------------- 17 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 18 | 19 | project = u'python-periphery' 20 | copyright = u'2015-2023, vsergeev / Ivan (Vanya) A. Sergeev' 21 | author = u'Vanya A. Sergeev' 22 | release = '2.4.1' 23 | 24 | # -- General configuration --------------------------------------------------- 25 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 26 | 27 | extensions = [ 28 | 'sphinx.ext.autodoc', 29 | 'sphinx.ext.napoleon', 30 | 'sphinx.ext.todo', 31 | 'sphinx.ext.viewcode', 32 | 'sphinx_rtd_theme', 33 | ] 34 | 35 | autoclass_content = 'init' 36 | autodoc_member_order = 'bysource' 37 | 38 | templates_path = ['_templates'] 39 | exclude_patterns = ['_build'] 40 | 41 | pygments_style = 'sphinx' 42 | 43 | # -- Options for HTML output ------------------------------------------------- 44 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 45 | 46 | html_theme = 'sphinx_rtd_theme' 47 | html_static_path = ['_static'] 48 | html_show_sourcelink = False 49 | -------------------------------------------------------------------------------- /docs/gpio.rst: -------------------------------------------------------------------------------- 1 | GPIO 2 | ==== 3 | 4 | Code Example 5 | ------------ 6 | 7 | .. code-block:: python 8 | 9 | from periphery import GPIO 10 | 11 | # Open GPIO /dev/gpiochip0 line 10 with input direction 12 | gpio_in = GPIO("/dev/gpiochip0", 10, "in") 13 | # Open GPIO /dev/gpiochip0 line 12 with output direction 14 | gpio_out = GPIO("/dev/gpiochip0", 12, "out") 15 | 16 | value = gpio_in.read() 17 | gpio_out.write(not value) 18 | 19 | gpio_in.close() 20 | gpio_out.close() 21 | 22 | API 23 | --- 24 | 25 | .. class:: periphery.GPIO(path, line, direction) 26 | :noindex: 27 | 28 | .. autoclass:: periphery.gpio_cdev2.Cdev2GPIO 29 | :noindex: 30 | 31 | .. class:: periphery.GPIO(line, direction) 32 | :noindex: 33 | 34 | .. autoclass:: periphery.SysfsGPIO 35 | :noindex: 36 | 37 | .. autoclass:: periphery.GPIO 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | .. autoclass:: periphery.EdgeEvent 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | .. autoclass:: periphery.GPIOError 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | -------------------------------------------------------------------------------- /docs/i2c.rst: -------------------------------------------------------------------------------- 1 | I2C 2 | === 3 | 4 | Code Example 5 | ------------ 6 | 7 | .. code-block:: python 8 | 9 | from periphery import I2C 10 | 11 | # Open i2c-0 controller 12 | i2c = I2C("/dev/i2c-0") 13 | 14 | # Read byte at address 0x100 of EEPROM at 0x50 15 | msgs = [I2C.Message([0x01, 0x00]), I2C.Message([0x00], read=True)] 16 | i2c.transfer(0x50, msgs) 17 | print("0x100: 0x{:02x}".format(msgs[1].data[0])) 18 | 19 | i2c.close() 20 | 21 | API 22 | --- 23 | 24 | .. autoclass:: periphery.I2C 25 | :members: transfer, close, fd, devpath, Message 26 | :undoc-members: 27 | :show-inheritance: 28 | 29 | .. autoclass:: periphery.I2CError 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. periphery documentation master file, created by 2 | sphinx-quickstart2 on Sat Jun 20 18:30:50 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to python-periphery's documentation! 7 | ============================================ 8 | 9 | python-periphery is a pure Python library for GPIO, LED, PWM, SPI, I2C, MMIO, 10 | and Serial peripheral I/O interface access in userspace Linux. It is useful in 11 | embedded Linux environments (including Raspberry Pi, BeagleBone, etc. 12 | platforms) for interfacing with external peripherals. python-periphery is 13 | compatible with Python 2 and Python 3, is written in pure Python, and is MIT 14 | licensed. 15 | 16 | Contents 17 | -------- 18 | 19 | .. toctree:: 20 | :maxdepth: 1 21 | 22 | gpio 23 | led 24 | pwm 25 | spi 26 | i2c 27 | mmio 28 | serial 29 | version 30 | 31 | -------------------------------------------------------------------------------- /docs/led.rst: -------------------------------------------------------------------------------- 1 | LED 2 | ==== 3 | 4 | Code Example 5 | ------------ 6 | 7 | .. code-block:: python 8 | 9 | from periphery import LED 10 | 11 | # Open LED "led0" with initial state off 12 | led0 = LED("led0", False) 13 | # Open LED "led1" with initial state on 14 | led1 = LED("led1", True) 15 | 16 | value = led0.read() 17 | led1.write(value) 18 | 19 | # Set custom brightness level 20 | led1.write(led1.max_brightness / 2) 21 | 22 | led0.close() 23 | led1.close() 24 | 25 | API 26 | --- 27 | 28 | .. autoclass:: periphery.LED 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | 33 | .. autoclass:: periphery.LEDError 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | -------------------------------------------------------------------------------- /docs/mmio.rst: -------------------------------------------------------------------------------- 1 | MMIO 2 | ==== 3 | 4 | Code Example 5 | ------------ 6 | 7 | .. code-block:: python 8 | 9 | from periphery import MMIO 10 | 11 | # Open am335x real-time clock subsystem page 12 | rtc_mmio = MMIO(0x44E3E000, 0x1000) 13 | 14 | # Read current time 15 | rtc_secs = rtc_mmio.read32(0x00) 16 | rtc_mins = rtc_mmio.read32(0x04) 17 | rtc_hrs = rtc_mmio.read32(0x08) 18 | 19 | print("hours: {:02x} minutes: {:02x} seconds: {:02x}".format(rtc_hrs, rtc_mins, rtc_secs)) 20 | 21 | rtc_mmio.close() 22 | 23 | # Open am335x control module page 24 | ctrl_mmio = MMIO(0x44E10000, 0x1000) 25 | 26 | # Read MAC address 27 | mac_id0_lo = ctrl_mmio.read32(0x630) 28 | mac_id0_hi = ctrl_mmio.read32(0x634) 29 | 30 | print("MAC address: {:04x}{:08x}".format(mac_id0_lo, mac_id0_hi)) 31 | 32 | ctrl_mmio.close() 33 | 34 | API 35 | --- 36 | 37 | .. autoclass:: periphery.MMIO 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | .. autoclass:: periphery.MMIOError 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | -------------------------------------------------------------------------------- /docs/pwm.rst: -------------------------------------------------------------------------------- 1 | PWM 2 | === 3 | 4 | Code Example 5 | ------------ 6 | 7 | .. code-block:: python 8 | 9 | from periphery import PWM 10 | 11 | # Open PWM chip 0, channel 10 12 | pwm = PWM(0, 10) 13 | 14 | # Set frequency to 1 kHz 15 | pwm.frequency = 1e3 16 | # Set duty cycle to 75% 17 | pwm.duty_cycle = 0.75 18 | 19 | pwm.enable() 20 | 21 | # Change duty cycle to 50% 22 | pwm.duty_cycle = 0.50 23 | 24 | pwm.close() 25 | 26 | API 27 | --- 28 | 29 | .. autoclass:: periphery.PWM 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | .. autoclass:: periphery.PWMError 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=2 2 | sphinx-rtd-theme>=1.2.0 3 | -------------------------------------------------------------------------------- /docs/serial.rst: -------------------------------------------------------------------------------- 1 | Serial 2 | ====== 3 | 4 | Code Example 5 | ------------ 6 | 7 | .. code-block:: python 8 | 9 | from periphery import Serial 10 | 11 | # Open /dev/ttyUSB0 with baudrate 115200, and defaults of 8N1, no flow control 12 | serial = Serial("/dev/ttyUSB0", 115200) 13 | 14 | serial.write(b"Hello World!") 15 | 16 | # Read up to 128 bytes with 500ms timeout 17 | buf = serial.read(128, 0.5) 18 | print("read {:d} bytes: _{:s}_".format(len(buf), buf)) 19 | 20 | serial.close() 21 | 22 | API 23 | --- 24 | 25 | .. autoclass:: periphery.Serial 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | .. autoclass:: periphery.SerialError 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | 35 | -------------------------------------------------------------------------------- /docs/spi.rst: -------------------------------------------------------------------------------- 1 | SPI 2 | === 3 | 4 | Code Example 5 | ------------ 6 | 7 | .. code-block:: python 8 | 9 | from periphery import SPI 10 | 11 | # Open spidev1.0 with mode 0 and max speed 1MHz 12 | spi = SPI("/dev/spidev1.0", 0, 1000000) 13 | 14 | data_out = [0xaa, 0xbb, 0xcc, 0xdd] 15 | data_in = spi.transfer(data_out) 16 | 17 | print("shifted out [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_out)) 18 | print("shifted in [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_in)) 19 | 20 | spi.close() 21 | 22 | API 23 | --- 24 | 25 | .. autoclass:: periphery.SPI 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | .. autoclass:: periphery.SPIError 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | 35 | -------------------------------------------------------------------------------- /docs/version.rst: -------------------------------------------------------------------------------- 1 | Version and Helper Functions 2 | ---------------------------- 3 | 4 | .. automodule:: periphery 5 | :members: __version__, version, sleep, sleep_ms, sleep_us 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | -------------------------------------------------------------------------------- /periphery/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | __version__ = "2.4.1" 4 | "Module version string." 5 | 6 | version = (2, 4, 1) 7 | "Module version tuple." 8 | 9 | 10 | def sleep(seconds): 11 | """Sleep for the specified number of seconds. 12 | 13 | Args: 14 | seconds (int, long, float): duration in seconds. 15 | 16 | """ 17 | time.sleep(seconds) 18 | 19 | 20 | def sleep_ms(milliseconds): 21 | """Sleep for the specified number of milliseconds. 22 | 23 | Args: 24 | milliseconds (int, long, float): duration in milliseconds. 25 | 26 | """ 27 | time.sleep(milliseconds / 1000.0) 28 | 29 | 30 | def sleep_us(microseconds): 31 | """Sleep for the specified number of microseconds. 32 | 33 | Args: 34 | microseconds (int, long, float): duration in microseconds. 35 | 36 | """ 37 | time.sleep(microseconds / 1000000.0) 38 | 39 | 40 | from periphery.gpio import GPIO, SysfsGPIO, CdevGPIO, EdgeEvent, GPIOError 41 | from periphery.led import LED, LEDError 42 | from periphery.pwm import PWM, PWMError 43 | from periphery.spi import SPI, SPIError 44 | from periphery.i2c import I2C, I2CError 45 | from periphery.mmio import MMIO, MMIOError 46 | from periphery.serial import Serial, SerialError 47 | -------------------------------------------------------------------------------- /periphery/__init__.pyi: -------------------------------------------------------------------------------- 1 | from periphery.gpio import ( 2 | GPIO as GPIO, 3 | CdevGPIO as CdevGPIO, 4 | EdgeEvent as EdgeEvent, 5 | GPIOError as GPIOError, 6 | SysfsGPIO as SysfsGPIO, 7 | ) 8 | from periphery.i2c import I2C as I2C, I2CError as I2CError 9 | from periphery.led import LED as LED, LEDError as LEDError 10 | from periphery.mmio import MMIO as MMIO, MMIOError as MMIOError 11 | from periphery.pwm import PWM as PWM, PWMError as PWMError 12 | from periphery.serial import Serial as Serial, SerialError as SerialError 13 | from periphery.spi import SPI as SPI, SPIError as SPIError 14 | 15 | version: tuple[int, int, int] 16 | 17 | def sleep(seconds: float) -> None: ... 18 | def sleep_ms(milliseconds: float) -> None: ... 19 | def sleep_us(microseconds: float) -> None: ... 20 | -------------------------------------------------------------------------------- /periphery/gpio.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import os 3 | import select 4 | 5 | 6 | class GPIOError(IOError): 7 | """Base class for GPIO errors.""" 8 | pass 9 | 10 | 11 | class EdgeEvent(collections.namedtuple('EdgeEvent', ['edge', 'timestamp'])): 12 | def __new__(cls, edge, timestamp): 13 | """EdgeEvent containing the event edge and event time reported by Linux. 14 | 15 | Args: 16 | edge (str): event edge, either "rising" or "falling". 17 | timestamp (int): event time in nanoseconds. 18 | """ 19 | return super(EdgeEvent, cls).__new__(cls, edge, timestamp) 20 | 21 | 22 | class GPIO(object): 23 | def __new__(cls, *args, **kwargs): 24 | if len(args) > 2: 25 | return CdevGPIO.__new__(cls, *args, **kwargs) 26 | else: 27 | return SysfsGPIO.__new__(cls, *args, **kwargs) 28 | 29 | def __del__(self): 30 | self.close() 31 | 32 | def __enter__(self): 33 | return self 34 | 35 | def __exit__(self, t, value, traceback): 36 | self.close() 37 | 38 | # Methods 39 | 40 | def read(self): 41 | """Read the state of the GPIO. 42 | 43 | Returns: 44 | bool: ``True`` for high state, ``False`` for low state. 45 | 46 | Raises: 47 | GPIOError: if an I/O or OS error occurs. 48 | 49 | """ 50 | raise NotImplementedError() 51 | 52 | def write(self, value): 53 | """Set the state of the GPIO to `value`. 54 | 55 | Args: 56 | value (bool): ``True`` for high state, ``False`` for low state. 57 | 58 | Raises: 59 | GPIOError: if an I/O or OS error occurs. 60 | TypeError: if `value` type is not bool. 61 | 62 | """ 63 | raise NotImplementedError() 64 | 65 | def poll(self, timeout=None): 66 | """Poll a GPIO for the edge event configured with the .edge property 67 | with an optional timeout. 68 | 69 | For character device GPIOs, the edge event should be consumed with 70 | `read_event()`. For sysfs GPIOs, the edge event should be consumed with 71 | `read()`. 72 | 73 | `timeout` can be a positive number for a timeout in seconds, zero for a 74 | non-blocking poll, or negative or None for a blocking poll. Default is 75 | a blocking poll. 76 | 77 | Args: 78 | timeout (int, float, None): timeout duration in seconds. 79 | 80 | Returns: 81 | bool: ``True`` if an edge event occurred, ``False`` on timeout. 82 | 83 | Raises: 84 | GPIOError: if an I/O or OS error occurs. 85 | TypeError: if `timeout` type is not None or int. 86 | 87 | """ 88 | raise NotImplementedError() 89 | 90 | def read_event(self): 91 | """Read the edge event that occurred with the GPIO. 92 | 93 | This method is intended for use with character device GPIOs and is 94 | unsupported by sysfs GPIOs. 95 | 96 | Returns: 97 | EdgeEvent: a namedtuple containing the string edge event that 98 | occurred (either ``"rising"`` or ``"falling"``), and the event time 99 | reported by Linux in nanoseconds. 100 | 101 | Raises: 102 | GPIOError: if an I/O or OS error occurs. 103 | NotImplementedError: if called on a sysfs GPIO. 104 | 105 | """ 106 | raise NotImplementedError() 107 | 108 | @staticmethod 109 | def poll_multiple(gpios, timeout=None): 110 | """Poll multiple GPIOs for the edge event configured with the .edge 111 | property with an optional timeout. 112 | 113 | For character device GPIOs, the edge event should be consumed with 114 | `read_event()`. For sysfs GPIOs, the edge event should be consumed with 115 | `read()`. 116 | 117 | `timeout` can be a positive number for a timeout in seconds, zero for a 118 | non-blocking poll, or negative or None for a blocking poll. Default is 119 | a blocking poll. 120 | 121 | Args: 122 | gpios (list): list of GPIO objects to poll. 123 | timeout (int, float, None): timeout duration in seconds. 124 | 125 | Returns: 126 | list: list of GPIO objects for which an edge event occurred. 127 | 128 | Raises: 129 | GPIOError: if an I/O or OS error occurs. 130 | TypeError: if `timeout` type is not None or int. 131 | 132 | """ 133 | if not isinstance(timeout, (int, float, type(None))): 134 | raise TypeError("Invalid timeout type, should be integer, float, or None.") 135 | 136 | # Setup poll 137 | p = select.poll() 138 | 139 | # Register GPIO file descriptors and build map of fd to object 140 | fd_gpio_map = {} 141 | for gpio in gpios: 142 | if isinstance(gpio, SysfsGPIO): 143 | p.register(gpio.fd, select.POLLPRI | select.POLLERR) 144 | else: 145 | p.register(gpio.fd, select.POLLIN | select.POLLRDNORM) 146 | 147 | fd_gpio_map[gpio.fd] = gpio 148 | 149 | # Scale timeout to milliseconds 150 | if isinstance(timeout, (int, float)) and timeout > 0: 151 | timeout *= 1000 152 | 153 | # Poll 154 | events = p.poll(timeout) 155 | 156 | # Gather GPIOs that had edge events occur 157 | results = [] 158 | for (fd, _) in events: 159 | gpio = fd_gpio_map[fd] 160 | 161 | results.append(gpio) 162 | 163 | if isinstance(gpio, SysfsGPIO): 164 | # Rewind for read 165 | try: 166 | os.lseek(fd, 0, os.SEEK_SET) 167 | except OSError as e: 168 | raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) 169 | 170 | return results 171 | 172 | def close(self): 173 | """Close the GPIO. 174 | 175 | Raises: 176 | GPIOError: if an I/O or OS error occurs. 177 | 178 | """ 179 | raise NotImplementedError() 180 | 181 | # Immutable properties 182 | 183 | @property 184 | def devpath(self): 185 | """Get the device path of the underlying GPIO device. 186 | 187 | :type: str 188 | """ 189 | raise NotImplementedError() 190 | 191 | @property 192 | def fd(self): 193 | """Get the line file descriptor of the GPIO object. 194 | 195 | :type: int 196 | """ 197 | raise NotImplementedError() 198 | 199 | @property 200 | def line(self): 201 | """Get the GPIO object's line number. 202 | 203 | :type: int 204 | """ 205 | raise NotImplementedError() 206 | 207 | @property 208 | def name(self): 209 | """Get the line name of the GPIO. 210 | 211 | This method is intended for use with character device GPIOs and always 212 | returns the empty string for sysfs GPIOs. 213 | 214 | :type: str 215 | """ 216 | raise NotImplementedError() 217 | 218 | @property 219 | def label(self): 220 | """Get the line consumer label of the GPIO. 221 | 222 | This method is intended for use with character device GPIOs and always 223 | returns the empty string for sysfs GPIOs. 224 | 225 | :type: str 226 | """ 227 | raise NotImplementedError() 228 | 229 | @property 230 | def chip_fd(self): 231 | """Get the GPIO chip file descriptor of the GPIO object. 232 | 233 | This method is intended for use with character device GPIOs and is unsupported by sysfs GPIOs. 234 | 235 | Raises: 236 | NotImplementedError: if accessed on a sysfs GPIO. 237 | 238 | :type: int 239 | """ 240 | raise NotImplementedError() 241 | 242 | @property 243 | def chip_name(self): 244 | """Get the name of the GPIO chip associated with the GPIO. 245 | 246 | :type: str 247 | """ 248 | raise NotImplementedError() 249 | 250 | @property 251 | def chip_label(self): 252 | """Get the label of the GPIO chip associated with the GPIO. 253 | 254 | :type: str 255 | """ 256 | raise NotImplementedError() 257 | 258 | # Mutable properties 259 | 260 | def _get_direction(self): 261 | raise NotImplementedError() 262 | 263 | def _set_direction(self, direction): 264 | raise NotImplementedError() 265 | 266 | direction = property(_get_direction, _set_direction) 267 | """Get or set the GPIO's direction. Can be "in", "out", "high", "low". 268 | 269 | Direction "in" is input; "out" is output, initialized to low; "high" is 270 | output, initialized to high; and "low" is output, initialized to low. 271 | 272 | Raises: 273 | GPIOError: if an I/O or OS error occurs. 274 | TypeError: if `direction` type is not str. 275 | ValueError: if `direction` value is invalid. 276 | 277 | :type: str 278 | """ 279 | 280 | def _get_edge(self): 281 | raise NotImplementedError() 282 | 283 | def _set_edge(self, edge): 284 | raise NotImplementedError() 285 | 286 | edge = property(_get_edge, _set_edge) 287 | """Get or set the GPIO's interrupt edge. Can be "none", "rising", 288 | "falling", "both". 289 | 290 | Raises: 291 | GPIOError: if an I/O or OS error occurs. 292 | TypeError: if `edge` type is not str. 293 | ValueError: if `edge` value is invalid. 294 | 295 | :type: str 296 | """ 297 | 298 | def _get_bias(self): 299 | raise NotImplementedError() 300 | 301 | def _set_bias(self, bias): 302 | raise NotImplementedError() 303 | 304 | bias = property(_get_bias, _set_bias) 305 | """Get or set the GPIO's line bias. Can be "default", "pull_up", 306 | "pull_down", "disable". 307 | 308 | This property is not supported by sysfs GPIOs. 309 | 310 | Raises: 311 | GPIOError: if an I/O or OS error occurs. 312 | TypeError: if `bias` type is not str. 313 | ValueError: if `bias` value is invalid. 314 | 315 | :type: str 316 | """ 317 | 318 | def _get_drive(self): 319 | raise NotImplementedError() 320 | 321 | def _set_drive(self, drive): 322 | raise NotImplementedError() 323 | 324 | drive = property(_get_drive, _set_drive) 325 | """Get or set the GPIO's line drive. Can be "default" (for push-pull), 326 | "open_drain", "open_source". 327 | 328 | This property is not supported by sysfs GPIOs. 329 | 330 | Raises: 331 | GPIOError: if an I/O or OS error occurs. 332 | TypeError: if `drive` type is not str. 333 | ValueError: if `drive` value is invalid. 334 | 335 | :type: str 336 | """ 337 | 338 | def _get_inverted(self): 339 | raise NotImplementedError() 340 | 341 | def _set_inverted(self, inverted): 342 | raise NotImplementedError() 343 | 344 | inverted = property(_get_inverted, _set_inverted) 345 | """Get or set the GPIO's inverted (active low) property. 346 | 347 | Raises: 348 | GPIOError: if an I/O or OS error occurs. 349 | TypeError: if `inverted` type is not bool. 350 | 351 | :type: bool 352 | """ 353 | 354 | # String representation 355 | 356 | def __str__(self): 357 | """Get the string representation of the GPIO. 358 | 359 | :type: str 360 | """ 361 | raise NotImplementedError() 362 | 363 | 364 | # Assign GPIO classes 365 | from . import gpio_cdev1 366 | from . import gpio_cdev2 367 | from . import gpio_sysfs 368 | 369 | CdevGPIO = gpio_cdev2.Cdev2GPIO if gpio_cdev2.Cdev2GPIO.SUPPORTED else gpio_cdev1.Cdev1GPIO 370 | SysfsGPIO = gpio_sysfs.SysfsGPIO 371 | -------------------------------------------------------------------------------- /periphery/gpio.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | from typing import Any 3 | 4 | KERNEL_VERSION: tuple[int, int] 5 | 6 | class GPIOError(IOError): ... 7 | 8 | class EdgeEvent: 9 | def __new__(cls, edge: str, timestamp: int) -> EdgeEvent: ... # noqa: Y034 10 | 11 | class GPIO: 12 | def __new__(cls, *args: Any, **kwargs: Any) -> GPIO: ... # noqa: Y034 13 | def __del__(self) -> None: ... 14 | def __enter__(self) -> GPIO: ... # noqa: Y034 15 | def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... 16 | def read(self) -> bool: ... 17 | def write(self, value: bool) -> None: ... 18 | def poll(self, timeout: float | None = ...) -> bool: ... 19 | def read_event(self) -> EdgeEvent: ... 20 | @staticmethod 21 | def poll_multiple(gpios: list[GPIO], timeout: float | None = ...) -> list[GPIO]: ... 22 | def close(self) -> None: ... 23 | @property 24 | def devpath(self) -> str: ... 25 | @property 26 | def fd(self) -> int: ... 27 | @property 28 | def line(self) -> int: ... 29 | @property 30 | def name(self) -> str: ... 31 | @property 32 | def label(self) -> str: ... 33 | @property 34 | def chip_fd(self) -> int: ... 35 | @property 36 | def chip_name(self) -> str: ... 37 | @property 38 | def chip_label(self) -> str: ... 39 | direction: property 40 | edge: property 41 | bias: property 42 | drive: property 43 | inverted: property 44 | 45 | class CdevGPIO(GPIO): 46 | def __init__( # pyright: ignore [reportInconsistentConstructor] 47 | self, 48 | path: str, 49 | line: int | str, 50 | direction: str, 51 | edge: str = ..., 52 | bias: str = ..., 53 | drive: str = ..., 54 | inverted: bool = ..., 55 | label: str | None = ..., 56 | ) -> None: ... 57 | def __new__(self, path: str, line: int | str, direction: str, **kwargs: Any) -> CdevGPIO: ... # noqa: Y034 58 | 59 | class SysfsGPIO(GPIO): 60 | def __init__(self, line: int, direction: str) -> None: ... 61 | def __new__(self, line: int, direction: str) -> SysfsGPIO: ... # noqa: Y034 62 | -------------------------------------------------------------------------------- /periphery/gpio_cdev1.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import ctypes 3 | import fcntl 4 | import os 5 | import select 6 | 7 | from .gpio import GPIO, GPIOError, EdgeEvent 8 | 9 | 10 | try: 11 | KERNEL_VERSION = tuple([int(s) for s in platform.release().split(".")[:2]]) 12 | except ValueError: 13 | KERNEL_VERSION = (0, 0) 14 | 15 | 16 | _GPIO_NAME_MAX_SIZE = 32 17 | _GPIOHANDLES_MAX = 64 18 | 19 | 20 | class _CGpiochipInfo(ctypes.Structure): 21 | _fields_ = [ 22 | ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 23 | ('label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 24 | ('lines', ctypes.c_uint32), 25 | ] 26 | 27 | 28 | class _CGpiolineInfo(ctypes.Structure): 29 | _fields_ = [ 30 | ('line_offset', ctypes.c_uint32), 31 | ('flags', ctypes.c_uint32), 32 | ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 33 | ('consumer', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 34 | ] 35 | 36 | 37 | class _CGpiohandleRequest(ctypes.Structure): 38 | _fields_ = [ 39 | ('lineoffsets', ctypes.c_uint32 * _GPIOHANDLES_MAX), 40 | ('flags', ctypes.c_uint32), 41 | ('default_values', ctypes.c_uint8 * _GPIOHANDLES_MAX), 42 | ('consumer_label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 43 | ('lines', ctypes.c_uint32), 44 | ('fd', ctypes.c_int), 45 | ] 46 | 47 | 48 | class _CGpiohandleData(ctypes.Structure): 49 | _fields_ = [ 50 | ('values', ctypes.c_uint8 * _GPIOHANDLES_MAX), 51 | ] 52 | 53 | 54 | class _CGpioeventRequest(ctypes.Structure): 55 | _fields_ = [ 56 | ('lineoffset', ctypes.c_uint32), 57 | ('handleflags', ctypes.c_uint32), 58 | ('eventflags', ctypes.c_uint32), 59 | ('consumer_label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 60 | ('fd', ctypes.c_int), 61 | ] 62 | 63 | 64 | class _CGpioeventData(ctypes.Structure): 65 | _fields_ = [ 66 | ('timestamp', ctypes.c_uint64), 67 | ('id', ctypes.c_uint32), 68 | ] 69 | 70 | 71 | class Cdev1GPIO(GPIO): 72 | # Constants scraped from 73 | _GPIOHANDLE_GET_LINE_VALUES_IOCTL = 0xc040b408 74 | _GPIOHANDLE_SET_LINE_VALUES_IOCTL = 0xc040b409 75 | _GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 76 | _GPIO_GET_LINEINFO_IOCTL = 0xc048b402 77 | _GPIO_GET_LINEHANDLE_IOCTL = 0xc16cb403 78 | _GPIO_GET_LINEEVENT_IOCTL = 0xc030b404 79 | _GPIOHANDLE_REQUEST_INPUT = 0x1 80 | _GPIOHANDLE_REQUEST_OUTPUT = 0x2 81 | _GPIOHANDLE_REQUEST_ACTIVE_LOW = 0x4 82 | _GPIOHANDLE_REQUEST_OPEN_DRAIN = 0x8 83 | _GPIOHANDLE_REQUEST_OPEN_SOURCE = 0x10 84 | _GPIOHANDLE_REQUEST_BIAS_PULL_UP = 0x20 85 | _GPIOHANDLE_REQUEST_BIAS_PULL_DOWN = 0x40 86 | _GPIOHANDLE_REQUEST_BIAS_DISABLE = 0x80 87 | _GPIOEVENT_REQUEST_RISING_EDGE = 0x1 88 | _GPIOEVENT_REQUEST_FALLING_EDGE = 0x2 89 | _GPIOEVENT_REQUEST_BOTH_EDGES = 0x3 90 | _GPIOEVENT_EVENT_RISING_EDGE = 0x1 91 | _GPIOEVENT_EVENT_FALLING_EDGE = 0x2 92 | 93 | _SUPPORTS_LINE_BIAS = KERNEL_VERSION >= (5, 5) 94 | 95 | def __init__(self, path, line, direction, edge="none", bias="default", drive="default", inverted=False, label=None): 96 | """**Character device GPIO (ABI version 1)** 97 | 98 | Instantiate a GPIO object and open the character device GPIO with the 99 | specified line and direction at the specified GPIO chip path (e.g. 100 | "/dev/gpiochip0"). Defaults properties can be overridden with keyword 101 | arguments. 102 | 103 | Args: 104 | path (str): GPIO chip character device path. 105 | line (int, str): GPIO line number or name. 106 | direction (str): GPIO direction, can be "in", "out", "high", or 107 | "low". 108 | edge (str): GPIO interrupt edge, can be "none", "rising", 109 | "falling", or "both". 110 | bias (str): GPIO line bias, can be "default", "pull_up", 111 | "pull_down", or "disable". 112 | drive (str): GPIO line drive, can be "default", "open_drain", or 113 | "open_source". 114 | inverted (bool): GPIO is inverted (active low). 115 | label (str, None): GPIO line consumer label. 116 | 117 | Returns: 118 | Cdev1GPIO: GPIO object. 119 | 120 | Raises: 121 | GPIOError: if an I/O or OS error occurs. 122 | TypeError: if `path`, `line`, `direction`, `edge`, `bias`, `drive`, 123 | `inverted`, or `label` types are invalid. 124 | ValueError: if `direction`, `edge`, `bias`, or `drive` value is 125 | invalid. 126 | LookupError: if the GPIO line was not found by the provided name. 127 | 128 | """ 129 | self._devpath = None 130 | self._line = None 131 | self._line_fd = None 132 | self._chip_fd = None 133 | self._direction = None 134 | self._edge = None 135 | self._bias = None 136 | self._drive = None 137 | self._inverted = None 138 | self._label = None 139 | 140 | self._open(path, line, direction, edge, bias, drive, inverted, label) 141 | 142 | def __new__(self, path, line, direction, **kwargs): 143 | return object.__new__(Cdev1GPIO) 144 | 145 | def _open(self, path, line, direction, edge, bias, drive, inverted, label): 146 | if not isinstance(path, str): 147 | raise TypeError("Invalid path type, should be string.") 148 | 149 | if not isinstance(line, (int, str)): 150 | raise TypeError("Invalid line type, should be integer or string.") 151 | 152 | if not isinstance(direction, str): 153 | raise TypeError("Invalid direction type, should be string.") 154 | elif direction not in ["in", "out", "high", "low"]: 155 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 156 | 157 | if not isinstance(edge, str): 158 | raise TypeError("Invalid edge type, should be string.") 159 | elif edge not in ["none", "rising", "falling", "both"]: 160 | raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") 161 | 162 | if not isinstance(bias, str): 163 | raise TypeError("Invalid bias type, should be string.") 164 | elif bias not in ["default", "pull_up", "pull_down", "disable"]: 165 | raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") 166 | 167 | if not isinstance(drive, str): 168 | raise TypeError("Invalid drive type, should be string.") 169 | elif drive not in ["default", "open_drain", "open_source"]: 170 | raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") 171 | 172 | if not isinstance(inverted, bool): 173 | raise TypeError("Invalid drive type, should be bool.") 174 | 175 | if not isinstance(label, (type(None), str)): 176 | raise TypeError("Invalid label type, should be None or str.") 177 | 178 | if isinstance(line, str): 179 | line = self._find_line_by_name(path, line) 180 | 181 | # Open GPIO chip 182 | try: 183 | self._chip_fd = os.open(path, 0) 184 | except OSError as e: 185 | raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) 186 | 187 | self._devpath = path 188 | self._line = line 189 | self._label = label.encode() if label is not None else b"periphery" 190 | 191 | self._reopen(direction, edge, bias, drive, inverted) 192 | 193 | def _reopen(self, direction, edge, bias, drive, inverted): 194 | flags = 0 195 | 196 | if bias != "default" and not Cdev1GPIO._SUPPORTS_LINE_BIAS: 197 | raise GPIOError(None, "Line bias configuration not supported by kernel version {}.{}.".format(*KERNEL_VERSION)) 198 | elif bias == "pull_up": 199 | flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_BIAS_PULL_UP 200 | elif bias == "pull_down": 201 | flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_BIAS_PULL_DOWN 202 | elif bias == "disable": 203 | flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_BIAS_DISABLE 204 | 205 | if drive == "open_drain": 206 | flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_OPEN_DRAIN 207 | elif drive == "open_source": 208 | flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_OPEN_SOURCE 209 | 210 | if inverted: 211 | flags |= Cdev1GPIO._GPIOHANDLE_REQUEST_ACTIVE_LOW 212 | 213 | # FIXME this should really use GPIOHANDLE_SET_CONFIG_IOCTL instead of 214 | # closing and reopening, especially to preserve output value on 215 | # configuration changes 216 | 217 | # Close existing line 218 | if self._line_fd is not None: 219 | try: 220 | os.close(self._line_fd) 221 | except OSError as e: 222 | raise GPIOError(e.errno, "Closing existing GPIO line: " + e.strerror) 223 | 224 | self._line_fd = None 225 | 226 | if direction == "in": 227 | if edge == "none": 228 | request = _CGpiohandleRequest() 229 | 230 | request.lineoffsets[0] = self._line 231 | request.flags = flags | Cdev1GPIO._GPIOHANDLE_REQUEST_INPUT 232 | request.consumer_label = self._label 233 | request.lines = 1 234 | 235 | try: 236 | fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEHANDLE_IOCTL, request) 237 | except (OSError, IOError) as e: 238 | raise GPIOError(e.errno, "Opening input line handle: " + e.strerror) 239 | 240 | self._line_fd = request.fd 241 | else: 242 | request = _CGpioeventRequest() 243 | 244 | request.lineoffset = self._line 245 | request.handleflags = flags | Cdev1GPIO._GPIOHANDLE_REQUEST_INPUT 246 | 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 247 | request.consumer_label = self._label 248 | 249 | try: 250 | fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEEVENT_IOCTL, request) 251 | except (OSError, IOError) as e: 252 | raise GPIOError(e.errno, "Opening input line event handle: " + e.strerror) 253 | 254 | self._line_fd = request.fd 255 | else: 256 | request = _CGpiohandleRequest() 257 | initial_value = True if direction == "high" else False 258 | initial_value ^= inverted 259 | 260 | request.lineoffsets[0] = self._line 261 | request.flags = flags | Cdev1GPIO._GPIOHANDLE_REQUEST_OUTPUT 262 | request.default_values[0] = initial_value 263 | request.consumer_label = self._label 264 | request.lines = 1 265 | 266 | try: 267 | fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEHANDLE_IOCTL, request) 268 | except (OSError, IOError) as e: 269 | raise GPIOError(e.errno, "Opening output line handle: " + e.strerror) 270 | 271 | self._line_fd = request.fd 272 | 273 | self._direction = "in" if direction == "in" else "out" 274 | self._edge = edge 275 | self._bias = bias 276 | self._drive = drive 277 | self._inverted = inverted 278 | 279 | def _find_line_by_name(self, path, line): 280 | # Open GPIO chip 281 | try: 282 | fd = os.open(path, 0) 283 | except OSError as e: 284 | raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) 285 | 286 | # Get chip info for number of lines 287 | chip_info = _CGpiochipInfo() 288 | try: 289 | fcntl.ioctl(fd, Cdev1GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 290 | except (OSError, IOError) as e: 291 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 292 | 293 | # Get each line info 294 | line_info = _CGpiolineInfo() 295 | found = False 296 | for i in range(chip_info.lines): 297 | line_info.line_offset = i 298 | try: 299 | fcntl.ioctl(fd, Cdev1GPIO._GPIO_GET_LINEINFO_IOCTL, line_info) 300 | except (OSError, IOError) as e: 301 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 302 | 303 | if line_info.name.decode() == line: 304 | found = True 305 | break 306 | 307 | try: 308 | os.close(fd) 309 | except OSError as e: 310 | raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) 311 | 312 | if found: 313 | return i 314 | 315 | raise LookupError("Opening GPIO line: GPIO line \"{:s}\" not found by name.".format(line)) 316 | 317 | # Methods 318 | 319 | def read(self): 320 | data = _CGpiohandleData() 321 | 322 | try: 323 | fcntl.ioctl(self._line_fd, Cdev1GPIO._GPIOHANDLE_GET_LINE_VALUES_IOCTL, data) 324 | except (OSError, IOError) as e: 325 | raise GPIOError(e.errno, "Getting line value: " + e.strerror) 326 | 327 | return bool(data.values[0]) 328 | 329 | def write(self, value): 330 | if not isinstance(value, bool): 331 | raise TypeError("Invalid value type, should be bool.") 332 | elif self._direction != "out": 333 | raise GPIOError(None, "Invalid operation: cannot write to input GPIO") 334 | 335 | data = _CGpiohandleData() 336 | 337 | data.values[0] = value 338 | 339 | try: 340 | fcntl.ioctl(self._line_fd, Cdev1GPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data) 341 | except (OSError, IOError) as e: 342 | raise GPIOError(e.errno, "Setting line value: " + e.strerror) 343 | 344 | def poll(self, timeout=None): 345 | if not isinstance(timeout, (int, float, type(None))): 346 | raise TypeError("Invalid timeout type, should be integer, float, or None.") 347 | elif self._direction != "in": 348 | raise GPIOError(None, "Invalid operation: cannot poll output GPIO") 349 | 350 | # Setup poll 351 | p = select.poll() 352 | p.register(self._line_fd, select.POLLIN | select.POLLPRI | select.POLLERR) 353 | 354 | # Scale timeout to milliseconds 355 | if isinstance(timeout, (int, float)) and timeout > 0: 356 | timeout *= 1000 357 | 358 | # Poll 359 | events = p.poll(timeout) 360 | 361 | return len(events) > 0 362 | 363 | def read_event(self): 364 | if self._direction != "in": 365 | raise GPIOError(None, "Invalid operation: cannot read event of output GPIO") 366 | elif self._edge == "none": 367 | raise GPIOError(None, "Invalid operation: GPIO edge not set") 368 | 369 | try: 370 | buf = os.read(self._line_fd, ctypes.sizeof(_CGpioeventData)) 371 | except OSError as e: 372 | raise GPIOError(e.errno, "Reading GPIO event: " + e.strerror) 373 | 374 | event_data = _CGpioeventData.from_buffer_copy(buf) 375 | 376 | if event_data.id == Cdev1GPIO._GPIOEVENT_EVENT_RISING_EDGE: 377 | edge = "rising" 378 | elif event_data.id == Cdev1GPIO._GPIOEVENT_EVENT_FALLING_EDGE: 379 | edge = "falling" 380 | else: 381 | edge = "none" 382 | 383 | timestamp = event_data.timestamp 384 | 385 | return EdgeEvent(edge, timestamp) 386 | 387 | def close(self): 388 | try: 389 | if self._line_fd is not None: 390 | os.close(self._line_fd) 391 | except OSError as e: 392 | raise GPIOError(e.errno, "Closing GPIO line: " + e.strerror) 393 | 394 | try: 395 | if self._chip_fd is not None: 396 | os.close(self._chip_fd) 397 | except OSError as e: 398 | raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) 399 | 400 | self._line_fd = None 401 | self._chip_fd = None 402 | self._edge = "none" 403 | self._direction = "in" 404 | self._line = None 405 | 406 | # Immutable properties 407 | 408 | @property 409 | def devpath(self): 410 | return self._devpath 411 | 412 | @property 413 | def fd(self): 414 | return self._line_fd 415 | 416 | @property 417 | def line(self): 418 | return self._line 419 | 420 | @property 421 | def name(self): 422 | line_info = _CGpiolineInfo() 423 | line_info.line_offset = self._line 424 | 425 | try: 426 | fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEINFO_IOCTL, line_info) 427 | except (OSError, IOError) as e: 428 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 429 | 430 | return line_info.name.decode() 431 | 432 | @property 433 | def label(self): 434 | line_info = _CGpiolineInfo() 435 | line_info.line_offset = self._line 436 | 437 | try: 438 | fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_LINEINFO_IOCTL, line_info) 439 | except (OSError, IOError) as e: 440 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 441 | 442 | return line_info.consumer.decode() 443 | 444 | @property 445 | def chip_fd(self): 446 | return self._chip_fd 447 | 448 | @property 449 | def chip_name(self): 450 | chip_info = _CGpiochipInfo() 451 | 452 | try: 453 | fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 454 | except (OSError, IOError) as e: 455 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 456 | 457 | return chip_info.name.decode() 458 | 459 | @property 460 | def chip_label(self): 461 | chip_info = _CGpiochipInfo() 462 | 463 | try: 464 | fcntl.ioctl(self._chip_fd, Cdev1GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 465 | except (OSError, IOError) as e: 466 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 467 | 468 | return chip_info.label.decode() 469 | 470 | # Mutable properties 471 | 472 | def _get_direction(self): 473 | return self._direction 474 | 475 | def _set_direction(self, direction): 476 | if not isinstance(direction, str): 477 | raise TypeError("Invalid direction type, should be string.") 478 | if direction not in ["in", "out", "high", "low"]: 479 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 480 | 481 | if self._direction == direction: 482 | return 483 | 484 | self._reopen(direction, "none", self._bias, self._drive, self._inverted) 485 | 486 | direction = property(_get_direction, _set_direction) 487 | 488 | def _get_edge(self): 489 | return self._edge 490 | 491 | def _set_edge(self, edge): 492 | if not isinstance(edge, str): 493 | raise TypeError("Invalid edge type, should be string.") 494 | if edge not in ["none", "rising", "falling", "both"]: 495 | raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") 496 | 497 | if self._direction != "in": 498 | raise GPIOError(None, "Invalid operation: cannot set edge on output GPIO") 499 | 500 | if self._edge == edge: 501 | return 502 | 503 | self._reopen(self._direction, edge, self._bias, self._drive, self._inverted) 504 | 505 | edge = property(_get_edge, _set_edge) 506 | 507 | def _get_bias(self): 508 | return self._bias 509 | 510 | def _set_bias(self, bias): 511 | if not isinstance(bias, str): 512 | raise TypeError("Invalid bias type, should be string.") 513 | if bias not in ["default", "pull_up", "pull_down", "disable"]: 514 | raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") 515 | 516 | if self._bias == bias: 517 | return 518 | 519 | self._reopen(self._direction, self._edge, bias, self._drive, self._inverted) 520 | 521 | bias = property(_get_bias, _set_bias) 522 | 523 | def _get_drive(self): 524 | return self._drive 525 | 526 | def _set_drive(self, drive): 527 | if not isinstance(drive, str): 528 | raise TypeError("Invalid drive type, should be string.") 529 | if drive not in ["default", "open_drain", "open_source"]: 530 | raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") 531 | 532 | if self._direction != "out" and drive != "default": 533 | raise GPIOError(None, "Invalid operation: cannot set line drive on input GPIO") 534 | 535 | if self._drive == drive: 536 | return 537 | 538 | self._reopen(self._direction, self._edge, self._bias, drive, self._inverted) 539 | 540 | drive = property(_get_drive, _set_drive) 541 | 542 | def _get_inverted(self): 543 | return self._inverted 544 | 545 | def _set_inverted(self, inverted): 546 | if not isinstance(inverted, bool): 547 | raise TypeError("Invalid drive type, should be bool.") 548 | 549 | if self._inverted == inverted: 550 | return 551 | 552 | self._reopen(self._direction, self._edge, self._bias, self._drive, inverted) 553 | 554 | inverted = property(_get_inverted, _set_inverted) 555 | 556 | # String representation 557 | 558 | def __str__(self): 559 | try: 560 | str_name = self.name 561 | except GPIOError: 562 | str_name = "" 563 | 564 | try: 565 | str_label = self.label 566 | except GPIOError: 567 | str_label = "" 568 | 569 | try: 570 | str_direction = self.direction 571 | except GPIOError: 572 | str_direction = "" 573 | 574 | try: 575 | str_edge = self.edge 576 | except GPIOError: 577 | str_edge = "" 578 | 579 | try: 580 | str_bias = self.bias 581 | except GPIOError: 582 | str_bias = "" 583 | 584 | try: 585 | str_drive = self.drive 586 | except GPIOError: 587 | str_drive = "" 588 | 589 | try: 590 | str_inverted = str(self.inverted) 591 | except GPIOError: 592 | str_inverted = "" 593 | 594 | try: 595 | str_chip_name = self.chip_name 596 | except GPIOError: 597 | str_chip_name = "" 598 | 599 | try: 600 | str_chip_label = self.chip_label 601 | except GPIOError: 602 | str_chip_label = "" 603 | 604 | 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)" \ 605 | .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) 606 | -------------------------------------------------------------------------------- /periphery/gpio_cdev2.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import ctypes 3 | import fcntl 4 | import os 5 | import select 6 | 7 | from .gpio import GPIO, GPIOError, EdgeEvent 8 | 9 | 10 | try: 11 | KERNEL_VERSION = tuple([int(s) for s in platform.release().split(".")[:2]]) 12 | except ValueError: 13 | KERNEL_VERSION = (0, 0) 14 | 15 | 16 | _GPIO_NAME_MAX_SIZE = 32 17 | _GPIO_V2_LINES_MAX = 64 18 | _GPIO_V2_LINE_NUM_ATTRS_MAX = 10 19 | 20 | 21 | class _CGpiochipInfo(ctypes.Structure): 22 | _fields_ = [ 23 | ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 24 | ('label', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 25 | ('lines', ctypes.c_uint32), 26 | ] 27 | 28 | 29 | class _CGpioV2LineValues(ctypes.Structure): 30 | _fields_ = [ 31 | ('bits', ctypes.c_uint64), 32 | ('mask', ctypes.c_uint64), 33 | ] 34 | 35 | 36 | class _CGpioV2LineAttributeData(ctypes.Union): 37 | _fields_ = [ 38 | ('flags', ctypes.c_uint64), 39 | ('values', ctypes.c_uint64), 40 | ('debounce_period_us', ctypes.c_uint32), 41 | ] 42 | 43 | 44 | class _CGpioV2LineAttribute(ctypes.Structure): 45 | _fields_ = [ 46 | ('id', ctypes.c_uint32), 47 | ('padding', ctypes.c_uint32), 48 | ('data', _CGpioV2LineAttributeData), 49 | ] 50 | 51 | 52 | class _CGpioV2LineConfigAttribute(ctypes.Structure): 53 | _fields_ = [ 54 | ('attr', _CGpioV2LineAttribute), 55 | ('mask', ctypes.c_uint64), 56 | ] 57 | 58 | 59 | class _CGpioV2LineConfig(ctypes.Structure): 60 | _fields_ = [ 61 | ('flags', ctypes.c_uint64), 62 | ('num_attrs', ctypes.c_uint32), 63 | ('padding', ctypes.c_uint32 * 5), 64 | ('attrs', _CGpioV2LineConfigAttribute * _GPIO_V2_LINE_NUM_ATTRS_MAX), 65 | ] 66 | 67 | 68 | class _CGpioV2LineRequest(ctypes.Structure): 69 | _fields_ = [ 70 | ('offsets', ctypes.c_uint32 * _GPIO_V2_LINES_MAX), 71 | ('consumer', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 72 | ('config', _CGpioV2LineConfig), 73 | ('num_lines', ctypes.c_uint32), 74 | ('event_buffer_size', ctypes.c_uint32), 75 | ('padding', ctypes.c_uint32 * 5), 76 | ('fd', ctypes.c_int32), 77 | ] 78 | 79 | 80 | class _CGpioV2LineInfo(ctypes.Structure): 81 | _fields_ = [ 82 | ('name', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 83 | ('consumer', ctypes.c_char * _GPIO_NAME_MAX_SIZE), 84 | ('offset', ctypes.c_uint32), 85 | ('num_attrs', ctypes.c_uint32), 86 | ('flags', ctypes.c_uint64), 87 | ('attrs', _CGpioV2LineAttribute * _GPIO_V2_LINE_NUM_ATTRS_MAX), 88 | ('padding', ctypes.c_uint32 * 4), 89 | ] 90 | 91 | 92 | class _CGpioV2LineEvent(ctypes.Structure): 93 | _fields_ = [ 94 | ('timestamp_ns', ctypes.c_uint64), 95 | ('id', ctypes.c_uint32), 96 | ('offset', ctypes.c_uint32), 97 | ('seqno', ctypes.c_uint32), 98 | ('line_seqno', ctypes.c_uint32), 99 | ('padding', ctypes.c_uint32 * 6), 100 | ] 101 | 102 | 103 | class Cdev2GPIO(GPIO): 104 | # Constants scraped from 105 | _GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 106 | _GPIO_V2_GET_LINEINFO_IOCTL = 0xc100b405 107 | _GPIO_V2_GET_LINE_IOCTL = 0xc250b407 108 | _GPIO_V2_LINE_GET_VALUES_IOCTL = 0xc010b40e 109 | _GPIO_V2_LINE_SET_CONFIG_IOCTL = 0xc110b40d 110 | _GPIO_V2_LINE_SET_VALUES_IOCTL = 0xc010b40f 111 | _GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES = 0x2 112 | _GPIO_V2_LINE_EVENT_RISING_EDGE = 0x1 113 | _GPIO_V2_LINE_EVENT_FALLING_EDGE = 0x2 114 | _GPIO_V2_LINE_FLAG_ACTIVE_LOW = 0x2 115 | _GPIO_V2_LINE_FLAG_INPUT = 0x4 116 | _GPIO_V2_LINE_FLAG_OUTPUT = 0x8 117 | _GPIO_V2_LINE_FLAG_EDGE_RISING = 0x10 118 | _GPIO_V2_LINE_FLAG_EDGE_FALLING = 0x20 119 | _GPIO_V2_LINE_FLAG_OPEN_DRAIN = 0x40 120 | _GPIO_V2_LINE_FLAG_OPEN_SOURCE = 0x80 121 | _GPIO_V2_LINE_FLAG_BIAS_PULL_UP = 0x100 122 | _GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN = 0x200 123 | _GPIO_V2_LINE_FLAG_BIAS_DISABLED = 0x400 124 | _GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME = 0x800 125 | 126 | SUPPORTED = KERNEL_VERSION >= (5, 10) 127 | 128 | def __init__(self, path, line, direction, edge="none", bias="default", drive="default", inverted=False, label=None): 129 | """**Character device GPIO (ABI version 2)** 130 | 131 | Instantiate a GPIO object and open the character device GPIO with the 132 | specified line and direction at the specified GPIO chip path (e.g. 133 | "/dev/gpiochip0"). Defaults properties can be overridden with keyword 134 | arguments. 135 | 136 | Args: 137 | path (str): GPIO chip character device path. 138 | line (int, str): GPIO line number or name. 139 | direction (str): GPIO direction, can be "in", "out", "high", or 140 | "low". 141 | edge (str): GPIO interrupt edge, can be "none", "rising", 142 | "falling", or "both". 143 | bias (str): GPIO line bias, can be "default", "pull_up", 144 | "pull_down", or "disable". 145 | drive (str): GPIO line drive, can be "default", "open_drain", or 146 | "open_source". 147 | inverted (bool): GPIO is inverted (active low). 148 | label (str, None): GPIO line consumer label. 149 | 150 | Returns: 151 | Cdev2GPIO: GPIO object. 152 | 153 | Raises: 154 | GPIOError: if an I/O or OS error occurs. 155 | TypeError: if `path`, `line`, `direction`, `edge`, `bias`, `drive`, 156 | `inverted`, or `label` types are invalid. 157 | ValueError: if `direction`, `edge`, `bias`, or `drive` value is 158 | invalid. 159 | LookupError: if the GPIO line was not found by the provided name. 160 | 161 | """ 162 | self._devpath = None 163 | self._line = None 164 | self._line_fd = None 165 | self._chip_fd = None 166 | self._direction = None 167 | self._edge = None 168 | self._bias = None 169 | self._drive = None 170 | self._inverted = None 171 | self._label = None 172 | 173 | self._open(path, line, direction, edge, bias, drive, inverted, label) 174 | 175 | def __new__(self, path, line, direction, **kwargs): 176 | return object.__new__(Cdev2GPIO) 177 | 178 | def _open(self, path, line, direction, edge, bias, drive, inverted, label): 179 | if not isinstance(path, str): 180 | raise TypeError("Invalid path type, should be string.") 181 | 182 | if not isinstance(line, (int, str)): 183 | raise TypeError("Invalid line type, should be integer or string.") 184 | 185 | if not isinstance(direction, str): 186 | raise TypeError("Invalid direction type, should be string.") 187 | elif direction not in ["in", "out", "high", "low"]: 188 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 189 | 190 | if not isinstance(edge, str): 191 | raise TypeError("Invalid edge type, should be string.") 192 | elif edge not in ["none", "rising", "falling", "both"]: 193 | raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") 194 | 195 | if not isinstance(bias, str): 196 | raise TypeError("Invalid bias type, should be string.") 197 | elif bias not in ["default", "pull_up", "pull_down", "disable"]: 198 | raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") 199 | 200 | if not isinstance(drive, str): 201 | raise TypeError("Invalid drive type, should be string.") 202 | elif drive not in ["default", "open_drain", "open_source"]: 203 | raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") 204 | 205 | if not isinstance(inverted, bool): 206 | raise TypeError("Invalid drive type, should be bool.") 207 | 208 | if not isinstance(label, (type(None), str)): 209 | raise TypeError("Invalid label type, should be None or str.") 210 | 211 | if isinstance(line, str): 212 | line = self._find_line_by_name(path, line) 213 | 214 | # Open GPIO chip 215 | try: 216 | self._chip_fd = os.open(path, 0) 217 | except OSError as e: 218 | raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) 219 | 220 | self._devpath = path 221 | self._line = line 222 | self._label = label.encode() if label is not None else b"periphery" 223 | 224 | self._reopen(direction, edge, bias, drive, inverted) 225 | 226 | def _reopen(self, direction, edge, bias, drive, inverted): 227 | flags = 0 228 | 229 | if bias == "pull_up": 230 | flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_BIAS_PULL_UP 231 | elif bias == "pull_down": 232 | flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_BIAS_PULL_DOWN 233 | elif bias == "disable": 234 | flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_BIAS_DISABLED 235 | 236 | if drive == "open_drain": 237 | flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_OPEN_DRAIN 238 | elif drive == "open_source": 239 | flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_OPEN_SOURCE 240 | 241 | if inverted: 242 | flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_ACTIVE_LOW 243 | 244 | # FIXME this should really use GPIOHANDLE_SET_CONFIG_IOCTL instead of 245 | # closing and reopening, especially to preserve output value on 246 | # configuration changes 247 | 248 | # Close existing line 249 | if self._line_fd is not None: 250 | try: 251 | os.close(self._line_fd) 252 | except OSError as e: 253 | raise GPIOError(e.errno, "Closing existing GPIO line: " + e.strerror) 254 | 255 | self._line_fd = None 256 | 257 | line_request = _CGpioV2LineRequest() 258 | 259 | if direction == "in": 260 | 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 261 | flags |= Cdev2GPIO._GPIO_V2_LINE_FLAG_EVENT_CLOCK_REALTIME if edge != "none" else 0 262 | 263 | line_request.offsets[0] = self._line 264 | line_request.consumer = self._label 265 | line_request.config.flags = flags | Cdev2GPIO._GPIO_V2_LINE_FLAG_INPUT 266 | line_request.num_lines = 1 267 | 268 | try: 269 | fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINE_IOCTL, line_request) 270 | except (OSError, IOError) as e: 271 | raise GPIOError(e.errno, "Opening input line handle: " + e.strerror) 272 | else: 273 | initial_value = True if direction == "high" else False 274 | initial_value ^= inverted 275 | 276 | line_request.offsets[0] = self._line 277 | line_request.consumer = self._label 278 | line_request.config.flags = flags | Cdev2GPIO._GPIO_V2_LINE_FLAG_OUTPUT 279 | line_request.config.num_attrs = 1 280 | line_request.config.attrs[0].attr.id = Cdev2GPIO._GPIO_V2_LINE_ATTR_ID_OUTPUT_VALUES 281 | line_request.config.attrs[0].attr.data.values = int(initial_value) 282 | line_request.config.attrs[0].mask = 0x1 283 | line_request.num_lines = 1 284 | 285 | try: 286 | fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINE_IOCTL, line_request) 287 | except (OSError, IOError) as e: 288 | raise GPIOError(e.errno, "Opening output line handle: " + e.strerror) 289 | 290 | self._line_fd = line_request.fd 291 | 292 | self._direction = "in" if direction == "in" else "out" 293 | self._edge = edge 294 | self._bias = bias 295 | self._drive = drive 296 | self._inverted = inverted 297 | 298 | def _find_line_by_name(self, path, line): 299 | # Open GPIO chip 300 | try: 301 | fd = os.open(path, 0) 302 | except OSError as e: 303 | raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) 304 | 305 | # Get chip info for number of lines 306 | chip_info = _CGpiochipInfo() 307 | try: 308 | fcntl.ioctl(fd, Cdev2GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 309 | except (OSError, IOError) as e: 310 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 311 | 312 | # Get each line info 313 | line_info = _CGpioV2LineInfo() 314 | found = False 315 | for i in range(chip_info.lines): 316 | line_info.offset = i 317 | try: 318 | fcntl.ioctl(fd, Cdev2GPIO._GPIO_V2_GET_LINEINFO_IOCTL, line_info) 319 | except (OSError, IOError) as e: 320 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 321 | 322 | if line_info.name.decode() == line: 323 | found = True 324 | break 325 | 326 | try: 327 | os.close(fd) 328 | except OSError as e: 329 | raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) 330 | 331 | if found: 332 | return i 333 | 334 | raise LookupError("Opening GPIO line: GPIO line \"{:s}\" not found by name.".format(line)) 335 | 336 | # Methods 337 | 338 | def read(self): 339 | data = _CGpioV2LineValues() 340 | 341 | data.mask = 0x1 342 | 343 | try: 344 | fcntl.ioctl(self._line_fd, Cdev2GPIO._GPIO_V2_LINE_GET_VALUES_IOCTL, data) 345 | except (OSError, IOError) as e: 346 | raise GPIOError(e.errno, "Getting line value: " + e.strerror) 347 | 348 | return bool(data.bits & 0x1) 349 | 350 | def write(self, value): 351 | if not isinstance(value, bool): 352 | raise TypeError("Invalid value type, should be bool.") 353 | elif self._direction != "out": 354 | raise GPIOError(None, "Invalid operation: cannot write to input GPIO") 355 | 356 | data = _CGpioV2LineValues() 357 | 358 | data.mask = 0x1 359 | data.bits = int(value) 360 | 361 | try: 362 | fcntl.ioctl(self._line_fd, Cdev2GPIO._GPIO_V2_LINE_SET_VALUES_IOCTL, data) 363 | except (OSError, IOError) as e: 364 | raise GPIOError(e.errno, "Setting line value: " + e.strerror) 365 | 366 | def poll(self, timeout=None): 367 | if not isinstance(timeout, (int, float, type(None))): 368 | raise TypeError("Invalid timeout type, should be integer, float, or None.") 369 | elif self._direction != "in": 370 | raise GPIOError(None, "Invalid operation: cannot poll output GPIO") 371 | 372 | # Setup poll 373 | p = select.poll() 374 | p.register(self._line_fd, select.POLLIN | select.POLLPRI | select.POLLERR) 375 | 376 | # Scale timeout to milliseconds 377 | if isinstance(timeout, (int, float)) and timeout > 0: 378 | timeout *= 1000 379 | 380 | # Poll 381 | events = p.poll(timeout) 382 | 383 | return len(events) > 0 384 | 385 | def read_event(self): 386 | if self._direction != "in": 387 | raise GPIOError(None, "Invalid operation: cannot read event of output GPIO") 388 | elif self._edge == "none": 389 | raise GPIOError(None, "Invalid operation: GPIO edge not set") 390 | 391 | try: 392 | buf = os.read(self._line_fd, ctypes.sizeof(_CGpioV2LineEvent)) 393 | except OSError as e: 394 | raise GPIOError(e.errno, "Reading GPIO event: " + e.strerror) 395 | 396 | line_event = _CGpioV2LineEvent.from_buffer_copy(buf) 397 | 398 | if line_event.id == Cdev2GPIO._GPIO_V2_LINE_EVENT_RISING_EDGE: 399 | edge = "rising" 400 | elif line_event.id == Cdev2GPIO._GPIO_V2_LINE_EVENT_FALLING_EDGE: 401 | edge = "falling" 402 | else: 403 | edge = "none" 404 | 405 | timestamp = line_event.timestamp_ns 406 | 407 | return EdgeEvent(edge, timestamp) 408 | 409 | def close(self): 410 | try: 411 | if self._line_fd is not None: 412 | os.close(self._line_fd) 413 | except OSError as e: 414 | raise GPIOError(e.errno, "Closing GPIO line: " + e.strerror) 415 | 416 | try: 417 | if self._chip_fd is not None: 418 | os.close(self._chip_fd) 419 | except OSError as e: 420 | raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) 421 | 422 | self._line_fd = None 423 | self._chip_fd = None 424 | self._edge = "none" 425 | self._direction = "in" 426 | self._line = None 427 | 428 | # Immutable properties 429 | 430 | @property 431 | def devpath(self): 432 | return self._devpath 433 | 434 | @property 435 | def fd(self): 436 | return self._line_fd 437 | 438 | @property 439 | def line(self): 440 | return self._line 441 | 442 | @property 443 | def name(self): 444 | line_info = _CGpioV2LineInfo() 445 | line_info.offset = self._line 446 | 447 | try: 448 | fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINEINFO_IOCTL, line_info) 449 | except (OSError, IOError) as e: 450 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 451 | 452 | return line_info.name.decode() 453 | 454 | @property 455 | def label(self): 456 | line_info = _CGpioV2LineInfo() 457 | line_info.offset = self._line 458 | 459 | try: 460 | fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_V2_GET_LINEINFO_IOCTL, line_info) 461 | except (OSError, IOError) as e: 462 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 463 | 464 | return line_info.consumer.decode() 465 | 466 | @property 467 | def chip_fd(self): 468 | return self._chip_fd 469 | 470 | @property 471 | def chip_name(self): 472 | chip_info = _CGpiochipInfo() 473 | 474 | try: 475 | fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 476 | except (OSError, IOError) as e: 477 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 478 | 479 | return chip_info.name.decode() 480 | 481 | @property 482 | def chip_label(self): 483 | chip_info = _CGpiochipInfo() 484 | 485 | try: 486 | fcntl.ioctl(self._chip_fd, Cdev2GPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 487 | except (OSError, IOError) as e: 488 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 489 | 490 | return chip_info.label.decode() 491 | 492 | # Mutable properties 493 | 494 | def _get_direction(self): 495 | return self._direction 496 | 497 | def _set_direction(self, direction): 498 | if not isinstance(direction, str): 499 | raise TypeError("Invalid direction type, should be string.") 500 | if direction not in ["in", "out", "high", "low"]: 501 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 502 | 503 | if self._direction == direction: 504 | return 505 | 506 | self._reopen(direction, "none", self._bias, self._drive, self._inverted) 507 | 508 | direction = property(_get_direction, _set_direction) 509 | 510 | def _get_edge(self): 511 | return self._edge 512 | 513 | def _set_edge(self, edge): 514 | if not isinstance(edge, str): 515 | raise TypeError("Invalid edge type, should be string.") 516 | if edge not in ["none", "rising", "falling", "both"]: 517 | raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") 518 | 519 | if self._direction != "in": 520 | raise GPIOError(None, "Invalid operation: cannot set edge on output GPIO") 521 | 522 | if self._edge == edge: 523 | return 524 | 525 | self._reopen(self._direction, edge, self._bias, self._drive, self._inverted) 526 | 527 | edge = property(_get_edge, _set_edge) 528 | 529 | def _get_bias(self): 530 | return self._bias 531 | 532 | def _set_bias(self, bias): 533 | if not isinstance(bias, str): 534 | raise TypeError("Invalid bias type, should be string.") 535 | if bias not in ["default", "pull_up", "pull_down", "disable"]: 536 | raise ValueError("Invalid bias, can be: \"default\", \"pull_up\", \"pull_down\", \"disable\".") 537 | 538 | if self._bias == bias: 539 | return 540 | 541 | self._reopen(self._direction, self._edge, bias, self._drive, self._inverted) 542 | 543 | bias = property(_get_bias, _set_bias) 544 | 545 | def _get_drive(self): 546 | return self._drive 547 | 548 | def _set_drive(self, drive): 549 | if not isinstance(drive, str): 550 | raise TypeError("Invalid drive type, should be string.") 551 | if drive not in ["default", "open_drain", "open_source"]: 552 | raise ValueError("Invalid drive, can be: \"default\", \"open_drain\", \"open_source\".") 553 | 554 | if self._direction != "out" and drive != "default": 555 | raise GPIOError(None, "Invalid operation: cannot set line drive on input GPIO") 556 | 557 | if self._drive == drive: 558 | return 559 | 560 | self._reopen(self._direction, self._edge, self._bias, drive, self._inverted) 561 | 562 | drive = property(_get_drive, _set_drive) 563 | 564 | def _get_inverted(self): 565 | return self._inverted 566 | 567 | def _set_inverted(self, inverted): 568 | if not isinstance(inverted, bool): 569 | raise TypeError("Invalid drive type, should be bool.") 570 | 571 | if self._inverted == inverted: 572 | return 573 | 574 | self._reopen(self._direction, self._edge, self._bias, self._drive, inverted) 575 | 576 | inverted = property(_get_inverted, _set_inverted) 577 | 578 | # String representation 579 | 580 | def __str__(self): 581 | try: 582 | str_name = self.name 583 | except GPIOError: 584 | str_name = "" 585 | 586 | try: 587 | str_label = self.label 588 | except GPIOError: 589 | str_label = "" 590 | 591 | try: 592 | str_direction = self.direction 593 | except GPIOError: 594 | str_direction = "" 595 | 596 | try: 597 | str_edge = self.edge 598 | except GPIOError: 599 | str_edge = "" 600 | 601 | try: 602 | str_bias = self.bias 603 | except GPIOError: 604 | str_bias = "" 605 | 606 | try: 607 | str_drive = self.drive 608 | except GPIOError: 609 | str_drive = "" 610 | 611 | try: 612 | str_inverted = str(self.inverted) 613 | except GPIOError: 614 | str_inverted = "" 615 | 616 | try: 617 | str_chip_name = self.chip_name 618 | except GPIOError: 619 | str_chip_name = "" 620 | 621 | try: 622 | str_chip_label = self.chip_label 623 | except GPIOError: 624 | str_chip_label = "" 625 | 626 | 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)" \ 627 | .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) 628 | -------------------------------------------------------------------------------- /periphery/gpio_sysfs.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import os 3 | import os.path 4 | import select 5 | import time 6 | 7 | from .gpio import GPIO, GPIOError 8 | 9 | 10 | class SysfsGPIO(GPIO): 11 | # Number of retries to check for GPIO export or direction on open 12 | _GPIO_STAT_RETRIES = 10 13 | # Delay between check for GPIO export or direction write on open (100ms) 14 | _GPIO_STAT_DELAY = 0.1 15 | 16 | def __init__(self, line, direction): 17 | """**Sysfs GPIO** 18 | 19 | Instantiate a GPIO object and open the sysfs GPIO with the specified 20 | line and direction. 21 | 22 | `direction` can be "in" for input; "out" for output, initialized to 23 | low; "high" for output, initialized to high; or "low" for output, 24 | initialized to low. 25 | 26 | Args: 27 | line (int): GPIO line number. 28 | direction (str): GPIO direction, can be "in", "out", "high", or 29 | "low", 30 | 31 | Returns: 32 | SysfsGPIO: GPIO object. 33 | 34 | Raises: 35 | GPIOError: if an I/O or OS error occurs. 36 | TypeError: if `line` or `direction` types are invalid. 37 | ValueError: if `direction` value is invalid. 38 | TimeoutError: if waiting for GPIO export times out. 39 | 40 | """ 41 | self._fd = None 42 | self._line = None 43 | self._exported = False 44 | 45 | self._open(line, direction) 46 | 47 | def __new__(self, line, direction): 48 | return object.__new__(SysfsGPIO) 49 | 50 | def _open(self, line, direction): 51 | if not isinstance(line, int): 52 | raise TypeError("Invalid line type, should be integer.") 53 | if not isinstance(direction, str): 54 | raise TypeError("Invalid direction type, should be string.") 55 | if direction.lower() not in ["in", "out", "high", "low"]: 56 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 57 | 58 | gpio_path = "/sys/class/gpio/gpio{:d}".format(line) 59 | 60 | if not os.path.isdir(gpio_path): 61 | # Export the line 62 | try: 63 | with open("/sys/class/gpio/export", "w") as f_export: 64 | f_export.write("{:d}\n".format(line)) 65 | except IOError as e: 66 | raise GPIOError(e.errno, "Exporting GPIO: " + e.strerror) 67 | 68 | # Loop until GPIO is exported 69 | for i in range(SysfsGPIO._GPIO_STAT_RETRIES): 70 | if os.path.isdir(gpio_path): 71 | self._exported = True 72 | break 73 | 74 | time.sleep(SysfsGPIO._GPIO_STAT_DELAY) 75 | 76 | if not self._exported: 77 | raise TimeoutError("Exporting GPIO: waiting for \"{:s}\" timed out".format(gpio_path)) 78 | 79 | # Loop until direction is writable. This could take some time after 80 | # export as application of udev rules after export is asynchronous. 81 | for i in range(SysfsGPIO._GPIO_STAT_RETRIES): 82 | try: 83 | with open(os.path.join(gpio_path, "direction"), 'w'): 84 | break 85 | except IOError as e: 86 | if e.errno != errno.EACCES or (e.errno == errno.EACCES and i == SysfsGPIO._GPIO_STAT_RETRIES - 1): 87 | raise GPIOError(e.errno, "Opening GPIO direction: " + e.strerror) 88 | 89 | time.sleep(SysfsGPIO._GPIO_STAT_DELAY) 90 | 91 | # Open value 92 | try: 93 | self._fd = os.open(os.path.join(gpio_path, "value"), os.O_RDWR) 94 | except OSError as e: 95 | raise GPIOError(e.errno, "Opening GPIO: " + e.strerror) 96 | 97 | self._line = line 98 | self._path = gpio_path 99 | 100 | # Initialize direction 101 | if self.direction != direction.lower(): 102 | self.direction = direction 103 | 104 | # Methods 105 | 106 | def read(self): 107 | # Read value 108 | try: 109 | buf = os.read(self._fd, 2) 110 | except OSError as e: 111 | raise GPIOError(e.errno, "Reading GPIO: " + e.strerror) 112 | 113 | # Rewind 114 | try: 115 | os.lseek(self._fd, 0, os.SEEK_SET) 116 | except OSError as e: 117 | raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) 118 | 119 | if buf[0] == b"0"[0]: 120 | return False 121 | elif buf[0] == b"1"[0]: 122 | return True 123 | 124 | raise GPIOError(None, "Unknown GPIO value: {}".format(buf)) 125 | 126 | def write(self, value): 127 | if not isinstance(value, bool): 128 | raise TypeError("Invalid value type, should be bool.") 129 | 130 | # Write value 131 | try: 132 | if value: 133 | os.write(self._fd, b"1\n") 134 | else: 135 | os.write(self._fd, b"0\n") 136 | except OSError as e: 137 | raise GPIOError(e.errno, "Writing GPIO: " + e.strerror) 138 | 139 | # Rewind 140 | try: 141 | os.lseek(self._fd, 0, os.SEEK_SET) 142 | except OSError as e: 143 | raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) 144 | 145 | def poll(self, timeout=None): 146 | if not isinstance(timeout, (int, float, type(None))): 147 | raise TypeError("Invalid timeout type, should be integer, float, or None.") 148 | 149 | # Setup poll 150 | p = select.poll() 151 | p.register(self._fd, select.POLLPRI | select.POLLERR) 152 | 153 | # Scale timeout to milliseconds 154 | if isinstance(timeout, (int, float)) and timeout > 0: 155 | timeout *= 1000 156 | 157 | # Poll 158 | events = p.poll(timeout) 159 | 160 | # If GPIO edge interrupt occurred 161 | if events: 162 | # Rewind 163 | try: 164 | os.lseek(self._fd, 0, os.SEEK_SET) 165 | except OSError as e: 166 | raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) 167 | 168 | return True 169 | 170 | return False 171 | 172 | def read_event(self): 173 | raise NotImplementedError() 174 | 175 | def close(self): 176 | if self._fd is None: 177 | return 178 | 179 | try: 180 | os.close(self._fd) 181 | except OSError as e: 182 | raise GPIOError(e.errno, "Closing GPIO: " + e.strerror) 183 | 184 | self._fd = None 185 | 186 | if self._exported: 187 | # Unexport the line 188 | try: 189 | unexport_fd = os.open("/sys/class/gpio/unexport", os.O_WRONLY) 190 | os.write(unexport_fd, "{:d}\n".format(self._line).encode()) 191 | os.close(unexport_fd) 192 | except OSError as e: 193 | raise GPIOError(e.errno, "Unexporting GPIO: " + e.strerror) 194 | 195 | # Immutable properties 196 | 197 | @property 198 | def devpath(self): 199 | return self._path 200 | 201 | @property 202 | def fd(self): 203 | return self._fd 204 | 205 | @property 206 | def line(self): 207 | return self._line 208 | 209 | @property 210 | def name(self): 211 | return "" 212 | 213 | @property 214 | def label(self): 215 | return "" 216 | 217 | @property 218 | def chip_fd(self): 219 | raise NotImplementedError("Sysfs GPIO does not have a gpiochip file descriptor.") 220 | 221 | @property 222 | def chip_name(self): 223 | gpio_path = os.path.join(self._path, "device") 224 | 225 | gpiochip_path = os.readlink(gpio_path) 226 | 227 | if '/' not in gpiochip_path: 228 | raise GPIOError(None, "Reading gpiochip name: invalid device symlink \"{:s}\"".format(gpiochip_path)) 229 | 230 | return gpiochip_path.split('/')[-1] 231 | 232 | @property 233 | def chip_label(self): 234 | gpio_path = "/sys/class/gpio/{:s}/label".format(self.chip_name) 235 | 236 | try: 237 | with open(gpio_path, "r") as f_label: 238 | label = f_label.read() 239 | except (GPIOError, IOError) as e: 240 | if isinstance(e, IOError): 241 | raise GPIOError(e.errno, "Reading gpiochip label: " + e.strerror) 242 | 243 | raise GPIOError(None, "Reading gpiochip label: " + e.strerror) 244 | 245 | return label.strip() 246 | 247 | # Mutable properties 248 | 249 | def _get_direction(self): 250 | # Read direction 251 | try: 252 | with open(os.path.join(self._path, "direction"), "r") as f_direction: 253 | direction = f_direction.read() 254 | except IOError as e: 255 | raise GPIOError(e.errno, "Getting GPIO direction: " + e.strerror) 256 | 257 | return direction.strip() 258 | 259 | def _set_direction(self, direction): 260 | if not isinstance(direction, str): 261 | raise TypeError("Invalid direction type, should be string.") 262 | if direction.lower() not in ["in", "out", "high", "low"]: 263 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 264 | 265 | # Write direction 266 | try: 267 | with open(os.path.join(self._path, "direction"), "w") as f_direction: 268 | f_direction.write(direction.lower() + "\n") 269 | except IOError as e: 270 | raise GPIOError(e.errno, "Setting GPIO direction: " + e.strerror) 271 | 272 | direction = property(_get_direction, _set_direction) 273 | 274 | def _get_edge(self): 275 | # Read edge 276 | try: 277 | with open(os.path.join(self._path, "edge"), "r") as f_edge: 278 | edge = f_edge.read() 279 | except IOError as e: 280 | raise GPIOError(e.errno, "Getting GPIO edge: " + e.strerror) 281 | 282 | return edge.strip() 283 | 284 | def _set_edge(self, edge): 285 | if not isinstance(edge, str): 286 | raise TypeError("Invalid edge type, should be string.") 287 | if edge.lower() not in ["none", "rising", "falling", "both"]: 288 | raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") 289 | 290 | # Write edge 291 | try: 292 | with open(os.path.join(self._path, "edge"), "w") as f_edge: 293 | f_edge.write(edge.lower() + "\n") 294 | except IOError as e: 295 | raise GPIOError(e.errno, "Setting GPIO edge: " + e.strerror) 296 | 297 | edge = property(_get_edge, _set_edge) 298 | 299 | def _get_bias(self): 300 | raise NotImplementedError("Sysfs GPIO does not support line bias property.") 301 | 302 | def _set_bias(self, bias): 303 | raise NotImplementedError("Sysfs GPIO does not support line bias property.") 304 | 305 | bias = property(_get_bias, _set_bias) 306 | 307 | def _get_drive(self): 308 | raise NotImplementedError("Sysfs GPIO does not support line drive property.") 309 | 310 | def _set_drive(self, drive): 311 | raise NotImplementedError("Sysfs GPIO does not support line drive property.") 312 | 313 | drive = property(_get_drive, _set_drive) 314 | 315 | def _get_inverted(self): 316 | # Read active_low 317 | try: 318 | with open(os.path.join(self._path, "active_low"), "r") as f_inverted: 319 | inverted = f_inverted.read().strip() 320 | except IOError as e: 321 | raise GPIOError(e.errno, "Getting GPIO active_low: " + e.strerror) 322 | 323 | if inverted == "0": 324 | return False 325 | elif inverted == "1": 326 | return True 327 | 328 | raise GPIOError(None, "Unknown GPIO active_low value: {}".format(inverted)) 329 | 330 | def _set_inverted(self, inverted): 331 | if not isinstance(inverted, bool): 332 | raise TypeError("Invalid drive type, should be bool.") 333 | 334 | # Write active_low 335 | try: 336 | with open(os.path.join(self._path, "active_low"), "w") as f_active_low: 337 | f_active_low.write("1\n" if inverted else "0\n") 338 | except IOError as e: 339 | raise GPIOError(e.errno, "Setting GPIO active_low: " + e.strerror) 340 | 341 | inverted = property(_get_inverted, _set_inverted) 342 | 343 | # String representation 344 | 345 | def __str__(self): 346 | try: 347 | str_direction = self.direction 348 | except GPIOError: 349 | str_direction = "" 350 | 351 | try: 352 | str_edge = self.edge 353 | except GPIOError: 354 | str_edge = "" 355 | 356 | try: 357 | str_chip_name = self.chip_name 358 | except GPIOError: 359 | str_chip_name = "" 360 | 361 | try: 362 | str_chip_label = self.chip_label 363 | except GPIOError: 364 | str_chip_label = "" 365 | 366 | try: 367 | str_inverted = str(self.inverted) 368 | except GPIOError: 369 | str_inverted = "" 370 | 371 | return "GPIO {:d} (device={:s}, fd={:d}, direction={:s}, edge={:s}, inverted={:s}, chip_name=\"{:s}\", chip_label=\"{:s}\", type=sysfs)" \ 372 | .format(self._line, self._path, self._fd, str_direction, str_edge, str_inverted, str_chip_name, str_chip_label) 373 | -------------------------------------------------------------------------------- /periphery/i2c.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ctypes 3 | import array 4 | import fcntl 5 | 6 | 7 | class I2CError(IOError): 8 | """Base class for I2C errors.""" 9 | pass 10 | 11 | 12 | class _CI2CMessage(ctypes.Structure): 13 | _fields_ = [ 14 | ("addr", ctypes.c_ushort), 15 | ("flags", ctypes.c_ushort), 16 | ("len", ctypes.c_ushort), 17 | ("buf", ctypes.POINTER(ctypes.c_ubyte)), 18 | ] 19 | 20 | 21 | class _CI2CIocTransfer(ctypes.Structure): 22 | _fields_ = [ 23 | ("msgs", ctypes.POINTER(_CI2CMessage)), 24 | ("nmsgs", ctypes.c_uint), 25 | ] 26 | 27 | 28 | class I2C(object): 29 | # Constants scraped from and 30 | _I2C_IOC_FUNCS = 0x705 31 | _I2C_IOC_RDWR = 0x707 32 | _I2C_FUNC_I2C = 0x1 33 | _I2C_M_TEN = 0x0010 34 | _I2C_M_RD = 0x0001 35 | _I2C_M_STOP = 0x8000 36 | _I2C_M_NOSTART = 0x4000 37 | _I2C_M_REV_DIR_ADDR = 0x2000 38 | _I2C_M_IGNORE_NAK = 0x1000 39 | _I2C_M_NO_RD_ACK = 0x0800 40 | _I2C_M_RECV_LEN = 0x0400 41 | 42 | def __init__(self, devpath): 43 | """Instantiate an I2C object and open the i2c-dev device at the 44 | specified path. 45 | 46 | Args: 47 | devpath (str): i2c-dev device path. 48 | 49 | Returns: 50 | I2C: I2C object. 51 | 52 | Raises: 53 | I2CError: if an I/O or OS error occurs. 54 | 55 | """ 56 | self._fd = None 57 | self._devpath = None 58 | self._open(devpath) 59 | 60 | def __del__(self): 61 | self.close() 62 | 63 | def __enter__(self): 64 | return self 65 | 66 | def __exit__(self, t, value, traceback): 67 | self.close() 68 | 69 | def _open(self, devpath): 70 | # Open i2c device 71 | try: 72 | self._fd = os.open(devpath, os.O_RDWR) 73 | except OSError as e: 74 | raise I2CError(e.errno, "Opening I2C device: " + e.strerror) 75 | 76 | self._devpath = devpath 77 | 78 | # Query supported functions 79 | buf = array.array('I', [0]) 80 | try: 81 | fcntl.ioctl(self._fd, I2C._I2C_IOC_FUNCS, buf, True) 82 | except (OSError, IOError) as e: 83 | self.close() 84 | raise I2CError(e.errno, "Querying supported functions: " + e.strerror) 85 | 86 | # Check that I2C_RDWR ioctl() is supported on this device 87 | if (buf[0] & I2C._I2C_FUNC_I2C) == 0: 88 | self.close() 89 | raise I2CError(None, "I2C not supported on device \"{:s}\"".format(devpath)) 90 | 91 | # Methods 92 | 93 | def transfer(self, address, messages): 94 | """Transfer `messages` to the specified I2C `address`. Modifies the 95 | `messages` array with the results of any read transactions. 96 | 97 | Args: 98 | address (int): I2C address. 99 | messages (list): list of I2C.Message messages. 100 | 101 | Raises: 102 | I2CError: if an I/O or OS error occurs. 103 | TypeError: if `messages` type is not list. 104 | ValueError: if `messages` length is zero, or if message data is not valid bytes. 105 | 106 | """ 107 | if not isinstance(messages, list): 108 | raise TypeError("Invalid messages type, should be list of I2C.Message.") 109 | elif len(messages) == 0: 110 | raise ValueError("Invalid messages data, should be non-zero length.") 111 | 112 | # Convert I2C.Message messages to _CI2CMessage messages 113 | cmessages = (_CI2CMessage * len(messages))() 114 | for i in range(len(messages)): 115 | # Convert I2C.Message data to bytes 116 | if isinstance(messages[i].data, bytes): 117 | data = messages[i].data 118 | elif isinstance(messages[i].data, bytearray): 119 | data = bytes(messages[i].data) 120 | elif isinstance(messages[i].data, list): 121 | data = bytes(bytearray(messages[i].data)) 122 | 123 | cmessages[i].addr = address 124 | cmessages[i].flags = messages[i].flags | (I2C._I2C_M_RD if messages[i].read else 0) 125 | cmessages[i].len = len(data) 126 | cmessages[i].buf = ctypes.cast(ctypes.create_string_buffer(data, len(data)), ctypes.POINTER(ctypes.c_ubyte)) 127 | 128 | # Prepare transfer structure 129 | i2c_xfer = _CI2CIocTransfer() 130 | i2c_xfer.nmsgs = len(cmessages) 131 | i2c_xfer.msgs = cmessages 132 | 133 | # Transfer 134 | try: 135 | fcntl.ioctl(self._fd, I2C._I2C_IOC_RDWR, i2c_xfer, False) 136 | except (OSError, IOError) as e: 137 | raise I2CError(e.errno, "I2C transfer: " + e.strerror) 138 | 139 | # Update any read I2C.Message messages 140 | for i in range(len(messages)): 141 | if messages[i].read: 142 | data = [cmessages[i].buf[j] for j in range(cmessages[i].len)] 143 | # Convert read data to type used in I2C.Message messages 144 | if isinstance(messages[i].data, list): 145 | messages[i].data = data 146 | elif isinstance(messages[i].data, bytearray): 147 | messages[i].data = bytearray(data) 148 | elif isinstance(messages[i].data, bytes): 149 | messages[i].data = bytes(bytearray(data)) 150 | 151 | def close(self): 152 | """Close the i2c-dev I2C device. 153 | 154 | Raises: 155 | I2CError: if an I/O or OS error occurs. 156 | 157 | """ 158 | if self._fd is None: 159 | return 160 | 161 | try: 162 | os.close(self._fd) 163 | except OSError as e: 164 | raise I2CError(e.errno, "Closing I2C device: " + e.strerror) 165 | 166 | self._fd = None 167 | 168 | # Immutable properties 169 | 170 | @property 171 | def fd(self): 172 | """Get the file descriptor of the underlying i2c-dev device. 173 | 174 | :type: int 175 | """ 176 | return self._fd 177 | 178 | @property 179 | def devpath(self): 180 | """Get the device path of the underlying i2c-dev device. 181 | 182 | :type: str 183 | """ 184 | return self._devpath 185 | 186 | # String representation 187 | 188 | def __str__(self): 189 | return "I2C (device={:s}, fd={:d})".format(self.devpath, self.fd) 190 | 191 | class Message: 192 | def __init__(self, data, read=False, flags=0): 193 | """Instantiate an I2C Message object. 194 | 195 | Args: 196 | data (bytes, bytearray, list): a byte array or list of 8-bit 197 | integers to write. 198 | read (bool): specify this as a read message, where `data` 199 | serves as placeholder bytes for the read. 200 | flags (int): additional i2c-dev flags for this message. 201 | 202 | Returns: 203 | Message: Message object. 204 | 205 | Raises: 206 | TypeError: if `data`, `read`, or `flags` types are invalid. 207 | 208 | """ 209 | if not isinstance(data, (bytes, bytearray, list)): 210 | raise TypeError("Invalid data type, should be bytes, bytearray, or list.") 211 | if not isinstance(read, bool): 212 | raise TypeError("Invalid read type, should be boolean.") 213 | if not isinstance(flags, int): 214 | raise TypeError("Invalid flags type, should be integer.") 215 | 216 | self.data = data 217 | self.read = read 218 | self.flags = flags 219 | -------------------------------------------------------------------------------- /periphery/i2c.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | 3 | class I2CError(IOError): ... 4 | 5 | class I2C: 6 | class Message: 7 | data: bytes | bytearray | list[int] 8 | read: bool 9 | flags: int 10 | def __init__(self, data: bytes | bytearray | list[int], read: bool = ..., flags: int = ...) -> None: ... 11 | 12 | def __init__(self, devpath: str) -> None: ... 13 | def __del__(self) -> None: ... 14 | def __enter__(self) -> I2C: ... # noqa: Y034 15 | def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... 16 | def transfer(self, address: int, messages: list[Message]) -> None: ... 17 | def close(self) -> None: ... 18 | @property 19 | def fd(self) -> int: ... 20 | @property 21 | def devpath(self) -> str: ... 22 | -------------------------------------------------------------------------------- /periphery/led.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | 4 | 5 | class LEDError(IOError): 6 | """Base class for LED errors.""" 7 | pass 8 | 9 | 10 | class LED(object): 11 | def __init__(self, name, brightness=None): 12 | """Instantiate an LED object and open the sysfs LED corresponding to 13 | the specified name. 14 | 15 | `brightness` can be a boolean for on/off, integer value for a specific 16 | brightness, or None to preserve existing brightness. Default is 17 | preserve existing brightness. 18 | 19 | Args: 20 | name (str): Linux led name. 21 | brightness (bool, int, None): Initial brightness. 22 | 23 | Returns: 24 | LED: LED object. 25 | 26 | Raises: 27 | LEDError: if an I/O or OS error occurs. 28 | TypeError: if `name` or `brightness` types are invalid. 29 | LookupError: if LED name does not exist. 30 | ValueError: if `brightness` value is invalid. 31 | 32 | """ 33 | self._path = None 34 | self._fd = None 35 | self._name = None 36 | self._max_brightness = None 37 | self._open(name, brightness) 38 | 39 | def __del__(self): 40 | self.close() 41 | 42 | def __enter__(self): 43 | return self 44 | 45 | def __exit__(self, t, value, traceback): 46 | self.close() 47 | 48 | def _open(self, name, brightness): 49 | if not isinstance(name, str): 50 | raise TypeError("Invalid name type, should be string.") 51 | if not isinstance(brightness, (bool, int, type(None))): 52 | raise TypeError("Invalid brightness type, should be bool, int, or None.") 53 | 54 | led_path = "/sys/class/leds/{:s}".format(name) 55 | 56 | if not os.path.isdir(led_path): 57 | raise LookupError("Opening LED: LED \"{:s}\" not found.".format(name)) 58 | 59 | # Read max brightness 60 | try: 61 | with open(os.path.join(led_path, "max_brightness"), "r") as f_max_brightness: 62 | self._max_brightness = int(f_max_brightness.read()) 63 | except IOError as e: 64 | raise LEDError(e.errno, "Reading LED max brightness: " + e.strerror) 65 | 66 | # Open brightness 67 | try: 68 | self._fd = os.open(os.path.join(led_path, "brightness"), os.O_RDWR) 69 | except OSError as e: 70 | raise LEDError(e.errno, "Opening LED brightness: " + e.strerror) 71 | 72 | self._name = name 73 | self._path = led_path 74 | 75 | # Set initial brightness 76 | if brightness: 77 | self.write(brightness) 78 | 79 | # Methods 80 | 81 | def read(self): 82 | """Read the brightness of the LED. 83 | 84 | Returns: 85 | int: Current brightness. 86 | 87 | Raises: 88 | LEDError: if an I/O or OS error occurs. 89 | 90 | """ 91 | # Read value 92 | try: 93 | buf = os.read(self._fd, 8) 94 | except OSError as e: 95 | raise LEDError(e.errno, "Reading LED brightness: " + e.strerror) 96 | 97 | # Rewind 98 | try: 99 | os.lseek(self._fd, 0, os.SEEK_SET) 100 | except OSError as e: 101 | raise LEDError(e.errno, "Rewinding LED brightness: " + e.strerror) 102 | 103 | return int(buf) 104 | 105 | def write(self, brightness): 106 | """Set the brightness of the LED to `brightness`. 107 | 108 | `brightness` can be a boolean for on/off, or integer value for a 109 | specific brightness. 110 | 111 | Args: 112 | brightness (bool, int): Brightness value to set. 113 | 114 | Raises: 115 | LEDError: if an I/O or OS error occurs. 116 | TypeError: if `brightness` type is not bool or int. 117 | 118 | """ 119 | if not isinstance(brightness, (bool, int)): 120 | raise TypeError("Invalid brightness type, should be bool or int.") 121 | 122 | if isinstance(brightness, bool): 123 | brightness = self._max_brightness if brightness else 0 124 | else: 125 | if not 0 <= brightness <= self._max_brightness: 126 | raise ValueError("Invalid brightness value: should be between 0 and {:d}".format(self._max_brightness)) 127 | 128 | # Write value 129 | try: 130 | os.write(self._fd, "{:d}\n".format(brightness).encode()) 131 | except OSError as e: 132 | raise LEDError(e.errno, "Writing LED brightness: " + e.strerror) 133 | 134 | # Rewind 135 | try: 136 | os.lseek(self._fd, 0, os.SEEK_SET) 137 | except OSError as e: 138 | raise LEDError(e.errno, "Rewinding LED brightness: " + e.strerror) 139 | 140 | def close(self): 141 | """Close the sysfs LED. 142 | 143 | Raises: 144 | LEDError: if an I/O or OS error occurs. 145 | 146 | """ 147 | if self._fd is None: 148 | return 149 | 150 | try: 151 | os.close(self._fd) 152 | except OSError as e: 153 | raise LEDError(e.errno, "Closing LED: " + e.strerror) 154 | 155 | self._fd = None 156 | 157 | # Immutable properties 158 | 159 | @property 160 | def devpath(self): 161 | """Get the device path of the underlying sysfs LED device. 162 | 163 | :type: str 164 | """ 165 | return self._path 166 | 167 | @property 168 | def fd(self): 169 | """Get the file descriptor for the underlying sysfs LED "brightness" 170 | file of the LED object. 171 | 172 | :type: int 173 | """ 174 | return self._fd 175 | 176 | @property 177 | def name(self): 178 | """Get the sysfs LED name. 179 | 180 | :type: str 181 | """ 182 | return self._name 183 | 184 | @property 185 | def max_brightness(self): 186 | """Get the LED's max brightness. 187 | 188 | :type: int 189 | """ 190 | return self._max_brightness 191 | 192 | # Mutable properties 193 | 194 | def _get_brightness(self): 195 | # Read brightness 196 | return self.read() 197 | 198 | def _set_brightness(self, brightness): 199 | return self.write(brightness) 200 | 201 | brightness = property(_get_brightness, _set_brightness) 202 | """Get or set the LED's brightness. 203 | 204 | Value can be a boolean for on/off, or integer value a for specific 205 | brightness. 206 | 207 | Raises: 208 | LEDError: if an I/O or OS error occurs. 209 | TypeError: if `brightness` type is not bool or int. 210 | ValueError: if `brightness` value is invalid. 211 | 212 | :type: int 213 | """ 214 | 215 | # String representation 216 | 217 | def __str__(self): 218 | return "LED {:s} (device={:s}, fd={:d}, max_brightness={:d})".format(self._name, self._path, self._fd, self._max_brightness) 219 | -------------------------------------------------------------------------------- /periphery/led.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | 3 | class LEDError(IOError): ... 4 | 5 | class LED: 6 | def __init__(self, name: str, brightness: bool | int | None = ...) -> None: ... 7 | def __del__(self) -> None: ... 8 | def __enter__(self) -> LED: ... # noqa: Y034 9 | def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... 10 | def read(self) -> int: ... 11 | def write(self, brightness: bool | int) -> None: ... 12 | def close(self) -> None: ... 13 | @property 14 | def devpath(self) -> str: ... 15 | @property 16 | def fd(self) -> int: ... 17 | @property 18 | def name(self) -> str: ... 19 | @property 20 | def max_brightness(self) -> int: ... 21 | brightness: int 22 | -------------------------------------------------------------------------------- /periphery/mmio.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import mmap 4 | import ctypes 5 | 6 | 7 | # Alias long to int on Python 3 8 | if sys.version_info[0] >= 3: 9 | long = int 10 | 11 | 12 | class MMIOError(IOError): 13 | """Base class for MMIO errors.""" 14 | pass 15 | 16 | 17 | class MMIO(object): 18 | def __init__(self, physaddr, size, path="/dev/mem"): 19 | """Instantiate an MMIO object and map the region of physical memory 20 | specified by the `physaddr` base physical address and `size` size in 21 | bytes. The default memory character device "/dev/mem" can be overridden 22 | with the keyword argument `path`, for use with sandboxed memory 23 | character devices, e.g. "/dev/gpiomem". 24 | 25 | Args: 26 | physaddr (int, long): base physical address of memory region. 27 | size (int, long): size of memory region. 28 | path (str): memory character device path. 29 | 30 | Returns: 31 | MMIO: MMIO object. 32 | 33 | Raises: 34 | MMIOError: if an I/O or OS error occurs. 35 | TypeError: if `physaddr` or `size` types are invalid. 36 | 37 | """ 38 | self.mapping = None 39 | self._open(physaddr, size, path) 40 | 41 | def __del__(self): 42 | self.close() 43 | 44 | def __enter__(self): 45 | return self 46 | 47 | def __exit__(self, t, value, traceback): 48 | self.close() 49 | 50 | def _open(self, physaddr, size, path): 51 | if not isinstance(physaddr, (int, long)): 52 | raise TypeError("Invalid physaddr type, should be integer.") 53 | if not isinstance(size, (int, long)): 54 | raise TypeError("Invalid size type, should be integer.") 55 | 56 | pagesize = os.sysconf(os.sysconf_names['SC_PAGESIZE']) 57 | 58 | self._physaddr = physaddr 59 | self._size = size 60 | self._aligned_physaddr = physaddr - (physaddr % pagesize) 61 | self._aligned_size = size + (physaddr - self._aligned_physaddr) 62 | 63 | try: 64 | fd = os.open(path, os.O_RDWR | os.O_SYNC) 65 | except OSError as e: 66 | raise MMIOError(e.errno, "Opening {:s}: {:s}".format(path, e.strerror)) 67 | 68 | try: 69 | self.mapping = mmap.mmap(fd, self._aligned_size, flags=mmap.MAP_SHARED, prot=(mmap.PROT_READ | mmap.PROT_WRITE), offset=self._aligned_physaddr) 70 | except OSError as e: 71 | raise MMIOError(e.errno, "Mapping {:s}: {:s}".format(path, e.strerror)) 72 | 73 | try: 74 | os.close(fd) 75 | except OSError as e: 76 | raise MMIOError(e.errno, "Closing {:s}: {:s}".format(path, e.strerror)) 77 | 78 | # Methods 79 | 80 | def _adjust_offset(self, offset): 81 | return offset + (self._physaddr - self._aligned_physaddr) 82 | 83 | def _validate_offset(self, offset, length): 84 | if (offset + length) > self._aligned_size: 85 | raise ValueError("Offset out of bounds.") 86 | 87 | def read32(self, offset): 88 | """Read 32-bits from the specified `offset` in bytes, relative to the 89 | base physical address of the MMIO region. 90 | 91 | Args: 92 | offset (int, long): offset from base physical address, in bytes. 93 | 94 | Returns: 95 | int: 32-bit value read. 96 | 97 | Raises: 98 | TypeError: if `offset` type is invalid. 99 | ValueError: if `offset` is out of bounds. 100 | 101 | """ 102 | if not isinstance(offset, (int, long)): 103 | raise TypeError("Invalid offset type, should be integer.") 104 | 105 | offset = self._adjust_offset(offset) 106 | self._validate_offset(offset, 4) 107 | return ctypes.c_uint32.from_buffer(self.mapping, offset).value 108 | 109 | def read16(self, offset): 110 | """Read 16-bits from the specified `offset` in bytes, relative to the 111 | base physical address of the MMIO region. 112 | 113 | Args: 114 | offset (int, long): offset from base physical address, in bytes. 115 | 116 | Returns: 117 | int: 16-bit value read. 118 | 119 | Raises: 120 | TypeError: if `offset` type is invalid. 121 | ValueError: if `offset` is out of bounds. 122 | 123 | """ 124 | if not isinstance(offset, (int, long)): 125 | raise TypeError("Invalid offset type, should be integer.") 126 | 127 | offset = self._adjust_offset(offset) 128 | self._validate_offset(offset, 2) 129 | return ctypes.c_uint16.from_buffer(self.mapping, offset).value 130 | 131 | def read8(self, offset): 132 | """Read 8-bits from the specified `offset` in bytes, relative to the 133 | base physical address of the MMIO region. 134 | 135 | Args: 136 | offset (int, long): offset from base physical address, in bytes. 137 | 138 | Returns: 139 | int: 8-bit value read. 140 | 141 | Raises: 142 | TypeError: if `offset` type is invalid. 143 | ValueError: if `offset` is out of bounds. 144 | 145 | """ 146 | if not isinstance(offset, (int, long)): 147 | raise TypeError("Invalid offset type, should be integer.") 148 | 149 | offset = self._adjust_offset(offset) 150 | self._validate_offset(offset, 1) 151 | return ctypes.c_uint8.from_buffer(self.mapping, offset).value 152 | 153 | def read(self, offset, length): 154 | """Read a string of bytes from the specified `offset` in bytes, 155 | relative to the base physical address of the MMIO region. 156 | 157 | Args: 158 | offset (int, long): offset from base physical address, in bytes. 159 | length (int): number of bytes to read. 160 | 161 | Returns: 162 | bytes: bytes read. 163 | 164 | Raises: 165 | TypeError: if `offset` type is invalid. 166 | ValueError: if `offset` is out of bounds. 167 | 168 | """ 169 | if not isinstance(offset, (int, long)): 170 | raise TypeError("Invalid offset type, should be integer.") 171 | 172 | offset = self._adjust_offset(offset) 173 | self._validate_offset(offset, length) 174 | return bytes(self.mapping[offset:offset + length]) 175 | 176 | def write32(self, offset, value): 177 | """Write 32-bits to the specified `offset` in bytes, relative to the 178 | base physical address of the MMIO region. 179 | 180 | Args: 181 | offset (int, long): offset from base physical address, in bytes. 182 | value (int, long): 32-bit value to write. 183 | 184 | Raises: 185 | TypeError: if `offset` or `value` type are invalid. 186 | ValueError: if `offset` or `value` are out of bounds. 187 | 188 | """ 189 | if not isinstance(offset, (int, long)): 190 | raise TypeError("Invalid offset type, should be integer.") 191 | if not isinstance(value, (int, long)): 192 | raise TypeError("Invalid value type, should be integer.") 193 | if value < 0 or value > 0xffffffff: 194 | raise ValueError("Value out of bounds.") 195 | 196 | offset = self._adjust_offset(offset) 197 | self._validate_offset(offset, 4) 198 | ctypes.c_uint32.from_buffer(self.mapping, offset).value = value 199 | 200 | def write16(self, offset, value): 201 | """Write 16-bits to the specified `offset` in bytes, relative to the 202 | base physical address of the MMIO region. 203 | 204 | Args: 205 | offset (int, long): offset from base physical address, in bytes. 206 | value (int, long): 16-bit value to write. 207 | 208 | Raises: 209 | TypeError: if `offset` or `value` type are invalid. 210 | ValueError: if `offset` or `value` are out of bounds. 211 | 212 | """ 213 | if not isinstance(offset, (int, long)): 214 | raise TypeError("Invalid offset type, should be integer.") 215 | if not isinstance(value, (int, long)): 216 | raise TypeError("Invalid value type, should be integer.") 217 | if value < 0 or value > 0xffff: 218 | raise ValueError("Value out of bounds.") 219 | 220 | offset = self._adjust_offset(offset) 221 | self._validate_offset(offset, 2) 222 | ctypes.c_uint16.from_buffer(self.mapping, offset).value = value 223 | 224 | def write8(self, offset, value): 225 | """Write 8-bits to the specified `offset` in bytes, relative to the 226 | base physical address of the MMIO region. 227 | 228 | Args: 229 | offset (int, long): offset from base physical address, in bytes. 230 | value (int, long): 8-bit value to write. 231 | 232 | Raises: 233 | TypeError: if `offset` or `value` type are invalid. 234 | ValueError: if `offset` or `value` are out of bounds. 235 | 236 | """ 237 | if not isinstance(offset, (int, long)): 238 | raise TypeError("Invalid offset type, should be integer.") 239 | if not isinstance(value, (int, long)): 240 | raise TypeError("Invalid value type, should be integer.") 241 | if value < 0 or value > 0xff: 242 | raise ValueError("Value out of bounds.") 243 | 244 | offset = self._adjust_offset(offset) 245 | self._validate_offset(offset, 1) 246 | ctypes.c_uint8.from_buffer(self.mapping, offset).value = value 247 | 248 | def write(self, offset, data): 249 | """Write a string of bytes to the specified `offset` in bytes, relative 250 | to the base physical address of the MMIO region. 251 | 252 | Args: 253 | offset (int, long): offset from base physical address, in bytes. 254 | data (bytes, bytearray, list): a byte array or list of 8-bit 255 | integers to write. 256 | 257 | Raises: 258 | TypeError: if `offset` or `data` type are invalid. 259 | ValueError: if `offset` is out of bounds, or if data is not valid bytes. 260 | 261 | """ 262 | if not isinstance(offset, (int, long)): 263 | raise TypeError("Invalid offset type, should be integer.") 264 | if not isinstance(data, (bytes, bytearray, list)): 265 | raise TypeError("Invalid data type, expected bytes, bytearray, or list.") 266 | 267 | offset = self._adjust_offset(offset) 268 | self._validate_offset(offset, len(data)) 269 | 270 | data = bytes(bytearray(data)) 271 | self.mapping[offset:offset + len(data)] = data 272 | 273 | def close(self): 274 | """Unmap the MMIO object's mapped physical memory.""" 275 | if self.mapping is None: 276 | return 277 | 278 | self.mapping.close() 279 | self.mapping = None 280 | 281 | self._fd = None 282 | 283 | # Immutable properties 284 | 285 | @property 286 | def base(self): 287 | """Get the base physical address of the MMIO region. 288 | 289 | :type: int 290 | """ 291 | return self._physaddr 292 | 293 | @property 294 | def size(self): 295 | """Get the mapping size of the MMIO region. 296 | 297 | :type: int 298 | """ 299 | return self._size 300 | 301 | @property 302 | def pointer(self): 303 | """Get a ctypes void pointer to the memory mapped region. 304 | 305 | :type: ctypes.c_void_p 306 | """ 307 | return ctypes.cast(ctypes.pointer(ctypes.c_uint8.from_buffer(self.mapping, self._adjust_offset(0))), ctypes.c_void_p) 308 | 309 | # String representation 310 | 311 | def __str__(self): 312 | return "MMIO 0x{:08x} (size={:d})".format(self.base, self.size) 313 | -------------------------------------------------------------------------------- /periphery/mmio.pyi: -------------------------------------------------------------------------------- 1 | from ctypes import c_void_p 2 | from types import TracebackType 3 | 4 | class MMIOError(IOError): ... 5 | 6 | class MMIO: 7 | def __init__(self, physaddr: int, size: int, path: str = ...) -> None: ... 8 | def __del__(self) -> None: ... 9 | def __enter__(self) -> MMIO: ... # noqa: Y034 10 | def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... 11 | def read32(self, offset: int) -> int: ... 12 | def read16(self, offset: int) -> int: ... 13 | def read8(self, offset: int) -> int: ... 14 | def read(self, offset: int, length: int) -> bytes: ... 15 | def write32(self, offset: int, value: int) -> None: ... 16 | def write16(self, offset: int, value: int) -> None: ... 17 | def write8(self, offset: int, value: int) -> None: ... 18 | def write(self, offset: int, data: bytes | bytearray | list[int]) -> None: ... 19 | def close(self) -> None: ... 20 | @property 21 | def base(self) -> int: ... 22 | @property 23 | def size(self) -> int: ... 24 | @property 25 | def pointer(self) -> c_void_p: ... 26 | -------------------------------------------------------------------------------- /periphery/pwm.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import os 3 | import time 4 | 5 | 6 | class PWMError(IOError): 7 | """Base class for PWM errors.""" 8 | pass 9 | 10 | 11 | class PWM(object): 12 | # Number of retries to check for successful PWM export on open 13 | _PWM_STAT_RETRIES = 10 14 | # Delay between check for scucessful PWM export on open (100ms) 15 | _PWM_STAT_DELAY = 0.1 16 | 17 | def __init__(self, chip, channel): 18 | """Instantiate a PWM object and open the sysfs PWM corresponding to the 19 | specified chip and channel. 20 | 21 | Args: 22 | chip (int): PWM chip number. 23 | channel (int): PWM channel number. 24 | 25 | Returns: 26 | PWM: PWM object. 27 | 28 | Raises: 29 | PWMError: if an I/O or OS error occurs. 30 | TypeError: if `chip` or `channel` types are invalid. 31 | LookupError: if PWM chip does not exist. 32 | TimeoutError: if waiting for PWM export times out. 33 | 34 | """ 35 | self._chip = None 36 | self._channel = None 37 | self._path = None 38 | self._period_ns = None 39 | self._open(chip, channel) 40 | 41 | def __del__(self): 42 | self.close() 43 | 44 | def __enter__(self): 45 | return self 46 | 47 | def __exit__(self, t, value, traceback): 48 | self.close() 49 | 50 | def _open(self, chip, channel): 51 | if not isinstance(chip, int): 52 | raise TypeError("Invalid chip type, should be integer.") 53 | if not isinstance(channel, int): 54 | raise TypeError("Invalid channel type, should be integer.") 55 | 56 | chip_path = "/sys/class/pwm/pwmchip{}".format(chip) 57 | channel_path = "/sys/class/pwm/pwmchip{}/pwm{}".format(chip, channel) 58 | 59 | if not os.path.isdir(chip_path): 60 | raise LookupError("Opening PWM: PWM chip {} not found.".format(chip)) 61 | 62 | if not os.path.isdir(channel_path): 63 | # Export the PWM 64 | try: 65 | with open(os.path.join(chip_path, "export"), "w") as f_export: 66 | f_export.write("{:d}\n".format(channel)) 67 | except IOError as e: 68 | raise PWMError(e.errno, "Exporting PWM channel: " + e.strerror) 69 | 70 | # Loop until PWM is exported 71 | exported = False 72 | for i in range(PWM._PWM_STAT_RETRIES): 73 | if os.path.isdir(channel_path): 74 | exported = True 75 | break 76 | 77 | time.sleep(PWM._PWM_STAT_DELAY) 78 | 79 | if not exported: 80 | raise TimeoutError("Exporting PWM: waiting for \"{:s}\" timed out".format(channel_path)) 81 | 82 | # Loop until period is writable. This could take some time after 83 | # export as application of udev rules after export is asynchronous. 84 | for i in range(PWM._PWM_STAT_RETRIES): 85 | try: 86 | with open(os.path.join(channel_path, "period"), 'w'): 87 | break 88 | except IOError as e: 89 | if e.errno != errno.EACCES or (e.errno == errno.EACCES and i == PWM._PWM_STAT_RETRIES - 1): 90 | raise PWMError(e.errno, "Opening PWM period: " + e.strerror) 91 | 92 | time.sleep(PWM._PWM_STAT_DELAY) 93 | 94 | self._chip = chip 95 | self._channel = channel 96 | self._path = channel_path 97 | 98 | # Cache the period for fast duty cycle updates 99 | self._period_ns = self._get_period_ns() 100 | 101 | def close(self): 102 | """Close the PWM.""" 103 | 104 | if self._channel is not None: 105 | # Unexport the PWM channel 106 | try: 107 | unexport_fd = os.open("/sys/class/pwm/pwmchip{}/unexport".format(self._chip), os.O_WRONLY) 108 | os.write(unexport_fd, "{:d}\n".format(self._channel).encode()) 109 | os.close(unexport_fd) 110 | except OSError as e: 111 | raise PWMError(e.errno, "Unexporting PWM: " + e.strerror) 112 | 113 | self._chip = None 114 | self._channel = None 115 | 116 | def _write_channel_attr(self, attr, value): 117 | with open(os.path.join(self._path, attr), 'w') as f_attr: 118 | f_attr.write(value + "\n") 119 | 120 | def _read_channel_attr(self, attr): 121 | with open(os.path.join(self._path, attr), 'r') as f_attr: 122 | return f_attr.read().strip() 123 | 124 | # Methods 125 | 126 | def enable(self): 127 | """Enable the PWM output.""" 128 | self.enabled = True 129 | 130 | def disable(self): 131 | """Disable the PWM output.""" 132 | self.enabled = False 133 | 134 | # Immutable properties 135 | 136 | @property 137 | def devpath(self): 138 | """Get the device path of the underlying sysfs PWM device. 139 | 140 | :type: str 141 | """ 142 | return self._path 143 | 144 | @property 145 | def chip(self): 146 | """Get the PWM chip number. 147 | 148 | :type: int 149 | """ 150 | return self._chip 151 | 152 | @property 153 | def channel(self): 154 | """Get the PWM channel number. 155 | 156 | :type: int 157 | """ 158 | return self._channel 159 | 160 | # Mutable properties 161 | 162 | def _get_period_ns(self): 163 | period_ns_str = self._read_channel_attr("period") 164 | 165 | try: 166 | period_ns = int(period_ns_str) 167 | except ValueError: 168 | raise PWMError(None, "Unknown period value: \"{:s}\"".format(period_ns_str)) 169 | 170 | # Update our cached period 171 | self._period_ns = period_ns 172 | 173 | return period_ns 174 | 175 | def _set_period_ns(self, period_ns): 176 | if not isinstance(period_ns, int): 177 | raise TypeError("Invalid period type, should be int.") 178 | 179 | self._write_channel_attr("period", str(period_ns)) 180 | 181 | # Update our cached period 182 | self._period_ns = period_ns 183 | 184 | period_ns = property(_get_period_ns, _set_period_ns) 185 | """Get or set the PWM's output period in nanoseconds. 186 | 187 | Raises: 188 | PWMError: if an I/O or OS error occurs. 189 | TypeError: if value type is not int. 190 | 191 | :type: int 192 | """ 193 | 194 | def _get_duty_cycle_ns(self): 195 | duty_cycle_ns_str = self._read_channel_attr("duty_cycle") 196 | 197 | try: 198 | duty_cycle_ns = int(duty_cycle_ns_str) 199 | except ValueError: 200 | raise PWMError(None, "Unknown duty cycle value: \"{:s}\"".format(duty_cycle_ns_str)) 201 | 202 | return duty_cycle_ns 203 | 204 | def _set_duty_cycle_ns(self, duty_cycle_ns): 205 | if not isinstance(duty_cycle_ns, int): 206 | raise TypeError("Invalid duty cycle type, should be int.") 207 | 208 | self._write_channel_attr("duty_cycle", str(duty_cycle_ns)) 209 | 210 | duty_cycle_ns = property(_get_duty_cycle_ns, _set_duty_cycle_ns) 211 | """Get or set the PWM's output duty cycle in nanoseconds. 212 | 213 | Raises: 214 | PWMError: if an I/O or OS error occurs. 215 | TypeError: if value type is not int. 216 | 217 | :type: int 218 | """ 219 | 220 | def _get_period(self): 221 | return float(self.period_ns) / 1e9 222 | 223 | def _set_period(self, period): 224 | if not isinstance(period, (int, float)): 225 | raise TypeError("Invalid period type, should be int or float.") 226 | 227 | # Convert period from seconds to integer nanoseconds 228 | self.period_ns = int(period * 1e9) 229 | 230 | period = property(_get_period, _set_period) 231 | """Get or set the PWM's output period in seconds. 232 | 233 | Raises: 234 | PWMError: if an I/O or OS error occurs. 235 | TypeError: if value type is not int or float. 236 | 237 | :type: int, float 238 | """ 239 | 240 | def _get_duty_cycle(self): 241 | return float(self.duty_cycle_ns) / self._period_ns 242 | 243 | def _set_duty_cycle(self, duty_cycle): 244 | if not isinstance(duty_cycle, (int, float)): 245 | raise TypeError("Invalid duty cycle type, should be int or float.") 246 | elif not 0.0 <= duty_cycle <= 1.0: 247 | raise ValueError("Invalid duty cycle value, should be between 0.0 and 1.0.") 248 | 249 | # Convert duty cycle from ratio to nanoseconds 250 | self.duty_cycle_ns = int(duty_cycle * self._period_ns) 251 | 252 | duty_cycle = property(_get_duty_cycle, _set_duty_cycle) 253 | """Get or set the PWM's output duty cycle as a ratio from 0.0 to 1.0. 254 | 255 | Raises: 256 | PWMError: if an I/O or OS error occurs. 257 | TypeError: if value type is not int or float. 258 | ValueError: if value is out of bounds of 0.0 to 1.0. 259 | 260 | :type: int, float 261 | """ 262 | 263 | def _get_frequency(self): 264 | return 1.0 / self.period 265 | 266 | def _set_frequency(self, frequency): 267 | if not isinstance(frequency, (int, float)): 268 | raise TypeError("Invalid frequency type, should be int or float.") 269 | 270 | self.period = 1.0 / frequency 271 | 272 | frequency = property(_get_frequency, _set_frequency) 273 | """Get or set the PWM's output frequency in Hertz. 274 | 275 | Raises: 276 | PWMError: if an I/O or OS error occurs. 277 | TypeError: if value type is not int or float. 278 | 279 | :type: int, float 280 | """ 281 | 282 | def _get_polarity(self): 283 | return self._read_channel_attr("polarity") 284 | 285 | def _set_polarity(self, polarity): 286 | if not isinstance(polarity, str): 287 | raise TypeError("Invalid polarity type, should be str.") 288 | elif polarity.lower() not in ["normal", "inversed"]: 289 | raise ValueError("Invalid polarity, can be: \"normal\" or \"inversed\".") 290 | 291 | self._write_channel_attr("polarity", polarity.lower()) 292 | 293 | polarity = property(_get_polarity, _set_polarity) 294 | """Get or set the PWM's output polarity. Can be "normal" or "inversed". 295 | 296 | Raises: 297 | PWMError: if an I/O or OS error occurs. 298 | TypeError: if value type is not str. 299 | ValueError: if value is invalid. 300 | 301 | :type: str 302 | """ 303 | 304 | def _get_enabled(self): 305 | enabled = self._read_channel_attr("enable") 306 | 307 | if enabled == "1": 308 | return True 309 | elif enabled == "0": 310 | return False 311 | 312 | raise PWMError(None, "Unknown enabled value: \"{:s}\"".format(enabled)) 313 | 314 | def _set_enabled(self, value): 315 | if not isinstance(value, bool): 316 | raise TypeError("Invalid enabled type, should be bool.") 317 | 318 | self._write_channel_attr("enable", "1" if value else "0") 319 | 320 | enabled = property(_get_enabled, _set_enabled) 321 | """Get or set the PWM's output enabled state. 322 | 323 | Raises: 324 | PWMError: if an I/O or OS error occurs. 325 | TypeError: if value type is not bool. 326 | 327 | :type: bool 328 | """ 329 | 330 | # String representation 331 | 332 | def __str__(self): 333 | return "PWM {:d}, chip {:d} (period={:f} sec, duty_cycle={:f}%, polarity={:s}, enabled={:s})" \ 334 | .format(self._channel, self._chip, self.period, self.duty_cycle * 100, self.polarity, str(self.enabled)) 335 | -------------------------------------------------------------------------------- /periphery/pwm.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | 3 | class PWMError(IOError): ... 4 | 5 | class PWM: 6 | def __init__(self, chip: int, channel: int) -> None: ... 7 | def __del__(self) -> None: ... 8 | def __enter__(self) -> PWM: ... # noqa: Y034 9 | def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... 10 | def close(self) -> None: ... 11 | enabled: bool 12 | def enable(self) -> None: ... 13 | def disable(self) -> None: ... 14 | @property 15 | def devpath(self) -> str: ... 16 | @property 17 | def chip(self) -> int: ... 18 | @property 19 | def channel(self) -> int: ... 20 | period_ns: int 21 | duty_cycle_ns: int 22 | period: int | float 23 | duty_cycle: int | float 24 | frequency: int | float 25 | polarity: str 26 | -------------------------------------------------------------------------------- /periphery/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeev/python-periphery/c3b9d0f81144c241c77a353902473e73000e2c27/periphery/py.typed -------------------------------------------------------------------------------- /periphery/serial.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | 3 | class SerialError(IOError): ... 4 | 5 | class Serial: 6 | def __init__( 7 | self, 8 | devpath: str, 9 | baudrate: int, 10 | databits: int = ..., 11 | parity: str = ..., 12 | stopbits: int = ..., 13 | xonxoff: bool = ..., 14 | rtscts: bool = ..., 15 | ) -> None: ... 16 | def __del__(self) -> None: ... 17 | def __enter__(self) -> Serial: ... # noqa: Y034 18 | def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... 19 | def read(self, length: int, timeout: float | None = ...) -> bytes: ... 20 | def write(self, data: bytes | bytearray | list[int]) -> int: ... 21 | def poll(self, timeout: float | None = ...) -> bool: ... 22 | def flush(self) -> None: ... 23 | def input_waiting(self) -> int: ... 24 | def output_waiting(self) -> int: ... 25 | def close(self) -> None: ... 26 | @property 27 | def fd(self) -> int: ... 28 | @property 29 | def devpath(self) -> str: ... 30 | baudrate: int 31 | databits: int 32 | parity: str 33 | stopbits: int 34 | xonxoff: bool 35 | rtscts: bool 36 | vmin: int 37 | vtime: float 38 | -------------------------------------------------------------------------------- /periphery/spi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fcntl 3 | import array 4 | import ctypes 5 | import platform 6 | 7 | 8 | try: 9 | KERNEL_VERSION = tuple([int(s) for s in platform.release().split(".")[:2]]) 10 | except ValueError: 11 | KERNEL_VERSION = (0, 0) 12 | 13 | 14 | class SPIError(IOError): 15 | """Base class for SPI errors.""" 16 | pass 17 | 18 | 19 | class _CSpiIocTransfer(ctypes.Structure): 20 | _fields_ = [ 21 | ('tx_buf', ctypes.c_ulonglong), 22 | ('rx_buf', ctypes.c_ulonglong), 23 | ('len', ctypes.c_uint), 24 | ('speed_hz', ctypes.c_uint), 25 | ('delay_usecs', ctypes.c_ushort), 26 | ('bits_per_word', ctypes.c_ubyte), 27 | ('cs_change', ctypes.c_ubyte), 28 | ('tx_nbits', ctypes.c_ubyte), 29 | ('rx_nbits', ctypes.c_ubyte), 30 | ('pad', ctypes.c_ushort), 31 | ] 32 | 33 | 34 | class SPI(object): 35 | # Constants scraped from 36 | _SPI_CPHA = 0x1 37 | _SPI_CPOL = 0x2 38 | _SPI_LSB_FIRST = 0x8 39 | _SPI_IOC_WR_MODE = 0x40016b01 40 | _SPI_IOC_RD_MODE = 0x80016b01 41 | _SPI_IOC_WR_MODE32 = 0x40046b05 42 | _SPI_IOC_RD_MODE32 = 0x80046b05 43 | _SPI_IOC_WR_MAX_SPEED_HZ = 0x40046b04 44 | _SPI_IOC_RD_MAX_SPEED_HZ = 0x80046b04 45 | _SPI_IOC_WR_BITS_PER_WORD = 0x40016b03 46 | _SPI_IOC_RD_BITS_PER_WORD = 0x80016b03 47 | _SPI_IOC_MESSAGE_1 = 0x40206b00 48 | 49 | _SUPPORTS_MODE32 = KERNEL_VERSION >= (3, 15) 50 | 51 | def __init__(self, devpath, mode, max_speed, bit_order="msb", bits_per_word=8, extra_flags=0): 52 | """Instantiate a SPI object and open the spidev device at the specified 53 | path with the specified SPI mode, max speed in hertz, and the defaults 54 | of "msb" bit order and 8 bits per word. 55 | 56 | Args: 57 | devpath (str): spidev device path. 58 | mode (int): SPI mode, can be 0, 1, 2, 3. 59 | max_speed (int, float): maximum speed in Hertz. 60 | bit_order (str): bit order, can be "msb" or "lsb". 61 | bits_per_word (int): bits per word. 62 | extra_flags (int): extra spidev flags to be bitwise-ORed with the SPI mode. 63 | 64 | Returns: 65 | SPI: SPI object. 66 | 67 | Raises: 68 | SPIError: if an I/O or OS error occurs. 69 | TypeError: if `devpath`, `mode`, `max_speed`, `bit_order`, `bits_per_word`, or `extra_flags` types are invalid. 70 | ValueError: if `mode`, `bit_order`, `bits_per_word`, or `extra_flags` values are invalid. 71 | 72 | """ 73 | self._fd = None 74 | self._devpath = None 75 | self._open(devpath, mode, max_speed, bit_order, bits_per_word, extra_flags) 76 | 77 | def __del__(self): 78 | self.close() 79 | 80 | def __enter__(self): 81 | return self 82 | 83 | def __exit__(self, t, value, traceback): 84 | self.close() 85 | 86 | def _open(self, devpath, mode, max_speed, bit_order, bits_per_word, extra_flags): 87 | if not isinstance(devpath, str): 88 | raise TypeError("Invalid devpath type, should be string.") 89 | elif not isinstance(mode, int): 90 | raise TypeError("Invalid mode type, should be integer.") 91 | elif not isinstance(max_speed, (int, float)): 92 | raise TypeError("Invalid max_speed type, should be integer or float.") 93 | elif not isinstance(bit_order, str): 94 | raise TypeError("Invalid bit_order type, should be string.") 95 | elif not isinstance(bits_per_word, int): 96 | raise TypeError("Invalid bits_per_word type, should be integer.") 97 | elif not isinstance(extra_flags, int): 98 | raise TypeError("Invalid extra_flags type, should be integer.") 99 | 100 | if mode not in [0, 1, 2, 3]: 101 | raise ValueError("Invalid mode, can be 0, 1, 2, 3.") 102 | elif bit_order.lower() not in ["msb", "lsb"]: 103 | raise ValueError("Invalid bit_order, can be \"msb\" or \"lsb\".") 104 | elif bits_per_word < 0 or bits_per_word > 255: 105 | raise ValueError("Invalid bits_per_word, must be 0-255.") 106 | 107 | # Open spidev 108 | try: 109 | self._fd = os.open(devpath, os.O_RDWR) 110 | except OSError as e: 111 | raise SPIError(e.errno, "Opening SPI device: " + e.strerror) 112 | 113 | self._devpath = devpath 114 | 115 | bit_order = bit_order.lower() 116 | 117 | # Set mode, bit order, extra flags 118 | if extra_flags > 0xff: 119 | if not SPI._SUPPORTS_MODE32: 120 | raise SPIError(None, "32-bit mode configuration not supported by kernel version {}.{}.".format(*KERNEL_VERSION)) 121 | 122 | # Use 32-bit mode if extra flags is wider than 8-bits 123 | buf = array.array("I", [mode | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) | extra_flags]) 124 | try: 125 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE32, buf, False) 126 | except (OSError, IOError) as e: 127 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 128 | else: 129 | # Prefer 8-bit mode for compatibility with older kernels 130 | buf = array.array("B", [mode | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) | extra_flags]) 131 | try: 132 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) 133 | except (OSError, IOError) as e: 134 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 135 | 136 | # Set max speed 137 | buf = array.array("I", [int(max_speed)]) 138 | try: 139 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MAX_SPEED_HZ, buf, False) 140 | except (OSError, IOError) as e: 141 | raise SPIError(e.errno, "Setting SPI max speed: " + e.strerror) 142 | 143 | # Set bits per word 144 | buf = array.array("B", [bits_per_word]) 145 | try: 146 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_BITS_PER_WORD, buf, False) 147 | except (OSError, IOError) as e: 148 | raise SPIError(e.errno, "Setting SPI bits per word: " + e.strerror) 149 | 150 | # Methods 151 | 152 | def transfer(self, data): 153 | """Shift out `data` and return shifted in data. 154 | 155 | Args: 156 | data (bytes, bytearray, list): a byte array or list of 8-bit integers to shift out. 157 | 158 | Returns: 159 | bytes, bytearray, list: data shifted in. 160 | 161 | Raises: 162 | SPIError: if an I/O or OS error occurs. 163 | TypeError: if `data` type is invalid. 164 | ValueError: if data is not valid bytes. 165 | 166 | """ 167 | if not isinstance(data, (bytes, bytearray, list)): 168 | raise TypeError("Invalid data type, should be bytes, bytearray, or list.") 169 | 170 | # Create mutable array 171 | try: 172 | buf = array.array('B', data) 173 | except OverflowError: 174 | raise ValueError("Invalid data bytes.") 175 | 176 | buf_addr, buf_len = buf.buffer_info() 177 | 178 | # Prepare transfer structure 179 | spi_xfer = _CSpiIocTransfer() 180 | spi_xfer.tx_buf = buf_addr 181 | spi_xfer.rx_buf = buf_addr 182 | spi_xfer.len = buf_len 183 | 184 | # Transfer 185 | try: 186 | fcntl.ioctl(self._fd, SPI._SPI_IOC_MESSAGE_1, spi_xfer) 187 | except (OSError, IOError) as e: 188 | raise SPIError(e.errno, "SPI transfer: " + e.strerror) 189 | 190 | # Return shifted out data with the same type as shifted in data 191 | if isinstance(data, bytes): 192 | return bytes(bytearray(buf)) 193 | elif isinstance(data, bytearray): 194 | return bytearray(buf) 195 | elif isinstance(data, list): 196 | return buf.tolist() 197 | 198 | def close(self): 199 | """Close the spidev SPI device. 200 | 201 | Raises: 202 | SPIError: if an I/O or OS error occurs. 203 | 204 | """ 205 | if self._fd is None: 206 | return 207 | 208 | try: 209 | os.close(self._fd) 210 | except OSError as e: 211 | raise SPIError(e.errno, "Closing SPI device: " + e.strerror) 212 | 213 | self._fd = None 214 | 215 | # Immutable properties 216 | 217 | @property 218 | def fd(self): 219 | """Get the file descriptor of the underlying spidev device. 220 | 221 | :type: int 222 | """ 223 | return self._fd 224 | 225 | @property 226 | def devpath(self): 227 | """Get the device path of the underlying spidev device. 228 | 229 | :type: str 230 | """ 231 | return self._devpath 232 | 233 | # Mutable properties 234 | 235 | def _get_mode(self): 236 | buf = array.array('B', [0]) 237 | 238 | # Get mode 239 | try: 240 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 241 | except (OSError, IOError) as e: 242 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 243 | 244 | return buf[0] & 0x3 245 | 246 | def _set_mode(self, mode): 247 | if not isinstance(mode, int): 248 | raise TypeError("Invalid mode type, should be integer.") 249 | if mode not in [0, 1, 2, 3]: 250 | raise ValueError("Invalid mode, can be 0, 1, 2, 3.") 251 | 252 | # Read-modify-write mode, because the mode contains bits for other settings 253 | 254 | # Get mode 255 | buf = array.array('B', [0]) 256 | try: 257 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 258 | except (OSError, IOError) as e: 259 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 260 | 261 | buf[0] = (buf[0] & ~(SPI._SPI_CPOL | SPI._SPI_CPHA)) | mode 262 | 263 | # Set mode 264 | try: 265 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) 266 | except (OSError, IOError) as e: 267 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 268 | 269 | mode = property(_get_mode, _set_mode) 270 | """Get or set the SPI mode. Can be 0, 1, 2, 3. 271 | 272 | Raises: 273 | SPIError: if an I/O or OS error occurs. 274 | TypeError: if `mode` type is not int. 275 | ValueError: if `mode` value is invalid. 276 | 277 | :type: int 278 | """ 279 | 280 | def _get_max_speed(self): 281 | # Get max speed 282 | buf = array.array('I', [0]) 283 | try: 284 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MAX_SPEED_HZ, buf, True) 285 | except (OSError, IOError) as e: 286 | raise SPIError(e.errno, "Getting SPI max speed: " + e.strerror) 287 | 288 | return buf[0] 289 | 290 | def _set_max_speed(self, max_speed): 291 | if not isinstance(max_speed, (int, float)): 292 | raise TypeError("Invalid max_speed type, should be integer or float.") 293 | 294 | # Set max speed 295 | buf = array.array('I', [int(max_speed)]) 296 | try: 297 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MAX_SPEED_HZ, buf, False) 298 | except (OSError, IOError) as e: 299 | raise SPIError(e.errno, "Setting SPI max speed: " + e.strerror) 300 | 301 | max_speed = property(_get_max_speed, _set_max_speed) 302 | """Get or set the maximum speed in Hertz. 303 | 304 | Raises: 305 | SPIError: if an I/O or OS error occurs. 306 | TypeError: if `max_speed` type is not int or float. 307 | 308 | :type: int, float 309 | """ 310 | 311 | def _get_bit_order(self): 312 | # Get mode 313 | buf = array.array('B', [0]) 314 | try: 315 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 316 | except (OSError, IOError) as e: 317 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 318 | 319 | if (buf[0] & SPI._SPI_LSB_FIRST) > 0: 320 | return "lsb" 321 | 322 | return "msb" 323 | 324 | def _set_bit_order(self, bit_order): 325 | if not isinstance(bit_order, str): 326 | raise TypeError("Invalid bit_order type, should be string.") 327 | elif bit_order.lower() not in ["msb", "lsb"]: 328 | raise ValueError("Invalid bit_order, can be \"msb\" or \"lsb\".") 329 | 330 | # Read-modify-write mode, because the mode contains bits for other settings 331 | 332 | # Get mode 333 | buf = array.array('B', [0]) 334 | try: 335 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 336 | except (OSError, IOError) as e: 337 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 338 | 339 | bit_order = bit_order.lower() 340 | buf[0] = (buf[0] & ~SPI._SPI_LSB_FIRST) | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) 341 | 342 | # Set mode 343 | try: 344 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) 345 | except (OSError, IOError) as e: 346 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 347 | 348 | bit_order = property(_get_bit_order, _set_bit_order) 349 | """Get or set the SPI bit order. Can be "msb" or "lsb". 350 | 351 | Raises: 352 | SPIError: if an I/O or OS error occurs. 353 | TypeError: if `bit_order` type is not str. 354 | ValueError: if `bit_order` value is invalid. 355 | 356 | :type: str 357 | """ 358 | 359 | def _get_bits_per_word(self): 360 | # Get bits per word 361 | buf = array.array('B', [0]) 362 | try: 363 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_BITS_PER_WORD, buf, True) 364 | except (OSError, IOError) as e: 365 | raise SPIError(e.errno, "Getting SPI bits per word: " + e.strerror) 366 | 367 | return buf[0] 368 | 369 | def _set_bits_per_word(self, bits_per_word): 370 | if not isinstance(bits_per_word, int): 371 | raise TypeError("Invalid bits_per_word type, should be integer.") 372 | if bits_per_word < 0 or bits_per_word > 255: 373 | raise ValueError("Invalid bits_per_word, must be 0-255.") 374 | 375 | # Set bits per word 376 | buf = array.array('B', [bits_per_word]) 377 | try: 378 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_BITS_PER_WORD, buf, False) 379 | except (OSError, IOError) as e: 380 | raise SPIError(e.errno, "Setting SPI bits per word: " + e.strerror) 381 | 382 | bits_per_word = property(_get_bits_per_word, _set_bits_per_word) 383 | """Get or set the SPI bits per word. 384 | 385 | Raises: 386 | SPIError: if an I/O or OS error occurs. 387 | TypeError: if `bits_per_word` type is not int. 388 | ValueError: if `bits_per_word` value is invalid. 389 | 390 | :type: int 391 | """ 392 | 393 | def _get_extra_flags(self): 394 | if SPI._SUPPORTS_MODE32: 395 | buf = array.array('I', [0]) 396 | rd_cmd = SPI._SPI_IOC_RD_MODE32 397 | else: 398 | buf = array.array('B', [0]) 399 | rd_cmd = SPI._SPI_IOC_RD_MODE 400 | 401 | try: 402 | fcntl.ioctl(self._fd, rd_cmd, buf, True) 403 | except (OSError, IOError) as e: 404 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 405 | 406 | return buf[0] & ~(SPI._SPI_LSB_FIRST | SPI._SPI_CPHA | SPI._SPI_CPOL) 407 | 408 | def _set_extra_flags(self, extra_flags): 409 | if not isinstance(extra_flags, int): 410 | raise TypeError("Invalid extra_flags type, should be integer.") 411 | 412 | # Read-modify-write mode, because the mode contains bits for other settings 413 | 414 | if extra_flags > 0xff: 415 | if not SPI._SUPPORTS_MODE32: 416 | raise SPIError(None, "32-bit mode configuration not supported by kernel version {}.{}.".format(*KERNEL_VERSION)) 417 | 418 | buf = array.array('I', [0]) 419 | rd_cmd = SPI._SPI_IOC_RD_MODE32 420 | wr_cmd = SPI._SPI_IOC_WR_MODE32 421 | else: 422 | buf = array.array('B', [0]) 423 | rd_cmd = SPI._SPI_IOC_RD_MODE 424 | wr_cmd = SPI._SPI_IOC_WR_MODE 425 | 426 | # Get mode 427 | try: 428 | fcntl.ioctl(self._fd, rd_cmd, buf, True) 429 | except (OSError, IOError) as e: 430 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 431 | 432 | buf[0] = (buf[0] & (SPI._SPI_LSB_FIRST | SPI._SPI_CPHA | SPI._SPI_CPOL)) | extra_flags 433 | 434 | # Set mode 435 | try: 436 | fcntl.ioctl(self._fd, wr_cmd, buf, False) 437 | except (OSError, IOError) as e: 438 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 439 | 440 | extra_flags = property(_get_extra_flags, _set_extra_flags) 441 | """Get or set the spidev extra flags. Extra flags are bitwise-ORed with the SPI mode. 442 | 443 | Raises: 444 | SPIError: if an I/O or OS error occurs. 445 | TypeError: if `extra_flags` type is not int. 446 | ValueError: if `extra_flags` value is invalid. 447 | 448 | :type: int 449 | """ 450 | 451 | # String representation 452 | 453 | def __str__(self): 454 | return "SPI (device={:s}, fd={:d}, mode={:d}, max_speed={:d}, bit_order={:s}, bits_per_word={:d}, extra_flags=0x{:08x})" \ 455 | .format(self.devpath, self.fd, self.mode, self.max_speed, self.bit_order, self.bits_per_word, self.extra_flags) 456 | -------------------------------------------------------------------------------- /periphery/spi.pyi: -------------------------------------------------------------------------------- 1 | from types import TracebackType 2 | 3 | KERNEL_VERSION: tuple[int, int] 4 | 5 | class SPIError(IOError): ... 6 | 7 | class SPI: 8 | def __init__( 9 | self, devpath: str, mode: int, max_speed: float, bit_order: str = ..., bits_per_word: int = ..., extra_flags: int = ... 10 | ) -> None: ... 11 | def __del__(self) -> None: ... 12 | def __enter__(self) -> SPI: ... # noqa: Y034 13 | def __exit__(self, t: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None) -> None: ... 14 | def transfer(self, data: bytes | bytearray | list[int]) -> bytes | bytearray | list[int]: ... 15 | def close(self) -> None: ... 16 | @property 17 | def fd(self) -> int: ... 18 | @property 19 | def devpath(self) -> str: ... 20 | mode: int 21 | max_speed: float 22 | bit_order: str 23 | bits_per_word: int 24 | extra_flags: int 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | license_file = LICENSE 4 | 5 | [bdist_wheel] 6 | universal = 1 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | setup( 7 | name='python-periphery', 8 | version='2.4.1', 9 | description='A pure Python 2/3 library for peripheral I/O (GPIO, LED, PWM, SPI, I2C, MMIO, Serial) in Linux.', 10 | author='vsergeev', 11 | author_email='v@sergeev.io', 12 | url='https://github.com/vsergeev/python-periphery', 13 | packages=['periphery'], 14 | package_data={'periphery': ['*.pyi', 'py.typed']}, 15 | 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.""", 16 | classifiers=[ 17 | 'Development Status :: 5 - Production/Stable', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: POSIX :: Linux', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 2', 22 | 'Programming Language :: Python :: 3', 23 | 'Programming Language :: Python :: Implementation :: CPython', 24 | 'Topic :: Software Development :: Libraries :: Python Modules', 25 | 'Topic :: Software Development :: Embedded Systems', 26 | 'Topic :: System :: Hardware', 27 | 'Topic :: System :: Hardware :: Hardware Drivers', 28 | ], 29 | license='MIT', 30 | keywords='gpio spi led pwm i2c mmio serial uart embedded linux beaglebone raspberrypi rpi odroid', 31 | ) 32 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vsergeev/python-periphery/c3b9d0f81144c241c77a353902473e73000e2c27/tests/__init__.py -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | STR_OKAY = " [\x1b[1;32m OK \x1b[0m]" 5 | STR_FAIL = " [\x1b[1;31mFAIL\x1b[0m]" 6 | 7 | 8 | def ptest(): 9 | frame = inspect.stack()[1] 10 | function, lineno = frame[3], frame[2] 11 | print("\n\nStarting test {:s}():{:d}".format(function, lineno)) 12 | 13 | 14 | def pokay(msg): 15 | print("{:s} {:s}".format(STR_OKAY, msg)) 16 | 17 | 18 | def passert(name, condition): 19 | frame = inspect.stack()[1] 20 | filename, function, lineno = frame[1].split('/')[-1], frame[3], frame[2] 21 | print("{:s} {:s} {:s}():{:d} {:s}".format(STR_OKAY if condition else STR_FAIL, filename, function, lineno, name)) 22 | assert condition 23 | 24 | 25 | class AssertRaises(object): 26 | def __init__(self, name, exception_type): 27 | self.name = name 28 | self.exception_type = exception_type 29 | 30 | def __enter__(self): 31 | return self 32 | 33 | def __exit__(self, t, value, traceback): 34 | frame = inspect.stack()[1] 35 | filename, function, lineno = frame[1].split('/')[-1], frame[3], frame[2] 36 | 37 | if not isinstance(value, self.exception_type): 38 | print("{:s} {:s} {:s}():{:d} {:s}".format(STR_FAIL, filename, function, lineno, self.name)) 39 | return False 40 | 41 | print("{:s} {:s} {:s}():{:d} {:s}".format(STR_OKAY, filename, function, lineno, self.name)) 42 | return True 43 | -------------------------------------------------------------------------------- /tests/test_gpio.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import threading 4 | import time 5 | 6 | import periphery 7 | from .test import ptest, pokay, passert, AssertRaises 8 | 9 | if sys.version_info[0] == 3: 10 | raw_input = input 11 | import queue 12 | else: 13 | import Queue as queue 14 | 15 | 16 | path = None 17 | line_input = None 18 | line_output = None 19 | 20 | 21 | def test_arguments(): 22 | ptest() 23 | 24 | # Invalid open types 25 | with AssertRaises("invalid open types", TypeError): 26 | periphery.GPIO(1, 1, "in") 27 | with AssertRaises("invalid open types", TypeError): 28 | periphery.GPIO("abc", 2.3, "in") 29 | with AssertRaises("invalid open types", TypeError): 30 | periphery.GPIO("abc", 1, 1) 31 | # Invalid direction 32 | with AssertRaises("invalid direction", ValueError): 33 | periphery.GPIO("abc", 1, "blah") 34 | 35 | 36 | def test_open_close(): 37 | ptest() 38 | 39 | # Open non-existent GPIO (export should fail with EINVAL) 40 | with AssertRaises("non-existent GPIO", periphery.GPIOError): 41 | periphery.GPIO(path, 9999, "in") 42 | 43 | # Open legitimate GPIO 44 | gpio = periphery.GPIO(path, line_output, "in") 45 | passert("property line", gpio.line == line_output) 46 | passert("direction is in", gpio.direction == "in") 47 | passert("fd >= 0", gpio.fd >= 0) 48 | passert("chip_fd >= 0", gpio.chip_fd >= 0) 49 | 50 | # Check default label 51 | passert("property label", gpio.label == "periphery") 52 | 53 | # Set invalid direction 54 | with AssertRaises("set invalid direction", ValueError): 55 | gpio.direction = "blah" 56 | # Set invalid edge 57 | with AssertRaises("set invalid edge", ValueError): 58 | gpio.edge = "blah" 59 | # Set invalid bias 60 | with AssertRaises("set invalid bias", ValueError): 61 | gpio.bias = "blah" 62 | # Set invalid drive 63 | with AssertRaises("set invalid drive", ValueError): 64 | gpio.drive = "blah" 65 | 66 | # Set direction out, check direction out, check value low 67 | gpio.direction = "out" 68 | passert("direction is out", gpio.direction == "out") 69 | passert("value is low", gpio.read() == False) 70 | # Set direction low, check direction out, check value low 71 | gpio.direction = "low" 72 | passert("direction is out", gpio.direction == "out") 73 | passert("value is low", gpio.read() == False) 74 | # Set direction high, check direction out, check value high 75 | gpio.direction = "high" 76 | passert("direction is out", gpio.direction == "out") 77 | passert("value is high", gpio.read() == True) 78 | 79 | # Set drive open drain, check drive open drain 80 | gpio.drive = "open_drain" 81 | passert("drive is open drain", gpio.drive == "open_drain") 82 | # Set drive open source, check drive open source 83 | gpio.drive = "open_source" 84 | passert("drive is open drain", gpio.drive == "open_source") 85 | # Set drive default, check drive default 86 | gpio.drive = "default" 87 | passert("drive is default", gpio.drive == "default") 88 | 89 | # Set inverted true, check inverted true 90 | gpio.inverted = True 91 | passert("inverted is True", gpio.inverted == True) 92 | # Set inverted false, check inverted false 93 | gpio.inverted = False 94 | passert("inverted is False", gpio.inverted == False) 95 | 96 | # Attempt to set interrupt edge on output GPIO 97 | with AssertRaises("set interrupt edge on output GPIO", periphery.GPIOError): 98 | gpio.edge = "rising" 99 | # Attempt to read event on output GPIO 100 | with AssertRaises("read event on output GPIO", periphery.GPIOError): 101 | gpio.read_event() 102 | 103 | # Set direction in, check direction in 104 | gpio.direction = "in" 105 | passert("direction is in", gpio.direction == "in") 106 | 107 | # Set edge none, check edge none 108 | gpio.edge = "none" 109 | passert("edge is none", gpio.edge == "none") 110 | # Set edge rising, check edge rising 111 | gpio.edge = "rising" 112 | passert("edge is rising", gpio.edge == "rising") 113 | # Set edge falling, check edge falling 114 | gpio.edge = "falling" 115 | passert("edge is falling", gpio.edge == "falling") 116 | # Set edge both, check edge both 117 | gpio.edge = "both" 118 | passert("edge is both", gpio.edge == "both") 119 | # Set edge none, check edge none 120 | gpio.edge = "none" 121 | passert("edge is none", gpio.edge == "none") 122 | 123 | # Set bias pull up, check bias pull up 124 | gpio.bias = "pull_up" 125 | passert("bias is pull up", gpio.bias == "pull_up") 126 | # Set bias pull down, check bias pull down 127 | gpio.bias = "pull_down" 128 | passert("bias is pull down", gpio.bias == "pull_down") 129 | # Set bias disable, check bias disable 130 | gpio.bias = "disable" 131 | passert("bias is disable", gpio.bias == "disable") 132 | # Set bias default, check bias default 133 | gpio.bias = "default" 134 | passert("bias is default", gpio.bias == "default") 135 | 136 | # Attempt to set drive on input GPIO 137 | with AssertRaises("set drive on input GPIO", periphery.GPIOError): 138 | gpio.drive = "open_drain" 139 | 140 | gpio.close() 141 | 142 | # Open with keyword arguments 143 | gpio = periphery.GPIO(path, line_input, "in", edge="rising", bias="default", drive="default", inverted=False, label="test123") 144 | passert("property line", gpio.line == line_input) 145 | passert("direction is in", gpio.direction == "in") 146 | passert("fd >= 0", gpio.fd >= 0) 147 | passert("chip_fd >= 0", gpio.chip_fd >= 0) 148 | passert("edge is rising", gpio.edge == "rising") 149 | passert("bias is default", gpio.bias == "default") 150 | passert("drive is default", gpio.drive == "default") 151 | passert("inverted is False", gpio.inverted == False) 152 | passert("label is test123", gpio.label == "test123") 153 | 154 | gpio.close() 155 | 156 | 157 | def test_loopback(): 158 | ptest() 159 | 160 | # Open in and out lines 161 | gpio_in = periphery.GPIO(path, line_input, "in") 162 | gpio_out = periphery.GPIO(path, line_output, "out") 163 | 164 | # Drive out low, check in low 165 | print("Drive out low, check in low") 166 | gpio_out.write(False) 167 | passert("value is False", gpio_in.read() == False) 168 | 169 | # Drive out high, check in high 170 | print("Drive out high, check in high") 171 | gpio_out.write(True) 172 | passert("value is True", gpio_in.read() == True) 173 | 174 | # Wrapper for running poll() in a thread 175 | def threaded_poll(gpio, timeout): 176 | ret = queue.Queue() 177 | 178 | def f(): 179 | ret.put(gpio.poll(timeout)) 180 | 181 | thread = threading.Thread(target=f) 182 | thread.start() 183 | return ret 184 | 185 | # Check poll falling 1 -> 0 interrupt 186 | print("Check poll falling 1 -> 0 interrupt") 187 | gpio_in.edge = "falling" 188 | poll_ret = threaded_poll(gpio_in, 5) 189 | time.sleep(0.5) 190 | gpio_out.write(False) 191 | passert("gpio_in polled True", poll_ret.get() == True) 192 | passert("value is low", gpio_in.read() == False) 193 | event = gpio_in.read_event() 194 | passert("event edge is falling", event.edge == "falling") 195 | passert("event timestamp is non-zero", event.timestamp != 0) 196 | 197 | # Check poll rising 0 -> 1 interrupt 198 | print("Check poll rising 0 -> 1 interrupt") 199 | gpio_in.edge = "rising" 200 | poll_ret = threaded_poll(gpio_in, 5) 201 | time.sleep(0.5) 202 | gpio_out.write(True) 203 | passert("gpin_in polled True", poll_ret.get() == True) 204 | passert("value is high", gpio_in.read() == True) 205 | event = gpio_in.read_event() 206 | passert("event edge is rising", event.edge == "rising") 207 | passert("event timestamp is non-zero", event.timestamp != 0) 208 | 209 | # Set edge to both 210 | gpio_in.edge = "both" 211 | 212 | # Check poll falling 1 -> 0 interrupt 213 | print("Check poll falling 1 -> 0 interrupt") 214 | poll_ret = threaded_poll(gpio_in, 5) 215 | time.sleep(0.5) 216 | gpio_out.write(False) 217 | passert("gpio_in polled True", poll_ret.get() == True) 218 | passert("value is low", gpio_in.read() == False) 219 | event = gpio_in.read_event() 220 | passert("event edge is falling", event.edge == "falling") 221 | passert("event timestamp is non-zero", event.timestamp != 0) 222 | 223 | # Check poll rising 0 -> 1 interrupt 224 | print("Check poll rising 0 -> 1 interrupt") 225 | poll_ret = threaded_poll(gpio_in, 5) 226 | time.sleep(0.5) 227 | gpio_out.write(True) 228 | passert("gpio_in polled True", poll_ret.get() == True) 229 | passert("value is high", gpio_in.read() == True) 230 | event = gpio_in.read_event() 231 | passert("event edge is rising", event.edge == "rising") 232 | passert("event timestamp is non-zero", event.timestamp != 0) 233 | 234 | # Check poll timeout 235 | print("Check poll timeout") 236 | passert("gpio_in polled False", gpio_in.poll(1) == False) 237 | 238 | # Check poll falling 1 -> 0 interrupt with the poll_multiple() API 239 | print("Check poll falling 1 -> 0 interrupt with poll_multiple()") 240 | gpio_out.write(False) 241 | gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) 242 | passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) 243 | passert("value is low", gpio_in.read() == False) 244 | event = gpio_in.read_event() 245 | passert("event edge is falling", event.edge == "falling") 246 | passert("event timestamp is non-zero", event.timestamp != 0) 247 | 248 | # Check poll rising 0 -> 1 interrupt with the poll_multiple() API 249 | print("Check poll rising 0 -> 1 interrupt with poll_multiple()") 250 | gpio_out.write(True) 251 | gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) 252 | passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) 253 | passert("value is high", gpio_in.read() == True) 254 | event = gpio_in.read_event() 255 | passert("event edge is rising", event.edge == "rising") 256 | passert("event timestamp is non-zero", event.timestamp != 0) 257 | 258 | # Check poll timeout 259 | print("Check poll timeout with poll_multiple()") 260 | gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) 261 | passert("gpios ready is empty", gpios_ready == []) 262 | 263 | gpio_in.close() 264 | gpio_out.close() 265 | 266 | # Open both GPIOs as inputs 267 | gpio_in = periphery.GPIO(path, line_input, "in") 268 | gpio_out = periphery.GPIO(path, line_output, "in") 269 | 270 | # Set bias pull-up, check value is high 271 | print("Check input GPIO reads high with pull-up bias") 272 | gpio_in.bias = "pull_up" 273 | time.sleep(0.1) 274 | passert("value is high", gpio_in.read() == True) 275 | 276 | # Set bias pull-down, check value is low 277 | print("Check input GPIO reads low with pull-down bias") 278 | gpio_in.bias = "pull_down" 279 | time.sleep(0.1) 280 | passert("value is low", gpio_in.read() == False) 281 | 282 | gpio_in.close() 283 | gpio_out.close() 284 | 285 | 286 | def test_interactive(): 287 | print("Starting interactive test...") 288 | 289 | gpio = periphery.GPIO(path, line_output, "out") 290 | 291 | print("Starting interactive test. Get out your multimeter, buddy!") 292 | raw_input("Press enter to continue...") 293 | 294 | # Check tostring 295 | print("GPIO description: {}".format(str(gpio))) 296 | passert("interactive success", raw_input("GPIO description looks ok? y/n ") == "y") 297 | 298 | # Drive GPIO out low 299 | gpio.write(False) 300 | passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") 301 | 302 | # Drive GPIO out high 303 | gpio.write(True) 304 | passert("interactive success", raw_input("GPIO out is high? y/n ") == "y") 305 | 306 | # Drive GPIO out low 307 | gpio.write(False) 308 | passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") 309 | 310 | gpio.close() 311 | 312 | 313 | if __name__ == "__main__": 314 | if os.environ.get("CI") == "true": 315 | test_arguments() 316 | sys.exit(0) 317 | 318 | if len(sys.argv) < 3: 319 | print("Usage: python -m tests.test_gpio ") 320 | print("") 321 | print("[1/4] Argument test: No requirements.") 322 | print("[2/4] Open/close test: GPIO #2 should be real.") 323 | print("[3/4] Loopback test: GPIOs #1 and #2 should be connected with a wire.") 324 | print("[4/4] Interactive test: GPIO #2 should be observed with a multimeter.") 325 | print("") 326 | print("Hint: for Raspberry Pi 3,") 327 | print("Use GPIO 17 (header pin 11) and GPIO 27 (header pin 13),") 328 | print("connect a loopback between them, and run this test with:") 329 | print(" python -m tests.test_gpio /dev/gpiochip0 17 27") 330 | print("") 331 | sys.exit(1) 332 | 333 | path = sys.argv[1] 334 | line_input = int(sys.argv[2]) 335 | line_output = int(sys.argv[3]) 336 | 337 | test_arguments() 338 | pokay("Arguments test passed.") 339 | test_open_close() 340 | pokay("Open/close test passed.") 341 | test_loopback() 342 | pokay("Loopback test passed.") 343 | test_interactive() 344 | pokay("Interactive test passed.") 345 | 346 | pokay("All tests passed!") 347 | -------------------------------------------------------------------------------- /tests/test_gpio_sysfs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import threading 4 | import time 5 | 6 | import periphery 7 | from .test import ptest, pokay, passert, AssertRaises 8 | 9 | if sys.version_info[0] == 3: 10 | raw_input = input 11 | import queue 12 | else: 13 | import Queue as queue 14 | 15 | 16 | line_input = None 17 | line_output = None 18 | 19 | 20 | def test_arguments(): 21 | ptest() 22 | 23 | # Invalid open types 24 | with AssertRaises("invalid open types", TypeError): 25 | periphery.GPIO("abc", "out") 26 | with AssertRaises("invalid open types", TypeError): 27 | periphery.GPIO(100, 100) 28 | # Invalid direction 29 | with AssertRaises("invalid direction", ValueError): 30 | periphery.GPIO(100, "blah") 31 | 32 | 33 | 34 | def test_open_close(): 35 | ptest() 36 | 37 | # Open non-existent GPIO (export should fail with EINVAL) 38 | with AssertRaises("non-existent GPIO", periphery.GPIOError): 39 | periphery.GPIO(9999, "in") 40 | 41 | # Open legitimate GPIO 42 | gpio = periphery.GPIO(line_output, "in") 43 | passert("property line", gpio.line == line_output) 44 | passert("direction is in", gpio.direction == "in") 45 | passert("fd >= 0", gpio.fd >= 0) 46 | 47 | # Set invalid direction 48 | with AssertRaises("set invalid direction", ValueError): 49 | gpio.direction = "blah" 50 | # Set invalid edge 51 | with AssertRaises("set invalid edge", ValueError): 52 | gpio.edge = "blah" 53 | # Unsupported property bias 54 | with AssertRaises("unsupported property bias", NotImplementedError): 55 | _ = gpio.bias 56 | with AssertRaises("unsupported property bias", NotImplementedError): 57 | gpio.bias = "pull_up" 58 | # Unsupported property drive 59 | with AssertRaises("unsupported property drive", NotImplementedError): 60 | _ = gpio.drive 61 | with AssertRaises("unsupported property drive", NotImplementedError): 62 | gpio.drive = "open_drain" 63 | # Unsupported proprety 64 | with AssertRaises("unsupported property chip_fd", NotImplementedError): 65 | _ = gpio.chip_fd 66 | # Unsupported method 67 | with AssertRaises("unsupported method", NotImplementedError): 68 | gpio.read_event() 69 | 70 | # Set direction out, check direction out, check value low 71 | gpio.direction = "out" 72 | passert("direction is out", gpio.direction == "out") 73 | passert("value is low", gpio.read() == False) 74 | # Set direction low, check direction out, check value low 75 | gpio.direction = "low" 76 | passert("direction is out", gpio.direction == "out") 77 | passert("value is low", gpio.read() == False) 78 | # Set direction high, check direction out, check value high 79 | gpio.direction = "high" 80 | passert("direction is out", gpio.direction == "out") 81 | passert("ivalue is high", gpio.read() == True) 82 | 83 | # Set inverted true, check inverted 84 | gpio.inverted = True 85 | passert("inverted is True", gpio.inverted == True) 86 | # Set inverted false, check inverted 87 | gpio.inverted = False 88 | passert("inverted is False", gpio.inverted == False) 89 | 90 | # Set direction in, check direction in 91 | gpio.direction = "in" 92 | passert("direction is in", gpio.direction == "in") 93 | 94 | # Set edge none, check edge none 95 | gpio.edge = "none" 96 | passert("edge is none", gpio.edge == "none") 97 | # Set edge rising, check edge rising 98 | gpio.edge = "rising" 99 | passert("edge is rising", gpio.edge == "rising") 100 | # Set edge falling, check edge falling 101 | gpio.edge = "falling" 102 | passert("edge is falling", gpio.edge == "falling") 103 | # Set edge both, check edge both 104 | gpio.edge = "both" 105 | passert("edge is both", gpio.edge == "both") 106 | # Set edge none, check edge none 107 | gpio.edge = "none" 108 | passert("edge is none", gpio.edge == "none") 109 | 110 | gpio.close() 111 | 112 | 113 | def test_loopback(): 114 | ptest() 115 | 116 | # Open in and out lines 117 | gpio_in = periphery.GPIO(line_input, "in") 118 | gpio_out = periphery.GPIO(line_output, "out") 119 | 120 | # Drive out low, check in low 121 | print("Drive out low, check in low") 122 | gpio_out.write(False) 123 | passert("value is low", gpio_in.read() == False) 124 | 125 | # Drive out high, check in high 126 | print("Drive out high, check in high") 127 | gpio_out.write(True) 128 | passert("value is high", gpio_in.read() == True) 129 | 130 | # Wrapper for running poll() in a thread 131 | def threaded_poll(gpio, timeout): 132 | ret = queue.Queue() 133 | 134 | def f(): 135 | ret.put(gpio.poll(timeout)) 136 | 137 | thread = threading.Thread(target=f) 138 | thread.start() 139 | return ret 140 | 141 | # Check poll falling 1 -> 0 interrupt 142 | print("Check poll falling 1 -> 0 interrupt") 143 | gpio_in.edge = "falling" 144 | poll_ret = threaded_poll(gpio_in, 5) 145 | time.sleep(0.5) 146 | gpio_out.write(False) 147 | passert("gpio_in polled True", poll_ret.get() == True) 148 | passert("value is low", gpio_in.read() == False) 149 | 150 | # Check poll rising 0 -> 1 interrupt 151 | print("Check poll rising 0 -> 1 interrupt") 152 | gpio_in.edge = "rising" 153 | poll_ret = threaded_poll(gpio_in, 5) 154 | time.sleep(0.5) 155 | gpio_out.write(True) 156 | passert("gpio_in polled True", poll_ret.get() == True) 157 | passert("value is high", gpio_in.read() == True) 158 | 159 | # Set edge to both 160 | gpio_in.edge = "both" 161 | 162 | # Check poll falling 1 -> 0 interrupt 163 | print("Check poll falling 1 -> 0 interrupt") 164 | poll_ret = threaded_poll(gpio_in, 5) 165 | time.sleep(0.5) 166 | gpio_out.write(False) 167 | passert("gpio_in polled True", poll_ret.get() == True) 168 | passert("value is low", gpio_in.read() == False) 169 | 170 | # Check poll rising 0 -> 1 interrupt 171 | print("Check poll rising 0 -> 1 interrupt") 172 | poll_ret = threaded_poll(gpio_in, 5) 173 | time.sleep(0.5) 174 | gpio_out.write(True) 175 | passert("gpio_in polled True", poll_ret.get() == True) 176 | passert("value is high", gpio_in.read() == True) 177 | 178 | # Check poll timeout 179 | print("Check poll timeout") 180 | passert("gpio_in polled False", gpio_in.poll(1) == False) 181 | 182 | # Check poll falling 1 -> 0 interrupt with the poll_multiple() API 183 | print("Check poll falling 1 -> 0 interrupt with poll_multiple()") 184 | gpio_out.write(False) 185 | gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) 186 | passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) 187 | passert("value is low", gpio_in.read() == False) 188 | 189 | # Check poll rising 0 -> 1 interrupt with the poll_multiple() API 190 | print("Check poll rising 0 -> 1 interrupt with poll_multiple()") 191 | gpio_out.write(True) 192 | gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) 193 | passert("gpios ready is gpio_in", gpios_ready == [gpio_in]) 194 | passert("value is high", gpio_in.read() == True) 195 | 196 | # Check poll timeout 197 | print("Check poll timeout with poll_multiple()") 198 | gpios_ready = periphery.GPIO.poll_multiple([gpio_in], 1) 199 | passert("gpios ready is empty", gpios_ready == []) 200 | 201 | gpio_in.close() 202 | gpio_out.close() 203 | 204 | 205 | def test_interactive(): 206 | ptest() 207 | 208 | gpio = periphery.GPIO(line_output, "out") 209 | 210 | print("Starting interactive test. Get out your multimeter, buddy!") 211 | raw_input("Press enter to continue...") 212 | 213 | # Check tostring 214 | print("GPIO description: {}".format(str(gpio))) 215 | passert("interactive success", raw_input("GPIO description looks ok? y/n ") == "y") 216 | 217 | # Drive GPIO out low 218 | gpio.write(False) 219 | passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") 220 | 221 | # Drive GPIO out high 222 | gpio.write(True) 223 | passert("interactive success", raw_input("GPIO out is high? y/n ") == "y") 224 | 225 | # Drive GPIO out low 226 | gpio.write(False) 227 | passert("interactive success", raw_input("GPIO out is low? y/n ") == "y") 228 | 229 | gpio.close() 230 | 231 | 232 | if __name__ == "__main__": 233 | if os.environ.get("CI") == "true": 234 | test_arguments() 235 | sys.exit(0) 236 | 237 | if len(sys.argv) < 3: 238 | print("Usage: python -m tests.test_gpio ") 239 | print("") 240 | print("[1/4] Argument test: No requirements.") 241 | print("[2/4] Open/close test: GPIO #2 should be real.") 242 | print("[3/4] Loopback test: GPIOs #1 and #2 should be connected with a wire.") 243 | print("[4/4] Interactive test: GPIO #2 should be observed with a multimeter.") 244 | print("") 245 | print("Hint: for Raspberry Pi 3,") 246 | print("Use GPIO 17 (header pin 11) and GPIO 27 (header pin 13),") 247 | print("connect a loopback between them, and run this test with:") 248 | print(" python -m tests.test_gpio_sysfs 17 27") 249 | print("") 250 | sys.exit(1) 251 | 252 | line_input = int(sys.argv[1]) 253 | line_output = int(sys.argv[2]) 254 | 255 | test_arguments() 256 | pokay("Arguments test passed.") 257 | test_open_close() 258 | pokay("Open/close test passed.") 259 | test_loopback() 260 | pokay("Loopback test passed.") 261 | test_interactive() 262 | pokay("Interactive test passed.") 263 | 264 | pokay("All tests passed!") 265 | -------------------------------------------------------------------------------- /tests/test_i2c.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import periphery 5 | from .test import ptest, pokay, passert, AssertRaises 6 | 7 | if sys.version_info[0] == 3: 8 | raw_input = input 9 | 10 | 11 | i2c_devpath = None 12 | 13 | 14 | def test_arguments(): 15 | ptest() 16 | 17 | # Open with invalid type 18 | with AssertRaises("open invalid type", TypeError): 19 | periphery.I2C(123) 20 | 21 | 22 | def test_open_close(): 23 | ptest() 24 | 25 | # Open non-existent device 26 | with AssertRaises("non-existent device", periphery.I2CError): 27 | periphery.I2C("/foo/bar") 28 | 29 | # Open legitimate device 30 | i2c = periphery.I2C(i2c_devpath) 31 | passert("fd >= 0", i2c.fd >= 0) 32 | 33 | # Close I2C 34 | i2c.close() 35 | 36 | 37 | def test_loopback(): 38 | ptest() 39 | # No general way to do a loopback test for I2C without a real component, skipping... 40 | 41 | 42 | def test_interactive(): 43 | ptest() 44 | 45 | # Open device 2 46 | i2c = periphery.I2C(i2c_devpath) 47 | 48 | print("") 49 | print("Starting interactive test. Get out your logic analyzer, buddy!") 50 | raw_input("Press enter to continue...") 51 | 52 | # Check tostring 53 | print("I2C description: {}".format(str(i2c))) 54 | passert("interactive success", raw_input("I2C description looks ok? y/n ") == "y") 55 | 56 | # There isn't much we can do without assuming a device on the other end, 57 | # because I2C needs an acknowledgement bit on each transferred byte. 58 | # 59 | # But we can send a transaction and expect it to time out. 60 | 61 | # S [ 0x7a W ] [0xaa] [0xbb] [0xcc] [0xdd] NA 62 | messages = [periphery.I2C.Message([0xaa, 0xbb, 0xcc, 0xdd])] 63 | 64 | raw_input("Press enter to start transfer...") 65 | 66 | # Transfer to non-existent device 67 | with AssertRaises("transfer to non-existent device", periphery.I2CError): 68 | i2c.transfer(0x7a, messages) 69 | 70 | i2c.close() 71 | 72 | success = raw_input("I2C transfer occurred? y/n ") 73 | passert("interactive success", success == "y") 74 | 75 | 76 | if __name__ == "__main__": 77 | if os.environ.get("CI") == "true": 78 | test_arguments() 79 | sys.exit(0) 80 | 81 | if len(sys.argv) < 2: 82 | print("Usage: python -m tests.test_i2c ") 83 | print("") 84 | print("[1/4] Arguments test: No requirements.") 85 | print("[2/4] Open/close test: I2C device should be real.") 86 | print("[3/4] Loopback test: No test.") 87 | print("[4/4] Interactive test: I2C bus should be observed with an oscilloscope or logic analyzer.") 88 | print("") 89 | print("Hint: for Raspberry Pi 3, enable I2C1 with:") 90 | print(" $ echo \"dtparam=i2c_arm=on\" | sudo tee -a /boot/config.txt") 91 | print(" $ sudo reboot") 92 | print("Use pins I2C1 SDA (header pin 2) and I2C1 SCL (header pin 3),") 93 | print("and run this test with:") 94 | print(" python -m tests.test_i2c /dev/i2c-1") 95 | print("") 96 | sys.exit(1) 97 | 98 | i2c_devpath = sys.argv[1] 99 | 100 | test_arguments() 101 | pokay("Arguments test passed.") 102 | test_open_close() 103 | pokay("Open/close test passed.") 104 | test_loopback() 105 | pokay("Loopback test passed.") 106 | test_interactive() 107 | pokay("Interactive test passed.") 108 | 109 | pokay("All tests passed!") 110 | -------------------------------------------------------------------------------- /tests/test_led.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | import periphery 6 | from .test import ptest, pokay, passert, AssertRaises 7 | 8 | if sys.version_info[0] == 3: 9 | raw_input = input 10 | 11 | 12 | led_name = None 13 | 14 | 15 | def test_arguments(): 16 | ptest() 17 | 18 | # Invalid open types 19 | with AssertRaises("invalid open types", TypeError): 20 | periphery.LED("abc", "out") 21 | with AssertRaises("invalid open types", TypeError): 22 | periphery.LED(100, 100) 23 | 24 | 25 | def test_open_close(): 26 | ptest() 27 | 28 | # Open non-existent LED 29 | with AssertRaises("non-existent led", LookupError): 30 | periphery.LED("invalid_led_XXX", 0) 31 | 32 | # Open legitimate LED 33 | led = periphery.LED(led_name, 0) 34 | passert("property name", led.name == led_name) 35 | passert("fd >= 0", led.fd >= 0) 36 | passert("max_brightness > 0", led.max_brightness > 0) 37 | 38 | # Set brightness to True, check brightness 39 | led.write(True) 40 | time.sleep(0.01) 41 | passert("brightness is max", led.read() == led.max_brightness) 42 | 43 | # Set brightness to False, check brightness 44 | led.write(False) 45 | time.sleep(0.01) 46 | passert("brightness is zero", led.read() == 0) 47 | 48 | # Set brightness to 1, check brightness 49 | led.write(1) 50 | time.sleep(0.01) 51 | passert("brightness is non-zero", led.read() >= 1) 52 | 53 | # Set brightness to 0, check brightness 54 | led.write(0) 55 | time.sleep(0.01) 56 | passert("brightness is zero", led.read() == 0) 57 | 58 | # Set brightness to 1, check brightness 59 | led.brightness = 1 60 | time.sleep(0.01) 61 | passert("brightness is non-zero", led.brightness >= 1) 62 | 63 | # Set brightness to 0, check brightness 64 | led.brightness = 0 65 | time.sleep(0.01) 66 | passert("brightness is zero", led.brightness == 0) 67 | 68 | led.close() 69 | 70 | 71 | def test_loopback(): 72 | ptest() 73 | # No general way to do a loopback test for I2C without a real component, skipping... 74 | 75 | 76 | def test_interactive(): 77 | ptest() 78 | 79 | led = periphery.LED(led_name, False) 80 | 81 | raw_input("Press enter to continue...") 82 | 83 | # Check tostring 84 | print("LED description: {}".format(str(led))) 85 | passert("interactive success", raw_input("LED description looks ok? y/n ") == "y") 86 | 87 | # Turn LED off 88 | led.write(False) 89 | passert("interactive success", raw_input("LED is off? y/n ") == "y") 90 | 91 | # Turn LED on 92 | led.write(True) 93 | passert("interactive success", raw_input("LED is on? y/n ") == "y") 94 | 95 | # Turn LED off 96 | led.write(False) 97 | passert("interactive success", raw_input("LED is off? y/n ") == "y") 98 | 99 | # Turn LED on 100 | led.write(True) 101 | passert("interactive success", raw_input("LED is on? y/n ") == "y") 102 | 103 | led.close() 104 | 105 | 106 | if __name__ == "__main__": 107 | if os.environ.get("CI") == "true": 108 | test_arguments() 109 | sys.exit(0) 110 | 111 | if len(sys.argv) < 2: 112 | print("Usage: python -m tests.test_led ") 113 | print("") 114 | print("[1/4] Arguments test: No requirements.") 115 | print("[2/4] Open/close test: LED should be real.") 116 | print("[3/4] Loopback test: No test.") 117 | print("[4/4] Interactive test: LED should be observed.") 118 | print("") 119 | print("Hint: for Raspberry Pi 3, disable triggers for led1:") 120 | print(" $ echo none > /sys/class/leds/led1/trigger") 121 | print("Observe led1 (red power LED), and run this test:") 122 | print(" python -m tests.test_led led1") 123 | print("") 124 | sys.exit(1) 125 | 126 | led_name = sys.argv[1] 127 | 128 | test_arguments() 129 | pokay("Arguments test passed.") 130 | test_open_close() 131 | pokay("Open/close test passed.") 132 | test_loopback() 133 | pokay("Loopback test passed.") 134 | test_interactive() 135 | pokay("Interactive test passed.") 136 | 137 | pokay("All tests passed!") 138 | -------------------------------------------------------------------------------- /tests/test_mmio.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | import periphery 6 | from .test import ptest, pokay, passert, AssertRaises 7 | 8 | if sys.version_info[0] == 3: 9 | raw_input = input 10 | 11 | 12 | PAGE_SIZE = 4096 13 | CONTROL_MODULE_BASE = 0x44e10000 14 | USB_VID_PID_OFFSET = 0x7f4 15 | USB_VID_PID = 0x04516141 16 | RTCSS_BASE = 0x44e3e000 17 | RTC_SCRATCH2_REG_OFFSET = 0x68 18 | RTC_KICK0R_REG_OFFSET = 0x6C 19 | RTC_KICK1R_REG_OFFSET = 0x70 20 | 21 | 22 | def test_arguments(): 23 | ptest() 24 | 25 | 26 | def test_open_close(): 27 | ptest() 28 | 29 | # Open aligned base 30 | mmio = periphery.MMIO(CONTROL_MODULE_BASE, PAGE_SIZE) 31 | # Check properties 32 | passert("property base", mmio.base == CONTROL_MODULE_BASE) 33 | passert("property size", mmio.size == PAGE_SIZE) 34 | # Try to write immutable properties 35 | with AssertRaises("write immutable", AttributeError): 36 | mmio.base = 1000 37 | with AssertRaises("write immutable", AttributeError): 38 | mmio.size = 1000 39 | mmio.close() 40 | 41 | # Open unaligned base 42 | mmio = periphery.MMIO(CONTROL_MODULE_BASE + 123, PAGE_SIZE) 43 | # Check properties 44 | passert("property base", mmio.base == CONTROL_MODULE_BASE + 123) 45 | passert("property size", mmio.size == PAGE_SIZE) 46 | # Read out of bounds 47 | with AssertRaises("read 1 byte over", ValueError): 48 | mmio.read32(PAGE_SIZE - 3) 49 | with AssertRaises("read 2 bytes over", ValueError): 50 | mmio.read32(PAGE_SIZE - 2) 51 | with AssertRaises("read 3 bytes over", ValueError): 52 | mmio.read32(PAGE_SIZE - 1) 53 | with AssertRaises("read 4 bytes over", ValueError): 54 | mmio.read32(PAGE_SIZE) 55 | mmio.close() 56 | 57 | 58 | def test_loopback(): 59 | ptest() 60 | 61 | # Open control module 62 | mmio = periphery.MMIO(CONTROL_MODULE_BASE, PAGE_SIZE) 63 | 64 | # Read and compare USB VID/PID with read32() 65 | passert("compare USB VID/PID", mmio.read32(USB_VID_PID_OFFSET) == USB_VID_PID) 66 | # Read and compare USB VID/PID with bytes read 67 | data = mmio.read(USB_VID_PID_OFFSET, 4) 68 | data = bytearray(data) 69 | passert("compare byte 1", data[0] == USB_VID_PID & 0xff) 70 | passert("compare byte 2", data[1] == (USB_VID_PID >> 8) & 0xff) 71 | passert("compare byte 3", data[2] == (USB_VID_PID >> 16) & 0xff) 72 | passert("compare byte 4", data[3] == (USB_VID_PID >> 24) & 0xff) 73 | 74 | mmio.close() 75 | 76 | # Open RTC subsystem 77 | mmio = periphery.MMIO(RTCSS_BASE, PAGE_SIZE) 78 | 79 | # Disable write protection 80 | mmio.write32(RTC_KICK0R_REG_OFFSET, 0x83E70B13) 81 | mmio.write32(RTC_KICK1R_REG_OFFSET, 0x95A4F1E0) 82 | 83 | # Write/Read RTC Scratch2 Register 84 | mmio.write32(RTC_SCRATCH2_REG_OFFSET, 0xdeadbeef) 85 | passert("compare write 32-bit uint and readback", mmio.read32(RTC_SCRATCH2_REG_OFFSET) == 0xdeadbeef) 86 | 87 | # Write/Read RTC Scratch2 Register with bytes write 88 | mmio.write(RTC_SCRATCH2_REG_OFFSET, b"\xaa\xbb\xcc\xdd") 89 | data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) 90 | passert("compare write 4-byte bytes and readback", data == b"\xaa\xbb\xcc\xdd") 91 | 92 | # Write/Read RTC Scratch2 Register with bytearray write 93 | mmio.write(RTC_SCRATCH2_REG_OFFSET, bytearray(b"\xbb\xcc\xdd\xee")) 94 | data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) 95 | passert("compare write 4-byte bytearray and readback", data == b"\xbb\xcc\xdd\xee") 96 | 97 | # Write/Read RTC Scratch2 Register with list write 98 | mmio.write(RTC_SCRATCH2_REG_OFFSET, [0xcc, 0xdd, 0xee, 0xff]) 99 | data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) 100 | passert("compare write 4-byte list and readback", data == b"\xcc\xdd\xee\xff") 101 | 102 | # Write/Read RTC Scratch2 Register with 16-bit write 103 | mmio.write16(RTC_SCRATCH2_REG_OFFSET, 0xaabb) 104 | passert("compare write 16-bit uint and readback", mmio.read16(RTC_SCRATCH2_REG_OFFSET) == 0xaabb) 105 | 106 | # Write/Read RTC Scratch2 Register with 8-bit write 107 | mmio.write8(RTC_SCRATCH2_REG_OFFSET, 0xab) 108 | passert("compare write 8-bit uint and readback", mmio.read8(RTC_SCRATCH2_REG_OFFSET) == 0xab) 109 | 110 | mmio.close() 111 | 112 | 113 | def test_interactive(): 114 | ptest() 115 | 116 | mmio = periphery.MMIO(RTCSS_BASE, PAGE_SIZE) 117 | 118 | # Check tostring 119 | print("MMIO description: {}".format(str(mmio))) 120 | passert("interactive success", raw_input("MMIO description looks ok? y/n ") == "y") 121 | 122 | print("Waiting for seconds ones digit to reset to 0...\n") 123 | 124 | # Wait until seconds low go to 0, so we don't have to deal with 125 | # overflows in comparing times 126 | tic = time.time() 127 | while mmio.read32(0x00) & 0xf != 0: 128 | periphery.sleep(1) 129 | passert("less than 12 seconds elapsed", (time.time() - tic) < 12) 130 | 131 | # Compare passage of OS time with RTC time 132 | 133 | tic = time.time() 134 | rtc_tic = mmio.read32(0x00) & 0xf 135 | 136 | bcd2dec = lambda x: 10 * ((x >> 4) & 0xf) + (x & 0xf) 137 | 138 | print("Date: {:04d}-{:02d}-{:02d}".format(2000 + bcd2dec(mmio.read32(0x14)), bcd2dec(mmio.read32(0x10)), bcd2dec(mmio.read32(0x0c)))) 139 | print("Time: {:02d}:{:02d}:{:02d}".format(bcd2dec(mmio.read32(0x08) & 0x7f), bcd2dec(mmio.read32(0x04)), bcd2dec(mmio.read32(0x00)))) 140 | 141 | periphery.sleep(3) 142 | 143 | print("Date: {:04d}-{:02d}-{:02d}".format(2000 + bcd2dec(mmio.read32(0x14)), bcd2dec(mmio.read32(0x10)), bcd2dec(mmio.read32(0x0c)))) 144 | print("Time: {:02d}:{:02d}:{:02d}".format(bcd2dec(mmio.read32(0x08) & 0x7f), bcd2dec(mmio.read32(0x04)), bcd2dec(mmio.read32(0x00)))) 145 | 146 | toc = time.time() 147 | rtc_toc = mmio.read32(0x00) & 0xf 148 | 149 | passert("real time elapsed", (toc - tic) > 2) 150 | passert("rtc time elapsed", (rtc_toc - rtc_tic) > 2) 151 | 152 | mmio.close() 153 | 154 | 155 | if __name__ == "__main__": 156 | if os.environ.get("CI") == "true": 157 | test_arguments() 158 | sys.exit(0) 159 | 160 | print("WARNING: This test suite assumes a BeagleBone Black (AM335x) host!") 161 | print("Other systems may experience unintended and dire consequences!") 162 | raw_input("Press enter to continue!") 163 | 164 | test_arguments() 165 | pokay("Arguments test passed.") 166 | test_open_close() 167 | pokay("Open/close test passed.") 168 | test_loopback() 169 | pokay("Loopback test passed.") 170 | test_interactive() 171 | pokay("Interactive test passed.") 172 | 173 | pokay("All tests passed!") 174 | -------------------------------------------------------------------------------- /tests/test_pwm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import periphery 5 | from .test import ptest, pokay, passert, AssertRaises 6 | 7 | if sys.version_info[0] == 3: 8 | raw_input = input 9 | 10 | 11 | pwm_chip = None 12 | pwm_channel = None 13 | 14 | 15 | def test_arguments(): 16 | ptest() 17 | 18 | # Invalid open types 19 | with AssertRaises("invalid open types", TypeError): 20 | periphery.PWM("foo", 0) 21 | with AssertRaises("invalid open types", TypeError): 22 | periphery.PWM(0, "foo") 23 | 24 | 25 | 26 | def test_open_close(): 27 | ptest() 28 | 29 | # Open non-existent PWM chip 30 | with AssertRaises("non-existent PWM chip", LookupError): 31 | periphery.PWM(9999, pwm_channel) 32 | 33 | # Open non-existent PWM channel 34 | with AssertRaises("non-existent PWM channel", periphery.PWMError): 35 | periphery.PWM(pwm_chip, 9999) 36 | 37 | # Open legitimate PWM chip/channel 38 | pwm = periphery.PWM(pwm_chip, pwm_channel) 39 | passert("property chip", pwm.chip == pwm_chip) 40 | passert("property channel", pwm.channel == pwm_channel) 41 | 42 | # Initialize period and duty cycle 43 | pwm.period = 5e-3 44 | pwm.duty_cycle = 0 45 | 46 | # Set period, check period, check period_ns, check frequency 47 | pwm.period = 1e-3 48 | passert("period is correct", abs(pwm.period - 1e-3) < 1e-4) 49 | passert("period_ns is correct", abs(pwm.period_ns - 1000000) < 1e5) 50 | passert("frequency is correct", abs(pwm.frequency - 1000) < 100) 51 | pwm.period = 5e-4 52 | passert("period is correct", abs(pwm.period - 5e-4) < 1e-5) 53 | passert("period_ns is correct", abs(pwm.period_ns - 500000) < 1e4) 54 | passert("frequency is correct", abs(pwm.frequency - 2000) < 100) 55 | 56 | # Set frequency, check frequency, check period, check period_ns 57 | pwm.frequency = 1000 58 | passert("frequency is correct", abs(pwm.frequency - 1000) < 100) 59 | passert("period is correct", abs(pwm.period - 1e-3) < 1e-4) 60 | passert("period_ns is correct", abs(pwm.period_ns - 1000000) < 1e5) 61 | pwm.frequency = 2000 62 | passert("frequency is correct", abs(pwm.frequency - 2000) < 100) 63 | passert("period is correct", abs(pwm.period - 5e-4) < 1e-5) 64 | passert("period_ns is correct", abs(pwm.period_ns - 500000) < 1e4) 65 | 66 | # Set period_ns, check period_ns, check period, check frequency 67 | pwm.period_ns = 1000000 68 | passert("period_ns is correct", abs(pwm.period_ns - 1000000) < 1e5) 69 | passert("period is correct", abs(pwm.period - 1e-3) < 1e-4) 70 | passert("frequency is correct", abs(pwm.frequency - 1000) < 100) 71 | pwm.period_ns = 500000 72 | passert("period_ns is correct", abs(pwm.period_ns - 500000) < 1e4) 73 | passert("period is correct", abs(pwm.period - 5e-4) < 1e-5) 74 | passert("frequency is correct", abs(pwm.frequency - 2000) < 100) 75 | 76 | pwm.period_ns = 1000000 77 | 78 | # Set duty cycle, check duty cycle, check duty_cycle_ns 79 | pwm.duty_cycle = 0.25 80 | passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.25) < 1e-3) 81 | passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 250000) < 1e4) 82 | pwm.duty_cycle = 0.50 83 | passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.50) < 1e-3) 84 | passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 500000) < 1e4) 85 | pwm.duty_cycle = 0.75 86 | passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.75) < 1e-3) 87 | passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 750000) < 1e4) 88 | 89 | # Set duty_cycle_ns, check duty_cycle_ns, check duty_cycle 90 | pwm.duty_cycle_ns = 250000 91 | passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 250000) < 1e4) 92 | passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.25) < 1e-3) 93 | pwm.duty_cycle_ns = 500000 94 | passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 500000) < 1e4) 95 | passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.50) < 1e-3) 96 | pwm.duty_cycle_ns = 750000 97 | passert("duty_cycle_ns is correct", abs(pwm.duty_cycle_ns - 750000) < 1e4) 98 | passert("duty_cycle is correct", abs(pwm.duty_cycle - 0.75) < 1e-3) 99 | 100 | # Set polarity, check polarity 101 | pwm.polarity = "normal" 102 | passert("polarity is normal", pwm.polarity == "normal") 103 | pwm.polarity = "inversed" 104 | passert("polarity is inversed", pwm.polarity == "inversed") 105 | # Set enabled, check enabled 106 | pwm.enabled = True 107 | passert("pwm is enabled", pwm.enabled == True) 108 | pwm.enabled = False 109 | passert("pwm is disabled", pwm.enabled == False) 110 | # Use enable()/disable(), check enabled 111 | pwm.enable() 112 | passert("pwm is enabled", pwm.enabled == True) 113 | pwm.disable() 114 | passert("pwm is disabled", pwm.enabled == False) 115 | 116 | # Set invalid polarity 117 | with AssertRaises("set invalid polarity", ValueError): 118 | pwm.polarity = "foo" 119 | 120 | pwm.close() 121 | 122 | 123 | def test_loopback(): 124 | ptest() 125 | 126 | 127 | def test_interactive(): 128 | ptest() 129 | 130 | pwm = periphery.PWM(pwm_chip, pwm_channel) 131 | 132 | print("Starting interactive test. Get out your oscilloscope, buddy!") 133 | raw_input("Press enter to continue...") 134 | 135 | # Set initial parameters and enable PWM 136 | pwm.duty_cycle = 0.0 137 | pwm.frequency = 1e3 138 | pwm.polarity = "normal" 139 | pwm.enabled = True 140 | 141 | # Check tostring 142 | print("PWM description: {}".format(str(pwm))) 143 | passert("interactive success", raw_input("PWM description looks ok? y/n ") == "y") 144 | 145 | # Set 1 kHz frequency, 0.25 duty cycle 146 | pwm.frequency = 1e3 147 | pwm.duty_cycle = 0.25 148 | passert("interactive success", raw_input("Frequency is 1 kHz, duty cycle is 25%? y/n ") == "y") 149 | 150 | # Set 1 kHz frequency, 0.50 duty cycle 151 | pwm.frequency = 1e3 152 | pwm.duty_cycle = 0.50 153 | passert("interactive success", raw_input("Frequency is 1 kHz, duty cycle is 50%? y/n ") == "y") 154 | 155 | # Set 2 kHz frequency, 0.25 duty cycle 156 | pwm.frequency = 2e3 157 | pwm.duty_cycle = 0.25 158 | passert("interactive success", raw_input("Frequency is 2 kHz, duty cycle is 25%? y/n ") == "y") 159 | 160 | # Set 2 kHz frequency, 0.50 duty cycle 161 | pwm.frequency = 2e3 162 | pwm.duty_cycle = 0.50 163 | passert("interactive success", raw_input("Frequency is 2 kHz, duty cycle is 50%? y/n ") == "y") 164 | 165 | pwm.duty_cycle = 0.0 166 | pwm.enabled = False 167 | 168 | pwm.close() 169 | 170 | 171 | if __name__ == "__main__": 172 | if os.environ.get("CI") == "true": 173 | test_arguments() 174 | sys.exit(0) 175 | 176 | if len(sys.argv) < 3: 177 | print("Usage: python -m tests.test_pwm ") 178 | print("") 179 | print("[1/4] Arguments test: No requirements.") 180 | print("[2/4] Open/close test: PWM channel should be real.") 181 | print("[3/4] Loopback test: No test.") 182 | print("[4/4] Interactive test: PWM channel should be observed with an oscilloscope or logic analyzer.") 183 | print("") 184 | print("Hint: for Raspberry Pi 3, enable PWM0 and PWM1 with:") 185 | print(" $ echo \"dtoverlay=pwm-2chan,pin=18,func=2,pin2=13,func2=4\" | sudo tee -a /boot/config.txt") 186 | print(" $ sudo reboot") 187 | print("Monitor GPIO 18 (header pin 12), and run this test with:") 188 | print(" python -m tests.test_pwm 0 0") 189 | print("or, monitor GPIO 13 (header pin 33), and run this test with:") 190 | print(" python -m tests.test_pwm 0 1") 191 | print("") 192 | 193 | sys.exit(1) 194 | 195 | pwm_chip = int(sys.argv[1]) 196 | pwm_channel = int(sys.argv[2]) 197 | 198 | test_arguments() 199 | pokay("Arguments test passed.") 200 | test_open_close() 201 | pokay("Open/close test passed.") 202 | test_loopback() 203 | pokay("Loopback test passed.") 204 | test_interactive() 205 | pokay("Interactive test passed.") 206 | 207 | pokay("All tests passed!") 208 | -------------------------------------------------------------------------------- /tests/test_serial.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | 5 | import periphery 6 | from .test import ptest, pokay, passert, AssertRaises 7 | 8 | if sys.version_info[0] == 3: 9 | raw_input = input 10 | 11 | 12 | serial_device = None 13 | 14 | 15 | def test_arguments(): 16 | ptest() 17 | 18 | # Invalid data bits 19 | with AssertRaises("invalid databits", ValueError): 20 | periphery.Serial("/dev/ttyS0", 115200, databits=4) 21 | with AssertRaises("invalid databits", ValueError): 22 | periphery.Serial("/dev/ttyS0", 115200, databits=9) 23 | # Invalid parity 24 | with AssertRaises("invalid parity", ValueError): 25 | periphery.Serial("/dev/ttyS0", 115200, parity="blah") 26 | # Invalid stop bits 27 | with AssertRaises("invalid stop bits", ValueError): 28 | periphery.Serial("/dev/ttyS0", 115200, stopbits=0) 29 | with AssertRaises("invalid stop bits", ValueError): 30 | periphery.Serial("/dev/ttyS0", 115200, stopbits=3) 31 | 32 | # Everything else is fair game, although termios might not like it. 33 | 34 | 35 | def test_open_close(): 36 | ptest() 37 | 38 | serial = periphery.Serial(serial_device, 115200) 39 | 40 | # Confirm default settings 41 | passert("fd > 0", serial.fd > 0) 42 | passert("baudrate is 115200", serial.baudrate == 115200) 43 | passert("databits is 8", serial.databits == 8) 44 | passert("parity is none", serial.parity == "none") 45 | passert("stopbits is 1", serial.stopbits == 1) 46 | passert("xonxoff is False", serial.xonxoff == False) 47 | passert("rtscts is False", serial.rtscts == False) 48 | passert("vmin is 0", serial.vmin == 0) 49 | passert("vtime is 0", serial.vtime == 0) 50 | 51 | # Change some stuff and check that it changed 52 | serial.baudrate = 4800 53 | passert("baudrate is 4800", serial.baudrate == 4800) 54 | serial.baudrate = 9600 55 | passert("baudrate is 9600", serial.baudrate == 9600) 56 | serial.databits = 7 57 | passert("databits is 7", serial.databits == 7) 58 | serial.parity = "odd" 59 | passert("parity is odd", serial.parity == "odd") 60 | serial.stopbits = 2 61 | passert("stopbits is 2", serial.stopbits == 2) 62 | serial.xonxoff = True 63 | passert("xonxoff is True", serial.xonxoff == True) 64 | # Test serial port may not support rtscts 65 | serial.vmin = 50 66 | passert("vmin is 50", serial.vmin == 50) 67 | serial.vtime = 15.3 68 | passert("vtime is 15.3", abs(serial.vtime - 15.3) < 0.1) 69 | 70 | serial.close() 71 | 72 | 73 | def test_loopback(): 74 | ptest() 75 | 76 | 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." 77 | 78 | serial = periphery.Serial(serial_device, 115200) 79 | 80 | # Test write/flush/read with bytes write 81 | print("Write, flush, read lorem ipsum with bytes type") 82 | passert("wrote lorem ipsum bytes", serial.write(lorem_ipsum) == len(lorem_ipsum)) 83 | serial.flush() 84 | buf = serial.read(len(lorem_ipsum), timeout=3) 85 | passert("compare readback lorem ipsum", buf == lorem_ipsum) 86 | 87 | # Test write/flush/read with bytearray write 88 | print("Write, flush, read lorem ipsum with bytearray type") 89 | passert("wrote lorem ipsum bytearray", serial.write(bytearray(lorem_ipsum)) == len(lorem_ipsum)) 90 | serial.flush() 91 | buf = serial.read(len(lorem_ipsum), timeout=3) 92 | passert("compare readback lorem ipsum", buf == lorem_ipsum) 93 | 94 | # Test write/flush/read with list write 95 | print("Write, flush, read lorem ipsum with list type") 96 | passert("write lorem ipsum list", serial.write(list(bytearray(lorem_ipsum))) == len(lorem_ipsum)) 97 | serial.flush() 98 | buf = serial.read(len(lorem_ipsum), timeout=3) 99 | passert("compare readback lorem ipsum", buf == lorem_ipsum) 100 | 101 | # Test poll/write/flush/poll/input waiting/read 102 | print("Write, flush, poll, input waiting, read lorem ipsum") 103 | passert("poll timed out", serial.poll(0.5) == False) 104 | passert("write lorem ipsum", serial.write(lorem_ipsum) == len(lorem_ipsum)) 105 | serial.flush() 106 | passert("poll succeeded", serial.poll(0.5) == True) 107 | periphery.sleep_ms(500) 108 | passert("input waiting is lorem ipsum size", serial.input_waiting() == len(lorem_ipsum)) 109 | buf = serial.read(len(lorem_ipsum)) 110 | passert("compare readback lorem ipsum", buf == lorem_ipsum) 111 | 112 | # Test non-blocking poll 113 | print("Check non-blocking poll") 114 | passert("non-blocking poll is False", serial.poll(0) == False) 115 | 116 | # Test a very large read-write (likely to exceed internal buffer size (~4096)) 117 | print("Write, flush, read large buffer") 118 | lorem_hugesum = b"\xaa" * (4096 * 3) 119 | passert("wrote lorem hugesum", serial.write(lorem_hugesum) == len(lorem_hugesum)) 120 | serial.flush() 121 | buf = serial.read(len(lorem_hugesum), timeout=3) 122 | passert("compare readback lorem hugesum", buf == lorem_hugesum) 123 | 124 | # Test read timeout 125 | print("Check read timeout") 126 | tic = time.time() 127 | passert("read timed out", serial.read(4096 * 3, timeout=2) == b"") 128 | toc = time.time() 129 | passert("time elapsed", (toc - tic) > 1) 130 | 131 | # Test non-blocking read 132 | print("Check non-blocking read") 133 | tic = time.time() 134 | passert("read non-blocking is empty", serial.read(4096 * 3, timeout=0) == b"") 135 | toc = time.time() 136 | # Assuming we weren't context switched out for a second 137 | passert("almost no time elapsed", int(toc - tic) == 0) 138 | 139 | # Test blocking read with vmin=5 termios timeout 140 | print("Check blocking read with vmin termios timeout") 141 | serial.vmin = 5 142 | passert("write 5 bytes of lorem ipsum", serial.write(lorem_ipsum[0:5]) == 5) 143 | serial.flush() 144 | buf = serial.read(len(lorem_ipsum)) 145 | passert("compare readback partial lorem ipsum", buf == lorem_ipsum[0:5]) 146 | 147 | # Test blocking read with vmin=5, vtime=2 termios timeout 148 | print("Check blocking read with vmin + vtime termios timeout") 149 | serial.vtime = 2 150 | passert("write 3 bytes of lorem ipsum", serial.write(lorem_ipsum[0:3]) == 3) 151 | serial.flush() 152 | tic = time.time() 153 | buf = serial.read(len(lorem_ipsum)) 154 | toc = time.time() 155 | passert("compare readback partial lorem ipsum", buf == lorem_ipsum[0:3]) 156 | passert("time elapsed", (toc - tic) > 1) 157 | 158 | serial.close() 159 | 160 | 161 | def test_interactive(): 162 | ptest() 163 | 164 | buf = b"Hello World!" 165 | 166 | serial = periphery.Serial(serial_device, 4800) 167 | 168 | print("Starting interactive test. Get out your logic analyzer, buddy!") 169 | raw_input("Press enter to continue...") 170 | 171 | # Check tostring 172 | print("Serial description: {}".format(str(serial))) 173 | passert("interactive success", raw_input("Serial description looks ok? y/n ") == "y") 174 | 175 | serial.baudrate = 4800 176 | raw_input("Press enter to start transfer...") 177 | passert("serial write", serial.write(buf) == len(buf)) 178 | passert("interactive success", raw_input("Serial transfer baudrate 4800, 8n1 occurred? y/n ") == "y") 179 | 180 | serial.baudrate = 9600 181 | raw_input("Press enter to start transfer...") 182 | passert("serial write", serial.write(buf) == len(buf)) 183 | passert("interactive success", raw_input("Serial transfer baudrate 9600, 8n1 occurred? y/n ") == "y") 184 | 185 | serial.baudrate = 115200 186 | raw_input("Press enter to start transfer...") 187 | passert("serial write", serial.write(buf) == len(buf)) 188 | passert("interactive success", raw_input("Serial transfer baudrate 115200, 8n1 occurred? y/n ") == "y") 189 | 190 | serial.close() 191 | 192 | 193 | if __name__ == "__main__": 194 | if os.environ.get("CI") == "true": 195 | test_arguments() 196 | sys.exit(0) 197 | 198 | if len(sys.argv) < 2: 199 | print("Usage: python -m tests.test_serial ") 200 | print("") 201 | print("[1/4] Arguments test: No requirements.") 202 | print("[2/4] Open/close test: Serial port device should be real.") 203 | print("[3/4] Loopback test: Serial TX and RX should be connected with a wire.") 204 | print("[4/4] Interactive test: Serial TX should be observed with an oscilloscope or logic analyzer.") 205 | print("") 206 | print("Hint: for Raspberry Pi 3, enable UART0 with:") 207 | print(" $ echo \"dtoverlay=pi3-disable-bt\" | sudo tee -a /boot/config.txt") 208 | print(" $ sudo systemctl disable hciuart") 209 | print(" $ sudo reboot") 210 | print(" (Note that this will disable Bluetooth)") 211 | print("Use pins UART0 TXD (header pin 8) and UART0 RXD (header pin 10),") 212 | print("connect a loopback between TXD and RXD, and run this test with:") 213 | print(" python -m tests.test_serial /dev/ttyAMA0") 214 | print("") 215 | sys.exit(1) 216 | 217 | serial_device = sys.argv[1] 218 | 219 | test_arguments() 220 | pokay("Arguments test passed.") 221 | test_open_close() 222 | pokay("Open/close test passed.") 223 | test_loopback() 224 | pokay("Loopback test passed.") 225 | test_interactive() 226 | pokay("Interactive test passed.") 227 | 228 | pokay("All tests passed!") 229 | -------------------------------------------------------------------------------- /tests/test_spi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import periphery 5 | from .test import ptest, pokay, passert, AssertRaises 6 | 7 | if sys.version_info[0] == 3: 8 | raw_input = input 9 | 10 | 11 | spi_device = None 12 | 13 | 14 | def test_arguments(): 15 | ptest() 16 | 17 | # Invalid mode 18 | with AssertRaises("invalid mode", ValueError): 19 | periphery.SPI("/dev/spidev0.0", 4, int(1e6)) 20 | # Invalid bit order 21 | with AssertRaises("invalid bit order", ValueError): 22 | periphery.SPI("/dev/spidev0.0", 4, int(1e6), bit_order="blah") 23 | 24 | 25 | def test_open_close(): 26 | ptest() 27 | 28 | # Normal open (mode=1, max_speed = 100000) 29 | spi = periphery.SPI(spi_device, 1, 100000) 30 | 31 | # Confirm fd and defaults 32 | passert("fd > 0", spi.fd > 0) 33 | passert("mode is 1", spi.mode == 1) 34 | passert("max speed is 100000", spi.max_speed == 100000) 35 | passert("default bit_order is msb", spi.bit_order == "msb") 36 | passert("default bits_per_word is 8", spi.bits_per_word == 8) 37 | 38 | # Not going to try different bit order or bits per word, because not 39 | # all SPI controllers support them 40 | 41 | # Try modes 0, 1, 2, 3 42 | spi.mode = 0 43 | passert("spi mode is 0", spi.mode == 0) 44 | spi.mode = 1 45 | passert("spi mode is 1", spi.mode == 1) 46 | spi.mode = 2 47 | passert("spi mode is 2", spi.mode == 2) 48 | spi.mode = 3 49 | passert("spi mode is 3", spi.mode == 3) 50 | 51 | # Try max speeds 100Khz, 500KHz, 1MHz, 2MHz 52 | spi.max_speed = 100000 53 | passert("max speed is 100KHz", spi.max_speed == 100000) 54 | spi.max_speed = 500000 55 | passert("max speed is 500KHz", spi.max_speed == 500000) 56 | spi.max_speed = 1000000 57 | passert("max speed is 1MHz", spi.max_speed == 1000000) 58 | spi.max_speed = 2e6 59 | passert("max speed is 2MHz", spi.max_speed == 2000000) 60 | 61 | spi.close() 62 | 63 | 64 | def test_loopback(): 65 | ptest() 66 | 67 | spi = periphery.SPI(spi_device, 0, 100000) 68 | 69 | # Try list transfer 70 | buf_in = list(range(256)) * 4 71 | buf_out = spi.transfer(buf_in) 72 | passert("compare readback", buf_out == buf_in) 73 | 74 | # Try bytearray transfer 75 | buf_in = bytearray(buf_in) 76 | buf_out = spi.transfer(buf_in) 77 | passert("compare readback", buf_out == buf_in) 78 | 79 | # Try bytes transfer 80 | buf_in = bytes(bytearray(buf_in)) 81 | buf_out = spi.transfer(buf_in) 82 | passert("compare readback", buf_out == buf_in) 83 | 84 | spi.close() 85 | 86 | 87 | def test_interactive(): 88 | ptest() 89 | 90 | spi = periphery.SPI(spi_device, 0, 100000) 91 | 92 | print("Starting interactive test. Get out your logic analyzer, buddy!") 93 | raw_input("Press enter to continue...") 94 | 95 | # Check tostring 96 | print("SPI description: {}".format(str(spi))) 97 | passert("interactive success", raw_input("SPI description looks ok? y/n ") == "y") 98 | 99 | # Mode 0 transfer 100 | raw_input("Press enter to start transfer...") 101 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 102 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 103 | passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 0 occurred? y/n ") == "y") 104 | 105 | # Mode 1 transfer 106 | spi.mode = 1 107 | raw_input("Press enter to start transfer...") 108 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 109 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 110 | passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 1 occurred? y/n ") == "y") 111 | 112 | # Mode 2 transfer 113 | spi.mode = 2 114 | raw_input("Press enter to start transfer...") 115 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 116 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 117 | passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 2 occurred? y/n ") == "y") 118 | 119 | # Mode 3 transfer 120 | spi.mode = 3 121 | raw_input("Press enter to start transfer...") 122 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 123 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 124 | passert("interactive success", raw_input("SPI transfer speed <= 100KHz, mode 3 occurred? y/n ") == "y") 125 | 126 | spi.mode = 0 127 | 128 | # 500KHz transfer 129 | spi.max_speed = 500000 130 | raw_input("Press enter to start transfer...") 131 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 132 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 133 | passert("interactive success", raw_input("SPI transfer speed <= 500KHz, mode 0 occurred? y/n ") == "y") 134 | 135 | # 1MHz transfer 136 | spi.max_speed = 1000000 137 | raw_input("Press enter to start transfer...") 138 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 139 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 140 | passert("interactive success", raw_input("SPI transfer speed <= 1MHz, mode 0 occurred? y/n ") == "y") 141 | 142 | spi.close() 143 | 144 | 145 | if __name__ == "__main__": 146 | if os.environ.get("CI") == "true": 147 | test_arguments() 148 | sys.exit(0) 149 | 150 | if len(sys.argv) < 2: 151 | print("Usage: python -m tests.test_spi ") 152 | print("") 153 | print("[1/4] Arguments test: No requirements.") 154 | print("[2/4] Open/close test: SPI device should be real.") 155 | print("[3/4] Loopback test: SPI MISO and MOSI should be connected with a wire.") 156 | print("[4/4] Interactive test: SPI MOSI, CLK, CS should be observed with an oscilloscope or logic analyzer.") 157 | print("") 158 | print("Hint: for Raspberry Pi 3, enable SPI0 with:") 159 | print(" $ echo \"dtparam=spi=on\" | sudo tee -a /boot/config.txt") 160 | print(" $ sudo reboot") 161 | print("Use pins SPI0 MOSI (header pin 19), SPI0 MISO (header pin 21), SPI0 SCLK (header pin 23),") 162 | print("connect a loopback between MOSI and MISO, and run this test with:") 163 | print(" python -m tests.test_spi /dev/spidev0.0") 164 | print("") 165 | sys.exit(1) 166 | 167 | spi_device = sys.argv[1] 168 | 169 | test_arguments() 170 | pokay("Arguments test passed.") 171 | test_open_close() 172 | pokay("Open/close test passed.") 173 | test_loopback() 174 | pokay("Loopback test passed.") 175 | test_interactive() 176 | pokay("Interactive test passed.") 177 | 178 | pokay("All tests passed!") 179 | --------------------------------------------------------------------------------