├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── conf.py ├── gpio.rst ├── i2c.rst ├── index.rst ├── led.rst ├── mmio.rst ├── pwm.rst ├── serial.rst ├── spi.rst └── version.rst ├── periphery ├── __init__.py ├── gpio.py ├── i2c.py ├── led.py ├── mmio.py ├── pwm.py ├── serial.py └── spi.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── asserts.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 /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | docs/_build/ 4 | build/ 5 | dist/ 6 | python_periphery.egg-info/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | - "3.7" 8 | - "pypy" 9 | - "pypy3" 10 | 11 | script: 12 | - python -m tests.test_gpio 13 | - python -m tests.test_gpio_sysfs 14 | - python -m tests.test_spi 15 | - python -m tests.test_i2c 16 | - python -m tests.test_mmio 17 | - python -m tests.test_serial 18 | - python -m tests.test_led 19 | - python -m tests.test_pwm 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | * v2.0.0 - 10/28/2019 2 | * GPIO 3 | * Add support for character device GPIOs. 4 | * Remove support for preserve direction from GPIO constructor. 5 | * Add retry loop to direction write after export to accommodate delayed 6 | udev permission rule application for sysfs GPIOs. 7 | * Unexport GPIO line on close for sysfs GPIOs. 8 | * Fix handling of `timeout=None` with sysfs GPIO `poll()`. 9 | * Add `devpath` property. 10 | * PWM 11 | * Fix chip and channel argument names in PWM constructor and 12 | documentation. 13 | * Add retry loop to PWM open after export to accommodate delayed 14 | creation of sysfs files by kernel driver. 15 | * Unexport PWM channel on close. 16 | * Add nanosecond `period_ns` and `duty_cycle_ns` properties. 17 | * Add `devpath` property. 18 | * LED 19 | * Raise `LookupError` instead of `ValueError` if LED name is not found 20 | during open. 21 | * Add `devpath` property. 22 | * Fix exception handling for Python 2 with `ioctl()` operations in Serial, 23 | SPI, and I2C modules. 24 | * Fix `with` statement context manager support for all modules. 25 | * Update tests with running hints for Raspberry Pi 3. 26 | * Contributors 27 | * Uwe Kleine-König, @ukleinek - 0005260 28 | * Heath Robinson, @ubiquitousthey - ac457d6 29 | 30 | * v1.1.2 - 06/25/2019 31 | * Add LICENSE file to packaging. 32 | 33 | * v1.1.1 - 04/03/2018 34 | * Fix handling of delayed pin directory export when opening a GPIO. 35 | 36 | * v1.1.0 - 10/24/2016 37 | * Add support for preserving pin direction when opening GPIO. 38 | * Improve GPIO poll() implementation to work with more platforms. 39 | * Improve atomicity of MMIO fixed width writes. 40 | * Add PWM module. 41 | * Add LED module. 42 | * Add support for universal wheel packaging. 43 | * Contributors 44 | * Sanket Dasgupta - 8ac7b40 45 | * Joseph Kogut - 022ef29, d2e9132 46 | * Hector Martin - 1e3343a 47 | * Francesco Valla - 34b3877 48 | 49 | * v1.0.0 - 06/25/2015 50 | * Initial release. 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2019 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 [![Build Status](https://travis-ci.org/vsergeev/python-periphery.svg?branch=master)](https://travis-ci.org/vsergeev/python-periphery) [![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 | ## Installation 10 | 11 | With pip: 12 | ``` text 13 | pip install python-periphery 14 | ``` 15 | 16 | With easy_install: 17 | ``` text 18 | easy_install python-periphery 19 | ``` 20 | 21 | With setup.py: 22 | ``` text 23 | git clone https://github.com/vsergeev/python-periphery.git 24 | cd python-periphery 25 | python setup.py install 26 | ``` 27 | 28 | ## Examples 29 | 30 | ### GPIO 31 | 32 | ``` python 33 | from periphery import GPIO 34 | 35 | # Open GPIO /dev/gpiochip0 line 10 with input direction 36 | gpio_in = GPIO("/dev/gpiochip0", 10, "in") 37 | # Open GPIO /dev/gpiochip0 line 12 with output direction 38 | gpio_out = GPIO("/dev/gpiochip0", 12, "out") 39 | 40 | value = gpio_in.read() 41 | gpio_out.write(not value) 42 | 43 | gpio_in.close() 44 | gpio_out.close() 45 | ``` 46 | 47 | [Go to GPIO documentation.](http://python-periphery.readthedocs.org/en/latest/gpio.html) 48 | 49 | ### LED 50 | 51 | ``` python 52 | from periphery import LED 53 | 54 | # Open LED "led0" with initial state off 55 | led0 = LED("led0", False) 56 | # Open LED "led1" with initial state on 57 | led1 = LED("led1", True) 58 | 59 | value = led0.read() 60 | led1.write(value) 61 | 62 | # Set custom brightness level 63 | led1.write(led1.max_brightness / 2) 64 | 65 | led0.close() 66 | led1.close() 67 | ``` 68 | 69 | [Go to LED documentation.](http://python-periphery.readthedocs.org/en/latest/led.html) 70 | 71 | ### PWM 72 | 73 | ``` python 74 | from periphery import PWM 75 | 76 | # Open PWM chip 0, channel 10 77 | pwm = PWM(0, 10) 78 | 79 | # Set frequency to 1 kHz 80 | pwm.frequency = 1e3 81 | # Set duty cycle to 75% 82 | pwm.duty_cycle = 0.75 83 | 84 | pwm.enable() 85 | 86 | # Change duty cycle to 50% 87 | pwm.duty_cycle = 0.50 88 | 89 | pwm.close() 90 | ``` 91 | 92 | [Go to PWM documentation.](http://python-periphery.readthedocs.org/en/latest/pwm.html) 93 | 94 | ### SPI 95 | 96 | ``` python 97 | from periphery import SPI 98 | 99 | # Open spidev1.0 with mode 0 and max speed 1MHz 100 | spi = SPI("/dev/spidev1.0", 0, 1000000) 101 | 102 | data_out = [0xaa, 0xbb, 0xcc, 0xdd] 103 | data_in = spi.transfer(data_out) 104 | 105 | print("shifted out [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_out)) 106 | print("shifted in [0x{:02x}, 0x{:02x}, 0x{:02x}, 0x{:02x}]".format(*data_in)) 107 | 108 | spi.close() 109 | ``` 110 | 111 | [Go to SPI documentation.](http://python-periphery.readthedocs.org/en/latest/spi.html) 112 | 113 | ### I2C 114 | 115 | ``` python 116 | from periphery import I2C 117 | 118 | # Open i2c-0 controller 119 | i2c = I2C("/dev/i2c-0") 120 | 121 | # Read byte at address 0x100 of EEPROM at 0x50 122 | msgs = [I2C.Message([0x01, 0x00]), I2C.Message([0x00], read=True)] 123 | i2c.transfer(0x50, msgs) 124 | print("0x100: 0x{:02x}".format(msgs[1].data[0])) 125 | 126 | i2c.close() 127 | ``` 128 | 129 | [Go to I2C documentation.](http://python-periphery.readthedocs.org/en/latest/i2c.html) 130 | 131 | ### MMIO 132 | 133 | ``` python 134 | from periphery import MMIO 135 | 136 | # Open am335x real-time clock subsystem page 137 | rtc_mmio = MMIO(0x44E3E000, 0x1000) 138 | 139 | # Read current time 140 | rtc_secs = rtc_mmio.read32(0x00) 141 | rtc_mins = rtc_mmio.read32(0x04) 142 | rtc_hrs = rtc_mmio.read32(0x08) 143 | 144 | print("hours: {:02x} minutes: {:02x} seconds: {:02x}".format(rtc_hrs, rtc_mins, rtc_secs)) 145 | 146 | rtc_mmio.close() 147 | 148 | # Open am335x control module page 149 | ctrl_mmio = MMIO(0x44E10000, 0x1000) 150 | 151 | # Read MAC address 152 | mac_id0_lo = ctrl_mmio.read32(0x630) 153 | mac_id0_hi = ctrl_mmio.read32(0x634) 154 | 155 | print("MAC address: {:04x}{:08x}".format(mac_id0_lo, mac_id0_hi)) 156 | 157 | ctrl_mmio.close() 158 | ``` 159 | 160 | [Go to MMIO documentation.](http://python-periphery.readthedocs.org/en/latest/mmio.html) 161 | 162 | ### Serial 163 | 164 | ``` python 165 | from periphery import Serial 166 | 167 | # Open /dev/ttyUSB0 with baudrate 115200, and defaults of 8N1, no flow control 168 | serial = Serial("/dev/ttyUSB0", 115200) 169 | 170 | serial.write(b"Hello World!") 171 | 172 | # Read up to 128 bytes with 500ms timeout 173 | buf = serial.read(128, 0.5) 174 | print("read {:d} bytes: _{:s}_".format(len(buf), buf)) 175 | 176 | serial.close() 177 | ``` 178 | 179 | [Go to Serial documentation.](http://python-periphery.readthedocs.org/en/latest/serial.html) 180 | 181 | ## Documentation 182 | 183 | Documentation is hosted at [http://python-periphery.readthedocs.org/](http://python-periphery.readthedocs.org/). 184 | 185 | To build documentation locally with Sphinx, run: 186 | 187 | ``` 188 | cd docs 189 | make html 190 | ``` 191 | 192 | Sphinx will produce the HTML documentation in `docs/_build/html/`. 193 | 194 | Run `make help` to see other output targets (LaTeX, man, text, etc.). 195 | 196 | ## Testing 197 | 198 | 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. 199 | 200 | ## License 201 | 202 | python-periphery is MIT licensed. See the included [LICENSE](LICENSE) file. 203 | 204 | -------------------------------------------------------------------------------- /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 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('..')) 16 | 17 | # -- Project information ----------------------------------------------------- 18 | 19 | project = u'python-periphery' 20 | copyright = u'2015-2019, vsergeev / Ivan (Vanya) A. Sergeev' 21 | author = u'Vanya A. Sergeev' 22 | 23 | # The short X.Y version. 24 | version = '2.0.0' 25 | # The full version, including alpha/beta/rc tags. 26 | release = version 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.napoleon', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.viewcode', 38 | ] 39 | 40 | autoclass_content = 'init' 41 | autodoc_member_order = 'bysource' 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ['_templates'] 45 | 46 | # The master toctree document. 47 | master_doc = 'index' 48 | 49 | # List of patterns, relative to source directory, that match files and 50 | # directories to ignore when looking for source files. 51 | # This pattern also affects html_static_path and html_extra_path. 52 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 53 | 54 | # The name of the Pygments (syntax highlighting) style to use. 55 | pygments_style = 'sphinx' 56 | 57 | # -- Options for HTML output ------------------------------------------------- 58 | 59 | # The theme to use for HTML and HTML Help pages. See the documentation for 60 | # a list of builtin themes. 61 | # 62 | html_theme = 'sphinx_rtd_theme' 63 | 64 | # Add any paths that contain custom static files (such as style sheets) here, 65 | # relative to this directory. They are copied after the builtin static files, 66 | # so a file named "default.css" will overwrite the builtin "default.css". 67 | html_static_path = ['_static'] 68 | 69 | # If true, links to the reST sources are added to the pages. 70 | html_show_sourcelink = False 71 | -------------------------------------------------------------------------------- /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 | 27 | .. autoclass:: periphery.CdevGPIO 28 | 29 | .. class:: periphery.GPIO(line, direction) 30 | 31 | .. autoclass:: periphery.SysfsGPIO 32 | 33 | .. autoclass:: periphery.GPIO 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | .. autoclass:: periphery.EdgeEvent 39 | :members: 40 | :undoc-members: 41 | :show-inheritance: 42 | 43 | .. autoclass:: periphery.GPIOError 44 | :members: 45 | :undoc-members: 46 | :show-inheritance: 47 | 48 | -------------------------------------------------------------------------------- /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: 4 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/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.0.0" 4 | "Module version string." 5 | 6 | version = (2, 0, 0) 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/gpio.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import ctypes 3 | import errno 4 | import fcntl 5 | import os 6 | import os.path 7 | import select 8 | import time 9 | 10 | 11 | class GPIOError(IOError): 12 | """Base class for GPIO errors.""" 13 | pass 14 | 15 | 16 | class EdgeEvent(collections.namedtuple('EdgeEvent', ['edge', 'timestamp'])): 17 | def __new__(cls, edge, timestamp): 18 | """EdgeEvent containing the event edge and event time reported by Linux. 19 | 20 | Args: 21 | edge (str): event edge, either "rising" or "falling". 22 | timestamp (int): event time in nanoseconds. 23 | """ 24 | return super(EdgeEvent, cls).__new__(cls, edge, timestamp) 25 | 26 | 27 | class GPIO(object): 28 | def __new__(cls, *args): 29 | if len(args) > 2: 30 | return CdevGPIO.__new__(cls, *args) 31 | else: 32 | return SysfsGPIO.__new__(cls, *args) 33 | 34 | def __del__(self): 35 | self.close() 36 | 37 | def __enter__(self): 38 | return self 39 | 40 | def __exit__(self, t, value, traceback): 41 | self.close() 42 | 43 | # Methods 44 | 45 | def read(self): 46 | """Read the state of the GPIO. 47 | 48 | Returns: 49 | bool: ``True`` for high state, ``False`` for low state. 50 | 51 | Raises: 52 | GPIOError: if an I/O or OS error occurs. 53 | 54 | """ 55 | raise NotImplementedError() 56 | 57 | def write(self, value): 58 | """Set the state of the GPIO to `value`. 59 | 60 | Args: 61 | value (bool): ``True`` for high state, ``False`` for low state. 62 | 63 | Raises: 64 | GPIOError: if an I/O or OS error occurs. 65 | TypeError: if `value` type is not bool. 66 | 67 | """ 68 | raise NotImplementedError() 69 | 70 | def poll(self, timeout=None): 71 | """Poll a GPIO for the edge event configured with the .edge property. 72 | 73 | For character device GPIOs, the edge event should be consumed with 74 | `read_event()`. For sysfs GPIOs, the edge event should be consumed with 75 | `read()`. 76 | 77 | `timeout` can be a positive number for a timeout in seconds, 0 for a 78 | non-blocking poll, or negative or None for a blocking poll. Defaults to 79 | blocking poll. 80 | 81 | Args: 82 | timeout (int, float, None): timeout duration in seconds. 83 | 84 | Returns: 85 | bool: ``True`` if an edge event occurred, ``False`` on timeout. 86 | 87 | Raises: 88 | GPIOError: if an I/O or OS error occurs. 89 | TypeError: if `timeout` type is not None or int. 90 | 91 | """ 92 | raise NotImplementedError() 93 | 94 | def read_event(self): 95 | """Read the edge event that occurred with the GPIO. 96 | 97 | This method is intended for use with character device GPIOs and is 98 | unsupported by sysfs GPIOs. 99 | 100 | Returns: 101 | EdgeEvent: a namedtuple containing the string edge event that 102 | occurred (either ``"rising"`` or ``"falling"``), and the event time 103 | reported by Linux in nanoseconds. 104 | 105 | Raises: 106 | GPIOError: if an I/O or OS error occurs. 107 | NotImplementedError: if called on a sysfs GPIO. 108 | 109 | """ 110 | raise NotImplementedError() 111 | 112 | def close(self): 113 | """Close the sysfs GPIO. 114 | 115 | Raises: 116 | GPIOError: if an I/O or OS error occurs. 117 | 118 | """ 119 | raise NotImplementedError() 120 | 121 | # Immutable properties 122 | 123 | @property 124 | def devpath(self): 125 | """Get the device path of the underlying GPIO device. 126 | 127 | :type: str 128 | """ 129 | raise NotImplementedError() 130 | 131 | @property 132 | def fd(self): 133 | """Get the line file descriptor of the GPIO object. 134 | 135 | :type: int 136 | """ 137 | raise NotImplementedError() 138 | 139 | @property 140 | def line(self): 141 | """Get the GPIO object's line number. 142 | 143 | :type: int 144 | """ 145 | raise NotImplementedError() 146 | 147 | @property 148 | def name(self): 149 | """Get the line name of the GPIO. 150 | 151 | his method is intended for use with character device GPIOs and always 152 | returns the empty string for sysfs GPIOs. 153 | 154 | :type: str 155 | """ 156 | raise NotImplementedError() 157 | 158 | @property 159 | def chip_fd(self): 160 | """Get the GPIO chip file descriptor of the GPIO object. 161 | 162 | This method is intended for use with character device GPIOs and is unsupported by sysfs GPIOs. 163 | 164 | Raises: 165 | NotImplementedError: if accessed on a sysfs GPIO. 166 | 167 | :type: int 168 | """ 169 | raise NotImplementedError() 170 | 171 | @property 172 | def chip_name(self): 173 | """Get the name of the GPIO chip associated with the GPIO. 174 | 175 | :type: str 176 | """ 177 | raise NotImplementedError() 178 | 179 | @property 180 | def chip_label(self): 181 | """ Get the label of the GPIO chip associated with the GPIO. 182 | 183 | :type: str 184 | """ 185 | raise NotImplementedError() 186 | 187 | # Mutable properties 188 | 189 | def _get_direction(self): 190 | raise NotImplementedError() 191 | 192 | def _set_direction(self, direction): 193 | raise NotImplementedError() 194 | 195 | direction = property(_get_direction, _set_direction) 196 | """Get or set the GPIO's direction. Can be "in", "out", "high", "low". 197 | 198 | Direction "in" is input; "out" is output, initialized to low; "high" is 199 | output, initialized to high; and "low" is output, initialized to low. 200 | 201 | Raises: 202 | GPIOError: if an I/O or OS error occurs. 203 | TypeError: if `direction` type is not str. 204 | ValueError: if `direction` value is invalid. 205 | 206 | :type: str 207 | """ 208 | 209 | def _get_edge(self): 210 | raise NotImplementedError() 211 | 212 | def _set_edge(self, edge): 213 | raise NotImplementedError() 214 | 215 | edge = property(_get_edge, _set_edge) 216 | """Get or set the GPIO's interrupt edge. Can be "none", "rising", 217 | "falling", "both". 218 | 219 | Raises: 220 | GPIOError: if an I/O or OS error occurs. 221 | TypeError: if `edge` type is not str. 222 | ValueError: if `edge` value is invalid. 223 | 224 | :type: str 225 | """ 226 | 227 | # String representation 228 | 229 | def __str__(self): 230 | """Get the string representation of the GPIO. 231 | 232 | :type: str 233 | """ 234 | raise NotImplementedError() 235 | 236 | 237 | class _CGpiochipInfo(ctypes.Structure): 238 | _fields_ = [ 239 | ('name', ctypes.c_char * 32), 240 | ('label', ctypes.c_char * 32), 241 | ('lines', ctypes.c_uint32), 242 | ] 243 | 244 | 245 | class _CGpiolineInfo(ctypes.Structure): 246 | _fields_ = [ 247 | ('line_offset', ctypes.c_uint32), 248 | ('flags', ctypes.c_uint32), 249 | ('name', ctypes.c_char * 32), 250 | ('consumer', ctypes.c_char * 32), 251 | ] 252 | 253 | 254 | class _CGpiohandleRequest(ctypes.Structure): 255 | _fields_ = [ 256 | ('lineoffsets', ctypes.c_uint32 * 64), 257 | ('flags', ctypes.c_uint32), 258 | ('default_values', ctypes.c_uint8 * 64), 259 | ('consumer_label', ctypes.c_char * 32), 260 | ('lines', ctypes.c_uint32), 261 | ('fd', ctypes.c_int), 262 | ] 263 | 264 | 265 | class _CGpiohandleData(ctypes.Structure): 266 | _fields_ = [ 267 | ('values', ctypes.c_uint8 * 64), 268 | ] 269 | 270 | 271 | class _CGpioeventRequest(ctypes.Structure): 272 | _fields_ = [ 273 | ('lineoffset', ctypes.c_uint32), 274 | ('handleflags', ctypes.c_uint32), 275 | ('eventflags', ctypes.c_uint32), 276 | ('consumer_label', ctypes.c_char * 32), 277 | ('fd', ctypes.c_int), 278 | ] 279 | 280 | 281 | class _CGpioeventData(ctypes.Structure): 282 | _fields_ = [ 283 | ('timestamp', ctypes.c_uint64), 284 | ('id', ctypes.c_uint32), 285 | ] 286 | 287 | 288 | class CdevGPIO(GPIO): 289 | # Constants scraped from 290 | _GPIOHANDLE_GET_LINE_VALUES_IOCTL = 0xc040b408 291 | _GPIOHANDLE_SET_LINE_VALUES_IOCTL = 0xc040b409 292 | _GPIO_GET_CHIPINFO_IOCTL = 0x8044b401 293 | _GPIO_GET_LINEINFO_IOCTL = 0xc048b402 294 | _GPIO_GET_LINEHANDLE_IOCTL = 0xc16cb403 295 | _GPIO_GET_LINEEVENT_IOCTL = 0xc030b404 296 | _GPIOHANDLE_REQUEST_INPUT = 0x1 297 | _GPIOHANDLE_REQUEST_OUTPUT = 0x2 298 | _GPIOEVENT_REQUEST_RISING_EDGE = 0x1 299 | _GPIOEVENT_REQUEST_FALLING_EDGE = 0x2 300 | _GPIOEVENT_REQUEST_BOTH_EDGES = 0x3 301 | _GPIOEVENT_EVENT_RISING_EDGE = 0x1 302 | _GPIOEVENT_EVENT_FALLING_EDGE = 0x2 303 | 304 | def __init__(self, path, line, direction): 305 | """**Character device GPIO** 306 | 307 | Instantiate a GPIO object and open the character device GPIO with the 308 | specified line and direction at the specified GPIO chip path (e.g. 309 | "/dev/gpiochip0"). 310 | 311 | `direction` can be "in" for input; "out" for output, initialized to 312 | low; "high" for output, initialized to high; or "low" for output, 313 | initialized to low. 314 | 315 | Args: 316 | path (str): GPIO chip character device path. 317 | line (int, str): GPIO line number or name. 318 | direction (str): GPIO direction, can be "in", "out", "high", or 319 | "low". 320 | 321 | Returns: 322 | CdevGPIO: GPIO object. 323 | 324 | Raises: 325 | GPIOError: if an I/O or OS error occurs. 326 | TypeError: if `path`, `line`, or `direction` types are invalid. 327 | ValueError: if `direction` value is invalid. 328 | LookupError: if the GPIO line was not found by the provided name. 329 | 330 | """ 331 | self._devpath = None 332 | self._line_fd = None 333 | self._chip_fd = None 334 | self._edge = "none" 335 | self._direction = "in" 336 | self._line = None 337 | 338 | self._open(path, line, direction) 339 | 340 | def __new__(self, path, line, direction): 341 | return object.__new__(CdevGPIO) 342 | 343 | def _open(self, path, line, direction): 344 | if not isinstance(path, str): 345 | raise TypeError("Invalid path type, should be string.") 346 | if not isinstance(line, (int, str)): 347 | raise TypeError("Invalid line type, should be integer or string.") 348 | if not isinstance(direction, str): 349 | raise TypeError("Invalid direction type, should be string.") 350 | if direction.lower() not in ["in", "out", "high", "low"]: 351 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 352 | 353 | # Open GPIO chip 354 | try: 355 | self._chip_fd = os.open(path, 0) 356 | except OSError as e: 357 | raise GPIOError(e.errno, "Opening GPIO chip: " + e.strerror) 358 | 359 | self._devpath = path 360 | 361 | if isinstance(line, int): 362 | self._line = line 363 | self._reopen(direction, "none") 364 | else: 365 | self._line = self._find_line_by_name(line) 366 | self._reopen(direction, "none") 367 | 368 | def _reopen(self, direction, edge): 369 | # Close existing line 370 | if self._line_fd is not None: 371 | try: 372 | os.close(self._line_fd) 373 | except OSError as e: 374 | raise GPIOError(e.errno, "Closing existing GPIO line: " + e.strerror) 375 | 376 | if direction == "in": 377 | if edge == "none": 378 | request = _CGpiohandleRequest() 379 | 380 | request.lineoffsets[0] = self._line 381 | request.flags = CdevGPIO._GPIOHANDLE_REQUEST_INPUT 382 | request.consumer_label = b"periphery" 383 | request.lines = 1 384 | 385 | try: 386 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_LINEHANDLE_IOCTL, request) 387 | except (OSError, IOError) as e: 388 | raise GPIOError(e.errno, "Opening input line handle: " + e.strerror) 389 | 390 | self._line_fd = request.fd 391 | self._direction = "in" 392 | self._edge = "none" 393 | else: 394 | request = _CGpioeventRequest() 395 | 396 | request.lineoffset = self._line 397 | request.handleflags = CdevGPIO._GPIOHANDLE_REQUEST_INPUT 398 | request.eventflags = CdevGPIO._GPIOEVENT_REQUEST_RISING_EDGE if edge == "rising" else CdevGPIO._GPIOEVENT_REQUEST_FALLING_EDGE if edge == "falling" else CdevGPIO._GPIOEVENT_REQUEST_BOTH_EDGES 399 | request.consumer_label = b"periphery" 400 | 401 | try: 402 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_LINEEVENT_IOCTL, request) 403 | except (OSError, IOError) as e: 404 | raise GPIOError(e.errno, "Opening input line event handle: " + e.strerror) 405 | 406 | self._line_fd = request.fd 407 | self._direction = "in" 408 | self._edge = edge 409 | else: 410 | request = _CGpiohandleRequest() 411 | initial_value = True if direction == "high" else False 412 | 413 | request.lineoffsets[0] = self._line 414 | request.flags = CdevGPIO._GPIOHANDLE_REQUEST_OUTPUT 415 | request.default_values[0] = initial_value 416 | request.consumer_label = b"periphery" 417 | request.lines = 1 418 | 419 | try: 420 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_LINEHANDLE_IOCTL, request) 421 | except (OSError, IOError) as e: 422 | raise GPIOError(e.errno, "Opening output line handle: " + e.strerror) 423 | 424 | self._line_fd = request.fd 425 | self._direction = "out" 426 | self._edge = "none" 427 | 428 | def _find_line_by_name(self, line): 429 | # Get chip info for number of lines 430 | chip_info = _CGpiochipInfo() 431 | try: 432 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 433 | except (OSError, IOError) as e: 434 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 435 | 436 | # Get each line info 437 | line_info = _CGpiolineInfo() 438 | for i in range(chip_info.lines): 439 | line_info.line_offset = i 440 | try: 441 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_LINEINFO_IOCTL, line_info) 442 | except (OSError, IOError) as e: 443 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 444 | 445 | if line_info.name.decode() == line: 446 | return i 447 | 448 | raise LookupError("Opening GPIO line: GPIO line \"{:s}\" not found by name.".format(line)) 449 | 450 | # Methods 451 | 452 | def read(self): 453 | data = _CGpiohandleData() 454 | 455 | try: 456 | fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_GET_LINE_VALUES_IOCTL, data) 457 | except (OSError, IOError) as e: 458 | raise GPIOError(e.errno, "Getting line value: " + e.strerror) 459 | 460 | return bool(data.values[0]) 461 | 462 | def write(self, value): 463 | if not isinstance(value, bool): 464 | raise TypeError("Invalid value type, should be bool.") 465 | 466 | data = _CGpiohandleData() 467 | 468 | data.values[0] = value 469 | 470 | try: 471 | fcntl.ioctl(self._line_fd, CdevGPIO._GPIOHANDLE_SET_LINE_VALUES_IOCTL, data) 472 | except (OSError, IOError) as e: 473 | raise GPIOError(e.errno, "Setting line value: " + e.strerror) 474 | 475 | def poll(self, timeout=None): 476 | if not isinstance(timeout, (int, float, type(None))): 477 | raise TypeError("Invalid timeout type, should be integer, float, or None.") 478 | 479 | # Setup poll 480 | p = select.poll() 481 | p.register(self._line_fd, select.POLLIN | select.POLLPRI | select.POLLERR) 482 | 483 | # Scale timeout to milliseconds 484 | if isinstance(timeout, (int, float)) and timeout > 0: 485 | timeout *= 1000 486 | 487 | # Poll 488 | events = p.poll(timeout) 489 | 490 | return len(events) > 0 491 | 492 | def read_event(self): 493 | if self._edge == "none": 494 | raise GPIOError(None, "Invalid operation: GPIO edge not set") 495 | 496 | try: 497 | buf = os.read(self._line_fd, ctypes.sizeof(_CGpioeventData)) 498 | except OSError as e: 499 | raise GPIOError(e.errno, "Reading GPIO event: " + e.strerror) 500 | 501 | event_data = _CGpioeventData.from_buffer_copy(buf) 502 | 503 | if event_data.id == CdevGPIO._GPIOEVENT_EVENT_RISING_EDGE: 504 | edge = "rising" 505 | elif event_data.id == CdevGPIO._GPIOEVENT_EVENT_FALLING_EDGE: 506 | edge = "falling" 507 | else: 508 | edge = "none" 509 | 510 | timestamp = event_data.timestamp 511 | 512 | return EdgeEvent(edge, timestamp) 513 | 514 | def close(self): 515 | try: 516 | if self._line_fd is not None: 517 | os.close(self._line_fd) 518 | except OSError as e: 519 | raise GPIOError(e.errno, "Closing GPIO line: " + e.strerror) 520 | 521 | try: 522 | if self._chip_fd is not None: 523 | os.close(self._chip_fd) 524 | except OSError as e: 525 | raise GPIOError(e.errno, "Closing GPIO chip: " + e.strerror) 526 | 527 | self._line_fd = None 528 | self._chip_fd = None 529 | self._edge = "none" 530 | self._direction = "in" 531 | self._line = None 532 | 533 | # Immutable properties 534 | 535 | @property 536 | def devpath(self): 537 | return self._devpath 538 | 539 | @property 540 | def fd(self): 541 | return self._line_fd 542 | 543 | @property 544 | def line(self): 545 | return self._line 546 | 547 | @property 548 | def name(self): 549 | line_info = _CGpiolineInfo() 550 | line_info.line_offset = self._line 551 | 552 | try: 553 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_LINEINFO_IOCTL, line_info) 554 | except (OSError, IOError) as e: 555 | raise GPIOError(e.errno, "Querying GPIO line info: " + e.strerror) 556 | 557 | return line_info.name.decode() 558 | 559 | @property 560 | def chip_fd(self): 561 | return self._chip_fd 562 | 563 | @property 564 | def chip_name(self): 565 | chip_info = _CGpiochipInfo() 566 | 567 | try: 568 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 569 | except (OSError, IOError) as e: 570 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 571 | 572 | return chip_info.name.decode() 573 | 574 | @property 575 | def chip_label(self): 576 | chip_info = _CGpiochipInfo() 577 | 578 | try: 579 | fcntl.ioctl(self._chip_fd, CdevGPIO._GPIO_GET_CHIPINFO_IOCTL, chip_info) 580 | except (OSError, IOError) as e: 581 | raise GPIOError(e.errno, "Querying GPIO chip info: " + e.strerror) 582 | 583 | return chip_info.label.decode() 584 | 585 | # Mutable properties 586 | 587 | def _get_direction(self): 588 | return self._direction 589 | 590 | def _set_direction(self, direction): 591 | if not isinstance(direction, str): 592 | raise TypeError("Invalid direction type, should be string.") 593 | if direction.lower() not in ["in", "out", "high", "low"]: 594 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 595 | 596 | if self._direction == direction: 597 | return 598 | 599 | self._reopen(direction, "none") 600 | 601 | direction = property(_get_direction, _set_direction) 602 | 603 | def _get_edge(self): 604 | return self._edge 605 | 606 | def _set_edge(self, edge): 607 | if not isinstance(edge, str): 608 | raise TypeError("Invalid edge type, should be string.") 609 | if edge.lower() not in ["none", "rising", "falling", "both"]: 610 | raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") 611 | 612 | if self._direction != "in": 613 | raise GPIOError(None, "Invalid operation: cannot set edge on output GPIO") 614 | 615 | if self._edge == edge: 616 | return 617 | 618 | self._reopen("in", edge) 619 | 620 | edge = property(_get_edge, _set_edge) 621 | 622 | # String representation 623 | 624 | def __str__(self): 625 | try: 626 | str_name = self.name 627 | except GPIOError: 628 | str_name = "" 629 | 630 | try: 631 | str_direction = self.direction 632 | except GPIOError: 633 | str_direction = "" 634 | 635 | try: 636 | str_edge = self.edge 637 | except GPIOError: 638 | str_edge = "" 639 | 640 | try: 641 | str_chip_name = self.chip_name 642 | except GPIOError: 643 | str_chip_name = "" 644 | 645 | try: 646 | str_chip_label = self.chip_label 647 | except GPIOError: 648 | str_chip_label = "" 649 | 650 | return "GPIO {:d} (name=\"{:s}\", device={:s}, line_fd={:d}, chip_fd={:d}, direction={:s}, edge={:s}, chip_name=\"{:s}\", chip_label=\"{:s}\", type=cdev)" \ 651 | .format(self._line, str_name, self._devpath, self._line_fd, self._chip_fd, str_direction, str_edge, str_chip_name, str_chip_label) 652 | 653 | 654 | class SysfsGPIO(GPIO): 655 | # Number of retries to check for GPIO export or direction write on open 656 | GPIO_OPEN_RETRIES = 10 657 | # Delay between check for GPIO export or direction write on open (100ms) 658 | GPIO_OPEN_DELAY = 0.1 659 | 660 | def __init__(self, line, direction): 661 | """**Sysfs GPIO** 662 | 663 | Instantiate a GPIO object and open the sysfs GPIO with the specified 664 | line and direction. 665 | 666 | `direction` can be "in" for input; "out" for output, initialized to 667 | low; "high" for output, initialized to high; or "low" for output, 668 | initialized to low. 669 | 670 | Args: 671 | line (int): GPIO line number. 672 | direction (str): GPIO direction, can be "in", "out", "high", or 673 | "low", 674 | 675 | Returns: 676 | SysfsGPIO: GPIO object. 677 | 678 | Raises: 679 | GPIOError: if an I/O or OS error occurs. 680 | TypeError: if `line` or `direction` types are invalid. 681 | ValueError: if `direction` value is invalid. 682 | TimeoutError: if waiting for GPIO export times out. 683 | 684 | """ 685 | self._fd = None 686 | self._line = None 687 | 688 | self._open(line, direction) 689 | 690 | def __new__(self, line, direction): 691 | return object.__new__(SysfsGPIO) 692 | 693 | def _open(self, line, direction): 694 | if not isinstance(line, int): 695 | raise TypeError("Invalid line type, should be integer.") 696 | if not isinstance(direction, str): 697 | raise TypeError("Invalid direction type, should be string.") 698 | if direction.lower() not in ["in", "out", "high", "low"]: 699 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 700 | 701 | gpio_path = "/sys/class/gpio/gpio{:d}".format(line) 702 | 703 | if not os.path.isdir(gpio_path): 704 | # Export the line 705 | try: 706 | with open("/sys/class/gpio/export", "w") as f_export: 707 | f_export.write("{:d}\n".format(line)) 708 | except IOError as e: 709 | raise GPIOError(e.errno, "Exporting GPIO: " + e.strerror) 710 | 711 | # Loop until GPIO is exported 712 | exported = False 713 | for i in range(SysfsGPIO.GPIO_OPEN_RETRIES): 714 | if os.path.isdir(gpio_path): 715 | exported = True 716 | break 717 | 718 | time.sleep(SysfsGPIO.GPIO_OPEN_DELAY) 719 | 720 | if not exported: 721 | raise TimeoutError("Exporting GPIO: waiting for \"{:s}\" timed out".format(gpio_path)) 722 | 723 | # Write direction, looping in case of EACCES errors due to delayed udev 724 | # permission rule application after export 725 | for i in range(SysfsGPIO.GPIO_OPEN_RETRIES): 726 | try: 727 | with open(os.path.join(gpio_path, "direction"), "w") as f_direction: 728 | f_direction.write(direction.lower() + "\n") 729 | break 730 | except IOError as e: 731 | if e.errno != errno.EACCES or (e.errno == errno.EACCES and i == SysfsGPIO.GPIO_OPEN_RETRIES - 1): 732 | raise GPIOError(e.errno, "Setting GPIO direction: " + e.strerror) 733 | 734 | time.sleep(SysfsGPIO.GPIO_OPEN_DELAY) 735 | else: 736 | # Write direction 737 | try: 738 | with open(os.path.join(gpio_path, "direction"), "w") as f_direction: 739 | f_direction.write(direction.lower() + "\n") 740 | except IOError as e: 741 | raise GPIOError(e.errno, "Setting GPIO direction: " + e.strerror) 742 | 743 | # Open value 744 | try: 745 | self._fd = os.open(os.path.join(gpio_path, "value"), os.O_RDWR) 746 | except OSError as e: 747 | raise GPIOError(e.errno, "Opening GPIO: " + e.strerror) 748 | 749 | self._line = line 750 | self._path = gpio_path 751 | 752 | # Methods 753 | 754 | def read(self): 755 | # Read value 756 | try: 757 | buf = os.read(self._fd, 2) 758 | except OSError as e: 759 | raise GPIOError(e.errno, "Reading GPIO: " + e.strerror) 760 | 761 | # Rewind 762 | try: 763 | os.lseek(self._fd, 0, os.SEEK_SET) 764 | except OSError as e: 765 | raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) 766 | 767 | if buf[0] == b"0"[0]: 768 | return False 769 | elif buf[0] == b"1"[0]: 770 | return True 771 | 772 | raise GPIOError(None, "Unknown GPIO value: {}".format(buf)) 773 | 774 | def write(self, value): 775 | if not isinstance(value, bool): 776 | raise TypeError("Invalid value type, should be bool.") 777 | 778 | # Write value 779 | try: 780 | if value: 781 | os.write(self._fd, b"1\n") 782 | else: 783 | os.write(self._fd, b"0\n") 784 | except OSError as e: 785 | raise GPIOError(e.errno, "Writing GPIO: " + e.strerror) 786 | 787 | # Rewind 788 | try: 789 | os.lseek(self._fd, 0, os.SEEK_SET) 790 | except OSError as e: 791 | raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) 792 | 793 | def poll(self, timeout=None): 794 | if not isinstance(timeout, (int, float, type(None))): 795 | raise TypeError("Invalid timeout type, should be integer, float, or None.") 796 | 797 | # Setup poll 798 | p = select.poll() 799 | p.register(self._fd, select.POLLPRI | select.POLLERR) 800 | 801 | # Scale timeout to milliseconds 802 | if isinstance(timeout, (int, float)) and timeout > 0: 803 | timeout *= 1000 804 | 805 | # Poll 806 | events = p.poll(timeout) 807 | 808 | # If GPIO edge interrupt occurred 809 | if events: 810 | # Rewind 811 | try: 812 | os.lseek(self._fd, 0, os.SEEK_SET) 813 | except OSError as e: 814 | raise GPIOError(e.errno, "Rewinding GPIO: " + e.strerror) 815 | 816 | return True 817 | 818 | return False 819 | 820 | def read_event(self): 821 | raise NotImplementedError() 822 | 823 | def close(self): 824 | if self._fd is None: 825 | return 826 | 827 | try: 828 | os.close(self._fd) 829 | except OSError as e: 830 | raise GPIOError(e.errno, "Closing GPIO: " + e.strerror) 831 | 832 | self._fd = None 833 | 834 | # Unexport the line 835 | try: 836 | unexport_fd = os.open("/sys/class/gpio/unexport", os.O_WRONLY) 837 | os.write(unexport_fd, "{:d}\n".format(self._line).encode()) 838 | os.close(unexport_fd) 839 | except OSError as e: 840 | raise GPIOError(e.errno, "Unexporting GPIO: " + e.strerror) 841 | 842 | # Immutable properties 843 | 844 | @property 845 | def devpath(self): 846 | return self._path 847 | 848 | @property 849 | def fd(self): 850 | return self._fd 851 | 852 | @property 853 | def line(self): 854 | return self._line 855 | 856 | @property 857 | def name(self): 858 | return "" 859 | 860 | @property 861 | def chip_fd(self): 862 | raise NotImplementedError("Sysfs GPIO does not have a gpiochip file descriptor.") 863 | 864 | @property 865 | def chip_name(self): 866 | gpio_path = os.path.join(self._path, "device") 867 | 868 | gpiochip_path = os.readlink(gpio_path) 869 | 870 | if '/' not in gpiochip_path: 871 | raise GPIOError(None, "Reading gpiochip name: invalid device symlink \"{:s}\"".format(gpiochip_path)) 872 | 873 | return gpiochip_path.split('/')[-1] 874 | 875 | @property 876 | def chip_label(self): 877 | gpio_path = "/sys/class/gpio/{:s}/label".format(self.chip_name) 878 | 879 | try: 880 | with open(gpio_path, "r") as f_label: 881 | label = f_label.read() 882 | except (GPIOError, IOError) as e: 883 | if isinstance(e, IOError): 884 | raise GPIOError(e.errno, "Reading gpiochip label: " + e.strerror) 885 | 886 | raise GPIOError(None, "Reading gpiochip label: " + e.strerror) 887 | 888 | return label.strip() 889 | 890 | # Mutable properties 891 | 892 | def _get_direction(self): 893 | # Read direction 894 | try: 895 | with open(os.path.join(self._path, "direction"), "r") as f_direction: 896 | direction = f_direction.read() 897 | except IOError as e: 898 | raise GPIOError(e.errno, "Getting GPIO direction: " + e.strerror) 899 | 900 | return direction.strip() 901 | 902 | def _set_direction(self, direction): 903 | if not isinstance(direction, str): 904 | raise TypeError("Invalid direction type, should be string.") 905 | if direction.lower() not in ["in", "out", "high", "low"]: 906 | raise ValueError("Invalid direction, can be: \"in\", \"out\", \"high\", \"low\".") 907 | 908 | # Write direction 909 | try: 910 | with open(os.path.join(self._path, "direction"), "w") as f_direction: 911 | f_direction.write(direction.lower() + "\n") 912 | except IOError as e: 913 | raise GPIOError(e.errno, "Setting GPIO direction: " + e.strerror) 914 | 915 | direction = property(_get_direction, _set_direction) 916 | 917 | def _get_edge(self): 918 | # Read edge 919 | try: 920 | with open(os.path.join(self._path, "edge"), "r") as f_edge: 921 | edge = f_edge.read() 922 | except IOError as e: 923 | raise GPIOError(e.errno, "Getting GPIO edge: " + e.strerror) 924 | 925 | return edge.strip() 926 | 927 | def _set_edge(self, edge): 928 | if not isinstance(edge, str): 929 | raise TypeError("Invalid edge type, should be string.") 930 | if edge.lower() not in ["none", "rising", "falling", "both"]: 931 | raise ValueError("Invalid edge, can be: \"none\", \"rising\", \"falling\", \"both\".") 932 | 933 | # Write edge 934 | try: 935 | with open(os.path.join(self._path, "edge"), "w") as f_edge: 936 | f_edge.write(edge.lower() + "\n") 937 | except IOError as e: 938 | raise GPIOError(e.errno, "Setting GPIO edge: " + e.strerror) 939 | 940 | edge = property(_get_edge, _set_edge) 941 | 942 | # String representation 943 | 944 | def __str__(self): 945 | try: 946 | str_direction = self.direction 947 | except GPIOError: 948 | str_direction = "" 949 | 950 | try: 951 | str_edge = self.edge 952 | except GPIOError: 953 | str_edge = "" 954 | 955 | try: 956 | str_chip_name = self.chip_name 957 | except GPIOError: 958 | str_chip_name = "" 959 | 960 | try: 961 | str_chip_label = self.chip_label 962 | except GPIOError: 963 | str_chip_label = "" 964 | 965 | return "GPIO {:d} (device={:s}, fd={:d}, direction={:s}, edge={:s}, chip_name=\"{:s}\", chip_label=\"{:s}\", type=sysfs)" \ 966 | .format(self._line, self._path, self._fd, str_direction, str_edge, str_chip_name, str_chip_label) 967 | -------------------------------------------------------------------------------- /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/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/mmio.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import mmap 4 | import ctypes 5 | import struct 6 | 7 | 8 | # Alias long to int on Python 3 9 | if sys.version_info[0] >= 3: 10 | long = int 11 | 12 | 13 | class MMIOError(IOError): 14 | """Base class for MMIO errors.""" 15 | pass 16 | 17 | 18 | class MMIO(object): 19 | def __init__(self, physaddr, size): 20 | """Instantiate an MMIO object and map the region of physical memory 21 | specified by the address base `physaddr` and size `size` in bytes. 22 | 23 | Args: 24 | physaddr (int, long): base physical address of memory region. 25 | size (int, long): size of memory region. 26 | 27 | Returns: 28 | MMIO: MMIO object. 29 | 30 | Raises: 31 | MMIOError: if an I/O or OS error occurs. 32 | TypeError: if `physaddr` or `size` types are invalid. 33 | 34 | """ 35 | self.mapping = None 36 | self._open(physaddr, size) 37 | 38 | def __del__(self): 39 | self.close() 40 | 41 | def __enter__(self): 42 | return self 43 | 44 | def __exit__(self, t, value, traceback): 45 | self.close() 46 | 47 | def _open(self, physaddr, size): 48 | if not isinstance(physaddr, (int, long)): 49 | raise TypeError("Invalid physaddr type, should be integer.") 50 | if not isinstance(size, (int, long)): 51 | raise TypeError("Invalid size type, should be integer.") 52 | 53 | pagesize = os.sysconf(os.sysconf_names['SC_PAGESIZE']) 54 | 55 | self._physaddr = physaddr 56 | self._size = size 57 | self._aligned_physaddr = physaddr - (physaddr % pagesize) 58 | self._aligned_size = size + (physaddr - self._aligned_physaddr) 59 | 60 | try: 61 | fd = os.open("/dev/mem", os.O_RDWR | os.O_SYNC) 62 | except OSError as e: 63 | raise MMIOError(e.errno, "Opening /dev/mem: " + e.strerror) 64 | 65 | try: 66 | self.mapping = mmap.mmap(fd, self._aligned_size, flags=mmap.MAP_SHARED, prot=(mmap.PROT_READ | mmap.PROT_WRITE), offset=self._aligned_physaddr) 67 | except OSError as e: 68 | raise MMIOError(e.errno, "Mapping /dev/mem: " + e.strerror) 69 | 70 | try: 71 | os.close(fd) 72 | except OSError as e: 73 | raise MMIOError(e.errno, "Closing /dev/mem: " + e.strerror) 74 | 75 | # Methods 76 | 77 | def _adjust_offset(self, offset): 78 | return offset + (self._physaddr - self._aligned_physaddr) 79 | 80 | def _validate_offset(self, offset, length): 81 | if (offset + length) > self._aligned_size: 82 | raise ValueError("Offset out of bounds.") 83 | 84 | def read32(self, offset): 85 | """Read 32-bits from the specified `offset` in bytes, relative to the 86 | base physical address of the MMIO region. 87 | 88 | Args: 89 | offset (int, long): offset from base physical address, in bytes. 90 | 91 | Returns: 92 | int: 32-bit value read. 93 | 94 | Raises: 95 | TypeError: if `offset` type is invalid. 96 | ValueError: if `offset` is out of bounds. 97 | 98 | """ 99 | if not isinstance(offset, (int, long)): 100 | raise TypeError("Invalid offset type, should be integer.") 101 | 102 | offset = self._adjust_offset(offset) 103 | self._validate_offset(offset, 4) 104 | return struct.unpack("=L", self.mapping[offset:offset + 4])[0] 105 | 106 | def read16(self, offset): 107 | """Read 16-bits from the specified `offset` in bytes, relative to the 108 | base physical address of the MMIO region. 109 | 110 | Args: 111 | offset (int, long): offset from base physical address, in bytes. 112 | 113 | Returns: 114 | int: 16-bit value read. 115 | 116 | Raises: 117 | TypeError: if `offset` type is invalid. 118 | ValueError: if `offset` is out of bounds. 119 | 120 | """ 121 | if not isinstance(offset, (int, long)): 122 | raise TypeError("Invalid offset type, should be integer.") 123 | 124 | offset = self._adjust_offset(offset) 125 | self._validate_offset(offset, 2) 126 | return struct.unpack("=H", self.mapping[offset:offset + 2])[0] 127 | 128 | def read8(self, offset): 129 | """Read 8-bits from the specified `offset` in bytes, relative to the 130 | base physical address of the MMIO region. 131 | 132 | Args: 133 | offset (int, long): offset from base physical address, in bytes. 134 | 135 | Returns: 136 | int: 8-bit value read. 137 | 138 | Raises: 139 | TypeError: if `offset` type is invalid. 140 | ValueError: if `offset` is out of bounds. 141 | 142 | """ 143 | if not isinstance(offset, (int, long)): 144 | raise TypeError("Invalid offset type, should be integer.") 145 | 146 | offset = self._adjust_offset(offset) 147 | self._validate_offset(offset, 1) 148 | return struct.unpack("B", self.mapping[offset:offset + 1])[0] 149 | 150 | def read(self, offset, length): 151 | """Read a string of bytes from the specified `offset` in bytes, 152 | relative to the base physical address of the MMIO region. 153 | 154 | Args: 155 | offset (int, long): offset from base physical address, in bytes. 156 | length (int): number of bytes to read. 157 | 158 | Returns: 159 | bytes: bytes read. 160 | 161 | Raises: 162 | TypeError: if `offset` type is invalid. 163 | ValueError: if `offset` is out of bounds. 164 | 165 | """ 166 | if not isinstance(offset, (int, long)): 167 | raise TypeError("Invalid offset type, should be integer.") 168 | 169 | offset = self._adjust_offset(offset) 170 | self._validate_offset(offset, length) 171 | return bytes(self.mapping[offset:offset + length]) 172 | 173 | def write32(self, offset, value): 174 | """Write 32-bits to the specified `offset` in bytes, relative to the 175 | base physical address of the MMIO region. 176 | 177 | Args: 178 | offset (int, long): offset from base physical address, in bytes. 179 | value (int, long): 32-bit value to write. 180 | 181 | Raises: 182 | TypeError: if `offset` or `value` type are invalid. 183 | ValueError: if `offset` or `value` are out of bounds. 184 | 185 | """ 186 | if not isinstance(offset, (int, long)): 187 | raise TypeError("Invalid offset type, should be integer.") 188 | if not isinstance(value, (int, long)): 189 | raise TypeError("Invalid value type, should be integer.") 190 | if value < 0 or value > 0xffffffff: 191 | raise ValueError("Value out of bounds.") 192 | 193 | offset = self._adjust_offset(offset) 194 | self._validate_offset(offset, 4) 195 | self.mapping[offset:offset + 4] = struct.pack("=L", value) 196 | 197 | def write16(self, offset, value): 198 | """Write 16-bits to the specified `offset` in bytes, relative to the 199 | base physical address of the MMIO region. 200 | 201 | Args: 202 | offset (int, long): offset from base physical address, in bytes. 203 | value (int, long): 16-bit value to write. 204 | 205 | Raises: 206 | TypeError: if `offset` or `value` type are invalid. 207 | ValueError: if `offset` or `value` are out of bounds. 208 | 209 | """ 210 | if not isinstance(offset, (int, long)): 211 | raise TypeError("Invalid offset type, should be integer.") 212 | if not isinstance(value, (int, long)): 213 | raise TypeError("Invalid value type, should be integer.") 214 | if value < 0 or value > 0xffff: 215 | raise ValueError("Value out of bounds.") 216 | 217 | offset = self._adjust_offset(offset) 218 | self._validate_offset(offset, 2) 219 | self.mapping[offset:offset + 2] = struct.pack("=H", value) 220 | 221 | def write8(self, offset, value): 222 | """Write 8-bits to the specified `offset` in bytes, relative to the 223 | base physical address of the MMIO region. 224 | 225 | Args: 226 | offset (int, long): offset from base physical address, in bytes. 227 | value (int, long): 8-bit value to write. 228 | 229 | Raises: 230 | TypeError: if `offset` or `value` type are invalid. 231 | ValueError: if `offset` or `value` are out of bounds. 232 | 233 | """ 234 | if not isinstance(offset, (int, long)): 235 | raise TypeError("Invalid offset type, should be integer.") 236 | if not isinstance(value, (int, long)): 237 | raise TypeError("Invalid value type, should be integer.") 238 | if value < 0 or value > 0xff: 239 | raise ValueError("Value out of bounds.") 240 | 241 | offset = self._adjust_offset(offset) 242 | self._validate_offset(offset, 1) 243 | self.mapping[offset:offset + 1] = struct.pack("B", value) 244 | 245 | def write(self, offset, data): 246 | """Write a string of bytes to the specified `offset` in bytes, relative 247 | to the base physical address of the MMIO region. 248 | 249 | Args: 250 | offset (int, long): offset from base physical address, in bytes. 251 | data (bytes, bytearray, list): a byte array or list of 8-bit 252 | integers to write. 253 | 254 | Raises: 255 | TypeError: if `offset` or `data` type are invalid. 256 | ValueError: if `offset` is out of bounds, or if data is not valid bytes. 257 | 258 | """ 259 | if not isinstance(offset, (int, long)): 260 | raise TypeError("Invalid offset type, should be integer.") 261 | if not isinstance(data, (bytes, bytearray, list)): 262 | raise TypeError("Invalid data type, expected bytes, bytearray, or list.") 263 | 264 | offset = self._adjust_offset(offset) 265 | self._validate_offset(offset, len(data)) 266 | 267 | data = bytes(bytearray(data)) 268 | self.mapping[offset:offset + len(data)] = data 269 | 270 | def close(self): 271 | """Unmap the MMIO object's mapped physical memory.""" 272 | if self.mapping is None: 273 | return 274 | 275 | self.mapping.close() 276 | self.mapping = None 277 | 278 | self._fd = None 279 | 280 | # Immutable properties 281 | 282 | @property 283 | def base(self): 284 | """Get the base physical address of the MMIO region. 285 | 286 | :type: int 287 | """ 288 | return self._physaddr 289 | 290 | @property 291 | def size(self): 292 | """Get the mapping size of the MMIO region. 293 | 294 | :type: int 295 | """ 296 | return self._size 297 | 298 | @property 299 | def pointer(self): 300 | """Get a ctypes void pointer to the memory mapped region. 301 | 302 | :type: ctypes.c_void_p 303 | """ 304 | return ctypes.cast(ctypes.pointer(ctypes.c_uint8.from_buffer(self.mapping, 0)), ctypes.c_void_p) 305 | 306 | # String representation 307 | 308 | def __str__(self): 309 | return "MMIO 0x{:08x} (size={:d})".format(self.base, self.size) 310 | -------------------------------------------------------------------------------- /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/serial.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fcntl 3 | import array 4 | import termios 5 | import select 6 | 7 | 8 | class SerialError(IOError): 9 | """Base class for Serial errors.""" 10 | pass 11 | 12 | 13 | class Serial(object): 14 | _DATABITS_TO_CFLAG = { 15 | 5: termios.CS5, 6: termios.CS6, 7: termios.CS7, 8: termios.CS8 16 | } 17 | _CFLAG_TO_DATABITS = {v: k for k, v in _DATABITS_TO_CFLAG.items()} 18 | 19 | _BAUDRATE_TO_OSPEED = { 20 | 50: termios.B50, 75: termios.B75, 110: termios.B110, 134: termios.B134, 21 | 150: termios.B150, 200: termios.B200, 300: termios.B300, 22 | 600: termios.B600, 1200: termios.B1200, 1800: termios.B1800, 23 | 2400: termios.B2400, 4800: termios.B4800, 9600: termios.B9600, 24 | 19200: termios.B19200, 38400: termios.B38400, 57600: termios.B57600, 25 | 115200: termios.B115200, 230400: termios.B230400, 26 | # Linux baudrates bits missing in termios module included below 27 | 460800: 0x1004, 500000: 0x1005, 576000: 0x1006, 28 | 921600: 0x1007, 1000000: 0x1008, 1152000: 0x1009, 29 | 1500000: 0x100A, 2000000: 0x100B, 2500000: 0x100C, 30 | 3000000: 0x100D, 3500000: 0x100E, 4000000: 0x100F, 31 | } 32 | _OSPEED_TO_BAUDRATE = {v: k for k, v in _BAUDRATE_TO_OSPEED.items()} 33 | 34 | def __init__(self, devpath, baudrate, databits=8, parity="none", stopbits=1, xonxoff=False, rtscts=False): 35 | """Instantiate a Serial object and open the tty device at the specified 36 | path with the specified baudrate, and the defaults of 8 data bits, no 37 | parity, 1 stop bit, no software flow control (xonxoff), and no hardware 38 | flow control (rtscts). 39 | 40 | Args: 41 | devpath (str): tty device path. 42 | baudrate (int): baudrate. 43 | databits (int): data bits, can be 5, 6, 7, 8. 44 | parity (str): parity, can be "none", "even", "odd". 45 | stopbits (int): stop bits, can be 1 or 2. 46 | xonxoff (bool): software flow control. 47 | rtscts (bool): hardware flow control. 48 | 49 | Returns: 50 | Serial: Serial object. 51 | 52 | Raises: 53 | SerialError: if an I/O or OS error occurs. 54 | TypeError: if `devpath`, `baudrate`, `databits`, `parity`, `stopbits`, `xonxoff`, or `rtscts` types are invalid. 55 | ValueError: if `baudrate`, `databits`, `parity`, or `stopbits` values are invalid. 56 | 57 | """ 58 | self._fd = None 59 | self._devpath = None 60 | self._open(devpath, baudrate, databits, parity, stopbits, xonxoff, rtscts) 61 | 62 | def __del__(self): 63 | self.close() 64 | 65 | def __enter__(self): 66 | return self 67 | 68 | def __exit__(self, t, value, traceback): 69 | self.close() 70 | 71 | def _open(self, devpath, baudrate, databits, parity, stopbits, xonxoff, rtscts): 72 | if not isinstance(devpath, str): 73 | raise TypeError("Invalid devpath type, should be string.") 74 | elif not isinstance(baudrate, int): 75 | raise TypeError("Invalid baud rate type, should be integer.") 76 | elif not isinstance(databits, int): 77 | raise TypeError("Invalid data bits type, should be integer.") 78 | elif not isinstance(parity, str): 79 | raise TypeError("Invalid parity type, should be string.") 80 | elif not isinstance(stopbits, int): 81 | raise TypeError("Invalid stop bits type, should be integer.") 82 | elif not isinstance(xonxoff, bool): 83 | raise TypeError("Invalid xonxoff type, should be boolean.") 84 | elif not isinstance(rtscts, bool): 85 | raise TypeError("Invalid rtscts type, should be boolean.") 86 | 87 | if baudrate not in Serial._BAUDRATE_TO_OSPEED: 88 | raise ValueError("Unknown baud rate: {:d}".format(baudrate)) 89 | elif databits not in [5, 6, 7, 8]: 90 | raise ValueError("Invalid data bits, can be 5, 6, 7, 8.") 91 | elif parity.lower() not in ["none", "even", "odd"]: 92 | raise ValueError("Invalid parity, can be: \"none\", \"even\", \"odd\".") 93 | elif stopbits not in [1, 2]: 94 | raise ValueError("Invalid stop bits, can be 1, 2.") 95 | 96 | # Open tty 97 | try: 98 | self._fd = os.open(devpath, os.O_RDWR | os.O_NOCTTY) 99 | except OSError as e: 100 | raise SerialError(e.errno, "Opening serial port: " + e.strerror) 101 | 102 | self._devpath = devpath 103 | 104 | parity = parity.lower() 105 | 106 | (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = (0, 0, 0, 0, 0, 0, [0] * 32) 107 | 108 | ### 109 | # iflag 110 | 111 | # Ignore break characters 112 | iflag = termios.IGNBRK 113 | 114 | # Setup parity 115 | if parity != "none": 116 | iflag |= (termios.INPCK | termios.ISTRIP) 117 | 118 | # Setup xonxoff 119 | if xonxoff: 120 | iflag |= (termios.IXON | termios.IXOFF) 121 | 122 | ####### 123 | # oflag 124 | oflag = 0 125 | 126 | ####### 127 | # lflag 128 | lflag = 0 129 | 130 | ####### 131 | # cflag 132 | 133 | # Enable receiver, ignore modem control lines 134 | cflag = (termios.CREAD | termios.CLOCAL) 135 | 136 | # Setup data bits 137 | cflag |= Serial._DATABITS_TO_CFLAG[databits] 138 | 139 | # Setup parity 140 | if parity == "even": 141 | cflag |= termios.PARENB 142 | elif parity == "odd": 143 | cflag |= (termios.PARENB | termios.PARODD) 144 | 145 | # Setup stop bits 146 | if stopbits == 2: 147 | cflag |= termios.CSTOPB 148 | 149 | # Setup rtscts 150 | if rtscts: 151 | cflag |= termios.CRTSCTS 152 | 153 | # Setup baud rate 154 | cflag |= Serial._BAUDRATE_TO_OSPEED[baudrate] 155 | 156 | ######## 157 | # ispeed 158 | ispeed = Serial._BAUDRATE_TO_OSPEED[baudrate] 159 | 160 | ######## 161 | # ospeed 162 | ospeed = Serial._BAUDRATE_TO_OSPEED[baudrate] 163 | 164 | # Set tty attributes 165 | try: 166 | termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) 167 | except termios.error as e: 168 | raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) 169 | 170 | # Methods 171 | 172 | def read(self, length, timeout=None): 173 | """Read up to `length` number of bytes from the serial port with an 174 | optional timeout. 175 | 176 | `timeout` can be positive for a timeout in seconds, 0 for a 177 | non-blocking read, or negative or None for a blocking read that will 178 | block until `length` number of bytes are read. Default is a blocking 179 | read. 180 | 181 | For a non-blocking or timeout-bound read, read() may return data whose 182 | length is less than or equal to the requested length. 183 | 184 | Args: 185 | length (int): length in bytes. 186 | timeout (int, float, None): timeout duration in seconds. 187 | 188 | Returns: 189 | bytes: data read. 190 | 191 | Raises: 192 | SerialError: if an I/O or OS error occurs. 193 | 194 | """ 195 | data = b"" 196 | 197 | # Read length bytes if timeout is None 198 | 199 | # Read up to length bytes if timeout is not None 200 | while True: 201 | if timeout is not None: 202 | # Select 203 | (rlist, _, _) = select.select([self._fd], [], [], timeout) 204 | # If timeout 205 | if self._fd not in rlist: 206 | break 207 | 208 | try: 209 | data += os.read(self._fd, length - len(data)) 210 | except OSError as e: 211 | raise SerialError(e.errno, "Reading serial port: " + e.strerror) 212 | 213 | if len(data) == length: 214 | break 215 | 216 | return data 217 | 218 | def write(self, data): 219 | """Write `data` to the serial port and return the number of bytes 220 | written. 221 | 222 | Args: 223 | data (bytes, bytearray, list): a byte array or list of 8-bit integers to write. 224 | 225 | Returns: 226 | int: number of bytes written. 227 | 228 | Raises: 229 | SerialError: if an I/O or OS error occurs. 230 | TypeError: if `data` type is invalid. 231 | ValueError: if data is not valid bytes. 232 | 233 | """ 234 | if not isinstance(data, (bytes, bytearray, list)): 235 | raise TypeError("Invalid data type, should be bytes, bytearray, or list.") 236 | 237 | if isinstance(data, list): 238 | data = bytearray(data) 239 | 240 | try: 241 | return os.write(self._fd, data) 242 | except OSError as e: 243 | raise SerialError(e.errno, "Writing serial port: " + e.strerror) 244 | 245 | def poll(self, timeout=None): 246 | """Poll for data available for reading from the serial port. 247 | 248 | `timeout` can be positive for a timeout in seconds, 0 for a 249 | non-blocking poll, or negative or None for a blocking poll. Default is 250 | a blocking poll. 251 | 252 | Args: 253 | timeout (int, float, None): timeout duration in seconds. 254 | 255 | Returns: 256 | bool: ``True`` if data is available for reading from the serial port, ``False`` if not. 257 | 258 | """ 259 | p = select.poll() 260 | p.register(self._fd, select.POLLIN | select.POLLPRI) 261 | events = p.poll(int(timeout * 1000)) 262 | 263 | if len(events) > 0: 264 | return True 265 | 266 | return False 267 | 268 | def flush(self): 269 | """Flush the write buffer of the serial port, blocking until all bytes 270 | are written. 271 | 272 | Raises: 273 | SerialError: if an I/O or OS error occurs. 274 | 275 | """ 276 | try: 277 | termios.tcdrain(self._fd) 278 | except termios.error as e: 279 | raise SerialError(e.errno, "Flushing serial port: " + e.strerror) 280 | 281 | def input_waiting(self): 282 | """Query the number of bytes waiting to be read from the serial port. 283 | 284 | Returns: 285 | int: number of bytes waiting to be read. 286 | 287 | Raises: 288 | SerialError: if an I/O or OS error occurs. 289 | 290 | """ 291 | # Get input waiting 292 | buf = array.array('I', [0]) 293 | try: 294 | fcntl.ioctl(self._fd, termios.TIOCINQ, buf, True) 295 | except (OSError, IOError) as e: 296 | raise SerialError(e.errno, "Querying input waiting: " + e.strerror) 297 | 298 | return buf[0] 299 | 300 | def output_waiting(self): 301 | """Query the number of bytes waiting to be written to the serial port. 302 | 303 | Returns: 304 | int: number of bytes waiting to be written. 305 | 306 | Raises: 307 | SerialError: if an I/O or OS error occurs. 308 | 309 | """ 310 | # Get input waiting 311 | buf = array.array('I', [0]) 312 | try: 313 | fcntl.ioctl(self._fd, termios.TIOCOUTQ, buf, True) 314 | except (OSError, IOError) as e: 315 | raise SerialError(e.errno, "Querying output waiting: " + e.strerror) 316 | 317 | return buf[0] 318 | 319 | def close(self): 320 | """Close the tty device. 321 | 322 | Raises: 323 | SerialError: if an I/O or OS error occurs. 324 | 325 | """ 326 | if self._fd is None: 327 | return 328 | 329 | try: 330 | os.close(self._fd) 331 | except OSError as e: 332 | raise SerialError(e.errno, "Closing serial port: " + e.strerror) 333 | 334 | self._fd = None 335 | 336 | # Immutable properties 337 | 338 | @property 339 | def fd(self): 340 | """Get the file descriptor of the underlying tty device. 341 | 342 | :type: int 343 | """ 344 | return self._fd 345 | 346 | @property 347 | def devpath(self): 348 | """Get the device path of the underlying tty device. 349 | 350 | :type: str 351 | """ 352 | return self._devpath 353 | 354 | # Mutable properties 355 | 356 | def _get_baudrate(self): 357 | # Get tty attributes 358 | try: 359 | (_, _, _, _, _, ospeed, _) = termios.tcgetattr(self._fd) 360 | except termios.error as e: 361 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 362 | 363 | if ospeed not in Serial._OSPEED_TO_BAUDRATE: 364 | raise SerialError(None, "Unknown baud rate: ospeed 0x{:x}".format(ospeed)) 365 | 366 | return Serial._OSPEED_TO_BAUDRATE[ospeed] 367 | 368 | def _set_baudrate(self, baudrate): 369 | if not isinstance(baudrate, int): 370 | raise TypeError("Invalid baud rate type, should be integer.") 371 | 372 | if baudrate not in Serial._BAUDRATE_TO_OSPEED: 373 | raise ValueError("Unknown baud rate: {:d}".format(baudrate)) 374 | 375 | # Get tty attributes 376 | try: 377 | (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) 378 | except termios.error as e: 379 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 380 | 381 | # Modify tty attributes 382 | cflag &= ~(termios.CBAUD | termios.CBAUDEX) 383 | cflag |= Serial._BAUDRATE_TO_OSPEED[baudrate] 384 | ispeed = Serial._BAUDRATE_TO_OSPEED[baudrate] 385 | ospeed = Serial._BAUDRATE_TO_OSPEED[baudrate] 386 | 387 | # Set tty attributes 388 | try: 389 | termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) 390 | except termios.error as e: 391 | raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) 392 | 393 | baudrate = property(_get_baudrate, _set_baudrate) 394 | """Get or set the baudrate. 395 | 396 | Raises: 397 | SerialError: if an I/O or OS error occurs. 398 | TypeError: if `baudrate` type is not int. 399 | ValueError: if `baudrate` value is not supported. 400 | 401 | :type: int 402 | """ 403 | 404 | def _get_databits(self): 405 | # Get tty attributes 406 | try: 407 | (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) 408 | except termios.error as e: 409 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 410 | 411 | cs = cflag & termios.CSIZE 412 | 413 | if cs not in Serial._CFLAG_TO_DATABITS: 414 | raise SerialError(None, "Unknown data bits setting: csize 0x{:x}".format(cs)) 415 | 416 | return Serial._CFLAG_TO_DATABITS[cs] 417 | 418 | def _set_databits(self, databits): 419 | if not isinstance(databits, int): 420 | raise TypeError("Invalid data bits type, should be integer.") 421 | elif databits not in [5, 6, 7, 8]: 422 | raise ValueError("Invalid data bits, can be 5, 6, 7, 8.") 423 | 424 | # Get tty attributes 425 | try: 426 | (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) 427 | except termios.error as e: 428 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 429 | 430 | # Modify tty attributes 431 | cflag &= ~termios.CSIZE 432 | cflag |= Serial._DATABITS_TO_CFLAG[databits] 433 | 434 | # Set tty attributes 435 | try: 436 | termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) 437 | except termios.error as e: 438 | raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) 439 | 440 | databits = property(_get_databits, _set_databits) 441 | """Get or set the data bits. Can be 5, 6, 7, 8. 442 | 443 | Raises: 444 | SerialError: if an I/O or OS error occurs. 445 | TypeError: if `databits` type is not int. 446 | ValueError: if `databits` value is invalid. 447 | 448 | :type: int 449 | """ 450 | 451 | def _get_parity(self): 452 | # Get tty attributes 453 | try: 454 | (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) 455 | except termios.error as e: 456 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 457 | 458 | if (cflag & termios.PARENB) == 0: 459 | return "none" 460 | elif (cflag & termios.PARODD) == 0: 461 | return "even" 462 | else: 463 | return "odd" 464 | 465 | def _set_parity(self, parity): 466 | if not isinstance(parity, str): 467 | raise TypeError("Invalid parity type, should be string.") 468 | elif parity.lower() not in ["none", "even", "odd"]: 469 | raise ValueError("Invalid parity, can be: \"none\", \"even\", \"odd\".") 470 | 471 | parity = parity.lower() 472 | 473 | # Get tty attributes 474 | try: 475 | (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) 476 | except termios.error as e: 477 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 478 | 479 | # Modify tty attributes 480 | iflag &= ~(termios.INPCK | termios.ISTRIP) 481 | cflag &= ~(termios.PARENB | termios.PARODD) 482 | if parity != "none": 483 | iflag |= (termios.INPCK | termios.ISTRIP) 484 | cflag |= termios.PARENB 485 | if parity == "odd": 486 | cflag |= termios.PARODD 487 | 488 | # Set tty attributes 489 | try: 490 | termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) 491 | except termios.error as e: 492 | raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) 493 | 494 | parity = property(_get_parity, _set_parity) 495 | """Get or set the parity. Can be "none", "even", "odd". 496 | 497 | Raises: 498 | SerialError: if an I/O or OS error occurs. 499 | TypeError: if `parity` type is not str. 500 | ValueError: if `parity` value is invalid. 501 | 502 | :type: str 503 | """ 504 | 505 | def _get_stopbits(self): 506 | # Get tty attributes 507 | try: 508 | (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) 509 | except termios.error as e: 510 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 511 | 512 | if (cflag & termios.CSTOPB) != 0: 513 | return 2 514 | else: 515 | return 1 516 | 517 | def _set_stopbits(self, stopbits): 518 | if not isinstance(stopbits, int): 519 | raise TypeError("Invalid stop bits type, should be integer.") 520 | elif stopbits not in [1, 2]: 521 | raise ValueError("Invalid stop bits, can be 1, 2.") 522 | 523 | # Get tty attributes 524 | try: 525 | (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) 526 | except termios.error as e: 527 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 528 | 529 | # Modify tty attributes 530 | cflag &= ~termios.CSTOPB 531 | if stopbits == 2: 532 | cflag |= termios.CSTOPB 533 | 534 | # Set tty attributes 535 | try: 536 | termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) 537 | except termios.error as e: 538 | raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) 539 | 540 | stopbits = property(_get_stopbits, _set_stopbits) 541 | """Get or set the stop bits. Can be 1 or 2. 542 | 543 | Raises: 544 | SerialError: if an I/O or OS error occurs. 545 | TypeError: if `stopbits` type is not int. 546 | ValueError: if `stopbits` value is invalid. 547 | 548 | :type: int 549 | """ 550 | 551 | def _get_xonxoff(self): 552 | # Get tty attributes 553 | try: 554 | (iflag, _, _, _, _, _, _) = termios.tcgetattr(self._fd) 555 | except termios.error as e: 556 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 557 | 558 | if (iflag & (termios.IXON | termios.IXOFF)) != 0: 559 | return True 560 | else: 561 | return False 562 | 563 | def _set_xonxoff(self, enabled): 564 | if not isinstance(enabled, bool): 565 | raise TypeError("Invalid enabled type, should be boolean.") 566 | 567 | # Get tty attributes 568 | try: 569 | (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) 570 | except termios.error as e: 571 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 572 | 573 | # Modify tty attributes 574 | iflag &= ~(termios.IXON | termios.IXOFF | termios.IXANY) 575 | if enabled: 576 | iflag |= (termios.IXON | termios.IXOFF) 577 | 578 | # Set tty attributes 579 | try: 580 | termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) 581 | except termios.error as e: 582 | raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) 583 | 584 | xonxoff = property(_get_xonxoff, _set_xonxoff) 585 | """Get or set software flow control. 586 | 587 | Raises: 588 | SerialError: if an I/O or OS error occurs. 589 | TypeError: if `xonxoff` type is not bool. 590 | 591 | :type: bool 592 | """ 593 | 594 | def _get_rtscts(self): 595 | # Get tty attributes 596 | try: 597 | (_, _, cflag, _, _, _, _) = termios.tcgetattr(self._fd) 598 | except termios.error as e: 599 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 600 | 601 | if (cflag & termios.CRTSCTS) != 0: 602 | return True 603 | else: 604 | return False 605 | 606 | def _set_rtscts(self, enabled): 607 | if not isinstance(enabled, bool): 608 | raise TypeError("Invalid enabled type, should be boolean.") 609 | 610 | # Get tty attributes 611 | try: 612 | (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(self._fd) 613 | except termios.error as e: 614 | raise SerialError(e.errno, "Getting serial port attributes: " + e.strerror) 615 | 616 | # Modify tty attributes 617 | cflag = ~termios.CRTSCTS 618 | if enabled: 619 | cflag |= termios.CRTSCTS 620 | 621 | # Set tty attributes 622 | try: 623 | termios.tcsetattr(self._fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]) 624 | except termios.error as e: 625 | raise SerialError(e.errno, "Setting serial port attributes: " + e.strerror) 626 | 627 | rtscts = property(_get_rtscts, _set_rtscts) 628 | """Get or set hardware flow control. 629 | 630 | Raises: 631 | SerialError: if an I/O or OS error occurs. 632 | TypeError: if `rtscts` type is not bool. 633 | 634 | :type: bool 635 | """ 636 | 637 | # String representation 638 | 639 | def __str__(self): 640 | return "Serial (device={:s}, fd={:d}, baudrate={:d}, databits={:d}, parity={:s}, stopbits={:d}, xonxoff={:s}, rtscts={:s})" \ 641 | .format(self.devpath, self.fd, self.baudrate, self.databits, self.parity, self.stopbits, str(self.xonxoff), str(self.rtscts)) 642 | -------------------------------------------------------------------------------- /periphery/spi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import fcntl 3 | import array 4 | import ctypes 5 | 6 | 7 | class SPIError(IOError): 8 | """Base class for SPI errors.""" 9 | pass 10 | 11 | 12 | class _CSpiIocTransfer(ctypes.Structure): 13 | _fields_ = [ 14 | ('tx_buf', ctypes.c_ulonglong), 15 | ('rx_buf', ctypes.c_ulonglong), 16 | ('len', ctypes.c_uint), 17 | ('speed_hz', ctypes.c_uint), 18 | ('delay_usecs', ctypes.c_ushort), 19 | ('bits_per_word', ctypes.c_ubyte), 20 | ('cs_change', ctypes.c_ubyte), 21 | ('tx_nbits', ctypes.c_ubyte), 22 | ('rx_nbits', ctypes.c_ubyte), 23 | ('pad', ctypes.c_ushort), 24 | ] 25 | 26 | 27 | class SPI(object): 28 | # Constants scraped from 29 | _SPI_CPHA = 0x1 30 | _SPI_CPOL = 0x2 31 | _SPI_LSB_FIRST = 0x8 32 | _SPI_IOC_WR_MODE = 0x40016b01 33 | _SPI_IOC_RD_MODE = 0x80016b01 34 | _SPI_IOC_WR_MAX_SPEED_HZ = 0x40046b04 35 | _SPI_IOC_RD_MAX_SPEED_HZ = 0x80046b04 36 | _SPI_IOC_WR_BITS_PER_WORD = 0x40016b03 37 | _SPI_IOC_RD_BITS_PER_WORD = 0x80016b03 38 | _SPI_IOC_MESSAGE_1 = 0x40206b00 39 | 40 | def __init__(self, devpath, mode, max_speed, bit_order="msb", bits_per_word=8, extra_flags=0): 41 | """Instantiate a SPI object and open the spidev device at the specified 42 | path with the specified SPI mode, max speed in hertz, and the defaults 43 | of "msb" bit order and 8 bits per word. 44 | 45 | Args: 46 | devpath (str): spidev device path. 47 | mode (int): SPI mode, can be 0, 1, 2, 3. 48 | max_speed (int, float): maximum speed in Hertz. 49 | bit_order (str): bit order, can be "msb" or "lsb". 50 | bits_per_word (int): bits per word. 51 | extra_flags (int): extra spidev flags to be bitwise-ORed with the SPI mode. 52 | 53 | Returns: 54 | SPI: SPI object. 55 | 56 | Raises: 57 | SPIError: if an I/O or OS error occurs. 58 | TypeError: if `devpath`, `mode`, `max_speed`, `bit_order`, `bits_per_word`, or `extra_flags` types are invalid. 59 | ValueError: if `mode`, `bit_order`, `bits_per_word`, or `extra_flags` values are invalid. 60 | 61 | """ 62 | self._fd = None 63 | self._devpath = None 64 | self._open(devpath, mode, max_speed, bit_order, bits_per_word, extra_flags) 65 | 66 | def __del__(self): 67 | self.close() 68 | 69 | def __enter__(self): 70 | return self 71 | 72 | def __exit__(self, t, value, traceback): 73 | self.close() 74 | 75 | def _open(self, devpath, mode, max_speed, bit_order, bits_per_word, extra_flags): 76 | if not isinstance(devpath, str): 77 | raise TypeError("Invalid devpath type, should be string.") 78 | elif not isinstance(mode, int): 79 | raise TypeError("Invalid mode type, should be integer.") 80 | elif not isinstance(max_speed, (int, float)): 81 | raise TypeError("Invalid max_speed type, should be integer or float.") 82 | elif not isinstance(bit_order, str): 83 | raise TypeError("Invalid bit_order type, should be string.") 84 | elif not isinstance(bits_per_word, int): 85 | raise TypeError("Invalid bits_per_word type, should be integer.") 86 | elif not isinstance(extra_flags, int): 87 | raise TypeError("Invalid extra_flags type, should be integer.") 88 | 89 | if mode not in [0, 1, 2, 3]: 90 | raise ValueError("Invalid mode, can be 0, 1, 2, 3.") 91 | elif bit_order.lower() not in ["msb", "lsb"]: 92 | raise ValueError("Invalid bit_order, can be \"msb\" or \"lsb\".") 93 | elif bits_per_word < 0 or bits_per_word > 255: 94 | raise ValueError("Invalid bits_per_word, must be 0-255.") 95 | elif extra_flags < 0 or extra_flags > 255: 96 | raise ValueError("Invalid extra_flags, must be 0-255.") 97 | 98 | # Open spidev 99 | try: 100 | self._fd = os.open(devpath, os.O_RDWR) 101 | except OSError as e: 102 | raise SPIError(e.errno, "Opening SPI device: " + e.strerror) 103 | 104 | self._devpath = devpath 105 | 106 | bit_order = bit_order.lower() 107 | 108 | # Set mode, bit order, extra flags 109 | buf = array.array("B", [mode | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) | extra_flags]) 110 | try: 111 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) 112 | except (OSError, IOError) as e: 113 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 114 | 115 | # Set max speed 116 | buf = array.array("I", [int(max_speed)]) 117 | try: 118 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MAX_SPEED_HZ, buf, False) 119 | except (OSError, IOError) as e: 120 | raise SPIError(e.errno, "Setting SPI max speed: " + e.strerror) 121 | 122 | # Set bits per word 123 | buf = array.array("B", [bits_per_word]) 124 | try: 125 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_BITS_PER_WORD, buf, False) 126 | except (OSError, IOError) as e: 127 | raise SPIError(e.errno, "Setting SPI bits per word: " + e.strerror) 128 | 129 | # Methods 130 | 131 | def transfer(self, data): 132 | """Shift out `data` and return shifted in data. 133 | 134 | Args: 135 | data (bytes, bytearray, list): a byte array or list of 8-bit integers to shift out. 136 | 137 | Returns: 138 | bytes, bytearray, list: data shifted in. 139 | 140 | Raises: 141 | SPIError: if an I/O or OS error occurs. 142 | TypeError: if `data` type is invalid. 143 | ValueError: if data is not valid bytes. 144 | 145 | """ 146 | if not isinstance(data, (bytes, bytearray, list)): 147 | raise TypeError("Invalid data type, should be bytes, bytearray, or list.") 148 | 149 | # Create mutable array 150 | try: 151 | buf = array.array('B', data) 152 | except OverflowError: 153 | raise ValueError("Invalid data bytes.") 154 | 155 | buf_addr, buf_len = buf.buffer_info() 156 | 157 | # Prepare transfer structure 158 | spi_xfer = _CSpiIocTransfer() 159 | spi_xfer.tx_buf = buf_addr 160 | spi_xfer.rx_buf = buf_addr 161 | spi_xfer.len = buf_len 162 | 163 | # Transfer 164 | try: 165 | fcntl.ioctl(self._fd, SPI._SPI_IOC_MESSAGE_1, spi_xfer) 166 | except (OSError, IOError) as e: 167 | raise SPIError(e.errno, "SPI transfer: " + e.strerror) 168 | 169 | # Return shifted out data with the same type as shifted in data 170 | if isinstance(data, bytes): 171 | return bytes(bytearray(buf)) 172 | elif isinstance(data, bytearray): 173 | return bytearray(buf) 174 | elif isinstance(data, list): 175 | return buf.tolist() 176 | 177 | def close(self): 178 | """Close the spidev SPI device. 179 | 180 | Raises: 181 | SPIError: if an I/O or OS error occurs. 182 | 183 | """ 184 | if self._fd is None: 185 | return 186 | 187 | try: 188 | os.close(self._fd) 189 | except OSError as e: 190 | raise SPIError(e.errno, "Closing SPI device: " + e.strerror) 191 | 192 | self._fd = None 193 | 194 | # Immutable properties 195 | 196 | @property 197 | def fd(self): 198 | """Get the file descriptor of the underlying spidev device. 199 | 200 | :type: int 201 | """ 202 | return self._fd 203 | 204 | @property 205 | def devpath(self): 206 | """Get the device path of the underlying spidev device. 207 | 208 | :type: str 209 | """ 210 | return self._devpath 211 | 212 | # Mutable properties 213 | 214 | def _get_mode(self): 215 | buf = array.array('B', [0]) 216 | 217 | # Get mode 218 | try: 219 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 220 | except (OSError, IOError) as e: 221 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 222 | 223 | return buf[0] & 0x3 224 | 225 | def _set_mode(self, mode): 226 | if not isinstance(mode, int): 227 | raise TypeError("Invalid mode type, should be integer.") 228 | if mode not in [0, 1, 2, 3]: 229 | raise ValueError("Invalid mode, can be 0, 1, 2, 3.") 230 | 231 | # Read-modify-write mode, because the mode contains bits for other settings 232 | 233 | # Get mode 234 | buf = array.array('B', [0]) 235 | try: 236 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 237 | except (OSError, IOError) as e: 238 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 239 | 240 | buf[0] = (buf[0] & ~(SPI._SPI_CPOL | SPI._SPI_CPHA)) | mode 241 | 242 | # Set mode 243 | try: 244 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) 245 | except (OSError, IOError) as e: 246 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 247 | 248 | mode = property(_get_mode, _set_mode) 249 | """Get or set the SPI mode. Can be 0, 1, 2, 3. 250 | 251 | Raises: 252 | SPIError: if an I/O or OS error occurs. 253 | TypeError: if `mode` type is not int. 254 | ValueError: if `mode` value is invalid. 255 | 256 | :type: int 257 | """ 258 | 259 | def _get_max_speed(self): 260 | # Get max speed 261 | buf = array.array('I', [0]) 262 | try: 263 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MAX_SPEED_HZ, buf, True) 264 | except (OSError, IOError) as e: 265 | raise SPIError(e.errno, "Getting SPI max speed: " + e.strerror) 266 | 267 | return buf[0] 268 | 269 | def _set_max_speed(self, max_speed): 270 | if not isinstance(max_speed, (int, float)): 271 | raise TypeError("Invalid max_speed type, should be integer or float.") 272 | 273 | # Set max speed 274 | buf = array.array('I', [int(max_speed)]) 275 | try: 276 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MAX_SPEED_HZ, buf, False) 277 | except (OSError, IOError) as e: 278 | raise SPIError(e.errno, "Setting SPI max speed: " + e.strerror) 279 | 280 | max_speed = property(_get_max_speed, _set_max_speed) 281 | """Get or set the maximum speed in Hertz. 282 | 283 | Raises: 284 | SPIError: if an I/O or OS error occurs. 285 | TypeError: if `max_speed` type is not int or float. 286 | 287 | :type: int, float 288 | """ 289 | 290 | def _get_bit_order(self): 291 | # Get mode 292 | buf = array.array('B', [0]) 293 | try: 294 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 295 | except (OSError, IOError) as e: 296 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 297 | 298 | if (buf[0] & SPI._SPI_LSB_FIRST) > 0: 299 | return "lsb" 300 | 301 | return "msb" 302 | 303 | def _set_bit_order(self, bit_order): 304 | if not isinstance(bit_order, str): 305 | raise TypeError("Invalid bit_order type, should be string.") 306 | elif bit_order.lower() not in ["msb", "lsb"]: 307 | raise ValueError("Invalid bit_order, can be \"msb\" or \"lsb\".") 308 | 309 | # Read-modify-write mode, because the mode contains bits for other settings 310 | 311 | # Get mode 312 | buf = array.array('B', [0]) 313 | try: 314 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 315 | except (OSError, IOError) as e: 316 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 317 | 318 | bit_order = bit_order.lower() 319 | buf[0] = (buf[0] & ~SPI._SPI_LSB_FIRST) | (SPI._SPI_LSB_FIRST if bit_order == "lsb" else 0) 320 | 321 | # Set mode 322 | try: 323 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) 324 | except (OSError, IOError) as e: 325 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 326 | 327 | bit_order = property(_get_bit_order, _set_bit_order) 328 | """Get or set the SPI bit order. Can be "msb" or "lsb". 329 | 330 | Raises: 331 | SPIError: if an I/O or OS error occurs. 332 | TypeError: if `bit_order` type is not str. 333 | ValueError: if `bit_order` value is invalid. 334 | 335 | :type: str 336 | """ 337 | 338 | def _get_bits_per_word(self): 339 | # Get bits per word 340 | buf = array.array('B', [0]) 341 | try: 342 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_BITS_PER_WORD, buf, True) 343 | except (OSError, IOError) as e: 344 | raise SPIError(e.errno, "Getting SPI bits per word: " + e.strerror) 345 | 346 | return buf[0] 347 | 348 | def _set_bits_per_word(self, bits_per_word): 349 | if not isinstance(bits_per_word, int): 350 | raise TypeError("Invalid bits_per_word type, should be integer.") 351 | if bits_per_word < 0 or bits_per_word > 255: 352 | raise ValueError("Invalid bits_per_word, must be 0-255.") 353 | 354 | # Set bits per word 355 | buf = array.array('B', [bits_per_word]) 356 | try: 357 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_BITS_PER_WORD, buf, False) 358 | except (OSError, IOError) as e: 359 | raise SPIError(e.errno, "Setting SPI bits per word: " + e.strerror) 360 | 361 | bits_per_word = property(_get_bits_per_word, _set_bits_per_word) 362 | """Get or set the SPI bits per word. 363 | 364 | Raises: 365 | SPIError: if an I/O or OS error occurs. 366 | TypeError: if `bits_per_word` type is not int. 367 | ValueError: if `bits_per_word` value is invalid. 368 | 369 | :type: int 370 | """ 371 | 372 | def _get_extra_flags(self): 373 | # Get mode 374 | buf = array.array('B', [0]) 375 | try: 376 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 377 | except (OSError, IOError) as e: 378 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 379 | 380 | return buf[0] & ~(SPI._SPI_LSB_FIRST | SPI._SPI_CPHA | SPI._SPI_CPOL) 381 | 382 | def _set_extra_flags(self, extra_flags): 383 | if not isinstance(extra_flags, int): 384 | raise TypeError("Invalid extra_flags type, should be integer.") 385 | if extra_flags < 0 or extra_flags > 255: 386 | raise ValueError("Invalid extra_flags, must be 0-255.") 387 | 388 | # Read-modify-write mode, because the mode contains bits for other settings 389 | 390 | # Get mode 391 | buf = array.array('B', [0]) 392 | try: 393 | fcntl.ioctl(self._fd, SPI._SPI_IOC_RD_MODE, buf, True) 394 | except (OSError, IOError) as e: 395 | raise SPIError(e.errno, "Getting SPI mode: " + e.strerror) 396 | 397 | buf[0] = (buf[0] & (SPI._SPI_LSB_FIRST | SPI._SPI_CPHA | SPI._SPI_CPOL)) | extra_flags 398 | 399 | # Set mode 400 | try: 401 | fcntl.ioctl(self._fd, SPI._SPI_IOC_WR_MODE, buf, False) 402 | except (OSError, IOError) as e: 403 | raise SPIError(e.errno, "Setting SPI mode: " + e.strerror) 404 | 405 | extra_flags = property(_get_extra_flags, _set_extra_flags) 406 | """Get or set the spidev extra flags. Extra flags are bitwise-ORed with the SPI mode. 407 | 408 | Raises: 409 | SPIError: if an I/O or OS error occurs. 410 | TypeError: if `extra_flags` type is not int. 411 | ValueError: if `extra_flags` value is invalid. 412 | 413 | :type: int 414 | """ 415 | 416 | # String representation 417 | 418 | def __str__(self): 419 | return "SPI (device={:s}, fd={:d}, mode={:d}, max_speed={:d}, bit_order={:s}, bits_per_word={:d}, extra_flags=0x{:02x})" \ 420 | .format(self.devpath, self.fd, self.mode, self.max_speed, self.bit_order, self.bits_per_word, self.extra_flags) 421 | -------------------------------------------------------------------------------- /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.0.0', 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 | 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.""", 15 | classifiers=[ 16 | 'Development Status :: 5 - Production/Stable', 17 | 'License :: OSI Approved :: MIT License', 18 | 'Operating System :: POSIX :: Linux', 19 | 'Programming Language :: Python', 20 | 'Programming Language :: Python :: 2', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: Implementation :: CPython', 23 | 'Topic :: Software Development :: Libraries :: Python Modules', 24 | 'Topic :: Software Development :: Embedded Systems', 25 | 'Topic :: System :: Hardware', 26 | 'Topic :: System :: Hardware :: Hardware Drivers', 27 | ], 28 | license='MIT', 29 | keywords='gpio spi led pwm i2c mmio serial uart embedded linux beaglebone raspberrypi rpi odroid', 30 | ) 31 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peng-zhihui/python-periphery/692449436f486cd4d17b53b0eb41ab32acccd5b2/tests/__init__.py -------------------------------------------------------------------------------- /tests/asserts.py: -------------------------------------------------------------------------------- 1 | class AssertRaises(object): 2 | def __init__(self, exception_type): 3 | self.exception_type = exception_type 4 | 5 | def __enter__(self): 6 | return self 7 | 8 | def __exit__(self, t, value, traceback): 9 | if isinstance(value, self.exception_type): 10 | return True 11 | 12 | return False 13 | -------------------------------------------------------------------------------- /tests/test_gpio.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import threading 4 | import time 5 | 6 | import periphery 7 | from .asserts import 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 | print("Starting arguments test...") 23 | 24 | # Invalid open types 25 | with AssertRaises(TypeError): 26 | periphery.GPIO(1, 1, "in") 27 | with AssertRaises(TypeError): 28 | periphery.GPIO("abc", 2.3, "in") 29 | with AssertRaises(TypeError): 30 | periphery.GPIO("abc", 1, 1) 31 | # Invalid direction 32 | with AssertRaises(ValueError): 33 | periphery.GPIO("abc", 1, "blah") 34 | 35 | print("Arguments test passed.") 36 | 37 | 38 | def test_open_close(): 39 | print("Starting open/close test...") 40 | 41 | # Open non-existent GPIO (export should fail with EINVAL) 42 | with AssertRaises(periphery.GPIOError): 43 | periphery.GPIO(path, 9999, "in") 44 | 45 | # Open legitimate GPIO 46 | gpio = periphery.GPIO(path, line_output, "in") 47 | assert gpio.line == line_output 48 | assert gpio.direction == "in" 49 | assert gpio.fd >= 0 50 | assert gpio.chip_fd >= 0 51 | 52 | # Set invalid direction 53 | with AssertRaises(ValueError): 54 | gpio.direction = "blah" 55 | # Set invalid edge 56 | with AssertRaises(ValueError): 57 | gpio.edge = "blah" 58 | 59 | # Set direction out, check direction out, check value low 60 | gpio.direction = "out" 61 | assert gpio.direction == "out" 62 | assert gpio.read() == False 63 | # Set direction low, check direction out, check value low 64 | gpio.direction = "low" 65 | assert gpio.direction == "out" 66 | assert gpio.read() == False 67 | # Set direction high, check direction out, check value high 68 | gpio.direction = "high" 69 | assert gpio.direction == "out" 70 | assert gpio.read() == True 71 | # Attempt to set interrupt edge on output GPIO 72 | with AssertRaises(periphery.GPIOError): 73 | gpio.edge = "rising" 74 | # Attempt to read event on output GPIO 75 | with AssertRaises(periphery.GPIOError): 76 | gpio.read_event() 77 | 78 | # Set direction in, check direction in 79 | gpio.direction = "in" 80 | assert gpio.direction == "in" 81 | 82 | # Set edge none, check edge none 83 | gpio.edge = "none" 84 | assert gpio.edge == "none" 85 | # Set edge rising, check edge rising 86 | gpio.edge = "rising" 87 | assert gpio.edge == "rising" 88 | # Set edge falling, check edge falling 89 | gpio.edge = "falling" 90 | assert gpio.edge == "falling" 91 | # Set edge both, check edge both 92 | gpio.edge = "both" 93 | assert gpio.edge == "both" 94 | # Set edge none, check edge none 95 | gpio.edge = "none" 96 | assert gpio.edge == "none" 97 | 98 | gpio.close() 99 | 100 | print("Open/close test passed.") 101 | 102 | 103 | def test_loopback(): 104 | print("Starting loopback test...") 105 | 106 | # Open in and out lines 107 | gpio_in = periphery.GPIO(path, line_input, "in") 108 | gpio_out = periphery.GPIO(path, line_output, "out") 109 | 110 | # Drive out low, check in low 111 | print("Drive out low, check in low") 112 | gpio_out.write(False) 113 | assert gpio_in.read() == False 114 | 115 | # Drive out high, check in high 116 | print("Drive out high, check in high") 117 | gpio_out.write(True) 118 | assert gpio_in.read() == True 119 | 120 | # Wrapper for running poll() in a thread 121 | def threaded_poll(gpio, timeout): 122 | ret = queue.Queue() 123 | 124 | def f(): 125 | ret.put(gpio.poll(timeout)) 126 | 127 | thread = threading.Thread(target=f) 128 | thread.start() 129 | return ret 130 | 131 | # Check poll falling 1 -> 0 interrupt 132 | print("Check poll falling 1 -> 0 interrupt") 133 | gpio_in.edge = "falling" 134 | poll_ret = threaded_poll(gpio_in, 5) 135 | time.sleep(0.5) 136 | gpio_out.write(False) 137 | assert poll_ret.get() == True 138 | assert gpio_in.read() == False 139 | event = gpio_in.read_event() 140 | assert event.edge == "falling" 141 | assert event.timestamp != 0 142 | 143 | # Check poll rising 0 -> 1 interrupt 144 | print("Check poll rising 0 -> 1 interrupt") 145 | gpio_in.edge = "rising" 146 | poll_ret = threaded_poll(gpio_in, 5) 147 | time.sleep(0.5) 148 | gpio_out.write(True) 149 | assert poll_ret.get() == True 150 | assert gpio_in.read() == True 151 | event = gpio_in.read_event() 152 | assert event.edge == "rising" 153 | assert event.timestamp != 0 154 | 155 | # Set edge to both 156 | gpio_in.edge = "both" 157 | 158 | # Check poll falling 1 -> 0 interrupt 159 | print("Check poll falling 1 -> 0 interrupt") 160 | poll_ret = threaded_poll(gpio_in, 5) 161 | time.sleep(0.5) 162 | gpio_out.write(False) 163 | assert poll_ret.get() == True 164 | assert gpio_in.read() == False 165 | event = gpio_in.read_event() 166 | assert event.edge == "falling" 167 | assert event.timestamp != 0 168 | 169 | # Check poll rising 0 -> 1 interrupt 170 | print("Check poll rising 0 -> 1 interrupt") 171 | poll_ret = threaded_poll(gpio_in, 5) 172 | time.sleep(0.5) 173 | gpio_out.write(True) 174 | assert poll_ret.get() == True 175 | assert gpio_in.read() == True 176 | event = gpio_in.read_event() 177 | assert event.edge == "rising" 178 | assert event.timestamp != 0 179 | 180 | # Check poll timeout 181 | print("Check poll timeout") 182 | assert gpio_in.poll(1) == False 183 | 184 | gpio_in.close() 185 | gpio_out.close() 186 | 187 | print("Loopback test passed.") 188 | 189 | 190 | def test_interactive(): 191 | print("Starting interactive test...") 192 | 193 | gpio = periphery.GPIO(path, line_output, "out") 194 | 195 | print("Starting interactive test. Get out your multimeter, buddy!") 196 | raw_input("Press enter to continue...") 197 | 198 | # Check tostring 199 | print("GPIO description: {}".format(str(gpio))) 200 | assert raw_input("GPIO description looks ok? y/n ") == "y" 201 | 202 | # Drive GPIO out low 203 | gpio.write(False) 204 | assert raw_input("GPIO out is low? y/n ") == "y" 205 | 206 | # Drive GPIO out high 207 | gpio.write(True) 208 | assert raw_input("GPIO out is high? y/n ") == "y" 209 | 210 | # Drive GPIO out low 211 | gpio.write(False) 212 | assert raw_input("GPIO out is low? y/n ") == "y" 213 | 214 | gpio.close() 215 | 216 | print("Interactive test passed.") 217 | 218 | 219 | if __name__ == "__main__": 220 | if os.environ.get("CI") == "true": 221 | test_arguments() 222 | sys.exit(0) 223 | 224 | if len(sys.argv) < 3: 225 | print("Usage: python -m tests.test_gpio ") 226 | print("") 227 | print("[1/4] Argument test: No requirements.") 228 | print("[2/4] Open/close test: GPIO #2 should be real.") 229 | print("[3/4] Loopback test: GPIOs #1 and #2 should be connected with a wire.") 230 | print("[4/4] Interactive test: GPIO #2 should be observed with a multimeter.") 231 | print("") 232 | print("Hint: for Raspberry Pi 3,") 233 | print("Use GPIO 17 (header pin 11) and GPIO 27 (header pin 13),") 234 | print("connect a loopback between them, and run this test with:") 235 | print(" python -m tests.test_gpio /dev/gpiochip0 17 27") 236 | print("") 237 | sys.exit(1) 238 | 239 | path = sys.argv[1] 240 | line_input = int(sys.argv[2]) 241 | line_output = int(sys.argv[3]) 242 | 243 | print("Starting GPIO tests...") 244 | 245 | test_arguments() 246 | test_open_close() 247 | test_loopback() 248 | test_interactive() 249 | 250 | print("All GPIO tests passed.") 251 | -------------------------------------------------------------------------------- /tests/test_gpio_sysfs.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import threading 4 | import time 5 | 6 | import periphery 7 | from .asserts import 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 | line_input = None 16 | line_output = None 17 | 18 | 19 | def test_arguments(): 20 | print("Starting arguments test...") 21 | 22 | # Invalid open types 23 | with AssertRaises(TypeError): 24 | periphery.GPIO("abc", "out") 25 | with AssertRaises(TypeError): 26 | periphery.GPIO(100, 100) 27 | # Invalid direction 28 | with AssertRaises(ValueError): 29 | periphery.GPIO(100, "blah") 30 | 31 | print("Arguments test passed.") 32 | 33 | 34 | def test_open_close(): 35 | print("Starting open/close test...") 36 | 37 | # Open non-existent GPIO (export should fail with EINVAL) 38 | with AssertRaises(periphery.GPIOError): 39 | periphery.GPIO(9999, "in") 40 | 41 | # Open legitimate GPIO 42 | gpio = periphery.GPIO(line_output, "in") 43 | assert gpio.line == line_output 44 | assert gpio.direction == "in" 45 | assert gpio.fd >= 0 46 | 47 | # Set invalid direction 48 | with AssertRaises(ValueError): 49 | gpio.direction = "blah" 50 | # Set invalid edge 51 | with AssertRaises(ValueError): 52 | gpio.edge = "blah" 53 | # Unsupported proprety 54 | with AssertRaises(NotImplementedError): 55 | _ = gpio.chip_fd 56 | # Unsupported method 57 | with AssertRaises(NotImplementedError): 58 | gpio.read_event() 59 | 60 | # Set direction out, check direction out, check value low 61 | gpio.direction = "out" 62 | assert gpio.direction == "out" 63 | assert gpio.read() == False 64 | # Set direction low, check direction out, check value low 65 | gpio.direction = "low" 66 | assert gpio.direction == "out" 67 | assert gpio.read() == False 68 | # Set direction high, check direction out, check value high 69 | gpio.direction = "high" 70 | assert gpio.direction == "out" 71 | assert gpio.read() == True 72 | 73 | # Set direction in, check direction in 74 | gpio.direction = "in" 75 | assert gpio.direction == "in" 76 | 77 | # Set edge none, check edge none 78 | gpio.edge = "none" 79 | assert gpio.edge == "none" 80 | # Set edge rising, check edge rising 81 | gpio.edge = "rising" 82 | assert gpio.edge == "rising" 83 | # Set edge falling, check edge falling 84 | gpio.edge = "falling" 85 | assert gpio.edge == "falling" 86 | # Set edge both, check edge both 87 | gpio.edge = "both" 88 | assert gpio.edge == "both" 89 | # Set edge none, check edge none 90 | gpio.edge = "none" 91 | assert gpio.edge == "none" 92 | 93 | gpio.close() 94 | 95 | print("Open/close test passed.") 96 | 97 | 98 | def test_loopback(): 99 | print("Starting loopback test...") 100 | 101 | # Open in and out lines 102 | gpio_in = periphery.GPIO(line_input, "in") 103 | gpio_out = periphery.GPIO(line_output, "out") 104 | 105 | # Drive out low, check in low 106 | print("Drive out low, check in low") 107 | gpio_out.write(False) 108 | assert gpio_in.read() == False 109 | 110 | # Drive out high, check in high 111 | print("Drive out high, check in high") 112 | gpio_out.write(True) 113 | assert gpio_in.read() == True 114 | 115 | # Wrapper for running poll() in a thread 116 | def threaded_poll(gpio, timeout): 117 | ret = queue.Queue() 118 | 119 | def f(): 120 | ret.put(gpio.poll(timeout)) 121 | 122 | thread = threading.Thread(target=f) 123 | thread.start() 124 | return ret 125 | 126 | # Check poll falling 1 -> 0 interrupt 127 | print("Check poll falling 1 -> 0 interrupt") 128 | gpio_in.edge = "falling" 129 | poll_ret = threaded_poll(gpio_in, 5) 130 | time.sleep(0.5) 131 | gpio_out.write(False) 132 | assert poll_ret.get() == True 133 | assert gpio_in.read() == False 134 | 135 | # Check poll rising 0 -> 1 interrupt 136 | print("Check poll rising 0 -> 1 interrupt") 137 | gpio_in.edge = "rising" 138 | poll_ret = threaded_poll(gpio_in, 5) 139 | time.sleep(0.5) 140 | gpio_out.write(True) 141 | assert poll_ret.get() == True 142 | assert gpio_in.read() == True 143 | 144 | # Set edge to both 145 | gpio_in.edge = "both" 146 | 147 | # Check poll falling 1 -> 0 interrupt 148 | print("Check poll falling 1 -> 0 interrupt") 149 | poll_ret = threaded_poll(gpio_in, 5) 150 | time.sleep(0.5) 151 | gpio_out.write(False) 152 | assert poll_ret.get() == True 153 | assert gpio_in.read() == False 154 | 155 | # Check poll rising 0 -> 1 interrupt 156 | print("Check poll rising 0 -> 1 interrupt") 157 | poll_ret = threaded_poll(gpio_in, 5) 158 | time.sleep(0.5) 159 | gpio_out.write(True) 160 | assert poll_ret.get() == True 161 | assert gpio_in.read() == True 162 | 163 | # Check poll timeout 164 | print("Check poll timeout") 165 | assert gpio_in.poll(1) == False 166 | 167 | gpio_in.close() 168 | gpio_out.close() 169 | 170 | print("Loopback test passed.") 171 | 172 | 173 | def test_interactive(): 174 | print("Starting interactive test...") 175 | 176 | gpio = periphery.GPIO(line_output, "out") 177 | 178 | print("Starting interactive test. Get out your multimeter, buddy!") 179 | raw_input("Press enter to continue...") 180 | 181 | # Check tostring 182 | print("GPIO description: {}".format(str(gpio))) 183 | assert raw_input("GPIO description looks ok? y/n ") == "y" 184 | 185 | # Drive GPIO out low 186 | gpio.write(False) 187 | assert raw_input("GPIO out is low? y/n ") == "y" 188 | 189 | # Drive GPIO out high 190 | gpio.write(True) 191 | assert raw_input("GPIO out is high? y/n ") == "y" 192 | 193 | # Drive GPIO out low 194 | gpio.write(False) 195 | assert raw_input("GPIO out is low? y/n ") == "y" 196 | 197 | gpio.close() 198 | 199 | print("Interactive test passed.") 200 | 201 | 202 | if __name__ == "__main__": 203 | if os.environ.get("CI") == "true": 204 | test_arguments() 205 | sys.exit(0) 206 | 207 | if len(sys.argv) < 3: 208 | print("Usage: python -m tests.test_gpio ") 209 | print("") 210 | print("[1/4] Argument test: No requirements.") 211 | print("[2/4] Open/close test: GPIO #2 should be real.") 212 | print("[3/4] Loopback test: GPIOs #1 and #2 should be connected with a wire.") 213 | print("[4/4] Interactive test: GPIO #2 should be observed with a multimeter.") 214 | print("") 215 | print("Hint: for Raspberry Pi 3,") 216 | print("Use GPIO 17 (header pin 11) and GPIO 27 (header pin 13),") 217 | print("connect a loopback between them, and run this test with:") 218 | print(" python -m tests.test_gpio_sysfs 17 27") 219 | print("") 220 | sys.exit(1) 221 | 222 | line_input = int(sys.argv[1]) 223 | line_output = int(sys.argv[2]) 224 | 225 | print("Starting GPIO tests...") 226 | 227 | test_arguments() 228 | test_open_close() 229 | test_loopback() 230 | test_interactive() 231 | 232 | print("All GPIO tests passed.") 233 | -------------------------------------------------------------------------------- /tests/test_i2c.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import periphery 4 | from .asserts import AssertRaises 5 | 6 | if sys.version_info[0] == 3: 7 | raw_input = input 8 | 9 | i2c_devpath = None 10 | 11 | 12 | def test_arguments(): 13 | print("Starting arguments test...") 14 | 15 | # Open with invalid type 16 | with AssertRaises(TypeError): 17 | periphery.I2C(123) 18 | 19 | print("Arguments test passed.") 20 | 21 | 22 | def test_open_close(): 23 | print("Starting open/close test...") 24 | 25 | # Open non-existent device 26 | with AssertRaises(periphery.I2CError): 27 | periphery.I2C("/foo/bar") 28 | 29 | # Open legitimate device 30 | i2c = periphery.I2C(i2c_devpath) 31 | assert i2c.fd > 0 32 | 33 | # Close I2C 34 | i2c.close() 35 | 36 | print("Open/close test passed.") 37 | 38 | 39 | def test_loopback(): 40 | print("Starting loopback test...") 41 | 42 | print("No general way to do a loopback test for I2C without a real component, skipping...") 43 | 44 | print("Loopback test passed.") 45 | 46 | 47 | def test_interactive(): 48 | print("Starting interactive test...") 49 | 50 | # Open device 2 51 | i2c = periphery.I2C(i2c_devpath) 52 | 53 | print("") 54 | print("Starting interactive test. Get out your logic analyzer, buddy!") 55 | raw_input("Press enter to continue...") 56 | 57 | # Check tostring 58 | print("I2C description: {}".format(str(i2c))) 59 | assert raw_input("I2C description looks ok? y/n ") == "y" 60 | 61 | # There isn't much we can do without assuming a device on the other end, 62 | # because I2C needs an acknowledgement bit on each transferred byte. 63 | # 64 | # But we can send a transaction and expect it to time out. 65 | 66 | # S [ 0x7a W ] [0xaa] [0xbb] [0xcc] [0xdd] NA 67 | messages = [periphery.I2C.Message([0xaa, 0xbb, 0xcc, 0xdd])] 68 | 69 | raw_input("Press enter to start transfer...") 70 | 71 | # Transfer to non-existent device 72 | with AssertRaises(periphery.I2CError): 73 | i2c.transfer(0x7a, messages) 74 | 75 | i2c.close() 76 | 77 | success = raw_input("I2C transfer occurred? y/n ") 78 | assert success == "y" 79 | 80 | print("Interactive test passed.") 81 | 82 | 83 | if __name__ == "__main__": 84 | if os.environ.get("CI") == "true": 85 | test_arguments() 86 | sys.exit(0) 87 | 88 | if len(sys.argv) < 2: 89 | print("Usage: python -m tests.test_i2c ") 90 | print("") 91 | print("[1/4] Arguments test: No requirements.") 92 | print("[2/4] Open/close test: I2C device should be real.") 93 | print("[3/4] Loopback test: No test.") 94 | print("[4/4] Interactive test: I2C bus should be observed with an oscilloscope or logic analyzer.") 95 | print("") 96 | print("Hint: for Raspberry Pi 3, enable I2C1 with:") 97 | print(" $ echo \"dtparam=i2c_arm=on\" | sudo tee -a /boot/config.txt") 98 | print(" $ sudo reboot") 99 | print("Use pins I2C1 SDA (header pin 2) and I2C1 SCL (header pin 3),") 100 | print("and run this test with:") 101 | print(" python -m tests.test_i2c /dev/i2c-1") 102 | print("") 103 | sys.exit(1) 104 | 105 | i2c_devpath = sys.argv[1] 106 | 107 | print("Starting I2C tests...") 108 | 109 | test_arguments() 110 | test_open_close() 111 | test_loopback() 112 | test_interactive() 113 | 114 | print("All I2C tests passed.") 115 | -------------------------------------------------------------------------------- /tests/test_led.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import periphery 5 | from .asserts import AssertRaises 6 | 7 | if sys.version_info[0] == 3: 8 | raw_input = input 9 | 10 | led_name = None 11 | 12 | 13 | def test_arguments(): 14 | print("Starting arguments test...") 15 | 16 | # Invalid open types 17 | with AssertRaises(TypeError): 18 | periphery.LED("abc", "out") 19 | with AssertRaises(TypeError): 20 | periphery.LED(100, 100) 21 | 22 | print("Arguments test passed.") 23 | 24 | 25 | def test_open_close(): 26 | print("Starting open/close test...") 27 | 28 | # Open non-existent LED 29 | with AssertRaises(LookupError): 30 | periphery.LED("invalid_led_XXX", 0) 31 | 32 | # Open legitimate LED 33 | led = periphery.LED(led_name, 0) 34 | assert led.name == led_name 35 | assert led.fd > 0 36 | assert led.max_brightness > 0 37 | 38 | # Set brightness to 1, check brightness 39 | led.write(1) 40 | time.sleep(0.01) 41 | assert led.read() >= 1 42 | 43 | # Set brightness to 0, check brightness 44 | led.write(0) 45 | time.sleep(0.01) 46 | assert led.read() == 0 47 | 48 | # Set brightness to 1, check brightness 49 | led.brightness = 1 50 | time.sleep(0.01) 51 | assert led.brightness >= 1 52 | 53 | # Set brightness to 0, check brightness 54 | led.brightness = 0 55 | time.sleep(0.01) 56 | assert led.brightness == 0 57 | 58 | # Set brightness to True, check brightness 59 | led.write(True) 60 | time.sleep(0.01) 61 | assert led.read() == led.max_brightness 62 | 63 | # Set brightness to False, check brightness 64 | led.write(False) 65 | time.sleep(0.01) 66 | assert led.read() == 0 67 | 68 | led.close() 69 | 70 | print("Open/close test passed.") 71 | 72 | 73 | def test_interactive(): 74 | print("Starting interactive test...") 75 | 76 | led = periphery.LED(led_name, False) 77 | 78 | raw_input("Press enter to continue...") 79 | 80 | # Check tostring 81 | print("LED description: {}".format(str(led))) 82 | assert raw_input("LED description looks ok? y/n ") == "y" 83 | 84 | # Turn LED off 85 | led.write(False) 86 | assert raw_input("LED is off? y/n ") == "y" 87 | 88 | # Turn LED on 89 | led.write(True) 90 | assert raw_input("LED is on? y/n ") == "y" 91 | 92 | # Turn LED off 93 | led.write(False) 94 | assert raw_input("LED is off? y/n ") == "y" 95 | 96 | # Turn LED on 97 | led.write(True) 98 | assert raw_input("LED is on? y/n ") == "y" 99 | 100 | led.close() 101 | 102 | print("Interactive test passed.") 103 | 104 | 105 | if __name__ == "__main__": 106 | if os.environ.get("CI") == "true": 107 | test_arguments() 108 | sys.exit(0) 109 | 110 | if len(sys.argv) < 2: 111 | print("Usage: python -m tests.test_led ") 112 | print("") 113 | print("[1/4] Arguments test: No requirements.") 114 | print("[2/4] Open/close test: LED should be real.") 115 | print("[3/4] Loopback test: No test.") 116 | print("[4/4] Interactive test: LED should be observed.") 117 | print("") 118 | print("Hint: for Raspberry Pi 3, disable triggers for led1:") 119 | print(" $ echo none > /sys/class/leds/led1/trigger") 120 | print("Observe led1 (red power LED), and run this test:") 121 | print(" python -m tests.test_led led1") 122 | print("") 123 | sys.exit(1) 124 | 125 | led_name = sys.argv[1] 126 | 127 | print("Starting LED tests...") 128 | 129 | test_arguments() 130 | test_open_close() 131 | test_interactive() 132 | 133 | print("All LED tests passed.") 134 | -------------------------------------------------------------------------------- /tests/test_mmio.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import periphery 5 | from .asserts import AssertRaises 6 | 7 | if sys.version_info[0] == 3: 8 | raw_input = input 9 | 10 | PAGE_SIZE = 4096 11 | CONTROL_MODULE_BASE = 0x44e10000 12 | USB_VID_PID_OFFSET = 0x7f4 13 | USB_VID_PID = 0x04516141 14 | RTCSS_BASE = 0x44e3e000 15 | RTC_SCRATCH2_REG_OFFSET = 0x68 16 | RTC_KICK0R_REG_OFFSET = 0x6C 17 | RTC_KICK1R_REG_OFFSET = 0x70 18 | 19 | 20 | def test_arguments(): 21 | print("Starting arguments test...") 22 | print("Arguments test passed.") 23 | 24 | 25 | def test_open_close(): 26 | print("Starting open/close test...") 27 | 28 | # Open aligned base 29 | mmio = periphery.MMIO(CONTROL_MODULE_BASE, PAGE_SIZE) 30 | # Check properties 31 | assert mmio.base == CONTROL_MODULE_BASE 32 | assert mmio.size == PAGE_SIZE 33 | # Try to write immutable properties 34 | with AssertRaises(AttributeError): 35 | mmio.base = 1000 36 | with AssertRaises(AttributeError): 37 | mmio.size = 1000 38 | mmio.close() 39 | 40 | # Open unaligned base 41 | mmio = periphery.MMIO(CONTROL_MODULE_BASE + 123, PAGE_SIZE) 42 | # Check properties 43 | assert mmio.base == CONTROL_MODULE_BASE + 123 44 | assert mmio.size == PAGE_SIZE 45 | # Read out of bounds 46 | with AssertRaises(ValueError): 47 | mmio.read32(PAGE_SIZE - 3) 48 | with AssertRaises(ValueError): 49 | mmio.read32(PAGE_SIZE - 2) 50 | with AssertRaises(ValueError): 51 | mmio.read32(PAGE_SIZE - 1) 52 | with AssertRaises(ValueError): 53 | mmio.read32(PAGE_SIZE) 54 | mmio.close() 55 | 56 | print("Open/close test passed.") 57 | 58 | 59 | def test_loopback(): 60 | print("Starting loopback test...") 61 | 62 | # Open control module 63 | mmio = periphery.MMIO(CONTROL_MODULE_BASE, PAGE_SIZE) 64 | 65 | # Read and compare USB VID/PID with read32() 66 | assert mmio.read32(USB_VID_PID_OFFSET) == USB_VID_PID 67 | # Read and compare USB VID/PID with bytes read 68 | data = mmio.read(USB_VID_PID_OFFSET, 4) 69 | data = bytearray(data) 70 | assert data[0] == USB_VID_PID & 0xff 71 | assert data[1] == (USB_VID_PID >> 8) & 0xff 72 | assert data[2] == (USB_VID_PID >> 16) & 0xff 73 | assert data[3] == (USB_VID_PID >> 24) & 0xff 74 | 75 | mmio.close() 76 | 77 | # Open RTC subsystem 78 | mmio = periphery.MMIO(RTCSS_BASE, PAGE_SIZE) 79 | 80 | # Disable write protection 81 | mmio.write32(RTC_KICK0R_REG_OFFSET, 0x83E70B13) 82 | mmio.write32(RTC_KICK1R_REG_OFFSET, 0x95A4F1E0) 83 | 84 | # Write/Read RTC Scratch2 Register 85 | mmio.write32(RTC_SCRATCH2_REG_OFFSET, 0xdeadbeef) 86 | assert mmio.read32(RTC_SCRATCH2_REG_OFFSET) == 0xdeadbeef 87 | 88 | # Write/Read RTC Scratch2 Register with bytes write 89 | mmio.write(RTC_SCRATCH2_REG_OFFSET, b"\xaa\xbb\xcc\xdd") 90 | data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) 91 | assert data == b"\xaa\xbb\xcc\xdd" 92 | 93 | # Write/Read RTC Scratch2 Register with bytearray write 94 | mmio.write(RTC_SCRATCH2_REG_OFFSET, bytearray(b"\xbb\xcc\xdd\xee")) 95 | data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) 96 | assert data == b"\xbb\xcc\xdd\xee" 97 | 98 | # Write/Read RTC Scratch2 Register with list write 99 | mmio.write(RTC_SCRATCH2_REG_OFFSET, [0xcc, 0xdd, 0xee, 0xff]) 100 | data = mmio.read(RTC_SCRATCH2_REG_OFFSET, 4) 101 | assert data == b"\xcc\xdd\xee\xff" 102 | 103 | # Write/Read RTC Scratch2 Register with 16-bit write 104 | mmio.write16(RTC_SCRATCH2_REG_OFFSET, 0xaabb) 105 | assert mmio.read16(RTC_SCRATCH2_REG_OFFSET) == 0xaabb 106 | 107 | # Write/Read RTC Scratch2 Register with 8-bit write 108 | mmio.write16(RTC_SCRATCH2_REG_OFFSET, 0xab) 109 | assert mmio.read8(RTC_SCRATCH2_REG_OFFSET) == 0xab 110 | 111 | mmio.close() 112 | 113 | print("Loopback test passed.") 114 | 115 | 116 | def test_interactive(): 117 | print("Starting interactive test...") 118 | 119 | mmio = periphery.MMIO(RTCSS_BASE, PAGE_SIZE) 120 | 121 | # Check tostring 122 | print("MMIO description: {}".format(str(mmio))) 123 | assert raw_input("MMIO description looks ok? y/n ") == "y" 124 | 125 | print("Waiting for seconds ones digit to reset to 0...\n") 126 | 127 | # Wait until seconds low go to 0, so we don't have to deal with 128 | # overflows in comparing times 129 | tic = time.time() 130 | while mmio.read32(0x00) & 0xf != 0: 131 | periphery.sleep(1) 132 | assert (time.time() - tic) < 12 133 | 134 | # Compare passage of OS time with RTC time 135 | 136 | tic = time.time() 137 | rtc_tic = mmio.read32(0x00) & 0xf 138 | 139 | bcd2dec = lambda x: 10 * ((x >> 4) & 0xf) + (x & 0xf) 140 | 141 | print("Date: {:04d}-{:02d}-{:02d}".format(2000 + bcd2dec(mmio.read32(0x14)), bcd2dec(mmio.read32(0x10)), bcd2dec(mmio.read32(0x0c)))) 142 | print("Time: {:02d}:{:02d}:{:02d}".format(bcd2dec(mmio.read32(0x08) & 0x7f), bcd2dec(mmio.read32(0x04)), bcd2dec(mmio.read32(0x00)))) 143 | 144 | periphery.sleep(3) 145 | 146 | print("Date: {:04d}-{:02d}-{:02d}".format(2000 + bcd2dec(mmio.read32(0x14)), bcd2dec(mmio.read32(0x10)), bcd2dec(mmio.read32(0x0c)))) 147 | print("Time: {:02d}:{:02d}:{:02d}".format(bcd2dec(mmio.read32(0x08) & 0x7f), bcd2dec(mmio.read32(0x04)), bcd2dec(mmio.read32(0x00)))) 148 | 149 | toc = time.time() 150 | rtc_toc = mmio.read32(0x00) & 0xf 151 | 152 | assert (toc - tic) > 2 153 | assert (rtc_toc - rtc_tic) > 2 154 | 155 | mmio.close() 156 | 157 | print("Interactive test passed.") 158 | 159 | 160 | if __name__ == "__main__": 161 | if os.environ.get("CI") == "true": 162 | test_arguments() 163 | sys.exit(0) 164 | 165 | print("WARNING: This test suite assumes a BeagleBone Black (AM335x) host!") 166 | print("Other systems may experience unintended and dire consequences!") 167 | raw_input("Press enter to continue!") 168 | 169 | print("Starting MMIO tests...") 170 | 171 | test_arguments() 172 | test_open_close() 173 | test_loopback() 174 | test_interactive() 175 | 176 | print("All MMIO tests passed.") 177 | -------------------------------------------------------------------------------- /tests/test_pwm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import periphery 4 | from .asserts import AssertRaises 5 | 6 | if sys.version_info[0] == 3: 7 | raw_input = input 8 | 9 | pwm_chip = None 10 | pwm_channel = None 11 | 12 | 13 | def test_arguments(): 14 | print("Starting arguments test...") 15 | 16 | # Invalid open types 17 | with AssertRaises(TypeError): 18 | periphery.PWM("foo", 0) 19 | with AssertRaises(TypeError): 20 | periphery.PWM(0, "foo") 21 | 22 | print("Arguments test passed.") 23 | 24 | 25 | def test_open_close(): 26 | print("Starting open/close test...") 27 | 28 | # Open non-existent PWM chip 29 | with AssertRaises(LookupError): 30 | periphery.PWM(9999, pwm_channel) 31 | 32 | # Open non-existent PWM channel 33 | with AssertRaises(periphery.PWMError): 34 | periphery.PWM(pwm_chip, 9999) 35 | 36 | # Open legitimate PWM chip/channel 37 | pwm = periphery.PWM(pwm_chip, pwm_channel) 38 | assert pwm.chip == pwm_chip 39 | assert pwm.channel == pwm_channel 40 | 41 | # Initialize period and duty cycle 42 | pwm.period = 5e-3 43 | pwm.duty_cycle = 0 44 | 45 | # Set period, check period, check period_ns, check frequency 46 | pwm.period = 1e-3 47 | assert abs(pwm.period - 1e-3) < 1e-4 48 | assert abs(pwm.period_ns - 1000000) < 1e5 49 | assert abs(pwm.frequency - 1000) < 100 50 | pwm.period = 5e-4 51 | assert abs(pwm.period - 5e-4) < 1e-5 52 | assert abs(pwm.period_ns - 500000) < 1e4 53 | assert abs(pwm.frequency - 2000) < 100 54 | 55 | # Set frequency, check frequency, check period, check period_ns 56 | pwm.frequency = 1000 57 | assert abs(pwm.frequency - 1000) < 100 58 | assert abs(pwm.period - 1e-3) < 1e-4 59 | assert abs(pwm.period_ns - 1000000) < 1e5 60 | pwm.frequency = 2000 61 | assert abs(pwm.frequency - 2000) < 100 62 | assert abs(pwm.period - 5e-4) < 1e-5 63 | assert abs(pwm.period_ns - 500000) < 1e4 64 | 65 | # Set period_ns, check period_ns, check period, check frequency 66 | pwm.period_ns = 1000000 67 | assert abs(pwm.period_ns - 1000000) < 1e5 68 | assert abs(pwm.period - 1e-3) < 1e-4 69 | assert abs(pwm.frequency - 1000) < 100 70 | pwm.period_ns = 500000 71 | assert abs(pwm.period_ns - 500000) < 1e4 72 | assert abs(pwm.period - 5e-4) < 1e-5 73 | assert abs(pwm.frequency - 2000) < 100 74 | 75 | pwm.period_ns = 1000000 76 | 77 | # Set duty cycle, check duty cycle, check duty_cycle_ns 78 | pwm.duty_cycle = 0.25 79 | assert abs(pwm.duty_cycle - 0.25) < 1e-3 80 | assert abs(pwm.duty_cycle_ns - 250000) < 1e4 81 | pwm.duty_cycle = 0.50 82 | assert abs(pwm.duty_cycle - 0.50) < 1e-3 83 | assert abs(pwm.duty_cycle_ns - 500000) < 1e4 84 | pwm.duty_cycle = 0.75 85 | assert abs(pwm.duty_cycle - 0.75) < 1e-3 86 | assert abs(pwm.duty_cycle_ns - 750000) < 1e4 87 | 88 | # Set duty_cycle_ns, check duty_cycle_ns, check duty_cycle 89 | pwm.duty_cycle_ns = 250000 90 | assert abs(pwm.duty_cycle_ns - 250000) < 1e4 91 | assert abs(pwm.duty_cycle - 0.25) < 1e-3 92 | pwm.duty_cycle_ns = 500000 93 | assert abs(pwm.duty_cycle_ns - 500000) < 1e4 94 | assert abs(pwm.duty_cycle - 0.50) < 1e-3 95 | pwm.duty_cycle_ns = 750000 96 | assert abs(pwm.duty_cycle_ns - 750000) < 1e4 97 | assert abs(pwm.duty_cycle - 0.75) < 1e-3 98 | 99 | # Set polarity, check polarity 100 | pwm.polarity = "normal" 101 | assert pwm.polarity == "normal" 102 | pwm.polarity = "inversed" 103 | assert pwm.polarity == "inversed" 104 | # Set enabled, check enabled 105 | pwm.enabled = True 106 | assert pwm.enabled == True 107 | pwm.enabled = False 108 | assert pwm.enabled == False 109 | # Use enable()/disable(), check enabled 110 | pwm.enable() 111 | assert pwm.enabled == True 112 | pwm.disable() 113 | assert pwm.enabled == False 114 | 115 | # Set invalid polarity 116 | with AssertRaises(ValueError): 117 | pwm.polarity = "foo" 118 | 119 | pwm.close() 120 | 121 | print("Open/close test passed.") 122 | 123 | 124 | def test_interactive(): 125 | print("Starting interactive test...") 126 | 127 | pwm = periphery.PWM(pwm_chip, pwm_channel) 128 | 129 | print("Starting interactive test. Get out your oscilloscope, buddy!") 130 | raw_input("Press enter to continue...") 131 | 132 | # Set initial parameters and enable PWM 133 | pwm.duty_cycle = 0.0 134 | pwm.frequency = 1e3 135 | pwm.polarity = "normal" 136 | pwm.enabled = True 137 | 138 | # Check tostring 139 | print("PWM description: {}".format(str(pwm))) 140 | assert raw_input("PWM description looks ok? y/n ") == "y" 141 | 142 | # Set 1 kHz frequency, 0.25 duty cycle 143 | pwm.frequency = 1e3 144 | pwm.duty_cycle = 0.25 145 | assert raw_input("Frequency is 1 kHz, duty cycle is 25%? y/n ") == "y" 146 | 147 | # Set 1 kHz frequency, 0.50 duty cycle 148 | pwm.frequency = 1e3 149 | pwm.duty_cycle = 0.50 150 | assert raw_input("Frequency is 1 kHz, duty cycle is 50%? y/n ") == "y" 151 | 152 | # Set 2 kHz frequency, 0.25 duty cycle 153 | pwm.frequency = 2e3 154 | pwm.duty_cycle = 0.25 155 | assert raw_input("Frequency is 2 kHz, duty cycle is 25%? y/n ") == "y" 156 | 157 | # Set 2 kHz frequency, 0.50 duty cycle 158 | pwm.frequency = 2e3 159 | pwm.duty_cycle = 0.50 160 | assert raw_input("Frequency is 2 kHz, duty cycle is 50%? y/n ") == "y" 161 | 162 | pwm.duty_cycle = 0.0 163 | pwm.enabled = False 164 | 165 | pwm.close() 166 | 167 | print("Interactive test passed.") 168 | 169 | 170 | if __name__ == "__main__": 171 | if os.environ.get("CI") == "true": 172 | test_arguments() 173 | sys.exit(0) 174 | 175 | if len(sys.argv) < 3: 176 | print("Usage: python -m tests.test_pwm ") 177 | print("") 178 | print("[1/4] Arguments test: No requirements.") 179 | print("[2/4] Open/close test: PWM channel should be real.") 180 | print("[3/4] Loopback test: No test.") 181 | print("[4/4] Interactive test: PWM channel should be observed with an oscilloscope or logic analyzer.") 182 | print("") 183 | print("Hint: for Raspberry Pi 3, enable PWM0 and PWM1 with:") 184 | print(" $ echo \"dtoverlay=pwm-2chan,pin=18,func=2,pin2=13,func2=4\" | sudo tee -a /boot/config.txt") 185 | print(" $ sudo reboot") 186 | print("Monitor GPIO 18 (header pin 12), and run this test with:") 187 | print(" python -m tests.test_pwm 0 0") 188 | print("or, monitor GPIO 13 (header pin 33), and run this test with:") 189 | print(" python -m tests.test_pwm 0 1") 190 | print("") 191 | 192 | sys.exit(1) 193 | 194 | pwm_chip = int(sys.argv[1]) 195 | pwm_channel = int(sys.argv[2]) 196 | 197 | print("Starting PMW tests...") 198 | 199 | test_arguments() 200 | test_open_close() 201 | test_interactive() 202 | 203 | print("All PWM tests passed.") 204 | -------------------------------------------------------------------------------- /tests/test_serial.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import periphery 5 | from .asserts import AssertRaises 6 | 7 | if sys.version_info[0] == 3: 8 | raw_input = input 9 | 10 | serial_device = None 11 | 12 | 13 | def test_arguments(): 14 | print("Starting arguments test...") 15 | 16 | # Invalid data bits 17 | with AssertRaises(ValueError): 18 | periphery.Serial("/dev/ttyS0", 115200, databits=4) 19 | with AssertRaises(ValueError): 20 | periphery.Serial("/dev/ttyS0", 115200, databits=9) 21 | # Invalid parity 22 | with AssertRaises(ValueError): 23 | periphery.Serial("/dev/ttyS0", 115200, parity="blah") 24 | # Invalid stop bits 25 | with AssertRaises(ValueError): 26 | periphery.Serial("/dev/ttyS0", 115200, stopbits=0) 27 | with AssertRaises(ValueError): 28 | periphery.Serial("/dev/ttyS0", 115200, stopbits=3) 29 | 30 | # Everything else is fair game, although termios might not like it. 31 | 32 | print("Arguments test passed.") 33 | 34 | 35 | def test_open_close(): 36 | print("Starting open/close test...") 37 | 38 | serial = periphery.Serial(serial_device, 115200) 39 | 40 | # Confirm default settings 41 | assert serial.fd > 0 42 | assert serial.baudrate == 115200 43 | assert serial.databits == 8 44 | assert serial.parity == "none" 45 | assert serial.stopbits == 1 46 | assert serial.xonxoff == False 47 | assert serial.rtscts == False 48 | 49 | # Change some stuff and check that it changed 50 | serial.baudrate = 4800 51 | assert serial.baudrate == 4800 52 | serial.baudrate = 9600 53 | assert serial.baudrate == 9600 54 | serial.databits = 7 55 | assert serial.databits == 7 56 | serial.parity = "odd" 57 | assert serial.parity == "odd" 58 | serial.stopbits = 2 59 | assert serial.stopbits == 2 60 | serial.xonxoff = True 61 | assert serial.xonxoff == True 62 | # Test serial port may not support rtscts 63 | 64 | serial.close() 65 | 66 | print("Open/close test passed.") 67 | 68 | 69 | def test_loopback(): 70 | print("Starting loopback test...") 71 | 72 | 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." 73 | 74 | serial = periphery.Serial(serial_device, 115200) 75 | 76 | # Test write/flush/read with bytes write 77 | print("Write, flush, read lorem ipsum with bytes type") 78 | assert serial.write(lorem_ipsum) == len(lorem_ipsum) 79 | serial.flush() 80 | buf = serial.read(len(lorem_ipsum), timeout=3) 81 | assert buf == lorem_ipsum 82 | 83 | # Test write/flush/read with bytearray write 84 | print("Write, flush, read lorem ipsum with bytearray type") 85 | assert serial.write(bytearray(lorem_ipsum)) == len(lorem_ipsum) 86 | serial.flush() 87 | buf = serial.read(len(lorem_ipsum), timeout=3) 88 | assert buf == lorem_ipsum 89 | 90 | # Test write/flush/read with list write 91 | print("Write, flush, read lorem ipsum with list type") 92 | assert serial.write(list(bytearray(lorem_ipsum))) == len(lorem_ipsum) 93 | serial.flush() 94 | buf = serial.read(len(lorem_ipsum), timeout=3) 95 | assert buf == lorem_ipsum 96 | 97 | # Test poll/write/flush/poll/input waiting/read 98 | print("Write, flush, poll, input waiting, read lorem ipsum") 99 | assert serial.poll(0.5) == False 100 | assert serial.write(lorem_ipsum) == len(lorem_ipsum) 101 | serial.flush() 102 | assert serial.poll(0.5) == True 103 | periphery.sleep_ms(500) 104 | assert serial.input_waiting() == len(lorem_ipsum) 105 | buf = serial.read(len(lorem_ipsum)) 106 | assert buf == lorem_ipsum 107 | 108 | # Test non-blocking poll 109 | print("Check non-blocking poll") 110 | assert serial.poll(0) == False 111 | 112 | # Test a very large read-write (likely to exceed internal buffer size (~4096)) 113 | print("Write, flush, read large buffer") 114 | lorem_hugesum = b"\xaa" * (4096 * 3) 115 | assert serial.write(lorem_hugesum) == len(lorem_hugesum) 116 | serial.flush() 117 | buf = serial.read(len(lorem_hugesum), timeout=3) 118 | assert buf == lorem_hugesum 119 | 120 | # Test read timeout 121 | print("Check read timeout") 122 | tic = time.time() 123 | assert serial.read(4096 * 3, timeout=2) == b"" 124 | toc = time.time() 125 | assert (toc - tic) > 1 126 | 127 | # Test non-blocking read 128 | print("Check non-blocking read") 129 | tic = time.time() 130 | assert serial.read(4096 * 3, timeout=0) == b"" 131 | toc = time.time() 132 | # Assuming we weren't context switched out for a second 133 | assert int(toc - tic) == 0 134 | 135 | serial.close() 136 | 137 | print("Loopback test passed.") 138 | 139 | 140 | def test_interactive(): 141 | print("Starting interactive test...") 142 | 143 | buf = b"Hello World!" 144 | 145 | serial = periphery.Serial(serial_device, 4800) 146 | 147 | print("Starting interactive test. Get out your logic analyzer, buddy!") 148 | raw_input("Press enter to continue...") 149 | 150 | # Check tostring 151 | print("Serial description: {}".format(str(serial))) 152 | assert raw_input("Serial description looks ok? y/n ") == "y" 153 | 154 | serial.baudrate = 4800 155 | raw_input("Press enter to start transfer...") 156 | assert serial.write(buf) == len(buf) 157 | assert raw_input("Serial transfer baudrate 4800, 8n1 occurred? y/n ") == "y" 158 | 159 | serial.baudrate = 9600 160 | raw_input("Press enter to start transfer...") 161 | assert serial.write(buf) == len(buf) 162 | assert raw_input("Serial transfer baudrate 9600, 8n1 occurred? y/n ") == "y" 163 | 164 | serial.baudrate = 115200 165 | raw_input("Press enter to start transfer...") 166 | assert serial.write(buf) == len(buf) 167 | assert raw_input("Serial transfer baudrate 115200, 8n1 occurred? y/n ") == "y" 168 | 169 | serial.close() 170 | 171 | print("Interactive test passed.") 172 | 173 | 174 | if __name__ == "__main__": 175 | if os.environ.get("CI") == "true": 176 | test_arguments() 177 | sys.exit(0) 178 | 179 | if len(sys.argv) < 2: 180 | print("Usage: python -m tests.test_serial ") 181 | print("") 182 | print("[1/4] Arguments test: No requirements.") 183 | print("[2/4] Open/close test: Serial port device should be real.") 184 | print("[3/4] Loopback test: Serial TX and RX should be connected with a wire.") 185 | print("[4/4] Interactive test: Serial TX should be observed with an oscilloscope or logic analyzer.") 186 | print("") 187 | print("Hint: for Raspberry Pi 3, enable UART0 with:") 188 | print(" $ echo \"dtoverlay=pi3-disable-bt\" | sudo tee -a /boot/config.txt") 189 | print(" $ sudo systemctl disable hciuart") 190 | print(" $ sudo reboot") 191 | print(" (Note that this will disable Bluetooth)") 192 | print("Use pins UART0 TXD (header pin 8) and UART0 RXD (header pin 10),") 193 | print("connect a loopback between TXD and RXD, and run this test with:") 194 | print(" python -m tests.test_serial /dev/ttyAMA0") 195 | print("") 196 | sys.exit(1) 197 | 198 | serial_device = sys.argv[1] 199 | 200 | print("Starting Serial tests...") 201 | 202 | test_arguments() 203 | test_open_close() 204 | test_loopback() 205 | test_interactive() 206 | 207 | print("All Serial tests passed.") 208 | -------------------------------------------------------------------------------- /tests/test_spi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import periphery 4 | from .asserts import AssertRaises 5 | 6 | if sys.version_info[0] == 3: 7 | raw_input = input 8 | 9 | spi_device = None 10 | 11 | 12 | def test_arguments(): 13 | print("Starting arguments test...") 14 | 15 | # Invalid mode 16 | with AssertRaises(ValueError): 17 | periphery.SPI("/dev/spidev0.0", 4, int(1e6)) 18 | # Invalid bit order 19 | with AssertRaises(ValueError): 20 | periphery.SPI("/dev/spidev0.0", 4, int(1e6), bit_order="blah") 21 | 22 | print("Arguments test passed.") 23 | 24 | 25 | def test_open_close(): 26 | print("Starting open/close test...") 27 | 28 | # Normal open (mode=1, max_speed = 100000) 29 | spi = periphery.SPI(spi_device, 1, 100000) 30 | 31 | # Confirm fd and defaults 32 | assert spi.fd > 0 33 | assert spi.mode == 1 34 | assert spi.max_speed == 100000 35 | assert spi.bit_order == "msb" 36 | assert spi.bits_per_word == 8 37 | assert spi.extra_flags == 0 38 | 39 | # Not going to try different bit order or bits per word, because not 40 | # all SPI controllers support them 41 | 42 | # Try modes 0, 1, 2, 3 43 | spi.mode = 0 44 | assert spi.mode == 0 45 | spi.mode = 1 46 | assert spi.mode == 1 47 | spi.mode = 2 48 | assert spi.mode == 2 49 | spi.mode = 3 50 | assert spi.mode == 3 51 | 52 | # Try max speeds 100Khz, 500KHz, 1MHz, 2MHz 53 | spi.max_speed = 100000 54 | assert spi.max_speed == 100000 55 | spi.max_speed = 500000 56 | assert spi.max_speed == 500000 57 | spi.max_speed = 1000000 58 | assert spi.max_speed == 1000000 59 | spi.max_speed = 2e6 60 | assert spi.max_speed == 2000000 61 | 62 | spi.close() 63 | 64 | print("Open/close test passed.") 65 | 66 | 67 | def test_loopback(): 68 | print("Starting loopback test...") 69 | 70 | spi = periphery.SPI(spi_device, 0, 100000) 71 | 72 | # Try list transfer 73 | buf_in = list(range(256)) * 4 74 | buf_out = spi.transfer(buf_in) 75 | assert buf_out == buf_in 76 | 77 | # Try bytearray transfer 78 | buf_in = bytearray(buf_in) 79 | buf_out = spi.transfer(buf_in) 80 | assert buf_out == buf_in 81 | 82 | # Try bytes transfer 83 | buf_in = bytes(bytearray(buf_in)) 84 | buf_out = spi.transfer(buf_in) 85 | assert buf_out == buf_in 86 | 87 | spi.close() 88 | 89 | print("Loopback test passed.") 90 | 91 | 92 | def test_interactive(): 93 | print("Starting interactive test...") 94 | 95 | spi = periphery.SPI(spi_device, 0, 100000) 96 | 97 | print("Starting interactive test. Get out your logic analyzer, buddy!") 98 | raw_input("Press enter to continue...") 99 | 100 | # Check tostring 101 | print("SPI description: {}".format(str(spi))) 102 | assert raw_input("SPI description looks ok? y/n ") == "y" 103 | 104 | # Mode 0 transfer 105 | raw_input("Press enter to start transfer...") 106 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 107 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 108 | assert raw_input("SPI transfer speed <= 100KHz, mode 0 occurred? y/n ") == "y" 109 | 110 | # Mode 1 transfer 111 | spi.mode = 1 112 | raw_input("Press enter to start transfer...") 113 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 114 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 115 | assert raw_input("SPI transfer speed <= 100KHz, mode 1 occurred? y/n ") == "y" 116 | 117 | # Mode 2 transfer 118 | spi.mode = 2 119 | raw_input("Press enter to start transfer...") 120 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 121 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 122 | assert raw_input("SPI transfer speed <= 100KHz, mode 2 occurred? y/n ") == "y" 123 | 124 | # Mode 3 transfer 125 | spi.mode = 3 126 | raw_input("Press enter to start transfer...") 127 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 128 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 129 | assert raw_input("SPI transfer speed <= 100KHz, mode 3 occurred? y/n ") == "y" 130 | 131 | spi.mode = 0 132 | 133 | # 500KHz transfer 134 | spi.max_speed = 500000 135 | raw_input("Press enter to start transfer...") 136 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 137 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 138 | assert raw_input("SPI transfer speed <= 500KHz, mode 0 occurred? y/n ") == "y" 139 | 140 | # 1MHz transfer 141 | spi.max_speed = 1000000 142 | raw_input("Press enter to start transfer...") 143 | spi.transfer([0x55, 0xaa, 0x0f, 0xf0]) 144 | print("SPI data 0x55, 0xaa, 0x0f, 0xf0") 145 | assert raw_input("SPI transfer speed <= 1MHz, mode 0 occurred? y/n ") == "y" 146 | 147 | spi.close() 148 | 149 | print("Interactive test passed.") 150 | 151 | 152 | if __name__ == "__main__": 153 | if os.environ.get("CI") == "true": 154 | test_arguments() 155 | sys.exit(0) 156 | 157 | if len(sys.argv) < 2: 158 | print("Usage: python -m tests.test_spi ") 159 | print("") 160 | print("[1/4] Arguments test: No requirements.") 161 | print("[2/4] Open/close test: SPI device should be real.") 162 | print("[3/4] Loopback test: SPI MISO and MOSI should be connected with a wire.") 163 | print("[4/4] Interactive test: SPI MOSI, CLK, CS should be observed with an oscilloscope or logic analyzer.") 164 | print("") 165 | print("Hint: for Raspberry Pi 3, enable SPI0 with:") 166 | print(" $ echo \"dtparam=spi=on\" | sudo tee -a /boot/config.txt") 167 | print(" $ sudo reboot") 168 | print("Use pins SPI0 MOSI (header pin 19), SPI0 MISO (header pin 21), SPI0 SCLK (header pin 23),") 169 | print("connect a loopback between MOSI and MISO, and run this test with:") 170 | print(" python -m tests.test_spi /dev/spidev0.0") 171 | print("") 172 | sys.exit(1) 173 | 174 | spi_device = sys.argv[1] 175 | 176 | print("Starting SPI tests...") 177 | 178 | test_arguments() 179 | test_open_close() 180 | test_loopback() 181 | test_interactive() 182 | 183 | print("All SPI tests passed.") 184 | --------------------------------------------------------------------------------